import { useState, useRef } from "react"; import { motion } from "framer-motion"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import IconButton from "@mui/material/IconButton"; import Dialog from "@mui/material/Dialog"; import DialogTitle from "@mui/material/DialogTitle"; import DialogContent from "@mui/material/DialogContent"; import DialogActions from "@mui/material/DialogActions"; import { useAuth } from "../../context/AuthContext"; import { useAlert } from "../../context/AlertContext"; import apiFetch from "../../utils/api"; import { iconBadgeSx } from "../../theme"; import { Card, Button, Modal, Field, TextField } from "../../ui"; import useDialogScrollLock from "../../ui/useDialogScrollLock"; 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; } const EditIcon = ( ); const CopyIcon = ( ); 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(null); // The 2FA setup dialog is bespoke (multi-step: setup → backup codes) and // locks scroll like the kit dialogs. useDialogScrollLock(show2FASetup); const [showModal, setShowModal] = useState(false); const [formData, setFormData] = useState({ username: "", email: "", new_password: "", current_password: "", first_name: "", last_name: "", }); 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í"; } const profileItems: Array<{ label: string; value: string }> = [ { label: "Uživatel", value: user?.username || "" }, { label: "E-mail", value: user?.email || "" }, { label: "Jméno", value: user?.fullName || "" }, { label: "Role", value: user?.roleDisplay || String(user?.role || ""), }, ]; return ( <> Váš účet {profileItems.map((item) => ( {item.label} {item.value} ))} {/* 2FA Section */} Dvoufaktorové ověření (2FA) {getTotpStatusText()} {!totpLoading && (totpEnabled ? ( ) : ( ))} {/* Edit Profile Modal */} setShowModal(false)} title="Upravit profil" maxWidth="sm" onSubmit={handleSubmit} submitText="Uložit změny" > setFormData({ ...formData, first_name: e.target.value }) } required /> setFormData({ ...formData, last_name: e.target.value }) } required /> setFormData({ ...formData, username: e.target.value }) } required /> setFormData({ ...formData, email: e.target.value }) } required /> setFormData({ ...formData, current_password: e.target.value }) } placeholder="Pro změnu hesla, zadejte aktuální heslo" /> setFormData({ ...formData, new_password: e.target.value }) } placeholder="Pro změnu hesla, zadejte nové heslo" /> {/* 2FA Setup Dialog — bespoke, multi-step: setup → backup codes. On the backup-codes step the footer + close are hidden and backdrop/ESC are locked; the single deliberate exit lives in the body and also clears the codes (so unsaved codes can't be silently lost). */} { setShow2FASetup(false); setTotpCode(""); } } fullWidth maxWidth="sm" disableScrollLock slotProps={{ paper: { sx: { borderRadius: 3 } } }} > {backupCodes ? "Záložní kódy" : "Nastavení 2FA"} {backupCodes ? ( 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. {backupCodes.map((code) => ( {code} ))} ) : ( Naskenujte QR kód v autentizační aplikaci (Google Authenticator, Authy, Microsoft Authenticator apod.) {totpQrUri && ( )} {totpSecret && ( Nebo zadejte klíč ručně: {totpSecret} { navigator.clipboard?.writeText(totpSecret); alert.success("Klíč zkopírován"); }} size="small" title="Kopírovat" aria-label="Kopírovat" sx={{ flexShrink: 0 }} > {CopyIcon} )} setTotpCode(e.target.value.replace(/\D/g, "")) } placeholder="000000" slotProps={{ htmlInput: { maxLength: 6, pattern: "[0-9]*" } }} sx={{ "& input": { textAlign: "center", fontSize: "1.25rem", letterSpacing: "0.4rem", fontFamily: "'DM Mono', Menlo, monospace", }, }} onKeyDown={(e) => { if (e.key === "Enter" && totpCode.length === 6) { onConfirm2FA(); } }} /> )} {/* Footer is hidden on the backup-codes step; the single deliberate exit lives in the body actions there. */} {backupCodes ? ( ) : ( <> )} {/* 2FA Disable Modal */} setShow2FADisable(false)} title="Deaktivovat 2FA" maxWidth="sm" loading={totpSubmitting} onSubmit={onDisable2FA} submitDisabled={disableCode.length !== 6} submitText="Deaktivovat 2FA" cancelText="Zrušit" > Pro deaktivaci dvoufaktorového ověření zadejte aktuální kód z autentizační aplikace. setDisableCode(e.target.value.replace(/\D/g, ""))} placeholder="000000" autoFocus slotProps={{ htmlInput: { maxLength: 6, pattern: "[0-9]*" } }} sx={{ "& input": { textAlign: "center", fontSize: "1.25rem", letterSpacing: "0.4rem", fontFamily: "'DM Mono', Menlo, monospace", }, }} onKeyDown={(e) => { if (e.key === "Enter" && disableCode.length === 6) { onDisable2FA(); } }} /> ); }