refactor: Enhance logging and privacy in SSE and authentication modules
- commit
- 3c14cb2215e8731c1d3fc1ec0e87f935ff1fa07b
- author
- Kit · · 2025-02-14 13:35 UTC
- parent
- d46837f
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
4 files · +54 -36
M src/lib/sseEmitter.ts +11 -6
| @@ -8,12 +8,16 @@ class CustomEventEmitter { | |||
| 8 | 8 | private maxListeners: number = 100; | |
| 9 | 9 | ||
| 10 | 10 | constructor() { | |
| 11 | - | console.debug('Initializing custom SSE emitter'); | |
| 11 | + | console.log('[SSE-Emitter] Initialized'); | |
| 12 | 12 | } | |
| 13 | 13 | ||
| 14 | 14 | emit(event: string, data: SSEEventData) { | |
| 15 | 15 | const eventListeners = this.listeners.get(event); | |
| 16 | 16 | if (eventListeners) { | |
| 17 | + | // Only log non-sensitive event types | |
| 18 | + | if (!['userStatusUpdate', 'chatMessage'].includes(data.type)) { | |
| 19 | + | console.log('[SSE-Emitter] Broadcasting event:', { type: data.type, listenersCount: eventListeners.length }); | |
| 20 | + | } | |
| 17 | 21 | eventListeners.forEach(listener => listener(data)); | |
| 18 | 22 | } | |
| 19 | 23 | } | |
| @@ -25,24 +29,25 @@ class CustomEventEmitter { | |||
| 25 | 29 | const eventListeners = this.listeners.get(event)!; | |
| 26 | 30 | ||
| 27 | 31 | if (eventListeners.length >= this.maxListeners) { | |
| 28 | - | console.warn(`Warning: Event '${event}' has exceeded the maximum number of listeners (${this.maxListeners})`); | |
| 32 | + | console.log(`[SSE-Emitter] Warning: Event '${event}' has exceeded the maximum number of listeners (${this.maxListeners})`); | |
| 29 | 33 | } | |
| 30 | 34 | ||
| 31 | 35 | eventListeners.push(listener); | |
| 36 | + | console.log('[SSE-Emitter] New listener added:', { event, totalListeners: eventListeners.length }); | |
| 32 | 37 | } | |
| 33 | 38 | ||
| 34 | 39 | removeListener(event: string, listenerToRemove: (data: SSEEventData) => void) { | |
| 35 | 40 | const eventListeners = this.listeners.get(event); | |
| 36 | 41 | if (eventListeners) { | |
| 37 | - | this.listeners.set( | |
| 38 | - | event, | |
| 39 | - | eventListeners.filter(listener => listener !== listenerToRemove) | |
| 40 | - | ); | |
| 42 | + | const newListeners = eventListeners.filter(listener => listener !== listenerToRemove); | |
| 43 | + | this.listeners.set(event, newListeners); | |
| 44 | + | console.log('[SSE-Emitter] Listener removed:', { event, remainingListeners: newListeners.length }); | |
| 41 | 45 | } | |
| 42 | 46 | } | |
| 43 | 47 | ||
| 44 | 48 | setMaxListeners(n: number) { | |
| 45 | 49 | this.maxListeners = n; | |
| 50 | + | console.log('[SSE-Emitter] Max listeners updated:', { newLimit: n }); | |
| 46 | 51 | } | |
| 47 | 52 | } | |
| 48 | 53 | ||
| @@ -8,12 +8,16 @@ class CustomEventEmitter { | |||||
| 8 | private maxListeners: number = 100; | 8 | private maxListeners: number = 100; | ||
| 9 | 9 | ||||
| 10 | constructor() { | 10 | constructor() { | ||
| 11 | - | console.debug('Initializing custom SSE emitter'); | 11 | + | console.log('[SSE-Emitter] Initialized'); |
| 12 | } | 12 | } | ||
| 13 | 13 | ||||
| 14 | emit(event: string, data: SSEEventData) { | 14 | emit(event: string, data: SSEEventData) { | ||
| 15 | const eventListeners = this.listeners.get(event); | 15 | const eventListeners = this.listeners.get(event); | ||
| 16 | if (eventListeners) { | 16 | if (eventListeners) { | ||
| 17 | + | // Only log non-sensitive event types | |||
| 18 | + | if (!['userStatusUpdate', 'chatMessage'].includes(data.type)) { | |||
| 19 | + | console.log('[SSE-Emitter] Broadcasting event:', { type: data.type, listenersCount: eventListeners.length }); | |||
| 20 | + | } | |||
| 17 | eventListeners.forEach(listener => listener(data)); | 21 | eventListeners.forEach(listener => listener(data)); | ||
| 18 | } | 22 | } | ||
| 19 | } | 23 | } | ||
| @@ -25,24 +29,25 @@ class CustomEventEmitter { | |||||
| 25 | const eventListeners = this.listeners.get(event)!; | 29 | const eventListeners = this.listeners.get(event)!; | ||
| 26 | 30 | ||||
| 27 | if (eventListeners.length >= this.maxListeners) { | 31 | if (eventListeners.length >= this.maxListeners) { | ||
| 28 | - | console.warn(`Warning: Event '${event}' has exceeded the maximum number of listeners (${this.maxListeners})`); | 32 | + | console.log(`[SSE-Emitter] Warning: Event '${event}' has exceeded the maximum number of listeners (${this.maxListeners})`); |
| 29 | } | 33 | } | ||
| 30 | 34 | ||||
| 31 | eventListeners.push(listener); | 35 | eventListeners.push(listener); | ||
| 36 | + | console.log('[SSE-Emitter] New listener added:', { event, totalListeners: eventListeners.length }); | |||
| 32 | } | 37 | } | ||
| 33 | 38 | ||||
| 34 | removeListener(event: string, listenerToRemove: (data: SSEEventData) => void) { | 39 | removeListener(event: string, listenerToRemove: (data: SSEEventData) => void) { | ||
| 35 | const eventListeners = this.listeners.get(event); | 40 | const eventListeners = this.listeners.get(event); | ||
| 36 | if (eventListeners) { | 41 | if (eventListeners) { | ||
| 37 | - | this.listeners.set( | 42 | + | const newListeners = eventListeners.filter(listener => listener !== listenerToRemove); |
| 38 | - | event, | 43 | + | this.listeners.set(event, newListeners); |
| 39 | - | eventListeners.filter(listener => listener !== listenerToRemove) | 44 | + | console.log('[SSE-Emitter] Listener removed:', { event, remainingListeners: newListeners.length }); |
| 40 | - | ); | |||
| 41 | } | 45 | } | ||
| 42 | } | 46 | } | ||
| 43 | 47 | ||||
| 44 | setMaxListeners(n: number) { | 48 | setMaxListeners(n: number) { | ||
| 45 | this.maxListeners = n; | 49 | this.maxListeners = n; | ||
| 50 | + | console.log('[SSE-Emitter] Max listeners updated:', { newLimit: n }); | |||
| 46 | } | 51 | } | ||
| 47 | } | 52 | } | ||
| 48 | 53 | ||||
M src/routes/api/chat/messages/+server.ts +18 -11
| @@ -58,7 +58,7 @@ export async function GET({ request, locals }) { | |||
| 58 | 58 | // Ensure default chat room exists | |
| 59 | 59 | await ensureDefaultChatRoom(db); | |
| 60 | 60 | ||
| 61 | - | console.debug('Fetching messages', { isPublic }); | |
| 61 | + | console.log('[Chat] Fetching messages', { isPublic }); | |
| 62 | 62 | const query = db.select() | |
| 63 | 63 | .from(messages) | |
| 64 | 64 | .orderBy(desc(messages.timestamp)); | |
| @@ -83,8 +83,8 @@ export async function GET({ request, locals }) { | |||
| 83 | 83 | headers: { 'Content-Type': 'application/json' } | |
| 84 | 84 | } | |
| 85 | 85 | ); | |
| 86 | - | } catch (error) { | |
| 87 | - | console.debug('Error fetching messages:', error); | |
| 86 | + | } catch { | |
| 87 | + | console.log('[Chat] Error fetching messages'); | |
| 88 | 88 | const errorResponse: GetMessagesResponse = { | |
| 89 | 89 | success: false, | |
| 90 | 90 | error: 'Failed to fetch messages' | |
| @@ -105,12 +105,13 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 105 | 105 | throw error(401, 'Authentication required'); | |
| 106 | 106 | } | |
| 107 | 107 | ||
| 108 | - | console.debug('Received new message POST'); | |
| 108 | + | console.log('[Chat] New message received'); | |
| 109 | 109 | try { | |
| 110 | 110 | // Check rate limiting | |
| 111 | 111 | const { canSend, retryAfter } = updateUserCooldown(locals.user.id); | |
| 112 | 112 | if (!canSend) { | |
| 113 | - | console.debug('Rate limited:', { userId: locals.user.id, retryAfter }); | |
| 113 | + | const maskedUserId = `${locals.user.id.slice(0, 4)}...${locals.user.id.slice(-4)}`; | |
| 114 | + | console.log('[Chat] Rate limited:', { userId: maskedUserId, retryAfter }); | |
| 114 | 115 | const errorResponse: SendMessageResponse = { | |
| 115 | 116 | success: false, | |
| 116 | 117 | error: 'Please wait before sending another message', | |
| @@ -131,7 +132,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 131 | 132 | ||
| 132 | 133 | const data = await request.json() as SendMessageRequest; | |
| 133 | 134 | if (!data.content || !data.userId) { | |
| 134 | - | console.debug('Invalid payload', data); | |
| 135 | + | console.log('[Chat] Invalid payload received'); | |
| 135 | 136 | const errorResponse: SendMessageResponse = { | |
| 136 | 137 | success: false, | |
| 137 | 138 | error: 'Invalid message payload' | |
| @@ -153,7 +154,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 153 | 154 | .get(); | |
| 154 | 155 | ||
| 155 | 156 | if (!chatRoom) { | |
| 156 | - | console.debug('Chat room not found:', chatRoomId); | |
| 157 | + | console.log('[Chat] Chat room not found:', chatRoomId); | |
| 157 | 158 | const errorResponse: SendMessageResponse = { | |
| 158 | 159 | success: false, | |
| 159 | 160 | error: 'Chat room not found' | |
| @@ -168,7 +169,8 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 168 | 169 | .get(); | |
| 169 | 170 | ||
| 170 | 171 | if (!user) { | |
| 171 | - | console.debug('User not found:', data.userId); | |
| 172 | + | const maskedUserId = `${data.userId.slice(0, 4)}...${data.userId.slice(-4)}`; | |
| 173 | + | console.log('[Chat] User not found:', maskedUserId); | |
| 172 | 174 | const errorResponse: SendMessageResponse = { | |
| 173 | 175 | success: false, | |
| 174 | 176 | error: 'User not found' | |
| @@ -186,7 +188,12 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 186 | 188 | }; | |
| 187 | 189 | ||
| 188 | 190 | await db.insert(messages).values(newMessage); | |
| 189 | - | console.debug('Message saved in DB', newMessage); | |
| 191 | + | console.log('[Chat] Message saved in DB', { | |
| 192 | + | messageId: newMessage.id, | |
| 193 | + | chatRoomId: newMessage.chatRoomId, | |
| 194 | + | type: newMessage.type, | |
| 195 | + | timestamp: newMessage.timestamp | |
| 196 | + | }); | |
| 190 | 197 | ||
| 191 | 198 | // Broadcast the new message via the unified SSE emitter | |
| 192 | 199 | sseEmitter.emit('sse', { type: 'chatMessage', data: newMessage }); | |
| @@ -203,8 +210,8 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||
| 203 | 210 | headers: { 'Content-Type': 'application/json' } | |
| 204 | 211 | } | |
| 205 | 212 | ); | |
| 206 | - | } catch (error) { | |
| 207 | - | console.debug('Error processing POST:', error); | |
| 213 | + | } catch { | |
| 214 | + | console.log('[Chat] Error processing message'); | |
| 208 | 215 | const errorResponse: SendMessageResponse = { | |
| 209 | 216 | success: false, | |
| 210 | 217 | error: 'Failed to save message' | |
| @@ -58,7 +58,7 @@ export async function GET({ request, locals }) { | |||||
| 58 | // Ensure default chat room exists | 58 | // Ensure default chat room exists | ||
| 59 | await ensureDefaultChatRoom(db); | 59 | await ensureDefaultChatRoom(db); | ||
| 60 | 60 | ||||
| 61 | - | console.debug('Fetching messages', { isPublic }); | 61 | + | console.log('[Chat] Fetching messages', { isPublic }); |
| 62 | const query = db.select() | 62 | const query = db.select() | ||
| 63 | .from(messages) | 63 | .from(messages) | ||
| 64 | .orderBy(desc(messages.timestamp)); | 64 | .orderBy(desc(messages.timestamp)); | ||
| @@ -83,8 +83,8 @@ export async function GET({ request, locals }) { | |||||
| 83 | headers: { 'Content-Type': 'application/json' } | 83 | headers: { 'Content-Type': 'application/json' } | ||
| 84 | } | 84 | } | ||
| 85 | ); | 85 | ); | ||
| 86 | - | } catch (error) { | 86 | + | } catch { |
| 87 | - | console.debug('Error fetching messages:', error); | 87 | + | console.log('[Chat] Error fetching messages'); |
| 88 | const errorResponse: GetMessagesResponse = { | 88 | const errorResponse: GetMessagesResponse = { | ||
| 89 | success: false, | 89 | success: false, | ||
| 90 | error: 'Failed to fetch messages' | 90 | error: 'Failed to fetch messages' | ||
| @@ -105,12 +105,13 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 105 | throw error(401, 'Authentication required'); | 105 | throw error(401, 'Authentication required'); | ||
| 106 | } | 106 | } | ||
| 107 | 107 | ||||
| 108 | - | console.debug('Received new message POST'); | 108 | + | console.log('[Chat] New message received'); |
| 109 | try { | 109 | try { | ||
| 110 | // Check rate limiting | 110 | // Check rate limiting | ||
| 111 | const { canSend, retryAfter } = updateUserCooldown(locals.user.id); | 111 | const { canSend, retryAfter } = updateUserCooldown(locals.user.id); | ||
| 112 | if (!canSend) { | 112 | if (!canSend) { | ||
| 113 | - | console.debug('Rate limited:', { userId: locals.user.id, retryAfter }); | 113 | + | const maskedUserId = `${locals.user.id.slice(0, 4)}...${locals.user.id.slice(-4)}`; |
| 114 | + | console.log('[Chat] Rate limited:', { userId: maskedUserId, retryAfter }); | |||
| 114 | const errorResponse: SendMessageResponse = { | 115 | const errorResponse: SendMessageResponse = { | ||
| 115 | success: false, | 116 | success: false, | ||
| 116 | error: 'Please wait before sending another message', | 117 | error: 'Please wait before sending another message', | ||
| @@ -131,7 +132,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 131 | 132 | ||||
| 132 | const data = await request.json() as SendMessageRequest; | 133 | const data = await request.json() as SendMessageRequest; | ||
| 133 | if (!data.content || !data.userId) { | 134 | if (!data.content || !data.userId) { | ||
| 134 | - | console.debug('Invalid payload', data); | 135 | + | console.log('[Chat] Invalid payload received'); |
| 135 | const errorResponse: SendMessageResponse = { | 136 | const errorResponse: SendMessageResponse = { | ||
| 136 | success: false, | 137 | success: false, | ||
| 137 | error: 'Invalid message payload' | 138 | error: 'Invalid message payload' | ||
| @@ -153,7 +154,7 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 153 | .get(); | 154 | .get(); | ||
| 154 | 155 | ||||
| 155 | if (!chatRoom) { | 156 | if (!chatRoom) { | ||
| 156 | - | console.debug('Chat room not found:', chatRoomId); | 157 | + | console.log('[Chat] Chat room not found:', chatRoomId); |
| 157 | const errorResponse: SendMessageResponse = { | 158 | const errorResponse: SendMessageResponse = { | ||
| 158 | success: false, | 159 | success: false, | ||
| 159 | error: 'Chat room not found' | 160 | error: 'Chat room not found' | ||
| @@ -168,7 +169,8 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 168 | .get(); | 169 | .get(); | ||
| 169 | 170 | ||||
| 170 | if (!user) { | 171 | if (!user) { | ||
| 171 | - | console.debug('User not found:', data.userId); | 172 | + | const maskedUserId = `${data.userId.slice(0, 4)}...${data.userId.slice(-4)}`; |
| 173 | + | console.log('[Chat] User not found:', maskedUserId); | |||
| 172 | const errorResponse: SendMessageResponse = { | 174 | const errorResponse: SendMessageResponse = { | ||
| 173 | success: false, | 175 | success: false, | ||
| 174 | error: 'User not found' | 176 | error: 'User not found' | ||
| @@ -186,7 +188,12 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 186 | }; | 188 | }; | ||
| 187 | 189 | ||||
| 188 | await db.insert(messages).values(newMessage); | 190 | await db.insert(messages).values(newMessage); | ||
| 189 | - | console.debug('Message saved in DB', newMessage); | 191 | + | console.log('[Chat] Message saved in DB', { |
| 192 | + | messageId: newMessage.id, | |||
| 193 | + | chatRoomId: newMessage.chatRoomId, | |||
| 194 | + | type: newMessage.type, | |||
| 195 | + | timestamp: newMessage.timestamp | |||
| 196 | + | }); | |||
| 190 | 197 | ||||
| 191 | // Broadcast the new message via the unified SSE emitter | 198 | // Broadcast the new message via the unified SSE emitter | ||
| 192 | sseEmitter.emit('sse', { type: 'chatMessage', data: newMessage }); | 199 | sseEmitter.emit('sse', { type: 'chatMessage', data: newMessage }); | ||
| @@ -203,8 +210,8 @@ export async function POST({ request, locals }: { request: Request, locals: App. | |||||
| 203 | headers: { 'Content-Type': 'application/json' } | 210 | headers: { 'Content-Type': 'application/json' } | ||
| 204 | } | 211 | } | ||
| 205 | ); | 212 | ); | ||
| 206 | - | } catch (error) { | 213 | + | } catch { |
| 207 | - | console.debug('Error processing POST:', error); | 214 | + | console.log('[Chat] Error processing message'); |
| 208 | const errorResponse: SendMessageResponse = { | 215 | const errorResponse: SendMessageResponse = { | ||
| 209 | success: false, | 216 | success: false, | ||
| 210 | error: 'Failed to save message' | 217 | error: 'Failed to save message' | ||
M src/routes/api/session/login/+server.ts +10 -9
| @@ -24,7 +24,7 @@ setInterval(() => { | |||
| 24 | 24 | }, 60 * 60 * 1000); | |
| 25 | 25 | ||
| 26 | 26 | export const POST: RequestHandler = async ({ request, cookies }) => { | |
| 27 | - | console.debug("POST /api/session/login called"); | |
| 27 | + | console.log("[Login] New login attempt received"); | |
| 28 | 28 | ||
| 29 | 29 | if (request.method !== 'POST') { | |
| 30 | 30 | return new Response(JSON.stringify({ error: 'Method Not Allowed' } as LoginResponseError), { status: 405 }); | |
| @@ -32,28 +32,29 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||
| 32 | 32 | ||
| 33 | 33 | // Get the IP address for rate limiting | |
| 34 | 34 | const ip = request.headers.get('x-forwarded-for') || 'unknown'; | |
| 35 | + | const maskedIp = ip.split('.').map((octet, idx) => idx < 3 ? 'xxx' : octet).join('.'); | |
| 35 | 36 | const now = Date.now(); | |
| 36 | 37 | const attemptData = loginAttempts.get(ip) || { count: 0, lastAttempt: 0 }; | |
| 37 | 38 | ||
| 38 | 39 | // If the user has failed three or more times, apply exponential backoff | |
| 39 | 40 | if (attemptData.count >= MAX_ATTEMPTS) { | |
| 40 | 41 | const delay = Math.pow(2, attemptData.count - MAX_ATTEMPTS + 1) * 1000; // delay in milliseconds | |
| 41 | - | console.debug(`Applying exponential backoff delay of ${delay}ms for IP ${ip}`); | |
| 42 | + | console.log(`[Login] Rate limit exceeded for IP ${maskedIp} - applying ${delay}ms delay`); | |
| 42 | 43 | await sleep(delay); | |
| 43 | 44 | } | |
| 44 | 45 | ||
| 45 | 46 | let body: unknown; | |
| 46 | 47 | try { | |
| 47 | 48 | body = await request.json(); | |
| 48 | - | } catch (err) { | |
| 49 | - | console.debug("Invalid JSON", err); | |
| 49 | + | } catch { | |
| 50 | + | console.log("[Login] Invalid JSON payload received"); | |
| 50 | 51 | return new Response(JSON.stringify({ error: 'Invalid JSON' } as LoginResponseError), { status: 400 }); | |
| 51 | 52 | } | |
| 52 | 53 | ||
| 53 | 54 | const { username, password } = body as { username: string, password: string }; | |
| 54 | 55 | ||
| 55 | 56 | if (typeof username !== 'string' || typeof password !== 'string') { | |
| 56 | - | console.debug("Missing or invalid input fields", { username, password }); | |
| 57 | + | console.log("Missing or invalid input fields", { username, password }); | |
| 57 | 58 | return new Response(JSON.stringify({ error: 'Missing or invalid input fields' } as LoginResponseError), { status: 400 }); | |
| 58 | 59 | } | |
| 59 | 60 | ||
| @@ -66,7 +67,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||
| 66 | 67 | attemptData.count++; | |
| 67 | 68 | attemptData.lastAttempt = now; | |
| 68 | 69 | loginAttempts.set(ip, attemptData); | |
| 69 | - | console.debug(`Login failed for IP ${ip}. Attempt count: ${attemptData.count}`); | |
| 70 | + | console.log(`[Login] Login failed for IP ${maskedIp}. Attempt count: ${attemptData.count}`); | |
| 70 | 71 | const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count); | |
| 71 | 72 | return new Response(JSON.stringify({ | |
| 72 | 73 | error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` | |
| @@ -80,14 +81,14 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||
| 80 | 81 | attemptData.count++; | |
| 81 | 82 | attemptData.lastAttempt = now; | |
| 82 | 83 | loginAttempts.set(ip, attemptData); | |
| 83 | - | console.debug(`Login failed for IP ${ip}. Attempt count: ${attemptData.count}`); | |
| 84 | + | console.log(`[Login] Login failed for IP ${maskedIp}. Attempt count: ${attemptData.count}`); | |
| 84 | 85 | const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count); | |
| 85 | 86 | return new Response(JSON.stringify({ | |
| 86 | 87 | error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` | |
| 87 | 88 | } as LoginResponseError), { status: 401 }); | |
| 88 | 89 | } | |
| 89 | - | } catch (err) { | |
| 90 | - | console.debug("Error verifying password", err); | |
| 90 | + | } catch { | |
| 91 | + | console.log("[Login] Error verifying password"); | |
| 91 | 92 | return new Response(JSON.stringify({ error: 'Internal Server Error' } as LoginResponseError), { status: 500 }); | |
| 92 | 93 | } | |
| 93 | 94 | ||
| @@ -24,7 +24,7 @@ setInterval(() => { | |||||
| 24 | }, 60 * 60 * 1000); | 24 | }, 60 * 60 * 1000); | ||
| 25 | 25 | ||||
| 26 | export const POST: RequestHandler = async ({ request, cookies }) => { | 26 | export const POST: RequestHandler = async ({ request, cookies }) => { | ||
| 27 | - | console.debug("POST /api/session/login called"); | 27 | + | console.log("[Login] New login attempt received"); |
| 28 | 28 | ||||
| 29 | if (request.method !== 'POST') { | 29 | if (request.method !== 'POST') { | ||
| 30 | return new Response(JSON.stringify({ error: 'Method Not Allowed' } as LoginResponseError), { status: 405 }); | 30 | return new Response(JSON.stringify({ error: 'Method Not Allowed' } as LoginResponseError), { status: 405 }); | ||
| @@ -32,28 +32,29 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||||
| 32 | 32 | ||||
| 33 | // Get the IP address for rate limiting | 33 | // Get the IP address for rate limiting | ||
| 34 | const ip = request.headers.get('x-forwarded-for') || 'unknown'; | 34 | const ip = request.headers.get('x-forwarded-for') || 'unknown'; | ||
| 35 | + | const maskedIp = ip.split('.').map((octet, idx) => idx < 3 ? 'xxx' : octet).join('.'); | |||
| 35 | const now = Date.now(); | 36 | const now = Date.now(); | ||
| 36 | const attemptData = loginAttempts.get(ip) || { count: 0, lastAttempt: 0 }; | 37 | const attemptData = loginAttempts.get(ip) || { count: 0, lastAttempt: 0 }; | ||
| 37 | 38 | ||||
| 38 | // If the user has failed three or more times, apply exponential backoff | 39 | // If the user has failed three or more times, apply exponential backoff | ||
| 39 | if (attemptData.count >= MAX_ATTEMPTS) { | 40 | if (attemptData.count >= MAX_ATTEMPTS) { | ||
| 40 | const delay = Math.pow(2, attemptData.count - MAX_ATTEMPTS + 1) * 1000; // delay in milliseconds | 41 | const delay = Math.pow(2, attemptData.count - MAX_ATTEMPTS + 1) * 1000; // delay in milliseconds | ||
| 41 | - | console.debug(`Applying exponential backoff delay of ${delay}ms for IP ${ip}`); | 42 | + | console.log(`[Login] Rate limit exceeded for IP ${maskedIp} - applying ${delay}ms delay`); |
| 42 | await sleep(delay); | 43 | await sleep(delay); | ||
| 43 | } | 44 | } | ||
| 44 | 45 | ||||
| 45 | let body: unknown; | 46 | let body: unknown; | ||
| 46 | try { | 47 | try { | ||
| 47 | body = await request.json(); | 48 | body = await request.json(); | ||
| 48 | - | } catch (err) { | 49 | + | } catch { |
| 49 | - | console.debug("Invalid JSON", err); | 50 | + | console.log("[Login] Invalid JSON payload received"); |
| 50 | return new Response(JSON.stringify({ error: 'Invalid JSON' } as LoginResponseError), { status: 400 }); | 51 | return new Response(JSON.stringify({ error: 'Invalid JSON' } as LoginResponseError), { status: 400 }); | ||
| 51 | } | 52 | } | ||
| 52 | 53 | ||||
| 53 | const { username, password } = body as { username: string, password: string }; | 54 | const { username, password } = body as { username: string, password: string }; | ||
| 54 | 55 | ||||
| 55 | if (typeof username !== 'string' || typeof password !== 'string') { | 56 | if (typeof username !== 'string' || typeof password !== 'string') { | ||
| 56 | - | console.debug("Missing or invalid input fields", { username, password }); | 57 | + | console.log("Missing or invalid input fields", { username, password }); |
| 57 | return new Response(JSON.stringify({ error: 'Missing or invalid input fields' } as LoginResponseError), { status: 400 }); | 58 | return new Response(JSON.stringify({ error: 'Missing or invalid input fields' } as LoginResponseError), { status: 400 }); | ||
| 58 | } | 59 | } | ||
| 59 | 60 | ||||
| @@ -66,7 +67,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||||
| 66 | attemptData.count++; | 67 | attemptData.count++; | ||
| 67 | attemptData.lastAttempt = now; | 68 | attemptData.lastAttempt = now; | ||
| 68 | loginAttempts.set(ip, attemptData); | 69 | loginAttempts.set(ip, attemptData); | ||
| 69 | - | console.debug(`Login failed for IP ${ip}. Attempt count: ${attemptData.count}`); | 70 | + | console.log(`[Login] Login failed for IP ${maskedIp}. Attempt count: ${attemptData.count}`); |
| 70 | const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count); | 71 | const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count); | ||
| 71 | return new Response(JSON.stringify({ | 72 | return new Response(JSON.stringify({ | ||
| 72 | error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` | 73 | error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` | ||
| @@ -80,14 +81,14 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||||
| 80 | attemptData.count++; | 81 | attemptData.count++; | ||
| 81 | attemptData.lastAttempt = now; | 82 | attemptData.lastAttempt = now; | ||
| 82 | loginAttempts.set(ip, attemptData); | 83 | loginAttempts.set(ip, attemptData); | ||
| 83 | - | console.debug(`Login failed for IP ${ip}. Attempt count: ${attemptData.count}`); | 84 | + | console.log(`[Login] Login failed for IP ${maskedIp}. Attempt count: ${attemptData.count}`); |
| 84 | const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count); | 85 | const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count); | ||
| 85 | return new Response(JSON.stringify({ | 86 | return new Response(JSON.stringify({ | ||
| 86 | error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` | 87 | error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` | ||
| 87 | } as LoginResponseError), { status: 401 }); | 88 | } as LoginResponseError), { status: 401 }); | ||
| 88 | } | 89 | } | ||
| 89 | - | } catch (err) { | 90 | + | } catch { |
| 90 | - | console.debug("Error verifying password", err); | 91 | + | console.log("[Login] Error verifying password"); |
| 91 | return new Response(JSON.stringify({ error: 'Internal Server Error' } as LoginResponseError), { status: 500 }); | 92 | return new Response(JSON.stringify({ error: 'Internal Server Error' } as LoginResponseError), { status: 500 }); | ||
| 92 | } | 93 | } | ||
| 93 | 94 | ||||
M src/routes/api/sse/+server.ts +15 -10
| @@ -4,7 +4,7 @@ import db from '$lib/db/db.server'; | |||
| 4 | 4 | import { users } from '$lib/db/schema'; | |
| 5 | 5 | import { eq, and, lt } from 'drizzle-orm'; | |
| 6 | 6 | ||
| 7 | - | console.debug('SSE connection initiated'); | |
| 7 | + | console.log('[SSE] Server initialized'); | |
| 8 | 8 | ||
| 9 | 9 | // Constants for timeout checks | |
| 10 | 10 | const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes | |
| @@ -13,7 +13,7 @@ const CHECK_INTERVAL_MS = 60 * 1000; // Check every minute | |||
| 13 | 13 | // Initialize periodic check for timed out users | |
| 14 | 14 | const timeoutInterval = setInterval(async () => { | |
| 15 | 15 | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | |
| 16 | - | console.debug('Checking for timed out users, threshold:', new Date(timeoutThreshold)); | |
| 16 | + | console.log('[SSE] Checking for timed out users', { checkTime: new Date().toISOString() }); | |
| 17 | 17 | ||
| 18 | 18 | try { | |
| 19 | 19 | // Find and update users who are marked as online but haven't been seen recently | |
| @@ -33,6 +33,7 @@ const timeoutInterval = setInterval(async () => { | |||
| 33 | 33 | ||
| 34 | 34 | // Broadcast status updates for timed out users | |
| 35 | 35 | result.forEach(user => { | |
| 36 | + | const maskedUserId = `${user.id.slice(0, 4)}...${user.id.slice(-4)}`; | |
| 36 | 37 | sseEmitter.emit('sse', { | |
| 37 | 38 | type: 'userStatusUpdate', | |
| 38 | 39 | data: { | |
| @@ -41,10 +42,10 @@ const timeoutInterval = setInterval(async () => { | |||
| 41 | 42 | lastSeen: now | |
| 42 | 43 | } | |
| 43 | 44 | }); | |
| 44 | - | console.debug('User timed out and marked offline:', user.id); | |
| 45 | + | console.log('[SSE] User timed out and marked offline:', maskedUserId); | |
| 45 | 46 | }); | |
| 46 | - | } catch (error) { | |
| 47 | - | console.debug('Error during timeout check:', error); | |
| 47 | + | } catch { | |
| 48 | + | console.log('[SSE] Error during timeout check'); | |
| 48 | 49 | } | |
| 49 | 50 | }, CHECK_INTERVAL_MS); | |
| 50 | 51 | ||
| @@ -91,9 +92,12 @@ export const GET: RequestHandler = async ({ locals }) => { | |||
| 91 | 92 | const payload = `event: ${event.type}\n` + | |
| 92 | 93 | `data: ${JSON.stringify(event.data)}\n\n`; | |
| 93 | 94 | controller.enqueue(encoder.encode(payload)); | |
| 94 | - | console.debug('Sent SSE event:', event); | |
| 95 | - | } catch (err) { | |
| 96 | - | console.debug('Error sending SSE event:', err); | |
| 95 | + | // Only log non-sensitive event types | |
| 96 | + | if (!['userStatusUpdate', 'chatMessage'].includes(event.type)) { | |
| 97 | + | console.log('[SSE] Event sent:', { type: event.type }); | |
| 98 | + | } | |
| 99 | + | } catch { | |
| 100 | + | console.log('[SSE] Error sending event'); | |
| 97 | 101 | } | |
| 98 | 102 | }; | |
| 99 | 103 | ||
| @@ -102,7 +106,7 @@ export const GET: RequestHandler = async ({ locals }) => { | |||
| 102 | 106 | }; | |
| 103 | 107 | ||
| 104 | 108 | sseEmitter.addListener('sse', onSSE); | |
| 105 | - | console.debug('Added SSE listener'); | |
| 109 | + | console.log('[SSE] Added new listener'); | |
| 106 | 110 | ||
| 107 | 111 | // Send initial connection message | |
| 108 | 112 | controller.enqueue(encoder.encode('data: Connected\n\n')); | |
| @@ -119,7 +123,8 @@ export const GET: RequestHandler = async ({ locals }) => { | |||
| 119 | 123 | async cancel(reason) { | |
| 120 | 124 | if (keepAliveInterval) clearInterval(keepAliveInterval); | |
| 121 | 125 | if (onSSE) sseEmitter.removeListener('sse', onSSE); | |
| 122 | - | console.debug('SSE connection cancelled:', reason); | |
| 126 | + | const maskedUserId = `${userId.slice(0, 4)}...${userId.slice(-4)}`; | |
| 127 | + | console.log('[SSE] Connection closed:', { userId: maskedUserId, reason }); | |
| 123 | 128 | ||
| 124 | 129 | // Update user status to offline in the database | |
| 125 | 130 | const now = Date.now(); | |
| @@ -4,7 +4,7 @@ import db from '$lib/db/db.server'; | |||||
| 4 | import { users } from '$lib/db/schema'; | 4 | import { users } from '$lib/db/schema'; | ||
| 5 | import { eq, and, lt } from 'drizzle-orm'; | 5 | import { eq, and, lt } from 'drizzle-orm'; | ||
| 6 | 6 | ||||
| 7 | - | console.debug('SSE connection initiated'); | 7 | + | console.log('[SSE] Server initialized'); |
| 8 | 8 | ||||
| 9 | // Constants for timeout checks | 9 | // Constants for timeout checks | ||
| 10 | const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes | 10 | const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes | ||
| @@ -13,7 +13,7 @@ const CHECK_INTERVAL_MS = 60 * 1000; // Check every minute | |||||
| 13 | // Initialize periodic check for timed out users | 13 | // Initialize periodic check for timed out users | ||
| 14 | const timeoutInterval = setInterval(async () => { | 14 | const timeoutInterval = setInterval(async () => { | ||
| 15 | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | 15 | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | ||
| 16 | - | console.debug('Checking for timed out users, threshold:', new Date(timeoutThreshold)); | 16 | + | console.log('[SSE] Checking for timed out users', { checkTime: new Date().toISOString() }); |
| 17 | 17 | ||||
| 18 | try { | 18 | try { | ||
| 19 | // Find and update users who are marked as online but haven't been seen recently | 19 | // Find and update users who are marked as online but haven't been seen recently | ||
| @@ -33,6 +33,7 @@ const timeoutInterval = setInterval(async () => { | |||||
| 33 | 33 | ||||
| 34 | // Broadcast status updates for timed out users | 34 | // Broadcast status updates for timed out users | ||
| 35 | result.forEach(user => { | 35 | result.forEach(user => { | ||
| 36 | + | const maskedUserId = `${user.id.slice(0, 4)}...${user.id.slice(-4)}`; | |||
| 36 | sseEmitter.emit('sse', { | 37 | sseEmitter.emit('sse', { | ||
| 37 | type: 'userStatusUpdate', | 38 | type: 'userStatusUpdate', | ||
| 38 | data: { | 39 | data: { | ||
| @@ -41,10 +42,10 @@ const timeoutInterval = setInterval(async () => { | |||||
| 41 | lastSeen: now | 42 | lastSeen: now | ||
| 42 | } | 43 | } | ||
| 43 | }); | 44 | }); | ||
| 44 | - | console.debug('User timed out and marked offline:', user.id); | 45 | + | console.log('[SSE] User timed out and marked offline:', maskedUserId); |
| 45 | }); | 46 | }); | ||
| 46 | - | } catch (error) { | 47 | + | } catch { |
| 47 | - | console.debug('Error during timeout check:', error); | 48 | + | console.log('[SSE] Error during timeout check'); |
| 48 | } | 49 | } | ||
| 49 | }, CHECK_INTERVAL_MS); | 50 | }, CHECK_INTERVAL_MS); | ||
| 50 | 51 | ||||
| @@ -91,9 +92,12 @@ export const GET: RequestHandler = async ({ locals }) => { | |||||
| 91 | const payload = `event: ${event.type}\n` + | 92 | const payload = `event: ${event.type}\n` + | ||
| 92 | `data: ${JSON.stringify(event.data)}\n\n`; | 93 | `data: ${JSON.stringify(event.data)}\n\n`; | ||
| 93 | controller.enqueue(encoder.encode(payload)); | 94 | controller.enqueue(encoder.encode(payload)); | ||
| 94 | - | console.debug('Sent SSE event:', event); | 95 | + | // Only log non-sensitive event types |
| 95 | - | } catch (err) { | 96 | + | if (!['userStatusUpdate', 'chatMessage'].includes(event.type)) { |
| 96 | - | console.debug('Error sending SSE event:', err); | 97 | + | console.log('[SSE] Event sent:', { type: event.type }); |
| 98 | + | } | |||
| 99 | + | } catch { | |||
| 100 | + | console.log('[SSE] Error sending event'); | |||
| 97 | } | 101 | } | ||
| 98 | }; | 102 | }; | ||
| 99 | 103 | ||||
| @@ -102,7 +106,7 @@ export const GET: RequestHandler = async ({ locals }) => { | |||||
| 102 | }; | 106 | }; | ||
| 103 | 107 | ||||
| 104 | sseEmitter.addListener('sse', onSSE); | 108 | sseEmitter.addListener('sse', onSSE); | ||
| 105 | - | console.debug('Added SSE listener'); | 109 | + | console.log('[SSE] Added new listener'); |
| 106 | 110 | ||||
| 107 | // Send initial connection message | 111 | // Send initial connection message | ||
| 108 | controller.enqueue(encoder.encode('data: Connected\n\n')); | 112 | controller.enqueue(encoder.encode('data: Connected\n\n')); | ||
| @@ -119,7 +123,8 @@ export const GET: RequestHandler = async ({ locals }) => { | |||||
| 119 | async cancel(reason) { | 123 | async cancel(reason) { | ||
| 120 | if (keepAliveInterval) clearInterval(keepAliveInterval); | 124 | if (keepAliveInterval) clearInterval(keepAliveInterval); | ||
| 121 | if (onSSE) sseEmitter.removeListener('sse', onSSE); | 125 | if (onSSE) sseEmitter.removeListener('sse', onSSE); | ||
| 122 | - | console.debug('SSE connection cancelled:', reason); | 126 | + | const maskedUserId = `${userId.slice(0, 4)}...${userId.slice(-4)}`; |
| 127 | + | console.log('[SSE] Connection closed:', { userId: maskedUserId, reason }); | |||
| 123 | 128 | ||||
| 124 | // Update user status to offline in the database | 129 | // Update user status to offline in the database | ||
| 125 | const now = Date.now(); | 130 | const now = Date.now(); | ||