fix: race condition in SSE authentication and status updates
- commit
- 8d5ef03c5b922f38c6806590952cc09b9e3a0788
- author
- Kit · · 2025-02-14 16:58 UTC
- parent
- ca9d24a
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
2 files · +64 -20
M src/routes/+layout.svelte +20 -2
| @@ -8,19 +8,29 @@ | |||
| 8 | 8 | let isVisible = $state(true); | |
| 9 | 9 | let statusInterval = $state<number | undefined>(undefined); | |
| 10 | 10 | let lastUserUpdate = $state<string | null>(null); | |
| 11 | + | let updateInProgress = $state(false); | |
| 11 | 12 | ||
| 12 | 13 | async function updateUserStatus(status: 'online' | 'offline' | 'busy') { | |
| 13 | - | if (!data.user?.id) return; | |
| 14 | + | if (!data.user?.id || updateInProgress) return; | |
| 14 | 15 | ||
| 16 | + | updateInProgress = true; | |
| 15 | 17 | const timestamp = new Date().toISOString(); | |
| 16 | 18 | console.debug('Updating user status:', { | |
| 17 | 19 | userId: data.user.id, | |
| 18 | 20 | status, | |
| 19 | 21 | timestamp, | |
| 20 | - | trigger: new Error().stack?.split('\n')[2]?.trim() | |
| 22 | + | trigger: new Error().stack?.split('\n')[2]?.trim(), | |
| 23 | + | hasSession: !!data.session, | |
| 24 | + | cookiePresent: browser ? document.cookie.includes('session=') : false | |
| 21 | 25 | }); | |
| 22 | 26 | ||
| 23 | 27 | try { | |
| 28 | + | // Add a small delay to ensure cookie is set after login | |
| 29 | + | if (status === 'online' && !document.cookie.includes('session=')) { | |
| 30 | + | console.debug('Waiting for session cookie before status update...'); | |
| 31 | + | await new Promise(resolve => setTimeout(resolve, 500)); | |
| 32 | + | } | |
| 33 | + | ||
| 24 | 34 | const response = await fetch('/api/status', { | |
| 25 | 35 | method: 'POST', | |
| 26 | 36 | headers: { 'Content-Type': 'application/json' }, | |
| @@ -32,9 +42,17 @@ | |||
| 32 | 42 | ||
| 33 | 43 | if (!response.ok) { | |
| 34 | 44 | console.error('Failed to update user status:', await response.json()); | |
| 45 | + | } else { | |
| 46 | + | console.debug('Status update successful:', { | |
| 47 | + | userId: data.user.id, | |
| 48 | + | status, | |
| 49 | + | cookiePresent: document.cookie.includes('session=') | |
| 50 | + | }); | |
| 35 | 51 | } | |
| 36 | 52 | } catch (error) { | |
| 37 | 53 | console.error('Error updating user status:', error); | |
| 54 | + | } finally { | |
| 55 | + | updateInProgress = false; | |
| 38 | 56 | } | |
| 39 | 57 | } | |
| 40 | 58 | ||
| @@ -8,19 +8,29 @@ | |||||
| 8 | let isVisible = $state(true); | 8 | let isVisible = $state(true); | ||
| 9 | let statusInterval = $state<number | undefined>(undefined); | 9 | let statusInterval = $state<number | undefined>(undefined); | ||
| 10 | let lastUserUpdate = $state<string | null>(null); | 10 | let lastUserUpdate = $state<string | null>(null); | ||
| 11 | + | let updateInProgress = $state(false); | |||
| 11 | 12 | ||||
| 12 | async function updateUserStatus(status: 'online' | 'offline' | 'busy') { | 13 | async function updateUserStatus(status: 'online' | 'offline' | 'busy') { | ||
| 13 | - | if (!data.user?.id) return; | 14 | + | if (!data.user?.id || updateInProgress) return; |
| 14 | 15 | ||||
| 16 | + | updateInProgress = true; | |||
| 15 | const timestamp = new Date().toISOString(); | 17 | const timestamp = new Date().toISOString(); | ||
| 16 | console.debug('Updating user status:', { | 18 | console.debug('Updating user status:', { | ||
| 17 | userId: data.user.id, | 19 | userId: data.user.id, | ||
| 18 | status, | 20 | status, | ||
| 19 | timestamp, | 21 | timestamp, | ||
| 20 | - | trigger: new Error().stack?.split('\n')[2]?.trim() | 22 | + | trigger: new Error().stack?.split('\n')[2]?.trim(), |
| 23 | + | hasSession: !!data.session, | |||
| 24 | + | cookiePresent: browser ? document.cookie.includes('session=') : false | |||
| 21 | }); | 25 | }); | ||
| 22 | 26 | ||||
| 23 | try { | 27 | try { | ||
| 28 | + | // Add a small delay to ensure cookie is set after login | |||
| 29 | + | if (status === 'online' && !document.cookie.includes('session=')) { | |||
| 30 | + | console.debug('Waiting for session cookie before status update...'); | |||
| 31 | + | await new Promise(resolve => setTimeout(resolve, 500)); | |||
| 32 | + | } | |||
| 33 | + | ||||
| 24 | const response = await fetch('/api/status', { | 34 | const response = await fetch('/api/status', { | ||
| 25 | method: 'POST', | 35 | method: 'POST', | ||
| 26 | headers: { 'Content-Type': 'application/json' }, | 36 | headers: { 'Content-Type': 'application/json' }, | ||
| @@ -32,9 +42,17 @@ | |||||
| 32 | 42 | ||||
| 33 | if (!response.ok) { | 43 | if (!response.ok) { | ||
| 34 | console.error('Failed to update user status:', await response.json()); | 44 | console.error('Failed to update user status:', await response.json()); | ||
| 45 | + | } else { | |||
| 46 | + | console.debug('Status update successful:', { | |||
| 47 | + | userId: data.user.id, | |||
| 48 | + | status, | |||
| 49 | + | cookiePresent: document.cookie.includes('session=') | |||
| 50 | + | }); | |||
| 35 | } | 51 | } | ||
| 36 | } catch (error) { | 52 | } catch (error) { | ||
| 37 | console.error('Error updating user status:', error); | 53 | console.error('Error updating user status:', error); | ||
| 54 | + | } finally { | |||
| 55 | + | updateInProgress = false; | |||
| 38 | } | 56 | } | ||
| 39 | } | 57 | } | ||
| 40 | 58 | ||||
M src/routes/api/sse/+server.ts +44 -18
| @@ -95,9 +95,24 @@ if (typeof process !== 'undefined') { | |||
| 95 | 95 | }); | |
| 96 | 96 | } | |
| 97 | 97 | ||
| 98 | - | export const GET: RequestHandler = async ({ request, locals }) => { | |
| 98 | + | export const GET: RequestHandler = async ({ request, locals, cookies }) => { | |
| 99 | + | // Add detailed debug logging for authentication state | |
| 100 | + | console.log('[SSE] Connection attempt:', { | |
| 101 | + | hasLocals: !!locals, | |
| 102 | + | hasUser: !!locals.user, | |
| 103 | + | userId: locals.user?.id, | |
| 104 | + | hasSessionCookie: !!cookies.get('session'), | |
| 105 | + | headers: Object.fromEntries(request.headers.entries()) | |
| 106 | + | }); | |
| 107 | + | ||
| 99 | 108 | if (!locals.user) { | |
| 100 | - | return new Response('Unauthorized', { status: 401 }); | |
| 109 | + | console.log('[SSE] Authentication failed - no user in locals'); | |
| 110 | + | return new Response('Unauthorized', { | |
| 111 | + | status: 401, | |
| 112 | + | headers: { | |
| 113 | + | 'Content-Type': 'application/json' | |
| 114 | + | } | |
| 115 | + | }); | |
| 101 | 116 | } | |
| 102 | 117 | ||
| 103 | 118 | const userId = locals.user.id; | |
| @@ -107,22 +122,33 @@ export const GET: RequestHandler = async ({ request, locals }) => { | |||
| 107 | 122 | ||
| 108 | 123 | // Update user status to online and lastSeen timestamp when establishing connection | |
| 109 | 124 | const now = Date.now(); | |
| 110 | - | await db.update(users) | |
| 111 | - | .set({ | |
| 112 | - | status: 'online', | |
| 113 | - | lastSeen: now | |
| 114 | - | }) | |
| 115 | - | .where(eq(users.id, userId)); | |
| 116 | - | ||
| 117 | - | // Broadcast the status update | |
| 118 | - | sseEmitter.emit('sse', { | |
| 119 | - | type: 'userStatusUpdate', | |
| 120 | - | data: { | |
| 121 | - | userId, | |
| 122 | - | status: 'online', | |
| 123 | - | lastSeen: now | |
| 124 | - | } | |
| 125 | - | }); | |
| 125 | + | ||
| 126 | + | try { | |
| 127 | + | await db.update(users) | |
| 128 | + | .set({ | |
| 129 | + | status: 'online', | |
| 130 | + | lastSeen: now | |
| 131 | + | }) | |
| 132 | + | .where(eq(users.id, userId)); | |
| 133 | + | ||
| 134 | + | console.log('[SSE] User status updated to online:', { | |
| 135 | + | userId: `${userId.slice(0, 4)}...${userId.slice(-4)}`, | |
| 136 | + | timestamp: new Date(now).toISOString() | |
| 137 | + | }); | |
| 138 | + | ||
| 139 | + | // Broadcast the status update | |
| 140 | + | sseEmitter.emit('sse', { | |
| 141 | + | type: 'userStatusUpdate', | |
| 142 | + | data: { | |
| 143 | + | userId, | |
| 144 | + | status: 'online', | |
| 145 | + | lastSeen: now | |
| 146 | + | } | |
| 147 | + | }); | |
| 148 | + | } catch (error) { | |
| 149 | + | console.error('[SSE] Error updating user status:', error); | |
| 150 | + | // Continue with SSE setup even if status update fails | |
| 151 | + | } | |
| 126 | 152 | ||
| 127 | 153 | const stream = new ReadableStream({ | |
| 128 | 154 | start(controller) { | |
| @@ -95,9 +95,24 @@ if (typeof process !== 'undefined') { | |||||
| 95 | }); | 95 | }); | ||
| 96 | } | 96 | } | ||
| 97 | 97 | ||||
| 98 | - | export const GET: RequestHandler = async ({ request, locals }) => { | 98 | + | export const GET: RequestHandler = async ({ request, locals, cookies }) => { |
| 99 | + | // Add detailed debug logging for authentication state | |||
| 100 | + | console.log('[SSE] Connection attempt:', { | |||
| 101 | + | hasLocals: !!locals, | |||
| 102 | + | hasUser: !!locals.user, | |||
| 103 | + | userId: locals.user?.id, | |||
| 104 | + | hasSessionCookie: !!cookies.get('session'), | |||
| 105 | + | headers: Object.fromEntries(request.headers.entries()) | |||
| 106 | + | }); | |||
| 107 | + | ||||
| 99 | if (!locals.user) { | 108 | if (!locals.user) { | ||
| 100 | - | return new Response('Unauthorized', { status: 401 }); | 109 | + | console.log('[SSE] Authentication failed - no user in locals'); |
| 110 | + | return new Response('Unauthorized', { | |||
| 111 | + | status: 401, | |||
| 112 | + | headers: { | |||
| 113 | + | 'Content-Type': 'application/json' | |||
| 114 | + | } | |||
| 115 | + | }); | |||
| 101 | } | 116 | } | ||
| 102 | 117 | ||||
| 103 | const userId = locals.user.id; | 118 | const userId = locals.user.id; | ||
| @@ -107,22 +122,33 @@ export const GET: RequestHandler = async ({ request, locals }) => { | |||||
| 107 | 122 | ||||
| 108 | // Update user status to online and lastSeen timestamp when establishing connection | 123 | // Update user status to online and lastSeen timestamp when establishing connection | ||
| 109 | const now = Date.now(); | 124 | const now = Date.now(); | ||
| 110 | - | await db.update(users) | 125 | + | |
| 111 | - | .set({ | 126 | + | try { |
| 112 | - | status: 'online', | 127 | + | await db.update(users) |
| 113 | - | lastSeen: now | 128 | + | .set({ |
| 114 | - | }) | 129 | + | status: 'online', |
| 115 | - | .where(eq(users.id, userId)); | 130 | + | lastSeen: now |
| 116 | - | 131 | + | }) | |
| 117 | - | // Broadcast the status update | 132 | + | .where(eq(users.id, userId)); |
| 118 | - | sseEmitter.emit('sse', { | 133 | + | |
| 119 | - | type: 'userStatusUpdate', | 134 | + | console.log('[SSE] User status updated to online:', { |
| 120 | - | data: { | 135 | + | userId: `${userId.slice(0, 4)}...${userId.slice(-4)}`, |
| 121 | - | userId, | 136 | + | timestamp: new Date(now).toISOString() |
| 122 | - | status: 'online', | 137 | + | }); |
| 123 | - | lastSeen: now | 138 | + | |
| 124 | - | } | 139 | + | // Broadcast the status update |
| 125 | - | }); | 140 | + | sseEmitter.emit('sse', { |
| 141 | + | type: 'userStatusUpdate', | |||
| 142 | + | data: { | |||
| 143 | + | userId, | |||
| 144 | + | status: 'online', | |||
| 145 | + | lastSeen: now | |||
| 146 | + | } | |||
| 147 | + | }); | |||
| 148 | + | } catch (error) { | |||
| 149 | + | console.error('[SSE] Error updating user status:', error); | |||
| 150 | + | // Continue with SSE setup even if status update fails | |||
| 151 | + | } | |||
| 126 | 152 | ||||
| 127 | const stream = new ReadableStream({ | 153 | const stream = new ReadableStream({ | ||
| 128 | start(controller) { | 154 | start(controller) { | ||