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). */}
{/* 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();
}
}}
/>
>
);
}