fix: limit public message access to 10 messages in both cache and API endpoints
- commit
- e1cfa3d0adfc13cb0b92444c182c2ebba9178e5f
- author
- Kit · · 2025-02-15 11:20 UTC
- parent
- 075016f
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
2 files · +79 -62
M src/lib/cache/message-cache.ts +31 -9
| @@ -15,16 +15,19 @@ class MessageCache { | |||
| 15 | 15 | private readonly maxMessagesPerRoom: number; | |
| 16 | 16 | private readonly maxRooms: number; | |
| 17 | 17 | private readonly cacheExpiryMs: number; | |
| 18 | + | private readonly publicMessageLimit: number; | |
| 18 | 19 | ||
| 19 | 20 | private constructor() { | |
| 20 | 21 | this.rooms = new Map(); | |
| 21 | 22 | this.maxMessagesPerRoom = 100; // Keep last 100 messages per room | |
| 22 | 23 | this.maxRooms = 100; // Maximum number of rooms to cache | |
| 23 | 24 | this.cacheExpiryMs = 30 * 60 * 1000; // 30 minutes cache expiry | |
| 25 | + | this.publicMessageLimit = 10; // Strict limit for public/non-auth users | |
| 24 | 26 | log.debug('[MessageCache] Initialized with settings:', { | |
| 25 | 27 | maxMessagesPerRoom: this.maxMessagesPerRoom, | |
| 26 | 28 | maxRooms: this.maxRooms, | |
| 27 | - | cacheExpiryMs: this.cacheExpiryMs | |
| 29 | + | cacheExpiryMs: this.cacheExpiryMs, | |
| 30 | + | publicMessageLimit: this.publicMessageLimit | |
| 28 | 31 | }); | |
| 29 | 32 | } | |
| 30 | 33 | ||
| @@ -98,19 +101,38 @@ class MessageCache { | |||
| 98 | 101 | }); | |
| 99 | 102 | } | |
| 100 | 103 | ||
| 101 | - | public getMessages(roomId: string = DEFAULT_CHAT_ROOM_ID, limit?: number): Message[] { | |
| 102 | - | const roomCache = this.rooms.get(roomId); | |
| 103 | - | if (!roomCache) { | |
| 104 | + | /** | |
| 105 | + | * Get messages for authenticated users | |
| 106 | + | */ | |
| 107 | + | public getMessages(roomId: string = DEFAULT_CHAT_ROOM_ID, limit: number = this.maxMessagesPerRoom): Message[] { | |
| 108 | + | this.cleanExpiredRooms(); | |
| 109 | + | const cache = this.rooms.get(roomId); | |
| 110 | + | ||
| 111 | + | if (!cache) { | |
| 112 | + | log.debug('[MessageCache] No messages found for room:', { roomId }); | |
| 104 | 113 | return []; | |
| 105 | 114 | } | |
| 106 | 115 | ||
| 107 | - | // Update last accessed time | |
| 108 | - | roomCache.lastUpdated = Date.now(); | |
| 116 | + | cache.lastUpdated = Date.now(); | |
| 117 | + | const messages = [...cache.messages]; | |
| 118 | + | return limit ? messages.slice(-limit) : messages; | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | /** | |
| 122 | + | * Get messages for public/non-authenticated users with strict limit | |
| 123 | + | */ | |
| 124 | + | public getPublicMessages(roomId: string = DEFAULT_CHAT_ROOM_ID): Message[] { | |
| 125 | + | this.cleanExpiredRooms(); | |
| 126 | + | const cache = this.rooms.get(roomId); | |
| 109 | 127 | ||
| 110 | - | if (limit) { | |
| 111 | - | return roomCache.messages.slice(-limit); | |
| 128 | + | if (!cache) { | |
| 129 | + | log.debug('[MessageCache] No public messages found for room:', { roomId }); | |
| 130 | + | return []; | |
| 112 | 131 | } | |
| 113 | - | return [...roomCache.messages]; // Return a copy to prevent external modifications | |
| 132 | + | ||
| 133 | + | cache.lastUpdated = Date.now(); | |
| 134 | + | const messages = [...cache.messages]; | |
| 135 | + | return messages.slice(-this.publicMessageLimit); | |
| 114 | 136 | } | |
| 115 | 137 | ||
| 116 | 138 | public addMessage(message: Message): void { | |
| @@ -15,16 +15,19 @@ class MessageCache { | |||||
| 15 | private readonly maxMessagesPerRoom: number; | 15 | private readonly maxMessagesPerRoom: number; | ||
| 16 | private readonly maxRooms: number; | 16 | private readonly maxRooms: number; | ||
| 17 | private readonly cacheExpiryMs: number; | 17 | private readonly cacheExpiryMs: number; | ||
| 18 | + | private readonly publicMessageLimit: number; | |||
| 18 | 19 | ||||
| 19 | private constructor() { | 20 | private constructor() { | ||
| 20 | this.rooms = new Map(); | 21 | this.rooms = new Map(); | ||
| 21 | this.maxMessagesPerRoom = 100; // Keep last 100 messages per room | 22 | this.maxMessagesPerRoom = 100; // Keep last 100 messages per room | ||
| 22 | this.maxRooms = 100; // Maximum number of rooms to cache | 23 | this.maxRooms = 100; // Maximum number of rooms to cache | ||
| 23 | this.cacheExpiryMs = 30 * 60 * 1000; // 30 minutes cache expiry | 24 | this.cacheExpiryMs = 30 * 60 * 1000; // 30 minutes cache expiry | ||
| 25 | + | this.publicMessageLimit = 10; // Strict limit for public/non-auth users | |||
| 24 | log.debug('[MessageCache] Initialized with settings:', { | 26 | log.debug('[MessageCache] Initialized with settings:', { | ||
| 25 | maxMessagesPerRoom: this.maxMessagesPerRoom, | 27 | maxMessagesPerRoom: this.maxMessagesPerRoom, | ||
| 26 | maxRooms: this.maxRooms, | 28 | maxRooms: this.maxRooms, | ||
| 27 | - | cacheExpiryMs: this.cacheExpiryMs | 29 | + | cacheExpiryMs: this.cacheExpiryMs, |
| 30 | + | publicMessageLimit: this.publicMessageLimit | |||
| 28 | }); | 31 | }); | ||
| 29 | } | 32 | } | ||
| 30 | 33 | ||||
| @@ -98,19 +101,38 @@ class MessageCache { | |||||
| 98 | }); | 101 | }); | ||
| 99 | } | 102 | } | ||
| 100 | 103 | ||||
| 101 | - | public getMessages(roomId: string = DEFAULT_CHAT_ROOM_ID, limit?: number): Message[] { | 104 | + | /** |
| 102 | - | const roomCache = this.rooms.get(roomId); | 105 | + | * Get messages for authenticated users |
| 103 | - | if (!roomCache) { | 106 | + | */ |
| 107 | + | public getMessages(roomId: string = DEFAULT_CHAT_ROOM_ID, limit: number = this.maxMessagesPerRoom): Message[] { | |||
| 108 | + | this.cleanExpiredRooms(); | |||
| 109 | + | const cache = this.rooms.get(roomId); | |||
| 110 | + | ||||
| 111 | + | if (!cache) { | |||
| 112 | + | log.debug('[MessageCache] No messages found for room:', { roomId }); | |||
| 104 | return []; | 113 | return []; | ||
| 105 | } | 114 | } | ||
| 106 | 115 | ||||
| 107 | - | // Update last accessed time | 116 | + | cache.lastUpdated = Date.now(); |
| 108 | - | roomCache.lastUpdated = Date.now(); | 117 | + | const messages = [...cache.messages]; |
| 118 | + | return limit ? messages.slice(-limit) : messages; | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | /** | |||
| 122 | + | * Get messages for public/non-authenticated users with strict limit | |||
| 123 | + | */ | |||
| 124 | + | public getPublicMessages(roomId: string = DEFAULT_CHAT_ROOM_ID): Message[] { | |||
| 125 | + | this.cleanExpiredRooms(); | |||
| 126 | + | const cache = this.rooms.get(roomId); | |||
| 109 | 127 | ||||
| 110 | - | if (limit) { | 128 | + | if (!cache) { |
| 111 | - | return roomCache.messages.slice(-limit); | 129 | + | log.debug('[MessageCache] No public messages found for room:', { roomId }); |
| 130 | + | return []; | |||
| 112 | } | 131 | } | ||
| 113 | - | return [...roomCache.messages]; // Return a copy to prevent external modifications | 132 | + | |
| 133 | + | cache.lastUpdated = Date.now(); | |||
| 134 | + | const messages = [...cache.messages]; | |||
| 135 | + | return messages.slice(-this.publicMessageLimit); | |||
| 114 | } | 136 | } | ||
| 115 | 137 | ||||
| 116 | public addMessage(message: Message): void { | 138 | public addMessage(message: Message): void { | ||
M src/routes/api/chat/messages/+server.ts +48 -53
| @@ -7,15 +7,15 @@ import type { | |||
| 7 | 7 | SendMessageResponse, | |
| 8 | 8 | GetMessagesResponse | |
| 9 | 9 | } from '$lib/types/payloads'; | |
| 10 | - | import { desc } from 'drizzle-orm'; | |
| 10 | + | import { desc, eq, lt, and } from 'drizzle-orm'; | |
| 11 | 11 | import { error } from '@sveltejs/kit'; | |
| 12 | 12 | import { sseEmitter } from '$lib/sseEmitter'; | |
| 13 | - | import { eq, lt, and } from 'drizzle-orm'; | |
| 14 | 13 | import { chatRooms } from '$lib/db/schema'; | |
| 15 | 14 | import { users } from '$lib/db/schema'; | |
| 16 | 15 | import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; | |
| 17 | 16 | import { messageCache } from '$lib/cache/message-cache'; | |
| 18 | 17 | import { createLogger } from '$lib/utils/logger.server'; | |
| 18 | + | import { SQL } from 'drizzle-orm'; | |
| 19 | 19 | ||
| 20 | 20 | const log = createLogger('chat-server'); | |
| 21 | 21 | ||
| @@ -94,46 +94,56 @@ export async function GET({ request, locals }) { | |||
| 94 | 94 | ||
| 95 | 95 | // Try to get messages from cache first | |
| 96 | 96 | if (!beforeTimestamp) { | |
| 97 | - | log.debug('Attempting to fetch messages from cache', { roomId }); | |
| 98 | - | const cachedMessages = messageCache.getMessages(roomId); | |
| 97 | + | log.debug('Attempting to fetch messages from cache', { | |
| 98 | + | roomId, | |
| 99 | + | isAuthenticated: !!locals.session | |
| 100 | + | }); | |
| 101 | + | ||
| 102 | + | // Use appropriate method based on authentication status | |
| 103 | + | const cachedMessages = locals.session | |
| 104 | + | ? messageCache.getMessages(roomId) | |
| 105 | + | : messageCache.getPublicMessages(roomId); | |
| 106 | + | ||
| 99 | 107 | if (cachedMessages && cachedMessages.length > 0) { | |
| 100 | 108 | log.debug('Messages found in cache', { | |
| 109 | + | count: cachedMessages.length, | |
| 101 | 110 | roomId, | |
| 102 | - | messageCount: cachedMessages.length | |
| 111 | + | isAuthenticated: !!locals.session | |
| 103 | 112 | }); | |
| 104 | 113 | fetchedMessages = cachedMessages; | |
| 105 | 114 | } | |
| 106 | 115 | } | |
| 107 | 116 | ||
| 108 | - | // If no cached messages or pagination request, fetch from DB | |
| 109 | - | if (fetchedMessages.length === 0) { | |
| 110 | - | if (!locals.user) { | |
| 111 | - | log.warn('Authentication required'); | |
| 112 | - | throw error(401, 'Authentication required'); | |
| 113 | - | } | |
| 117 | + | // If no cached messages or pagination requested, fetch from DB | |
| 118 | + | if (fetchedMessages.length === 0 || beforeTimestamp) { | |
| 119 | + | log.debug('Fetching messages from database', { | |
| 120 | + | beforeTimestamp, | |
| 121 | + | roomId, | |
| 122 | + | isAuthenticated: !!locals.session | |
| 123 | + | }); | |
| 124 | + | ||
| 125 | + | let conditions = eq(messages.chatRoomId, roomId) as SQL<unknown>; | |
| 114 | 126 | ||
| 115 | 127 | if (beforeTimestamp) { | |
| 116 | - | log.debug('Fetching messages from DB before timestamp', { | |
| 117 | - | roomId, | |
| 118 | - | beforeTimestamp | |
| 119 | - | }); | |
| 120 | - | const query = db.select() | |
| 121 | - | .from(messages) | |
| 122 | - | .where(and( | |
| 123 | - | eq(messages.chatRoomId, roomId), | |
| 124 | - | lt(messages.timestamp, parseInt(beforeTimestamp)) | |
| 125 | - | )) | |
| 126 | - | .orderBy(desc(messages.timestamp)) | |
| 127 | - | .limit(100); | |
| 128 | - | fetchedMessages = (await query).reverse(); | |
| 129 | - | } else { | |
| 130 | - | log.debug('Fetching messages from DB for instant chat', { roomId }); | |
| 131 | - | const query = db.select() | |
| 132 | - | .from(messages) | |
| 133 | - | .where(eq(messages.chatRoomId, roomId)) | |
| 134 | - | .orderBy(desc(messages.timestamp)) | |
| 135 | - | .limit(50); | |
| 136 | - | fetchedMessages = (await query).reverse(); | |
| 128 | + | conditions = and( | |
| 129 | + | conditions, | |
| 130 | + | lt(messages.timestamp, parseInt(beforeTimestamp)) | |
| 131 | + | ) as SQL<unknown>; | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | // Build and execute query with conditions | |
| 135 | + | const fetchLimit = locals.session ? 100 : 10; | |
| 136 | + | fetchedMessages = await db.select() | |
| 137 | + | .from(messages) | |
| 138 | + | .where(conditions) | |
| 139 | + | .orderBy(desc(messages.timestamp)) | |
| 140 | + | .limit(fetchLimit); | |
| 141 | + | ||
| 142 | + | // Update cache with new messages if not paginating | |
| 143 | + | if (!beforeTimestamp && fetchedMessages.length > 0) { | |
| 144 | + | for (const msg of fetchedMessages) { | |
| 145 | + | messageCache.addMessage(msg); | |
| 146 | + | } | |
| 137 | 147 | } | |
| 138 | 148 | } | |
| 139 | 149 | ||
| @@ -142,29 +152,14 @@ export async function GET({ request, locals }) { | |||
| 142 | 152 | messages: fetchedMessages | |
| 143 | 153 | }; | |
| 144 | 154 | ||
| 145 | - | return new Response( | |
| 146 | - | JSON.stringify(response), | |
| 147 | - | { | |
| 148 | - | status: 200, | |
| 149 | - | headers: { 'Content-Type': 'application/json' } | |
| 155 | + | return new Response(JSON.stringify(response), { | |
| 156 | + | headers: { | |
| 157 | + | 'Content-Type': 'application/json' | |
| 150 | 158 | } | |
| 151 | - | ); | |
| 152 | - | } catch (error) { | |
| 153 | - | log.error('Error fetching messages', { | |
| 154 | - | error: error instanceof Error ? error.message : 'Unknown error', | |
| 155 | - | roomId | |
| 156 | 159 | }); | |
| 157 | - | const errorResponse: GetMessagesResponse = { | |
| 158 | - | success: false, | |
| 159 | - | error: 'Failed to fetch messages' | |
| 160 | - | }; | |
| 161 | - | return new Response( | |
| 162 | - | JSON.stringify(errorResponse), | |
| 163 | - | { | |
| 164 | - | status: 500, | |
| 165 | - | headers: { 'Content-Type': 'application/json' } | |
| 166 | - | } | |
| 167 | - | ); | |
| 160 | + | } catch (err) { | |
| 161 | + | log.error('Error fetching messages:', { error: err }); | |
| 162 | + | throw error(500, 'Failed to fetch messages'); | |
| 168 | 163 | } | |
| 169 | 164 | } | |
| 170 | 165 | ||
| @@ -7,15 +7,15 @@ import type { | |||||
| 7 | SendMessageResponse, | 7 | SendMessageResponse, | ||
| 8 | GetMessagesResponse | 8 | GetMessagesResponse | ||
| 9 | } from '$lib/types/payloads'; | 9 | } from '$lib/types/payloads'; | ||
| 10 | - | import { desc } from 'drizzle-orm'; | 10 | + | import { desc, eq, lt, and } from 'drizzle-orm'; |
| 11 | import { error } from '@sveltejs/kit'; | 11 | import { error } from '@sveltejs/kit'; | ||
| 12 | import { sseEmitter } from '$lib/sseEmitter'; | 12 | import { sseEmitter } from '$lib/sseEmitter'; | ||
| 13 | - | import { eq, lt, and } from 'drizzle-orm'; | |||
| 14 | import { chatRooms } from '$lib/db/schema'; | 13 | import { chatRooms } from '$lib/db/schema'; | ||
| 15 | import { users } from '$lib/db/schema'; | 14 | import { users } from '$lib/db/schema'; | ||
| 16 | import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; | 15 | import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; | ||
| 17 | import { messageCache } from '$lib/cache/message-cache'; | 16 | import { messageCache } from '$lib/cache/message-cache'; | ||
| 18 | import { createLogger } from '$lib/utils/logger.server'; | 17 | import { createLogger } from '$lib/utils/logger.server'; | ||
| 18 | + | import { SQL } from 'drizzle-orm'; | |||
| 19 | 19 | ||||
| 20 | const log = createLogger('chat-server'); | 20 | const log = createLogger('chat-server'); | ||
| 21 | 21 | ||||
| @@ -94,46 +94,56 @@ export async function GET({ request, locals }) { | |||||
| 94 | 94 | ||||
| 95 | // Try to get messages from cache first | 95 | // Try to get messages from cache first | ||
| 96 | if (!beforeTimestamp) { | 96 | if (!beforeTimestamp) { | ||
| 97 | - | log.debug('Attempting to fetch messages from cache', { roomId }); | 97 | + | log.debug('Attempting to fetch messages from cache', { |
| 98 | - | const cachedMessages = messageCache.getMessages(roomId); | 98 | + | roomId, |
| 99 | + | isAuthenticated: !!locals.session | |||
| 100 | + | }); | |||
| 101 | + | ||||
| 102 | + | // Use appropriate method based on authentication status | |||
| 103 | + | const cachedMessages = locals.session | |||
| 104 | + | ? messageCache.getMessages(roomId) | |||
| 105 | + | : messageCache.getPublicMessages(roomId); | |||
| 106 | + | ||||
| 99 | if (cachedMessages && cachedMessages.length > 0) { | 107 | if (cachedMessages && cachedMessages.length > 0) { | ||
| 100 | log.debug('Messages found in cache', { | 108 | log.debug('Messages found in cache', { | ||
| 109 | + | count: cachedMessages.length, | |||
| 101 | roomId, | 110 | roomId, | ||
| 102 | - | messageCount: cachedMessages.length | 111 | + | isAuthenticated: !!locals.session |
| 103 | }); | 112 | }); | ||
| 104 | fetchedMessages = cachedMessages; | 113 | fetchedMessages = cachedMessages; | ||
| 105 | } | 114 | } | ||
| 106 | } | 115 | } | ||
| 107 | 116 | ||||
| 108 | - | // If no cached messages or pagination request, fetch from DB | 117 | + | // If no cached messages or pagination requested, fetch from DB |
| 109 | - | if (fetchedMessages.length === 0) { | 118 | + | if (fetchedMessages.length === 0 || beforeTimestamp) { |
| 110 | - | if (!locals.user) { | 119 | + | log.debug('Fetching messages from database', { |
| 111 | - | log.warn('Authentication required'); | 120 | + | beforeTimestamp, |
| 112 | - | throw error(401, 'Authentication required'); | 121 | + | roomId, |
| 113 | - | } | 122 | + | isAuthenticated: !!locals.session |
| 123 | + | }); | |||
| 124 | + | ||||
| 125 | + | let conditions = eq(messages.chatRoomId, roomId) as SQL<unknown>; | |||
| 114 | 126 | ||||
| 115 | if (beforeTimestamp) { | 127 | if (beforeTimestamp) { | ||
| 116 | - | log.debug('Fetching messages from DB before timestamp', { | 128 | + | conditions = and( |
| 117 | - | roomId, | 129 | + | conditions, |
| 118 | - | beforeTimestamp | 130 | + | lt(messages.timestamp, parseInt(beforeTimestamp)) |
| 119 | - | }); | 131 | + | ) as SQL<unknown>; |
| 120 | - | const query = db.select() | 132 | + | } |
| 121 | - | .from(messages) | 133 | + | |
| 122 | - | .where(and( | 134 | + | // Build and execute query with conditions |
| 123 | - | eq(messages.chatRoomId, roomId), | 135 | + | const fetchLimit = locals.session ? 100 : 10; |
| 124 | - | lt(messages.timestamp, parseInt(beforeTimestamp)) | 136 | + | fetchedMessages = await db.select() |
| 125 | - | )) | 137 | + | .from(messages) |
| 126 | - | .orderBy(desc(messages.timestamp)) | 138 | + | .where(conditions) |
| 127 | - | .limit(100); | 139 | + | .orderBy(desc(messages.timestamp)) |
| 128 | - | fetchedMessages = (await query).reverse(); | 140 | + | .limit(fetchLimit); |
| 129 | - | } else { | 141 | + | |
| 130 | - | log.debug('Fetching messages from DB for instant chat', { roomId }); | 142 | + | // Update cache with new messages if not paginating |
| 131 | - | const query = db.select() | 143 | + | if (!beforeTimestamp && fetchedMessages.length > 0) { |
| 132 | - | .from(messages) | 144 | + | for (const msg of fetchedMessages) { |
| 133 | - | .where(eq(messages.chatRoomId, roomId)) | 145 | + | messageCache.addMessage(msg); |
| 134 | - | .orderBy(desc(messages.timestamp)) | 146 | + | } |
| 135 | - | .limit(50); | |||
| 136 | - | fetchedMessages = (await query).reverse(); | |||
| 137 | } | 147 | } | ||
| 138 | } | 148 | } | ||
| 139 | 149 | ||||
| @@ -142,29 +152,14 @@ export async function GET({ request, locals }) { | |||||
| 142 | messages: fetchedMessages | 152 | messages: fetchedMessages | ||
| 143 | }; | 153 | }; | ||
| 144 | 154 | ||||
| 145 | - | return new Response( | 155 | + | return new Response(JSON.stringify(response), { |
| 146 | - | JSON.stringify(response), | 156 | + | headers: { |
| 147 | - | { | 157 | + | 'Content-Type': 'application/json' |
| 148 | - | status: 200, | |||
| 149 | - | headers: { 'Content-Type': 'application/json' } | |||
| 150 | } | 158 | } | ||
| 151 | - | ); | |||
| 152 | - | } catch (error) { | |||
| 153 | - | log.error('Error fetching messages', { | |||
| 154 | - | error: error instanceof Error ? error.message : 'Unknown error', | |||
| 155 | - | roomId | |||
| 156 | }); | 159 | }); | ||
| 157 | - | const errorResponse: GetMessagesResponse = { | 160 | + | } catch (err) { |
| 158 | - | success: false, | 161 | + | log.error('Error fetching messages:', { error: err }); |
| 159 | - | error: 'Failed to fetch messages' | 162 | + | throw error(500, 'Failed to fetch messages'); |
| 160 | - | }; | |||
| 161 | - | return new Response( | |||
| 162 | - | JSON.stringify(errorResponse), | |||
| 163 | - | { | |||
| 164 | - | status: 500, | |||
| 165 | - | headers: { 'Content-Type': 'application/json' } | |||
| 166 | - | } | |||
| 167 | - | ); | |||
| 168 | } | 163 | } | ||
| 169 | } | 164 | } | ||
| 170 | 165 | ||||