SPB Git
3.0 KB · 52 lines typescript
1import { generateSecret, generateURI, verifySync } from "otplib";2const totpOk = (token: string, secret: string) => { try { const r: any = verifySync({ secret, token, epochTolerance: 30 }); return typeof r === "boolean" ? r : Boolean(r?.valid); } catch { return false; } };3import { eq } from "drizzle-orm";4import { db, schema } from "@/db";5import { getSession, markMfaPassed, clientIp } from "@/lib/auth/session";6import { assertSameOrigin, jsonError, HttpError } from "@/lib/auth/guard";7import { rateLimit } from "@/lib/auth/ratelimit";8import { audit } from "@/lib/audit";9import { headers } from "next/headers";10import QRCode from "qrcode";1112/** POST { code } → verify TOTP for the pending session.  PUT { action: "setup" | "enable" | "disable", code? } → manage 2FA. */13export async function POST(req: Request) {14  try {15    await assertSameOrigin();16    const s = await getSession(); if (!s) throw new HttpError(401, "Unauthenticated");17    const ip = clientIp(await headers());18    if (!rateLimit(`totp:${ip}`, 8, 10 * 60_000)) throw new HttpError(429, "Too many attempts");19    const { code } = await req.json();20    const [u] = await db.select().from(schema.users).where(eq(schema.users.id, s.id));21    if (!u.totpSecret || !totpOk(String(code ?? ""), u.totpSecret)) { await audit(s.id, "auth.totp_failed", undefined, {}, ip); throw new HttpError(401, "Invalid code"); }22    await markMfaPassed(s.sessionId); await audit(s.id, "auth.totp_ok");23    return Response.json({ ok: true });24  } catch (e) { return jsonError(e); }25}26export async function PUT(req: Request) {27  try {28    await assertSameOrigin();29    const s = await getSession(); if (!s) throw new HttpError(401, "Unauthenticated");30    if (s.totpEnabled && !s.mfaPassed) throw new HttpError(403, "MFA required");31    const { action, code } = await req.json();32    if (action === "setup") {33      const secret = generateSecret();34      await db.update(schema.users).set({ totpSecret: secret, totpEnabled: false }).where(eq(schema.users.id, s.id));35      const uri = generateURI({ issuer: "Note SPBoucher AI", label: s.username, secret });36      return Response.json({ secret, qr: await QRCode.toDataURL(uri) });37    }38    const [u] = await db.select().from(schema.users).where(eq(schema.users.id, s.id));39    if (action === "enable") {40      if (!u.totpSecret || !totpOk(String(code ?? ""), u.totpSecret)) throw new HttpError(400, "Invalid code");41      await db.update(schema.users).set({ totpEnabled: true }).where(eq(schema.users.id, s.id)); await markMfaPassed(s.sessionId); await audit(s.id, "auth.totp_enabled");42      return Response.json({ ok: true });43    }44    if (action === "disable") {45      if (!u.totpSecret || !totpOk(String(code ?? ""), u.totpSecret)) throw new HttpError(400, "Invalid code");46      await db.update(schema.users).set({ totpEnabled: false, totpSecret: null }).where(eq(schema.users.id, s.id)); await audit(s.id, "auth.totp_disabled");47      return Response.json({ ok: true });48    }49    throw new HttpError(400, "Unknown action");50  } catch (e) { return jsonError(e); }51}52
Files · main
Menu

Theme