admin / Synapse-Sonar
publicAttack Surface Simulation
Synapse-Sonar / synapse-sonar / app / api / mfa / enroll / route.ts
2393 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { authenticator } from "otplib"; import qrcode from "qrcode"; import { requireTenantContext, HttpError } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { encryptJson } from "@/lib/crypto"; export const runtime = "nodejs"; /** * POST /api/mfa/enroll → begin enrollment: returns otpauth URL + QR data-URL. * The secret is stashed (encrypted) but mfaEnabled stays false until verified. */ export async function POST() { try { const ctx = await requireTenantContext(); const secret = authenticator.generateSecret(); const otpauth = authenticator.keyuri(ctx.username, "Synapse Sonar", secret); const qrDataUrl = await qrcode.toDataURL(otpauth); await prisma.user.update({ where: { id: ctx.userId }, data: { mfaSecret: encryptJson({ secret }) as never, mfaEnabled: false }, }); return NextResponse.json({ otpauth, qrDataUrl }); } catch (err) { return errorResponse(err); } } const VerifySchema = z.object({ token: z.string().min(6).max(6) }); /** PATCH /api/mfa/enroll → verify a TOTP code and flip mfaEnabled on. */ export async function PATCH(req: NextRequest) { try { const ctx = await requireTenantContext(); const { token } = VerifySchema.parse(await req.json()); const user = await prisma.user.findUnique({ where: { id: ctx.userId } }); if (!user?.mfaSecret) { return NextResponse.json({ error: "No enrollment in progress" }, { status: 400 }); } const { decryptJson } = await import("@/lib/crypto"); const { secret } = decryptJson<{ secret: string }>(user.mfaSecret as never); if (!authenticator.check(token, secret)) { return NextResponse.json({ error: "Invalid code" }, { status: 422 }); } await prisma.user.update({ where: { id: ctx.userId }, data: { mfaEnabled: true }, }); return NextResponse.json({ ok: true }); } catch (err) { return errorResponse(err); } } function errorResponse(err: unknown) { if (err instanceof HttpError) { return NextResponse.json({ error: err.message }, { status: err.status }); } if (err instanceof z.ZodError) { return NextResponse.json({ error: "Invalid input", details: err.issues }, { status: 422 }); } return NextResponse.json({ error: (err as Error).message }, { status: 500 }); } |