refactor(modals): migrate 8 ad-hoc modals to the shared FormModal
BulkAttendanceModal, ShiftFormModal, OrderConfirmationModal, DashQuickActions trip modal, the Attendance leave modal, and DashProfile's 3 modals (edit profile, 2FA setup, 2FA disable) now use the shared FormModal instead of hand-rolled overlays, inheriting the canonical open/close animation, blurred backdrop, ESC-to-close, click-outside, scroll-lock, staggered reveal, mobile full-screen and reduced-motion. Footers stay pinned via the real FormModal footer + submitDisabled; multi-step modals (2FA setup, order confirmation) drive title/footer/close-guards per step. Behaviour preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { motion } from "framer-motion";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
import { useAlert } from "../../context/AlertContext";
|
||||
import useModalLock from "../../hooks/useModalLock";
|
||||
import apiFetch from "../../utils/api";
|
||||
import FormModal from "../FormModal";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
@@ -71,8 +71,6 @@ export default function DashProfile({
|
||||
last_name: "",
|
||||
});
|
||||
|
||||
useModalLock(showModal);
|
||||
|
||||
const openEditModal = () => {
|
||||
const nameParts = (user?.fullName || "").split(" ");
|
||||
setFormData({
|
||||
@@ -261,482 +259,367 @@ export default function DashProfile({
|
||||
</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);
|
||||
<FormModal
|
||||
isOpen={showModal}
|
||||
onClose={() => setShowModal(false)}
|
||||
title="Upravit profil"
|
||||
size="md"
|
||||
onSubmit={handleSubmit}
|
||||
submitLabel="Uložit změny"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
</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>
|
||||
</FormModal>
|
||||
|
||||
{/* 2FA Disable Modal */}
|
||||
<AnimatePresence>
|
||||
{show2FADisable && (
|
||||
<motion.div
|
||||
className="admin-modal-overlay"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{/* 2FA Setup Modal — multi-step: setup → backup codes */}
|
||||
<FormModal
|
||||
isOpen={show2FASetup}
|
||||
onClose={() => {
|
||||
// Only reachable on the setup step (backdrop/ESC/× are locked while
|
||||
// backupCodes is set — that branch is unreachable so removed).
|
||||
setShow2FASetup(false);
|
||||
setTotpCode("");
|
||||
}}
|
||||
title={backupCodes ? "Záložní kódy" : "Nastavení 2FA"}
|
||||
size="md"
|
||||
loading={backupCodes ? false : totpSubmitting}
|
||||
onSubmit={
|
||||
backupCodes
|
||||
? () => {
|
||||
setShow2FASetup(false);
|
||||
setBackupCodes(null);
|
||||
}
|
||||
: onConfirm2FA
|
||||
}
|
||||
submitLabel={
|
||||
backupCodes ? "Rozumím, uložil jsem si kódy" : "Aktivovat 2FA"
|
||||
}
|
||||
submitDisabled={backupCodes ? false : totpCode.length !== 6}
|
||||
cancelLabel="Zrušit"
|
||||
// On the backup-codes step the cancel/× would silently dismiss unsaved
|
||||
// codes, so hide the footer + × and lock backdrop/ESC; the explicit
|
||||
// in-body primary is the single deliberate exit.
|
||||
hideFooter={!!backupCodes}
|
||||
hideCloseButton={!!backupCodes}
|
||||
closeOnBackdrop={!backupCodes}
|
||||
closeOnEsc={!backupCodes}
|
||||
>
|
||||
{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
|
||||
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 }}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
gap: "0.5rem",
|
||||
padding: "1rem",
|
||||
background: "var(--bg-secondary)",
|
||||
borderRadius: "0.5rem",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
<div className="admin-modal-header">
|
||||
<h2 className="admin-modal-title">Deaktivovat 2FA</h2>
|
||||
</div>
|
||||
<div className="admin-modal-body">
|
||||
<p
|
||||
{backupCodes.map((code) => (
|
||||
<div
|
||||
key={code}
|
||||
style={{
|
||||
color: "var(--text-secondary)",
|
||||
fontSize: "0.875rem",
|
||||
marginBottom: "1rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
textAlign: "center",
|
||||
color: "var(--text-primary)",
|
||||
}}
|
||||
>
|
||||
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",
|
||||
{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>
|
||||
{/* Footer is hidden on this step; the single deliberate exit lives
|
||||
in the body and also clears the codes. */}
|
||||
<div style={{ marginTop: "1.25rem", textAlign: "right" }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShow2FASetup(false);
|
||||
setBackupCodes(null);
|
||||
}}
|
||||
className="admin-btn admin-btn-primary"
|
||||
>
|
||||
Rozumím, uložil jsem si 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");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && disableCode.length === 6) {
|
||||
onDisable2FA();
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
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-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>
|
||||
)}
|
||||
<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>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</FormModal>
|
||||
|
||||
{/* 2FA Disable Modal */}
|
||||
<FormModal
|
||||
isOpen={show2FADisable}
|
||||
onClose={() => setShow2FADisable(false)}
|
||||
title="Deaktivovat 2FA"
|
||||
size="md"
|
||||
loading={totpSubmitting}
|
||||
onSubmit={onDisable2FA}
|
||||
submitDisabled={disableCode.length !== 6}
|
||||
submitLabel="Deaktivovat 2FA"
|
||||
cancelLabel="Zrušit"
|
||||
>
|
||||
<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>
|
||||
</FormModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user