feat: Implement structured logging with Pino across server routes
- Add Pino logger with configurable log levels - Create centralized logging utility with context-aware logging - Replace console.log/debug/error with structured logging - Enhance log messages with additional context and metadata - Configure log levels dynamically based on environment
- commit
- 2960188b03ac2e37d64fa384070a6d5969e95f57
- author
- Kit · · 2025-02-15 09:05 UTC
- parent
- afa6c9d
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
19 files · +507 -146
M package.json +2 -0
| @@ -54,6 +54,8 @@ | |||
| 54 | 54 | "@libsql/client": "^0.14.0", | |
| 55 | 55 | "dotenv": "^16.4.7", | |
| 56 | 56 | "drizzle-orm": "^0.39.3", | |
| 57 | + | "pino": "^9.6.0", | |
| 58 | + | "pino-pretty": "^13.0.0", | |
| 57 | 59 | "uuid": "^11.0.5", | |
| 58 | 60 | "xp.css": "^0.2.6" | |
| 59 | 61 | } | |
| @@ -54,6 +54,8 @@ | |||||
| 54 | "@libsql/client": "^0.14.0", | 54 | "@libsql/client": "^0.14.0", | ||
| 55 | "dotenv": "^16.4.7", | 55 | "dotenv": "^16.4.7", | ||
| 56 | "drizzle-orm": "^0.39.3", | 56 | "drizzle-orm": "^0.39.3", | ||
| 57 | + | "pino": "^9.6.0", | |||
| 58 | + | "pino-pretty": "^13.0.0", | |||
| 57 | "uuid": "^11.0.5", | 59 | "uuid": "^11.0.5", | ||
| 58 | "xp.css": "^0.2.6" | 60 | "xp.css": "^0.2.6" | ||
| 59 | } | 61 | } | ||
M pnpm-lock.yaml +184 -0 generated file: collapsed
M src/hooks.server.ts +16 -13
| @@ -6,33 +6,36 @@ import db from "$lib/db/db.server"; | |||
| 6 | 6 | import { handleRateLimit } from "$lib/api/rate-limiter"; | |
| 7 | 7 | import { users } from "$lib/db/schema"; | |
| 8 | 8 | import { initializeMessageCache } from "$lib/cache/initialize-cache"; | |
| 9 | + | import { createLogger } from "$lib/utils/logger.server"; | |
| 10 | + | ||
| 11 | + | const log = createLogger('hooks-server'); | |
| 9 | 12 | ||
| 10 | 13 | async function setAllUsersOffline() { | |
| 11 | 14 | try { | |
| 12 | - | console.log("Setting all users to offline status..."); | |
| 15 | + | log.info("Setting all users to offline status..."); | |
| 13 | 16 | await db.update(users) | |
| 14 | 17 | .set({ | |
| 15 | 18 | status: "offline" | |
| 16 | 19 | }); | |
| 17 | - | console.log("Successfully set all users to offline status"); | |
| 20 | + | log.info("Successfully set all users to offline status"); | |
| 18 | 21 | } catch (error) { | |
| 19 | - | console.error("Failed to set users offline:", error); | |
| 22 | + | log.error("Failed to set users offline", { error }); | |
| 20 | 23 | } | |
| 21 | 24 | } | |
| 22 | 25 | ||
| 23 | 26 | // Initialize database when the server starts | |
| 24 | - | console.debug("Initializing database on server start..."); | |
| 27 | + | log.debug("Initializing database on server start..."); | |
| 25 | 28 | initializeDatabase() | |
| 26 | 29 | .then(async () => { | |
| 27 | 30 | await setAllUsersOffline(); | |
| 28 | 31 | await initializeMessageCache(); | |
| 29 | 32 | }) | |
| 30 | 33 | .catch(error => { | |
| 31 | - | console.error('Failed to initialize database:', error); | |
| 34 | + | log.error('Failed to initialize database', { error }); | |
| 32 | 35 | }); | |
| 33 | 36 | ||
| 34 | 37 | export const handle: Handle = async ({ event, resolve }) => { | |
| 35 | - | console.debug("Handling request:", event.url.pathname); | |
| 38 | + | log.debug("Handling request", { path: event.url.pathname }); | |
| 36 | 39 | ||
| 37 | 40 | // Identify public chat and room requests | |
| 38 | 41 | const isPublicChatRequest = | |
| @@ -69,7 +72,7 @@ export const handle: Handle = async ({ event, resolve }) => { | |||
| 69 | 72 | ||
| 70 | 73 | // Handle public chat/room requests | |
| 71 | 74 | if (isPublicChatRequest || isPublicRoomRequest) { | |
| 72 | - | console.debug("Public request accessed:", event.url.pathname); | |
| 75 | + | log.debug("Public request accessed", { path: event.url.pathname }); | |
| 73 | 76 | event.locals.user = null; | |
| 74 | 77 | event.locals.session = null; // Public access doesn't require a session | |
| 75 | 78 | const response = await resolve(event); | |
| @@ -78,11 +81,11 @@ export const handle: Handle = async ({ event, resolve }) => { | |||
| 78 | 81 | ||
| 79 | 82 | // Handle public routes: if token exists, validate and propagate | |
| 80 | 83 | if (publicRoutes.includes(event.url.pathname)) { | |
| 81 | - | console.debug("Public route accessed:", event.url.pathname); | |
| 84 | + | log.debug("Public route accessed", { path: event.url.pathname }); | |
| 82 | 85 | if (token) { | |
| 83 | 86 | const { session, user } = await validateSessionToken(token); | |
| 84 | 87 | if (session !== null) { | |
| 85 | - | console.debug("Valid session found on public route for user:", user?.nickname); | |
| 88 | + | log.debug("Valid session found on public route", { user: user?.nickname }); | |
| 86 | 89 | setSessionTokenCookie(event, token, session.expiresAt); | |
| 87 | 90 | event.locals.session = session; | |
| 88 | 91 | event.locals.user = user; | |
| @@ -94,7 +97,7 @@ export const handle: Handle = async ({ event, resolve }) => { | |||
| 94 | 97 | ||
| 95 | 98 | // Protected routes: ensure authentication | |
| 96 | 99 | if (token === null) { | |
| 97 | - | console.debug("No session token found"); | |
| 100 | + | log.debug("No session token found"); | |
| 98 | 101 | event.locals.user = null; | |
| 99 | 102 | event.locals.session = null; | |
| 100 | 103 | const response = new Response("Unauthorized", { status: 401 }); | |
| @@ -103,12 +106,12 @@ export const handle: Handle = async ({ event, resolve }) => { | |||
| 103 | 106 | ||
| 104 | 107 | const { session, user } = await validateSessionToken(token); | |
| 105 | 108 | if (session !== null) { | |
| 106 | - | console.debug("Valid session found for user:", user?.nickname); | |
| 109 | + | log.debug("Valid session found", { user: user?.nickname }); | |
| 107 | 110 | setSessionTokenCookie(event, token, session.expiresAt); | |
| 108 | 111 | event.locals.session = session; | |
| 109 | 112 | event.locals.user = user; | |
| 110 | 113 | } else { | |
| 111 | - | console.debug("Invalid session token"); | |
| 114 | + | log.debug("Invalid session token"); | |
| 112 | 115 | deleteSessionTokenCookie(event); | |
| 113 | 116 | event.locals.session = null; | |
| 114 | 117 | event.locals.user = null; | |
| @@ -118,7 +121,7 @@ export const handle: Handle = async ({ event, resolve }) => { | |||
| 118 | 121 | ||
| 119 | 122 | // After authentication, ensure SSE endpoint does not trigger rate limiting | |
| 120 | 123 | if (event.url.pathname.startsWith('/api/sse') && !event.locals.user) { | |
| 121 | - | console.debug("SSE endpoint accessed without valid session, returning 401 without rate limiting"); | |
| 124 | + | log.debug("SSE endpoint accessed without valid session, returning 401 without rate limiting"); | |
| 122 | 125 | return addDebugHeaders(new Response("Unauthorized", { status: 401 })); | |
| 123 | 126 | } | |
| 124 | 127 | ||
| @@ -6,33 +6,36 @@ import db from "$lib/db/db.server"; | |||||
| 6 | import { handleRateLimit } from "$lib/api/rate-limiter"; | 6 | import { handleRateLimit } from "$lib/api/rate-limiter"; | ||
| 7 | import { users } from "$lib/db/schema"; | 7 | import { users } from "$lib/db/schema"; | ||
| 8 | import { initializeMessageCache } from "$lib/cache/initialize-cache"; | 8 | import { initializeMessageCache } from "$lib/cache/initialize-cache"; | ||
| 9 | + | import { createLogger } from "$lib/utils/logger.server"; | |||
| 10 | + | ||||
| 11 | + | const log = createLogger('hooks-server'); | |||
| 9 | 12 | ||||
| 10 | async function setAllUsersOffline() { | 13 | async function setAllUsersOffline() { | ||
| 11 | try { | 14 | try { | ||
| 12 | - | console.log("Setting all users to offline status..."); | 15 | + | log.info("Setting all users to offline status..."); |
| 13 | await db.update(users) | 16 | await db.update(users) | ||
| 14 | .set({ | 17 | .set({ | ||
| 15 | status: "offline" | 18 | status: "offline" | ||
| 16 | }); | 19 | }); | ||
| 17 | - | console.log("Successfully set all users to offline status"); | 20 | + | log.info("Successfully set all users to offline status"); |
| 18 | } catch (error) { | 21 | } catch (error) { | ||
| 19 | - | console.error("Failed to set users offline:", error); | 22 | + | log.error("Failed to set users offline", { error }); |
| 20 | } | 23 | } | ||
| 21 | } | 24 | } | ||
| 22 | 25 | ||||
| 23 | // Initialize database when the server starts | 26 | // Initialize database when the server starts | ||
| 24 | - | console.debug("Initializing database on server start..."); | 27 | + | log.debug("Initializing database on server start..."); |
| 25 | initializeDatabase() | 28 | initializeDatabase() | ||
| 26 | .then(async () => { | 29 | .then(async () => { | ||
| 27 | await setAllUsersOffline(); | 30 | await setAllUsersOffline(); | ||
| 28 | await initializeMessageCache(); | 31 | await initializeMessageCache(); | ||
| 29 | }) | 32 | }) | ||
| 30 | .catch(error => { | 33 | .catch(error => { | ||
| 31 | - | console.error('Failed to initialize database:', error); | 34 | + | log.error('Failed to initialize database', { error }); |
| 32 | }); | 35 | }); | ||
| 33 | 36 | ||||
| 34 | export const handle: Handle = async ({ event, resolve }) => { | 37 | export const handle: Handle = async ({ event, resolve }) => { | ||
| 35 | - | console.debug("Handling request:", event.url.pathname); | 38 | + | log.debug("Handling request", { path: event.url.pathname }); |
| 36 | 39 | ||||
| 37 | // Identify public chat and room requests | 40 | // Identify public chat and room requests | ||
| 38 | const isPublicChatRequest = | 41 | const isPublicChatRequest = | ||
| @@ -69,7 +72,7 @@ export const handle: Handle = async ({ event, resolve }) => { | |||||
| 69 | 72 | ||||
| 70 | // Handle public chat/room requests | 73 | // Handle public chat/room requests | ||
| 71 | if (isPublicChatRequest || isPublicRoomRequest) { | 74 | if (isPublicChatRequest || isPublicRoomRequest) { | ||
| 72 | - | console.debug("Public request accessed:", event.url.pathname); | 75 | + | log.debug("Public request accessed", { path: event.url.pathname }); |
| 73 | event.locals.user = null; | 76 | event.locals.user = null; | ||
| 74 | event.locals.session = null; // Public access doesn't require a session | 77 | event.locals.session = null; // Public access doesn't require a session | ||
| 75 | const response = await resolve(event); | 78 | const response = await resolve(event); | ||
| @@ -78,11 +81,11 @@ export const handle: Handle = async ({ event, resolve }) => { | |||||
| 78 | 81 | ||||
| 79 | // Handle public routes: if token exists, validate and propagate | 82 | // Handle public routes: if token exists, validate and propagate | ||
| 80 | if (publicRoutes.includes(event.url.pathname)) { | 83 | if (publicRoutes.includes(event.url.pathname)) { | ||
| 81 | - | console.debug("Public route accessed:", event.url.pathname); | 84 | + | log.debug("Public route accessed", { path: event.url.pathname }); |
| 82 | if (token) { | 85 | if (token) { | ||
| 83 | const { session, user } = await validateSessionToken(token); | 86 | const { session, user } = await validateSessionToken(token); | ||
| 84 | if (session !== null) { | 87 | if (session !== null) { | ||
| 85 | - | console.debug("Valid session found on public route for user:", user?.nickname); | 88 | + | log.debug("Valid session found on public route", { user: user?.nickname }); |
| 86 | setSessionTokenCookie(event, token, session.expiresAt); | 89 | setSessionTokenCookie(event, token, session.expiresAt); | ||
| 87 | event.locals.session = session; | 90 | event.locals.session = session; | ||
| 88 | event.locals.user = user; | 91 | event.locals.user = user; | ||
| @@ -94,7 +97,7 @@ export const handle: Handle = async ({ event, resolve }) => { | |||||
| 94 | 97 | ||||
| 95 | // Protected routes: ensure authentication | 98 | // Protected routes: ensure authentication | ||
| 96 | if (token === null) { | 99 | if (token === null) { | ||
| 97 | - | console.debug("No session token found"); | 100 | + | log.debug("No session token found"); |
| 98 | event.locals.user = null; | 101 | event.locals.user = null; | ||
| 99 | event.locals.session = null; | 102 | event.locals.session = null; | ||
| 100 | const response = new Response("Unauthorized", { status: 401 }); | 103 | const response = new Response("Unauthorized", { status: 401 }); | ||
| @@ -103,12 +106,12 @@ export const handle: Handle = async ({ event, resolve }) => { | |||||
| 103 | 106 | ||||
| 104 | const { session, user } = await validateSessionToken(token); | 107 | const { session, user } = await validateSessionToken(token); | ||
| 105 | if (session !== null) { | 108 | if (session !== null) { | ||
| 106 | - | console.debug("Valid session found for user:", user?.nickname); | 109 | + | log.debug("Valid session found", { user: user?.nickname }); |
| 107 | setSessionTokenCookie(event, token, session.expiresAt); | 110 | setSessionTokenCookie(event, token, session.expiresAt); | ||
| 108 | event.locals.session = session; | 111 | event.locals.session = session; | ||
| 109 | event.locals.user = user; | 112 | event.locals.user = user; | ||
| 110 | } else { | 113 | } else { | ||
| 111 | - | console.debug("Invalid session token"); | 114 | + | log.debug("Invalid session token"); |
| 112 | deleteSessionTokenCookie(event); | 115 | deleteSessionTokenCookie(event); | ||
| 113 | event.locals.session = null; | 116 | event.locals.session = null; | ||
| 114 | event.locals.user = null; | 117 | event.locals.user = null; | ||
| @@ -118,7 +121,7 @@ export const handle: Handle = async ({ event, resolve }) => { | |||||
| 118 | 121 | ||||
| 119 | // After authentication, ensure SSE endpoint does not trigger rate limiting | 122 | // After authentication, ensure SSE endpoint does not trigger rate limiting | ||
| 120 | if (event.url.pathname.startsWith('/api/sse') && !event.locals.user) { | 123 | if (event.url.pathname.startsWith('/api/sse') && !event.locals.user) { | ||
| 121 | - | console.debug("SSE endpoint accessed without valid session, returning 401 without rate limiting"); | 124 | + | log.debug("SSE endpoint accessed without valid session, returning 401 without rate limiting"); |
| 122 | return addDebugHeaders(new Response("Unauthorized", { status: 401 })); | 125 | return addDebugHeaders(new Response("Unauthorized", { status: 401 })); | ||
| 123 | } | 126 | } | ||
| 124 | 127 | ||||
M src/lib/api/rate-limiter.ts +5 -2
| @@ -1,4 +1,7 @@ | |||
| 1 | 1 | import type { RequestEvent } from '@sveltejs/kit'; | |
| 2 | + | import { createLogger } from '$lib/utils/logger.server'; | |
| 3 | + | ||
| 4 | + | const log = createLogger('rate-limiter'); | |
| 2 | 5 | ||
| 3 | 6 | const isDev = import.meta.env.DEV; | |
| 4 | 7 | ||
| @@ -86,7 +89,7 @@ function isRateLimited(ip: string, endpointType: keyof typeof RATE_LIMITS, isAut | |||
| 86 | 89 | ||
| 87 | 90 | // Check if we're over the limit | |
| 88 | 91 | if (validRequests.length >= points) { | |
| 89 | - | console.debug(`Rate limit exceeded for IP ${ip} on ${endpointType} endpoint (${isAuthenticated ? 'authenticated' : 'unauthenticated'})`); | |
| 92 | + | log.debug(`Rate limit exceeded for IP ${ip} on ${endpointType} endpoint (${isAuthenticated ? 'authenticated' : 'unauthenticated'})`); | |
| 90 | 93 | const oldestRequest = validRequests[0]; | |
| 91 | 94 | const retryAfter = durationMs - (now - oldestRequest.timestamp); | |
| 92 | 95 | return { limited: true, retryAfter }; | |
| @@ -104,7 +107,7 @@ function isRateLimited(ip: string, endpointType: keyof typeof RATE_LIMITS, isAut | |||
| 104 | 107 | const requests = ipRequests.get(ip) || []; | |
| 105 | 108 | const validRequests = requests.filter(req => now - req.timestamp < durationMs); | |
| 106 | 109 | ||
| 107 | - | console.debug(`Rate limit check for ${ip} on ${endpointType}: ${validRequests.length}/${points} requests in the last ${durationMs/1000}s`); | |
| 110 | + | log.debug(`Rate limit check for ${ip} on ${endpointType}: ${validRequests.length}/${points} requests in the last ${durationMs/1000}s`); | |
| 108 | 111 | ||
| 109 | 112 | if (validRequests.length >= points) { | |
| 110 | 113 | const oldestRequest = validRequests[0]; | |
| @@ -1,4 +1,7 @@ | |||||
| 1 | import type { RequestEvent } from '@sveltejs/kit'; | 1 | import type { RequestEvent } from '@sveltejs/kit'; | ||
| 2 | + | import { createLogger } from '$lib/utils/logger.server'; | |||
| 3 | + | ||||
| 4 | + | const log = createLogger('rate-limiter'); | |||
| 2 | 5 | ||||
| 3 | const isDev = import.meta.env.DEV; | 6 | const isDev = import.meta.env.DEV; | ||
| 4 | 7 | ||||
| @@ -86,7 +89,7 @@ function isRateLimited(ip: string, endpointType: keyof typeof RATE_LIMITS, isAut | |||||
| 86 | 89 | ||||
| 87 | // Check if we're over the limit | 90 | // Check if we're over the limit | ||
| 88 | if (validRequests.length >= points) { | 91 | if (validRequests.length >= points) { | ||
| 89 | - | console.debug(`Rate limit exceeded for IP ${ip} on ${endpointType} endpoint (${isAuthenticated ? 'authenticated' : 'unauthenticated'})`); | 92 | + | log.debug(`Rate limit exceeded for IP ${ip} on ${endpointType} endpoint (${isAuthenticated ? 'authenticated' : 'unauthenticated'})`); |
| 90 | const oldestRequest = validRequests[0]; | 93 | const oldestRequest = validRequests[0]; | ||
| 91 | const retryAfter = durationMs - (now - oldestRequest.timestamp); | 94 | const retryAfter = durationMs - (now - oldestRequest.timestamp); | ||
| 92 | return { limited: true, retryAfter }; | 95 | return { limited: true, retryAfter }; | ||
| @@ -104,7 +107,7 @@ function isRateLimited(ip: string, endpointType: keyof typeof RATE_LIMITS, isAut | |||||
| 104 | const requests = ipRequests.get(ip) || []; | 107 | const requests = ipRequests.get(ip) || []; | ||
| 105 | const validRequests = requests.filter(req => now - req.timestamp < durationMs); | 108 | const validRequests = requests.filter(req => now - req.timestamp < durationMs); | ||
| 106 | 109 | ||||
| 107 | - | console.debug(`Rate limit check for ${ip} on ${endpointType}: ${validRequests.length}/${points} requests in the last ${durationMs/1000}s`); | 110 | + | log.debug(`Rate limit check for ${ip} on ${endpointType}: ${validRequests.length}/${points} requests in the last ${durationMs/1000}s`); |
| 108 | 111 | ||||
| 109 | if (validRequests.length >= points) { | 112 | if (validRequests.length >= points) { | ||
| 110 | const oldestRequest = validRequests[0]; | 113 | const oldestRequest = validRequests[0]; | ||
M src/lib/api/session.cookie.ts +10 -6
| @@ -1,11 +1,15 @@ | |||
| 1 | 1 | import type { Cookies } from "@sveltejs/kit"; | |
| 2 | + | import { createLogger } from "$lib/utils/logger.server"; | |
| 3 | + | ||
| 4 | + | const log = createLogger('session-cookie'); | |
| 2 | 5 | ||
| 3 | 6 | // Helper function to determine cookie domain | |
| 4 | 7 | function getCookieDomain(): string | undefined { | |
| 5 | 8 | if (process.env.NODE_ENV === 'production') { | |
| 6 | 9 | // Check for Cloudflare Pages domain | |
| 7 | 10 | if (process.env.CF_PAGES_URL) { | |
| 8 | - | console.debug("Cloudflare Pages URL:", process.env.CF_PAGES_URL); | |
| 11 | + | const url = process.env.CF_PAGES_URL; | |
| 12 | + | log.debug("Cloudflare Pages URL:", { url }); | |
| 9 | 13 | // For preview deployments, use host-only cookie | |
| 10 | 14 | if (process.env.CF_PAGES_BRANCH !== 'main') { | |
| 11 | 15 | return undefined; | |
| @@ -20,10 +24,10 @@ function getCookieDomain(): string | undefined { | |||
| 20 | 24 | } | |
| 21 | 25 | ||
| 22 | 26 | export function setSessionTokenCookie({ cookies }: { cookies: Cookies }, token: string, expiresAt: number): void { | |
| 23 | - | console.debug("Setting session cookie with expiry:", new Date(expiresAt)); | |
| 27 | + | log.debug("Setting session cookie with expiry:", new Date(expiresAt)); | |
| 24 | 28 | ||
| 25 | 29 | const domain = getCookieDomain(); | |
| 26 | - | console.debug("Setting cookie for domain:", domain); | |
| 30 | + | log.debug("Setting cookie for domain:", { domain }); | |
| 27 | 31 | ||
| 28 | 32 | cookies.set("session", token, { | |
| 29 | 33 | httpOnly: true, | |
| @@ -36,10 +40,10 @@ export function setSessionTokenCookie({ cookies }: { cookies: Cookies }, token: | |||
| 36 | 40 | } | |
| 37 | 41 | ||
| 38 | 42 | export function deleteSessionTokenCookie({ cookies }: { cookies: Cookies }): void { | |
| 39 | - | console.debug("Deleting session cookie"); | |
| 43 | + | log.debug("Deleting session cookie"); | |
| 40 | 44 | ||
| 41 | 45 | const domain = getCookieDomain(); | |
| 42 | - | console.debug("Deleting cookie for domain:", domain); | |
| 46 | + | log.debug("Deleting cookie for domain:", { domain }); | |
| 43 | 47 | ||
| 44 | 48 | cookies.set("session", "", { | |
| 45 | 49 | httpOnly: true, | |
| @@ -52,6 +56,6 @@ export function deleteSessionTokenCookie({ cookies }: { cookies: Cookies }): voi | |||
| 52 | 56 | } | |
| 53 | 57 | ||
| 54 | 58 | export function getSessionTokenCookie({ cookies }: { cookies: Cookies }): string | undefined { | |
| 55 | - | console.debug("Getting session cookie"); | |
| 59 | + | log.debug("Getting session cookie"); | |
| 56 | 60 | return cookies.get("session"); | |
| 57 | 61 | } ⏎̸ | |
| @@ -1,11 +1,15 @@ | |||||
| 1 | import type { Cookies } from "@sveltejs/kit"; | 1 | import type { Cookies } from "@sveltejs/kit"; | ||
| 2 | + | import { createLogger } from "$lib/utils/logger.server"; | |||
| 3 | + | ||||
| 4 | + | const log = createLogger('session-cookie'); | |||
| 2 | 5 | ||||
| 3 | // Helper function to determine cookie domain | 6 | // Helper function to determine cookie domain | ||
| 4 | function getCookieDomain(): string | undefined { | 7 | function getCookieDomain(): string | undefined { | ||
| 5 | if (process.env.NODE_ENV === 'production') { | 8 | if (process.env.NODE_ENV === 'production') { | ||
| 6 | // Check for Cloudflare Pages domain | 9 | // Check for Cloudflare Pages domain | ||
| 7 | if (process.env.CF_PAGES_URL) { | 10 | if (process.env.CF_PAGES_URL) { | ||
| 8 | - | console.debug("Cloudflare Pages URL:", process.env.CF_PAGES_URL); | 11 | + | const url = process.env.CF_PAGES_URL; |
| 12 | + | log.debug("Cloudflare Pages URL:", { url }); | |||
| 9 | // For preview deployments, use host-only cookie | 13 | // For preview deployments, use host-only cookie | ||
| 10 | if (process.env.CF_PAGES_BRANCH !== 'main') { | 14 | if (process.env.CF_PAGES_BRANCH !== 'main') { | ||
| 11 | return undefined; | 15 | return undefined; | ||
| @@ -20,10 +24,10 @@ function getCookieDomain(): string | undefined { | |||||
| 20 | } | 24 | } | ||
| 21 | 25 | ||||
| 22 | export function setSessionTokenCookie({ cookies }: { cookies: Cookies }, token: string, expiresAt: number): void { | 26 | export function setSessionTokenCookie({ cookies }: { cookies: Cookies }, token: string, expiresAt: number): void { | ||
| 23 | - | console.debug("Setting session cookie with expiry:", new Date(expiresAt)); | 27 | + | log.debug("Setting session cookie with expiry:", new Date(expiresAt)); |
| 24 | 28 | ||||
| 25 | const domain = getCookieDomain(); | 29 | const domain = getCookieDomain(); | ||
| 26 | - | console.debug("Setting cookie for domain:", domain); | 30 | + | log.debug("Setting cookie for domain:", { domain }); |
| 27 | 31 | ||||
| 28 | cookies.set("session", token, { | 32 | cookies.set("session", token, { | ||
| 29 | httpOnly: true, | 33 | httpOnly: true, | ||
| @@ -36,10 +40,10 @@ export function setSessionTokenCookie({ cookies }: { cookies: Cookies }, token: | |||||
| 36 | } | 40 | } | ||
| 37 | 41 | ||||
| 38 | export function deleteSessionTokenCookie({ cookies }: { cookies: Cookies }): void { | 42 | export function deleteSessionTokenCookie({ cookies }: { cookies: Cookies }): void { | ||
| 39 | - | console.debug("Deleting session cookie"); | 43 | + | log.debug("Deleting session cookie"); |
| 40 | 44 | ||||
| 41 | const domain = getCookieDomain(); | 45 | const domain = getCookieDomain(); | ||
| 42 | - | console.debug("Deleting cookie for domain:", domain); | 46 | + | log.debug("Deleting cookie for domain:", { domain }); |
| 43 | 47 | ||||
| 44 | cookies.set("session", "", { | 48 | cookies.set("session", "", { | ||
| 45 | httpOnly: true, | 49 | httpOnly: true, | ||
| @@ -52,6 +56,6 @@ export function deleteSessionTokenCookie({ cookies }: { cookies: Cookies }): voi | |||||
| 52 | } | 56 | } | ||
| 53 | 57 | ||||
| 54 | export function getSessionTokenCookie({ cookies }: { cookies: Cookies }): string | undefined { | 58 | export function getSessionTokenCookie({ cookies }: { cookies: Cookies }): string | undefined { | ||
| 55 | - | console.debug("Getting session cookie"); | 59 | + | log.debug("Getting session cookie"); |
| 56 | return cookies.get("session"); | 60 | return cookies.get("session"); | ||
| 57 | } ⏎̸ | 61 | } ⏎̸ | ||
M src/lib/api/session.server.ts +15 -12
| @@ -2,6 +2,9 @@ import db from '../db/db.server'; | |||
| 2 | 2 | import { sessions, users } from '../db/schema'; | |
| 3 | 3 | import { eq } from 'drizzle-orm'; | |
| 4 | 4 | import type { Session, User } from '../types/chat'; | |
| 5 | + | import { createLogger } from '../utils/logger.server'; | |
| 6 | + | ||
| 7 | + | const log = createLogger('session-server'); | |
| 5 | 8 | ||
| 6 | 9 | // Helper: Compute SHA-256 hash of a message and encode it as a hex string. | |
| 7 | 10 | export async function sha256(message: string): Promise<string> { | |
| @@ -41,14 +44,14 @@ export function generateSessionToken(): string { | |||
| 41 | 44 | const randomBytesArray = new Uint8Array(20); | |
| 42 | 45 | crypto.getRandomValues(randomBytesArray); | |
| 43 | 46 | const token = base32Encode(randomBytesArray); | |
| 44 | - | console.debug("Generated session token:", token); | |
| 47 | + | log.debug("Generated session token", { token }); | |
| 45 | 48 | return token; | |
| 46 | 49 | } | |
| 47 | 50 | ||
| 48 | 51 | // Create a session record once a valid token is available. | |
| 49 | 52 | // The session ID is the SHA-256 hash of the token. | |
| 50 | 53 | export async function createSession(token: string, userId: string): Promise<Session> { | |
| 51 | - | console.debug("Creating session for user:", userId); | |
| 54 | + | log.debug("Creating session", { userId }); | |
| 52 | 55 | const sessionId = await sha256(token); | |
| 53 | 56 | const createdAt = Date.now(); | |
| 54 | 57 | const expiresAt = createdAt + 24 * 60 * 60 * 1000; // 24 hours expiry | |
| @@ -58,7 +61,7 @@ export async function createSession(token: string, userId: string): Promise<Sess | |||
| 58 | 61 | createdAt, | |
| 59 | 62 | expiresAt, | |
| 60 | 63 | }); | |
| 61 | - | console.debug("Session created with id:", sessionId); | |
| 64 | + | log.debug("Session created", { sessionId }); | |
| 62 | 65 | return { id: sessionId, userId, createdAt, expiresAt }; | |
| 63 | 66 | } | |
| 64 | 67 | ||
| @@ -73,41 +76,41 @@ export type SessionValidationResult = | |||
| 73 | 76 | ||
| 74 | 77 | // Validate a session token by converting it to its SHA‑256 hash, checking expiration, and fetching the user. | |
| 75 | 78 | export async function validateSessionToken(token: string): Promise<SessionValidationResult> { | |
| 76 | - | console.debug("Validating session token:", token); | |
| 79 | + | log.debug("Validating session token"); | |
| 77 | 80 | const sessionId = await sha256(token); | |
| 78 | 81 | const session = await db.query.sessions.findFirst({ | |
| 79 | 82 | where: eq(sessions.id, sessionId) | |
| 80 | 83 | }); | |
| 81 | 84 | if (!session) { | |
| 82 | - | console.debug("Session not found for id:", sessionId); | |
| 85 | + | log.debug("Session not found", { sessionId }); | |
| 83 | 86 | return { session: null, user: null }; | |
| 84 | 87 | } | |
| 85 | 88 | const now = Date.now(); | |
| 86 | 89 | if (session.expiresAt < now) { | |
| 87 | - | console.debug("Session expired for id:", sessionId); | |
| 90 | + | log.debug("Session expired", { sessionId }); | |
| 88 | 91 | return { session: null, user: null }; | |
| 89 | 92 | } | |
| 90 | 93 | const user = await db.query.users.findFirst({ | |
| 91 | 94 | where: eq(users.id, session.userId) | |
| 92 | 95 | }); | |
| 93 | 96 | if (!user) { | |
| 94 | - | console.debug("User not found for session id:", sessionId); | |
| 97 | + | log.debug("User not found for session", { sessionId }); | |
| 95 | 98 | return { session: null, user: null }; | |
| 96 | 99 | } | |
| 97 | - | console.debug("Session validated for id:", sessionId); | |
| 100 | + | log.debug("Session validated", { sessionId, userId: user.id }); | |
| 98 | 101 | return { session, user }; | |
| 99 | 102 | } | |
| 100 | 103 | ||
| 101 | 104 | // Invalidate a specific session by deleting it from the database. | |
| 102 | 105 | export async function invalidateSession(sessionId: string): Promise<void> { | |
| 103 | - | console.debug("Invalidating session with id:", sessionId); | |
| 106 | + | log.debug("Invalidating session", { sessionId }); | |
| 104 | 107 | await db.delete(sessions).where(eq(sessions.id, sessionId)); | |
| 105 | - | console.debug("Session invalidated:", sessionId); | |
| 108 | + | log.debug("Session invalidated", { sessionId }); | |
| 106 | 109 | } | |
| 107 | 110 | ||
| 108 | 111 | // Invalidate all sessions for a given user. | |
| 109 | 112 | export async function invalidateAllSessions(userId: string): Promise<void> { | |
| 110 | - | console.debug("Invalidating all sessions for user:", userId); | |
| 113 | + | log.debug("Invalidating all sessions for user", { userId }); | |
| 111 | 114 | await db.delete(sessions).where(eq(sessions.userId, userId)); | |
| 112 | - | console.debug("All sessions invalidated for user:", userId); | |
| 115 | + | log.debug("All sessions invalidated", { userId }); | |
| 113 | 116 | } ⏎̸ | |
| @@ -2,6 +2,9 @@ import db from '../db/db.server'; | |||||
| 2 | import { sessions, users } from '../db/schema'; | 2 | import { sessions, users } from '../db/schema'; | ||
| 3 | import { eq } from 'drizzle-orm'; | 3 | import { eq } from 'drizzle-orm'; | ||
| 4 | import type { Session, User } from '../types/chat'; | 4 | import type { Session, User } from '../types/chat'; | ||
| 5 | + | import { createLogger } from '../utils/logger.server'; | |||
| 6 | + | ||||
| 7 | + | const log = createLogger('session-server'); | |||
| 5 | 8 | ||||
| 6 | // Helper: Compute SHA-256 hash of a message and encode it as a hex string. | 9 | // Helper: Compute SHA-256 hash of a message and encode it as a hex string. | ||
| 7 | export async function sha256(message: string): Promise<string> { | 10 | export async function sha256(message: string): Promise<string> { | ||
| @@ -41,14 +44,14 @@ export function generateSessionToken(): string { | |||||
| 41 | const randomBytesArray = new Uint8Array(20); | 44 | const randomBytesArray = new Uint8Array(20); | ||
| 42 | crypto.getRandomValues(randomBytesArray); | 45 | crypto.getRandomValues(randomBytesArray); | ||
| 43 | const token = base32Encode(randomBytesArray); | 46 | const token = base32Encode(randomBytesArray); | ||
| 44 | - | console.debug("Generated session token:", token); | 47 | + | log.debug("Generated session token", { token }); |
| 45 | return token; | 48 | return token; | ||
| 46 | } | 49 | } | ||
| 47 | 50 | ||||
| 48 | // Create a session record once a valid token is available. | 51 | // Create a session record once a valid token is available. | ||
| 49 | // The session ID is the SHA-256 hash of the token. | 52 | // The session ID is the SHA-256 hash of the token. | ||
| 50 | export async function createSession(token: string, userId: string): Promise<Session> { | 53 | export async function createSession(token: string, userId: string): Promise<Session> { | ||
| 51 | - | console.debug("Creating session for user:", userId); | 54 | + | log.debug("Creating session", { userId }); |
| 52 | const sessionId = await sha256(token); | 55 | const sessionId = await sha256(token); | ||
| 53 | const createdAt = Date.now(); | 56 | const createdAt = Date.now(); | ||
| 54 | const expiresAt = createdAt + 24 * 60 * 60 * 1000; // 24 hours expiry | 57 | const expiresAt = createdAt + 24 * 60 * 60 * 1000; // 24 hours expiry | ||
| @@ -58,7 +61,7 @@ export async function createSession(token: string, userId: string): Promise<Sess | |||||
| 58 | createdAt, | 61 | createdAt, | ||
| 59 | expiresAt, | 62 | expiresAt, | ||
| 60 | }); | 63 | }); | ||
| 61 | - | console.debug("Session created with id:", sessionId); | 64 | + | log.debug("Session created", { sessionId }); |
| 62 | return { id: sessionId, userId, createdAt, expiresAt }; | 65 | return { id: sessionId, userId, createdAt, expiresAt }; | ||
| 63 | } | 66 | } | ||
| 64 | 67 | ||||
| @@ -73,41 +76,41 @@ export type SessionValidationResult = | |||||
| 73 | 76 | ||||
| 74 | // Validate a session token by converting it to its SHA‑256 hash, checking expiration, and fetching the user. | 77 | // Validate a session token by converting it to its SHA‑256 hash, checking expiration, and fetching the user. | ||
| 75 | export async function validateSessionToken(token: string): Promise<SessionValidationResult> { | 78 | export async function validateSessionToken(token: string): Promise<SessionValidationResult> { | ||
| 76 | - | console.debug("Validating session token:", token); | 79 | + | log.debug("Validating session token"); |
| 77 | const sessionId = await sha256(token); | 80 | const sessionId = await sha256(token); | ||
| 78 | const session = await db.query.sessions.findFirst({ | 81 | const session = await db.query.sessions.findFirst({ | ||
| 79 | where: eq(sessions.id, sessionId) | 82 | where: eq(sessions.id, sessionId) | ||
| 80 | }); | 83 | }); | ||
| 81 | if (!session) { | 84 | if (!session) { | ||
| 82 | - | console.debug("Session not found for id:", sessionId); | 85 | + | log.debug("Session not found", { sessionId }); |
| 83 | return { session: null, user: null }; | 86 | return { session: null, user: null }; | ||
| 84 | } | 87 | } | ||
| 85 | const now = Date.now(); | 88 | const now = Date.now(); | ||
| 86 | if (session.expiresAt < now) { | 89 | if (session.expiresAt < now) { | ||
| 87 | - | console.debug("Session expired for id:", sessionId); | 90 | + | log.debug("Session expired", { sessionId }); |
| 88 | return { session: null, user: null }; | 91 | return { session: null, user: null }; | ||
| 89 | } | 92 | } | ||
| 90 | const user = await db.query.users.findFirst({ | 93 | const user = await db.query.users.findFirst({ | ||
| 91 | where: eq(users.id, session.userId) | 94 | where: eq(users.id, session.userId) | ||
| 92 | }); | 95 | }); | ||
| 93 | if (!user) { | 96 | if (!user) { | ||
| 94 | - | console.debug("User not found for session id:", sessionId); | 97 | + | log.debug("User not found for session", { sessionId }); |
| 95 | return { session: null, user: null }; | 98 | return { session: null, user: null }; | ||
| 96 | } | 99 | } | ||
| 97 | - | console.debug("Session validated for id:", sessionId); | 100 | + | log.debug("Session validated", { sessionId, userId: user.id }); |
| 98 | return { session, user }; | 101 | return { session, user }; | ||
| 99 | } | 102 | } | ||
| 100 | 103 | ||||
| 101 | // Invalidate a specific session by deleting it from the database. | 104 | // Invalidate a specific session by deleting it from the database. | ||
| 102 | export async function invalidateSession(sessionId: string): Promise<void> { | 105 | export async function invalidateSession(sessionId: string): Promise<void> { | ||
| 103 | - | console.debug("Invalidating session with id:", sessionId); | 106 | + | log.debug("Invalidating session", { sessionId }); |
| 104 | await db.delete(sessions).where(eq(sessions.id, sessionId)); | 107 | await db.delete(sessions).where(eq(sessions.id, sessionId)); | ||
| 105 | - | console.debug("Session invalidated:", sessionId); | 108 | + | log.debug("Session invalidated", { sessionId }); |
| 106 | } | 109 | } | ||
| 107 | 110 | ||||
| 108 | // Invalidate all sessions for a given user. | 111 | // Invalidate all sessions for a given user. | ||
| 109 | export async function invalidateAllSessions(userId: string): Promise<void> { | 112 | export async function invalidateAllSessions(userId: string): Promise<void> { | ||
| 110 | - | console.debug("Invalidating all sessions for user:", userId); | 113 | + | log.debug("Invalidating all sessions for user", { userId }); |
| 111 | await db.delete(sessions).where(eq(sessions.userId, userId)); | 114 | await db.delete(sessions).where(eq(sessions.userId, userId)); | ||
| 112 | - | console.debug("All sessions invalidated for user:", userId); | 115 | + | log.debug("All sessions invalidated", { userId }); |
| 113 | } ⏎̸ | 116 | } ⏎̸ | ||
M src/lib/cache/initialize-cache.ts +7 -4
| @@ -2,16 +2,19 @@ import { messageCache } from '$lib/cache/message-cache'; | |||
| 2 | 2 | import db from '$lib/db/db.server'; | |
| 3 | 3 | import { messages } from '$lib/db/schema'; | |
| 4 | 4 | import { desc } from 'drizzle-orm'; | |
| 5 | + | import { createLogger } from '$lib/utils/logger.server'; | |
| 6 | + | ||
| 7 | + | const log = createLogger('cache-initialize'); | |
| 5 | 8 | ||
| 6 | 9 | let isCacheInitialized = false; | |
| 7 | 10 | ||
| 8 | 11 | export async function initializeMessageCache(): Promise<void> { | |
| 9 | 12 | if (isCacheInitialized) { | |
| 10 | - | console.debug('[Cache] Message cache already initialized.'); | |
| 13 | + | log.debug('[Cache] Message cache already initialized.'); | |
| 11 | 14 | return; | |
| 12 | 15 | } | |
| 13 | 16 | try { | |
| 14 | - | console.debug('[Cache] Initializing message cache with pre-population.'); | |
| 17 | + | log.debug('[Cache] Initializing message cache with pre-population.'); | |
| 15 | 18 | const fetchedMessages = await db.select() | |
| 16 | 19 | .from(messages) | |
| 17 | 20 | .orderBy(desc(messages.timestamp)) | |
| @@ -20,11 +23,11 @@ export async function initializeMessageCache(): Promise<void> { | |||
| 20 | 23 | await messageCache.initialize(fetchedMessages.reverse()); | |
| 21 | 24 | isCacheInitialized = true; | |
| 22 | 25 | const stats = messageCache.getCacheStats(); | |
| 23 | - | console.debug('[Cache] Message cache initialized successfully:', { | |
| 26 | + | log.debug('[Cache] Message cache initialized successfully:', { | |
| 24 | 27 | roomCount: stats.roomCount, | |
| 25 | 28 | totalMessages: stats.totalMessages | |
| 26 | 29 | }); | |
| 27 | 30 | } catch (error) { | |
| 28 | - | console.error('[Cache] Failed to initialize message cache:', error); | |
| 31 | + | log.error('[Cache] Failed to initialize message cache:', { error: error instanceof Error ? error.message : 'Unknown error' }); | |
| 29 | 32 | } | |
| 30 | 33 | } ⏎̸ | |
| @@ -2,16 +2,19 @@ import { messageCache } from '$lib/cache/message-cache'; | |||||
| 2 | import db from '$lib/db/db.server'; | 2 | import db from '$lib/db/db.server'; | ||
| 3 | import { messages } from '$lib/db/schema'; | 3 | import { messages } from '$lib/db/schema'; | ||
| 4 | import { desc } from 'drizzle-orm'; | 4 | import { desc } from 'drizzle-orm'; | ||
| 5 | + | import { createLogger } from '$lib/utils/logger.server'; | |||
| 6 | + | ||||
| 7 | + | const log = createLogger('cache-initialize'); | |||
| 5 | 8 | ||||
| 6 | let isCacheInitialized = false; | 9 | let isCacheInitialized = false; | ||
| 7 | 10 | ||||
| 8 | export async function initializeMessageCache(): Promise<void> { | 11 | export async function initializeMessageCache(): Promise<void> { | ||
| 9 | if (isCacheInitialized) { | 12 | if (isCacheInitialized) { | ||
| 10 | - | console.debug('[Cache] Message cache already initialized.'); | 13 | + | log.debug('[Cache] Message cache already initialized.'); |
| 11 | return; | 14 | return; | ||
| 12 | } | 15 | } | ||
| 13 | try { | 16 | try { | ||
| 14 | - | console.debug('[Cache] Initializing message cache with pre-population.'); | 17 | + | log.debug('[Cache] Initializing message cache with pre-population.'); |
| 15 | const fetchedMessages = await db.select() | 18 | const fetchedMessages = await db.select() | ||
| 16 | .from(messages) | 19 | .from(messages) | ||
| 17 | .orderBy(desc(messages.timestamp)) | 20 | .orderBy(desc(messages.timestamp)) | ||
| @@ -20,11 +23,11 @@ export async function initializeMessageCache(): Promise<void> { | |||||
| 20 | await messageCache.initialize(fetchedMessages.reverse()); | 23 | await messageCache.initialize(fetchedMessages.reverse()); | ||
| 21 | isCacheInitialized = true; | 24 | isCacheInitialized = true; | ||
| 22 | const stats = messageCache.getCacheStats(); | 25 | const stats = messageCache.getCacheStats(); | ||
| 23 | - | console.debug('[Cache] Message cache initialized successfully:', { | 26 | + | log.debug('[Cache] Message cache initialized successfully:', { |
| 24 | roomCount: stats.roomCount, | 27 | roomCount: stats.roomCount, | ||
| 25 | totalMessages: stats.totalMessages | 28 | totalMessages: stats.totalMessages | ||
| 26 | }); | 29 | }); | ||
| 27 | } catch (error) { | 30 | } catch (error) { | ||
| 28 | - | console.error('[Cache] Failed to initialize message cache:', error); | 31 | + | log.error('[Cache] Failed to initialize message cache:', { error: error instanceof Error ? error.message : 'Unknown error' }); |
| 29 | } | 32 | } | ||
| 30 | } ⏎̸ | 33 | } ⏎̸ | ||
M src/lib/db/db.server.ts +8 -5
| @@ -2,30 +2,33 @@ import { drizzle } from 'drizzle-orm/libsql'; | |||
| 2 | 2 | import { createClient } from '@libsql/client'; | |
| 3 | 3 | import { TURSO_AUTH_TOKEN, TURSO_DATABASE_URL } from '$env/static/private'; | |
| 4 | 4 | import schema, { ensureDefaultChatRoom } from './schema'; | |
| 5 | + | import { createLogger } from '$lib/utils/logger.server'; | |
| 6 | + | ||
| 7 | + | const log = createLogger('db-server'); | |
| 5 | 8 | ||
| 6 | 9 | const client = createClient({ url: TURSO_DATABASE_URL, authToken: TURSO_AUTH_TOKEN }); | |
| 7 | 10 | const db = drizzle(client, { schema }); | |
| 8 | 11 | ||
| 9 | 12 | // Initialize database with required data | |
| 10 | 13 | export const initializeDatabase = async () => { | |
| 11 | - | console.debug('Initializing database...'); | |
| 14 | + | log.debug('Initializing database...'); | |
| 12 | 15 | try { | |
| 13 | 16 | await ensureDefaultChatRoom(db); | |
| 14 | - | console.debug('Database initialization complete'); | |
| 17 | + | log.debug('Database initialization complete'); | |
| 15 | 18 | } catch (error) { | |
| 16 | - | console.error('Error initializing database:', error); | |
| 19 | + | log.error('Error initializing database:', { error: error instanceof Error ? error.message : 'Unknown error' }); | |
| 17 | 20 | throw error; | |
| 18 | 21 | } | |
| 19 | 22 | }; | |
| 20 | 23 | ||
| 21 | 24 | // Call initialization when the module is loaded | |
| 22 | 25 | initializeDatabase().catch(error => { | |
| 23 | - | console.error('Failed to initialize database:', error); | |
| 26 | + | log.error('Failed to initialize database:', { error: error instanceof Error ? error.message : 'Unknown error' }); | |
| 24 | 27 | }); | |
| 25 | 28 | ||
| 26 | 29 | export const testConnection = async () => { | |
| 27 | 30 | const result = await db.query.users.findMany(); | |
| 28 | - | console.log(result); | |
| 31 | + | log.debug('Test connection result:', result); | |
| 29 | 32 | }; | |
| 30 | 33 | ||
| 31 | 34 | export default db;⏎̸ | |
| @@ -2,30 +2,33 @@ import { drizzle } from 'drizzle-orm/libsql'; | |||||
| 2 | import { createClient } from '@libsql/client'; | 2 | import { createClient } from '@libsql/client'; | ||
| 3 | import { TURSO_AUTH_TOKEN, TURSO_DATABASE_URL } from '$env/static/private'; | 3 | import { TURSO_AUTH_TOKEN, TURSO_DATABASE_URL } from '$env/static/private'; | ||
| 4 | import schema, { ensureDefaultChatRoom } from './schema'; | 4 | import schema, { ensureDefaultChatRoom } from './schema'; | ||
| 5 | + | import { createLogger } from '$lib/utils/logger.server'; | |||
| 6 | + | ||||
| 7 | + | const log = createLogger('db-server'); | |||
| 5 | 8 | ||||
| 6 | const client = createClient({ url: TURSO_DATABASE_URL, authToken: TURSO_AUTH_TOKEN }); | 9 | const client = createClient({ url: TURSO_DATABASE_URL, authToken: TURSO_AUTH_TOKEN }); | ||
| 7 | const db = drizzle(client, { schema }); | 10 | const db = drizzle(client, { schema }); | ||
| 8 | 11 | ||||
| 9 | // Initialize database with required data | 12 | // Initialize database with required data | ||
| 10 | export const initializeDatabase = async () => { | 13 | export const initializeDatabase = async () => { | ||
| 11 | - | console.debug('Initializing database...'); | 14 | + | log.debug('Initializing database...'); |
| 12 | try { | 15 | try { | ||
| 13 | await ensureDefaultChatRoom(db); | 16 | await ensureDefaultChatRoom(db); | ||
| 14 | - | console.debug('Database initialization complete'); | 17 | + | log.debug('Database initialization complete'); |
| 15 | } catch (error) { | 18 | } catch (error) { | ||
| 16 | - | console.error('Error initializing database:', error); | 19 | + | log.error('Error initializing database:', { error: error instanceof Error ? error.message : 'Unknown error' }); |
| 17 | throw error; | 20 | throw error; | ||
| 18 | } | 21 | } | ||
| 19 | }; | 22 | }; | ||
| 20 | 23 | ||||
| 21 | // Call initialization when the module is loaded | 24 | // Call initialization when the module is loaded | ||
| 22 | initializeDatabase().catch(error => { | 25 | initializeDatabase().catch(error => { | ||
| 23 | - | console.error('Failed to initialize database:', error); | 26 | + | log.error('Failed to initialize database:', { error: error instanceof Error ? error.message : 'Unknown error' }); |
| 24 | }); | 27 | }); | ||
| 25 | 28 | ||||
| 26 | export const testConnection = async () => { | 29 | export const testConnection = async () => { | ||
| 27 | const result = await db.query.users.findMany(); | 30 | const result = await db.query.users.findMany(); | ||
| 28 | - | console.log(result); | 31 | + | log.debug('Test connection result:', result); |
| 29 | }; | 32 | }; | ||
| 30 | 33 | ||||
| 31 | export default db;⏎̸ | 34 | export default db;⏎̸ | ||
M src/lib/db/schema.ts +9 -6
| @@ -4,6 +4,9 @@ import type { User, ChatRoom, Message, UserStatus, MessageType, Session } from " | |||
| 4 | 4 | import { v4 as uuidv4 } from "uuid"; | |
| 5 | 5 | import { eq } from "drizzle-orm"; | |
| 6 | 6 | import type { LibSQLDatabase } from "drizzle-orm/libsql"; | |
| 7 | + | import { createLogger } from "$lib/utils/logger.server"; | |
| 8 | + | ||
| 9 | + | const log = createLogger('schema'); | |
| 7 | 10 | ||
| 8 | 11 | // Default chat room ID - using a UUID to avoid collisions | |
| 9 | 12 | export const DEFAULT_CHAT_ROOM_ID = '00000000-0000-0000-0000-000000000001'; | |
| @@ -120,7 +123,7 @@ const schema = { | |||
| 120 | 123 | ||
| 121 | 124 | // Function to ensure default chat room exists | |
| 122 | 125 | export async function ensureDefaultChatRoom(db: LibSQLDatabase<typeof schema>) { | |
| 123 | - | console.debug('Ensuring default chat room exists...'); | |
| 126 | + | log.debug('Ensuring default chat room exists...'); | |
| 124 | 127 | try { | |
| 125 | 128 | // Use a transaction to handle concurrent creation attempts | |
| 126 | 129 | await db.transaction(async (tx) => { | |
| @@ -130,25 +133,25 @@ export async function ensureDefaultChatRoom(db: LibSQLDatabase<typeof schema>) { | |||
| 130 | 133 | .get(); | |
| 131 | 134 | ||
| 132 | 135 | if (!defaultRoom) { | |
| 133 | - | console.debug('Creating default chat room...'); | |
| 136 | + | log.debug('Creating default chat room...'); | |
| 134 | 137 | await tx.insert(chatRooms).values({ | |
| 135 | 138 | id: DEFAULT_CHAT_ROOM_ID, | |
| 136 | 139 | name: 'General', | |
| 137 | 140 | type: 'group', | |
| 138 | 141 | createdAt: Date.now() | |
| 139 | 142 | }); | |
| 140 | - | console.debug('Default chat room created successfully'); | |
| 143 | + | log.debug('Default chat room created successfully'); | |
| 141 | 144 | } else { | |
| 142 | - | console.debug('Default chat room already exists'); | |
| 145 | + | log.debug('Default chat room already exists'); | |
| 143 | 146 | } | |
| 144 | 147 | }); | |
| 145 | 148 | } catch (error: unknown) { | |
| 146 | 149 | // If the error is a unique constraint violation, another process probably created the room | |
| 147 | 150 | if (isLibSQLError(error) && error.code === 'SQLITE_CONSTRAINT') { | |
| 148 | - | console.debug('Default chat room was created by another process'); | |
| 151 | + | log.debug('Default chat room was created by another process'); | |
| 149 | 152 | return; | |
| 150 | 153 | } | |
| 151 | - | console.error('Error ensuring default chat room:', error); | |
| 154 | + | log.error('Error ensuring default chat room:', { error: error instanceof Error ? error.message : 'Unknown error' }); | |
| 152 | 155 | throw error; | |
| 153 | 156 | } | |
| 154 | 157 | } | |
| @@ -4,6 +4,9 @@ import type { User, ChatRoom, Message, UserStatus, MessageType, Session } from " | |||||
| 4 | import { v4 as uuidv4 } from "uuid"; | 4 | import { v4 as uuidv4 } from "uuid"; | ||
| 5 | import { eq } from "drizzle-orm"; | 5 | import { eq } from "drizzle-orm"; | ||
| 6 | import type { LibSQLDatabase } from "drizzle-orm/libsql"; | 6 | import type { LibSQLDatabase } from "drizzle-orm/libsql"; | ||
| 7 | + | import { createLogger } from "$lib/utils/logger.server"; | |||
| 8 | + | ||||
| 9 | + | const log = createLogger('schema'); | |||
| 7 | 10 | ||||
| 8 | // Default chat room ID - using a UUID to avoid collisions | 11 | // Default chat room ID - using a UUID to avoid collisions | ||
| 9 | export const DEFAULT_CHAT_ROOM_ID = '00000000-0000-0000-0000-000000000001'; | 12 | export const DEFAULT_CHAT_ROOM_ID = '00000000-0000-0000-0000-000000000001'; | ||
| @@ -120,7 +123,7 @@ const schema = { | |||||
| 120 | 123 | ||||
| 121 | // Function to ensure default chat room exists | 124 | // Function to ensure default chat room exists | ||
| 122 | export async function ensureDefaultChatRoom(db: LibSQLDatabase<typeof schema>) { | 125 | export async function ensureDefaultChatRoom(db: LibSQLDatabase<typeof schema>) { | ||
| 123 | - | console.debug('Ensuring default chat room exists...'); | 126 | + | log.debug('Ensuring default chat room exists...'); |
| 124 | try { | 127 | try { | ||
| 125 | // Use a transaction to handle concurrent creation attempts | 128 | // Use a transaction to handle concurrent creation attempts | ||
| 126 | await db.transaction(async (tx) => { | 129 | await db.transaction(async (tx) => { | ||
| @@ -130,25 +133,25 @@ export async function ensureDefaultChatRoom(db: LibSQLDatabase<typeof schema>) { | |||||
| 130 | .get(); | 133 | .get(); | ||
| 131 | 134 | ||||
| 132 | if (!defaultRoom) { | 135 | if (!defaultRoom) { | ||
| 133 | - | console.debug('Creating default chat room...'); | 136 | + | log.debug('Creating default chat room...'); |
| 134 | await tx.insert(chatRooms).values({ | 137 | await tx.insert(chatRooms).values({ | ||
| 135 | id: DEFAULT_CHAT_ROOM_ID, | 138 | id: DEFAULT_CHAT_ROOM_ID, | ||
| 136 | name: 'General', | 139 | name: 'General', | ||
| 137 | type: 'group', | 140 | type: 'group', | ||
| 138 | createdAt: Date.now() | 141 | createdAt: Date.now() | ||
| 139 | }); | 142 | }); | ||
| 140 | - | console.debug('Default chat room created successfully'); | 143 | + | log.debug('Default chat room created successfully'); |
| 141 | } else { | 144 | } else { | ||
| 142 | - | console.debug('Default chat room already exists'); | 145 | + | log.debug('Default chat room already exists'); |
| 143 | } | 146 | } | ||
| 144 | }); | 147 | }); | ||
| 145 | } catch (error: unknown) { | 148 | } catch (error: unknown) { | ||
| 146 | // If the error is a unique constraint violation, another process probably created the room | 149 | // If the error is a unique constraint violation, another process probably created the room | ||
| 147 | if (isLibSQLError(error) && error.code === 'SQLITE_CONSTRAINT') { | 150 | if (isLibSQLError(error) && error.code === 'SQLITE_CONSTRAINT') { | ||
| 148 | - | console.debug('Default chat room was created by another process'); | 151 | + | log.debug('Default chat room was created by another process'); |
| 149 | return; | 152 | return; | ||
| 150 | } | 153 | } | ||
| 151 | - | console.error('Error ensuring default chat room:', error); | 154 | + | log.error('Error ensuring default chat room:', { error: error instanceof Error ? error.message : 'Unknown error' }); |
| 152 | throw error; | 155 | throw error; | ||
| 153 | } | 156 | } | ||
| 154 | } | 157 | } | ||
A src/lib/utils/logger.server.ts +55 -0
| @@ -0,0 +1,55 @@ | |||
| 1 | + | import pino from 'pino'; | |
| 2 | + | ||
| 3 | + | const logLevel = process.env.NODE_ENV === 'development' ? 'debug' : 'info'; | |
| 4 | + | ||
| 5 | + | class LoggerService { | |
| 6 | + | private static instance: LoggerService; | |
| 7 | + | private logger: pino.Logger; | |
| 8 | + | private moduleLoggers: Map<string, Logger>; | |
| 9 | + | ||
| 10 | + | private constructor() { | |
| 11 | + | this.logger = pino({ | |
| 12 | + | transport: { | |
| 13 | + | target: 'pino-pretty', | |
| 14 | + | options: { | |
| 15 | + | colorize: true, | |
| 16 | + | translateTime: 'HH:MM:ss', | |
| 17 | + | ignore: 'pid,hostname', | |
| 18 | + | }, | |
| 19 | + | }, | |
| 20 | + | level: logLevel, | |
| 21 | + | }); | |
| 22 | + | this.moduleLoggers = new Map(); | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | public static getInstance(): LoggerService { | |
| 26 | + | if (!LoggerService.instance) { | |
| 27 | + | LoggerService.instance = new LoggerService(); | |
| 28 | + | } | |
| 29 | + | return LoggerService.instance; | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | public getLogger(module: string): Logger { | |
| 33 | + | if (!this.moduleLoggers.has(module)) { | |
| 34 | + | this.moduleLoggers.set(module, { | |
| 35 | + | debug: (msg: string, obj = {}) => this.logger.debug({ module, ...obj }, msg), | |
| 36 | + | info: (msg: string, obj = {}) => this.logger.info({ module, ...obj }, msg), | |
| 37 | + | warn: (msg: string, obj = {}) => this.logger.warn({ module, ...obj }, msg), | |
| 38 | + | error: (msg: string, obj = {}) => this.logger.error({ module, ...obj }, msg) | |
| 39 | + | }); | |
| 40 | + | } | |
| 41 | + | return this.moduleLoggers.get(module)!; | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | export interface Logger { | |
| 46 | + | debug: (msg: string, obj?: object) => void; | |
| 47 | + | info: (msg: string, obj?: object) => void; | |
| 48 | + | warn: (msg: string, obj?: object) => void; | |
| 49 | + | error: (msg: string, obj?: object) => void; | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | // Export a function to get a logger for a specific module | |
| 53 | + | export const createLogger = (module: string): Logger => { | |
| 54 | + | return LoggerService.getInstance().getLogger(module); | |
| 55 | + | }; ⏎̸ | |
| @@ -0,0 +1,55 @@ | |||||
| 1 | + | import pino from 'pino'; | |||
| 2 | + | ||||
| 3 | + | const logLevel = process.env.NODE_ENV === 'development' ? 'debug' : 'info'; | |||
| 4 | + | ||||
| 5 | + | class LoggerService { | |||
| 6 | + | private static instance: LoggerService; | |||
| 7 | + | private logger: pino.Logger; | |||
| 8 | + | private moduleLoggers: Map<string, Logger>; | |||
| 9 | + | ||||
| 10 | + | private constructor() { | |||
| 11 | + | this.logger = pino({ | |||
| 12 | + | transport: { | |||
| 13 | + | target: 'pino-pretty', | |||
| 14 | + | options: { | |||
| 15 | + | colorize: true, | |||
| 16 | + | translateTime: 'HH:MM:ss', | |||
| 17 | + | ignore: 'pid,hostname', | |||
| 18 | + | }, | |||
| 19 | + | }, | |||
| 20 | + | level: logLevel, | |||
| 21 | + | }); | |||
| 22 | + | this.moduleLoggers = new Map(); | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | public static getInstance(): LoggerService { | |||
| 26 | + | if (!LoggerService.instance) { | |||
| 27 | + | LoggerService.instance = new LoggerService(); | |||
| 28 | + | } | |||
| 29 | + | return LoggerService.instance; | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | public getLogger(module: string): Logger { | |||
| 33 | + | if (!this.moduleLoggers.has(module)) { | |||
| 34 | + | this.moduleLoggers.set(module, { | |||
| 35 | + | debug: (msg: string, obj = {}) => this.logger.debug({ module, ...obj }, msg), | |||
| 36 | + | info: (msg: string, obj = {}) => this.logger.info({ module, ...obj }, msg), | |||
| 37 | + | warn: (msg: string, obj = {}) => this.logger.warn({ module, ...obj }, msg), | |||
| 38 | + | error: (msg: string, obj = {}) => this.logger.error({ module, ...obj }, msg) | |||
| 39 | + | }); | |||
| 40 | + | } | |||
| 41 | + | return this.moduleLoggers.get(module)!; | |||
| 42 | + | } | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | export interface Logger { | |||
| 46 | + | debug: (msg: string, obj?: object) => void; | |||
| 47 | + | info: (msg: string, obj?: object) => void; | |||
| 48 | + | warn: (msg: string, obj?: object) => void; | |||
| 49 | + | error: (msg: string, obj?: object) => void; | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | // Export a function to get a logger for a specific module | |||
| 53 | + | export const createLogger = (module: string): Logger => { | |||
| 54 | + | return LoggerService.getInstance().getLogger(module); | |||
| 55 | + | }; ⏎̸ | |||
M src/routes/api/chat/messages/+server.ts +68 -46
| @@ -15,6 +15,9 @@ import { chatRooms, DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | |||
| 15 | 15 | import { users } from '$lib/db/schema'; | |
| 16 | 16 | import { ensureDefaultChatRoom } from '$lib/db/schema'; | |
| 17 | 17 | import { messageCache } from '$lib/cache/message-cache'; | |
| 18 | + | import { createLogger } from '$lib/utils/logger.server'; | |
| 19 | + | ||
| 20 | + | const log = createLogger('chat-server'); | |
| 18 | 21 | ||
| 19 | 22 | // Rate limiting configuration | |
| 20 | 23 | const INITIAL_COOLDOWN = 1000; // 1 second | |
| @@ -54,7 +57,7 @@ async function initializeCache() { | |||
| 54 | 57 | if (isCacheInitialized) return; | |
| 55 | 58 | ||
| 56 | 59 | try { | |
| 57 | - | console.log('[Chat] Initializing message cache'); | |
| 60 | + | log.info('Initializing message cache'); | |
| 58 | 61 | // Fetch messages from all rooms, limited to last 100 per room | |
| 59 | 62 | const fetchedMessages = await db.select() | |
| 60 | 63 | .from(messages) | |
| @@ -66,12 +69,12 @@ async function initializeCache() { | |||
| 66 | 69 | ||
| 67 | 70 | // Log cache stats after initialization | |
| 68 | 71 | const stats = messageCache.getCacheStats(); | |
| 69 | - | console.log('[Chat] Message cache initialized successfully:', { | |
| 72 | + | log.info('Message cache initialized successfully', { | |
| 70 | 73 | roomCount: stats.roomCount, | |
| 71 | 74 | totalMessages: stats.totalMessages | |
| 72 | 75 | }); | |
| 73 | 76 | } catch (error) { | |
| 74 | - | console.error('[Chat] Failed to initialize message cache:', error); | |
| 77 | + | log.error('Failed to initialize message cache', { error }); | |
| 75 | 78 | // Don't set isCacheInitialized to true on error | |
| 76 | 79 | } | |
| 77 | 80 | } | |
| @@ -79,47 +82,44 @@ async function initializeCache() { | |||
| 79 | 82 | // GET endpoint: fetch messages from cache or DB | |
| 80 | 83 | export async function GET({ request, locals }) { | |
| 81 | 84 | const url = new URL(request.url); | |
| 82 | - | const isPublic = url.searchParams.get('public') === 'true'; | |
| 83 | - | const roomId = url.searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID; | |
| 84 | 85 | const beforeTimestamp = url.searchParams.get('before'); | |
| 85 | - | ||
| 86 | - | if (!isPublic && !locals.user) { | |
| 87 | - | throw error(401, 'Authentication required'); | |
| 88 | - | } | |
| 86 | + | const roomId = url.searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID; | |
| 87 | + | let fetchedMessages: Message[] = []; | |
| 89 | 88 | ||
| 90 | 89 | try { | |
| 91 | 90 | // Ensure default chat room exists | |
| 92 | 91 | await ensureDefaultChatRoom(db); | |
| 93 | - | ||
| 94 | - | let fetchedMessages: Message[]; | |
| 95 | - | if (isPublic) { | |
| 96 | - | if (!isCacheInitialized) { | |
| 97 | - | await initializeCache(); | |
| 98 | - | } | |
| 99 | - | ||
| 100 | - | if (beforeTimestamp) { | |
| 101 | - | // For public users requesting older messages, fetch from DB with limit | |
| 102 | - | console.log('[Chat] Fetching public messages from DB before timestamp:', beforeTimestamp); | |
| 103 | - | const query = db.select() | |
| 104 | - | .from(messages) | |
| 105 | - | .where(and( | |
| 106 | - | eq(messages.chatRoomId, roomId), | |
| 107 | - | lt(messages.timestamp, parseInt(beforeTimestamp)) | |
| 108 | - | )) | |
| 109 | - | .orderBy(desc(messages.timestamp)) | |
| 110 | - | .limit(10); | |
| 111 | - | fetchedMessages = (await query).reverse(); | |
| 112 | - | } else { | |
| 113 | - | console.log('[Chat] Fetching public messages from cache', { roomId }); | |
| 114 | - | fetchedMessages = messageCache.getMessages(roomId, 10); | |
| 92 | + | ||
| 93 | + | // Initialize cache if needed | |
| 94 | + | if (!isCacheInitialized) { | |
| 95 | + | await initializeCache(); | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // Try to get messages from cache first | |
| 99 | + | if (!beforeTimestamp) { | |
| 100 | + | log.debug('Attempting to fetch messages from cache', { roomId }); | |
| 101 | + | const cachedMessages = messageCache.getMessages(roomId); | |
| 102 | + | if (cachedMessages && cachedMessages.length > 0) { | |
| 103 | + | log.debug('Messages found in cache', { | |
| 104 | + | roomId, | |
| 105 | + | messageCount: cachedMessages.length | |
| 106 | + | }); | |
| 107 | + | fetchedMessages = cachedMessages; | |
| 115 | 108 | } | |
| 116 | - | } else { | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | // If no cached messages or pagination request, fetch from DB | |
| 112 | + | if (fetchedMessages.length === 0) { | |
| 117 | 113 | if (!locals.user) { | |
| 114 | + | log.warn('Authentication required'); | |
| 118 | 115 | throw error(401, 'Authentication required'); | |
| 119 | 116 | } | |
| 120 | 117 | ||
| 121 | 118 | if (beforeTimestamp) { | |
| 122 | - | console.log('[Chat] Fetching messages from DB before timestamp:', beforeTimestamp); | |
| 119 | + | log.debug('Fetching messages from DB before timestamp', { | |
| 120 | + | roomId, | |
| 121 | + | beforeTimestamp | |
| 122 | + | }); | |
| 123 | 123 | const query = db.select() | |
| 124 | 124 | .from(messages) | |
| 125 | 125 | .where(and( | |
| @@ -130,7 +130,7 @@ export async function GET({ request, locals }) { | |||
| 130 | 130 | .limit(100); | |
| 131 | 131 | fetchedMessages = (await query).reverse(); | |
| 132 | 132 | } else { | |
| 133 | - | console.log('[Chat] Fetching messages from DB for instant chat', { roomId }); | |
| 133 | + | log.debug('Fetching messages from DB for instant chat', { roomId }); | |
| 134 | 134 | const query = db.select() | |
| 135 | 135 | .from(messages) | |
| 136 | 136 | .where(eq(messages.chatRoomId, roomId)) | |
| @@ -153,7 +153,10 @@ export async function GET({ request, locals }) { | |||
| 153 | 153 | } | |
| 154 | 154 | ); | |
| 155 | 155 | } catch (error) { | |
| 156 | - | console.log('[Chat] Error fetching messages:', error); | |
| 156 | + | log.error('Error fetching messages', { | |
| 157 | + | error: error instanceof Error ? error.message : 'Unknown error', | |
| 158 | + | roomId | |
| 159 | + | }); | |
| 157 | 160 | const errorResponse: GetMessagesResponse = { | |
| 158 | 161 | success: false, | |
| 159 | 162 | error: 'Failed to fetch messages' | |
| @@ -171,16 +174,20 @@ export async function GET({ request, locals }) { | |||
| 171 | 174 | // POST endpoint: receive a new message, save it in the DB, and broadcast via SSE | |
| 172 | 175 | export async function POST({ request, locals }: { request: Request, locals: App.Locals }) { | |
| 173 | 176 | if (!locals.user) { | |
| 177 | + | log.warn('Authentication required'); | |
| 174 | 178 | throw error(401, 'Authentication required'); | |
| 175 | 179 | } | |
| 176 | 180 | ||
| 177 | - | console.log('[Chat] New message received'); | |
| 181 | + | log.debug('New message received'); | |
| 178 | 182 | try { | |
| 179 | 183 | // Check rate limiting | |
| 180 | 184 | const { canSend, retryAfter } = updateUserCooldown(locals.user.id); | |
| 181 | 185 | if (!canSend) { | |
| 182 | 186 | const maskedUserId = `${locals.user.id.slice(0, 4)}...${locals.user.id.slice(-4)}`; | |
| 183 | - | console.log('[Chat] Rate limited:', { userId: maskedUserId, retryAfter }); | |
| 187 | + | log.warn('Rate limited', { | |
| 188 | + | userId: maskedUserId, | |
| 189 | + | retryAfter | |
| 190 | + | }); | |
| 184 | 191 | const errorResponse: SendMessageResponse = { | |
| 185 | 192 | success: false, | |
| 186 | 193 | error: 'Please wait before sending another message', | |
| @@ -201,7 +208,10 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 201 | 208 | ||
| 202 | 209 | const data = await request.json() as SendMessageRequest; | |
| 203 | 210 | if (!data.content || !data.userId) { | |
| 204 | - | console.log('[Chat] Invalid payload received'); | |
| 211 | + | log.warn('Invalid payload received', { | |
| 212 | + | hasContent: Boolean(data.content), | |
| 213 | + | hasUserId: Boolean(data.userId) | |
| 214 | + | }); | |
| 205 | 215 | const errorResponse: SendMessageResponse = { | |
| 206 | 216 | success: false, | |
| 207 | 217 | error: 'Invalid message payload' | |
| @@ -210,6 +220,10 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 210 | 220 | } | |
| 211 | 221 | ||
| 212 | 222 | if (data.userId !== locals.user.id) { | |
| 223 | + | log.warn('Unauthorized message attempt', { | |
| 224 | + | requestedUserId: data.userId, | |
| 225 | + | actualUserId: `${locals.user.id.slice(0, 4)}...${locals.user.id.slice(-4)}` | |
| 226 | + | }); | |
| 213 | 227 | throw error(403, 'Cannot post messages as another user'); | |
| 214 | 228 | } | |
| 215 | 229 | ||
| @@ -223,7 +237,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 223 | 237 | .get(); | |
| 224 | 238 | ||
| 225 | 239 | if (!chatRoom) { | |
| 226 | - | console.log('[Chat] Chat room not found:', chatRoomId); | |
| 240 | + | log.warn('Chat room not found', { chatRoomId }); | |
| 227 | 241 | const errorResponse: SendMessageResponse = { | |
| 228 | 242 | success: false, | |
| 229 | 243 | error: 'Chat room not found' | |
| @@ -239,7 +253,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 239 | 253 | ||
| 240 | 254 | if (!user) { | |
| 241 | 255 | const maskedUserId = `${data.userId.slice(0, 4)}...${data.userId.slice(-4)}`; | |
| 242 | - | console.log('[Chat] User not found:', maskedUserId); | |
| 256 | + | log.warn('User not found', { maskedUserId }); | |
| 243 | 257 | const errorResponse: SendMessageResponse = { | |
| 244 | 258 | success: false, | |
| 245 | 259 | error: 'User not found' | |
| @@ -249,7 +263,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 249 | 263 | ||
| 250 | 264 | const newMessage: Message = { | |
| 251 | 265 | id: uuidv4(), | |
| 252 | - | chatRoomId: data.chatRoomId || DEFAULT_CHAT_ROOM_ID, | |
| 266 | + | chatRoomId, | |
| 253 | 267 | senderId: data.userId, | |
| 254 | 268 | content: data.content, | |
| 255 | 269 | type: data.type || 'chat', | |
| @@ -258,7 +272,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 258 | 272 | ||
| 259 | 273 | // Save to DB | |
| 260 | 274 | await db.insert(messages).values(newMessage); | |
| 261 | - | console.log('[Chat] Message saved in DB', { | |
| 275 | + | log.debug('Message saved in DB', { | |
| 262 | 276 | messageId: newMessage.id, | |
| 263 | 277 | chatRoomId: newMessage.chatRoomId, | |
| 264 | 278 | type: newMessage.type, | |
| @@ -267,7 +281,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 267 | 281 | ||
| 268 | 282 | // Update message cache | |
| 269 | 283 | messageCache.addMessage(newMessage); | |
| 270 | - | console.debug('[Chat] Message added to cache', { | |
| 284 | + | log.debug('Message added to cache', { | |
| 271 | 285 | messageId: newMessage.id, | |
| 272 | 286 | chatRoomId: newMessage.chatRoomId | |
| 273 | 287 | }); | |
| @@ -275,6 +289,12 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 275 | 289 | // Broadcast the new message via the unified SSE emitter | |
| 276 | 290 | sseEmitter.emit('sse', { type: 'chatMessage', data: newMessage }); | |
| 277 | 291 | ||
| 292 | + | log.debug('Message processed successfully', { | |
| 293 | + | messageId: newMessage.id, | |
| 294 | + | userId: `${newMessage.senderId.slice(0, 4)}...${newMessage.senderId.slice(-4)}`, | |
| 295 | + | roomId: newMessage.chatRoomId | |
| 296 | + | }); | |
| 297 | + | ||
| 278 | 298 | const successResponse: SendMessageResponse = { | |
| 279 | 299 | success: true, | |
| 280 | 300 | message: newMessage | |
| @@ -287,8 +307,10 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 287 | 307 | headers: { 'Content-Type': 'application/json' } | |
| 288 | 308 | } | |
| 289 | 309 | ); | |
| 290 | - | } catch { | |
| 291 | - | console.log('[Chat] Error processing message'); | |
| 310 | + | } catch (error) { | |
| 311 | + | log.error('Error processing message', { | |
| 312 | + | error: error instanceof Error ? error.message : 'Unknown error' | |
| 313 | + | }); | |
| 292 | 314 | const errorResponse: SendMessageResponse = { | |
| 293 | 315 | success: false, | |
| 294 | 316 | error: 'Failed to save message' | |
| @@ -301,4 +323,4 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 301 | 323 | } | |
| 302 | 324 | ); | |
| 303 | 325 | } | |
| 304 | - | } ⏎̸ | |
| 326 | + | }⏎̸ | |
| @@ -15,6 +15,9 @@ import { chatRooms, DEFAULT_CHAT_ROOM_ID } from '$lib/db/schema'; | |||||
| 15 | import { users } from '$lib/db/schema'; | 15 | import { users } from '$lib/db/schema'; | ||
| 16 | import { ensureDefaultChatRoom } from '$lib/db/schema'; | 16 | import { ensureDefaultChatRoom } from '$lib/db/schema'; | ||
| 17 | import { messageCache } from '$lib/cache/message-cache'; | 17 | import { messageCache } from '$lib/cache/message-cache'; | ||
| 18 | + | import { createLogger } from '$lib/utils/logger.server'; | |||
| 19 | + | ||||
| 20 | + | const log = createLogger('chat-server'); | |||
| 18 | 21 | ||||
| 19 | // Rate limiting configuration | 22 | // Rate limiting configuration | ||
| 20 | const INITIAL_COOLDOWN = 1000; // 1 second | 23 | const INITIAL_COOLDOWN = 1000; // 1 second | ||
| @@ -54,7 +57,7 @@ async function initializeCache() { | |||||
| 54 | if (isCacheInitialized) return; | 57 | if (isCacheInitialized) return; | ||
| 55 | 58 | ||||
| 56 | try { | 59 | try { | ||
| 57 | - | console.log('[Chat] Initializing message cache'); | 60 | + | log.info('Initializing message cache'); |
| 58 | // Fetch messages from all rooms, limited to last 100 per room | 61 | // Fetch messages from all rooms, limited to last 100 per room | ||
| 59 | const fetchedMessages = await db.select() | 62 | const fetchedMessages = await db.select() | ||
| 60 | .from(messages) | 63 | .from(messages) | ||
| @@ -66,12 +69,12 @@ async function initializeCache() { | |||||
| 66 | 69 | ||||
| 67 | // Log cache stats after initialization | 70 | // Log cache stats after initialization | ||
| 68 | const stats = messageCache.getCacheStats(); | 71 | const stats = messageCache.getCacheStats(); | ||
| 69 | - | console.log('[Chat] Message cache initialized successfully:', { | 72 | + | log.info('Message cache initialized successfully', { |
| 70 | roomCount: stats.roomCount, | 73 | roomCount: stats.roomCount, | ||
| 71 | totalMessages: stats.totalMessages | 74 | totalMessages: stats.totalMessages | ||
| 72 | }); | 75 | }); | ||
| 73 | } catch (error) { | 76 | } catch (error) { | ||
| 74 | - | console.error('[Chat] Failed to initialize message cache:', error); | 77 | + | log.error('Failed to initialize message cache', { error }); |
| 75 | // Don't set isCacheInitialized to true on error | 78 | // Don't set isCacheInitialized to true on error | ||
| 76 | } | 79 | } | ||
| 77 | } | 80 | } | ||
| @@ -79,47 +82,44 @@ async function initializeCache() { | |||||
| 79 | // GET endpoint: fetch messages from cache or DB | 82 | // GET endpoint: fetch messages from cache or DB | ||
| 80 | export async function GET({ request, locals }) { | 83 | export async function GET({ request, locals }) { | ||
| 81 | const url = new URL(request.url); | 84 | const url = new URL(request.url); | ||
| 82 | - | const isPublic = url.searchParams.get('public') === 'true'; | |||
| 83 | - | const roomId = url.searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID; | |||
| 84 | const beforeTimestamp = url.searchParams.get('before'); | 85 | const beforeTimestamp = url.searchParams.get('before'); | ||
| 85 | - | 86 | + | const roomId = url.searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID; | |
| 86 | - | if (!isPublic && !locals.user) { | 87 | + | let fetchedMessages: Message[] = []; |
| 87 | - | throw error(401, 'Authentication required'); | |||
| 88 | - | } | |||
| 89 | 88 | ||||
| 90 | try { | 89 | try { | ||
| 91 | // Ensure default chat room exists | 90 | // Ensure default chat room exists | ||
| 92 | await ensureDefaultChatRoom(db); | 91 | await ensureDefaultChatRoom(db); | ||
| 93 | - | 92 | + | ||
| 94 | - | let fetchedMessages: Message[]; | 93 | + | // Initialize cache if needed |
| 95 | - | if (isPublic) { | 94 | + | if (!isCacheInitialized) { |
| 96 | - | if (!isCacheInitialized) { | 95 | + | await initializeCache(); |
| 97 | - | await initializeCache(); | 96 | + | } |
| 98 | - | } | 97 | + | |
| 99 | - | 98 | + | // Try to get messages from cache first | |
| 100 | - | if (beforeTimestamp) { | 99 | + | if (!beforeTimestamp) { |
| 101 | - | // For public users requesting older messages, fetch from DB with limit | 100 | + | log.debug('Attempting to fetch messages from cache', { roomId }); |
| 102 | - | console.log('[Chat] Fetching public messages from DB before timestamp:', beforeTimestamp); | 101 | + | const cachedMessages = messageCache.getMessages(roomId); |
| 103 | - | const query = db.select() | 102 | + | if (cachedMessages && cachedMessages.length > 0) { |
| 104 | - | .from(messages) | 103 | + | log.debug('Messages found in cache', { |
| 105 | - | .where(and( | 104 | + | roomId, |
| 106 | - | eq(messages.chatRoomId, roomId), | 105 | + | messageCount: cachedMessages.length |
| 107 | - | lt(messages.timestamp, parseInt(beforeTimestamp)) | 106 | + | }); |
| 108 | - | )) | 107 | + | fetchedMessages = cachedMessages; |
| 109 | - | .orderBy(desc(messages.timestamp)) | |||
| 110 | - | .limit(10); | |||
| 111 | - | fetchedMessages = (await query).reverse(); | |||
| 112 | - | } else { | |||
| 113 | - | console.log('[Chat] Fetching public messages from cache', { roomId }); | |||
| 114 | - | fetchedMessages = messageCache.getMessages(roomId, 10); | |||
| 115 | } | 108 | } | ||
| 116 | - | } else { | 109 | + | } |
| 110 | + | ||||
| 111 | + | // If no cached messages or pagination request, fetch from DB | |||
| 112 | + | if (fetchedMessages.length === 0) { | |||
| 117 | if (!locals.user) { | 113 | if (!locals.user) { | ||
| 114 | + | log.warn('Authentication required'); | |||
| 118 | throw error(401, 'Authentication required'); | 115 | throw error(401, 'Authentication required'); | ||
| 119 | } | 116 | } | ||
| 120 | 117 | ||||
| 121 | if (beforeTimestamp) { | 118 | if (beforeTimestamp) { | ||
| 122 | - | console.log('[Chat] Fetching messages from DB before timestamp:', beforeTimestamp); | 119 | + | log.debug('Fetching messages from DB before timestamp', { |
| 120 | + | roomId, | |||
| 121 | + | beforeTimestamp | |||
| 122 | + | }); | |||
| 123 | const query = db.select() | 123 | const query = db.select() | ||
| 124 | .from(messages) | 124 | .from(messages) | ||
| 125 | .where(and( | 125 | .where(and( | ||
| @@ -130,7 +130,7 @@ export async function GET({ request, locals }) { | |||||
| 130 | .limit(100); | 130 | .limit(100); | ||
| 131 | fetchedMessages = (await query).reverse(); | 131 | fetchedMessages = (await query).reverse(); | ||
| 132 | } else { | 132 | } else { | ||
| 133 | - | console.log('[Chat] Fetching messages from DB for instant chat', { roomId }); | 133 | + | log.debug('Fetching messages from DB for instant chat', { roomId }); |
| 134 | const query = db.select() | 134 | const query = db.select() | ||
| 135 | .from(messages) | 135 | .from(messages) | ||
| 136 | .where(eq(messages.chatRoomId, roomId)) | 136 | .where(eq(messages.chatRoomId, roomId)) | ||
| @@ -153,7 +153,10 @@ export async function GET({ request, locals }) { | |||||
| 153 | } | 153 | } | ||
| 154 | ); | 154 | ); | ||
| 155 | } catch (error) { | 155 | } catch (error) { | ||
| 156 | - | console.log('[Chat] Error fetching messages:', error); | 156 | + | log.error('Error fetching messages', { |
| 157 | + | error: error instanceof Error ? error.message : 'Unknown error', | |||
| 158 | + | roomId | |||
| 159 | + | }); | |||
| 157 | const errorResponse: GetMessagesResponse = { | 160 | const errorResponse: GetMessagesResponse = { | ||
| 158 | success: false, | 161 | success: false, | ||
| 159 | error: 'Failed to fetch messages' | 162 | error: 'Failed to fetch messages' | ||
| @@ -171,16 +174,20 @@ export async function GET({ request, locals }) { | |||||
| 171 | // POST endpoint: receive a new message, save it in the DB, and broadcast via SSE | 174 | // POST endpoint: receive a new message, save it in the DB, and broadcast via SSE | ||
| 172 | export async function POST({ request, locals }: { request: Request, locals: App.Locals }) { | 175 | export async function POST({ request, locals }: { request: Request, locals: App.Locals }) { | ||
| 173 | if (!locals.user) { | 176 | if (!locals.user) { | ||
| 177 | + | log.warn('Authentication required'); | |||
| 174 | throw error(401, 'Authentication required'); | 178 | throw error(401, 'Authentication required'); | ||
| 175 | } | 179 | } | ||
| 176 | 180 | ||||
| 177 | - | console.log('[Chat] New message received'); | 181 | + | log.debug('New message received'); |
| 178 | try { | 182 | try { | ||
| 179 | // Check rate limiting | 183 | // Check rate limiting | ||
| 180 | const { canSend, retryAfter } = updateUserCooldown(locals.user.id); | 184 | const { canSend, retryAfter } = updateUserCooldown(locals.user.id); | ||
| 181 | if (!canSend) { | 185 | if (!canSend) { | ||
| 182 | const maskedUserId = `${locals.user.id.slice(0, 4)}...${locals.user.id.slice(-4)}`; | 186 | const maskedUserId = `${locals.user.id.slice(0, 4)}...${locals.user.id.slice(-4)}`; | ||
| 183 | - | console.log('[Chat] Rate limited:', { userId: maskedUserId, retryAfter }); | 187 | + | log.warn('Rate limited', { |
| 188 | + | userId: maskedUserId, | |||
| 189 | + | retryAfter | |||
| 190 | + | }); | |||
| 184 | const errorResponse: SendMessageResponse = { | 191 | const errorResponse: SendMessageResponse = { | ||
| 185 | success: false, | 192 | success: false, | ||
| 186 | error: 'Please wait before sending another message', | 193 | error: 'Please wait before sending another message', | ||
| @@ -201,7 +208,10 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 201 | 208 | ||||
| 202 | const data = await request.json() as SendMessageRequest; | 209 | const data = await request.json() as SendMessageRequest; | ||
| 203 | if (!data.content || !data.userId) { | 210 | if (!data.content || !data.userId) { | ||
| 204 | - | console.log('[Chat] Invalid payload received'); | 211 | + | log.warn('Invalid payload received', { |
| 212 | + | hasContent: Boolean(data.content), | |||
| 213 | + | hasUserId: Boolean(data.userId) | |||
| 214 | + | }); | |||
| 205 | const errorResponse: SendMessageResponse = { | 215 | const errorResponse: SendMessageResponse = { | ||
| 206 | success: false, | 216 | success: false, | ||
| 207 | error: 'Invalid message payload' | 217 | error: 'Invalid message payload' | ||
| @@ -210,6 +220,10 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 210 | } | 220 | } | ||
| 211 | 221 | ||||
| 212 | if (data.userId !== locals.user.id) { | 222 | if (data.userId !== locals.user.id) { | ||
| 223 | + | log.warn('Unauthorized message attempt', { | |||
| 224 | + | requestedUserId: data.userId, | |||
| 225 | + | actualUserId: `${locals.user.id.slice(0, 4)}...${locals.user.id.slice(-4)}` | |||
| 226 | + | }); | |||
| 213 | throw error(403, 'Cannot post messages as another user'); | 227 | throw error(403, 'Cannot post messages as another user'); | ||
| 214 | } | 228 | } | ||
| 215 | 229 | ||||
| @@ -223,7 +237,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 223 | .get(); | 237 | .get(); | ||
| 224 | 238 | ||||
| 225 | if (!chatRoom) { | 239 | if (!chatRoom) { | ||
| 226 | - | console.log('[Chat] Chat room not found:', chatRoomId); | 240 | + | log.warn('Chat room not found', { chatRoomId }); |
| 227 | const errorResponse: SendMessageResponse = { | 241 | const errorResponse: SendMessageResponse = { | ||
| 228 | success: false, | 242 | success: false, | ||
| 229 | error: 'Chat room not found' | 243 | error: 'Chat room not found' | ||
| @@ -239,7 +253,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 239 | 253 | ||||
| 240 | if (!user) { | 254 | if (!user) { | ||
| 241 | const maskedUserId = `${data.userId.slice(0, 4)}...${data.userId.slice(-4)}`; | 255 | const maskedUserId = `${data.userId.slice(0, 4)}...${data.userId.slice(-4)}`; | ||
| 242 | - | console.log('[Chat] User not found:', maskedUserId); | 256 | + | log.warn('User not found', { maskedUserId }); |
| 243 | const errorResponse: SendMessageResponse = { | 257 | const errorResponse: SendMessageResponse = { | ||
| 244 | success: false, | 258 | success: false, | ||
| 245 | error: 'User not found' | 259 | error: 'User not found' | ||
| @@ -249,7 +263,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 249 | 263 | ||||
| 250 | const newMessage: Message = { | 264 | const newMessage: Message = { | ||
| 251 | id: uuidv4(), | 265 | id: uuidv4(), | ||
| 252 | - | chatRoomId: data.chatRoomId || DEFAULT_CHAT_ROOM_ID, | 266 | + | chatRoomId, |
| 253 | senderId: data.userId, | 267 | senderId: data.userId, | ||
| 254 | content: data.content, | 268 | content: data.content, | ||
| 255 | type: data.type || 'chat', | 269 | type: data.type || 'chat', | ||
| @@ -258,7 +272,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 258 | 272 | ||||
| 259 | // Save to DB | 273 | // Save to DB | ||
| 260 | await db.insert(messages).values(newMessage); | 274 | await db.insert(messages).values(newMessage); | ||
| 261 | - | console.log('[Chat] Message saved in DB', { | 275 | + | log.debug('Message saved in DB', { |
| 262 | messageId: newMessage.id, | 276 | messageId: newMessage.id, | ||
| 263 | chatRoomId: newMessage.chatRoomId, | 277 | chatRoomId: newMessage.chatRoomId, | ||
| 264 | type: newMessage.type, | 278 | type: newMessage.type, | ||
| @@ -267,7 +281,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 267 | 281 | ||||
| 268 | // Update message cache | 282 | // Update message cache | ||
| 269 | messageCache.addMessage(newMessage); | 283 | messageCache.addMessage(newMessage); | ||
| 270 | - | console.debug('[Chat] Message added to cache', { | 284 | + | log.debug('Message added to cache', { |
| 271 | messageId: newMessage.id, | 285 | messageId: newMessage.id, | ||
| 272 | chatRoomId: newMessage.chatRoomId | 286 | chatRoomId: newMessage.chatRoomId | ||
| 273 | }); | 287 | }); | ||
| @@ -275,6 +289,12 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 275 | // Broadcast the new message via the unified SSE emitter | 289 | // Broadcast the new message via the unified SSE emitter | ||
| 276 | sseEmitter.emit('sse', { type: 'chatMessage', data: newMessage }); | 290 | sseEmitter.emit('sse', { type: 'chatMessage', data: newMessage }); | ||
| 277 | 291 | ||||
| 292 | + | log.debug('Message processed successfully', { | |||
| 293 | + | messageId: newMessage.id, | |||
| 294 | + | userId: `${newMessage.senderId.slice(0, 4)}...${newMessage.senderId.slice(-4)}`, | |||
| 295 | + | roomId: newMessage.chatRoomId | |||
| 296 | + | }); | |||
| 297 | + | ||||
| 278 | const successResponse: SendMessageResponse = { | 298 | const successResponse: SendMessageResponse = { | ||
| 279 | success: true, | 299 | success: true, | ||
| 280 | message: newMessage | 300 | message: newMessage | ||
| @@ -287,8 +307,10 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 287 | headers: { 'Content-Type': 'application/json' } | 307 | headers: { 'Content-Type': 'application/json' } | ||
| 288 | } | 308 | } | ||
| 289 | ); | 309 | ); | ||
| 290 | - | } catch { | 310 | + | } catch (error) { |
| 291 | - | console.log('[Chat] Error processing message'); | 311 | + | log.error('Error processing message', { |
| 312 | + | error: error instanceof Error ? error.message : 'Unknown error' | |||
| 313 | + | }); | |||
| 292 | const errorResponse: SendMessageResponse = { | 314 | const errorResponse: SendMessageResponse = { | ||
| 293 | success: false, | 315 | success: false, | ||
| 294 | error: 'Failed to save message' | 316 | error: 'Failed to save message' | ||
| @@ -301,4 +323,4 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 301 | } | 323 | } | ||
| 302 | ); | 324 | ); | ||
| 303 | } | 325 | } | ||
| 304 | - | } ⏎̸ | 326 | + | }⏎̸ |
M src/routes/api/rooms/[roomId]/+server.ts +10 -4
| @@ -4,7 +4,9 @@ import { messages, users } from '$lib/db/schema'; | |||
| 4 | 4 | import type { PublicRoomResponse } from '$lib/types/payloads'; | |
| 5 | 5 | import { error } from '@sveltejs/kit'; | |
| 6 | 6 | import { createSafeUser } from '$lib/types/chat'; | |
| 7 | + | import { createLogger } from '$lib/utils/logger.server'; | |
| 7 | 8 | ||
| 9 | + | const log = createLogger('rooms-server'); | |
| 8 | 10 | ||
| 9 | 11 | export async function GET({ params, url, locals }): Promise<Response> { | |
| 10 | 12 | const { roomId } = params; | |
| @@ -12,11 +14,12 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||
| 12 | 14 | ||
| 13 | 15 | // Check authentication for non-public requests | |
| 14 | 16 | if (!isPublic && !locals.user) { | |
| 17 | + | log.warn('Authentication required'); | |
| 15 | 18 | throw error(401, 'Authentication required'); | |
| 16 | 19 | } | |
| 17 | 20 | ||
| 18 | 21 | try { | |
| 19 | - | console.log('[Rooms] Fetching room data:', { | |
| 22 | + | log.debug('Fetching room data', { | |
| 20 | 23 | roomId, | |
| 21 | 24 | isPublic, | |
| 22 | 25 | requestedBy: locals.user ? `${locals.user.id.slice(0, 4)}...${locals.user.id.slice(-4)}` : 'public' | |
| @@ -47,7 +50,7 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||
| 47 | 50 | buddyList: sanitizedUsers | |
| 48 | 51 | }; | |
| 49 | 52 | ||
| 50 | - | console.log('[Rooms] Successfully fetched room data:', { | |
| 53 | + | log.debug('Successfully fetched room data', { | |
| 51 | 54 | roomId, | |
| 52 | 55 | userCount: sanitizedUsers.length, | |
| 53 | 56 | isPublic | |
| @@ -57,8 +60,11 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||
| 57 | 60 | status: 200, | |
| 58 | 61 | headers: { 'Content-Type': 'application/json' } | |
| 59 | 62 | }); | |
| 60 | - | } catch { | |
| 61 | - | console.log('[Rooms] Error fetching room data:', { roomId }); | |
| 63 | + | } catch (error) { | |
| 64 | + | log.error('Error fetching room data', { | |
| 65 | + | roomId, | |
| 66 | + | error: error instanceof Error ? error.message : 'Unknown error' | |
| 67 | + | }); | |
| 62 | 68 | const errorResponse: PublicRoomResponse = { | |
| 63 | 69 | success: false, | |
| 64 | 70 | error: 'Failed to fetch room data' | |
| @@ -4,7 +4,9 @@ import { messages, users } from '$lib/db/schema'; | |||||
| 4 | import type { PublicRoomResponse } from '$lib/types/payloads'; | 4 | import type { PublicRoomResponse } from '$lib/types/payloads'; | ||
| 5 | import { error } from '@sveltejs/kit'; | 5 | import { error } from '@sveltejs/kit'; | ||
| 6 | import { createSafeUser } from '$lib/types/chat'; | 6 | import { createSafeUser } from '$lib/types/chat'; | ||
| 7 | + | import { createLogger } from '$lib/utils/logger.server'; | |||
| 7 | 8 | ||||
| 9 | + | const log = createLogger('rooms-server'); | |||
| 8 | 10 | ||||
| 9 | export async function GET({ params, url, locals }): Promise<Response> { | 11 | export async function GET({ params, url, locals }): Promise<Response> { | ||
| 10 | const { roomId } = params; | 12 | const { roomId } = params; | ||
| @@ -12,11 +14,12 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||||
| 12 | 14 | ||||
| 13 | // Check authentication for non-public requests | 15 | // Check authentication for non-public requests | ||
| 14 | if (!isPublic && !locals.user) { | 16 | if (!isPublic && !locals.user) { | ||
| 17 | + | log.warn('Authentication required'); | |||
| 15 | throw error(401, 'Authentication required'); | 18 | throw error(401, 'Authentication required'); | ||
| 16 | } | 19 | } | ||
| 17 | 20 | ||||
| 18 | try { | 21 | try { | ||
| 19 | - | console.log('[Rooms] Fetching room data:', { | 22 | + | log.debug('Fetching room data', { |
| 20 | roomId, | 23 | roomId, | ||
| 21 | isPublic, | 24 | isPublic, | ||
| 22 | requestedBy: locals.user ? `${locals.user.id.slice(0, 4)}...${locals.user.id.slice(-4)}` : 'public' | 25 | requestedBy: locals.user ? `${locals.user.id.slice(0, 4)}...${locals.user.id.slice(-4)}` : 'public' | ||
| @@ -47,7 +50,7 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||||
| 47 | buddyList: sanitizedUsers | 50 | buddyList: sanitizedUsers | ||
| 48 | }; | 51 | }; | ||
| 49 | 52 | ||||
| 50 | - | console.log('[Rooms] Successfully fetched room data:', { | 53 | + | log.debug('Successfully fetched room data', { |
| 51 | roomId, | 54 | roomId, | ||
| 52 | userCount: sanitizedUsers.length, | 55 | userCount: sanitizedUsers.length, | ||
| 53 | isPublic | 56 | isPublic | ||
| @@ -57,8 +60,11 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||||
| 57 | status: 200, | 60 | status: 200, | ||
| 58 | headers: { 'Content-Type': 'application/json' } | 61 | headers: { 'Content-Type': 'application/json' } | ||
| 59 | }); | 62 | }); | ||
| 60 | - | } catch { | 63 | + | } catch (error) { |
| 61 | - | console.log('[Rooms] Error fetching room data:', { roomId }); | 64 | + | log.error('Error fetching room data', { |
| 65 | + | roomId, | |||
| 66 | + | error: error instanceof Error ? error.message : 'Unknown error' | |||
| 67 | + | }); | |||
| 62 | const errorResponse: PublicRoomResponse = { | 68 | const errorResponse: PublicRoomResponse = { | ||
| 63 | success: false, | 69 | success: false, | ||
| 64 | error: 'Failed to fetch room data' | 70 | error: 'Failed to fetch room data' | ||
M src/routes/api/session/login/+server.ts +28 -8
| @@ -7,6 +7,9 @@ import { verifyPassword } from '$lib/utils/password'; | |||
| 7 | 7 | import { generateSessionToken, createSession } from '$lib/api/session.server'; | |
| 8 | 8 | import { setSessionTokenCookie } from '$lib/api/session.cookie'; | |
| 9 | 9 | import { createSafeUser } from '$lib/types/chat'; | |
| 10 | + | import { createLogger } from '$lib/utils/logger.server'; | |
| 11 | + | ||
| 12 | + | const log = createLogger('login-server'); | |
| 10 | 13 | ||
| 11 | 14 | // In-memory map to track failed login attempts per IP | |
| 12 | 15 | const loginAttempts = new Map<string, { count: number, lastAttempt: number }>(); | |
| @@ -25,9 +28,10 @@ setInterval(() => { | |||
| 25 | 28 | }, 60 * 60 * 1000); | |
| 26 | 29 | ||
| 27 | 30 | export const POST: RequestHandler = async ({ request, cookies }) => { | |
| 28 | - | console.log("[Login] New login attempt received"); | |
| 31 | + | log.debug('New login attempt received'); | |
| 29 | 32 | ||
| 30 | 33 | if (request.method !== 'POST') { | |
| 34 | + | log.warn('Invalid method used', { method: request.method }); | |
| 31 | 35 | return new Response(JSON.stringify({ error: 'Method Not Allowed' } as LoginResponseError), { status: 405 }); | |
| 32 | 36 | } | |
| 33 | 37 | ||
| @@ -40,7 +44,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||
| 40 | 44 | // If the user has failed three or more times, apply exponential backoff | |
| 41 | 45 | if (attemptData.count >= MAX_ATTEMPTS) { | |
| 42 | 46 | const delay = Math.pow(2, attemptData.count - MAX_ATTEMPTS + 1) * 1000; // delay in milliseconds | |
| 43 | - | console.log(`[Login] Rate limit exceeded for IP ${maskedIp} - applying ${delay}ms delay`); | |
| 47 | + | log.warn('Rate limit exceeded', { maskedIp, delay }); | |
| 44 | 48 | await sleep(delay); | |
| 45 | 49 | } | |
| 46 | 50 | ||
| @@ -48,14 +52,17 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||
| 48 | 52 | try { | |
| 49 | 53 | body = await request.json(); | |
| 50 | 54 | } catch { | |
| 51 | - | console.log("[Login] Invalid JSON payload received"); | |
| 55 | + | log.warn('Invalid JSON payload received'); | |
| 52 | 56 | return new Response(JSON.stringify({ error: 'Invalid JSON' } as LoginResponseError), { status: 400 }); | |
| 53 | 57 | } | |
| 54 | 58 | ||
| 55 | 59 | const { username, password } = body as { username: string, password: string }; | |
| 56 | 60 | ||
| 57 | 61 | if (typeof username !== 'string' || typeof password !== 'string') { | |
| 58 | - | console.log("Missing or invalid input fields", { username, password }); | |
| 62 | + | log.warn('Missing or invalid input fields', { | |
| 63 | + | hasUsername: typeof username === 'string', | |
| 64 | + | hasPassword: typeof password === 'string' | |
| 65 | + | }); | |
| 59 | 66 | return new Response(JSON.stringify({ error: 'Missing or invalid input fields' } as LoginResponseError), { status: 400 }); | |
| 60 | 67 | } | |
| 61 | 68 | ||
| @@ -68,7 +75,11 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||
| 68 | 75 | attemptData.count++; | |
| 69 | 76 | attemptData.lastAttempt = now; | |
| 70 | 77 | loginAttempts.set(ip, attemptData); | |
| 71 | - | console.log(`[Login] Login failed for IP ${maskedIp}. Attempt count: ${attemptData.count}`); | |
| 78 | + | log.warn('Login failed - user not found', { | |
| 79 | + | maskedIp, | |
| 80 | + | attemptCount: attemptData.count, | |
| 81 | + | username: username.trim() | |
| 82 | + | }); | |
| 72 | 83 | const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count); | |
| 73 | 84 | return new Response(JSON.stringify({ | |
| 74 | 85 | error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` | |
| @@ -82,14 +93,18 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||
| 82 | 93 | attemptData.count++; | |
| 83 | 94 | attemptData.lastAttempt = now; | |
| 84 | 95 | loginAttempts.set(ip, attemptData); | |
| 85 | - | console.log(`[Login] Login failed for IP ${maskedIp}. Attempt count: ${attemptData.count}`); | |
| 96 | + | log.warn('Login failed - invalid password', { | |
| 97 | + | maskedIp, | |
| 98 | + | attemptCount: attemptData.count, | |
| 99 | + | userId: `${user.id.slice(0, 4)}...${user.id.slice(-4)}` | |
| 100 | + | }); | |
| 86 | 101 | const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count); | |
| 87 | 102 | return new Response(JSON.stringify({ | |
| 88 | 103 | error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` | |
| 89 | 104 | } as LoginResponseError), { status: 401 }); | |
| 90 | 105 | } | |
| 91 | - | } catch { | |
| 92 | - | console.log("[Login] Error verifying password"); | |
| 106 | + | } catch (error) { | |
| 107 | + | log.error('Error verifying password', { error }); | |
| 93 | 108 | return new Response(JSON.stringify({ error: 'Internal Server Error' } as LoginResponseError), { status: 500 }); | |
| 94 | 109 | } | |
| 95 | 110 | ||
| @@ -111,6 +126,11 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||
| 111 | 126 | // Set session cookie | |
| 112 | 127 | setSessionTokenCookie({ cookies }, token, session.expiresAt); | |
| 113 | 128 | ||
| 129 | + | log.info('Login successful', { | |
| 130 | + | userId: `${user.id.slice(0, 4)}...${user.id.slice(-4)}`, | |
| 131 | + | expiresAt: new Date(session.expiresAt).toISOString() | |
| 132 | + | }); | |
| 133 | + | ||
| 114 | 134 | return new Response(JSON.stringify({ | |
| 115 | 135 | success: true, | |
| 116 | 136 | user: createSafeUser(user) | |
| @@ -7,6 +7,9 @@ import { verifyPassword } from '$lib/utils/password'; | |||||
| 7 | import { generateSessionToken, createSession } from '$lib/api/session.server'; | 7 | import { generateSessionToken, createSession } from '$lib/api/session.server'; | ||
| 8 | import { setSessionTokenCookie } from '$lib/api/session.cookie'; | 8 | import { setSessionTokenCookie } from '$lib/api/session.cookie'; | ||
| 9 | import { createSafeUser } from '$lib/types/chat'; | 9 | import { createSafeUser } from '$lib/types/chat'; | ||
| 10 | + | import { createLogger } from '$lib/utils/logger.server'; | |||
| 11 | + | ||||
| 12 | + | const log = createLogger('login-server'); | |||
| 10 | 13 | ||||
| 11 | // In-memory map to track failed login attempts per IP | 14 | // In-memory map to track failed login attempts per IP | ||
| 12 | const loginAttempts = new Map<string, { count: number, lastAttempt: number }>(); | 15 | const loginAttempts = new Map<string, { count: number, lastAttempt: number }>(); | ||
| @@ -25,9 +28,10 @@ setInterval(() => { | |||||
| 25 | }, 60 * 60 * 1000); | 28 | }, 60 * 60 * 1000); | ||
| 26 | 29 | ||||
| 27 | export const POST: RequestHandler = async ({ request, cookies }) => { | 30 | export const POST: RequestHandler = async ({ request, cookies }) => { | ||
| 28 | - | console.log("[Login] New login attempt received"); | 31 | + | log.debug('New login attempt received'); |
| 29 | 32 | ||||
| 30 | if (request.method !== 'POST') { | 33 | if (request.method !== 'POST') { | ||
| 34 | + | log.warn('Invalid method used', { method: request.method }); | |||
| 31 | return new Response(JSON.stringify({ error: 'Method Not Allowed' } as LoginResponseError), { status: 405 }); | 35 | return new Response(JSON.stringify({ error: 'Method Not Allowed' } as LoginResponseError), { status: 405 }); | ||
| 32 | } | 36 | } | ||
| 33 | 37 | ||||
| @@ -40,7 +44,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||||
| 40 | // If the user has failed three or more times, apply exponential backoff | 44 | // If the user has failed three or more times, apply exponential backoff | ||
| 41 | if (attemptData.count >= MAX_ATTEMPTS) { | 45 | if (attemptData.count >= MAX_ATTEMPTS) { | ||
| 42 | const delay = Math.pow(2, attemptData.count - MAX_ATTEMPTS + 1) * 1000; // delay in milliseconds | 46 | const delay = Math.pow(2, attemptData.count - MAX_ATTEMPTS + 1) * 1000; // delay in milliseconds | ||
| 43 | - | console.log(`[Login] Rate limit exceeded for IP ${maskedIp} - applying ${delay}ms delay`); | 47 | + | log.warn('Rate limit exceeded', { maskedIp, delay }); |
| 44 | await sleep(delay); | 48 | await sleep(delay); | ||
| 45 | } | 49 | } | ||
| 46 | 50 | ||||
| @@ -48,14 +52,17 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||||
| 48 | try { | 52 | try { | ||
| 49 | body = await request.json(); | 53 | body = await request.json(); | ||
| 50 | } catch { | 54 | } catch { | ||
| 51 | - | console.log("[Login] Invalid JSON payload received"); | 55 | + | log.warn('Invalid JSON payload received'); |
| 52 | return new Response(JSON.stringify({ error: 'Invalid JSON' } as LoginResponseError), { status: 400 }); | 56 | return new Response(JSON.stringify({ error: 'Invalid JSON' } as LoginResponseError), { status: 400 }); | ||
| 53 | } | 57 | } | ||
| 54 | 58 | ||||
| 55 | const { username, password } = body as { username: string, password: string }; | 59 | const { username, password } = body as { username: string, password: string }; | ||
| 56 | 60 | ||||
| 57 | if (typeof username !== 'string' || typeof password !== 'string') { | 61 | if (typeof username !== 'string' || typeof password !== 'string') { | ||
| 58 | - | console.log("Missing or invalid input fields", { username, password }); | 62 | + | log.warn('Missing or invalid input fields', { |
| 63 | + | hasUsername: typeof username === 'string', | |||
| 64 | + | hasPassword: typeof password === 'string' | |||
| 65 | + | }); | |||
| 59 | return new Response(JSON.stringify({ error: 'Missing or invalid input fields' } as LoginResponseError), { status: 400 }); | 66 | return new Response(JSON.stringify({ error: 'Missing or invalid input fields' } as LoginResponseError), { status: 400 }); | ||
| 60 | } | 67 | } | ||
| 61 | 68 | ||||
| @@ -68,7 +75,11 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||||
| 68 | attemptData.count++; | 75 | attemptData.count++; | ||
| 69 | attemptData.lastAttempt = now; | 76 | attemptData.lastAttempt = now; | ||
| 70 | loginAttempts.set(ip, attemptData); | 77 | loginAttempts.set(ip, attemptData); | ||
| 71 | - | console.log(`[Login] Login failed for IP ${maskedIp}. Attempt count: ${attemptData.count}`); | 78 | + | log.warn('Login failed - user not found', { |
| 79 | + | maskedIp, | |||
| 80 | + | attemptCount: attemptData.count, | |||
| 81 | + | username: username.trim() | |||
| 82 | + | }); | |||
| 72 | const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count); | 83 | const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count); | ||
| 73 | return new Response(JSON.stringify({ | 84 | return new Response(JSON.stringify({ | ||
| 74 | error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` | 85 | error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` | ||
| @@ -82,14 +93,18 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||||
| 82 | attemptData.count++; | 93 | attemptData.count++; | ||
| 83 | attemptData.lastAttempt = now; | 94 | attemptData.lastAttempt = now; | ||
| 84 | loginAttempts.set(ip, attemptData); | 95 | loginAttempts.set(ip, attemptData); | ||
| 85 | - | console.log(`[Login] Login failed for IP ${maskedIp}. Attempt count: ${attemptData.count}`); | 96 | + | log.warn('Login failed - invalid password', { |
| 97 | + | maskedIp, | |||
| 98 | + | attemptCount: attemptData.count, | |||
| 99 | + | userId: `${user.id.slice(0, 4)}...${user.id.slice(-4)}` | |||
| 100 | + | }); | |||
| 86 | const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count); | 101 | const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count); | ||
| 87 | return new Response(JSON.stringify({ | 102 | return new Response(JSON.stringify({ | ||
| 88 | error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` | 103 | error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` | ||
| 89 | } as LoginResponseError), { status: 401 }); | 104 | } as LoginResponseError), { status: 401 }); | ||
| 90 | } | 105 | } | ||
| 91 | - | } catch { | 106 | + | } catch (error) { |
| 92 | - | console.log("[Login] Error verifying password"); | 107 | + | log.error('Error verifying password', { error }); |
| 93 | return new Response(JSON.stringify({ error: 'Internal Server Error' } as LoginResponseError), { status: 500 }); | 108 | return new Response(JSON.stringify({ error: 'Internal Server Error' } as LoginResponseError), { status: 500 }); | ||
| 94 | } | 109 | } | ||
| 95 | 110 | ||||
| @@ -111,6 +126,11 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||||
| 111 | // Set session cookie | 126 | // Set session cookie | ||
| 112 | setSessionTokenCookie({ cookies }, token, session.expiresAt); | 127 | setSessionTokenCookie({ cookies }, token, session.expiresAt); | ||
| 113 | 128 | ||||
| 129 | + | log.info('Login successful', { | |||
| 130 | + | userId: `${user.id.slice(0, 4)}...${user.id.slice(-4)}`, | |||
| 131 | + | expiresAt: new Date(session.expiresAt).toISOString() | |||
| 132 | + | }); | |||
| 133 | + | ||||
| 114 | return new Response(JSON.stringify({ | 134 | return new Response(JSON.stringify({ | ||
| 115 | success: true, | 135 | success: true, | ||
| 116 | user: createSafeUser(user) | 136 | user: createSafeUser(user) | ||
M src/routes/api/session/logout/+server.ts +6 -1
| @@ -1,6 +1,9 @@ | |||
| 1 | 1 | import { json } from '@sveltejs/kit'; | |
| 2 | 2 | import { invalidateSession } from '$lib/api/session.server'; | |
| 3 | 3 | import { sha256 } from '$lib/api/session.server'; // re-use sha256 helper to compute session id | |
| 4 | + | import { createLogger } from '$lib/utils/logger.server'; | |
| 5 | + | ||
| 6 | + | const log = createLogger('logout-server'); | |
| 4 | 7 | ||
| 5 | 8 | export async function POST({ cookies }) { | |
| 6 | 9 | const token = cookies.get('session'); | |
| @@ -9,7 +12,9 @@ export async function POST({ cookies }) { | |||
| 9 | 12 | const sessionId = await sha256(token); | |
| 10 | 13 | await invalidateSession(sessionId); | |
| 11 | 14 | cookies.delete('session', { path: '/' }); | |
| 12 | - | console.debug("Logged out session with id:", sessionId); | |
| 15 | + | log.info('Session invalidated', { sessionId: `${sessionId.slice(0, 4)}...${sessionId.slice(-4)}` }); | |
| 16 | + | } else { | |
| 17 | + | log.debug('No session token found in cookies'); | |
| 13 | 18 | } | |
| 14 | 19 | ||
| 15 | 20 | return json({ success: true }); | |
| @@ -1,6 +1,9 @@ | |||||
| 1 | import { json } from '@sveltejs/kit'; | 1 | import { json } from '@sveltejs/kit'; | ||
| 2 | import { invalidateSession } from '$lib/api/session.server'; | 2 | import { invalidateSession } from '$lib/api/session.server'; | ||
| 3 | import { sha256 } from '$lib/api/session.server'; // re-use sha256 helper to compute session id | 3 | import { sha256 } from '$lib/api/session.server'; // re-use sha256 helper to compute session id | ||
| 4 | + | import { createLogger } from '$lib/utils/logger.server'; | |||
| 5 | + | ||||
| 6 | + | const log = createLogger('logout-server'); | |||
| 4 | 7 | ||||
| 5 | export async function POST({ cookies }) { | 8 | export async function POST({ cookies }) { | ||
| 6 | const token = cookies.get('session'); | 9 | const token = cookies.get('session'); | ||
| @@ -9,7 +12,9 @@ export async function POST({ cookies }) { | |||||
| 9 | const sessionId = await sha256(token); | 12 | const sessionId = await sha256(token); | ||
| 10 | await invalidateSession(sessionId); | 13 | await invalidateSession(sessionId); | ||
| 11 | cookies.delete('session', { path: '/' }); | 14 | cookies.delete('session', { path: '/' }); | ||
| 12 | - | console.debug("Logged out session with id:", sessionId); | 15 | + | log.info('Session invalidated', { sessionId: `${sessionId.slice(0, 4)}...${sessionId.slice(-4)}` }); |
| 16 | + | } else { | |||
| 17 | + | log.debug('No session token found in cookies'); | |||
| 13 | } | 18 | } | ||
| 14 | 19 | ||||
| 15 | return json({ success: true }); | 20 | return json({ success: true }); | ||
M src/routes/api/session/validate/+server.ts +12 -2
| @@ -1,17 +1,27 @@ | |||
| 1 | 1 | import { json } from '@sveltejs/kit'; | |
| 2 | 2 | import { validateSessionToken } from '$lib/api/session.server'; | |
| 3 | + | import { createLogger } from '$lib/utils/logger.server'; | |
| 4 | + | ||
| 5 | + | const log = createLogger('validate-server'); | |
| 3 | 6 | ||
| 4 | 7 | export async function GET({ cookies }) { | |
| 5 | 8 | const token = cookies.get('session'); | |
| 6 | 9 | ||
| 7 | 10 | if (!token) { | |
| 8 | - | console.debug("No session token found in cookies."); | |
| 11 | + | log.debug('No session token found in cookies'); | |
| 9 | 12 | return json({ session: null, user: null }, { status: 401 }); | |
| 10 | 13 | } | |
| 11 | 14 | ||
| 12 | 15 | const result = await validateSessionToken(token); | |
| 13 | 16 | ||
| 14 | - | console.debug("Validate endpoint result:", result); | |
| 17 | + | if (result.session && result.user) { | |
| 18 | + | log.debug('Session validated', { | |
| 19 | + | userId: `${result.user.id.slice(0, 4)}...${result.user.id.slice(-4)}`, | |
| 20 | + | sessionId: `${result.session.id.slice(0, 4)}...${result.session.id.slice(-4)}` | |
| 21 | + | }); | |
| 22 | + | } else { | |
| 23 | + | log.debug('Invalid session token'); | |
| 24 | + | } | |
| 15 | 25 | ||
| 16 | 26 | return json(result); | |
| 17 | 27 | } ⏎̸ | |
| @@ -1,17 +1,27 @@ | |||||
| 1 | import { json } from '@sveltejs/kit'; | 1 | import { json } from '@sveltejs/kit'; | ||
| 2 | import { validateSessionToken } from '$lib/api/session.server'; | 2 | import { validateSessionToken } from '$lib/api/session.server'; | ||
| 3 | + | import { createLogger } from '$lib/utils/logger.server'; | |||
| 4 | + | ||||
| 5 | + | const log = createLogger('validate-server'); | |||
| 3 | 6 | ||||
| 4 | export async function GET({ cookies }) { | 7 | export async function GET({ cookies }) { | ||
| 5 | const token = cookies.get('session'); | 8 | const token = cookies.get('session'); | ||
| 6 | 9 | ||||
| 7 | if (!token) { | 10 | if (!token) { | ||
| 8 | - | console.debug("No session token found in cookies."); | 11 | + | log.debug('No session token found in cookies'); |
| 9 | return json({ session: null, user: null }, { status: 401 }); | 12 | return json({ session: null, user: null }, { status: 401 }); | ||
| 10 | } | 13 | } | ||
| 11 | 14 | ||||
| 12 | const result = await validateSessionToken(token); | 15 | const result = await validateSessionToken(token); | ||
| 13 | 16 | ||||
| 14 | - | console.debug("Validate endpoint result:", result); | 17 | + | if (result.session && result.user) { |
| 18 | + | log.debug('Session validated', { | |||
| 19 | + | userId: `${result.user.id.slice(0, 4)}...${result.user.id.slice(-4)}`, | |||
| 20 | + | sessionId: `${result.session.id.slice(0, 4)}...${result.session.id.slice(-4)}` | |||
| 21 | + | }); | |||
| 22 | + | } else { | |||
| 23 | + | log.debug('Invalid session token'); | |||
| 24 | + | } | |||
| 15 | 25 | ||||
| 16 | return json(result); | 26 | return json(result); | ||
| 17 | } ⏎̸ | 27 | } ⏎̸ | ||
M src/routes/api/sse/+server.ts +26 -26
| @@ -10,6 +10,9 @@ import db from '$lib/db/db.server'; | |||
| 10 | 10 | import { users } from '$lib/db/schema'; | |
| 11 | 11 | import { eq, and, lt } from 'drizzle-orm'; | |
| 12 | 12 | import { createSafeUser } from '$lib/types/chat'; | |
| 13 | + | import { createLogger } from '$lib/utils/logger.server'; | |
| 14 | + | ||
| 15 | + | const log = createLogger('sse-server'); | |
| 13 | 16 | ||
| 14 | 17 | console.log('[SSE] Server initialized'); | |
| 15 | 18 | ||
| @@ -35,7 +38,7 @@ function startTimeoutCheck() { | |||
| 35 | 38 | ||
| 36 | 39 | timeoutInterval = setInterval(async () => { | |
| 37 | 40 | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | |
| 38 | - | console.log('[SSE] Checking for timed out users', { | |
| 41 | + | log.debug('Checking for timed out users', { | |
| 39 | 42 | checkTime: new Date().toISOString(), | |
| 40 | 43 | timeoutThreshold: new Date(timeoutThreshold).toISOString() | |
| 41 | 44 | }); | |
| @@ -60,13 +63,13 @@ function startTimeoutCheck() { | |||
| 60 | 63 | }); | |
| 61 | 64 | ||
| 62 | 65 | if (result.length > 0) { | |
| 63 | - | console.log('[SSE] Found timed out users:', result.map(user => ({ | |
| 64 | - | nickname: user.nickname, | |
| 65 | - | lastSeen: new Date(user.lastSeen || 0).toISOString() | |
| 66 | - | }))); | |
| 66 | + | log.info('User status updated to offline', { | |
| 67 | + | usersUpdated: result.length, | |
| 68 | + | timeoutThreshold: new Date(timeoutThreshold).toISOString() | |
| 69 | + | }); | |
| 67 | 70 | } | |
| 68 | 71 | } catch (error) { | |
| 69 | - | console.error('[SSE] Error during timeout check:', error); | |
| 72 | + | log.error('Error during timeout check', { error }); | |
| 70 | 73 | } | |
| 71 | 74 | }, CHECK_INTERVAL_MS); | |
| 72 | 75 | } | |
| @@ -81,7 +84,7 @@ function startBuddyListUpdateInterval() { | |||
| 81 | 84 | const now = Date.now(); | |
| 82 | 85 | // Only fetch and broadcast if 10 seconds have passed since last update | |
| 83 | 86 | if (now - lastBuddyListUpdate >= BUDDY_LIST_BROADCAST_INTERVAL) { | |
| 84 | - | console.log('[SSE] Broadcasting buddy list update...'); | |
| 87 | + | log.debug('Broadcasting buddy list update...'); | |
| 85 | 88 | // Query the complete buddy list from the database. | |
| 86 | 89 | const buddyList = await db.select().from(users); | |
| 87 | 90 | // Use createSafeUser to properly sanitize user data | |
| @@ -98,7 +101,7 @@ function startBuddyListUpdateInterval() { | |||
| 98 | 101 | lastBuddyListUpdate = now; | |
| 99 | 102 | } | |
| 100 | 103 | } catch (error) { | |
| 101 | - | console.error('[SSE] Error fetching buddy list for update:', error); | |
| 104 | + | log.error('Error fetching buddy list for update', { error }); | |
| 102 | 105 | } | |
| 103 | 106 | }, 1000); // Check every second but only broadcast every 10 seconds | |
| 104 | 107 | } | |
| @@ -108,9 +111,9 @@ if (!globalThis.__sseIntervalsStarted) { | |||
| 108 | 111 | startTimeoutCheck(); | |
| 109 | 112 | startBuddyListUpdateInterval(); | |
| 110 | 113 | globalThis.__sseIntervalsStarted = true; | |
| 111 | - | console.log('[SSE] Started global intervals for timeout and buddy list updates.'); | |
| 114 | + | log.info('Started global intervals for timeout and buddy list updates'); | |
| 112 | 115 | } else { | |
| 113 | - | console.log('[SSE] Global intervals already started, skipping initialization.'); | |
| 116 | + | log.debug('Global intervals already started, skipping initialization'); | |
| 114 | 117 | } | |
| 115 | 118 | ||
| 116 | 119 | // Ensure the interval is cleared if the module is reloaded | |
| @@ -134,17 +137,14 @@ if (typeof process !== 'undefined') { | |||
| 134 | 137 | }); | |
| 135 | 138 | } | |
| 136 | 139 | ||
| 137 | - | export const GET: RequestHandler = async ({ request, locals, cookies }) => { | |
| 138 | - | console.log('[SSE] Connection attempt:', { | |
| 139 | - | hasLocals: !!locals, | |
| 140 | - | hasUser: !!locals.user, | |
| 141 | - | userId: locals.user?.id, | |
| 142 | - | hasSessionCookie: !!cookies.get('session'), | |
| 143 | - | headers: Object.fromEntries(request.headers.entries()) | |
| 140 | + | export const GET: RequestHandler = async ({ request, locals, }) => { | |
| 141 | + | log.debug('Processing SSE request', { | |
| 142 | + | url: request.url, | |
| 143 | + | userId: locals.user?.id | |
| 144 | 144 | }); | |
| 145 | 145 | ||
| 146 | 146 | if (!locals.user) { | |
| 147 | - | console.log('[SSE] Authentication failed - no user in locals'); | |
| 147 | + | log.warn('Authentication failed - no user in locals'); | |
| 148 | 148 | return new Response('Unauthorized', { | |
| 149 | 149 | status: 401, | |
| 150 | 150 | headers: { | |
| @@ -168,7 +168,7 @@ export const GET: RequestHandler = async ({ request, locals, cookies }) => { | |||
| 168 | 168 | }) | |
| 169 | 169 | .where(eq(users.id, userId)); | |
| 170 | 170 | ||
| 171 | - | console.log('[SSE] User status updated to online:', { | |
| 171 | + | log.info('User status updated to online', { | |
| 172 | 172 | userId: `${userId.slice(0, 4)}...${userId.slice(-4)}`, | |
| 173 | 173 | timestamp: new Date(now).toISOString() | |
| 174 | 174 | }); | |
| @@ -182,7 +182,7 @@ export const GET: RequestHandler = async ({ request, locals, cookies }) => { | |||
| 182 | 182 | } | |
| 183 | 183 | }); | |
| 184 | 184 | } catch (error) { | |
| 185 | - | console.error('[SSE] Error updating user status:', error); | |
| 185 | + | log.error('Error updating user status', { error }); | |
| 186 | 186 | } | |
| 187 | 187 | ||
| 188 | 188 | const stream = new ReadableStream({ | |
| @@ -193,10 +193,10 @@ export const GET: RequestHandler = async ({ request, locals, cookies }) => { | |||
| 193 | 193 | `data: ${JSON.stringify(event.data)}\n\n`; | |
| 194 | 194 | controller.enqueue(encoder.encode(payload)); | |
| 195 | 195 | if (!['userStatusUpdate', 'chatMessage'].includes(event.type)) { | |
| 196 | - | console.log('[SSE] Event sent:', { type: event.type }); | |
| 196 | + | log.info('Event sent', { type: event.type }); | |
| 197 | 197 | } | |
| 198 | - | } catch { | |
| 199 | - | console.log('[SSE] Error sending event'); | |
| 198 | + | } catch (error) { | |
| 199 | + | log.error('Error sending event', { error }); | |
| 200 | 200 | } | |
| 201 | 201 | }; | |
| 202 | 202 | ||
| @@ -208,10 +208,10 @@ export const GET: RequestHandler = async ({ request, locals, cookies }) => { | |||
| 208 | 208 | request.signal.addEventListener('abort', () => { | |
| 209 | 209 | if (onSSE) { | |
| 210 | 210 | sseEmitter.removeListener('sse', onSSE); | |
| 211 | - | console.log('[SSE] Removed listener due to abort signal.'); | |
| 211 | + | log.info('Removed listener due to abort signal'); | |
| 212 | 212 | } | |
| 213 | 213 | }); | |
| 214 | - | console.log('[SSE] Added new listener'); | |
| 214 | + | log.info('Added new listener'); | |
| 215 | 215 | ||
| 216 | 216 | controller.enqueue(encoder.encode('data: Connected\n\n')); | |
| 217 | 217 | ||
| @@ -223,7 +223,7 @@ export const GET: RequestHandler = async ({ request, locals, cookies }) => { | |||
| 223 | 223 | if (keepAliveInterval) clearInterval(keepAliveInterval); | |
| 224 | 224 | if (onSSE) sseEmitter.removeListener('sse', onSSE); | |
| 225 | 225 | const maskedUserId = `${userId.slice(0, 4)}...${userId.slice(-4)}`; | |
| 226 | - | console.log('[SSE] Connection closed:', { userId: maskedUserId, reason }); | |
| 226 | + | log.info('Connection closed', { userId: maskedUserId, reason }); | |
| 227 | 227 | ||
| 228 | 228 | const now = Date.now(); | |
| 229 | 229 | await db.update(users) | |
| @@ -10,6 +10,9 @@ import db from '$lib/db/db.server'; | |||||
| 10 | import { users } from '$lib/db/schema'; | 10 | import { users } from '$lib/db/schema'; | ||
| 11 | import { eq, and, lt } from 'drizzle-orm'; | 11 | import { eq, and, lt } from 'drizzle-orm'; | ||
| 12 | import { createSafeUser } from '$lib/types/chat'; | 12 | import { createSafeUser } from '$lib/types/chat'; | ||
| 13 | + | import { createLogger } from '$lib/utils/logger.server'; | |||
| 14 | + | ||||
| 15 | + | const log = createLogger('sse-server'); | |||
| 13 | 16 | ||||
| 14 | console.log('[SSE] Server initialized'); | 17 | console.log('[SSE] Server initialized'); | ||
| 15 | 18 | ||||
| @@ -35,7 +38,7 @@ function startTimeoutCheck() { | |||||
| 35 | 38 | ||||
| 36 | timeoutInterval = setInterval(async () => { | 39 | timeoutInterval = setInterval(async () => { | ||
| 37 | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | 40 | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | ||
| 38 | - | console.log('[SSE] Checking for timed out users', { | 41 | + | log.debug('Checking for timed out users', { |
| 39 | checkTime: new Date().toISOString(), | 42 | checkTime: new Date().toISOString(), | ||
| 40 | timeoutThreshold: new Date(timeoutThreshold).toISOString() | 43 | timeoutThreshold: new Date(timeoutThreshold).toISOString() | ||
| 41 | }); | 44 | }); | ||
| @@ -60,13 +63,13 @@ function startTimeoutCheck() { | |||||
| 60 | }); | 63 | }); | ||
| 61 | 64 | ||||
| 62 | if (result.length > 0) { | 65 | if (result.length > 0) { | ||
| 63 | - | console.log('[SSE] Found timed out users:', result.map(user => ({ | 66 | + | log.info('User status updated to offline', { |
| 64 | - | nickname: user.nickname, | 67 | + | usersUpdated: result.length, |
| 65 | - | lastSeen: new Date(user.lastSeen || 0).toISOString() | 68 | + | timeoutThreshold: new Date(timeoutThreshold).toISOString() |
| 66 | - | }))); | 69 | + | }); |
| 67 | } | 70 | } | ||
| 68 | } catch (error) { | 71 | } catch (error) { | ||
| 69 | - | console.error('[SSE] Error during timeout check:', error); | 72 | + | log.error('Error during timeout check', { error }); |
| 70 | } | 73 | } | ||
| 71 | }, CHECK_INTERVAL_MS); | 74 | }, CHECK_INTERVAL_MS); | ||
| 72 | } | 75 | } | ||
| @@ -81,7 +84,7 @@ function startBuddyListUpdateInterval() { | |||||
| 81 | const now = Date.now(); | 84 | const now = Date.now(); | ||
| 82 | // Only fetch and broadcast if 10 seconds have passed since last update | 85 | // Only fetch and broadcast if 10 seconds have passed since last update | ||
| 83 | if (now - lastBuddyListUpdate >= BUDDY_LIST_BROADCAST_INTERVAL) { | 86 | if (now - lastBuddyListUpdate >= BUDDY_LIST_BROADCAST_INTERVAL) { | ||
| 84 | - | console.log('[SSE] Broadcasting buddy list update...'); | 87 | + | log.debug('Broadcasting buddy list update...'); |
| 85 | // Query the complete buddy list from the database. | 88 | // Query the complete buddy list from the database. | ||
| 86 | const buddyList = await db.select().from(users); | 89 | const buddyList = await db.select().from(users); | ||
| 87 | // Use createSafeUser to properly sanitize user data | 90 | // Use createSafeUser to properly sanitize user data | ||
| @@ -98,7 +101,7 @@ function startBuddyListUpdateInterval() { | |||||
| 98 | lastBuddyListUpdate = now; | 101 | lastBuddyListUpdate = now; | ||
| 99 | } | 102 | } | ||
| 100 | } catch (error) { | 103 | } catch (error) { | ||
| 101 | - | console.error('[SSE] Error fetching buddy list for update:', error); | 104 | + | log.error('Error fetching buddy list for update', { error }); |
| 102 | } | 105 | } | ||
| 103 | }, 1000); // Check every second but only broadcast every 10 seconds | 106 | }, 1000); // Check every second but only broadcast every 10 seconds | ||
| 104 | } | 107 | } | ||
| @@ -108,9 +111,9 @@ if (!globalThis.__sseIntervalsStarted) { | |||||
| 108 | startTimeoutCheck(); | 111 | startTimeoutCheck(); | ||
| 109 | startBuddyListUpdateInterval(); | 112 | startBuddyListUpdateInterval(); | ||
| 110 | globalThis.__sseIntervalsStarted = true; | 113 | globalThis.__sseIntervalsStarted = true; | ||
| 111 | - | console.log('[SSE] Started global intervals for timeout and buddy list updates.'); | 114 | + | log.info('Started global intervals for timeout and buddy list updates'); |
| 112 | } else { | 115 | } else { | ||
| 113 | - | console.log('[SSE] Global intervals already started, skipping initialization.'); | 116 | + | log.debug('Global intervals already started, skipping initialization'); |
| 114 | } | 117 | } | ||
| 115 | 118 | ||||
| 116 | // Ensure the interval is cleared if the module is reloaded | 119 | // Ensure the interval is cleared if the module is reloaded | ||
| @@ -134,17 +137,14 @@ if (typeof process !== 'undefined') { | |||||
| 134 | }); | 137 | }); | ||
| 135 | } | 138 | } | ||
| 136 | 139 | ||||
| 137 | - | export const GET: RequestHandler = async ({ request, locals, cookies }) => { | 140 | + | export const GET: RequestHandler = async ({ request, locals, }) => { |
| 138 | - | console.log('[SSE] Connection attempt:', { | 141 | + | log.debug('Processing SSE request', { |
| 139 | - | hasLocals: !!locals, | 142 | + | url: request.url, |
| 140 | - | hasUser: !!locals.user, | 143 | + | userId: locals.user?.id |
| 141 | - | userId: locals.user?.id, | |||
| 142 | - | hasSessionCookie: !!cookies.get('session'), | |||
| 143 | - | headers: Object.fromEntries(request.headers.entries()) | |||
| 144 | }); | 144 | }); | ||
| 145 | 145 | ||||
| 146 | if (!locals.user) { | 146 | if (!locals.user) { | ||
| 147 | - | console.log('[SSE] Authentication failed - no user in locals'); | 147 | + | log.warn('Authentication failed - no user in locals'); |
| 148 | return new Response('Unauthorized', { | 148 | return new Response('Unauthorized', { | ||
| 149 | status: 401, | 149 | status: 401, | ||
| 150 | headers: { | 150 | headers: { | ||
| @@ -168,7 +168,7 @@ export const GET: RequestHandler = async ({ request, locals, cookies }) => { | |||||
| 168 | }) | 168 | }) | ||
| 169 | .where(eq(users.id, userId)); | 169 | .where(eq(users.id, userId)); | ||
| 170 | 170 | ||||
| 171 | - | console.log('[SSE] User status updated to online:', { | 171 | + | log.info('User status updated to online', { |
| 172 | userId: `${userId.slice(0, 4)}...${userId.slice(-4)}`, | 172 | userId: `${userId.slice(0, 4)}...${userId.slice(-4)}`, | ||
| 173 | timestamp: new Date(now).toISOString() | 173 | timestamp: new Date(now).toISOString() | ||
| 174 | }); | 174 | }); | ||
| @@ -182,7 +182,7 @@ export const GET: RequestHandler = async ({ request, locals, cookies }) => { | |||||
| 182 | } | 182 | } | ||
| 183 | }); | 183 | }); | ||
| 184 | } catch (error) { | 184 | } catch (error) { | ||
| 185 | - | console.error('[SSE] Error updating user status:', error); | 185 | + | log.error('Error updating user status', { error }); |
| 186 | } | 186 | } | ||
| 187 | 187 | ||||
| 188 | const stream = new ReadableStream({ | 188 | const stream = new ReadableStream({ | ||
| @@ -193,10 +193,10 @@ export const GET: RequestHandler = async ({ request, locals, cookies }) => { | |||||
| 193 | `data: ${JSON.stringify(event.data)}\n\n`; | 193 | `data: ${JSON.stringify(event.data)}\n\n`; | ||
| 194 | controller.enqueue(encoder.encode(payload)); | 194 | controller.enqueue(encoder.encode(payload)); | ||
| 195 | if (!['userStatusUpdate', 'chatMessage'].includes(event.type)) { | 195 | if (!['userStatusUpdate', 'chatMessage'].includes(event.type)) { | ||
| 196 | - | console.log('[SSE] Event sent:', { type: event.type }); | 196 | + | log.info('Event sent', { type: event.type }); |
| 197 | } | 197 | } | ||
| 198 | - | } catch { | 198 | + | } catch (error) { |
| 199 | - | console.log('[SSE] Error sending event'); | 199 | + | log.error('Error sending event', { error }); |
| 200 | } | 200 | } | ||
| 201 | }; | 201 | }; | ||
| 202 | 202 | ||||
| @@ -208,10 +208,10 @@ export const GET: RequestHandler = async ({ request, locals, cookies }) => { | |||||
| 208 | request.signal.addEventListener('abort', () => { | 208 | request.signal.addEventListener('abort', () => { | ||
| 209 | if (onSSE) { | 209 | if (onSSE) { | ||
| 210 | sseEmitter.removeListener('sse', onSSE); | 210 | sseEmitter.removeListener('sse', onSSE); | ||
| 211 | - | console.log('[SSE] Removed listener due to abort signal.'); | 211 | + | log.info('Removed listener due to abort signal'); |
| 212 | } | 212 | } | ||
| 213 | }); | 213 | }); | ||
| 214 | - | console.log('[SSE] Added new listener'); | 214 | + | log.info('Added new listener'); |
| 215 | 215 | ||||
| 216 | controller.enqueue(encoder.encode('data: Connected\n\n')); | 216 | controller.enqueue(encoder.encode('data: Connected\n\n')); | ||
| 217 | 217 | ||||
| @@ -223,7 +223,7 @@ export const GET: RequestHandler = async ({ request, locals, cookies }) => { | |||||
| 223 | if (keepAliveInterval) clearInterval(keepAliveInterval); | 223 | if (keepAliveInterval) clearInterval(keepAliveInterval); | ||
| 224 | if (onSSE) sseEmitter.removeListener('sse', onSSE); | 224 | if (onSSE) sseEmitter.removeListener('sse', onSSE); | ||
| 225 | const maskedUserId = `${userId.slice(0, 4)}...${userId.slice(-4)}`; | 225 | const maskedUserId = `${userId.slice(0, 4)}...${userId.slice(-4)}`; | ||
| 226 | - | console.log('[SSE] Connection closed:', { userId: maskedUserId, reason }); | 226 | + | log.info('Connection closed', { userId: maskedUserId, reason }); |
| 227 | 227 | ||||
| 228 | const now = Date.now(); | 228 | const now = Date.now(); | ||
| 229 | await db.update(users) | 229 | await db.update(users) | ||
M src/routes/api/status/+server.ts +11 -3
| @@ -5,21 +5,26 @@ import { eq } from 'drizzle-orm'; | |||
| 5 | 5 | import { validateSessionToken, generateSessionToken, createSession } from '$lib/api/session.server'; | |
| 6 | 6 | import { setSessionTokenCookie } from '$lib/api/session.cookie'; | |
| 7 | 7 | import { createSafeUser } from '$lib/types/chat'; | |
| 8 | + | import { createLogger } from '$lib/utils/logger.server'; | |
| 9 | + | ||
| 10 | + | const log = createLogger('status-server'); | |
| 8 | 11 | ||
| 9 | 12 | export async function POST({ request, cookies, locals }) { | |
| 10 | 13 | if (!locals.user) { | |
| 14 | + | log.warn('Authentication required'); | |
| 11 | 15 | throw error(401, 'Authentication required'); | |
| 12 | 16 | } | |
| 13 | 17 | ||
| 14 | 18 | const { status } = await request.json(); | |
| 15 | 19 | const validStatuses = ['online', 'away', 'busy', 'offline']; | |
| 16 | 20 | if (!validStatuses.includes(status)) { | |
| 21 | + | log.warn('Invalid status received', { status }); | |
| 17 | 22 | throw error(400, 'Invalid status'); | |
| 18 | 23 | } | |
| 19 | 24 | ||
| 20 | 25 | const userId = locals.user.id; | |
| 21 | 26 | const maskedUserId = `${userId.slice(0, 4)}...${userId.slice(-4)}`; | |
| 22 | - | console.log('[Status] Updating user status:', { userId: maskedUserId, status }); | |
| 27 | + | log.debug('Updating user status', { userId: maskedUserId, status }); | |
| 23 | 28 | ||
| 24 | 29 | const now = Date.now(); | |
| 25 | 30 | ||
| @@ -33,7 +38,7 @@ export async function POST({ request, cookies, locals }) { | |||
| 33 | 38 | .where(eq(users.id, userId)) | |
| 34 | 39 | .returning() | |
| 35 | 40 | .get(); | |
| 36 | - | console.log('[Status] Database updated successfully'); | |
| 41 | + | log.debug('Database updated successfully', { userId: maskedUserId, status }); | |
| 37 | 42 | ||
| 38 | 43 | // Renew session if status is online and session is near expiry | |
| 39 | 44 | if (status === 'online') { | |
| @@ -47,7 +52,10 @@ export async function POST({ request, cookies, locals }) { | |||
| 47 | 52 | const newToken = generateSessionToken(); | |
| 48 | 53 | const newSession = await createSession(newToken, userId); | |
| 49 | 54 | setSessionTokenCookie({ cookies }, newToken, newSession.expiresAt); | |
| 50 | - | console.log('[Status] Session renewed:', { userId: maskedUserId, expiresAt: new Date(newSession.expiresAt).toISOString() }); | |
| 55 | + | log.info('Session renewed', { | |
| 56 | + | userId: maskedUserId, | |
| 57 | + | expiresAt: new Date(newSession.expiresAt).toISOString() | |
| 58 | + | }); | |
| 51 | 59 | } | |
| 52 | 60 | } | |
| 53 | 61 | } | |
| @@ -5,21 +5,26 @@ import { eq } from 'drizzle-orm'; | |||||
| 5 | import { validateSessionToken, generateSessionToken, createSession } from '$lib/api/session.server'; | 5 | import { validateSessionToken, generateSessionToken, createSession } from '$lib/api/session.server'; | ||
| 6 | import { setSessionTokenCookie } from '$lib/api/session.cookie'; | 6 | import { setSessionTokenCookie } from '$lib/api/session.cookie'; | ||
| 7 | import { createSafeUser } from '$lib/types/chat'; | 7 | import { createSafeUser } from '$lib/types/chat'; | ||
| 8 | + | import { createLogger } from '$lib/utils/logger.server'; | |||
| 9 | + | ||||
| 10 | + | const log = createLogger('status-server'); | |||
| 8 | 11 | ||||
| 9 | export async function POST({ request, cookies, locals }) { | 12 | export async function POST({ request, cookies, locals }) { | ||
| 10 | if (!locals.user) { | 13 | if (!locals.user) { | ||
| 14 | + | log.warn('Authentication required'); | |||
| 11 | throw error(401, 'Authentication required'); | 15 | throw error(401, 'Authentication required'); | ||
| 12 | } | 16 | } | ||
| 13 | 17 | ||||
| 14 | const { status } = await request.json(); | 18 | const { status } = await request.json(); | ||
| 15 | const validStatuses = ['online', 'away', 'busy', 'offline']; | 19 | const validStatuses = ['online', 'away', 'busy', 'offline']; | ||
| 16 | if (!validStatuses.includes(status)) { | 20 | if (!validStatuses.includes(status)) { | ||
| 21 | + | log.warn('Invalid status received', { status }); | |||
| 17 | throw error(400, 'Invalid status'); | 22 | throw error(400, 'Invalid status'); | ||
| 18 | } | 23 | } | ||
| 19 | 24 | ||||
| 20 | const userId = locals.user.id; | 25 | const userId = locals.user.id; | ||
| 21 | const maskedUserId = `${userId.slice(0, 4)}...${userId.slice(-4)}`; | 26 | const maskedUserId = `${userId.slice(0, 4)}...${userId.slice(-4)}`; | ||
| 22 | - | console.log('[Status] Updating user status:', { userId: maskedUserId, status }); | 27 | + | log.debug('Updating user status', { userId: maskedUserId, status }); |
| 23 | 28 | ||||
| 24 | const now = Date.now(); | 29 | const now = Date.now(); | ||
| 25 | 30 | ||||
| @@ -33,7 +38,7 @@ export async function POST({ request, cookies, locals }) { | |||||
| 33 | .where(eq(users.id, userId)) | 38 | .where(eq(users.id, userId)) | ||
| 34 | .returning() | 39 | .returning() | ||
| 35 | .get(); | 40 | .get(); | ||
| 36 | - | console.log('[Status] Database updated successfully'); | 41 | + | log.debug('Database updated successfully', { userId: maskedUserId, status }); |
| 37 | 42 | ||||
| 38 | // Renew session if status is online and session is near expiry | 43 | // Renew session if status is online and session is near expiry | ||
| 39 | if (status === 'online') { | 44 | if (status === 'online') { | ||
| @@ -47,7 +52,10 @@ export async function POST({ request, cookies, locals }) { | |||||
| 47 | const newToken = generateSessionToken(); | 52 | const newToken = generateSessionToken(); | ||
| 48 | const newSession = await createSession(newToken, userId); | 53 | const newSession = await createSession(newToken, userId); | ||
| 49 | setSessionTokenCookie({ cookies }, newToken, newSession.expiresAt); | 54 | setSessionTokenCookie({ cookies }, newToken, newSession.expiresAt); | ||
| 50 | - | console.log('[Status] Session renewed:', { userId: maskedUserId, expiresAt: new Date(newSession.expiresAt).toISOString() }); | 55 | + | log.info('Session renewed', { |
| 56 | + | userId: maskedUserId, | |||
| 57 | + | expiresAt: new Date(newSession.expiresAt).toISOString() | |||
| 58 | + | }); | |||
| 51 | } | 59 | } | ||
| 52 | } | 60 | } | ||
| 53 | } | 61 | } | ||
M src/routes/api/users/[userId]/+server.ts +12 -5
| @@ -4,10 +4,13 @@ import { users } from '$lib/db/schema'; | |||
| 4 | 4 | import { eq } from 'drizzle-orm'; | |
| 5 | 5 | import type { RequestHandler } from './$types'; | |
| 6 | 6 | import { createSafeUser } from '$lib/types/chat'; | |
| 7 | + | import { createLogger } from '$lib/utils/logger.server'; | |
| 8 | + | ||
| 9 | + | const log = createLogger('users-server'); | |
| 7 | 10 | ||
| 8 | 11 | export const GET: RequestHandler = async ({ params }) => { | |
| 9 | 12 | const maskedUserId = `${params.userId.slice(0, 4)}...${params.userId.slice(-4)}`; | |
| 10 | - | console.log('[Users] Fetching user data:', { userId: maskedUserId }); | |
| 13 | + | log.debug('Fetching user data', { userId: maskedUserId }); | |
| 11 | 14 | ||
| 12 | 15 | try { | |
| 13 | 16 | const user = await db.select() | |
| @@ -16,23 +19,27 @@ export const GET: RequestHandler = async ({ params }) => { | |||
| 16 | 19 | .get(); | |
| 17 | 20 | ||
| 18 | 21 | if (!user) { | |
| 19 | - | console.log('[Users] User not found:', { userId: maskedUserId }); | |
| 22 | + | log.warn('User not found', { userId: maskedUserId }); | |
| 20 | 23 | throw error(404, 'User not found'); | |
| 21 | 24 | } | |
| 22 | 25 | ||
| 23 | 26 | const safeUser = createSafeUser(user); | |
| 24 | 27 | ||
| 25 | - | console.log('[Users] User data retrieved:', { | |
| 28 | + | log.debug('User data retrieved', { | |
| 26 | 29 | userId: maskedUserId, | |
| 27 | 30 | status: safeUser.status, | |
| 28 | 31 | lastSeen: safeUser.lastSeen ? new Date(safeUser.lastSeen).toISOString() : null | |
| 29 | 32 | }); | |
| 33 | + | ||
| 30 | 34 | return json({ | |
| 31 | 35 | success: true, | |
| 32 | 36 | user: safeUser | |
| 33 | 37 | }); | |
| 34 | - | } catch { | |
| 35 | - | console.log('[Users] Error fetching user data:', { userId: maskedUserId }); | |
| 38 | + | } catch (err) { | |
| 39 | + | log.error('Error fetching user data', { | |
| 40 | + | userId: maskedUserId, | |
| 41 | + | error: err instanceof Error ? err.message : 'Unknown error' | |
| 42 | + | }); | |
| 36 | 43 | throw error(500, 'Failed to fetch user information'); | |
| 37 | 44 | } | |
| 38 | 45 | };⏎̸ | |
| @@ -4,10 +4,13 @@ import { users } from '$lib/db/schema'; | |||||
| 4 | import { eq } from 'drizzle-orm'; | 4 | import { eq } from 'drizzle-orm'; | ||
| 5 | import type { RequestHandler } from './$types'; | 5 | import type { RequestHandler } from './$types'; | ||
| 6 | import { createSafeUser } from '$lib/types/chat'; | 6 | import { createSafeUser } from '$lib/types/chat'; | ||
| 7 | + | import { createLogger } from '$lib/utils/logger.server'; | |||
| 8 | + | ||||
| 9 | + | const log = createLogger('users-server'); | |||
| 7 | 10 | ||||
| 8 | export const GET: RequestHandler = async ({ params }) => { | 11 | export const GET: RequestHandler = async ({ params }) => { | ||
| 9 | const maskedUserId = `${params.userId.slice(0, 4)}...${params.userId.slice(-4)}`; | 12 | const maskedUserId = `${params.userId.slice(0, 4)}...${params.userId.slice(-4)}`; | ||
| 10 | - | console.log('[Users] Fetching user data:', { userId: maskedUserId }); | 13 | + | log.debug('Fetching user data', { userId: maskedUserId }); |
| 11 | 14 | ||||
| 12 | try { | 15 | try { | ||
| 13 | const user = await db.select() | 16 | const user = await db.select() | ||
| @@ -16,23 +19,27 @@ export const GET: RequestHandler = async ({ params }) => { | |||||
| 16 | .get(); | 19 | .get(); | ||
| 17 | 20 | ||||
| 18 | if (!user) { | 21 | if (!user) { | ||
| 19 | - | console.log('[Users] User not found:', { userId: maskedUserId }); | 22 | + | log.warn('User not found', { userId: maskedUserId }); |
| 20 | throw error(404, 'User not found'); | 23 | throw error(404, 'User not found'); | ||
| 21 | } | 24 | } | ||
| 22 | 25 | ||||
| 23 | const safeUser = createSafeUser(user); | 26 | const safeUser = createSafeUser(user); | ||
| 24 | 27 | ||||
| 25 | - | console.log('[Users] User data retrieved:', { | 28 | + | log.debug('User data retrieved', { |
| 26 | userId: maskedUserId, | 29 | userId: maskedUserId, | ||
| 27 | status: safeUser.status, | 30 | status: safeUser.status, | ||
| 28 | lastSeen: safeUser.lastSeen ? new Date(safeUser.lastSeen).toISOString() : null | 31 | lastSeen: safeUser.lastSeen ? new Date(safeUser.lastSeen).toISOString() : null | ||
| 29 | }); | 32 | }); | ||
| 33 | + | ||||
| 30 | return json({ | 34 | return json({ | ||
| 31 | success: true, | 35 | success: true, | ||
| 32 | user: safeUser | 36 | user: safeUser | ||
| 33 | }); | 37 | }); | ||
| 34 | - | } catch { | 38 | + | } catch (err) { |
| 35 | - | console.log('[Users] Error fetching user data:', { userId: maskedUserId }); | 39 | + | log.error('Error fetching user data', { |
| 40 | + | userId: maskedUserId, | |||
| 41 | + | error: err instanceof Error ? err.message : 'Unknown error' | |||
| 42 | + | }); | |||
| 36 | throw error(500, 'Failed to fetch user information'); | 43 | throw error(500, 'Failed to fetch user information'); | ||
| 37 | } | 44 | } | ||
| 38 | };⏎̸ | 45 | };⏎̸ | ||
M vite.config.ts +23 -3
| @@ -2,6 +2,19 @@ import { defineConfig } from 'vitest/config'; | |||
| 2 | 2 | import { loadEnv } from 'vite'; | |
| 3 | 3 | import { sveltekit } from '@sveltejs/kit/vite'; | |
| 4 | 4 | ||
| 5 | + | /** | |
| 6 | + | * Logging configuration | |
| 7 | + | * This project uses two logging systems: | |
| 8 | + | * 1. Console logging (development/client-side) - controlled by console stripping | |
| 9 | + | * 2. Pino logging (server-side) - controlled by LOG_LEVEL | |
| 10 | + | * | |
| 11 | + | * LOG_LEVEL affects both systems: | |
| 12 | + | * - error: only error logs | |
| 13 | + | * - warn: warn and error logs | |
| 14 | + | * - info: info, warn, and error logs (default in production) | |
| 15 | + | * - debug: all logs (default in development) | |
| 16 | + | */ | |
| 17 | + | ||
| 5 | 18 | // Define which console methods to strip based on LOG_LEVEL | |
| 6 | 19 | const getConsoleFuncsToStrip = (logLevel: string) => { | |
| 7 | 20 | switch (logLevel?.toLowerCase()) { | |
| @@ -18,9 +31,11 @@ const getConsoleFuncsToStrip = (logLevel: string) => { | |||
| 18 | 31 | }; | |
| 19 | 32 | ||
| 20 | 33 | export default defineConfig(({ mode }) => { | |
| 21 | - | // Load env file based on `mode` value. mode can be 'development', 'production', etc. | |
| 34 | + | // Load env file based on `mode` value | |
| 22 | 35 | const env = loadEnv(mode, process.cwd(), ''); | |
| 23 | - | console.log(`Building with LOG_LEVEL=${env.LOG_LEVEL || 'info'} in ${mode} mode`); | |
| 36 | + | const logLevel = env.LOG_LEVEL || (mode === 'development' ? 'debug' : 'info'); | |
| 37 | + | ||
| 38 | + | console.log(`Building with LOG_LEVEL=${logLevel} in ${mode} mode`); | |
| 24 | 39 | ||
| 25 | 40 | return { | |
| 26 | 41 | plugins: [sveltekit()], | |
| @@ -31,9 +46,14 @@ export default defineConfig(({ mode }) => { | |||
| 31 | 46 | minify: 'terser', | |
| 32 | 47 | terserOptions: { | |
| 33 | 48 | compress: { | |
| 34 | - | pure_funcs: getConsoleFuncsToStrip(env.LOG_LEVEL || 'info') | |
| 49 | + | // Strip console methods based on LOG_LEVEL | |
| 50 | + | pure_funcs: getConsoleFuncsToStrip(logLevel) | |
| 35 | 51 | } | |
| 36 | 52 | }, | |
| 53 | + | }, | |
| 54 | + | // Make LOG_LEVEL available to client-side code | |
| 55 | + | define: { | |
| 56 | + | 'process.env.LOG_LEVEL': JSON.stringify(logLevel) | |
| 37 | 57 | } | |
| 38 | 58 | }; | |
| 39 | 59 | }); | |
| @@ -2,6 +2,19 @@ import { defineConfig } from 'vitest/config'; | |||||
| 2 | import { loadEnv } from 'vite'; | 2 | import { loadEnv } from 'vite'; | ||
| 3 | import { sveltekit } from '@sveltejs/kit/vite'; | 3 | import { sveltekit } from '@sveltejs/kit/vite'; | ||
| 4 | 4 | ||||
| 5 | + | /** | |||
| 6 | + | * Logging configuration | |||
| 7 | + | * This project uses two logging systems: | |||
| 8 | + | * 1. Console logging (development/client-side) - controlled by console stripping | |||
| 9 | + | * 2. Pino logging (server-side) - controlled by LOG_LEVEL | |||
| 10 | + | * | |||
| 11 | + | * LOG_LEVEL affects both systems: | |||
| 12 | + | * - error: only error logs | |||
| 13 | + | * - warn: warn and error logs | |||
| 14 | + | * - info: info, warn, and error logs (default in production) | |||
| 15 | + | * - debug: all logs (default in development) | |||
| 16 | + | */ | |||
| 17 | + | ||||
| 5 | // Define which console methods to strip based on LOG_LEVEL | 18 | // Define which console methods to strip based on LOG_LEVEL | ||
| 6 | const getConsoleFuncsToStrip = (logLevel: string) => { | 19 | const getConsoleFuncsToStrip = (logLevel: string) => { | ||
| 7 | switch (logLevel?.toLowerCase()) { | 20 | switch (logLevel?.toLowerCase()) { | ||
| @@ -18,9 +31,11 @@ const getConsoleFuncsToStrip = (logLevel: string) => { | |||||
| 18 | }; | 31 | }; | ||
| 19 | 32 | ||||
| 20 | export default defineConfig(({ mode }) => { | 33 | export default defineConfig(({ mode }) => { | ||
| 21 | - | // Load env file based on `mode` value. mode can be 'development', 'production', etc. | 34 | + | // Load env file based on `mode` value |
| 22 | const env = loadEnv(mode, process.cwd(), ''); | 35 | const env = loadEnv(mode, process.cwd(), ''); | ||
| 23 | - | console.log(`Building with LOG_LEVEL=${env.LOG_LEVEL || 'info'} in ${mode} mode`); | 36 | + | const logLevel = env.LOG_LEVEL || (mode === 'development' ? 'debug' : 'info'); |
| 37 | + | ||||
| 38 | + | console.log(`Building with LOG_LEVEL=${logLevel} in ${mode} mode`); | |||
| 24 | 39 | ||||
| 25 | return { | 40 | return { | ||
| 26 | plugins: [sveltekit()], | 41 | plugins: [sveltekit()], | ||
| @@ -31,9 +46,14 @@ export default defineConfig(({ mode }) => { | |||||
| 31 | minify: 'terser', | 46 | minify: 'terser', | ||
| 32 | terserOptions: { | 47 | terserOptions: { | ||
| 33 | compress: { | 48 | compress: { | ||
| 34 | - | pure_funcs: getConsoleFuncsToStrip(env.LOG_LEVEL || 'info') | 49 | + | // Strip console methods based on LOG_LEVEL |
| 50 | + | pure_funcs: getConsoleFuncsToStrip(logLevel) | |||
| 35 | } | 51 | } | ||
| 36 | }, | 52 | }, | ||
| 53 | + | }, | |||
| 54 | + | // Make LOG_LEVEL available to client-side code | |||
| 55 | + | define: { | |||
| 56 | + | 'process.env.LOG_LEVEL': JSON.stringify(logLevel) | |||
| 37 | } | 57 | } | ||
| 38 | }; | 58 | }; | ||
| 39 | }); | 59 | }); | ||