- Change attendance idx_attendance_user_date from unique to index (allow multiple shifts per day) - Reset migrations to single baseline init migration - Add seed script with admin user (admin/admin) - Update CLAUDE.md with migration workflow documentation - Various frontend fixes (queries, pages, hooks) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
743 lines
27 KiB
TypeScript
743 lines
27 KiB
TypeScript
import { useState, useRef } from "react";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import { useAuth } from "../../context/AuthContext";
|
|
import { useAlert } from "../../context/AlertContext";
|
|
import useModalLock from "../../hooks/useModalLock";
|
|
import apiFetch from "../../utils/api";
|
|
|
|
const API_BASE = "/api/admin";
|
|
|
|
interface DashProfileProps {
|
|
totpEnabled: boolean;
|
|
totpLoading: boolean;
|
|
totpSubmitting: boolean;
|
|
onStart2FASetup: () => void;
|
|
onConfirm2FA: () => void;
|
|
onDisable2FA: () => void;
|
|
totpSecret: string | null;
|
|
totpQrUri: string | null;
|
|
totpCode: string;
|
|
setTotpCode: (code: string) => void;
|
|
backupCodes: string[] | null;
|
|
setBackupCodes: (codes: string[] | null) => void;
|
|
show2FASetup: boolean;
|
|
setShow2FASetup: (show: boolean) => void;
|
|
show2FADisable: boolean;
|
|
setShow2FADisable: (show: boolean) => void;
|
|
disableCode: string;
|
|
setDisableCode: (code: string) => void;
|
|
}
|
|
|
|
interface ProfileFormData {
|
|
username: string;
|
|
email: string;
|
|
new_password: string;
|
|
current_password: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
}
|
|
|
|
export default function DashProfile({
|
|
totpEnabled,
|
|
totpLoading,
|
|
totpSubmitting,
|
|
onStart2FASetup,
|
|
onConfirm2FA,
|
|
onDisable2FA,
|
|
totpSecret,
|
|
totpQrUri,
|
|
totpCode,
|
|
setTotpCode,
|
|
backupCodes,
|
|
setBackupCodes,
|
|
show2FASetup,
|
|
setShow2FASetup,
|
|
show2FADisable,
|
|
setShow2FADisable,
|
|
disableCode,
|
|
setDisableCode,
|
|
}: DashProfileProps) {
|
|
const { user, updateUser } = useAuth();
|
|
const alert = useAlert();
|
|
const totpSetupRef = useRef<HTMLInputElement>(null);
|
|
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [formData, setFormData] = useState<ProfileFormData>({
|
|
username: "",
|
|
email: "",
|
|
new_password: "",
|
|
current_password: "",
|
|
first_name: "",
|
|
last_name: "",
|
|
});
|
|
|
|
useModalLock(showModal);
|
|
|
|
const openEditModal = () => {
|
|
const nameParts = (user?.fullName || "").split(" ");
|
|
setFormData({
|
|
username: user?.username || "",
|
|
email: user?.email || "",
|
|
new_password: "",
|
|
current_password: "",
|
|
first_name: nameParts[0] || "",
|
|
last_name: nameParts.slice(1).join(" ") || "",
|
|
});
|
|
setShowModal(true);
|
|
};
|
|
|
|
const handleSubmit = async (e?: React.FormEvent) => {
|
|
e?.preventDefault();
|
|
const dataToSave = { ...formData };
|
|
|
|
if (dataToSave.new_password && !dataToSave.current_password) {
|
|
alert.error("Pro změnu hesla zadejte aktuální heslo");
|
|
return;
|
|
}
|
|
if (dataToSave.current_password && !dataToSave.new_password) {
|
|
alert.error("Pro změnu hesla zadejte nové heslo");
|
|
return;
|
|
}
|
|
|
|
// Strip empty password fields so Zod doesn't reject ""
|
|
if (!dataToSave.current_password)
|
|
delete (dataToSave as any).current_password;
|
|
if (!dataToSave.new_password) delete (dataToSave as any).new_password;
|
|
|
|
try {
|
|
const response = await apiFetch(`${API_BASE}/profile`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(dataToSave),
|
|
});
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
updateUser({
|
|
username: dataToSave.username,
|
|
email: dataToSave.email,
|
|
fullName: `${dataToSave.first_name} ${dataToSave.last_name}`.trim(),
|
|
});
|
|
setShowModal(false);
|
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
alert.success("Profil byl upraven");
|
|
} else {
|
|
alert.error(data.error || "Nepodařilo se uložit profil");
|
|
}
|
|
} catch {
|
|
alert.error("Chyba připojení");
|
|
}
|
|
};
|
|
|
|
function getTotpStatusText(): string {
|
|
if (totpLoading) {
|
|
return "Načítání...";
|
|
}
|
|
return totpEnabled ? "Aktivní" : "Neaktivní";
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<motion.div
|
|
className="admin-card"
|
|
initial={{ opacity: 0, y: 12 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.25, delay: 0.15 }}
|
|
>
|
|
<div className="admin-card-header flex-between">
|
|
<h2 className="admin-card-title">Váš účet</h2>
|
|
<button
|
|
onClick={openEditModal}
|
|
className="admin-btn admin-btn-secondary admin-btn-sm"
|
|
>
|
|
<svg
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
|
</svg>
|
|
Upravit
|
|
</button>
|
|
</div>
|
|
<div className="admin-card-body">
|
|
<div className="dash-profile-grid">
|
|
<div className="dash-profile-item">
|
|
<span className="dash-profile-label">Uživatel</span>
|
|
<span className="dash-profile-value">{user?.username}</span>
|
|
</div>
|
|
<div className="dash-profile-item">
|
|
<span className="dash-profile-label">E-mail</span>
|
|
<span className="dash-profile-value">{user?.email}</span>
|
|
</div>
|
|
<div className="dash-profile-item">
|
|
<span className="dash-profile-label">Jméno</span>
|
|
<span className="dash-profile-value">{user?.fullName}</span>
|
|
</div>
|
|
<div className="dash-profile-item">
|
|
<span className="dash-profile-label">Role</span>
|
|
<span className="dash-profile-value">
|
|
{user?.roleDisplay || String(user?.role || "")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 2FA Section */}
|
|
<div
|
|
style={{
|
|
borderTop: "1px solid var(--border-color)",
|
|
marginTop: "1rem",
|
|
paddingTop: "1rem",
|
|
}}
|
|
>
|
|
<div className="flex-between">
|
|
<div className="flex-row-gap">
|
|
<div
|
|
style={{
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: "50%",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
background: totpEnabled
|
|
? "var(--success-light)"
|
|
: "rgba(var(--text-secondary-rgb, 107, 114, 128), 0.1)",
|
|
color: totpEnabled
|
|
? "var(--success)"
|
|
: "var(--text-secondary)",
|
|
}}
|
|
>
|
|
<svg
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
|
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div style={{ fontWeight: 500, fontSize: "0.875rem" }}>
|
|
Dvoufaktorové ověření (2FA)
|
|
</div>
|
|
<div
|
|
className={totpEnabled ? "text-success" : "text-secondary"}
|
|
style={{ fontSize: "0.75rem" }}
|
|
>
|
|
{getTotpStatusText()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{!totpLoading &&
|
|
(totpEnabled ? (
|
|
<button
|
|
onClick={() => {
|
|
setDisableCode("");
|
|
setShow2FADisable(true);
|
|
}}
|
|
className="admin-btn admin-btn-primary admin-btn-sm"
|
|
>
|
|
Deaktivovat
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={onStart2FASetup}
|
|
disabled={totpSubmitting}
|
|
className="admin-btn admin-btn-primary admin-btn-sm"
|
|
>
|
|
{totpSubmitting ? "Generuji..." : "Aktivovat"}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Edit Profile Modal */}
|
|
<AnimatePresence>
|
|
{showModal && (
|
|
<motion.div
|
|
className="admin-modal-overlay"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<div
|
|
className="admin-modal-backdrop"
|
|
onClick={() => setShowModal(false)}
|
|
/>
|
|
<motion.div
|
|
className="admin-modal"
|
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<div className="admin-modal-header">
|
|
<h2 className="admin-modal-title">Upravit profil</h2>
|
|
</div>
|
|
<div className="admin-modal-body">
|
|
<div className="admin-form">
|
|
<div className="admin-form-row">
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">Jméno</label>
|
|
<input
|
|
type="text"
|
|
value={formData.first_name}
|
|
onChange={(e) =>
|
|
setFormData({
|
|
...formData,
|
|
first_name: e.target.value,
|
|
})
|
|
}
|
|
required
|
|
className="admin-form-input"
|
|
/>
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">Příjmení</label>
|
|
<input
|
|
type="text"
|
|
value={formData.last_name}
|
|
onChange={(e) =>
|
|
setFormData({
|
|
...formData,
|
|
last_name: e.target.value,
|
|
})
|
|
}
|
|
required
|
|
className="admin-form-input"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">
|
|
Uživatelské jméno
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.username}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, username: e.target.value })
|
|
}
|
|
required
|
|
className="admin-form-input"
|
|
/>
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">E-mail</label>
|
|
<input
|
|
type="email"
|
|
value={formData.email}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, email: e.target.value })
|
|
}
|
|
required
|
|
className="admin-form-input"
|
|
/>
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">Aktuální heslo</label>
|
|
<input
|
|
type="password"
|
|
value={formData.current_password}
|
|
onChange={(e) =>
|
|
setFormData({
|
|
...formData,
|
|
current_password: e.target.value,
|
|
})
|
|
}
|
|
className="admin-form-input"
|
|
placeholder="Pro změnu hesla, zadejte aktuální heslo"
|
|
/>
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">
|
|
Nové heslo
|
|
</label>
|
|
<input
|
|
type="password"
|
|
value={formData.new_password}
|
|
onChange={(e) =>
|
|
setFormData({
|
|
...formData,
|
|
new_password: e.target.value,
|
|
})
|
|
}
|
|
className="admin-form-input"
|
|
placeholder="Pro změnu hesla, zadejte nové heslo"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="admin-modal-footer">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowModal(false)}
|
|
className="admin-btn admin-btn-secondary"
|
|
>
|
|
Zrušit
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleSubmit}
|
|
className="admin-btn admin-btn-primary"
|
|
>
|
|
Uložit změny
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* 2FA Setup Modal */}
|
|
<AnimatePresence>
|
|
{show2FASetup && (
|
|
<motion.div
|
|
className="admin-modal-overlay"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<div
|
|
className="admin-modal-backdrop"
|
|
onClick={() => {
|
|
if (!backupCodes) {
|
|
setShow2FASetup(false);
|
|
}
|
|
}}
|
|
/>
|
|
<motion.div
|
|
className="admin-modal"
|
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<div className="admin-modal-header">
|
|
<h2 className="admin-modal-title">
|
|
{backupCodes ? "Záložní kódy" : "Nastavení 2FA"}
|
|
</h2>
|
|
</div>
|
|
<div className="admin-modal-body">
|
|
{backupCodes ? (
|
|
<div>
|
|
<div className="admin-role-locked-notice mb-4">
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
<line x1="12" y1="9" x2="12" y2="13" />
|
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
</svg>
|
|
Uložte si tyto kódy na bezpečné místo. Každý kód lze
|
|
použít pouze jednou. Po zavření tohoto okna je již
|
|
neuvidíte.
|
|
</div>
|
|
<div
|
|
style={{
|
|
display: "grid",
|
|
gridTemplateColumns: "repeat(2, 1fr)",
|
|
gap: "0.5rem",
|
|
padding: "1rem",
|
|
background: "var(--bg-secondary)",
|
|
borderRadius: "0.5rem",
|
|
fontFamily: "monospace",
|
|
fontSize: "1rem",
|
|
}}
|
|
>
|
|
{backupCodes.map((code) => (
|
|
<div
|
|
key={code}
|
|
style={{
|
|
padding: "0.25rem 0.5rem",
|
|
textAlign: "center",
|
|
color: "var(--text-primary)",
|
|
}}
|
|
>
|
|
{code}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div style={{ marginTop: "0.75rem" }}>
|
|
<button
|
|
onClick={() => {
|
|
navigator.clipboard?.writeText(
|
|
backupCodes.join("\n"),
|
|
);
|
|
alert.success("Kódy zkopírovány");
|
|
}}
|
|
className="admin-btn admin-btn-secondary admin-btn-sm"
|
|
>
|
|
<svg
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<rect
|
|
x="9"
|
|
y="9"
|
|
width="13"
|
|
height="13"
|
|
rx="2"
|
|
ry="2"
|
|
/>
|
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
</svg>
|
|
Kopírovat kódy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<p
|
|
className="text-secondary"
|
|
style={{ fontSize: "0.875rem", marginBottom: "1rem" }}
|
|
>
|
|
Naskenujte QR kód v autentizační aplikaci (Google
|
|
Authenticator, Authy, Microsoft Authenticator apod.)
|
|
</p>
|
|
{totpQrUri && (
|
|
<div
|
|
style={{ textAlign: "center", marginBottom: "1rem" }}
|
|
>
|
|
<img
|
|
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(totpQrUri)}`}
|
|
alt="TOTP QR Code"
|
|
style={{
|
|
width: 200,
|
|
height: 200,
|
|
borderRadius: "0.5rem",
|
|
border: "1px solid var(--border-color)",
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
{totpSecret && (
|
|
<div className="mb-4">
|
|
<label
|
|
className="admin-form-label"
|
|
style={{ fontSize: "0.75rem" }}
|
|
>
|
|
Nebo zadejte klíč ručně:
|
|
</label>
|
|
<div
|
|
style={{
|
|
padding: "0.5rem 0.75rem",
|
|
background: "var(--bg-secondary)",
|
|
borderRadius: "0.375rem",
|
|
fontFamily: "monospace",
|
|
fontSize: "0.875rem",
|
|
wordBreak: "break-all",
|
|
color: "var(--text-primary)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
gap: "0.5rem",
|
|
}}
|
|
>
|
|
<span>{totpSecret}</span>
|
|
<button
|
|
onClick={() => {
|
|
navigator.clipboard?.writeText(totpSecret);
|
|
alert.success("Klíč zkopírován");
|
|
}}
|
|
className="admin-btn-icon"
|
|
title="Kopírovat"
|
|
aria-label="Kopírovat"
|
|
style={{ flexShrink: 0 }}
|
|
>
|
|
<svg
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<rect
|
|
x="9"
|
|
y="9"
|
|
width="13"
|
|
height="13"
|
|
rx="2"
|
|
ry="2"
|
|
/>
|
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">
|
|
Ověřovací kód z aplikace
|
|
</label>
|
|
<input
|
|
ref={totpSetupRef}
|
|
type="text"
|
|
inputMode="numeric"
|
|
pattern="[0-9]*"
|
|
maxLength={6}
|
|
value={totpCode}
|
|
onChange={(e) =>
|
|
setTotpCode(e.target.value.replace(/\D/g, ""))
|
|
}
|
|
placeholder="000000"
|
|
className="admin-form-input"
|
|
style={{
|
|
textAlign: "center",
|
|
fontSize: "1.25rem",
|
|
letterSpacing: "0.4rem",
|
|
fontFamily: "monospace",
|
|
}}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter" && totpCode.length === 6) {
|
|
onConfirm2FA();
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="admin-modal-footer">
|
|
{backupCodes ? (
|
|
<button
|
|
onClick={() => {
|
|
setShow2FASetup(false);
|
|
setBackupCodes(null);
|
|
}}
|
|
className="admin-btn admin-btn-primary"
|
|
>
|
|
Rozumím, uložil jsem si kódy
|
|
</button>
|
|
) : (
|
|
<>
|
|
<button
|
|
onClick={() => setShow2FASetup(false)}
|
|
className="admin-btn admin-btn-secondary"
|
|
disabled={totpSubmitting}
|
|
>
|
|
Zrušit
|
|
</button>
|
|
<button
|
|
onClick={onConfirm2FA}
|
|
className="admin-btn admin-btn-primary"
|
|
disabled={totpSubmitting || totpCode.length !== 6}
|
|
>
|
|
{totpSubmitting ? "Ověřuji..." : "Aktivovat 2FA"}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* 2FA Disable Modal */}
|
|
<AnimatePresence>
|
|
{show2FADisable && (
|
|
<motion.div
|
|
className="admin-modal-overlay"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<div
|
|
className="admin-modal-backdrop"
|
|
onClick={() => setShow2FADisable(false)}
|
|
/>
|
|
<motion.div
|
|
className="admin-modal"
|
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<div className="admin-modal-header">
|
|
<h2 className="admin-modal-title">Deaktivovat 2FA</h2>
|
|
</div>
|
|
<div className="admin-modal-body">
|
|
<p
|
|
style={{
|
|
color: "var(--text-secondary)",
|
|
fontSize: "0.875rem",
|
|
marginBottom: "1rem",
|
|
}}
|
|
>
|
|
Pro deaktivaci dvoufaktorového ověření zadejte aktuální kód z
|
|
autentizační aplikace.
|
|
</p>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">Ověřovací kód</label>
|
|
<input
|
|
type="text"
|
|
inputMode="numeric"
|
|
pattern="[0-9]*"
|
|
maxLength={6}
|
|
value={disableCode}
|
|
onChange={(e) =>
|
|
setDisableCode(e.target.value.replace(/\D/g, ""))
|
|
}
|
|
placeholder="000000"
|
|
className="admin-form-input"
|
|
style={{
|
|
textAlign: "center",
|
|
fontSize: "1.25rem",
|
|
letterSpacing: "0.4rem",
|
|
fontFamily: "monospace",
|
|
}}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter" && disableCode.length === 6) {
|
|
onDisable2FA();
|
|
}
|
|
}}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="admin-modal-footer">
|
|
<button
|
|
onClick={() => setShow2FADisable(false)}
|
|
className="admin-btn admin-btn-secondary"
|
|
disabled={totpSubmitting}
|
|
>
|
|
Zrušit
|
|
</button>
|
|
<button
|
|
onClick={onDisable2FA}
|
|
className="admin-btn admin-btn-primary"
|
|
disabled={totpSubmitting || disableCode.length !== 6}
|
|
>
|
|
{totpSubmitting ? "Deaktivuji..." : "Deaktivovat 2FA"}
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</>
|
|
);
|
|
}
|