Catalyst / admin/Synapse-Sonar 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Synapse-Sonar

public

Attack Surface Simulation

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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 });
}