detached a5ef447

refactor(api): migrate auth endpoints to Convex

commit
a5ef44750336b45c6b65067e2ad7cac7a3dcb473
author
bnema · · 2026-01-18 18:34 UTC
parent
cb3eadb
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +32 -36
M src/routes/api/register/+server.ts +7 -13
@@ -1,11 +1,9 @@
11 import type { RequestHandler } from '@sveltejs/kit';
22 import type { RegisterResponseSuccess, RegisterResponseError } from '$lib/types/payloads';
3-import db from '$lib/db/db.server';
4-import { users } from '$lib/db/schema';
3+import { users } from '$lib/db/convex.server';
54 import { hashPassword } from '$lib/utils/password';
65 import { createRegistrationSchema, DEFAULT_PASSWORD_CONSTRAINTS } from '$lib/validation/password';
76 import { createLogger } from '$lib/utils/logger.server';
8-import { eq } from 'drizzle-orm/sql';
97
108 const log = createLogger('register-server');
119
@@ -102,9 +100,9 @@ export const POST: RequestHandler = async ({ request }) => {
102100 const username = suUsername.trim();
103101 const password = suPassword.trim();
104102
105- // Check if nickname already exists
106- const existingUser = await db.select().from(users).where(eq(users.nickname, username));
107- if (existingUser.length > 0) {
103+ // Check if nickname already exists using Convex
104+ const existingUser = await users.getByNickname(username);
105+ if (existingUser) {
108106 log.warn('Username already exists', { username });
109107 return new Response(
110108 JSON.stringify({ error: 'This username is already taken' } as RegisterResponseError),
@@ -123,17 +121,13 @@ export const POST: RequestHandler = async ({ request }) => {
123121 { status: 500 }
124122 );
125123 }
126- // Insert the new user into the database.
124+
125+ // Insert the new user into Convex
127126 try {
128- await db.insert(users).values({
129- nickname: username,
130- password: hashedPassword,
131- createdAt: Date.now()
132- });
127+ await users.create(username, hashedPassword);
133128 log.info(`User ${username} registered successfully`);
134129 } catch (err: unknown) {
135130 log.error('Database insertion error', { error: err as object });
136- // Assume a duplicate user error if the email (or derived unique field) already exists.
137131 return new Response(
138132 JSON.stringify({
139133 error: 'User registration failed. Possibly user already exists.'
@@ -1,11 +1,9 @@
1 import type { RequestHandler } from '@sveltejs/kit';1 import type { RequestHandler } from '@sveltejs/kit';
2 import type { RegisterResponseSuccess, RegisterResponseError } from '$lib/types/payloads';2 import type { RegisterResponseSuccess, RegisterResponseError } from '$lib/types/payloads';
3-import db from '$lib/db/db.server';3+import { users } from '$lib/db/convex.server';
4-import { users } from '$lib/db/schema';
5 import { hashPassword } from '$lib/utils/password';4 import { hashPassword } from '$lib/utils/password';
6 import { createRegistrationSchema, DEFAULT_PASSWORD_CONSTRAINTS } from '$lib/validation/password';5 import { createRegistrationSchema, DEFAULT_PASSWORD_CONSTRAINTS } from '$lib/validation/password';
7 import { createLogger } from '$lib/utils/logger.server';6 import { createLogger } from '$lib/utils/logger.server';
8-import { eq } from 'drizzle-orm/sql';
9 7
10 const log = createLogger('register-server');8 const log = createLogger('register-server');
11 9
@@ -102,9 +100,9 @@ export const POST: RequestHandler = async ({ request }) => {
102 const username = suUsername.trim();100 const username = suUsername.trim();
103 const password = suPassword.trim();101 const password = suPassword.trim();
104 102
105- // Check if nickname already exists103+ // Check if nickname already exists using Convex
106- const existingUser = await db.select().from(users).where(eq(users.nickname, username));104+ const existingUser = await users.getByNickname(username);
107- if (existingUser.length > 0) {105+ if (existingUser) {
108 log.warn('Username already exists', { username });106 log.warn('Username already exists', { username });
109 return new Response(107 return new Response(
110 JSON.stringify({ error: 'This username is already taken' } as RegisterResponseError),108 JSON.stringify({ error: 'This username is already taken' } as RegisterResponseError),
@@ -123,17 +121,13 @@ export const POST: RequestHandler = async ({ request }) => {
123 { status: 500 }121 { status: 500 }
124 );122 );
125 }123 }
126- // Insert the new user into the database.124+
125+ // Insert the new user into Convex
127 try {126 try {
128- await db.insert(users).values({127+ await users.create(username, hashedPassword);
129- nickname: username,
130- password: hashedPassword,
131- createdAt: Date.now()
132- });
133 log.info(`User ${username} registered successfully`);128 log.info(`User ${username} registered successfully`);
134 } catch (err: unknown) {129 } catch (err: unknown) {
135 log.error('Database insertion error', { error: err as object });130 log.error('Database insertion error', { error: err as object });
136- // Assume a duplicate user error if the email (or derived unique field) already exists.
137 return new Response(131 return new Response(
138 JSON.stringify({132 JSON.stringify({
139 error: 'User registration failed. Possibly user already exists.'133 error: 'User registration failed. Possibly user already exists.'
M src/routes/api/session/login/+server.ts +19 -19
@@ -1,8 +1,6 @@
11 import type { RequestHandler } from '@sveltejs/kit';
22 import type { LoginResponseSuccess, LoginResponseError } from '$lib/types/payloads';
3-import db from '$lib/db/db.server';
4-import { users } from '$lib/db/schema';
5-import { eq } from 'drizzle-orm/sql';
3+import { users } from '$lib/db/convex.server';
64 import { verifyPassword } from '$lib/utils/password';
75 import { generateSessionToken, createSession } from '$lib/api/session.server';
86 import { setSessionTokenCookie } from '$lib/api/session.cookie';
@@ -84,10 +82,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
8482 });
8583 }
8684
87- // Find user by username
88- const user = await db.query.users.findFirst({
89- where: eq(users.nickname, username.trim())
90- });
85+ // Find user by username using Convex
86+ const user = await users.getByNickname(username.trim());
9187
9288 if (!user) {
9389 attemptData.count++;
@@ -107,6 +103,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
107103 );
108104 }
109105
106+ // Use native Convex _id
107+ const userId = user._id;
108+
110109 // Verify password
111110 try {
112111 const isValid = await verifyPassword(password.trim(), user.password);
@@ -117,7 +116,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
117116 log.warn('Login failed - invalid password', {
118117 maskedIp,
119118 attemptCount: attemptData.count,
120- userId: `${user.id.slice(0, 4)}...${user.id.slice(-4)}`
119+ odlUserId: `${userId.slice(0, 4)}...${userId.slice(-4)}`
121120 });
122121 const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count);
123122 return new Response(
@@ -139,29 +138,30 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
139138
140139 // Generate session token and create session
141140 const token = generateSessionToken();
142- const session = await createSession(token, user.id);
141+ const session = await createSession(token, userId);
143142
144- // Update user status to online
145- await db
146- .update(users)
147- .set({
148- status: 'online',
149- lastSeen: Date.now()
150- })
151- .where(eq(users.id, user.id));
143+ // Update user status to online using Convex
144+ await users.updateStatus(userId, 'online');
152145
153146 // Set session cookie
154147 setSessionTokenCookie({ cookies }, token, session.expiresAt);
155148
156149 log.info('Login successful', {
157- userId: `${user.id.slice(0, 4)}...${user.id.slice(-4)}`,
150+ odlUserId: `${userId.slice(0, 4)}...${userId.slice(-4)}`,
158151 expiresAt: new Date(session.expiresAt).toISOString()
159152 });
160153
154+ // Return user with native Convex ID
161155 return new Response(
162156 JSON.stringify({
163157 success: true,
164- user: createSafeUser(user)
158+ user: createSafeUser({
159+ id: userId,
160+ nickname: user.nickname,
161+ status: 'online',
162+ avatarUrl: user.avatarUrl,
163+ lastSeen: user.lastSeen
164+ })
165165 } as LoginResponseSuccess),
166166 {
167167 status: 200
@@ -1,8 +1,6 @@
1 import type { RequestHandler } from '@sveltejs/kit';1 import type { RequestHandler } from '@sveltejs/kit';
2 import type { LoginResponseSuccess, LoginResponseError } from '$lib/types/payloads';2 import type { LoginResponseSuccess, LoginResponseError } from '$lib/types/payloads';
3-import db from '$lib/db/db.server';3+import { users } from '$lib/db/convex.server';
4-import { users } from '$lib/db/schema';
5-import { eq } from 'drizzle-orm/sql';
6 import { verifyPassword } from '$lib/utils/password';4 import { verifyPassword } from '$lib/utils/password';
7 import { generateSessionToken, createSession } from '$lib/api/session.server';5 import { generateSessionToken, createSession } from '$lib/api/session.server';
8 import { setSessionTokenCookie } from '$lib/api/session.cookie';6 import { setSessionTokenCookie } from '$lib/api/session.cookie';
@@ -84,10 +82,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
84 });82 });
85 }83 }
86 84
87- // Find user by username85+ // Find user by username using Convex
88- const user = await db.query.users.findFirst({86+ const user = await users.getByNickname(username.trim());
89- where: eq(users.nickname, username.trim())
90- });
91 87
92 if (!user) {88 if (!user) {
93 attemptData.count++;89 attemptData.count++;
@@ -107,6 +103,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
107 );103 );
108 }104 }
109 105
106+ // Use native Convex _id
107+ const userId = user._id;
108+
110 // Verify password109 // Verify password
111 try {110 try {
112 const isValid = await verifyPassword(password.trim(), user.password);111 const isValid = await verifyPassword(password.trim(), user.password);
@@ -117,7 +116,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
117 log.warn('Login failed - invalid password', {116 log.warn('Login failed - invalid password', {
118 maskedIp,117 maskedIp,
119 attemptCount: attemptData.count,118 attemptCount: attemptData.count,
120- userId: `${user.id.slice(0, 4)}...${user.id.slice(-4)}`119+ odlUserId: `${userId.slice(0, 4)}...${userId.slice(-4)}`
121 });120 });
122 const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count);121 const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count);
123 return new Response(122 return new Response(
@@ -139,29 +138,30 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
139 138
140 // Generate session token and create session139 // Generate session token and create session
141 const token = generateSessionToken();140 const token = generateSessionToken();
142- const session = await createSession(token, user.id);141+ const session = await createSession(token, userId);
143 142
144- // Update user status to online143+ // Update user status to online using Convex
145- await db144+ await users.updateStatus(userId, 'online');
146- .update(users)
147- .set({
148- status: 'online',
149- lastSeen: Date.now()
150- })
151- .where(eq(users.id, user.id));
152 145
153 // Set session cookie146 // Set session cookie
154 setSessionTokenCookie({ cookies }, token, session.expiresAt);147 setSessionTokenCookie({ cookies }, token, session.expiresAt);
155 148
156 log.info('Login successful', {149 log.info('Login successful', {
157- userId: `${user.id.slice(0, 4)}...${user.id.slice(-4)}`,150+ odlUserId: `${userId.slice(0, 4)}...${userId.slice(-4)}`,
158 expiresAt: new Date(session.expiresAt).toISOString()151 expiresAt: new Date(session.expiresAt).toISOString()
159 });152 });
160 153
154+ // Return user with native Convex ID
161 return new Response(155 return new Response(
162 JSON.stringify({156 JSON.stringify({
163 success: true,157 success: true,
164- user: createSafeUser(user)158+ user: createSafeUser({
159+ id: userId,
160+ nickname: user.nickname,
161+ status: 'online',
162+ avatarUrl: user.avatarUrl,
163+ lastSeen: user.lastSeen
164+ })
165 } as LoginResponseSuccess),165 } as LoginResponseSuccess),
166 {166 {
167 status: 200167 status: 200
M src/routes/api/session/logout/+server.ts +3 -2
@@ -1,11 +1,12 @@
11 import { json } from '@sveltejs/kit';
2+import type { RequestHandler } from './$types';
23 import { invalidateSession } from '$lib/api/session.server';
34 import { sha256 } from '$lib/api/session.server'; // re-use sha256 helper to compute session id
45 import { createLogger } from '$lib/utils/logger.server';
56
67 const log = createLogger('logout-server');
78
8-export async function POST({ cookies }) {
9+export const POST: RequestHandler = async ({ cookies }) => {
910 const token = cookies.get('session');
1011
1112 if (token) {
@@ -20,4 +21,4 @@ export async function POST({ cookies }) {
2021 }
2122
2223 return json({ success: true });
23-}
24+};
@@ -1,11 +1,12 @@
1 import { json } from '@sveltejs/kit';1 import { json } from '@sveltejs/kit';
2+import type { RequestHandler } from './$types';
2 import { invalidateSession } from '$lib/api/session.server';3 import { invalidateSession } from '$lib/api/session.server';
3 import { sha256 } from '$lib/api/session.server'; // re-use sha256 helper to compute session id4 import { sha256 } from '$lib/api/session.server'; // re-use sha256 helper to compute session id
4 import { createLogger } from '$lib/utils/logger.server';5 import { createLogger } from '$lib/utils/logger.server';
5 6
6 const log = createLogger('logout-server');7 const log = createLogger('logout-server');
7 8
8-export async function POST({ cookies }) {9+export const POST: RequestHandler = async ({ cookies }) => {
9 const token = cookies.get('session');10 const token = cookies.get('session');
10 11
11 if (token) {12 if (token) {
@@ -20,4 +21,4 @@ export async function POST({ cookies }) {
20 }21 }
21 22
22 return json({ success: true });23 return json({ success: true });
23-}24+};
M src/routes/api/session/validate/+server.ts +3 -2
@@ -1,10 +1,11 @@
11 import { json } from '@sveltejs/kit';
2+import type { RequestHandler } from './$types';
23 import { validateSessionToken } from '$lib/api/session.server';
34 import { createLogger } from '$lib/utils/logger.server';
45
56 const log = createLogger('validate-server');
67
7-export async function GET({ cookies }) {
8+export const GET: RequestHandler = async ({ cookies }) => {
89 const token = cookies.get('session');
910
1011 if (!token) {
@@ -24,4 +25,4 @@ export async function GET({ cookies }) {
2425 }
2526
2627 return json(result);
27-}
28+};
@@ -1,10 +1,11 @@
1 import { json } from '@sveltejs/kit';1 import { json } from '@sveltejs/kit';
2+import type { RequestHandler } from './$types';
2 import { validateSessionToken } from '$lib/api/session.server';3 import { validateSessionToken } from '$lib/api/session.server';
3 import { createLogger } from '$lib/utils/logger.server';4 import { createLogger } from '$lib/utils/logger.server';
4 5
5 const log = createLogger('validate-server');6 const log = createLogger('validate-server');
6 7
7-export async function GET({ cookies }) {8+export const GET: RequestHandler = async ({ cookies }) => {
8 const token = cookies.get('session');9 const token = cookies.get('session');
9 10
10 if (!token) {11 if (!token) {
@@ -24,4 +25,4 @@ export async function GET({ cookies }) {
24 }25 }
25 26
26 return json(result);27 return json(result);
27-}28+};

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