"use client"; import React, { useState } from "react"; import Link from "next/link"; import { KeyRound, ShieldCheck, AlertCircle, Sliders } from "lucide-react"; import { useTranslations } from "../../../../i18n/provider"; import { useAccount } from "../../../../lib/useAccount"; import { TotpEnrolment, RecoveryCodes } from "../../../../components/TotpEnrolment"; import { Button } from "../../../../components/ui/Button"; /** * Crossing from the webmail into the console. * * The password is not asked for again - the session that got here proved it - * so this collects the one thing the console adds over the webmail: the second * factor. Sending an operator who was already signed in to a full sign-in form * was the app forgetting what it had already been told. * * An account with no factor yet is walked through enrolling one here rather * than being sent off to find the settings screen. */ export default function AdminStepUpPage() { const t = useTranslations(); const account = useAccount("user"); const [code, setCode] = useState(""); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [enrolmentToken, setEnrolmentToken] = useState(null); const [enrolSecret, setEnrolSecret] = useState(null); const [enrolUri, setEnrolUri] = useState(null); const [recoveryCodes, setRecoveryCodes] = useState(null); const submit = async (event: React.FormEvent) => { event.preventDefault(); setError(null); setLoading(true); try { // Confirming a fresh enrolment, using the grant the step-up handed back. if (enrolmentToken) { const res = await fetch("/api/v1/user/mfa/totp/confirm", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${enrolmentToken}` }, body: JSON.stringify({ code }), }); const data = await res.json(); if (!res.ok) throw new Error(data.message || t("auth.verificationFailed")); setRecoveryCodes(data.recovery_codes ?? []); setEnrolmentToken(null); setEnrolSecret(null); setCode(""); return; } const res = await fetch("/api/v1/auth/admin/step-up", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code }), }); const data = await res.json(); // No factor enrolled. The grant that comes back permits enrolling one and // nothing else, so the operator can do it without leaving this screen. if (res.status === 403 && data.error === "MFA_ENROLLMENT_REQUIRED" && data.enrollment_token) { const enroll = await fetch("/api/v1/user/mfa/totp/enroll", { method: "POST", headers: { Authorization: `Bearer ${data.enrollment_token}` }, }); const enrolData = await enroll.json(); if (!enroll.ok) throw new Error(enrolData.message || t("auth.enrolmentFailed")); setEnrolmentToken(data.enrollment_token); setEnrolSecret(enrolData.secret); setEnrolUri(enrolData.uri); setCode(""); return; } // A session with no webmail session behind it cannot step up; the sign-in // is where that starts. if (res.status === 401) { window.location.href = "/user/login?next=/admin/step-up"; return; } if (!res.ok) throw new Error(data.message || t("auth.verificationFailed")); window.location.href = "/admin/dashboard"; } catch (err: unknown) { setError(err instanceof Error ? err.message : t("auth.genericError")); } finally { setLoading(false); } }; return (

{t("auth.stepUpTitle")}

{account?.email ? t("auth.stepUpSignedInAs", { email: account.email }) : t("common.loading")}

{error && (
{error}
)} {recoveryCodes ? ( setRecoveryCodes(null)} continueLabel={t("common.continue")} /> ) : (
{enrolSecret ? (
{t("auth.enrolmentIntro")}
) : (
{t("auth.stepUpIntro")}
)}
setCode(e.target.value.replace(/\s/g, ""))} placeholder="123456" required className="w-full rounded-xl bg-dark-card py-3 pl-[38px] pr-3 text-center font-mono text-lg tracking-[0.3em] text-slate-100 placeholder-slate-500 shadow-edge transition-shadow focus:outline-none focus-visible:shadow-edge-accent" />
{/* A recovery code is the way in when the phone is gone, and nothing on the old screens said so. */}

{t("auth.orRecoveryCode")}

)}
{t("admin.backToWebmail")}
); }