detached e1cfa3d

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 UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
2 files · +79 -62
M src/lib/cache/message-cache.ts +31 -9
@@ -15,16 +15,19 @@ class MessageCache {
1515 private readonly maxMessagesPerRoom: number;
1616 private readonly maxRooms: number;
1717 private readonly cacheExpiryMs: number;
18+ private readonly publicMessageLimit: number;
1819
1920 private constructor() {
2021 this.rooms = new Map();
2122 this.maxMessagesPerRoom = 100; // Keep last 100 messages per room
2223 this.maxRooms = 100; // Maximum number of rooms to cache
2324 this.cacheExpiryMs = 30 * 60 * 1000; // 30 minutes cache expiry
25+ this.publicMessageLimit = 10; // Strict limit for public/non-auth users
2426 log.debug('[MessageCache] Initialized with settings:', {
2527 maxMessagesPerRoom: this.maxMessagesPerRoom,
2628 maxRooms: this.maxRooms,
27- cacheExpiryMs: this.cacheExpiryMs
29+ cacheExpiryMs: this.cacheExpiryMs,
30+ publicMessageLimit: this.publicMessageLimit
2831 });
2932 }
3033
@@ -98,19 +101,38 @@ class MessageCache {
98101 });
99102 }
100103
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 });
104113 return [];
105114 }
106115
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);
109127
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 [];
112131 }
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);
114136 }
115137
116138 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 room22 this.maxMessagesPerRoom = 100; // Keep last 100 messages per room
22 this.maxRooms = 100; // Maximum number of rooms to cache23 this.maxRooms = 100; // Maximum number of rooms to cache
23 this.cacheExpiryMs = 30 * 60 * 1000; // 30 minutes cache expiry24 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.cacheExpiryMs29+ 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 time116+ 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 modifications132+
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 {
77 SendMessageResponse,
88 GetMessagesResponse
99 } from '$lib/types/payloads';
10-import { desc } from 'drizzle-orm';
10+import { desc, eq, lt, and } from 'drizzle-orm';
1111 import { error } from '@sveltejs/kit';
1212 import { sseEmitter } from '$lib/sseEmitter';
13-import { eq, lt, and } from 'drizzle-orm';
1413 import { chatRooms } from '$lib/db/schema';
1514 import { users } from '$lib/db/schema';
1615 import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server';
1716 import { messageCache } from '$lib/cache/message-cache';
1817 import { createLogger } from '$lib/utils/logger.server';
18+import { SQL } from 'drizzle-orm';
1919
2020 const log = createLogger('chat-server');
2121
@@ -94,46 +94,56 @@ export async function GET({ request, locals }) {
9494
9595 // Try to get messages from cache first
9696 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+
99107 if (cachedMessages && cachedMessages.length > 0) {
100108 log.debug('Messages found in cache', {
109+ count: cachedMessages.length,
101110 roomId,
102- messageCount: cachedMessages.length
111+ isAuthenticated: !!locals.session
103112 });
104113 fetchedMessages = cachedMessages;
105114 }
106115 }
107116
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>;
114126
115127 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+ }
137147 }
138148 }
139149
@@ -142,29 +152,14 @@ export async function GET({ request, locals }) {
142152 messages: fetchedMessages
143153 };
144154
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'
150158 }
151- );
152- } catch (error) {
153- log.error('Error fetching messages', {
154- error: error instanceof Error ? error.message : 'Unknown error',
155- roomId
156159 });
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');
168163 }
169164 }
170165
@@ -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 first95 // 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.length111+ 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 DB117+ // 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- beforeTimestamp130+ 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: fetchedMessages152 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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 9

↑↓ moveenter openesc close