import { HttpError } from "@batch-cooking/error-tools"; import { type AdminLoginInput, type AdminUserView, ErrorCode } from "@batch-cooking/shared"; import argon2 from "argon2"; import { env } from "../../config/env.js"; import { prisma } from "../../db/prisma.js"; import { signAdminToken } from "../../lib/admin-jwt.js"; import { toSafeAdmin } from "../../lib/safe-admin.js"; /** Result of a successful admin login: the safe admin view plus the signed admin session JWT to set as a cookie. */ interface AdminAuthResult { admin: AdminUserView; token: string; } // Same reasoning as `auth.service.ts`'s `hashOptions`: argon2's real // defaults are deliberately expensive; the test suite hashes/verifies // against throwaway data many times per run, so a cheaper cost keeps it // fast without weakening anything real. Never applies outside NODE_ENV=test. const testHashOptions = { memoryCost: 8192, timeCost: 2, parallelism: 1 }; const hashOptions = env.NODE_ENV === "test" ? testHashOptions : undefined; /** Exposed so `src/scripts/create-admin.ts` hashes exactly the same way `login` verifies. */ export function hashAdminPassword(password: string): Promise { return argon2.hash(password, hashOptions); } /** * Verifies an admin operator's credentials, stamps `lastLoginAt`, and * issues a fresh admin session token. * * @throws {HttpError} `401 INVALID_CREDENTIALS` for either an unknown email * or a wrong password — deliberately indistinguishable, same reasoning as * `auth.service.ts`'s `login`. */ export async function adminLogin(input: AdminLoginInput): Promise { try { const admin = await prisma.adminUser.findUnique({ where: { email: input.email } }); if (!admin || !(await argon2.verify(admin.passwordHash, input.password))) { throw new HttpError(401, ErrorCode.INVALID_CREDENTIALS, "Invalid email or password"); } const updated = await prisma.adminUser.update({ where: { id: admin.id }, data: { lastLoginAt: new Date() }, }); const token = signAdminToken({ adminUserId: updated.id, tokenVersion: updated.tokenVersion, }); return { admin: toSafeAdmin(updated), token }; } catch (err) { // Rethrown as-is — `wrapAsyncHandler`/the error middleware handles it, // this service layer just isn't allowed a bare `await` per the repo's // async/try-catch convention. throw err; } }