import { and, eq, gt, isNull } from 'drizzle-orm' import { createHash, randomBytes } from 'node:crypto' import type { Db } from '../db/client.ts' import { authTokens, users } from '../db/schema.ts' export type TokenPurpose = 'magic_link' | 'password_reset' const hash = (token: string) => createHash('sha256').update(token).digest('hex') export async function createAuthToken(db: Db, userId: string, purpose: TokenPurpose, ttlMs: number): Promise { const token = randomBytes(32).toString('base64url') await db.insert(authTokens).values({ id: hash(token), userId, purpose, expiresAt: new Date(Date.now() + ttlMs) }) return token } /** Marks the token used and returns its (active) user's id, or null if invalid, expired or already used. */ export async function consumeAuthToken(db: Db, token: string, purpose: TokenPurpose): Promise { const [row] = await db .update(authTokens) .set({ usedAt: new Date() }) .where(and(eq(authTokens.id, hash(token)), eq(authTokens.purpose, purpose), isNull(authTokens.usedAt), gt(authTokens.expiresAt, new Date()))) .returning({ userId: authTokens.userId }) if (!row) return null const [user] = await db .select({ id: users.id }) .from(users) .where(and(eq(users.id, row.userId), eq(users.active, true))) return user?.id ?? null }