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 ? ( +

Загрузка профиля...

+ ) : ( +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ 2FA: {totpStatus.enabled ? "Включена" : "Выключена"} +
+
Режим: {String(totpStatus.mode || "-")}
+
+
+ + {totpStatus.enabled ? ( + <> + + + + ) : null} +
+
+ +
+ + +
+ + + )} +
+ + ); + } + 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 ? (
- - {totpStatus.enabled ? ( - <> - - - - ) : null}
) : null}
@@ -3627,6 +3899,21 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__"; onCopyUri={copyTotpUri} /> + + {!token || !role ? : null}