import { Context } from 'hono'; import { and, eq } from 'drizzle-orm'; import { auth } from './betterAuth.js'; import { db, dbGet } from '../db/index.js'; import { authAccounts } from '../db/auth-schema.js'; // Auth is provided by Better Auth (lib/betterAuth.ts): httpOnly cookie // sessions validated against the auth_sessions table on every request, so // revocation (ban/suspend/password reset) applies instantly. This module keeps // the request-side helpers that the route files use. // Re-exported for routes that hash/validate passwords outside Better Auth export { hashPassword, verifyPassword, validatePassword } from './passwordPolicy.js'; export interface AuthUser { id: string; email: string; name: string; phone: string | null; role: string; languagePreference: string | null; isClaimed: boolean; rucNumber: string | null; accountStatus: string; emailVerified: boolean; image: string | null; createdAt: Date | string; updatedAt: Date | string; /** ID of the Better Auth session backing this request. */ sessionId: string; } /** * Resolve the authenticated user for a request from its Better Auth session * cookie, or null when there is no valid session. Suspended/unclaimed/banned * accounts never get API access even with a live session cookie. */ export async function getAuthUser(c: Context): Promise { try { const session = await auth.api.getSession({ headers: c.req.raw.headers }); if (!session?.user) { return null; } const user = session.user as any; // Suspended (banned) or unclaimed accounts must not retain API access if (user.banned) { return null; } if (user.accountStatus && user.accountStatus !== 'active') { return null; } return { id: user.id, email: user.email, name: user.name, phone: user.phone ?? null, role: user.role ?? 'user', languagePreference: user.languagePreference ?? null, isClaimed: Boolean(user.isClaimed), rucNumber: user.rucNumber ?? null, accountStatus: user.accountStatus ?? 'active', emailVerified: Boolean(user.emailVerified), image: user.image ?? null, createdAt: user.createdAt, updatedAt: user.updatedAt, sessionId: session.session.id, }; } catch { return null; } } export function requireAuth(roles?: string[]) { return async (c: Context, next: () => Promise) => { const user = await getAuthUser(c); if (!user) { return c.json({ error: 'Unauthorized' }, 401); } if (roles && !roles.includes(user.role)) { return c.json({ error: 'Forbidden' }, 403); } c.set('user', user); await next(); }; } /** * Fetch only the credential password hash (never exposed via getAuthUser). * Returns null when the user has no password set (Google-only or unclaimed). */ export async function getUserPasswordHash(userId: string): Promise { const row = await dbGet( (db as any) .select({ password: (authAccounts as any).password }) .from(authAccounts) .where( and( eq((authAccounts as any).userId, userId), eq((authAccounts as any).providerId, 'credential') ) ) ); const hash = row?.password; return hash && String(hash).length > 0 ? String(hash) : null; } /** Whether the user has a linked Google account. */ export async function hasGoogleAccount(userId: string): Promise { const row = await dbGet( (db as any) .select({ id: (authAccounts as any).id }) .from(authAccounts) .where( and( eq((authAccounts as any).userId, userId), eq((authAccounts as any).providerId, 'google') ) ) ); return !!row; }