detached b4dc098

refactor(api): remove unused endpoints and SSE references

commit
b4dc098f6b374bb3154aa1312d5396054fc1275a
author
bnema · · 2026-01-18 22:23 UTC
parent
cb21e20
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +10 -313
M src/hooks.server.ts +3 -11
@@ -123,13 +123,11 @@ export const handle: Handle = async ({ event, resolve }) => {
123123 }
124124 }
125125
126- // Identify public chat and room requests if there is no valid session
126+ // Identify public chat requests if there is no valid session
127127 const isPublicChatRequest =
128128 event.url.pathname.startsWith('/api/chat/messages') && event.request.method === 'GET';
129- const isPublicRoomRequest =
130- event.url.pathname.startsWith('/api/rooms/') && event.request.method === 'GET';
131129
132- if ((isPublicChatRequest || isPublicRoomRequest) && !event.locals.session) {
130+ if (isPublicChatRequest && !event.locals.session) {
133131 log.debug('Public request accessed', { path: event.url.pathname });
134132 const response = await resolve(event);
135133 return addSecurityHeaders(response);
@@ -152,14 +150,8 @@ export const handle: Handle = async ({ event, resolve }) => {
152150 return addSecurityHeaders(response);
153151 }
154152
155- // Skip rate limiting for SSE endpoint when authenticated
156- if (event.url.pathname.startsWith('/api/sse')) {
157- const response = await resolve(event);
158- return addSecurityHeaders(response);
159- }
160-
161153 // Apply rate limiting for non-public endpoints
162- if (!isPublicChatRequest && !isPublicRoomRequest) {
154+ if (!isPublicChatRequest) {
163155 const rateLimitResult = await handleRateLimit(event);
164156 if (rateLimitResult?.status === 429) {
165157 return new Response('Too Many Requests', {
@@ -123,13 +123,11 @@ export const handle: Handle = async ({ event, resolve }) => {
123 }123 }
124 }124 }
125 125
126- // Identify public chat and room requests if there is no valid session126+ // Identify public chat requests if there is no valid session
127 const isPublicChatRequest =127 const isPublicChatRequest =
128 event.url.pathname.startsWith('/api/chat/messages') && event.request.method === 'GET';128 event.url.pathname.startsWith('/api/chat/messages') && event.request.method === 'GET';
129- const isPublicRoomRequest =
130- event.url.pathname.startsWith('/api/rooms/') && event.request.method === 'GET';
131 129
132- if ((isPublicChatRequest || isPublicRoomRequest) && !event.locals.session) {130+ if (isPublicChatRequest && !event.locals.session) {
133 log.debug('Public request accessed', { path: event.url.pathname });131 log.debug('Public request accessed', { path: event.url.pathname });
134 const response = await resolve(event);132 const response = await resolve(event);
135 return addSecurityHeaders(response);133 return addSecurityHeaders(response);
@@ -152,14 +150,8 @@ export const handle: Handle = async ({ event, resolve }) => {
152 return addSecurityHeaders(response);150 return addSecurityHeaders(response);
153 }151 }
154 152
155- // Skip rate limiting for SSE endpoint when authenticated
156- if (event.url.pathname.startsWith('/api/sse')) {
157- const response = await resolve(event);
158- return addSecurityHeaders(response);
159- }
160-
161 // Apply rate limiting for non-public endpoints153 // Apply rate limiting for non-public endpoints
162- if (!isPublicChatRequest && !isPublicRoomRequest) {154+ if (!isPublicChatRequest) {
163 const rateLimitResult = await handleRateLimit(event);155 const rateLimitResult = await handleRateLimit(event);
164 if (rateLimitResult?.status === 429) {156 if (rateLimitResult?.status === 429) {
165 return new Response('Too Many Requests', {157 return new Response('Too Many Requests', {
M src/lib/api/rate-limiter.ts +7 -80
@@ -13,7 +13,7 @@ const RATE_LIMITS = {
1313 durationMs: 5 * 60 * 1000
1414 },
1515
16- // Public endpoints (public chat, rooms)
16+ // Public endpoints (public chat)
1717 public: {
1818 points: isDev ? 1000 : 30,
1919 durationMs: 60 * 1000
@@ -23,18 +23,6 @@ const RATE_LIMITS = {
2323 protected: {
2424 points: isDev ? 1000 : 100,
2525 durationMs: 60 * 1000
26- },
27-
28- // SSE endpoints - separate limits for authenticated and unauthenticated users
29- sse: {
30- authenticated: {
31- points: isDev ? 1000 : 500,
32- durationMs: 60 * 1000
33- },
34- unauthenticated: {
35- points: 0,
36- durationMs: 60 * 1000
37- }
3826 }
3927 } as const;
4028
@@ -45,9 +33,7 @@ const ipRequests = new Map<string, Array<{ timestamp: number }>>();
4533 const MAX_RATE_LIMIT_DURATION = Math.max(
4634 RATE_LIMITS.auth.durationMs,
4735 RATE_LIMITS.public.durationMs,
48- RATE_LIMITS.protected.durationMs,
49- RATE_LIMITS.sse.authenticated.durationMs,
50- RATE_LIMITS.sse.unauthenticated.durationMs
36+ RATE_LIMITS.protected.durationMs
5137 );
5238
5339 // Lazy cleanup: remove stale entries on each check (Cloudflare Workers don't allow setInterval at global scope)
@@ -73,31 +59,15 @@ function getEndpointType(pathname: string, isPublic: boolean): keyof typeof RATE
7359 if (pathname.startsWith('/api/session/login') || pathname.startsWith('/api/register')) {
7460 return 'auth';
7561 }
76- if (pathname.startsWith('/api/sse')) {
77- return 'sse';
78- }
7962 if (isPublic) {
8063 return 'public';
8164 }
8265 return 'protected';
8366 }
8467
85-function getRateLimitPoints(
86- endpointType: keyof typeof RATE_LIMITS,
87- isAuthenticated: boolean
88-): number {
89- if (endpointType === 'sse') {
90- return isAuthenticated
91- ? RATE_LIMITS.sse.authenticated.points
92- : RATE_LIMITS.sse.unauthenticated.points;
93- }
94- return RATE_LIMITS[endpointType].points;
95-}
96-
9768 function isRateLimited(
9869 ip: string,
99- endpointType: keyof typeof RATE_LIMITS,
100- isAuthenticated: boolean = false
70+ endpointType: keyof typeof RATE_LIMITS
10171 ): { limited: boolean; retryAfter?: number } {
10272 // Lazy cleanup on each check (replaces setInterval for Cloudflare compatibility)
10373 cleanupStaleEntries();
@@ -108,38 +78,6 @@ function isRateLimited(
10878 }
10979
11080 const now = Date.now();
111-
112- // Handle SSE endpoints differently based on authentication status
113- if (endpointType === 'sse') {
114- const config = isAuthenticated
115- ? RATE_LIMITS.sse.authenticated
116- : RATE_LIMITS.sse.unauthenticated;
117- const { points, durationMs } = config;
118-
119- // Get or initialize request history for this IP
120- const requests = ipRequests.get(ip) || [];
121-
122- // Clean old requests outside the window
123- const validRequests = requests.filter((req) => now - req.timestamp < durationMs);
124-
125- // Check if we're over the limit
126- if (validRequests.length >= points) {
127- log.debug(
128- `Rate limit exceeded for IP ${ip} on ${endpointType} endpoint (${isAuthenticated ? 'authenticated' : 'unauthenticated'})`
129- );
130- const oldestRequest = validRequests[0];
131- const retryAfter = durationMs - (now - oldestRequest.timestamp);
132- return { limited: true, retryAfter };
133- }
134-
135- // Add this request
136- validRequests.push({ timestamp: now });
137- ipRequests.set(ip, validRequests);
138-
139- return { limited: false };
140- }
141-
142- // Handle other endpoints
14381 const { points, durationMs } = RATE_LIMITS[endpointType];
14482 const requests = ipRequests.get(ip) || [];
14583 const validRequests = requests.filter((req) => now - req.timestamp < durationMs);
@@ -164,7 +102,6 @@ export async function handleRateLimit(event: RequestEvent): Promise<Response | n
164102 const ip = event.getClientAddress();
165103 const url = new URL(event.request.url);
166104 const isPublic = url.searchParams.get('public') === 'true';
167- const isAuthenticated = Boolean(event.locals.user);
168105
169106 // Skip rate limiting for non-API routes
170107 if (!url.pathname.startsWith('/api')) {
@@ -172,16 +109,12 @@ export async function handleRateLimit(event: RequestEvent): Promise<Response | n
172109 }
173110
174111 const endpointType = getEndpointType(url.pathname, isPublic);
175- const { limited, retryAfter } = isRateLimited(ip, endpointType, isAuthenticated);
112+ const { limited, retryAfter } = isRateLimited(ip, endpointType);
176113
177114 if (limited) {
178115 // Construct a user-friendly error message
179116 let errorMessage = 'Too many requests. ';
180- if (endpointType === 'sse') {
181- errorMessage += isAuthenticated
182- ? 'Please wait before attempting to reconnect to the chat.'
183- : 'Please wait before attempting to view the public chat again.';
184- } else if (endpointType === 'auth') {
117+ if (endpointType === 'auth') {
185118 errorMessage += 'Please wait before attempting to log in again.';
186119 } else {
187120 errorMessage += 'Please try again later.';
@@ -196,13 +129,11 @@ export async function handleRateLimit(event: RequestEvent): Promise<Response | n
196129 // Only include potentially sensitive info in dev mode
197130 if (isDev) {
198131 responseBody.endpointType = endpointType;
199- responseBody.isAuthenticated = isAuthenticated;
200132 responseBody.debug = {
201- isAuthenticated,
202133 hasUser: Boolean(event.locals.user),
203134 hasSession: Boolean(event.locals.session),
204135 endpointType,
205- rateLimitPoints: getRateLimitPoints(endpointType, isAuthenticated)
136+ rateLimitPoints: RATE_LIMITS[endpointType].points
206137 };
207138 }
208139
@@ -213,14 +144,10 @@ export async function handleRateLimit(event: RequestEvent): Promise<Response | n
213144 };
214145
215146 if (isDev) {
216- headers['X-Debug-Is-Authenticated'] = isAuthenticated.toString();
217147 headers['X-Debug-Has-User'] = Boolean(event.locals.user).toString();
218148 headers['X-Debug-Has-Session'] = Boolean(event.locals.session).toString();
219149 headers['X-Debug-Endpoint-Type'] = endpointType;
220- headers['X-Debug-Rate-Limit-Points'] = getRateLimitPoints(
221- endpointType,
222- isAuthenticated
223- ).toString();
150+ headers['X-Debug-Rate-Limit-Points'] = RATE_LIMITS[endpointType].points.toString();
224151 }
225152
226153 return new Response(JSON.stringify(responseBody), {
@@ -13,7 +13,7 @@ const RATE_LIMITS = {
13 durationMs: 5 * 60 * 100013 durationMs: 5 * 60 * 1000
14 },14 },
15 15
16- // Public endpoints (public chat, rooms)16+ // Public endpoints (public chat)
17 public: {17 public: {
18 points: isDev ? 1000 : 30,18 points: isDev ? 1000 : 30,
19 durationMs: 60 * 100019 durationMs: 60 * 1000
@@ -23,18 +23,6 @@ const RATE_LIMITS = {
23 protected: {23 protected: {
24 points: isDev ? 1000 : 100,24 points: isDev ? 1000 : 100,
25 durationMs: 60 * 100025 durationMs: 60 * 1000
26- },
27-
28- // SSE endpoints - separate limits for authenticated and unauthenticated users
29- sse: {
30- authenticated: {
31- points: isDev ? 1000 : 500,
32- durationMs: 60 * 1000
33- },
34- unauthenticated: {
35- points: 0,
36- durationMs: 60 * 1000
37- }
38 }26 }
39 } as const;27 } as const;
40 28
@@ -45,9 +33,7 @@ const ipRequests = new Map<string, Array<{ timestamp: number }>>();
45 const MAX_RATE_LIMIT_DURATION = Math.max(33 const MAX_RATE_LIMIT_DURATION = Math.max(
46 RATE_LIMITS.auth.durationMs,34 RATE_LIMITS.auth.durationMs,
47 RATE_LIMITS.public.durationMs,35 RATE_LIMITS.public.durationMs,
48- RATE_LIMITS.protected.durationMs,36+ RATE_LIMITS.protected.durationMs
49- RATE_LIMITS.sse.authenticated.durationMs,
50- RATE_LIMITS.sse.unauthenticated.durationMs
51 );37 );
52 38
53 // Lazy cleanup: remove stale entries on each check (Cloudflare Workers don't allow setInterval at global scope)39 // Lazy cleanup: remove stale entries on each check (Cloudflare Workers don't allow setInterval at global scope)
@@ -73,31 +59,15 @@ function getEndpointType(pathname: string, isPublic: boolean): keyof typeof RATE
73 if (pathname.startsWith('/api/session/login') || pathname.startsWith('/api/register')) {59 if (pathname.startsWith('/api/session/login') || pathname.startsWith('/api/register')) {
74 return 'auth';60 return 'auth';
75 }61 }
76- if (pathname.startsWith('/api/sse')) {
77- return 'sse';
78- }
79 if (isPublic) {62 if (isPublic) {
80 return 'public';63 return 'public';
81 }64 }
82 return 'protected';65 return 'protected';
83 }66 }
84 67
85-function getRateLimitPoints(
86- endpointType: keyof typeof RATE_LIMITS,
87- isAuthenticated: boolean
88-): number {
89- if (endpointType === 'sse') {
90- return isAuthenticated
91- ? RATE_LIMITS.sse.authenticated.points
92- : RATE_LIMITS.sse.unauthenticated.points;
93- }
94- return RATE_LIMITS[endpointType].points;
95-}
96-
97 function isRateLimited(68 function isRateLimited(
98 ip: string,69 ip: string,
99- endpointType: keyof typeof RATE_LIMITS,70+ endpointType: keyof typeof RATE_LIMITS
100- isAuthenticated: boolean = false
101 ): { limited: boolean; retryAfter?: number } {71 ): { limited: boolean; retryAfter?: number } {
102 // Lazy cleanup on each check (replaces setInterval for Cloudflare compatibility)72 // Lazy cleanup on each check (replaces setInterval for Cloudflare compatibility)
103 cleanupStaleEntries();73 cleanupStaleEntries();
@@ -108,38 +78,6 @@ function isRateLimited(
108 }78 }
109 79
110 const now = Date.now();80 const now = Date.now();
111-
112- // Handle SSE endpoints differently based on authentication status
113- if (endpointType === 'sse') {
114- const config = isAuthenticated
115- ? RATE_LIMITS.sse.authenticated
116- : RATE_LIMITS.sse.unauthenticated;
117- const { points, durationMs } = config;
118-
119- // Get or initialize request history for this IP
120- const requests = ipRequests.get(ip) || [];
121-
122- // Clean old requests outside the window
123- const validRequests = requests.filter((req) => now - req.timestamp < durationMs);
124-
125- // Check if we're over the limit
126- if (validRequests.length >= points) {
127- log.debug(
128- `Rate limit exceeded for IP ${ip} on ${endpointType} endpoint (${isAuthenticated ? 'authenticated' : 'unauthenticated'})`
129- );
130- const oldestRequest = validRequests[0];
131- const retryAfter = durationMs - (now - oldestRequest.timestamp);
132- return { limited: true, retryAfter };
133- }
134-
135- // Add this request
136- validRequests.push({ timestamp: now });
137- ipRequests.set(ip, validRequests);
138-
139- return { limited: false };
140- }
141-
142- // Handle other endpoints
143 const { points, durationMs } = RATE_LIMITS[endpointType];81 const { points, durationMs } = RATE_LIMITS[endpointType];
144 const requests = ipRequests.get(ip) || [];82 const requests = ipRequests.get(ip) || [];
145 const validRequests = requests.filter((req) => now - req.timestamp < durationMs);83 const validRequests = requests.filter((req) => now - req.timestamp < durationMs);
@@ -164,7 +102,6 @@ export async function handleRateLimit(event: RequestEvent): Promise<Response | n
164 const ip = event.getClientAddress();102 const ip = event.getClientAddress();
165 const url = new URL(event.request.url);103 const url = new URL(event.request.url);
166 const isPublic = url.searchParams.get('public') === 'true';104 const isPublic = url.searchParams.get('public') === 'true';
167- const isAuthenticated = Boolean(event.locals.user);
168 105
169 // Skip rate limiting for non-API routes106 // Skip rate limiting for non-API routes
170 if (!url.pathname.startsWith('/api')) {107 if (!url.pathname.startsWith('/api')) {
@@ -172,16 +109,12 @@ export async function handleRateLimit(event: RequestEvent): Promise<Response | n
172 }109 }
173 110
174 const endpointType = getEndpointType(url.pathname, isPublic);111 const endpointType = getEndpointType(url.pathname, isPublic);
175- const { limited, retryAfter } = isRateLimited(ip, endpointType, isAuthenticated);112+ const { limited, retryAfter } = isRateLimited(ip, endpointType);
176 113
177 if (limited) {114 if (limited) {
178 // Construct a user-friendly error message115 // Construct a user-friendly error message
179 let errorMessage = 'Too many requests. ';116 let errorMessage = 'Too many requests. ';
180- if (endpointType === 'sse') {117+ if (endpointType === 'auth') {
181- errorMessage += isAuthenticated
182- ? 'Please wait before attempting to reconnect to the chat.'
183- : 'Please wait before attempting to view the public chat again.';
184- } else if (endpointType === 'auth') {
185 errorMessage += 'Please wait before attempting to log in again.';118 errorMessage += 'Please wait before attempting to log in again.';
186 } else {119 } else {
187 errorMessage += 'Please try again later.';120 errorMessage += 'Please try again later.';
@@ -196,13 +129,11 @@ export async function handleRateLimit(event: RequestEvent): Promise<Response | n
196 // Only include potentially sensitive info in dev mode129 // Only include potentially sensitive info in dev mode
197 if (isDev) {130 if (isDev) {
198 responseBody.endpointType = endpointType;131 responseBody.endpointType = endpointType;
199- responseBody.isAuthenticated = isAuthenticated;
200 responseBody.debug = {132 responseBody.debug = {
201- isAuthenticated,
202 hasUser: Boolean(event.locals.user),133 hasUser: Boolean(event.locals.user),
203 hasSession: Boolean(event.locals.session),134 hasSession: Boolean(event.locals.session),
204 endpointType,135 endpointType,
205- rateLimitPoints: getRateLimitPoints(endpointType, isAuthenticated)136+ rateLimitPoints: RATE_LIMITS[endpointType].points
206 };137 };
207 }138 }
208 139
@@ -213,14 +144,10 @@ export async function handleRateLimit(event: RequestEvent): Promise<Response | n
213 };144 };
214 145
215 if (isDev) {146 if (isDev) {
216- headers['X-Debug-Is-Authenticated'] = isAuthenticated.toString();
217 headers['X-Debug-Has-User'] = Boolean(event.locals.user).toString();147 headers['X-Debug-Has-User'] = Boolean(event.locals.user).toString();
218 headers['X-Debug-Has-Session'] = Boolean(event.locals.session).toString();148 headers['X-Debug-Has-Session'] = Boolean(event.locals.session).toString();
219 headers['X-Debug-Endpoint-Type'] = endpointType;149 headers['X-Debug-Endpoint-Type'] = endpointType;
220- headers['X-Debug-Rate-Limit-Points'] = getRateLimitPoints(150+ headers['X-Debug-Rate-Limit-Points'] = RATE_LIMITS[endpointType].points.toString();
221- endpointType,
222- isAuthenticated
223- ).toString();
224 }151 }
225 152
226 return new Response(JSON.stringify(responseBody), {153 return new Response(JSON.stringify(responseBody), {
D src/routes/api/rooms/[roomId]/+server.ts +0 -101
@@ -1,101 +0,0 @@
1-import { users } from '$lib/db/convex.server';
2-import type { SafeUser } from '$lib/types/chat';
3-import type { PublicRoomResponse } from '$lib/types/payloads';
4-import { createSafeUser } from '$lib/types/chat';
5-import { createLogger } from '$lib/utils/logger.server';
6-import type { RequestHandler } from './$types';
7-
8-const log = createLogger('rooms-server');
9-
10-const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
11-
12-export const GET: RequestHandler = async ({ params, url, locals }) => {
13- const { roomId } = params;
14- const isPublic = !locals.user || url.searchParams.get('public') === 'true';
15-
16- try {
17- log.debug('Fetching room data', {
18- roomId,
19- isPublic,
20- requestedBy: locals.user
21- ? `${locals.user.id.slice(0, 4)}...${locals.user.id.slice(-4)}`
22- : 'public'
23- });
24-
25- // Calculate timeout threshold
26- const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS;
27-
28- // Fetch all users from Convex
29- const fetchedUsers = await users.list();
30-
31- // Process users to mark them as offline if they've timed out
32- const processedUsers = fetchedUsers.map((user) => {
33- const lastSeen = user.lastSeen ?? 0;
34- const shouldBeOffline = user.status !== 'offline' && lastSeen < timeoutThreshold;
35- return {
36- id: user._id,
37- nickname: user.nickname,
38- status: shouldBeOffline ? 'offline' : user.status,
39- avatarUrl: user.avatarUrl,
40- lastSeen
41- };
42- });
43-
44- // Find users who should be marked offline and update them
45- const usersToUpdate = processedUsers.filter((user, index) => {
46- const original = fetchedUsers[index];
47- return user.status === 'offline' && original.status !== 'offline';
48- });
49-
50- // Update timed-out users in Convex
51- if (usersToUpdate.length > 0) {
52- await Promise.all(usersToUpdate.map((user) => users.updateStatus(user.id, 'offline')));
53-
54- log.debug('Updated offline status for users', {
55- count: usersToUpdate.length,
56- userIds: usersToUpdate.map((u) => u.id)
57- });
58- }
59-
60- // Sanitize user data
61- const sanitizedUsers: SafeUser[] = processedUsers.map((user) =>
62- createSafeUser({
63- id: user.id,
64- nickname: user.nickname,
65- status: user.status,
66- avatarUrl: user.avatarUrl,
67- lastSeen: user.lastSeen
68- })
69- );
70-
71- const responseData: PublicRoomResponse = {
72- success: true,
73- buddyList: sanitizedUsers
74- };
75-
76- log.debug('Successfully fetched room data', {
77- roomId,
78- userCount: sanitizedUsers.length,
79- isPublic,
80- offlineUpdates: usersToUpdate.length
81- });
82-
83- return new Response(JSON.stringify(responseData), {
84- status: 200,
85- headers: { 'Content-Type': 'application/json' }
86- });
87- } catch (error) {
88- log.error('Error fetching room data', {
89- roomId,
90- error: error instanceof Error ? error.message : 'Unknown error'
91- });
92- const errorResponse: PublicRoomResponse = {
93- success: false,
94- error: 'Failed to fetch room data'
95- };
96- return new Response(JSON.stringify(errorResponse), {
97- status: 500,
98- headers: { 'Content-Type': 'application/json' }
99- });
100- }
101-};
@@ -1,101 +0,0 @@
1-import { users } from '$lib/db/convex.server';
2-import type { SafeUser } from '$lib/types/chat';
3-import type { PublicRoomResponse } from '$lib/types/payloads';
4-import { createSafeUser } from '$lib/types/chat';
5-import { createLogger } from '$lib/utils/logger.server';
6-import type { RequestHandler } from './$types';
7-
8-const log = createLogger('rooms-server');
9-
10-const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
11-
12-export const GET: RequestHandler = async ({ params, url, locals }) => {
13- const { roomId } = params;
14- const isPublic = !locals.user || url.searchParams.get('public') === 'true';
15-
16- try {
17- log.debug('Fetching room data', {
18- roomId,
19- isPublic,
20- requestedBy: locals.user
21- ? `${locals.user.id.slice(0, 4)}...${locals.user.id.slice(-4)}`
22- : 'public'
23- });
24-
25- // Calculate timeout threshold
26- const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS;
27-
28- // Fetch all users from Convex
29- const fetchedUsers = await users.list();
30-
31- // Process users to mark them as offline if they've timed out
32- const processedUsers = fetchedUsers.map((user) => {
33- const lastSeen = user.lastSeen ?? 0;
34- const shouldBeOffline = user.status !== 'offline' && lastSeen < timeoutThreshold;
35- return {
36- id: user._id,
37- nickname: user.nickname,
38- status: shouldBeOffline ? 'offline' : user.status,
39- avatarUrl: user.avatarUrl,
40- lastSeen
41- };
42- });
43-
44- // Find users who should be marked offline and update them
45- const usersToUpdate = processedUsers.filter((user, index) => {
46- const original = fetchedUsers[index];
47- return user.status === 'offline' && original.status !== 'offline';
48- });
49-
50- // Update timed-out users in Convex
51- if (usersToUpdate.length > 0) {
52- await Promise.all(usersToUpdate.map((user) => users.updateStatus(user.id, 'offline')));
53-
54- log.debug('Updated offline status for users', {
55- count: usersToUpdate.length,
56- userIds: usersToUpdate.map((u) => u.id)
57- });
58- }
59-
60- // Sanitize user data
61- const sanitizedUsers: SafeUser[] = processedUsers.map((user) =>
62- createSafeUser({
63- id: user.id,
64- nickname: user.nickname,
65- status: user.status,
66- avatarUrl: user.avatarUrl,
67- lastSeen: user.lastSeen
68- })
69- );
70-
71- const responseData: PublicRoomResponse = {
72- success: true,
73- buddyList: sanitizedUsers
74- };
75-
76- log.debug('Successfully fetched room data', {
77- roomId,
78- userCount: sanitizedUsers.length,
79- isPublic,
80- offlineUpdates: usersToUpdate.length
81- });
82-
83- return new Response(JSON.stringify(responseData), {
84- status: 200,
85- headers: { 'Content-Type': 'application/json' }
86- });
87- } catch (error) {
88- log.error('Error fetching room data', {
89- roomId,
90- error: error instanceof Error ? error.message : 'Unknown error'
91- });
92- const errorResponse: PublicRoomResponse = {
93- success: false,
94- error: 'Failed to fetch room data'
95- };
96- return new Response(JSON.stringify(errorResponse), {
97- status: 500,
98- headers: { 'Content-Type': 'application/json' }
99- });
100- }
101-};
D src/routes/api/rooms/default/+server.ts +0 -42
@@ -1,42 +0,0 @@
1-import { chatRooms } from '$lib/db/convex.server';
2-import { createLogger } from '$lib/utils/logger.server';
3-import type { RequestHandler } from './$types';
4-
5-const log = createLogger('rooms-default-server');
6-
7-export const GET: RequestHandler = async () => {
8- try {
9- log.debug('Fetching default room');
10-
11- const room = await chatRooms.getOrCreateDefault();
12-
13- log.debug('Successfully fetched default room', {
14- roomId: room._id
15- });
16-
17- return new Response(
18- JSON.stringify({
19- success: true,
20- roomId: room._id
21- }),
22- {
23- status: 200,
24- headers: { 'Content-Type': 'application/json' }
25- }
26- );
27- } catch (error) {
28- log.error('Error fetching default room', {
29- error: error instanceof Error ? error.message : 'Unknown error'
30- });
31- return new Response(
32- JSON.stringify({
33- success: false,
34- error: 'Failed to fetch default room'
35- }),
36- {
37- status: 500,
38- headers: { 'Content-Type': 'application/json' }
39- }
40- );
41- }
42-};
@@ -1,42 +0,0 @@
1-import { chatRooms } from '$lib/db/convex.server';
2-import { createLogger } from '$lib/utils/logger.server';
3-import type { RequestHandler } from './$types';
4-
5-const log = createLogger('rooms-default-server');
6-
7-export const GET: RequestHandler = async () => {
8- try {
9- log.debug('Fetching default room');
10-
11- const room = await chatRooms.getOrCreateDefault();
12-
13- log.debug('Successfully fetched default room', {
14- roomId: room._id
15- });
16-
17- return new Response(
18- JSON.stringify({
19- success: true,
20- roomId: room._id
21- }),
22- {
23- status: 200,
24- headers: { 'Content-Type': 'application/json' }
25- }
26- );
27- } catch (error) {
28- log.error('Error fetching default room', {
29- error: error instanceof Error ? error.message : 'Unknown error'
30- });
31- return new Response(
32- JSON.stringify({
33- success: false,
34- error: 'Failed to fetch default room'
35- }),
36- {
37- status: 500,
38- headers: { 'Content-Type': 'application/json' }
39- }
40- );
41- }
42-};
D src/routes/api/session/logout/+server.ts +0 -33
@@ -1,33 +0,0 @@
1-import { json } from '@sveltejs/kit';
2-import type { RequestHandler } from './$types';
3-import { invalidateSession } from '$lib/api/session.server';
4-import { sha256 } from '$lib/api/session.server'; // re-use sha256 helper to compute session id
5-import { createLogger } from '$lib/utils/logger.server';
6-
7-const log = createLogger('logout-server');
8-
9-export const POST: RequestHandler = async ({ cookies }) => {
10- const token = cookies.get('session');
11-
12- if (!token) {
13- log.debug('No session token found in cookies');
14- return json({ success: true, invalidated: false });
15- }
16-
17- try {
18- const sessionId = await sha256(token);
19- await invalidateSession(sessionId);
20- cookies.delete('session', { path: '/' });
21- log.info('Session invalidated', {
22- sessionId: `${sessionId.slice(0, 4)}...${sessionId.slice(-4)}`
23- });
24- return json({ success: true, invalidated: true });
25- } catch (error) {
26- log.error('Failed to invalidate session', {
27- error: error instanceof Error ? error.message : 'Unknown error'
28- });
29- // Still delete cookie to ensure client is logged out
30- cookies.delete('session', { path: '/' });
31- return json({ success: false, error: 'Failed to invalidate session' }, { status: 500 });
32- }
33-};
@@ -1,33 +0,0 @@
1-import { json } from '@sveltejs/kit';
2-import type { RequestHandler } from './$types';
3-import { invalidateSession } from '$lib/api/session.server';
4-import { sha256 } from '$lib/api/session.server'; // re-use sha256 helper to compute session id
5-import { createLogger } from '$lib/utils/logger.server';
6-
7-const log = createLogger('logout-server');
8-
9-export const POST: RequestHandler = async ({ cookies }) => {
10- const token = cookies.get('session');
11-
12- if (!token) {
13- log.debug('No session token found in cookies');
14- return json({ success: true, invalidated: false });
15- }
16-
17- try {
18- const sessionId = await sha256(token);
19- await invalidateSession(sessionId);
20- cookies.delete('session', { path: '/' });
21- log.info('Session invalidated', {
22- sessionId: `${sessionId.slice(0, 4)}...${sessionId.slice(-4)}`
23- });
24- return json({ success: true, invalidated: true });
25- } catch (error) {
26- log.error('Failed to invalidate session', {
27- error: error instanceof Error ? error.message : 'Unknown error'
28- });
29- // Still delete cookie to ensure client is logged out
30- cookies.delete('session', { path: '/' });
31- return json({ success: false, error: 'Failed to invalidate session' }, { status: 500 });
32- }
33-};
D src/routes/api/users/[userId]/+server.ts +0 -46
@@ -1,46 +0,0 @@
1-import { error, json } from '@sveltejs/kit';
2-import { users } from '$lib/db/convex.server';
3-import type { RequestHandler } from './$types';
4-import { createSafeUser } from '$lib/types/chat';
5-import { createLogger } from '$lib/utils/logger.server';
6-
7-const log = createLogger('users-server');
8-
9-export const GET: RequestHandler = async ({ params }) => {
10- const maskedUserId = `${params.userId.slice(0, 4)}...${params.userId.slice(-4)}`;
11- log.debug('Fetching user data', { userId: maskedUserId });
12-
13- try {
14- const user = await users.getById(params.userId);
15-
16- if (!user) {
17- log.warn('User not found', { userId: maskedUserId });
18- throw error(404, 'User not found');
19- }
20-
21- const safeUser = createSafeUser({
22- id: user._id,
23- nickname: user.nickname,
24- status: user.status,
25- avatarUrl: user.avatarUrl,
26- lastSeen: user.lastSeen
27- });
28-
29- log.debug('User data retrieved', {
30- userId: maskedUserId,
31- status: safeUser.status,
32- lastSeen: safeUser.lastSeen ? new Date(safeUser.lastSeen).toISOString() : null
33- });
34-
35- return json({
36- success: true,
37- user: safeUser
38- });
39- } catch (err) {
40- log.error('Error fetching user data', {
41- userId: maskedUserId,
42- error: err instanceof Error ? err.message : 'Unknown error'
43- });
44- throw error(500, 'Failed to fetch user information');
45- }
46-};
@@ -1,46 +0,0 @@
1-import { error, json } from '@sveltejs/kit';
2-import { users } from '$lib/db/convex.server';
3-import type { RequestHandler } from './$types';
4-import { createSafeUser } from '$lib/types/chat';
5-import { createLogger } from '$lib/utils/logger.server';
6-
7-const log = createLogger('users-server');
8-
9-export const GET: RequestHandler = async ({ params }) => {
10- const maskedUserId = `${params.userId.slice(0, 4)}...${params.userId.slice(-4)}`;
11- log.debug('Fetching user data', { userId: maskedUserId });
12-
13- try {
14- const user = await users.getById(params.userId);
15-
16- if (!user) {
17- log.warn('User not found', { userId: maskedUserId });
18- throw error(404, 'User not found');
19- }
20-
21- const safeUser = createSafeUser({
22- id: user._id,
23- nickname: user.nickname,
24- status: user.status,
25- avatarUrl: user.avatarUrl,
26- lastSeen: user.lastSeen
27- });
28-
29- log.debug('User data retrieved', {
30- userId: maskedUserId,
31- status: safeUser.status,
32- lastSeen: safeUser.lastSeen ? new Date(safeUser.lastSeen).toISOString() : null
33- });
34-
35- return json({
36- success: true,
37- user: safeUser
38- });
39- } catch (err) {
40- log.error('Error fetching user data', {
41- userId: maskedUserId,
42- error: err instanceof Error ? err.message : 'Unknown error'
43- });
44- throw error(500, 'Failed to fetch user information');
45- }
46-};

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 8

↑↓ moveenter openesc close