mirror of
https://github.com/TronoSfera/Law.git
synced 2026-05-18 10:03:45 +03:00
add security test 13
This commit is contained in:
parent
4cb33afd6b
commit
6a1e7cf602
2 changed files with 319 additions and 14 deletions
|
|
@ -2773,6 +2773,23 @@
|
||||||
background: #fff;
|
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 {
|
.close {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
width: 34px;
|
width: 34px;
|
||||||
|
|
@ -2850,6 +2867,7 @@
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
|
.account-modal-grid { grid-template-columns: 1fr; }
|
||||||
.lawyer-dashboard-grid { grid-template-columns: 1fr; }
|
.lawyer-dashboard-grid { grid-template-columns: 1fr; }
|
||||||
.lawyer-dashboard-card {
|
.lawyer-dashboard-card {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<Overlay open={open} id="account-overlay" onClose={(event) => event.target.id === "account-overlay" && onClose()}>
|
||||||
|
<div className="modal account-modal" onClick={(event) => event.stopPropagation()}>
|
||||||
|
<div className="modal-head">
|
||||||
|
<div>
|
||||||
|
<h3>Личный кабинет</h3>
|
||||||
|
<p className="muted" style={{ marginTop: "0.35rem" }}>
|
||||||
|
Профиль и безопасность аккаунта.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button className="close" type="button" onClick={onClose}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{profileLoading ? (
|
||||||
|
<p className="muted">Загрузка профиля...</p>
|
||||||
|
) : (
|
||||||
|
<form className="stack" onSubmit={onSubmit}>
|
||||||
|
<div className="account-modal-grid">
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="account-name">Имя</label>
|
||||||
|
<input id="account-name" name="name" type="text" value={form.name} onChange={onFieldChange} />
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="account-email">Почта</label>
|
||||||
|
<input id="account-email" name="email" type="email" value={form.email} onChange={onFieldChange} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="account-modal-grid">
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="account-phone">Телефон</label>
|
||||||
|
<input id="account-phone" name="phone" type="text" value={form.phone} onChange={onFieldChange} />
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="account-password">Новый пароль</label>
|
||||||
|
<input
|
||||||
|
id="account-password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
value={form.password}
|
||||||
|
onChange={onFieldChange}
|
||||||
|
placeholder="Оставьте пустым, если не меняете"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="account-modal-grid">
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="account-password-confirm">Подтверждение пароля</label>
|
||||||
|
<input
|
||||||
|
id="account-password-confirm"
|
||||||
|
name="passwordConfirm"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
value={form.passwordConfirm}
|
||||||
|
onChange={onFieldChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="field" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="account-security-box">
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: "0.5rem", flexWrap: "wrap" }}>
|
||||||
|
<div>
|
||||||
|
<b>2FA</b>: {totpStatus.enabled ? "Включена" : "Выключена"}
|
||||||
|
</div>
|
||||||
|
<div className="muted">Режим: {String(totpStatus.mode || "-")}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: "0.6rem", display: "flex", gap: "0.45rem", flexWrap: "wrap" }}>
|
||||||
|
<button className="btn secondary" type="button" onClick={onSetupTotp}>
|
||||||
|
Настроить 2FA
|
||||||
|
</button>
|
||||||
|
{totpStatus.enabled ? (
|
||||||
|
<>
|
||||||
|
<button className="btn secondary" type="button" onClick={onRegenerateBackupCodes}>
|
||||||
|
Backup-коды
|
||||||
|
</button>
|
||||||
|
<button className="btn danger" type="button" onClick={onDisableTotp}>
|
||||||
|
Отключить 2FA
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", gap: "0.6rem", flexWrap: "wrap" }}>
|
||||||
|
<button className="btn" type="submit" disabled={saveLoading}>
|
||||||
|
{saveLoading ? "Сохраняем..." : "Сохранить изменения"}
|
||||||
|
</button>
|
||||||
|
<button className="btn secondary" type="button" onClick={onClose} disabled={saveLoading}>
|
||||||
|
Закрыть
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<StatusLine status={status} />
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Overlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function AttachmentPreviewModal({ open, title, url, fileName, mimeType, onClose }) {
|
function AttachmentPreviewModal({ open, title, url, fileName, mimeType, onClose }) {
|
||||||
const [resolvedUrl, setResolvedUrl] = useState("");
|
const [resolvedUrl, setResolvedUrl] = useState("");
|
||||||
const [resolvedText, setResolvedText] = useState("");
|
const [resolvedText, setResolvedText] = useState("");
|
||||||
|
|
@ -1036,6 +1152,23 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
|
||||||
code: "",
|
code: "",
|
||||||
loading: false,
|
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({
|
const [recordModal, setRecordModal] = useState({
|
||||||
open: false,
|
open: false,
|
||||||
|
|
@ -2743,6 +2876,146 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
|
||||||
[api, token]
|
[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(() => {
|
const closeTotpSetupModal = useCallback(() => {
|
||||||
setTotpSetupModal({
|
setTotpSetupModal({
|
||||||
open: false,
|
open: false,
|
||||||
|
|
@ -2931,6 +3204,13 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
|
||||||
code: "",
|
code: "",
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
|
setAccountModal({
|
||||||
|
open: false,
|
||||||
|
loading: false,
|
||||||
|
saving: false,
|
||||||
|
initial: { name: "", email: "", phone: "" },
|
||||||
|
form: { name: "", email: "", phone: "", password: "", passwordConfirm: "" },
|
||||||
|
});
|
||||||
setActiveSection("dashboard");
|
setActiveSection("dashboard");
|
||||||
}, [resetKanbanState, resetRequestWorkspaceState, resetTablesState]);
|
}, [resetKanbanState, resetRequestWorkspaceState, resetTablesState]);
|
||||||
|
|
||||||
|
|
@ -3054,7 +3334,8 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
|
||||||
if (!hasCurrent) setConfigActiveKey(dictionaryTableItems[0].key);
|
if (!hasCurrent) setConfigActiveKey(dictionaryTableItems[0].key);
|
||||||
}, [configActiveKey, dictionaryTableItems]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
document.body.classList.toggle("modal-open", anyOverlayOpen);
|
document.body.classList.toggle("modal-open", anyOverlayOpen);
|
||||||
return () => document.body.classList.remove("modal-open");
|
return () => document.body.classList.remove("modal-open");
|
||||||
|
|
@ -3068,10 +3349,11 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
|
||||||
closeKanbanSortModal();
|
closeKanbanSortModal();
|
||||||
setReassignModal((prev) => ({ ...prev, open: false }));
|
setReassignModal((prev) => ({ ...prev, open: false }));
|
||||||
closeTotpSetupModal();
|
closeTotpSetupModal();
|
||||||
|
closeAccountModal();
|
||||||
};
|
};
|
||||||
document.addEventListener("keydown", onEsc);
|
document.addEventListener("keydown", onEsc);
|
||||||
return () => document.removeEventListener("keydown", onEsc);
|
return () => document.removeEventListener("keydown", onEsc);
|
||||||
}, [closeKanbanSortModal, closeTotpSetupModal]);
|
}, [closeAccountModal, closeKanbanSortModal, closeTotpSetupModal]);
|
||||||
|
|
||||||
const menuItems = useMemo(() => {
|
const menuItems = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
|
@ -3202,19 +3484,9 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
|
||||||
</div>
|
</div>
|
||||||
{token && role ? (
|
{token && role ? (
|
||||||
<div style={{ marginTop: "0.5rem", display: "flex", gap: "0.4rem", flexWrap: "wrap" }}>
|
<div style={{ marginTop: "0.5rem", display: "flex", gap: "0.4rem", flexWrap: "wrap" }}>
|
||||||
<button className="btn secondary" type="button" onClick={setupTotp}>
|
<button className="btn secondary" type="button" onClick={openAccountModal}>
|
||||||
Настроить 2FA
|
Личный кабинет
|
||||||
</button>
|
</button>
|
||||||
{totpStatus.enabled ? (
|
|
||||||
<>
|
|
||||||
<button className="btn secondary" type="button" onClick={regenerateTotpBackupCodes}>
|
|
||||||
Backup-коды
|
|
||||||
</button>
|
|
||||||
<button className="btn danger" type="button" onClick={disableTotp}>
|
|
||||||
Отключить 2FA
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div style={{ marginTop: "0.75rem", display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
<div style={{ marginTop: "0.75rem", display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
||||||
|
|
@ -3627,6 +3899,21 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
|
||||||
onCopyUri={copyTotpUri}
|
onCopyUri={copyTotpUri}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AccountModal
|
||||||
|
open={accountModal.open}
|
||||||
|
status={getStatus("account")}
|
||||||
|
profileLoading={accountModal.loading}
|
||||||
|
saveLoading={accountModal.saving}
|
||||||
|
form={accountModal.form}
|
||||||
|
totpStatus={totpStatus}
|
||||||
|
onFieldChange={updateAccountField}
|
||||||
|
onClose={closeAccountModal}
|
||||||
|
onSubmit={submitAccountModal}
|
||||||
|
onSetupTotp={setupTotp}
|
||||||
|
onRegenerateBackupCodes={regenerateTotpBackupCodes}
|
||||||
|
onDisableTotp={disableTotp}
|
||||||
|
/>
|
||||||
|
|
||||||
{!token || !role ? <LoginScreen onSubmit={login} status={getStatus("login")} /> : null}
|
{!token || !role ? <LoginScreen onSubmit={login} status={getStatus("login")} /> : null}
|
||||||
<GlobalTooltipLayer />
|
<GlobalTooltipLayer />
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue