detached cb3eadb

refactor(auth): migrate session management to Convex

commit
cb3eadb4b4597068dca294cc745b5d16d36fed31
author
bnema · · 2026-01-18 18:34 UTC
parent
47beb8f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +43 -51
M src/hooks.server.ts +5 -6
@@ -1,7 +1,6 @@
11 import type { Handle } from '@sveltejs/kit';
2-import db from '$lib/db/db.server';
2+import { users } from '$lib/db/convex.server';
33 import { handleRateLimit } from '$lib/api/rate-limiter';
4-import { users } from '$lib/db/schema';
54 import { createLogger } from '$lib/utils/logger.server';
65 import { ensureDefaultChatRoom } from '$lib/utils/chat.server';
76 import { validateSessionToken } from '$lib/api/session.server';
@@ -24,11 +23,11 @@ function addSecurityHeaders(response: Response): Response {
2423 });
2524 }
2625
27-// Set all users to offline on server start
26+// Set all users to offline on server start (using Convex)
2827 async function setAllUsersOffline() {
2928 try {
30- await db.update(users).set({ status: 'offline' });
31- log.info('All users set to offline on server start');
29+ const result = await users.setAllOffline();
30+ log.info('All users set to offline on server start', { updated: result.updated });
3231 } catch (error: unknown) {
3332 log.error('Failed to set users offline on server start:', { error });
3433 }
@@ -38,7 +37,7 @@ async function setAllUsersOffline() {
3837 Promise.resolve()
3938 .then(async () => {
4039 log.info('Initializing server...');
41- await ensureDefaultChatRoom(db);
40+ await ensureDefaultChatRoom();
4241 await setAllUsersOffline();
4342 log.info('Server initialized successfully');
4443 })
@@ -1,7 +1,6 @@
1 import type { Handle } from '@sveltejs/kit';1 import type { Handle } from '@sveltejs/kit';
2-import db from '$lib/db/db.server';2+import { users } from '$lib/db/convex.server';
3 import { handleRateLimit } from '$lib/api/rate-limiter';3 import { handleRateLimit } from '$lib/api/rate-limiter';
4-import { users } from '$lib/db/schema';
5 import { createLogger } from '$lib/utils/logger.server';4 import { createLogger } from '$lib/utils/logger.server';
6 import { ensureDefaultChatRoom } from '$lib/utils/chat.server';5 import { ensureDefaultChatRoom } from '$lib/utils/chat.server';
7 import { validateSessionToken } from '$lib/api/session.server';6 import { validateSessionToken } from '$lib/api/session.server';
@@ -24,11 +23,11 @@ function addSecurityHeaders(response: Response): Response {
24 });23 });
25 }24 }
26 25
27-// Set all users to offline on server start26+// Set all users to offline on server start (using Convex)
28 async function setAllUsersOffline() {27 async function setAllUsersOffline() {
29 try {28 try {
30- await db.update(users).set({ status: 'offline' });29+ const result = await users.setAllOffline();
31- log.info('All users set to offline on server start');30+ log.info('All users set to offline on server start', { updated: result.updated });
32 } catch (error: unknown) {31 } catch (error: unknown) {
33 log.error('Failed to set users offline on server start:', { error });32 log.error('Failed to set users offline on server start:', { error });
34 }33 }
@@ -38,7 +37,7 @@ async function setAllUsersOffline() {
38 Promise.resolve()37 Promise.resolve()
39 .then(async () => {38 .then(async () => {
40 log.info('Initializing server...');39 log.info('Initializing server...');
41- await ensureDefaultChatRoom(db);40+ await ensureDefaultChatRoom();
42 await setAllUsersOffline();41 await setAllUsersOffline();
43 log.info('Server initialized successfully');42 log.info('Server initialized successfully');
44 })43 })
M src/lib/api/session.server.ts +38 -45
@@ -1,6 +1,4 @@
1-import db from '../db/db.server';
2-import { sessions, users } from '../db/schema';
3-import { eq } from 'drizzle-orm';
1+import { sessions } from '../db/convex.server';
42 import type { Session, User } from '../types/chat';
53 import { createLogger } from '../utils/logger.server';
64
@@ -17,7 +15,6 @@ export async function sha256(message: string): Promise<string> {
1715 }
1816
1917 // Helper: Custom Base32 encoder using the standard alphabet.
20-// We use this instead of @oslojs/encoding given Cloudflare Pages compatibility.
2118 const base32Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
2219 function base32Encode(data: Uint8Array): string {
2320 let bits = 0;
@@ -52,65 +49,61 @@ export function generateSessionToken(): string {
5249 // The session ID is the SHA-256 hash of the token.
5350 export async function createSession(token: string, userId: string): Promise<Session> {
5451 log.debug('Creating session', { userId });
55- const sessionId = await sha256(token);
52+ const tokenHash = await sha256(token);
5653 const createdAt = Date.now();
5754 const expiresAt = createdAt + 7 * 24 * 60 * 60 * 1000; // 7 days expiry
58- await db.insert(sessions).values({
59- id: sessionId,
60- userId,
61- createdAt,
62- expiresAt
63- });
64- log.debug('Session created', { sessionId });
65- return { id: sessionId, userId, createdAt, expiresAt };
55+
56+ await sessions.create(tokenHash, userId, expiresAt);
57+
58+ log.debug('Session created', { tokenHash });
59+ return { id: tokenHash, userId, createdAt, expiresAt };
6660 }
6761
6862 /**
6963 * Type distributed through our API.
7064 * Returns an object with both session and user if valid, or nulls if not.
71- * Note: This returns the full User type since it's used server-side in hooks.
7265 */
7366 export type SessionValidationResult =
7467 | { session: Session; user: User }
7568 | { session: null; user: null };
7669
77-// Validate a session token by converting it to its SHA‑256 hash, checking expiration, and fetching the user.
70+// Validate a session token by converting it to its SHA-256 hash, checking expiration, and fetching the user.
7871 export async function validateSessionToken(token: string): Promise<SessionValidationResult> {
7972 log.debug('Validating session token');
80- const sessionId = await sha256(token);
81- const session = await db.query.sessions.findFirst({
82- where: eq(sessions.id, sessionId)
83- });
84- if (!session) {
85- log.debug('Session not found', { sessionId });
86- return { session: null, user: null };
87- }
88- const now = Date.now();
89- if (session.expiresAt < now) {
90- log.debug('Session expired', { sessionId });
91- return { session: null, user: null };
92- }
93- const user = await db.query.users.findFirst({
94- where: eq(users.id, session.userId)
95- });
96- if (!user) {
97- log.debug('User not found for session', { sessionId });
73+ const tokenHash = await sha256(token);
74+
75+ const result = await sessions.validate(tokenHash);
76+
77+ if (!result.session || !result.user) {
78+ log.debug('Session not found or invalid', { tokenHash });
9879 return { session: null, user: null };
9980 }
100- log.debug('Session validated', { sessionId, userId: user.id });
81+
82+ // Map Convex types to our local types - using native Convex IDs
83+ const session: Session = {
84+ id: result.session.tokenHash,
85+ userId: result.user._id,
86+ createdAt: result.session.createdAt,
87+ expiresAt: result.session.expiresAt
88+ };
89+
90+ const user: User = {
91+ id: result.user._id,
92+ password: result.user.password,
93+ nickname: result.user.nickname,
94+ status: result.user.status as User['status'],
95+ avatarUrl: result.user.avatarUrl,
96+ createdAt: result.user.createdAt,
97+ lastSeen: result.user.lastSeen
98+ };
99+
100+ log.debug('Session validated', { tokenHash, userId: user.id });
101101 return { session, user };
102102 }
103103
104104 // Invalidate a specific session by deleting it from the database.
105-export async function invalidateSession(sessionId: string): Promise<void> {
106- log.debug('Invalidating session', { sessionId });
107- await db.delete(sessions).where(eq(sessions.id, sessionId));
108- log.debug('Session invalidated', { sessionId });
109-}
110-
111-// Invalidate all sessions for a given user.
112-export async function invalidateAllSessions(userId: string): Promise<void> {
113- log.debug('Invalidating all sessions for user', { userId });
114- await db.delete(sessions).where(eq(sessions.userId, userId));
115- log.debug('All sessions invalidated', { userId });
105+export async function invalidateSession(tokenHash: string): Promise<void> {
106+ log.debug('Invalidating session', { tokenHash });
107+ await sessions.remove(tokenHash);
108+ log.debug('Session invalidated', { tokenHash });
116109 }
@@ -1,6 +1,4 @@
1-import db from '../db/db.server';1+import { sessions } from '../db/convex.server';
2-import { sessions, users } from '../db/schema';
3-import { eq } from 'drizzle-orm';
4 import type { Session, User } from '../types/chat';2 import type { Session, User } from '../types/chat';
5 import { createLogger } from '../utils/logger.server';3 import { createLogger } from '../utils/logger.server';
6 4
@@ -17,7 +15,6 @@ export async function sha256(message: string): Promise<string> {
17 }15 }
18 16
19 // Helper: Custom Base32 encoder using the standard alphabet.17 // Helper: Custom Base32 encoder using the standard alphabet.
20-// We use this instead of @oslojs/encoding given Cloudflare Pages compatibility.
21 const base32Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';18 const base32Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
22 function base32Encode(data: Uint8Array): string {19 function base32Encode(data: Uint8Array): string {
23 let bits = 0;20 let bits = 0;
@@ -52,65 +49,61 @@ export function generateSessionToken(): string {
52 // The session ID is the SHA-256 hash of the token.49 // The session ID is the SHA-256 hash of the token.
53 export async function createSession(token: string, userId: string): Promise<Session> {50 export async function createSession(token: string, userId: string): Promise<Session> {
54 log.debug('Creating session', { userId });51 log.debug('Creating session', { userId });
55- const sessionId = await sha256(token);52+ const tokenHash = await sha256(token);
56 const createdAt = Date.now();53 const createdAt = Date.now();
57 const expiresAt = createdAt + 7 * 24 * 60 * 60 * 1000; // 7 days expiry54 const expiresAt = createdAt + 7 * 24 * 60 * 60 * 1000; // 7 days expiry
58- await db.insert(sessions).values({55+
59- id: sessionId,56+ await sessions.create(tokenHash, userId, expiresAt);
60- userId,57+
61- createdAt,58+ log.debug('Session created', { tokenHash });
62- expiresAt59+ return { id: tokenHash, userId, createdAt, expiresAt };
63- });
64- log.debug('Session created', { sessionId });
65- return { id: sessionId, userId, createdAt, expiresAt };
66 }60 }
67 61
68 /**62 /**
69 * Type distributed through our API.63 * Type distributed through our API.
70 * Returns an object with both session and user if valid, or nulls if not.64 * Returns an object with both session and user if valid, or nulls if not.
71- * Note: This returns the full User type since it's used server-side in hooks.
72 */65 */
73 export type SessionValidationResult =66 export type SessionValidationResult =
74 | { session: Session; user: User }67 | { session: Session; user: User }
75 | { session: null; user: null };68 | { session: null; user: null };
76 69
77-// Validate a session token by converting it to its SHA‑256 hash, checking expiration, and fetching the user.70+// Validate a session token by converting it to its SHA-256 hash, checking expiration, and fetching the user.
78 export async function validateSessionToken(token: string): Promise<SessionValidationResult> {71 export async function validateSessionToken(token: string): Promise<SessionValidationResult> {
79 log.debug('Validating session token');72 log.debug('Validating session token');
80- const sessionId = await sha256(token);73+ const tokenHash = await sha256(token);
81- const session = await db.query.sessions.findFirst({74+
82- where: eq(sessions.id, sessionId)75+ const result = await sessions.validate(tokenHash);
83- });76+
84- if (!session) {77+ if (!result.session || !result.user) {
85- log.debug('Session not found', { sessionId });78+ log.debug('Session not found or invalid', { tokenHash });
86- return { session: null, user: null };
87- }
88- const now = Date.now();
89- if (session.expiresAt < now) {
90- log.debug('Session expired', { sessionId });
91- return { session: null, user: null };
92- }
93- const user = await db.query.users.findFirst({
94- where: eq(users.id, session.userId)
95- });
96- if (!user) {
97- log.debug('User not found for session', { sessionId });
98 return { session: null, user: null };79 return { session: null, user: null };
99 }80 }
100- log.debug('Session validated', { sessionId, userId: user.id });81+
82+ // Map Convex types to our local types - using native Convex IDs
83+ const session: Session = {
84+ id: result.session.tokenHash,
85+ userId: result.user._id,
86+ createdAt: result.session.createdAt,
87+ expiresAt: result.session.expiresAt
88+ };
89+
90+ const user: User = {
91+ id: result.user._id,
92+ password: result.user.password,
93+ nickname: result.user.nickname,
94+ status: result.user.status as User['status'],
95+ avatarUrl: result.user.avatarUrl,
96+ createdAt: result.user.createdAt,
97+ lastSeen: result.user.lastSeen
98+ };
99+
100+ log.debug('Session validated', { tokenHash, userId: user.id });
101 return { session, user };101 return { session, user };
102 }102 }
103 103
104 // Invalidate a specific session by deleting it from the database.104 // Invalidate a specific session by deleting it from the database.
105-export async function invalidateSession(sessionId: string): Promise<void> {105+export async function invalidateSession(tokenHash: string): Promise<void> {
106- log.debug('Invalidating session', { sessionId });106+ log.debug('Invalidating session', { tokenHash });
107- await db.delete(sessions).where(eq(sessions.id, sessionId));107+ await sessions.remove(tokenHash);
108- log.debug('Session invalidated', { sessionId });108+ log.debug('Session invalidated', { tokenHash });
109-}
110-
111-// Invalidate all sessions for a given user.
112-export async function invalidateAllSessions(userId: string): Promise<void> {
113- log.debug('Invalidating all sessions for user', { userId });
114- await db.delete(sessions).where(eq(sessions.userId, userId));
115- log.debug('All sessions invalidated', { userId });
116 }109 }

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