style: run prettier on entire codebase
This commit is contained in:
@@ -1,44 +1,47 @@
|
||||
import { useState, useEffect, useRef } 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'
|
||||
import { useState, useEffect, useRef } 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)
|
||||
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<string | null>(null)
|
||||
const [totpCode, setTotpCode] = useState('')
|
||||
const [useBackupCode, setUseBackupCode] = useState(false)
|
||||
const totpInputRef = useRef<HTMLInputElement>(null)
|
||||
const [show2FA, setShow2FA] = useState(false);
|
||||
const [loginToken, setLoginToken] = useState<string | null>(null);
|
||||
const [totpCode, setTotpCode] = useState("");
|
||||
const [useBackupCode, setUseBackupCode] = useState(false);
|
||||
const totpInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldShowSessionExpiredAlert()) {
|
||||
alert.warning('Vaše relace vypršela. Přihlaste se prosím znovu.')
|
||||
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.success("Byli jste úspěšně odhlášeni.");
|
||||
}
|
||||
}, [alert])
|
||||
}, [alert]);
|
||||
|
||||
// Auto-focus TOTP input
|
||||
useEffect(() => {
|
||||
if (show2FA && totpInputRef.current) {
|
||||
totpInputRef.current.focus()
|
||||
totpInputRef.current.focus();
|
||||
}
|
||||
}, [show2FA, useBackupCode])
|
||||
}, [show2FA, useBackupCode]);
|
||||
|
||||
if (authLoading) {
|
||||
return (
|
||||
@@ -47,76 +50,83 @@ export default function Login() {
|
||||
<div className="admin-spinner" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (isAuthenticated && !animatingOut) {
|
||||
return <Navigate to="/" replace />
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
const result = await login(username, password, remember)
|
||||
const result = await login(username, password, remember);
|
||||
|
||||
if (result.requires2FA) {
|
||||
setLoginToken(result.loginToken ?? null)
|
||||
setShow2FA(true)
|
||||
setTotpCode('')
|
||||
setLoading(false)
|
||||
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)
|
||||
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)
|
||||
alert.success("Úspěšně přihlášeno");
|
||||
setAnimatingOut(true);
|
||||
setTimeout(() => setAnimatingOut(false), 400);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handle2FASubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!totpCode.trim()) return
|
||||
e.preventDefault();
|
||||
if (!totpCode.trim()) return;
|
||||
|
||||
setLoading(true)
|
||||
setLoading(true);
|
||||
|
||||
const result = await verify2FA(loginToken!, totpCode.trim(), remember, useBackupCode)
|
||||
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)
|
||||
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)
|
||||
alert.success("Úspěšně přihlášeno");
|
||||
setAnimatingOut(true);
|
||||
setTimeout(() => setAnimatingOut(false), 400);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setShow2FA(false)
|
||||
setLoginToken(null)
|
||||
setTotpCode('')
|
||||
setUseBackupCode(false)
|
||||
}
|
||||
setShow2FA(false);
|
||||
setLoginToken(null);
|
||||
setTotpCode("");
|
||||
setUseBackupCode(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="admin-login"
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={animatingOut
|
||||
? { scale: 1.5, opacity: 0, filter: 'blur(12px)' }
|
||||
: { scale: 1, opacity: 1, filter: 'none' }
|
||||
animate={
|
||||
animatingOut
|
||||
? { scale: 1.5, opacity: 0, filter: "blur(12px)" }
|
||||
: { scale: 1, opacity: 1, filter: "none" }
|
||||
}
|
||||
transition={animatingOut
|
||||
? { duration: 0.25, ease: [0.4, 0, 0.2, 1] }
|
||||
: { duration: 0.25, ease: [0.4, 0, 0.2, 1] }
|
||||
transition={
|
||||
animatingOut
|
||||
? { duration: 0.25, ease: [0.4, 0, 0.2, 1] }
|
||||
: { duration: 0.25, ease: [0.4, 0, 0.2, 1] }
|
||||
}
|
||||
>
|
||||
<div className="bg-orb bg-orb-1" />
|
||||
@@ -125,16 +135,34 @@ export default function Login() {
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="admin-login-theme-btn"
|
||||
title={theme === 'dark' ? 'Světlý režim' : 'Tmavý režim'}
|
||||
title={theme === "dark" ? "Světlý režim" : "Tmavý režim"}
|
||||
>
|
||||
<span className={`admin-theme-icon ${theme === 'light' ? 'visible' : ''}`}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<span
|
||||
className={`admin-theme-icon ${theme === "light" ? "visible" : ""}`}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
||||
</svg>
|
||||
</span>
|
||||
<span className={`admin-theme-icon ${theme === 'dark' ? 'visible' : ''}`}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<span
|
||||
className={`admin-theme-icon ${theme === "dark" ? "visible" : ""}`}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
</span>
|
||||
@@ -146,19 +174,25 @@ export default function Login() {
|
||||
key="login"
|
||||
className="admin-login-card"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={shake
|
||||
? { opacity: 1, y: 0, x: [0, -12, 12, -8, 8, -4, 4, 0] }
|
||||
: { opacity: 1, y: 0 }
|
||||
animate={
|
||||
shake
|
||||
? { opacity: 1, y: 0, x: [0, -12, 12, -8, 8, -4, 4, 0] }
|
||||
: { opacity: 1, y: 0 }
|
||||
}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={shake
|
||||
? { x: { duration: 0.5, ease: 'easeOut' } }
|
||||
: { duration: 0.3 }
|
||||
transition={
|
||||
shake
|
||||
? { x: { duration: 0.5, ease: "easeOut" } }
|
||||
: { duration: 0.3 }
|
||||
}
|
||||
>
|
||||
<div className="admin-login-header">
|
||||
<img
|
||||
src={theme === 'dark' ? '/images/logo-dark.png' : '/images/logo-light.png'}
|
||||
src={
|
||||
theme === "dark"
|
||||
? "/images/logo-dark.png"
|
||||
: "/images/logo-light.png"
|
||||
}
|
||||
alt="Logo"
|
||||
className="admin-login-logo"
|
||||
/>
|
||||
@@ -206,15 +240,18 @@ export default function Login() {
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="admin-btn admin-btn-primary"
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="admin-spinner" style={{ width: 20, height: 20, borderWidth: 2 }} />
|
||||
<div
|
||||
className="admin-spinner"
|
||||
style={{ width: 20, height: 20, borderWidth: 2 }}
|
||||
/>
|
||||
Přihlašování...
|
||||
</>
|
||||
) : (
|
||||
'Přihlásit se'
|
||||
"Přihlásit se"
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
@@ -224,19 +261,28 @@ export default function Login() {
|
||||
key="2fa"
|
||||
className="admin-login-card"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={shake
|
||||
? { opacity: 1, y: 0, x: [0, -12, 12, -8, 8, -4, 4, 0] }
|
||||
: { opacity: 1, y: 0 }
|
||||
animate={
|
||||
shake
|
||||
? { opacity: 1, y: 0, x: [0, -12, 12, -8, 8, -4, 4, 0] }
|
||||
: { opacity: 1, y: 0 }
|
||||
}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={shake
|
||||
? { x: { duration: 0.5, ease: 'easeOut' } }
|
||||
: { duration: 0.3 }
|
||||
transition={
|
||||
shake
|
||||
? { x: { duration: 0.5, ease: "easeOut" } }
|
||||
: { duration: 0.3 }
|
||||
}
|
||||
>
|
||||
<div className="admin-login-header">
|
||||
<div className="admin-login-2fa-icon">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
</svg>
|
||||
@@ -244,36 +290,43 @@ export default function Login() {
|
||||
<h1 className="admin-login-title">Dvoufaktorové ověření</h1>
|
||||
<p className="admin-login-subtitle">
|
||||
{useBackupCode
|
||||
? 'Zadejte jeden ze záložních kódů'
|
||||
: 'Zadejte 6místný kód z autentizační aplikace'
|
||||
}
|
||||
? "Zadejte jeden ze záložních kódů"
|
||||
: "Zadejte 6místný kód z autentizační aplikace"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handle2FASubmit} className="admin-form">
|
||||
<FormField label={useBackupCode ? 'Záložní kód' : 'Ověřovací kód'}>
|
||||
<FormField
|
||||
label={useBackupCode ? "Záložní kód" : "Ověřovací kód"}
|
||||
>
|
||||
<input
|
||||
ref={totpInputRef}
|
||||
id="totp-code"
|
||||
type="text"
|
||||
inputMode={useBackupCode ? 'text' : 'numeric'}
|
||||
pattern={useBackupCode ? undefined : '[0-9]*'}
|
||||
inputMode={useBackupCode ? "text" : "numeric"}
|
||||
pattern={useBackupCode ? undefined : "[0-9]*"}
|
||||
maxLength={useBackupCode ? 8 : 6}
|
||||
value={totpCode}
|
||||
onChange={(e) => {
|
||||
const val = useBackupCode ? e.target.value : e.target.value.replace(/\D/g, '')
|
||||
setTotpCode(val)
|
||||
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'
|
||||
}}
|
||||
placeholder={useBackupCode ? "XXXXXXXX" : "000000"}
|
||||
style={
|
||||
useBackupCode
|
||||
? {}
|
||||
: {
|
||||
textAlign: "center",
|
||||
fontSize: "1.5rem",
|
||||
letterSpacing: "0.5rem",
|
||||
fontFamily: "monospace",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
@@ -281,34 +334,54 @@ export default function Login() {
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="admin-btn admin-btn-primary"
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="admin-spinner" style={{ width: 20, height: 20, borderWidth: 2 }} />
|
||||
<div
|
||||
className="admin-spinner"
|
||||
style={{ width: 20, height: 20, borderWidth: 2 }}
|
||||
/>
|
||||
Ověřování...
|
||||
</>
|
||||
) : (
|
||||
'Ověřit'
|
||||
"Ověřit"
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginTop: '0.5rem' }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "0.5rem",
|
||||
marginTop: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
setUseBackupCode(!useBackupCode)
|
||||
setTotpCode('')
|
||||
setUseBackupCode(!useBackupCode);
|
||||
setTotpCode("");
|
||||
}}
|
||||
className="admin-back-link"
|
||||
style={{ border: 'none', background: 'none', cursor: 'pointer' }}
|
||||
style={{
|
||||
border: "none",
|
||||
background: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{useBackupCode ? 'Použít autentizační aplikaci' : 'Použít záložní kód'}
|
||||
{useBackupCode
|
||||
? "Použít autentizační aplikaci"
|
||||
: "Použít záložní kód"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="admin-back-link"
|
||||
style={{ border: 'none', background: 'none', cursor: 'pointer' }}
|
||||
style={{
|
||||
border: "none",
|
||||
background: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
← Zpět na přihlášení
|
||||
</button>
|
||||
@@ -317,5 +390,5 @@ export default function Login() {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user