import { useState, useEffect, useRef, useCallback } from "react"; import { Navigate } from "react-router-dom"; import { motion, AnimatePresence } from "framer-motion"; import { useAuth } from "../context/AuthContext"; import { useAlert } from "../context/AlertContext"; import { useTheme } from "../../context/ThemeContext"; import { shouldShowSessionExpiredAlert, shouldShowLogoutAlert, } from "../utils/api"; import FormField from "../components/FormField"; export default function Login() { const { login, verify2FA, isAuthenticated, loading: authLoading } = useAuth(); const alert = useAlert(); const { theme, toggleTheme } = useTheme(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [remember, setRemember] = useState(false); const [loading, setLoading] = useState(false); const [shake, setShake] = useState(false); const [animatingOut, setAnimatingOut] = useState(false); // 2FA state const [show2FA, setShow2FA] = useState(false); const [loginToken, setLoginToken] = useState(null); const [totpCode, setTotpCode] = useState(""); const [useBackupCode, setUseBackupCode] = useState(false); const totpInputRef = useRef(null); useEffect(() => { if (shouldShowSessionExpiredAlert()) { alert.warning("Vaše relace vypršela. Přihlaste se prosím znovu."); } else if (shouldShowLogoutAlert()) { alert.success("Byli jste úspěšně odhlášeni."); } }, [alert]); // Auto-focus TOTP input useEffect(() => { if (show2FA && totpInputRef.current) { totpInputRef.current.focus(); } }, [show2FA, useBackupCode]); if (authLoading) { return (
); } if (isAuthenticated && !animatingOut) { return ; } const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); const result = await login(username, password, remember); if (result.requires2FA) { setLoginToken(result.loginToken ?? null); setShow2FA(true); setTotpCode(""); setLoading(false); } else if (!result.success) { alert.error(result.error ?? "Chyba přihlášení"); setShake(true); setTimeout(() => setShake(false), 500); setLoading(false); } else { alert.success("Úspěšně přihlášeno"); setAnimatingOut(true); setTimeout(() => setAnimatingOut(false), 400); } }, [ username, password, remember, login, alert, setLoading, setShake, setAnimatingOut, setLoginToken, setShow2FA, setTotpCode, ], ); const handle2FASubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); if (!totpCode.trim()) return; setLoading(true); const result = await verify2FA( loginToken!, totpCode.trim(), remember, useBackupCode, ); if (!result.success) { alert.error(result.error ?? "Chyba ověření"); setShake(true); setTimeout(() => setShake(false), 500); setTotpCode(""); if (totpInputRef.current) totpInputRef.current.focus(); setLoading(false); } else { alert.success("Úspěšně přihlášeno"); setAnimatingOut(true); setTimeout(() => setAnimatingOut(false), 400); } }, [ totpCode, loginToken, remember, useBackupCode, verify2FA, alert, setLoading, setShake, setTotpCode, setAnimatingOut, ], ); const handleBack = useCallback(() => { setShow2FA(false); setLoginToken(null); setTotpCode(""); setUseBackupCode(false); }, [setShow2FA, setLoginToken, setTotpCode, setUseBackupCode]); return (
{!show2FA ? (
Logo { (e.target as HTMLImageElement).src = theme === "dark" ? "/images/logo-dark.png" : "/images/logo-light.png"; }} />

Interní systém

Přihlaste se ke svému účtu

setUsername(e.target.value)} required autoComplete="username" className="admin-form-input" placeholder="Zadejte uživatelské jméno" /> setPassword(e.target.value)} required autoComplete="current-password" className="admin-form-input" placeholder="Zadejte heslo" />
) : (

Dvoufaktorové ověření

{useBackupCode ? "Zadejte jeden ze záložních kódů" : "Zadejte 6místný kód z autentizační aplikace"}

{ const val = useBackupCode ? e.target.value : e.target.value.replace(/\D/g, ""); setTotpCode(val); }} required autoComplete="one-time-code" className="admin-form-input" placeholder={useBackupCode ? "XXXXXXXX" : "000000"} style={ useBackupCode ? {} : { textAlign: "center", fontSize: "1.5rem", letterSpacing: "0.5rem", fontFamily: "monospace", } } />
)}
); }