refactor(api): migrate chat/status/users/rooms endpoints to Convex
- commit
- 53f93b1be987a0aef6b2c4aa847778da0f7a2a89
- author
- bnema · · 2026-01-18 18:34 UTC
- parent
- a5ef447
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +161 -158
M src/routes/api/chat/messages/+server.ts +65 -96
| @@ -1,18 +1,12 @@ | |||
| 1 | - | import { v4 as uuidv4 } from 'uuid'; | |
| 2 | - | import db from '$lib/db/db.server'; | |
| 3 | - | import { messages } from '$lib/db/schema'; | |
| 1 | + | import { messages, chatRooms, users } from '$lib/db/convex.server'; | |
| 2 | + | import { getDefaultChatRoomId } from '$lib/utils/chat.server'; | |
| 4 | 3 | import type { Message } from '$lib/types/chat'; | |
| 5 | 4 | import type { SendMessageResponse, GetMessagesResponse } from '$lib/types/payloads'; | |
| 6 | - | import { and, desc, eq, lt } from 'drizzle-orm/sql'; | |
| 7 | - | import type { SQL } from 'drizzle-orm/sql'; | |
| 8 | 5 | import { error } from '@sveltejs/kit'; | |
| 9 | - | import { sseEmitter } from '$lib/sse/SSEEventEmitter'; | |
| 10 | - | import { chatRooms } from '$lib/db/schema'; | |
| 11 | - | import { users } from '$lib/db/schema'; | |
| 12 | - | import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; | |
| 13 | 6 | import { createLogger } from '$lib/utils/logger.server'; | |
| 14 | 7 | import { sanitizeStyleData } from '$lib/validation/text-formatting'; | |
| 15 | 8 | import { sendMessageSchema } from '$lib/validation/message'; | |
| 9 | + | import type { RequestHandler } from './$types'; | |
| 16 | 10 | ||
| 17 | 11 | const log = createLogger('chat-server'); | |
| 18 | 12 | ||
| @@ -31,7 +25,6 @@ function updateUserCooldown(userId: string): { canSend: boolean; retryAfter?: nu | |||
| 31 | 25 | currentCooldown: INITIAL_COOLDOWN | |
| 32 | 26 | }; | |
| 33 | 27 | ||
| 34 | - | // Check if enough time has passed since the last message | |
| 35 | 28 | const timeSinceLastMessage = now - userState.lastMessageTime; | |
| 36 | 29 | if (timeSinceLastMessage < userState.currentCooldown) { | |
| 37 | 30 | return { | |
| @@ -40,11 +33,9 @@ function updateUserCooldown(userId: string): { canSend: boolean; retryAfter?: nu | |||
| 40 | 33 | }; | |
| 41 | 34 | } | |
| 42 | 35 | ||
| 43 | - | // Reset cooldown if it's been long enough | |
| 44 | 36 | if (timeSinceLastMessage > userState.currentCooldown * 2) { | |
| 45 | 37 | userState.currentCooldown = INITIAL_COOLDOWN; | |
| 46 | 38 | } else { | |
| 47 | - | // Exponential backoff | |
| 48 | 39 | userState.currentCooldown = Math.min(userState.currentCooldown * 2, MAX_COOLDOWN); | |
| 49 | 40 | } | |
| 50 | 41 | ||
| @@ -54,68 +45,65 @@ function updateUserCooldown(userId: string): { canSend: boolean; retryAfter?: nu | |||
| 54 | 45 | return { canSend: true }; | |
| 55 | 46 | } | |
| 56 | 47 | ||
| 57 | - | // GET endpoint: fetch messages directly from DB | |
| 58 | - | export async function GET({ request, locals }) { | |
| 59 | - | const url = new URL(request.url); | |
| 48 | + | // GET endpoint: fetch messages from Convex | |
| 49 | + | export const GET: RequestHandler = async ({ url, locals }) => { | |
| 60 | 50 | const beforeTimestamp = url.searchParams.get('before'); | |
| 61 | - | const roomId = url.searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID; | |
| 51 | + | const roomIdParam = url.searchParams.get('roomId'); | |
| 62 | 52 | const isPublic = url.searchParams.get('public') === 'true'; | |
| 63 | 53 | ||
| 54 | + | // Use default room if not specified | |
| 55 | + | const roomId = roomIdParam || getDefaultChatRoomId(); | |
| 56 | + | ||
| 57 | + | if (!roomId) { | |
| 58 | + | log.error('No room ID available'); | |
| 59 | + | throw error(500, 'Chat room not configured'); | |
| 60 | + | } | |
| 61 | + | ||
| 64 | 62 | try { | |
| 65 | - | log.debug('Fetching messages from database', { | |
| 63 | + | log.debug('Fetching messages from Convex', { | |
| 66 | 64 | beforeTimestamp, | |
| 67 | 65 | roomId, | |
| 68 | 66 | isAuthenticated: !!locals.session, | |
| 69 | 67 | isPublic | |
| 70 | 68 | }); | |
| 71 | 69 | ||
| 72 | - | let conditions = eq(messages.chatRoomId, roomId) as SQL<unknown>; | |
| 73 | - | ||
| 74 | - | if (beforeTimestamp) { | |
| 75 | - | conditions = and( | |
| 76 | - | conditions, | |
| 77 | - | lt(messages.timestamp, parseInt(beforeTimestamp)) | |
| 78 | - | ) as SQL<unknown>; | |
| 79 | - | } | |
| 80 | - | ||
| 81 | - | // Fetch messages with a limit based on authentication status | |
| 82 | - | // Public requests are limited to 50 messages | |
| 83 | - | // Authenticated users get 100 messages per request | |
| 84 | 70 | const fetchLimit = isPublic || !locals.session ? 50 : 100; | |
| 85 | 71 | ||
| 86 | - | const fetchedMessages = await db | |
| 87 | - | .select() | |
| 88 | - | .from(messages) | |
| 89 | - | .where(conditions) | |
| 90 | - | .orderBy(desc(messages.timestamp)) | |
| 91 | - | .limit(fetchLimit) | |
| 92 | - | .then((rows) => | |
| 93 | - | rows.map((row) => ({ | |
| 94 | - | ...row, | |
| 95 | - | styleData: row.styleData ?? undefined, | |
| 96 | - | hasFormatting: row.hasFormatting ?? false | |
| 97 | - | })) | |
| 98 | - | ); | |
| 72 | + | const result = await messages.getByRoom( | |
| 73 | + | roomId, | |
| 74 | + | fetchLimit, | |
| 75 | + | beforeTimestamp ? parseInt(beforeTimestamp) : undefined | |
| 76 | + | ); | |
| 77 | + | ||
| 78 | + | // Map Convex messages to our Message type | |
| 79 | + | const fetchedMessages: Message[] = result.messages.map((msg) => ({ | |
| 80 | + | id: msg._id, | |
| 81 | + | chatRoomId: msg.chatRoomId, | |
| 82 | + | senderId: msg.senderId, | |
| 83 | + | content: msg.content, | |
| 84 | + | type: msg.type as Message['type'], | |
| 85 | + | timestamp: msg.timestamp, | |
| 86 | + | styleData: msg.styleData, | |
| 87 | + | hasFormatting: msg.hasFormatting ?? false | |
| 88 | + | })); | |
| 99 | 89 | ||
| 100 | 90 | const response: GetMessagesResponse = { | |
| 101 | 91 | success: true, | |
| 102 | 92 | messages: fetchedMessages, | |
| 103 | - | hasMore: fetchedMessages.length === fetchLimit // Indicate if there might be more messages | |
| 93 | + | hasMore: result.hasMore | |
| 104 | 94 | }; | |
| 105 | 95 | ||
| 106 | 96 | return new Response(JSON.stringify(response), { | |
| 107 | - | headers: { | |
| 108 | - | 'Content-Type': 'application/json' | |
| 109 | - | } | |
| 97 | + | headers: { 'Content-Type': 'application/json' } | |
| 110 | 98 | }); | |
| 111 | 99 | } catch (err) { | |
| 112 | 100 | log.error('Error fetching messages:', { error: err }); | |
| 113 | 101 | throw error(500, 'Failed to fetch messages'); | |
| 114 | 102 | } | |
| 115 | - | } | |
| 103 | + | }; | |
| 116 | 104 | ||
| 117 | - | // POST endpoint: receive a new message, save it in the DB, and broadcast via SSE | |
| 118 | - | export async function POST({ request, locals }: { request: Request; locals: App.Locals }) { | |
| 105 | + | // POST endpoint: send a new message via Convex | |
| 106 | + | export const POST: RequestHandler = async ({ request, locals }) => { | |
| 119 | 107 | if (!locals.user) { | |
| 120 | 108 | log.warn('Authentication required'); | |
| 121 | 109 | throw error(401, 'Authentication required'); | |
| @@ -170,61 +158,42 @@ export async function POST({ request, locals }: { request: Request; locals: App. | |||
| 170 | 158 | throw error(403, 'Cannot post messages as another user'); | |
| 171 | 159 | } | |
| 172 | 160 | ||
| 173 | - | const chatRoomId = data.chatRoomId || DEFAULT_CHAT_ROOM_ID; | |
| 174 | - | ||
| 175 | - | // Check chat room existence | |
| 176 | - | const chatRoom = await db.select().from(chatRooms).where(eq(chatRooms.id, chatRoomId)).get(); | |
| 161 | + | // Use default room if not specified | |
| 162 | + | const chatRoomId = data.chatRoomId || getDefaultChatRoomId(); | |
| 177 | 163 | ||
| 178 | - | if (!chatRoom) { | |
| 179 | - | log.warn('Chat room not found', { chatRoomId }); | |
| 164 | + | if (!chatRoomId) { | |
| 165 | + | log.error('No chat room ID available'); | |
| 180 | 166 | const errorResponse: SendMessageResponse = { | |
| 181 | 167 | success: false, | |
| 182 | - | error: 'Chat room not found' | |
| 168 | + | error: 'Chat room not configured' | |
| 183 | 169 | }; | |
| 184 | - | return new Response(JSON.stringify(errorResponse), { status: 404 }); | |
| 185 | - | } | |
| 186 | - | ||
| 187 | - | // Check user existence | |
| 188 | - | const user = await db.select().from(users).where(eq(users.id, data.userId)).get(); | |
| 189 | - | ||
| 190 | - | if (!user) { | |
| 191 | - | const maskedUserId = `${data.userId.slice(0, 4)}...${data.userId.slice(-4)}`; | |
| 192 | - | log.warn('User not found', { maskedUserId }); | |
| 193 | - | const errorResponse: SendMessageResponse = { | |
| 194 | - | success: false, | |
| 195 | - | error: 'User not found' | |
| 196 | - | }; | |
| 197 | - | return new Response(JSON.stringify(errorResponse), { status: 404 }); | |
| 170 | + | return new Response(JSON.stringify(errorResponse), { status: 500 }); | |
| 198 | 171 | } | |
| 199 | 172 | ||
| 200 | 173 | const validatedStyleData = sanitizeStyleData(data.styleData); | |
| 201 | 174 | ||
| 202 | - | const newMessage: Message = { | |
| 203 | - | id: uuidv4(), | |
| 175 | + | // Send message via Convex | |
| 176 | + | const convexMessage = await messages.send( | |
| 204 | 177 | chatRoomId, | |
| 205 | - | senderId: data.userId, | |
| 206 | - | content: data.content, | |
| 207 | - | type: data.type || 'chat', | |
| 208 | - | timestamp: Date.now(), | |
| 209 | - | styleData: validatedStyleData ? JSON.stringify(validatedStyleData) : undefined, | |
| 210 | - | hasFormatting: Boolean(validatedStyleData) | |
| 178 | + | data.userId, | |
| 179 | + | data.content, | |
| 180 | + | data.type || 'chat', | |
| 181 | + | validatedStyleData ? JSON.stringify(validatedStyleData) : undefined, | |
| 182 | + | Boolean(validatedStyleData) | |
| 183 | + | ); | |
| 184 | + | ||
| 185 | + | // Map to our Message type | |
| 186 | + | const newMessage: Message = { | |
| 187 | + | id: convexMessage._id, | |
| 188 | + | chatRoomId: convexMessage.chatRoomId, | |
| 189 | + | senderId: convexMessage.senderId, | |
| 190 | + | content: convexMessage.content, | |
| 191 | + | type: convexMessage.type as Message['type'], | |
| 192 | + | timestamp: convexMessage.timestamp, | |
| 193 | + | styleData: convexMessage.styleData, | |
| 194 | + | hasFormatting: convexMessage.hasFormatting ?? false | |
| 211 | 195 | }; | |
| 212 | 196 | ||
| 213 | - | // debug style data content at this point | |
| 214 | - | log.debug('Style data content', { styleData: validatedStyleData }); | |
| 215 | - | ||
| 216 | - | // Save to DB | |
| 217 | - | await db.insert(messages).values(newMessage); | |
| 218 | - | log.debug('Message saved in DB', { | |
| 219 | - | messageId: newMessage.id, | |
| 220 | - | chatRoomId: newMessage.chatRoomId, | |
| 221 | - | type: newMessage.type, | |
| 222 | - | timestamp: newMessage.timestamp | |
| 223 | - | }); | |
| 224 | - | ||
| 225 | - | // Broadcast the new message via SSE | |
| 226 | - | sseEmitter.emit('chatMessage', newMessage); | |
| 227 | - | ||
| 228 | 197 | log.debug('Message processed successfully', { | |
| 229 | 198 | messageId: newMessage.id, | |
| 230 | 199 | userId: `${newMessage.senderId.slice(0, 4)}...${newMessage.senderId.slice(-4)}`, | |
| @@ -240,9 +209,9 @@ export async function POST({ request, locals }: { request: Request; locals: App. | |||
| 240 | 209 | status: 201, | |
| 241 | 210 | headers: { 'Content-Type': 'application/json' } | |
| 242 | 211 | }); | |
| 243 | - | } catch (error) { | |
| 212 | + | } catch (err) { | |
| 244 | 213 | log.error('Error processing message', { | |
| 245 | - | error: error instanceof Error ? error.message : 'Unknown error' | |
| 214 | + | error: err instanceof Error ? err.message : 'Unknown error' | |
| 246 | 215 | }); | |
| 247 | 216 | const errorResponse: SendMessageResponse = { | |
| 248 | 217 | success: false, | |
| @@ -253,4 +222,4 @@ export async function POST({ request, locals }: { request: Request; locals: App. | |||
| 253 | 222 | headers: { 'Content-Type': 'application/json' } | |
| 254 | 223 | }); | |
| 255 | 224 | } | |
| 256 | - | } | |
| 225 | + | }; | |
| @@ -1,18 +1,12 @@ | |||||
| 1 | - | import { v4 as uuidv4 } from 'uuid'; | 1 | + | import { messages, chatRooms, users } from '$lib/db/convex.server'; |
| 2 | - | import db from '$lib/db/db.server'; | 2 | + | import { getDefaultChatRoomId } from '$lib/utils/chat.server'; |
| 3 | - | import { messages } from '$lib/db/schema'; | |||
| 4 | import type { Message } from '$lib/types/chat'; | 3 | import type { Message } from '$lib/types/chat'; | ||
| 5 | import type { SendMessageResponse, GetMessagesResponse } from '$lib/types/payloads'; | 4 | import type { SendMessageResponse, GetMessagesResponse } from '$lib/types/payloads'; | ||
| 6 | - | import { and, desc, eq, lt } from 'drizzle-orm/sql'; | |||
| 7 | - | import type { SQL } from 'drizzle-orm/sql'; | |||
| 8 | import { error } from '@sveltejs/kit'; | 5 | import { error } from '@sveltejs/kit'; | ||
| 9 | - | import { sseEmitter } from '$lib/sse/SSEEventEmitter'; | |||
| 10 | - | import { chatRooms } from '$lib/db/schema'; | |||
| 11 | - | import { users } from '$lib/db/schema'; | |||
| 12 | - | import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; | |||
| 13 | import { createLogger } from '$lib/utils/logger.server'; | 6 | import { createLogger } from '$lib/utils/logger.server'; | ||
| 14 | import { sanitizeStyleData } from '$lib/validation/text-formatting'; | 7 | import { sanitizeStyleData } from '$lib/validation/text-formatting'; | ||
| 15 | import { sendMessageSchema } from '$lib/validation/message'; | 8 | import { sendMessageSchema } from '$lib/validation/message'; | ||
| 9 | + | import type { RequestHandler } from './$types'; | |||
| 16 | 10 | ||||
| 17 | const log = createLogger('chat-server'); | 11 | const log = createLogger('chat-server'); | ||
| 18 | 12 | ||||
| @@ -31,7 +25,6 @@ function updateUserCooldown(userId: string): { canSend: boolean; retryAfter?: nu | |||||
| 31 | currentCooldown: INITIAL_COOLDOWN | 25 | currentCooldown: INITIAL_COOLDOWN | ||
| 32 | }; | 26 | }; | ||
| 33 | 27 | ||||
| 34 | - | // Check if enough time has passed since the last message | |||
| 35 | const timeSinceLastMessage = now - userState.lastMessageTime; | 28 | const timeSinceLastMessage = now - userState.lastMessageTime; | ||
| 36 | if (timeSinceLastMessage < userState.currentCooldown) { | 29 | if (timeSinceLastMessage < userState.currentCooldown) { | ||
| 37 | return { | 30 | return { | ||
| @@ -40,11 +33,9 @@ function updateUserCooldown(userId: string): { canSend: boolean; retryAfter?: nu | |||||
| 40 | }; | 33 | }; | ||
| 41 | } | 34 | } | ||
| 42 | 35 | ||||
| 43 | - | // Reset cooldown if it's been long enough | |||
| 44 | if (timeSinceLastMessage > userState.currentCooldown * 2) { | 36 | if (timeSinceLastMessage > userState.currentCooldown * 2) { | ||
| 45 | userState.currentCooldown = INITIAL_COOLDOWN; | 37 | userState.currentCooldown = INITIAL_COOLDOWN; | ||
| 46 | } else { | 38 | } else { | ||
| 47 | - | // Exponential backoff | |||
| 48 | userState.currentCooldown = Math.min(userState.currentCooldown * 2, MAX_COOLDOWN); | 39 | userState.currentCooldown = Math.min(userState.currentCooldown * 2, MAX_COOLDOWN); | ||
| 49 | } | 40 | } | ||
| 50 | 41 | ||||
| @@ -54,68 +45,65 @@ function updateUserCooldown(userId: string): { canSend: boolean; retryAfter?: nu | |||||
| 54 | return { canSend: true }; | 45 | return { canSend: true }; | ||
| 55 | } | 46 | } | ||
| 56 | 47 | ||||
| 57 | - | // GET endpoint: fetch messages directly from DB | 48 | + | // GET endpoint: fetch messages from Convex |
| 58 | - | export async function GET({ request, locals }) { | 49 | + | export const GET: RequestHandler = async ({ url, locals }) => { |
| 59 | - | const url = new URL(request.url); | |||
| 60 | const beforeTimestamp = url.searchParams.get('before'); | 50 | const beforeTimestamp = url.searchParams.get('before'); | ||
| 61 | - | const roomId = url.searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID; | 51 | + | const roomIdParam = url.searchParams.get('roomId'); |
| 62 | const isPublic = url.searchParams.get('public') === 'true'; | 52 | const isPublic = url.searchParams.get('public') === 'true'; | ||
| 63 | 53 | ||||
| 54 | + | // Use default room if not specified | |||
| 55 | + | const roomId = roomIdParam || getDefaultChatRoomId(); | |||
| 56 | + | ||||
| 57 | + | if (!roomId) { | |||
| 58 | + | log.error('No room ID available'); | |||
| 59 | + | throw error(500, 'Chat room not configured'); | |||
| 60 | + | } | |||
| 61 | + | ||||
| 64 | try { | 62 | try { | ||
| 65 | - | log.debug('Fetching messages from database', { | 63 | + | log.debug('Fetching messages from Convex', { |
| 66 | beforeTimestamp, | 64 | beforeTimestamp, | ||
| 67 | roomId, | 65 | roomId, | ||
| 68 | isAuthenticated: !!locals.session, | 66 | isAuthenticated: !!locals.session, | ||
| 69 | isPublic | 67 | isPublic | ||
| 70 | }); | 68 | }); | ||
| 71 | 69 | ||||
| 72 | - | let conditions = eq(messages.chatRoomId, roomId) as SQL<unknown>; | |||
| 73 | - | ||||
| 74 | - | if (beforeTimestamp) { | |||
| 75 | - | conditions = and( | |||
| 76 | - | conditions, | |||
| 77 | - | lt(messages.timestamp, parseInt(beforeTimestamp)) | |||
| 78 | - | ) as SQL<unknown>; | |||
| 79 | - | } | |||
| 80 | - | ||||
| 81 | - | // Fetch messages with a limit based on authentication status | |||
| 82 | - | // Public requests are limited to 50 messages | |||
| 83 | - | // Authenticated users get 100 messages per request | |||
| 84 | const fetchLimit = isPublic || !locals.session ? 50 : 100; | 70 | const fetchLimit = isPublic || !locals.session ? 50 : 100; | ||
| 85 | 71 | ||||
| 86 | - | const fetchedMessages = await db | 72 | + | const result = await messages.getByRoom( |
| 87 | - | .select() | 73 | + | roomId, |
| 88 | - | .from(messages) | 74 | + | fetchLimit, |
| 89 | - | .where(conditions) | 75 | + | beforeTimestamp ? parseInt(beforeTimestamp) : undefined |
| 90 | - | .orderBy(desc(messages.timestamp)) | 76 | + | ); |
| 91 | - | .limit(fetchLimit) | 77 | + | |
| 92 | - | .then((rows) => | 78 | + | // Map Convex messages to our Message type |
| 93 | - | rows.map((row) => ({ | 79 | + | const fetchedMessages: Message[] = result.messages.map((msg) => ({ |
| 94 | - | ...row, | 80 | + | id: msg._id, |
| 95 | - | styleData: row.styleData ?? undefined, | 81 | + | chatRoomId: msg.chatRoomId, |
| 96 | - | hasFormatting: row.hasFormatting ?? false | 82 | + | senderId: msg.senderId, |
| 97 | - | })) | 83 | + | content: msg.content, |
| 98 | - | ); | 84 | + | type: msg.type as Message['type'], |
| 85 | + | timestamp: msg.timestamp, | |||
| 86 | + | styleData: msg.styleData, | |||
| 87 | + | hasFormatting: msg.hasFormatting ?? false | |||
| 88 | + | })); | |||
| 99 | 89 | ||||
| 100 | const response: GetMessagesResponse = { | 90 | const response: GetMessagesResponse = { | ||
| 101 | success: true, | 91 | success: true, | ||
| 102 | messages: fetchedMessages, | 92 | messages: fetchedMessages, | ||
| 103 | - | hasMore: fetchedMessages.length === fetchLimit // Indicate if there might be more messages | 93 | + | hasMore: result.hasMore |
| 104 | }; | 94 | }; | ||
| 105 | 95 | ||||
| 106 | return new Response(JSON.stringify(response), { | 96 | return new Response(JSON.stringify(response), { | ||
| 107 | - | headers: { | 97 | + | headers: { 'Content-Type': 'application/json' } |
| 108 | - | 'Content-Type': 'application/json' | |||
| 109 | - | } | |||
| 110 | }); | 98 | }); | ||
| 111 | } catch (err) { | 99 | } catch (err) { | ||
| 112 | log.error('Error fetching messages:', { error: err }); | 100 | log.error('Error fetching messages:', { error: err }); | ||
| 113 | throw error(500, 'Failed to fetch messages'); | 101 | throw error(500, 'Failed to fetch messages'); | ||
| 114 | } | 102 | } | ||
| 115 | - | } | 103 | + | }; |
| 116 | 104 | ||||
| 117 | - | // POST endpoint: receive a new message, save it in the DB, and broadcast via SSE | 105 | + | // POST endpoint: send a new message via Convex |
| 118 | - | export async function POST({ request, locals }: { request: Request; locals: App.Locals }) { | 106 | + | export const POST: RequestHandler = async ({ request, locals }) => { |
| 119 | if (!locals.user) { | 107 | if (!locals.user) { | ||
| 120 | log.warn('Authentication required'); | 108 | log.warn('Authentication required'); | ||
| 121 | throw error(401, 'Authentication required'); | 109 | throw error(401, 'Authentication required'); | ||
| @@ -170,61 +158,42 @@ export async function POST({ request, locals }: { request: Request; locals: App. | |||||
| 170 | throw error(403, 'Cannot post messages as another user'); | 158 | throw error(403, 'Cannot post messages as another user'); | ||
| 171 | } | 159 | } | ||
| 172 | 160 | ||||
| 173 | - | const chatRoomId = data.chatRoomId || DEFAULT_CHAT_ROOM_ID; | 161 | + | // Use default room if not specified |
| 174 | - | 162 | + | const chatRoomId = data.chatRoomId || getDefaultChatRoomId(); | |
| 175 | - | // Check chat room existence | |||
| 176 | - | const chatRoom = await db.select().from(chatRooms).where(eq(chatRooms.id, chatRoomId)).get(); | |||
| 177 | 163 | ||||
| 178 | - | if (!chatRoom) { | 164 | + | if (!chatRoomId) { |
| 179 | - | log.warn('Chat room not found', { chatRoomId }); | 165 | + | log.error('No chat room ID available'); |
| 180 | const errorResponse: SendMessageResponse = { | 166 | const errorResponse: SendMessageResponse = { | ||
| 181 | success: false, | 167 | success: false, | ||
| 182 | - | error: 'Chat room not found' | 168 | + | error: 'Chat room not configured' |
| 183 | }; | 169 | }; | ||
| 184 | - | return new Response(JSON.stringify(errorResponse), { status: 404 }); | 170 | + | return new Response(JSON.stringify(errorResponse), { status: 500 }); |
| 185 | - | } | |||
| 186 | - | ||||
| 187 | - | // Check user existence | |||
| 188 | - | const user = await db.select().from(users).where(eq(users.id, data.userId)).get(); | |||
| 189 | - | ||||
| 190 | - | if (!user) { | |||
| 191 | - | const maskedUserId = `${data.userId.slice(0, 4)}...${data.userId.slice(-4)}`; | |||
| 192 | - | log.warn('User not found', { maskedUserId }); | |||
| 193 | - | const errorResponse: SendMessageResponse = { | |||
| 194 | - | success: false, | |||
| 195 | - | error: 'User not found' | |||
| 196 | - | }; | |||
| 197 | - | return new Response(JSON.stringify(errorResponse), { status: 404 }); | |||
| 198 | } | 171 | } | ||
| 199 | 172 | ||||
| 200 | const validatedStyleData = sanitizeStyleData(data.styleData); | 173 | const validatedStyleData = sanitizeStyleData(data.styleData); | ||
| 201 | 174 | ||||
| 202 | - | const newMessage: Message = { | 175 | + | // Send message via Convex |
| 203 | - | id: uuidv4(), | 176 | + | const convexMessage = await messages.send( |
| 204 | chatRoomId, | 177 | chatRoomId, | ||
| 205 | - | senderId: data.userId, | 178 | + | data.userId, |
| 206 | - | content: data.content, | 179 | + | data.content, |
| 207 | - | type: data.type || 'chat', | 180 | + | data.type || 'chat', |
| 208 | - | timestamp: Date.now(), | 181 | + | validatedStyleData ? JSON.stringify(validatedStyleData) : undefined, |
| 209 | - | styleData: validatedStyleData ? JSON.stringify(validatedStyleData) : undefined, | 182 | + | Boolean(validatedStyleData) |
| 210 | - | hasFormatting: Boolean(validatedStyleData) | 183 | + | ); |
| 184 | + | ||||
| 185 | + | // Map to our Message type | |||
| 186 | + | const newMessage: Message = { | |||
| 187 | + | id: convexMessage._id, | |||
| 188 | + | chatRoomId: convexMessage.chatRoomId, | |||
| 189 | + | senderId: convexMessage.senderId, | |||
| 190 | + | content: convexMessage.content, | |||
| 191 | + | type: convexMessage.type as Message['type'], | |||
| 192 | + | timestamp: convexMessage.timestamp, | |||
| 193 | + | styleData: convexMessage.styleData, | |||
| 194 | + | hasFormatting: convexMessage.hasFormatting ?? false | |||
| 211 | }; | 195 | }; | ||
| 212 | 196 | ||||
| 213 | - | // debug style data content at this point | |||
| 214 | - | log.debug('Style data content', { styleData: validatedStyleData }); | |||
| 215 | - | ||||
| 216 | - | // Save to DB | |||
| 217 | - | await db.insert(messages).values(newMessage); | |||
| 218 | - | log.debug('Message saved in DB', { | |||
| 219 | - | messageId: newMessage.id, | |||
| 220 | - | chatRoomId: newMessage.chatRoomId, | |||
| 221 | - | type: newMessage.type, | |||
| 222 | - | timestamp: newMessage.timestamp | |||
| 223 | - | }); | |||
| 224 | - | ||||
| 225 | - | // Broadcast the new message via SSE | |||
| 226 | - | sseEmitter.emit('chatMessage', newMessage); | |||
| 227 | - | ||||
| 228 | log.debug('Message processed successfully', { | 197 | log.debug('Message processed successfully', { | ||
| 229 | messageId: newMessage.id, | 198 | messageId: newMessage.id, | ||
| 230 | userId: `${newMessage.senderId.slice(0, 4)}...${newMessage.senderId.slice(-4)}`, | 199 | userId: `${newMessage.senderId.slice(0, 4)}...${newMessage.senderId.slice(-4)}`, | ||
| @@ -240,9 +209,9 @@ export async function POST({ request, locals }: { request: Request; locals: App. | |||||
| 240 | status: 201, | 209 | status: 201, | ||
| 241 | headers: { 'Content-Type': 'application/json' } | 210 | headers: { 'Content-Type': 'application/json' } | ||
| 242 | }); | 211 | }); | ||
| 243 | - | } catch (error) { | 212 | + | } catch (err) { |
| 244 | log.error('Error processing message', { | 213 | log.error('Error processing message', { | ||
| 245 | - | error: error instanceof Error ? error.message : 'Unknown error' | 214 | + | error: err instanceof Error ? err.message : 'Unknown error' |
| 246 | }); | 215 | }); | ||
| 247 | const errorResponse: SendMessageResponse = { | 216 | const errorResponse: SendMessageResponse = { | ||
| 248 | success: false, | 217 | success: false, | ||
| @@ -253,4 +222,4 @@ export async function POST({ request, locals }: { request: Request; locals: App. | |||||
| 253 | headers: { 'Content-Type': 'application/json' } | 222 | headers: { 'Content-Type': 'application/json' } | ||
| 254 | }); | 223 | }); | ||
| 255 | } | 224 | } | ||
| 256 | - | } | 225 | + | }; |
M src/routes/api/rooms/[roomId]/+server.ts +31 -37
| @@ -1,16 +1,15 @@ | |||
| 1 | - | import { eq } from 'drizzle-orm/sql'; | |
| 2 | - | import db from '$lib/db/db.server'; | |
| 3 | - | import { users } from '$lib/db/schema'; | |
| 1 | + | import { users } from '$lib/db/convex.server'; | |
| 4 | 2 | import type { SafeUser } from '$lib/types/chat'; | |
| 5 | 3 | import type { PublicRoomResponse } from '$lib/types/payloads'; | |
| 6 | 4 | import { createSafeUser } from '$lib/types/chat'; | |
| 7 | 5 | import { createLogger } from '$lib/utils/logger.server'; | |
| 6 | + | import type { RequestHandler } from './$types'; | |
| 8 | 7 | ||
| 9 | 8 | const log = createLogger('rooms-server'); | |
| 10 | 9 | ||
| 11 | 10 | const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes | |
| 12 | 11 | ||
| 13 | - | export async function GET({ params, url, locals }): Promise<Response> { | |
| 12 | + | export const GET: RequestHandler = async ({ params, url, locals }) => { | |
| 14 | 13 | const { roomId } = params; | |
| 15 | 14 | const isPublic = !locals.user || url.searchParams.get('public') === 'true'; | |
| 16 | 15 | ||
| @@ -26,52 +25,47 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||
| 26 | 25 | // Calculate timeout threshold | |
| 27 | 26 | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | |
| 28 | 27 | ||
| 29 | - | // Fetch buddy list: get all users | |
| 30 | - | const fetchedUsers = await db.select().from(users).execute(); | |
| 31 | - | ||
| 32 | - | // Normalize users to ensure that lastSeen is always a number (using 0 if null) | |
| 33 | - | const normalizedUsers = fetchedUsers.map((user: SafeUser & { lastSeen: number | null }) => ({ | |
| 34 | - | ...user, | |
| 35 | - | lastSeen: user.lastSeen ?? 0 | |
| 36 | - | })); | |
| 28 | + | // Fetch all users from Convex | |
| 29 | + | const fetchedUsers = await users.list(); | |
| 37 | 30 | ||
| 38 | 31 | // Process users to mark them as offline if they've timed out | |
| 39 | - | const processedUsers = normalizedUsers.map((user: SafeUser & { lastSeen: number }) => { | |
| 40 | - | const shouldBeOffline = user.status !== 'offline' && user.lastSeen < timeoutThreshold; | |
| 32 | + | const processedUsers = fetchedUsers.map((user) => { | |
| 33 | + | const lastSeen = user.lastSeen ?? 0; | |
| 34 | + | const shouldBeOffline = user.status !== 'offline' && lastSeen < timeoutThreshold; | |
| 41 | 35 | return { | |
| 42 | - | ...user, | |
| 43 | - | status: shouldBeOffline ? 'offline' : user.status | |
| 36 | + | id: user._id, | |
| 37 | + | nickname: user.nickname, | |
| 38 | + | status: shouldBeOffline ? 'offline' : user.status, | |
| 39 | + | avatarUrl: user.avatarUrl, | |
| 40 | + | lastSeen | |
| 44 | 41 | }; | |
| 45 | 42 | }); | |
| 46 | 43 | ||
| 47 | - | // Find users who were originally non-offline but now marked offline | |
| 48 | - | const usersToUpdate = processedUsers.filter( | |
| 49 | - | (user: SafeUser & { lastSeen: number }) => | |
| 50 | - | user.status === 'offline' && | |
| 51 | - | normalizedUsers.find((u: SafeUser & { lastSeen: number }) => u.id === user.id)?.status !== | |
| 52 | - | 'offline' | |
| 53 | - | ); | |
| 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 | + | }); | |
| 54 | 49 | ||
| 50 | + | // Update timed-out users in Convex | |
| 55 | 51 | if (usersToUpdate.length > 0) { | |
| 56 | - | await Promise.all( | |
| 57 | - | usersToUpdate.map((user: SafeUser & { lastSeen: number }) => | |
| 58 | - | db | |
| 59 | - | .update(users) | |
| 60 | - | .set({ status: 'offline', lastSeen: Date.now() }) | |
| 61 | - | .where(eq(users.id, user.id)) | |
| 62 | - | .execute() | |
| 63 | - | ) | |
| 64 | - | ); | |
| 52 | + | await Promise.all(usersToUpdate.map((user) => users.updateStatus(user.id, 'offline'))); | |
| 65 | 53 | ||
| 66 | 54 | log.debug('Updated offline status for users', { | |
| 67 | 55 | count: usersToUpdate.length, | |
| 68 | - | userIds: usersToUpdate.map((u: SafeUser & { lastSeen: number }) => u.id) | |
| 56 | + | userIds: usersToUpdate.map((u) => u.id) | |
| 69 | 57 | }); | |
| 70 | 58 | } | |
| 71 | 59 | ||
| 72 | - | // Sanitize user data using createSafeUser | |
| 73 | - | const sanitizedUsers = processedUsers.map((user: SafeUser & { lastSeen: number }) => | |
| 74 | - | createSafeUser(user) | |
| 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 | + | }) | |
| 75 | 69 | ); | |
| 76 | 70 | ||
| 77 | 71 | const responseData: PublicRoomResponse = { | |
| @@ -104,4 +98,4 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||
| 104 | 98 | headers: { 'Content-Type': 'application/json' } | |
| 105 | 99 | }); | |
| 106 | 100 | } | |
| 107 | - | } | |
| 101 | + | }; | |
| @@ -1,16 +1,15 @@ | |||||
| 1 | - | import { eq } from 'drizzle-orm/sql'; | 1 | + | import { users } from '$lib/db/convex.server'; |
| 2 | - | import db from '$lib/db/db.server'; | |||
| 3 | - | import { users } from '$lib/db/schema'; | |||
| 4 | import type { SafeUser } from '$lib/types/chat'; | 2 | import type { SafeUser } from '$lib/types/chat'; | ||
| 5 | import type { PublicRoomResponse } from '$lib/types/payloads'; | 3 | import type { PublicRoomResponse } from '$lib/types/payloads'; | ||
| 6 | import { createSafeUser } from '$lib/types/chat'; | 4 | import { createSafeUser } from '$lib/types/chat'; | ||
| 7 | import { createLogger } from '$lib/utils/logger.server'; | 5 | import { createLogger } from '$lib/utils/logger.server'; | ||
| 6 | + | import type { RequestHandler } from './$types'; | |||
| 8 | 7 | ||||
| 9 | const log = createLogger('rooms-server'); | 8 | const log = createLogger('rooms-server'); | ||
| 10 | 9 | ||||
| 11 | const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes | 10 | const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes | ||
| 12 | 11 | ||||
| 13 | - | export async function GET({ params, url, locals }): Promise<Response> { | 12 | + | export const GET: RequestHandler = async ({ params, url, locals }) => { |
| 14 | const { roomId } = params; | 13 | const { roomId } = params; | ||
| 15 | const isPublic = !locals.user || url.searchParams.get('public') === 'true'; | 14 | const isPublic = !locals.user || url.searchParams.get('public') === 'true'; | ||
| 16 | 15 | ||||
| @@ -26,52 +25,47 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||||
| 26 | // Calculate timeout threshold | 25 | // Calculate timeout threshold | ||
| 27 | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | 26 | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | ||
| 28 | 27 | ||||
| 29 | - | // Fetch buddy list: get all users | 28 | + | // Fetch all users from Convex |
| 30 | - | const fetchedUsers = await db.select().from(users).execute(); | 29 | + | const fetchedUsers = await users.list(); |
| 31 | - | ||||
| 32 | - | // Normalize users to ensure that lastSeen is always a number (using 0 if null) | |||
| 33 | - | const normalizedUsers = fetchedUsers.map((user: SafeUser & { lastSeen: number | null }) => ({ | |||
| 34 | - | ...user, | |||
| 35 | - | lastSeen: user.lastSeen ?? 0 | |||
| 36 | - | })); | |||
| 37 | 30 | ||||
| 38 | // Process users to mark them as offline if they've timed out | 31 | // Process users to mark them as offline if they've timed out | ||
| 39 | - | const processedUsers = normalizedUsers.map((user: SafeUser & { lastSeen: number }) => { | 32 | + | const processedUsers = fetchedUsers.map((user) => { |
| 40 | - | const shouldBeOffline = user.status !== 'offline' && user.lastSeen < timeoutThreshold; | 33 | + | const lastSeen = user.lastSeen ?? 0; |
| 34 | + | const shouldBeOffline = user.status !== 'offline' && lastSeen < timeoutThreshold; | |||
| 41 | return { | 35 | return { | ||
| 42 | - | ...user, | 36 | + | id: user._id, |
| 43 | - | status: shouldBeOffline ? 'offline' : user.status | 37 | + | nickname: user.nickname, |
| 38 | + | status: shouldBeOffline ? 'offline' : user.status, | |||
| 39 | + | avatarUrl: user.avatarUrl, | |||
| 40 | + | lastSeen | |||
| 44 | }; | 41 | }; | ||
| 45 | }); | 42 | }); | ||
| 46 | 43 | ||||
| 47 | - | // Find users who were originally non-offline but now marked offline | 44 | + | // Find users who should be marked offline and update them |
| 48 | - | const usersToUpdate = processedUsers.filter( | 45 | + | const usersToUpdate = processedUsers.filter((user, index) => { |
| 49 | - | (user: SafeUser & { lastSeen: number }) => | 46 | + | const original = fetchedUsers[index]; |
| 50 | - | user.status === 'offline' && | 47 | + | return user.status === 'offline' && original.status !== 'offline'; |
| 51 | - | normalizedUsers.find((u: SafeUser & { lastSeen: number }) => u.id === user.id)?.status !== | 48 | + | }); |
| 52 | - | 'offline' | |||
| 53 | - | ); | |||
| 54 | 49 | ||||
| 50 | + | // Update timed-out users in Convex | |||
| 55 | if (usersToUpdate.length > 0) { | 51 | if (usersToUpdate.length > 0) { | ||
| 56 | - | await Promise.all( | 52 | + | await Promise.all(usersToUpdate.map((user) => users.updateStatus(user.id, 'offline'))); |
| 57 | - | usersToUpdate.map((user: SafeUser & { lastSeen: number }) => | |||
| 58 | - | db | |||
| 59 | - | .update(users) | |||
| 60 | - | .set({ status: 'offline', lastSeen: Date.now() }) | |||
| 61 | - | .where(eq(users.id, user.id)) | |||
| 62 | - | .execute() | |||
| 63 | - | ) | |||
| 64 | - | ); | |||
| 65 | 53 | ||||
| 66 | log.debug('Updated offline status for users', { | 54 | log.debug('Updated offline status for users', { | ||
| 67 | count: usersToUpdate.length, | 55 | count: usersToUpdate.length, | ||
| 68 | - | userIds: usersToUpdate.map((u: SafeUser & { lastSeen: number }) => u.id) | 56 | + | userIds: usersToUpdate.map((u) => u.id) |
| 69 | }); | 57 | }); | ||
| 70 | } | 58 | } | ||
| 71 | 59 | ||||
| 72 | - | // Sanitize user data using createSafeUser | 60 | + | // Sanitize user data |
| 73 | - | const sanitizedUsers = processedUsers.map((user: SafeUser & { lastSeen: number }) => | 61 | + | const sanitizedUsers: SafeUser[] = processedUsers.map((user) => |
| 74 | - | createSafeUser(user) | 62 | + | createSafeUser({ |
| 63 | + | id: user.id, | |||
| 64 | + | nickname: user.nickname, | |||
| 65 | + | status: user.status, | |||
| 66 | + | avatarUrl: user.avatarUrl, | |||
| 67 | + | lastSeen: user.lastSeen | |||
| 68 | + | }) | |||
| 75 | ); | 69 | ); | ||
| 76 | 70 | ||||
| 77 | const responseData: PublicRoomResponse = { | 71 | const responseData: PublicRoomResponse = { | ||
| @@ -104,4 +98,4 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||||
| 104 | headers: { 'Content-Type': 'application/json' } | 98 | headers: { 'Content-Type': 'application/json' } | ||
| 105 | }); | 99 | }); | ||
| 106 | } | 100 | } | ||
| 107 | - | } | 101 | + | }; |
A src/routes/api/rooms/default/+server.ts +42 -0
| @@ -0,0 +1,42 @@ | |||
| 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 | + | }; | |
| @@ -0,0 +1,42 @@ | |||||
| 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 | + | }; | |||
M src/routes/api/status/+server.ts +14 -20
| @@ -1,11 +1,8 @@ | |||
| 1 | - | import db from '$lib/db/db.server'; | |
| 2 | - | import { users } from '$lib/db/schema'; | |
| 3 | - | import { eq } from 'drizzle-orm/sql'; | |
| 1 | + | import { users } from '$lib/db/convex.server'; | |
| 4 | 2 | import { validateSessionToken, generateSessionToken, createSession } from '$lib/api/session.server'; | |
| 5 | 3 | import { setSessionTokenCookie } from '$lib/api/session.cookie'; | |
| 6 | 4 | import { createSafeUser } from '$lib/types/chat'; | |
| 7 | 5 | import { createLogger } from '$lib/utils/logger.server'; | |
| 8 | - | import { buddyListCache } from '$lib/buddyListCache'; | |
| 9 | 6 | import type { RequestHandler } from './$types'; | |
| 10 | 7 | ||
| 11 | 8 | const log = createLogger('status-server'); | |
| @@ -36,21 +33,9 @@ export const POST: RequestHandler = async ({ request, cookies, locals }) => { | |||
| 36 | 33 | ||
| 37 | 34 | const now = Date.now(); | |
| 38 | 35 | ||
| 39 | - | // Update the user status in the database | |
| 40 | - | // Always update lastSeen to support timeout detection | |
| 41 | - | const updatedUser = await db | |
| 42 | - | .update(users) | |
| 43 | - | .set({ | |
| 44 | - | status, | |
| 45 | - | lastSeen: now // Always update lastSeen for proper timeout detection | |
| 46 | - | }) | |
| 47 | - | .where(eq(users.id, userId)) | |
| 48 | - | .returning() | |
| 49 | - | .get(); | |
| 50 | - | log.debug('Database updated successfully', { userId: maskedUserId, status }); | |
| 51 | - | ||
| 52 | - | // Invalidate buddy list cache when status changes | |
| 53 | - | buddyListCache.invalidate(); | |
| 36 | + | // Update the user status in Convex | |
| 37 | + | const updatedUser = await users.updateStatus(userId, status); | |
| 38 | + | log.debug('Convex updated successfully', { userId: maskedUserId, status }); | |
| 54 | 39 | ||
| 55 | 40 | // Renew session if status is online and session expires in less than 1 day | |
| 56 | 41 | if (status === 'online') { | |
| @@ -76,7 +61,13 @@ export const POST: RequestHandler = async ({ request, cookies, locals }) => { | |||
| 76 | 61 | return new Response( | |
| 77 | 62 | JSON.stringify({ | |
| 78 | 63 | success: true, | |
| 79 | - | user: createSafeUser(updatedUser) | |
| 64 | + | user: createSafeUser({ | |
| 65 | + | id: updatedUser._id, | |
| 66 | + | nickname: updatedUser.nickname, | |
| 67 | + | status: updatedUser.status, | |
| 68 | + | avatarUrl: updatedUser.avatarUrl, | |
| 69 | + | lastSeen: updatedUser.lastSeen | |
| 70 | + | }) | |
| 80 | 71 | }), | |
| 81 | 72 | { | |
| 82 | 73 | status: 200, | |
| @@ -84,6 +75,9 @@ export const POST: RequestHandler = async ({ request, cookies, locals }) => { | |||
| 84 | 75 | } | |
| 85 | 76 | ); | |
| 86 | 77 | } catch (error) { | |
| 78 | + | log.error('Error updating status', { | |
| 79 | + | error: error instanceof Error ? error.message : 'Unknown error' | |
| 80 | + | }); | |
| 87 | 81 | return new Response( | |
| 88 | 82 | JSON.stringify({ | |
| 89 | 83 | success: false, | |
| @@ -1,11 +1,8 @@ | |||||
| 1 | - | import db from '$lib/db/db.server'; | 1 | + | import { users } from '$lib/db/convex.server'; |
| 2 | - | import { users } from '$lib/db/schema'; | |||
| 3 | - | import { eq } from 'drizzle-orm/sql'; | |||
| 4 | import { validateSessionToken, generateSessionToken, createSession } from '$lib/api/session.server'; | 2 | import { validateSessionToken, generateSessionToken, createSession } from '$lib/api/session.server'; | ||
| 5 | import { setSessionTokenCookie } from '$lib/api/session.cookie'; | 3 | import { setSessionTokenCookie } from '$lib/api/session.cookie'; | ||
| 6 | import { createSafeUser } from '$lib/types/chat'; | 4 | import { createSafeUser } from '$lib/types/chat'; | ||
| 7 | import { createLogger } from '$lib/utils/logger.server'; | 5 | import { createLogger } from '$lib/utils/logger.server'; | ||
| 8 | - | import { buddyListCache } from '$lib/buddyListCache'; | |||
| 9 | import type { RequestHandler } from './$types'; | 6 | import type { RequestHandler } from './$types'; | ||
| 10 | 7 | ||||
| 11 | const log = createLogger('status-server'); | 8 | const log = createLogger('status-server'); | ||
| @@ -36,21 +33,9 @@ export const POST: RequestHandler = async ({ request, cookies, locals }) => { | |||||
| 36 | 33 | ||||
| 37 | const now = Date.now(); | 34 | const now = Date.now(); | ||
| 38 | 35 | ||||
| 39 | - | // Update the user status in the database | 36 | + | // Update the user status in Convex |
| 40 | - | // Always update lastSeen to support timeout detection | 37 | + | const updatedUser = await users.updateStatus(userId, status); |
| 41 | - | const updatedUser = await db | 38 | + | log.debug('Convex updated successfully', { userId: maskedUserId, status }); |
| 42 | - | .update(users) | |||
| 43 | - | .set({ | |||
| 44 | - | status, | |||
| 45 | - | lastSeen: now // Always update lastSeen for proper timeout detection | |||
| 46 | - | }) | |||
| 47 | - | .where(eq(users.id, userId)) | |||
| 48 | - | .returning() | |||
| 49 | - | .get(); | |||
| 50 | - | log.debug('Database updated successfully', { userId: maskedUserId, status }); | |||
| 51 | - | ||||
| 52 | - | // Invalidate buddy list cache when status changes | |||
| 53 | - | buddyListCache.invalidate(); | |||
| 54 | 39 | ||||
| 55 | // Renew session if status is online and session expires in less than 1 day | 40 | // Renew session if status is online and session expires in less than 1 day | ||
| 56 | if (status === 'online') { | 41 | if (status === 'online') { | ||
| @@ -76,7 +61,13 @@ export const POST: RequestHandler = async ({ request, cookies, locals }) => { | |||||
| 76 | return new Response( | 61 | return new Response( | ||
| 77 | JSON.stringify({ | 62 | JSON.stringify({ | ||
| 78 | success: true, | 63 | success: true, | ||
| 79 | - | user: createSafeUser(updatedUser) | 64 | + | user: createSafeUser({ |
| 65 | + | id: updatedUser._id, | |||
| 66 | + | nickname: updatedUser.nickname, | |||
| 67 | + | status: updatedUser.status, | |||
| 68 | + | avatarUrl: updatedUser.avatarUrl, | |||
| 69 | + | lastSeen: updatedUser.lastSeen | |||
| 70 | + | }) | |||
| 80 | }), | 71 | }), | ||
| 81 | { | 72 | { | ||
| 82 | status: 200, | 73 | status: 200, | ||
| @@ -84,6 +75,9 @@ export const POST: RequestHandler = async ({ request, cookies, locals }) => { | |||||
| 84 | } | 75 | } | ||
| 85 | ); | 76 | ); | ||
| 86 | } catch (error) { | 77 | } catch (error) { | ||
| 78 | + | log.error('Error updating status', { | |||
| 79 | + | error: error instanceof Error ? error.message : 'Unknown error' | |||
| 80 | + | }); | |||
| 87 | return new Response( | 81 | return new Response( | ||
| 88 | JSON.stringify({ | 82 | JSON.stringify({ | ||
| 89 | success: false, | 83 | success: false, | ||
M src/routes/api/users/[userId]/+server.ts +9 -5
| @@ -1,7 +1,5 @@ | |||
| 1 | 1 | import { error, json } from '@sveltejs/kit'; | |
| 2 | - | import db from '$lib/db/db.server'; | |
| 3 | - | import { users } from '$lib/db/schema'; | |
| 4 | - | import { eq } from 'drizzle-orm'; | |
| 2 | + | import { users } from '$lib/db/convex.server'; | |
| 5 | 3 | import type { RequestHandler } from './$types'; | |
| 6 | 4 | import { createSafeUser } from '$lib/types/chat'; | |
| 7 | 5 | import { createLogger } from '$lib/utils/logger.server'; | |
| @@ -13,14 +11,20 @@ export const GET: RequestHandler = async ({ params }) => { | |||
| 13 | 11 | log.debug('Fetching user data', { userId: maskedUserId }); | |
| 14 | 12 | ||
| 15 | 13 | try { | |
| 16 | - | const user = await db.select().from(users).where(eq(users.id, params.userId)).get(); | |
| 14 | + | const user = await users.getById(params.userId); | |
| 17 | 15 | ||
| 18 | 16 | if (!user) { | |
| 19 | 17 | log.warn('User not found', { userId: maskedUserId }); | |
| 20 | 18 | throw error(404, 'User not found'); | |
| 21 | 19 | } | |
| 22 | 20 | ||
| 23 | - | const safeUser = createSafeUser(user); | |
| 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 | + | }); | |
| 24 | 28 | ||
| 25 | 29 | log.debug('User data retrieved', { | |
| 26 | 30 | userId: maskedUserId, | |
| @@ -1,7 +1,5 @@ | |||||
| 1 | import { error, json } from '@sveltejs/kit'; | 1 | import { error, json } from '@sveltejs/kit'; | ||
| 2 | - | import db from '$lib/db/db.server'; | 2 | + | import { users } from '$lib/db/convex.server'; |
| 3 | - | import { users } from '$lib/db/schema'; | |||
| 4 | - | import { eq } from 'drizzle-orm'; | |||
| 5 | import type { RequestHandler } from './$types'; | 3 | import type { RequestHandler } from './$types'; | ||
| 6 | import { createSafeUser } from '$lib/types/chat'; | 4 | import { createSafeUser } from '$lib/types/chat'; | ||
| 7 | import { createLogger } from '$lib/utils/logger.server'; | 5 | import { createLogger } from '$lib/utils/logger.server'; | ||
| @@ -13,14 +11,20 @@ export const GET: RequestHandler = async ({ params }) => { | |||||
| 13 | log.debug('Fetching user data', { userId: maskedUserId }); | 11 | log.debug('Fetching user data', { userId: maskedUserId }); | ||
| 14 | 12 | ||||
| 15 | try { | 13 | try { | ||
| 16 | - | const user = await db.select().from(users).where(eq(users.id, params.userId)).get(); | 14 | + | const user = await users.getById(params.userId); |
| 17 | 15 | ||||
| 18 | if (!user) { | 16 | if (!user) { | ||
| 19 | log.warn('User not found', { userId: maskedUserId }); | 17 | log.warn('User not found', { userId: maskedUserId }); | ||
| 20 | throw error(404, 'User not found'); | 18 | throw error(404, 'User not found'); | ||
| 21 | } | 19 | } | ||
| 22 | 20 | ||||
| 23 | - | const safeUser = createSafeUser(user); | 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 | + | }); | |||
| 24 | 28 | ||||
| 25 | log.debug('User data retrieved', { | 29 | log.debug('User data retrieved', { | ||
| 26 | userId: maskedUserId, | 30 | userId: maskedUserId, | ||