hellth-hub/apps/admin/app/login/otp/page.tsx

189 lines
6.6 KiB
TypeScript
Raw Normal View History

"use client";
import { authClient } from "@/lib/auth-client";
import { useRouter } from "next/navigation";
import { useRef, useState } from "react";
const OTP_STEP_STORAGE_KEY = "hellthhub_login_step_otp";
function clearOtpStep() {
try {
window.localStorage.removeItem(OTP_STEP_STORAGE_KEY);
} catch {
// ignore storage access issues
}
}
export default function OtpLoginPage() {
const router = useRouter();
const inputRef = useRef<HTMLInputElement>(null);
const [method, setMethod] = useState<"totp" | "backup">("totp");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
setLoading(true);
try {
const code = inputRef.current?.value?.trim() ?? "";
const normalizedCode = method === "totp" ? code.replace(/\D/g, "") : code;
if (method === "totp" && normalizedCode.length !== 6) {
setError("Bitte den 6-stelligen OTP-Code eingeben.");
return;
}
if (method === "backup" && normalizedCode.length < 6) {
setError("Bitte einen gültigen Backup-Code eingeben.");
return;
}
const verifyResult =
method === "totp"
? await authClient.twoFactor.verifyTotp({ code: normalizedCode })
: await authClient.twoFactor.verifyBackupCode({ code: normalizedCode });
if (verifyResult.error) {
const message =
verifyResult.error.message ??
(method === "totp"
? "OTP-Code ungültig. Bitte erneut prüfen."
: "Backup-Code ungültig. Bitte erneut prüfen.");
if (
message.includes("session") ||
message.includes("Session") ||
message.includes("expired")
) {
setError(
"OTP-Sitzung abgelaufen. Bitte erneut mit E-Mail und Passwort anmelden.",
);
return;
}
setError(
method === "totp"
? "OTP-Code ungültig. Bitte erneut prüfen."
: "Backup-Code ungültig. Bitte erneut prüfen.",
);
return;
}
clearOtpStep();
if (inputRef.current) {
inputRef.current.value = "";
}
router.push("/");
} catch {
setError("2FA-Prüfung derzeit nicht möglich. Bitte erneut versuchen.");
} finally {
setLoading(false);
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-[#F9F9F9]">
<div className="w-full max-w-sm">
<div className="flex flex-col items-center mb-8">
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-xl bg-[#0F766E] text-lg font-bold text-white shadow-sm">
HH
</div>
<h1 className="text-xl font-bold text-[#333333]">Hellth Hub</h1>
<p className="text-sm text-gray-500 mt-2">OTP-Verifizierung</p>
</div>
<form
onSubmit={handleSubmit}
className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 space-y-4"
>
<div className="grid grid-cols-2 gap-2 rounded-lg bg-gray-100 p-1">
<button
type="button"
onClick={() => {
setMethod("totp");
setError(null);
if (inputRef.current) inputRef.current.value = "";
}}
className={`rounded-md px-3 py-2 text-sm font-medium transition-colors ${
method === "totp" ? "bg-white text-gray-900 shadow-sm" : "text-gray-600"
}`}
>
Authenticator-Code
</button>
<button
type="button"
onClick={() => {
setMethod("backup");
setError(null);
if (inputRef.current) inputRef.current.value = "";
}}
className={`rounded-md px-3 py-2 text-sm font-medium transition-colors ${
method === "backup" ? "bg-white text-gray-900 shadow-sm" : "text-gray-600"
}`}
>
Backup-Code
</button>
</div>
{error && (
<div className="flex items-center gap-2 text-sm text-red-600 bg-[#ECFDF5] border border-red-100 px-3 py-2.5 rounded-lg">
<span className="w-4 h-4 text-[#0F766E] shrink-0">!</span>
{error}
</div>
)}
<div>
<label className="block text-sm font-medium text-[#333333] mb-1.5">
{method === "totp" ? "OTP-Code (6-stellig)" : "Backup-Code"}
</label>
<input
ref={inputRef}
type="text"
required
autoFocus
suppressHydrationWarning
name={method === "totp" ? "one-time-code" : "backup-code"}
autoComplete={method === "totp" ? "one-time-code" : "off"}
inputMode={method === "totp" ? "numeric" : "text"}
pattern={method === "totp" ? "[0-9]{6}" : undefined}
maxLength={method === "totp" ? 6 : 64}
onInput={(e) => {
const input = e.currentTarget;
if (method === "totp") {
input.value = input.value.replace(/\D/g, "").slice(0, 6);
}
}}
placeholder={method === "totp" ? "123456" : "z. B. ABCD-EFGH"}
className="w-full px-3 py-2.5 border border-gray-200 rounded-lg text-sm text-[#333333] placeholder-gray-300 focus:outline-none focus:ring-2 focus:ring-[#0F766E] focus:border-transparent transition-shadow"
/>
{method === "backup" && (
<p className="mt-1 text-xs text-gray-500">
Jeder Backup-Code ist nur einmal gültig.
</p>
)}
</div>
<button
type="submit"
disabled={loading}
className="w-full py-2.5 bg-[#0F766E] text-white text-sm font-semibold rounded-lg hover:bg-[#115E59] transition-colors disabled:opacity-50 disabled:cursor-not-allowed mt-2"
>
{loading ? "Prüfen..." : method === "totp" ? "OTP prüfen" : "Backup-Code prüfen"}
</button>
<button
type="button"
onClick={() => {
clearOtpStep();
router.replace("/login");
}}
className="w-full py-2.5 bg-white text-[#333333] text-sm font-semibold rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors"
>
Zurück zu Login
</button>
</form>
</div>
</div>
);
}