"use server"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { LaravelEncrypter } from "@/lib/auth/laravel-encrypter"; import { generateTotpSecret, verifyTotp } from "@/lib/auth/totp"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { env } from "@/env"; async function sessionUserId(): Promise { const session = await auth(); if (!session?.user?.id) redirect("/login"); return Number(session.user.id); } /** Step 1: generate a secret, store it encrypted but UNconfirmed. */ export async function beginTwoFactor(): Promise { const id = await sessionUserId(); if (!env.APP_KEY) redirect("/settings/2fa?error=noappkey"); const secret = generateTotpSecret(); const encrypted = new LaravelEncrypter(env.APP_KEY).encrypt(secret); await prisma.user.update({ where: { id }, data: { twoFactorSecret: encrypted, twoFactorConfirmedAt: null }, }); revalidatePath("/settings/2fa"); } /** Step 2: verify a code against the pending secret, then confirm. */ export async function confirmTwoFactor(formData: FormData): Promise { const id = await sessionUserId(); if (!env.APP_KEY) redirect("/settings/2fa?error=noappkey"); const code = String(formData.get("code") ?? "").trim(); const user = await prisma.user.findUnique({ where: { id }, select: { twoFactorSecret: true }, }); let ok = false; if (user?.twoFactorSecret && code) { try { const secret = new LaravelEncrypter(env.APP_KEY).decrypt(user.twoFactorSecret); ok = verifyTotp(code, secret); } catch { ok = false; } } if (!ok) redirect("/settings/2fa?error=badcode"); await prisma.user.update({ where: { id }, data: { twoFactorConfirmedAt: new Date() } }); redirect("/settings/2fa?enabled=1"); } export async function disableTwoFactor(): Promise { const id = await sessionUserId(); await prisma.user.update({ where: { id }, data: { twoFactorSecret: null, twoFactorRecoveryCodes: null, twoFactorConfirmedAt: null, }, }); redirect("/settings/2fa?disabled=1"); }