From 75bf3e0dcd793d5453d4c6f5ed736d4bc1e174e9 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sat, 4 Apr 2026 12:35:18 +0200 Subject: [PATCH] feat(14-03): convert auth/oauth/csv services to async, await seedDefaults - auth.service.ts: 10 functions async, removed .all()/.get()/.run() - oauth.service.ts: 7 functions async, boolean conversion (used: true/false) - csv.service.ts: export/import functions async, removed .all()/.get()/.run() - server index.ts: seedDefaults() now awaited for async DB - PGlite smoke test confirms async services work end-to-end --- src/server/index.ts | 2 +- src/server/services/auth.service.ts | 70 ++++++++++---------- src/server/services/csv.service.ts | 45 ++++++------- src/server/services/oauth.service.ts | 97 +++++++++++++--------------- 4 files changed, 102 insertions(+), 112 deletions(-) diff --git a/src/server/index.ts b/src/server/index.ts index c601229..305dba5 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -16,7 +16,7 @@ import { threadRoutes } from "./routes/threads.ts"; import { totalRoutes } from "./routes/totals.ts"; // Seed default data on startup -seedDefaults(); +await seedDefaults(); const app = new Hono(); diff --git a/src/server/services/auth.service.ts b/src/server/services/auth.service.ts index 1ed003e..944877d 100644 --- a/src/server/services/auth.service.ts +++ b/src/server/services/auth.service.ts @@ -13,7 +13,11 @@ export async function createUser( password: string, ) { const passwordHash = await Bun.password.hash(password); - return db.insert(users).values({ username, passwordHash }).returning().get(); + const [row] = await db + .insert(users) + .values({ username, passwordHash }) + .returning(); + return row; } export async function verifyPassword( @@ -21,11 +25,10 @@ export async function verifyPassword( username: string, password: string, ) { - const user = db + const [user] = await db .select() .from(users) - .where(eq(users.username, username)) - .get(); + .where(eq(users.username, username)); if (!user) return null; @@ -33,8 +36,8 @@ export async function verifyPassword( return valid ? user : null; } -export function getUserCount(db: Db = prodDb): number { - const result = db.select({ value: count() }).from(users).get(); +export async function getUserCount(db: Db = prodDb): Promise { + const [result] = await db.select({ value: count() }).from(users); return result?.value ?? 0; } @@ -48,17 +51,17 @@ export async function changePassword( if (!user) return false; const newHash = await Bun.password.hash(newPassword); - db.update(users) + await db + .update(users) .set({ passwordHash: newHash }) - .where(eq(users.id, user.id)) - .run(); + .where(eq(users.id, user.id)); return true; } // ── Session Management ─────────────────────────────────────────────── -export function createSession( +export async function createSession( db: Db = prodDb, userId: number, expiryDays = 30, @@ -66,44 +69,44 @@ export function createSession( const id = randomBytes(32).toString("hex"); const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1000); - return db + const [row] = await db .insert(sessions) .values({ id, userId, expiresAt }) - .returning() - .get(); + .returning(); + + return row; } -export function getSession(db: Db = prodDb, sessionId: string) { - const session = db +export async function getSession(db: Db = prodDb, sessionId: string) { + const [session] = await db .select() .from(sessions) - .where(eq(sessions.id, sessionId)) - .get(); + .where(eq(sessions.id, sessionId)); if (!session) return null; if (session.expiresAt < new Date()) { - db.delete(sessions).where(eq(sessions.id, sessionId)).run(); + await db.delete(sessions).where(eq(sessions.id, sessionId)); return null; } return session; } -export function deleteSession(db: Db = prodDb, sessionId: string) { - db.delete(sessions).where(eq(sessions.id, sessionId)).run(); +export async function deleteSession(db: Db = prodDb, sessionId: string) { + await db.delete(sessions).where(eq(sessions.id, sessionId)); } -export function refreshSession( +export async function refreshSession( db: Db = prodDb, sessionId: string, expiryDays = 30, ) { const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1000); - db.update(sessions) + await db + .update(sessions) .set({ expiresAt }) - .where(eq(sessions.id, sessionId)) - .run(); + .where(eq(sessions.id, sessionId)); } // ── API Key Management ─────────────────────────────────────────────── @@ -113,11 +116,10 @@ export async function createApiKey(db: Db = prodDb, name: string) { const keyHash = await Bun.password.hash(rawKey); const keyPrefix = rawKey.slice(0, 8); - const record = db + const [record] = await db .insert(apiKeys) .values({ name, keyHash, keyPrefix }) - .returning() - .get(); + .returning(); return { ...record, rawKey }; } @@ -127,11 +129,10 @@ export async function verifyApiKey( rawKey: string, ): Promise { const prefix = rawKey.slice(0, 8); - const candidates = db + const candidates = await db .select() .from(apiKeys) - .where(eq(apiKeys.keyPrefix, prefix)) - .all(); + .where(eq(apiKeys.keyPrefix, prefix)); for (const candidate of candidates) { const valid = await Bun.password.verify(rawKey, candidate.keyHash); @@ -141,7 +142,7 @@ export async function verifyApiKey( return false; } -export function listApiKeys(db: Db = prodDb) { +export async function listApiKeys(db: Db = prodDb) { return db .select({ id: apiKeys.id, @@ -149,10 +150,9 @@ export function listApiKeys(db: Db = prodDb) { keyPrefix: apiKeys.keyPrefix, createdAt: apiKeys.createdAt, }) - .from(apiKeys) - .all(); + .from(apiKeys); } -export function deleteApiKey(db: Db = prodDb, id: number) { - db.delete(apiKeys).where(eq(apiKeys.id, id)).run(); +export async function deleteApiKey(db: Db = prodDb, id: number) { + await db.delete(apiKeys).where(eq(apiKeys.id, id)); } diff --git a/src/server/services/csv.service.ts b/src/server/services/csv.service.ts index 78c4b4a..5469c87 100644 --- a/src/server/services/csv.service.ts +++ b/src/server/services/csv.service.ts @@ -84,8 +84,8 @@ function parseCsv(content: string): { headers: string[]; rows: string[][] } { // ─── Export ─────────────────────────────────────────────────────────────────── -export function exportItemsCsv(db: Db = prodDb): string { - const rows = db +export async function exportItemsCsv(db: Db = prodDb): Promise { + const rows = await db .select({ name: items.name, quantity: items.quantity, @@ -96,8 +96,7 @@ export function exportItemsCsv(db: Db = prodDb): string { productUrl: items.productUrl, }) .from(items) - .innerJoin(categories, eq(items.categoryId, categories.id)) - .all(); + .innerJoin(categories, eq(items.categoryId, categories.id)); const header = "name,quantity,weightGrams,priceCents,category,notes,productUrl"; @@ -124,10 +123,10 @@ export interface ImportResult { errors: string[]; } -export function importItemsCsv( +export async function importItemsCsv( db: Db = prodDb, csvContent: string, -): ImportResult { +): Promise { const { headers, rows } = parseCsv(csvContent); const result: ImportResult = { @@ -152,10 +151,9 @@ export function importItemsCsv( // Build a local category cache (name → id) seeded from the DB const categoryCache = new Map(); - const existingCategories = db + const existingCategories = await db .select({ id: categories.id, name: categories.name }) - .from(categories) - .all(); + .from(categories); for (const cat of existingCategories) { categoryCache.set(cat.name.toLowerCase(), cat.id); } @@ -181,11 +179,10 @@ export function importItemsCsv( categoryId = categoryCache.get(cacheKey)!; } else { // Create a new category - const inserted = db + const [inserted] = await db .insert(categories) .values({ name: categoryName, icon: "package" }) - .returning() - .get(); + .returning(); categoryId = inserted.id; categoryCache.set(cacheKey, categoryId); result.createdCategories.push(categoryName); @@ -222,19 +219,17 @@ export function importItemsCsv( const notes = notesIdx >= 0 ? row[notesIdx]?.trim() || null : null; const productUrl = urlIdx >= 0 ? row[urlIdx]?.trim() || null : null; - db.insert(items) - .values({ - name, - quantity, - weightGrams, - priceCents, - categoryId, - notes, - productUrl, - imageFilename: null, - imageSourceUrl: null, - }) - .run(); + await db.insert(items).values({ + name, + quantity, + weightGrams, + priceCents, + categoryId, + notes, + productUrl, + imageFilename: null, + imageSourceUrl: null, + }); result.imported++; } catch (err) { diff --git a/src/server/services/oauth.service.ts b/src/server/services/oauth.service.ts index 10037e5..6aaa599 100644 --- a/src/server/services/oauth.service.ts +++ b/src/server/services/oauth.service.ts @@ -7,53 +7,50 @@ type Db = typeof prodDb; // ── Client Registration ────────────────────────────────────────────── -export function registerClient( +export async function registerClient( db: Db = prodDb, clientName: string, redirectUris: string[], -): { clientId: string } { +): Promise<{ clientId: string }> { const clientId = randomUUID(); const redirectUrisJson = JSON.stringify(redirectUris); - db.insert(oauthClients) - .values({ clientId, clientName, redirectUris: redirectUrisJson }) - .run(); + await db + .insert(oauthClients) + .values({ clientId, clientName, redirectUris: redirectUrisJson }); return { clientId }; } -export function getClient(db: Db = prodDb, clientId: string) { - return ( - db - .select() - .from(oauthClients) - .where(eq(oauthClients.clientId, clientId)) - .get() ?? null - ); +export async function getClient(db: Db = prodDb, clientId: string) { + const [row] = await db + .select() + .from(oauthClients) + .where(eq(oauthClients.clientId, clientId)); + + return row ?? null; } // ── Authorization Code ─────────────────────────────────────────────── -export function createAuthorizationCode( +export async function createAuthorizationCode( db: Db = prodDb, clientId: string, codeChallenge: string, codeChallengeMethod: string, redirectUri: string, -): { code: string } { +): Promise<{ code: string }> { const code = randomBytes(32).toString("hex"); const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes - db.insert(oauthCodes) - .values({ - code, - clientId, - codeChallenge, - codeChallengeMethod, - redirectUri, - expiresAt, - }) - .run(); + await db.insert(oauthCodes).values({ + code, + clientId, + codeChallenge, + codeChallengeMethod, + redirectUri, + expiresAt, + }); return { code }; } @@ -69,14 +66,13 @@ export async function exchangeCode( refreshToken: string; expiresIn: number; } | null> { - const record = db + const [record] = await db .select() .from(oauthCodes) - .where(eq(oauthCodes.code, code)) - .get(); + .where(eq(oauthCodes.code, code)); if (!record) return null; - if (record.used !== 0) return null; + if (record.used !== false) return null; if (record.clientId !== clientId) return null; if (record.redirectUri !== redirectUri) return null; if (record.expiresAt < new Date()) return null; @@ -89,17 +85,20 @@ export async function exchangeCode( if (computedChallenge !== record.codeChallenge) return null; // Mark code as used - db.update(oauthCodes).set({ used: 1 }).where(eq(oauthCodes.code, code)).run(); + await db + .update(oauthCodes) + .set({ used: true }) + .where(eq(oauthCodes.code, code)); return generateTokens(db, clientId); } // ── Token Management ───────────────────────────────────────────────── -function generateTokens( +async function generateTokens( db: Db, clientId: string, -): { accessToken: string; refreshToken: string; expiresIn: number } { +): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> { const accessToken = randomBytes(32).toString("hex"); const refreshToken = randomBytes(32).toString("hex"); @@ -113,15 +112,13 @@ function generateTokens( const expiresAt = new Date(Date.now() + 3600 * 1000); // 1 hour const refreshExpiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days - db.insert(oauthTokens) - .values({ - accessTokenHash, - refreshTokenHash, - clientId, - expiresAt, - refreshExpiresAt, - }) - .run(); + await db.insert(oauthTokens).values({ + accessTokenHash, + refreshTokenHash, + clientId, + expiresAt, + refreshExpiresAt, + }); return { accessToken, refreshToken, expiresIn: 3600 }; } @@ -132,11 +129,10 @@ export async function verifyAccessToken( ): Promise { const tokenHash = createHash("sha256").update(token).digest("hex"); - const record = db + const [record] = await db .select() .from(oauthTokens) - .where(eq(oauthTokens.accessTokenHash, tokenHash)) - .get(); + .where(eq(oauthTokens.accessTokenHash, tokenHash)); if (!record) return false; if (record.expiresAt < new Date()) return false; @@ -155,7 +151,7 @@ export async function refreshAccessToken( } | null> { const tokenHash = createHash("sha256").update(refreshToken).digest("hex"); - const record = db + const [record] = await db .select() .from(oauthTokens) .where( @@ -163,22 +159,21 @@ export async function refreshAccessToken( eq(oauthTokens.refreshTokenHash, tokenHash), eq(oauthTokens.clientId, clientId), ), - ) - .get(); + ); if (!record) return null; if (record.refreshExpiresAt < new Date()) return null; // Delete old token pair - db.delete(oauthTokens).where(eq(oauthTokens.id, record.id)).run(); + await db.delete(oauthTokens).where(eq(oauthTokens.id, record.id)); return generateTokens(db, clientId); } // ── Cleanup ────────────────────────────────────────────────────────── -export function cleanExpiredOAuthData(db: Db = prodDb): void { +export async function cleanExpiredOAuthData(db: Db = prodDb): Promise { const now = new Date(); - db.delete(oauthCodes).where(lt(oauthCodes.expiresAt, now)).run(); - db.delete(oauthTokens).where(lt(oauthTokens.expiresAt, now)).run(); + await db.delete(oauthCodes).where(lt(oauthCodes.expiresAt, now)); + await db.delete(oauthTokens).where(lt(oauthTokens.expiresAt, now)); }