From 6a1e7cf6029abb36a355cc953721e0cf6833cdee Mon Sep 17 00:00:00 2001
From: TronoSfera <119615520+TronoSfera@users.noreply.github.com>
Date: Mon, 2 Mar 2026 18:37:42 +0300
Subject: [PATCH] add security test 13
---
app/web/admin.css | 18 +++
app/web/admin.jsx | 315 +++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 319 insertions(+), 14 deletions(-)
diff --git a/app/web/admin.css b/app/web/admin.css
index 2fef3f9..819261a 100644
--- a/app/web/admin.css
+++ b/app/web/admin.css
@@ -2773,6 +2773,23 @@
background: #fff;
}
+ .account-modal {
+ width: min(760px, 100%);
+ }
+
+ .account-modal-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.75rem;
+ }
+
+ .account-security-box {
+ border: 1px solid var(--line);
+ border-radius: 12px;
+ padding: 0.75rem;
+ background: rgba(255, 255, 255, 0.03);
+ }
+
.close {
border: 1px solid var(--line);
width: 34px;
@@ -2850,6 +2867,7 @@
width: 200px;
height: 200px;
}
+ .account-modal-grid { grid-template-columns: 1fr; }
.lawyer-dashboard-grid { grid-template-columns: 1fr; }
.lawyer-dashboard-card {
grid-template-columns: 1fr;
diff --git a/app/web/admin.jsx b/app/web/admin.jsx
index 80ab251..98931fa 100644
--- a/app/web/admin.jsx
+++ b/app/web/admin.jsx
@@ -588,6 +588,122 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
);
}
+ function AccountModal({
+ open,
+ status,
+ profileLoading,
+ saveLoading,
+ form,
+ totpStatus,
+ onFieldChange,
+ onClose,
+ onSubmit,
+ onSetupTotp,
+ onRegenerateBackupCodes,
+ onDisableTotp,
+ }) {
+ if (!open) return null;
+ return (
+ event.target.id === "account-overlay" && onClose()}>
+ event.stopPropagation()}>
+
+
+
Личный кабинет
+
+ Профиль и безопасность аккаунта.
+
+
+
+
+ {profileLoading ? (
+
Загрузка профиля...
+ ) : (
+
+ )}
+
+
+ );
+ }
+
function AttachmentPreviewModal({ open, title, url, fileName, mimeType, onClose }) {
const [resolvedUrl, setResolvedUrl] = useState("");
const [resolvedText, setResolvedText] = useState("");
@@ -1036,6 +1152,23 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
code: "",
loading: false,
});
+ const [accountModal, setAccountModal] = useState({
+ open: false,
+ loading: false,
+ saving: false,
+ initial: {
+ name: "",
+ email: "",
+ phone: "",
+ },
+ form: {
+ name: "",
+ email: "",
+ phone: "",
+ password: "",
+ passwordConfirm: "",
+ },
+ });
const [recordModal, setRecordModal] = useState({
open: false,
@@ -2743,6 +2876,146 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
[api, token]
);
+ const openAccountModal = useCallback(async () => {
+ if (!token || !userId) {
+ setStatus("account", "Не удалось открыть профиль: отсутствует идентификатор пользователя", "error");
+ return;
+ }
+ setAccountModal((prev) => ({
+ ...prev,
+ open: true,
+ loading: true,
+ saving: false,
+ }));
+ setStatus("account", "Загрузка профиля...", "");
+ try {
+ const row = await api("/api/admin/crud/admin_users/" + encodeURIComponent(String(userId)));
+ const nextInitial = {
+ name: String(row?.name || ""),
+ email: String(row?.email || email || ""),
+ phone: String(row?.phone || ""),
+ };
+ setAccountModal({
+ open: true,
+ loading: false,
+ saving: false,
+ initial: nextInitial,
+ form: {
+ ...nextInitial,
+ password: "",
+ passwordConfirm: "",
+ },
+ });
+ setStatus("account", "", "");
+ } catch (error) {
+ setAccountModal((prev) => ({ ...prev, loading: false }));
+ setStatus("account", "Ошибка загрузки профиля: " + error.message, "error");
+ }
+ }, [api, email, setStatus, token, userId]);
+
+ const closeAccountModal = useCallback(() => {
+ setAccountModal((prev) => ({
+ ...prev,
+ open: false,
+ loading: false,
+ saving: false,
+ form: {
+ name: prev.initial.name,
+ email: prev.initial.email,
+ phone: prev.initial.phone,
+ password: "",
+ passwordConfirm: "",
+ },
+ }));
+ setStatus("account", "", "");
+ }, [setStatus]);
+
+ const updateAccountField = useCallback((event) => {
+ const fieldName = String(event?.target?.name || "");
+ if (!fieldName) return;
+ setAccountModal((prev) => ({
+ ...prev,
+ form: {
+ ...prev.form,
+ [fieldName]: event.target.value,
+ },
+ }));
+ }, []);
+
+ const submitAccountModal = useCallback(
+ async (event) => {
+ event.preventDefault();
+ if (!token || !userId) return;
+
+ const form = accountModal.form || {};
+ const initial = accountModal.initial || {};
+
+ const nextName = String(form.name || "").trim();
+ const nextEmail = String(form.email || "").trim().toLowerCase();
+ const nextPhone = String(form.phone || "").trim();
+ const nextPassword = String(form.password || "");
+ const nextPasswordConfirm = String(form.passwordConfirm || "");
+
+ if (!nextName) {
+ setStatus("account", "Имя не может быть пустым", "error");
+ return;
+ }
+ if (!nextEmail) {
+ setStatus("account", "Почта не может быть пустой", "error");
+ return;
+ }
+ if (nextPassword && nextPassword.length < 8) {
+ setStatus("account", "Пароль должен быть не менее 8 символов", "error");
+ return;
+ }
+ if (nextPassword !== nextPasswordConfirm) {
+ setStatus("account", "Пароли не совпадают", "error");
+ return;
+ }
+
+ const payload = {};
+ if (nextName !== String(initial.name || "").trim()) payload.name = nextName;
+ if (nextEmail !== String(initial.email || "").trim().toLowerCase()) payload.email = nextEmail;
+ if (nextPhone !== String(initial.phone || "").trim()) payload.phone = nextPhone || null;
+ if (nextPassword) payload.password = nextPassword;
+
+ if (!Object.keys(payload).length) {
+ setStatus("account", "Нет изменений для сохранения", "");
+ return;
+ }
+
+ try {
+ setAccountModal((prev) => ({ ...prev, saving: true }));
+ setStatus("account", "Сохранение...", "");
+ const row = await api("/api/admin/crud/admin_users/" + encodeURIComponent(String(userId)), {
+ method: "PATCH",
+ body: payload,
+ });
+ const nextInitial = {
+ name: String(row?.name || nextName),
+ email: String(row?.email || nextEmail),
+ phone: String(row?.phone || nextPhone),
+ };
+ setAccountModal((prev) => ({
+ ...prev,
+ saving: false,
+ initial: nextInitial,
+ form: {
+ ...nextInitial,
+ password: "",
+ passwordConfirm: "",
+ },
+ }));
+ if (nextInitial.email) setEmail(nextInitial.email);
+ setStatus("account", "Профиль обновлен", "ok");
+ } catch (error) {
+ setAccountModal((prev) => ({ ...prev, saving: false }));
+ setStatus("account", "Ошибка сохранения: " + error.message, "error");
+ }
+ },
+ [accountModal.form, accountModal.initial, api, setStatus, token, userId]
+ );
+
const closeTotpSetupModal = useCallback(() => {
setTotpSetupModal({
open: false,
@@ -2931,6 +3204,13 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
code: "",
loading: false,
});
+ setAccountModal({
+ open: false,
+ loading: false,
+ saving: false,
+ initial: { name: "", email: "", phone: "" },
+ form: { name: "", email: "", phone: "", password: "", passwordConfirm: "" },
+ });
setActiveSection("dashboard");
}, [resetKanbanState, resetRequestWorkspaceState, resetTablesState]);
@@ -3054,7 +3334,8 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
if (!hasCurrent) setConfigActiveKey(dictionaryTableItems[0].key);
}, [configActiveKey, dictionaryTableItems]);
- const anyOverlayOpen = recordModal.open || filterModal.open || reassignModal.open || kanbanSortModal.open || totpSetupModal.open;
+ const anyOverlayOpen =
+ recordModal.open || filterModal.open || reassignModal.open || kanbanSortModal.open || totpSetupModal.open || accountModal.open;
useEffect(() => {
document.body.classList.toggle("modal-open", anyOverlayOpen);
return () => document.body.classList.remove("modal-open");
@@ -3068,10 +3349,11 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
closeKanbanSortModal();
setReassignModal((prev) => ({ ...prev, open: false }));
closeTotpSetupModal();
+ closeAccountModal();
};
document.addEventListener("keydown", onEsc);
return () => document.removeEventListener("keydown", onEsc);
- }, [closeKanbanSortModal, closeTotpSetupModal]);
+ }, [closeAccountModal, closeKanbanSortModal, closeTotpSetupModal]);
const menuItems = useMemo(() => {
return [
@@ -3202,19 +3484,9 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
{token && role ? (
-
) : null}
@@ -3627,6 +3899,21 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
onCopyUri={copyTotpUri}
/>
+
+
{!token || !role ?
: null}
>