refactor: Improve type safety and login flow in chat components
- commit
- ad216c719a9c53bbd689da84c4a45e9e1887dc04
- author
- Kit · · 2025-02-17 10:41 UTC
- parent
- 93e8e7a
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
7 files · +35 -18
M src/lib/components/aim-login.svelte +1 -1
| @@ -14,7 +14,7 @@ | |||
| 14 | 14 | activeTab?: 'signin' | 'signup'; | |
| 15 | 15 | } | |
| 16 | 16 | ||
| 17 | - | let { showAuth, onLoginSuccess, activeTab: initialTab = 'signin' } = $props(); | |
| 17 | + | let { showAuth = $bindable(), onLoginSuccess, activeTab: initialTab = 'signin' } = $props(); | |
| 18 | 18 | ||
| 19 | 19 | let activeTab = $state(initialTab); | |
| 20 | 20 | ||
| @@ -14,7 +14,7 @@ | |||||
| 14 | activeTab?: 'signin' | 'signup'; | 14 | activeTab?: 'signin' | 'signup'; | ||
| 15 | } | 15 | } | ||
| 16 | 16 | ||||
| 17 | - | let { showAuth, onLoginSuccess, activeTab: initialTab = 'signin' } = $props(); | 17 | + | let { showAuth = $bindable(), onLoginSuccess, activeTab: initialTab = 'signin' } = $props(); |
| 18 | 18 | ||||
| 19 | let activeTab = $state(initialTab); | 19 | let activeTab = $state(initialTab); | ||
| 20 | 20 | ||||
M src/lib/components/chat-room.svelte +7 -2
| @@ -381,6 +381,10 @@ let totalUsers = $derived(onlineUsers.length); | |||
| 381 | 381 | function openSignup() { | |
| 382 | 382 | showAuth = true; | |
| 383 | 383 | } | |
| 384 | + | ||
| 385 | + | function handleLoginSuccess() { | |
| 386 | + | showAuth = false; | |
| 387 | + | } | |
| 384 | 388 | </script> | |
| 385 | 389 | ||
| 386 | 390 | {#if showChatRoom} | |
| @@ -537,8 +541,9 @@ function openSignup() { | |||
| 537 | 541 | ||
| 538 | 542 | {#if showAuth} | |
| 539 | 543 | <AimLogin | |
| 540 | - | bind:showAuth | |
| 541 | - | activeTab={'signup' as const} | |
| 544 | + | bind:showAuth | |
| 545 | + | activeTab={'signup' as const} | |
| 546 | + | onLoginSuccess={handleLoginSuccess} | |
| 542 | 547 | /> | |
| 543 | 548 | {/if} | |
| 544 | 549 | ||
| @@ -381,6 +381,10 @@ let totalUsers = $derived(onlineUsers.length); | |||||
| 381 | function openSignup() { | 381 | function openSignup() { | ||
| 382 | showAuth = true; | 382 | showAuth = true; | ||
| 383 | } | 383 | } | ||
| 384 | + | ||||
| 385 | + | function handleLoginSuccess() { | |||
| 386 | + | showAuth = false; | |||
| 387 | + | } | |||
| 384 | </script> | 388 | </script> | ||
| 385 | 389 | ||||
| 386 | {#if showChatRoom} | 390 | {#if showChatRoom} | ||
| @@ -537,8 +541,9 @@ function openSignup() { | |||||
| 537 | 541 | ||||
| 538 | {#if showAuth} | 542 | {#if showAuth} | ||
| 539 | <AimLogin | 543 | <AimLogin | ||
| 540 | - | bind:showAuth | 544 | + | bind:showAuth |
| 541 | - | activeTab={'signup' as const} | 545 | + | activeTab={'signup' as const} |
| 546 | + | onLoginSuccess={handleLoginSuccess} | |||
| 542 | /> | 547 | /> | ||
| 543 | {/if} | 548 | {/if} | ||
| 544 | 549 | ||||
M src/lib/types/chat.ts +8 -3
| @@ -33,9 +33,14 @@ export type UserStatus = 'offline' | 'online' | 'away' | string; | |||
| 33 | 33 | export type SafeUser = Omit<User, 'password' | 'createdAt'>; | |
| 34 | 34 | ||
| 35 | 35 | // Helper function to create a safe user object | |
| 36 | - | export function createSafeUser(user: User): SafeUser { | |
| 37 | - | // eslint-disable-next-line @typescript-eslint/no-unused-vars | |
| 38 | - | const { password, createdAt, ...safeUser } = user; | |
| 36 | + | export function createSafeUser(user: Partial<User> & Pick<User, 'id' | 'nickname' | 'status'>): SafeUser { | |
| 37 | + | const safeUser: SafeUser = { | |
| 38 | + | id: user.id, | |
| 39 | + | nickname: user.nickname, | |
| 40 | + | status: user.status, | |
| 41 | + | avatarUrl: user.avatarUrl, | |
| 42 | + | lastSeen: user.lastSeen | |
| 43 | + | }; | |
| 39 | 44 | return safeUser; | |
| 40 | 45 | } | |
| 41 | 46 | ||
| @@ -33,9 +33,14 @@ export type UserStatus = 'offline' | 'online' | 'away' | string; | |||||
| 33 | export type SafeUser = Omit<User, 'password' | 'createdAt'>; | 33 | export type SafeUser = Omit<User, 'password' | 'createdAt'>; | ||
| 34 | 34 | ||||
| 35 | // Helper function to create a safe user object | 35 | // Helper function to create a safe user object | ||
| 36 | - | export function createSafeUser(user: User): SafeUser { | 36 | + | export function createSafeUser(user: Partial<User> & Pick<User, 'id' | 'nickname' | 'status'>): SafeUser { |
| 37 | - | // eslint-disable-next-line @typescript-eslint/no-unused-vars | 37 | + | const safeUser: SafeUser = { |
| 38 | - | const { password, createdAt, ...safeUser } = user; | 38 | + | id: user.id, |
| 39 | + | nickname: user.nickname, | |||
| 40 | + | status: user.status, | |||
| 41 | + | avatarUrl: user.avatarUrl, | |||
| 42 | + | lastSeen: user.lastSeen | |||
| 43 | + | }; | |||
| 39 | return safeUser; | 44 | return safeUser; | ||
| 40 | } | 45 | } | ||
| 41 | 46 | ||||
M src/routes/+page.svelte +7 -1
| @@ -6,6 +6,9 @@ | |||
| 6 | 6 | let showChatRoom = $state(true); | |
| 7 | 7 | let showAuth = $state(false); | |
| 8 | 8 | ||
| 9 | + | function handleLoginSuccess() { | |
| 10 | + | showAuth = false; | |
| 11 | + | } | |
| 9 | 12 | </script> | |
| 10 | 13 | ||
| 11 | 14 | <div class="desktop content-wrapper"> | |
| @@ -16,7 +19,10 @@ | |||
| 16 | 19 | {/if} | |
| 17 | 20 | ||
| 18 | 21 | {#if showAuth} | |
| 19 | - | <AimLogin bind:showAuth /> | |
| 22 | + | <AimLogin | |
| 23 | + | bind:showAuth | |
| 24 | + | onLoginSuccess={handleLoginSuccess} | |
| 25 | + | /> | |
| 20 | 26 | {/if} | |
| 21 | 27 | </div> | |
| 22 | 28 | ||
| @@ -6,6 +6,9 @@ | |||||
| 6 | let showChatRoom = $state(true); | 6 | let showChatRoom = $state(true); | ||
| 7 | let showAuth = $state(false); | 7 | let showAuth = $state(false); | ||
| 8 | 8 | ||||
| 9 | + | function handleLoginSuccess() { | |||
| 10 | + | showAuth = false; | |||
| 11 | + | } | |||
| 9 | </script> | 12 | </script> | ||
| 10 | 13 | ||||
| 11 | <div class="desktop content-wrapper"> | 14 | <div class="desktop content-wrapper"> | ||
| @@ -16,7 +19,10 @@ | |||||
| 16 | {/if} | 19 | {/if} | ||
| 17 | 20 | ||||
| 18 | {#if showAuth} | 21 | {#if showAuth} | ||
| 19 | - | <AimLogin bind:showAuth /> | 22 | + | <AimLogin |
| 23 | + | bind:showAuth | |||
| 24 | + | onLoginSuccess={handleLoginSuccess} | |||
| 25 | + | /> | |||
| 20 | {/if} | 26 | {/if} | ||
| 21 | </div> | 27 | </div> | ||
| 22 | 28 | ||||
M src/routes/api/chat/messages/+server.ts +2 -2
| @@ -7,14 +7,14 @@ import type { | |||
| 7 | 7 | SendMessageResponse, | |
| 8 | 8 | GetMessagesResponse | |
| 9 | 9 | } from '$lib/types/payloads'; | |
| 10 | - | import { desc, eq, lt, and } from 'drizzle-orm'; | |
| 10 | + | import { and, desc, eq, lt } from 'drizzle-orm/sql'; | |
| 11 | + | import type { SQL } from 'drizzle-orm/sql'; | |
| 11 | 12 | import { error } from '@sveltejs/kit'; | |
| 12 | 13 | import { sseEmitter } from '$lib/sseEmitter'; | |
| 13 | 14 | import { chatRooms } from '$lib/db/schema'; | |
| 14 | 15 | import { users } from '$lib/db/schema'; | |
| 15 | 16 | import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; | |
| 16 | 17 | import { createLogger } from '$lib/utils/logger.server'; | |
| 17 | - | import { SQL } from 'drizzle-orm'; | |
| 18 | 18 | ||
| 19 | 19 | const log = createLogger('chat-server'); | |
| 20 | 20 | ||
| @@ -7,14 +7,14 @@ import type { | |||||
| 7 | SendMessageResponse, | 7 | SendMessageResponse, | ||
| 8 | GetMessagesResponse | 8 | GetMessagesResponse | ||
| 9 | } from '$lib/types/payloads'; | 9 | } from '$lib/types/payloads'; | ||
| 10 | - | import { desc, eq, lt, and } from 'drizzle-orm'; | 10 | + | import { and, desc, eq, lt } from 'drizzle-orm/sql'; |
| 11 | + | import type { SQL } from 'drizzle-orm/sql'; | |||
| 11 | import { error } from '@sveltejs/kit'; | 12 | import { error } from '@sveltejs/kit'; | ||
| 12 | import { sseEmitter } from '$lib/sseEmitter'; | 13 | import { sseEmitter } from '$lib/sseEmitter'; | ||
| 13 | import { chatRooms } from '$lib/db/schema'; | 14 | import { chatRooms } from '$lib/db/schema'; | ||
| 14 | import { users } from '$lib/db/schema'; | 15 | import { users } from '$lib/db/schema'; | ||
| 15 | import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; | 16 | import { DEFAULT_CHAT_ROOM_ID } from '$lib/utils/chat.server'; | ||
| 16 | import { createLogger } from '$lib/utils/logger.server'; | 17 | import { createLogger } from '$lib/utils/logger.server'; | ||
| 17 | - | import { SQL } from 'drizzle-orm'; | |||
| 18 | 18 | ||||
| 19 | const log = createLogger('chat-server'); | 19 | const log = createLogger('chat-server'); | ||
| 20 | 20 | ||||
M src/routes/api/register/+server.ts +1 -1
| @@ -5,7 +5,7 @@ import { users } from '$lib/db/schema'; | |||
| 5 | 5 | import { hashPassword } from '$lib/utils/password'; | |
| 6 | 6 | import { validateTurnstileToken } from '$lib/utils/turnstile.server'; | |
| 7 | 7 | import { createLogger } from '$lib/utils/logger.server'; | |
| 8 | - | import { eq } from 'drizzle-orm'; | |
| 8 | + | import { eq } from 'drizzle-orm/sql'; | |
| 9 | 9 | ||
| 10 | 10 | const log = createLogger('register-server'); | |
| 11 | 11 | ||
| @@ -5,7 +5,7 @@ import { users } from '$lib/db/schema'; | |||||
| 5 | import { hashPassword } from '$lib/utils/password'; | 5 | import { hashPassword } from '$lib/utils/password'; | ||
| 6 | import { validateTurnstileToken } from '$lib/utils/turnstile.server'; | 6 | import { validateTurnstileToken } from '$lib/utils/turnstile.server'; | ||
| 7 | import { createLogger } from '$lib/utils/logger.server'; | 7 | import { createLogger } from '$lib/utils/logger.server'; | ||
| 8 | - | import { eq } from 'drizzle-orm'; | 8 | + | import { eq } from 'drizzle-orm/sql'; |
| 9 | 9 | ||||
| 10 | const log = createLogger('register-server'); | 10 | const log = createLogger('register-server'); | ||
| 11 | 11 | ||||
M src/routes/api/rooms/[roomId]/+server.ts +9 -8
| @@ -1,6 +1,7 @@ | |||
| 1 | - | import { desc, eq } from 'drizzle-orm'; | |
| 1 | + | import { eq } from 'drizzle-orm/sql'; | |
| 2 | 2 | import db from '$lib/db/db.server'; | |
| 3 | - | import { messages, users } from '$lib/db/schema'; | |
| 3 | + | import { users } from '$lib/db/schema'; | |
| 4 | + | import type { SafeUser } from '$lib/types/chat'; | |
| 4 | 5 | import type { PublicRoomResponse } from '$lib/types/payloads'; | |
| 5 | 6 | import { createSafeUser } from '$lib/types/chat'; | |
| 6 | 7 | import { createLogger } from '$lib/utils/logger.server'; | |
| @@ -29,7 +30,7 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||
| 29 | 30 | .execute(); | |
| 30 | 31 | ||
| 31 | 32 | // Process users to mark them as offline if they've timed out | |
| 32 | - | const processedUsers = fetchedUsers.map(user => { | |
| 33 | + | const processedUsers = fetchedUsers.map((user: SafeUser & { lastSeen: number }) => { | |
| 33 | 34 | const shouldBeOffline = user.status === 'online' && user.lastSeen < timeoutThreshold; | |
| 34 | 35 | return { | |
| 35 | 36 | ...user, | |
| @@ -38,13 +39,13 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||
| 38 | 39 | }); | |
| 39 | 40 | ||
| 40 | 41 | // Update any users that should be offline in the database | |
| 41 | - | const usersToUpdate = processedUsers.filter(user => | |
| 42 | + | const usersToUpdate = processedUsers.filter((user: SafeUser & { lastSeen: number }) => | |
| 42 | 43 | user.status === 'offline' && | |
| 43 | - | fetchedUsers.find(u => u.id === user.id)?.status === 'online' | |
| 44 | + | fetchedUsers.find((u: SafeUser & { lastSeen: number }) => u.id === user.id)?.status === 'online' | |
| 44 | 45 | ); | |
| 45 | 46 | ||
| 46 | 47 | if (usersToUpdate.length > 0) { | |
| 47 | - | await Promise.all(usersToUpdate.map(user => | |
| 48 | + | await Promise.all(usersToUpdate.map((user: SafeUser & { lastSeen: number }) => | |
| 48 | 49 | db.update(users) | |
| 49 | 50 | .set({ status: 'offline', lastSeen: Date.now() }) | |
| 50 | 51 | .where(eq(users.id, user.id)) | |
| @@ -53,12 +54,12 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||
| 53 | 54 | ||
| 54 | 55 | log.debug('Updated offline status for users', { | |
| 55 | 56 | count: usersToUpdate.length, | |
| 56 | - | userIds: usersToUpdate.map(u => u.id) | |
| 57 | + | userIds: usersToUpdate.map((u: SafeUser & { lastSeen: number }) => u.id) | |
| 57 | 58 | }); | |
| 58 | 59 | } | |
| 59 | 60 | ||
| 60 | 61 | // Sanitize user data using createSafeUser | |
| 61 | - | const sanitizedUsers = processedUsers.map(user => createSafeUser(user)); | |
| 62 | + | const sanitizedUsers = processedUsers.map((user: SafeUser & { lastSeen: number }) => createSafeUser(user)); | |
| 62 | 63 | ||
| 63 | 64 | const responseData: PublicRoomResponse = { | |
| 64 | 65 | success: true, | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | - | import { desc, eq } from 'drizzle-orm'; | 1 | + | import { eq } from 'drizzle-orm/sql'; |
| 2 | import db from '$lib/db/db.server'; | 2 | import db from '$lib/db/db.server'; | ||
| 3 | - | import { messages, users } from '$lib/db/schema'; | 3 | + | import { users } from '$lib/db/schema'; |
| 4 | + | import type { SafeUser } from '$lib/types/chat'; | |||
| 4 | import type { PublicRoomResponse } from '$lib/types/payloads'; | 5 | import type { PublicRoomResponse } from '$lib/types/payloads'; | ||
| 5 | import { createSafeUser } from '$lib/types/chat'; | 6 | import { createSafeUser } from '$lib/types/chat'; | ||
| 6 | import { createLogger } from '$lib/utils/logger.server'; | 7 | import { createLogger } from '$lib/utils/logger.server'; | ||
| @@ -29,7 +30,7 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||||
| 29 | .execute(); | 30 | .execute(); | ||
| 30 | 31 | ||||
| 31 | // Process users to mark them as offline if they've timed out | 32 | // Process users to mark them as offline if they've timed out | ||
| 32 | - | const processedUsers = fetchedUsers.map(user => { | 33 | + | const processedUsers = fetchedUsers.map((user: SafeUser & { lastSeen: number }) => { |
| 33 | const shouldBeOffline = user.status === 'online' && user.lastSeen < timeoutThreshold; | 34 | const shouldBeOffline = user.status === 'online' && user.lastSeen < timeoutThreshold; | ||
| 34 | return { | 35 | return { | ||
| 35 | ...user, | 36 | ...user, | ||
| @@ -38,13 +39,13 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||||
| 38 | }); | 39 | }); | ||
| 39 | 40 | ||||
| 40 | // Update any users that should be offline in the database | 41 | // Update any users that should be offline in the database | ||
| 41 | - | const usersToUpdate = processedUsers.filter(user => | 42 | + | const usersToUpdate = processedUsers.filter((user: SafeUser & { lastSeen: number }) => |
| 42 | user.status === 'offline' && | 43 | user.status === 'offline' && | ||
| 43 | - | fetchedUsers.find(u => u.id === user.id)?.status === 'online' | 44 | + | fetchedUsers.find((u: SafeUser & { lastSeen: number }) => u.id === user.id)?.status === 'online' |
| 44 | ); | 45 | ); | ||
| 45 | 46 | ||||
| 46 | if (usersToUpdate.length > 0) { | 47 | if (usersToUpdate.length > 0) { | ||
| 47 | - | await Promise.all(usersToUpdate.map(user => | 48 | + | await Promise.all(usersToUpdate.map((user: SafeUser & { lastSeen: number }) => |
| 48 | db.update(users) | 49 | db.update(users) | ||
| 49 | .set({ status: 'offline', lastSeen: Date.now() }) | 50 | .set({ status: 'offline', lastSeen: Date.now() }) | ||
| 50 | .where(eq(users.id, user.id)) | 51 | .where(eq(users.id, user.id)) | ||
| @@ -53,12 +54,12 @@ export async function GET({ params, url, locals }): Promise<Response> { | |||||
| 53 | 54 | ||||
| 54 | log.debug('Updated offline status for users', { | 55 | log.debug('Updated offline status for users', { | ||
| 55 | count: usersToUpdate.length, | 56 | count: usersToUpdate.length, | ||
| 56 | - | userIds: usersToUpdate.map(u => u.id) | 57 | + | userIds: usersToUpdate.map((u: SafeUser & { lastSeen: number }) => u.id) |
| 57 | }); | 58 | }); | ||
| 58 | } | 59 | } | ||
| 59 | 60 | ||||
| 60 | // Sanitize user data using createSafeUser | 61 | // Sanitize user data using createSafeUser | ||
| 61 | - | const sanitizedUsers = processedUsers.map(user => createSafeUser(user)); | 62 | + | const sanitizedUsers = processedUsers.map((user: SafeUser & { lastSeen: number }) => createSafeUser(user)); |
| 62 | 63 | ||||
| 63 | const responseData: PublicRoomResponse = { | 64 | const responseData: PublicRoomResponse = { | ||
| 64 | success: true, | 65 | success: true, | ||