diff --git a/src/backend/index.js b/src/backend/index.js index 8d0161d..30e8e6d 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -1,7 +1,7 @@ import { Elysia, t } from 'elysia'; import { cors } from '@elysiajs/cors'; import { jwt } from '@elysiajs/jwt'; -import { JWT_SECRET, logger, LOG_LEVEL } from './config.js'; +import { JWT_SECRET, logger, LOG_LEVEL, logToFrontend } from './config.js'; import { registerUser, authenticateUser, @@ -79,6 +79,44 @@ const app = new Elysia() } }) + // Request logging middleware + .onRequest(({ request, params }) => { + const url = new URL(request.url); + const method = request.method; + const path = url.pathname; + const query = url.search; + + // Skip logging for health checks in development to reduce noise + if (path === '/api/health' && process.env.NODE_ENV === 'development') { + return; + } + + logger.info('Incoming request', { + method, + path, + query: query || undefined, + ip: request.headers.get('x-forwarded-for') || 'unknown', + userAgent: request.headers.get('user-agent')?.substring(0, 100), + }); + }) + .onAfterHandle(({ request, set }) => { + const url = new URL(request.url); + const path = url.pathname; + + // Skip logging for health checks in development + if (path === '/api/health' && process.env.NODE_ENV === 'development') { + return; + } + + // Log error responses + if (set.status >= 400) { + logger.warn('Request failed', { + path, + status: set.status, + }); + } + }) + /** * POST /api/auth/register * Register a new user @@ -706,6 +744,47 @@ const app = new Elysia() timestamp: new Date().toISOString(), })) + /** + * POST /api/logs + * Receive frontend logs and write them to frontend.log file + */ + .post( + '/api/logs', + async ({ body, user, headers }) => { + // Extract context from headers (Elysia provides headers as a plain object) + const userAgent = headers['user-agent'] || 'unknown'; + const context = { + userId: user?.id, + userAgent, + timestamp: new Date().toISOString(), + }; + + // Log each entry + const entries = Array.isArray(body) ? body : [body]; + for (const entry of entries) { + logToFrontend(entry.level || 'info', entry.message || 'No message', entry.data || null, context); + } + + return { success: true }; + }, + { + body: t.Union([ + t.Object({ + level: t.Optional(t.Union([t.Literal('debug'), t.Literal('info'), t.Literal('warn'), t.Literal('error')])), + message: t.String(), + data: t.Optional(t.Any()), + }), + t.Array( + t.Object({ + level: t.Optional(t.Union([t.Literal('debug'), t.Literal('info'), t.Literal('warn'), t.Literal('error')])), + message: t.String(), + data: t.Optional(t.Any()), + }) + ), + ]), + } + ) + // Serve static files and SPA fallback for all non-API routes .get('/*', ({ request }) => { const url = new URL(request.url); diff --git a/src/frontend/vite.config.js b/src/frontend/vite.config.js index 25fecde..bc72cc3 100644 --- a/src/frontend/vite.config.js +++ b/src/frontend/vite.config.js @@ -5,29 +5,42 @@ import { defineConfig } from 'vite'; function suppressURIErrorPlugin() { return { name: 'suppress-uri-error', + enforce: 'pre', // Run this plugin before others configureServer(server) { - server.middlewares.use((req, res, next) => { - // Intercept malformed requests before they reach Vite's middleware - try { - // Try to decode the URL to catch malformed URIs early - if (req.url) { - decodeURI(req.url); + // Return a function that will be called after all plugins are configured + // This ensures our middleware is added at the correct time + return () => { + // Add middleware BEFORE all other middlewares + // We insert it at position 0 to ensure it runs first + server.middlewares.stack.unshift({ + route: '', + handle: (req, res, next) => { + // Intercept malformed requests before they reach SvelteKit + try { + // Try to decode the URL to catch malformed URIs early + if (req.url) { + decodeURI(req.url); + // Also try the full URL construction that SvelteKit does + const base = `${server.config.server.https ? 'https' : 'http'}://${ + req.headers[':authority'] || req.headers.host || 'localhost' + }`; + decodeURI(new URL(base + req.url).pathname); + } + } catch (e) { + // Silently ignore malformed URIs from browser extensions + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('OK'); + return; } - } catch (e) { - // Silently ignore malformed URIs from browser extensions - // Don't call next(), just end the response - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('OK'); - return; - } - next(); - }); + next(); + }}); + }; } }; } export default defineConfig({ - plugins: [sveltekit(), suppressURIErrorPlugin()], + plugins: [suppressURIErrorPlugin(), sveltekit()], server: { host: 'localhost', port: 5173,