"use client"; import { cn } from "@/lib/utils"; import { signIn, verifyDevice2FA, resend2FACode } from "@/lib/actions/auth"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState, useEffect, useRef, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Ban } from "lucide-react"; function getDeviceHash(): string { const key = "caspos-device-id"; let id = localStorage.getItem(key); if (!id) { id = crypto.randomUUID(); localStorage.setItem(key, id); } return id; } export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<"div">) { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const [messageParam, setMessageParam] = useState(null); const [errorParam, setErrorParam] = useState(null); const router = useRouter(); // 2FA state const [show2FA, setShow2FA] = useState(false); const [otpDigits, setOtpDigits] = useState(["", "", "", "", "", ""]); const [userId, setUserId] = useState(null); const [userRole, setUserRole] = useState(null); const [resendCooldown, setResendCooldown] = useState(0); const inputRefs = useRef<(HTMLInputElement | null)[]>([]); useEffect(() => { const params = new URLSearchParams(window.location.search); setMessageParam(params.get('message')); setErrorParam(params.get('error')); }, []); // Resend cooldown timer useEffect(() => { if (resendCooldown <= 0) return; const timer = setTimeout(() => setResendCooldown(c => c - 1), 1000); return () => clearTimeout(timer); }, [resendCooldown]); const navigateAfterLogin = useCallback((role: string) => { const nextParam = typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get('next') : null; if (nextParam) { router.push(nextParam); } else { router.push("/"); } }, [router]); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); setError(null); try { const deviceHash = getDeviceHash(); const res = await signIn(email, password, deviceHash); if (!res.success) { setError(res.error || "Ein Fehler ist aufgetreten."); return; } if (res.requires2FA) { setUserId(res.userId || null); setUserRole(res.role || null); setShow2FA(true); setResendCooldown(60); setOtpDigits(["", "", "", "", "", ""]); setTimeout(() => inputRefs.current[0]?.focus(), 300); return; } navigateAfterLogin(res.role || "partner"); } catch (error: unknown) { setError(error instanceof Error ? error.message : "Ein Fehler ist aufgetreten."); } finally { setIsLoading(false); } }; const handleOtpChange = (index: number, value: string) => { if (!/^\d*$/.test(value)) return; const newDigits = [...otpDigits]; newDigits[index] = value.slice(-1); setOtpDigits(newDigits); // Auto-advance if (value && index < 5) { inputRefs.current[index + 1]?.focus(); } // Auto-submit when all filled if (newDigits.every(d => d !== "") && newDigits.join("").length === 6) { handleVerify2FA(newDigits.join("")); } }; const handleOtpKeyDown = (index: number, e: React.KeyboardEvent) => { if (e.key === "Backspace" && !otpDigits[index] && index > 0) { inputRefs.current[index - 1]?.focus(); } }; const handleOtpPaste = (e: React.ClipboardEvent) => { e.preventDefault(); const paste = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, 6); if (paste.length === 6) { const newDigits = paste.split(""); setOtpDigits(newDigits); inputRefs.current[5]?.focus(); handleVerify2FA(paste); } }; const isVerifyingRef = useRef(false); const handleVerify2FA = async (code: string) => { if (!userId || isVerifyingRef.current) return; isVerifyingRef.current = true; setIsLoading(true); setError(null); try { const deviceHash = getDeviceHash(); const res = await verifyDevice2FA(userId, code, deviceHash); if (!res.success) { setError(res.error || "Ungültiger Code."); setOtpDigits(["", "", "", "", "", ""]); inputRefs.current[0]?.focus(); return; } navigateAfterLogin(res.role || userRole || "partner"); } catch (err: unknown) { setError(err instanceof Error ? err.message : "Fehler bei der Verifizierung."); setOtpDigits(["", "", "", "", "", ""]); inputRefs.current[0]?.focus(); } finally { setIsLoading(false); isVerifyingRef.current = false; } }; const handleResend = async () => { if (!userId || resendCooldown > 0) return; setError(null); const deviceHash = getDeviceHash(); const res = await resend2FACode(userId, deviceHash); if (res.success) { setResendCooldown(60); } else { setError(res.error || "Fehler beim erneuten Senden."); } }; return (
{!show2FA ? ( Anmelden Geben Sie Ihre E-Mail-Adresse und Ihr Passwort ein, um sich anzumelden.
setEmail(e.target.value)} />
Passwort vergessen?
setPassword(e.target.value)} />
{errorParam === "gesperrt" && (

Ihr Konto wurde gesperrt. Bitte wenden Sie sich an den Administrator.

)} {messageParam === "concurrent" && (

Sie wurden abgemeldet, da Sie sich an einem anderen Gerät angemeldet haben.

)} {error &&

{error}

}
) : (
Sicherheitscode Wir haben einen 6-stelligen Code an
{email}
gesendet.
{/* OTP Input */}
{otpDigits.map((digit, i) => ( { inputRefs.current[i] = el; }} type="text" inputMode="numeric" maxLength={1} value={digit} onChange={(e) => handleOtpChange(i, e.target.value)} onKeyDown={(e) => handleOtpKeyDown(i, e)} className={cn( "h-14 w-11 rounded-xl border-2 bg-muted/50 text-center text-xl font-bold transition-all duration-200", "focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 focus:outline-none", "hover:border-muted-foreground/40", digit ? "border-blue-400 bg-blue-50 dark:bg-blue-950/30" : "border-muted-foreground/20" )} id={`otp-${i}`} autoComplete="one-time-code" /> ))}
{error && ( {error} )}
Code nicht erhalten?{" "}
)}
); }