From af98ec94e278be2e27c49134da8c6e2962062683 Mon Sep 17 00:00:00 2001 From: viktot14-ai Date: Wed, 9 Sep 2026 21:49:06 +0300 Subject: [PATCH] =?UTF-8?q?E2=20UI:=20=D0=B6=D1=83=D1=80=D0=BD=D0=B0=D0=BB?= =?UTF-8?q?=20=D0=B0=D1=83=D0=B4=D0=B8=D1=82=D0=B0=20=E2=80=94=20=D0=B2?= =?UTF-8?q?=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D0=B0=20=C2=AB=D0=90=D1=83=D0=B4?= =?UTF-8?q?=D0=B8=D1=82=C2=BB=20=D0=B2=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD?= =?UTF-8?q?=D0=BA=D0=B5=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuditTab: события (вход/неудачный вход/блокировка/user_create/user_update/ password_reset/operation_*/contour_send_*), фильтр по типу, пагинация 50, retention из security_settings, field-level old→new в раскрываемых деталях. - Цветовая маркировка: вход/успех зелёным, отказы/ошибки красным, сброс пароля жёлтым. --- frontend/src/pages/UsersAdmin.css | 35 +++++++- frontend/src/pages/UsersAdmin.jsx | 135 +++++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/UsersAdmin.css b/frontend/src/pages/UsersAdmin.css index 7a055bf..ec22ee6 100644 --- a/frontend/src/pages/UsersAdmin.css +++ b/frontend/src/pages/UsersAdmin.css @@ -78,4 +78,37 @@ .us-act-lock { background: #6e2c2c; color: #ffb3b3; } .us-act-unlock{ background: #1f4d2c; color: #7ee2a8; } .us-act-pwd { background: #21262d; color: #e6edf3; } -.us-empty { text-align: center; color: #8b949e; padding: 20px; } \ No newline at end of file +.us-empty { text-align: center; color: #8b949e; padding: 20px; } +.us-tabs { display: flex; gap: 2px; } +.us-tab { + padding: 8px 16px; background: transparent; border: none; border-bottom: 2px solid transparent; + color: #8b949e; font-size: 14px; font-weight: 600; cursor: pointer; +} +.us-tab:hover { color: #e6edf3; } +.us-tab-active { color: #e6edf3; border-bottom-color: #238636; } + +.us-audit { margin-top: 4px; } +.us-audit-meta { font-size: 12px; color: #8b949e; margin-left: auto; align-self: center; } +.us-time { white-space: nowrap; font-family: monospace; font-size: 12px; color: #8b949e; } +.us-ev { + display: inline-block; padding: 2px 9px; border-radius: 9px; font-size: 11px; font-weight: 600; + background: rgba(139, 148, 158, 0.15); color: #8b949e; +} +.us-ev-login, .us-ev-contour_send_ok { background: rgba(63, 185, 80, 0.15); color: #3fb950; } +.us-ev-login_failed, .us-ev-locked_out, .us-ev-contour_send_failed { + background: rgba(248, 81, 73, 0.15); color: #f85149; +} +.us-ev-user_create, .us-ev-operation_create { background: rgba(35, 134, 54, 0.2); color: #7ee2a8; } +.us-ev-password_reset { background: rgba(255, 209, 102, 0.15); color: #ffd166; } +.us-changes { margin-top: 6px; display: flex; flex-direction: column; gap: 3px; } +.us-change { font-size: 12px; color: #e6edf3; } +.us-change b { color: #8b949e; font-weight: 600; } +.us-old { color: #f85149; text-decoration: line-through; } +.us-new { color: #3fb950; } +details summary { cursor: pointer; font-size: 12px; color: #58a6ff; } +.us-pager { display: flex; gap: 12px; align-items: center; margin-top: 12px; font-size: 13px; color: #8b949e; } +.us-pager button { + padding: 6px 12px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; + color: #e6edf3; font-size: 12px; cursor: pointer; +} +.us-pager button:disabled { opacity: 0.4; cursor: not-allowed; } diff --git a/frontend/src/pages/UsersAdmin.jsx b/frontend/src/pages/UsersAdmin.jsx index 55d1a6f..0daba40 100644 --- a/frontend/src/pages/UsersAdmin.jsx +++ b/frontend/src/pages/UsersAdmin.jsx @@ -21,7 +21,120 @@ const STATUS_CLASS = { disabled: 'us-st-disabled', }; +const EVENT_LABEL = { + login: 'Вход', + login_failed: 'Неудачный вход', + logout: 'Выход', + locked_out: 'Блокировка (5 неудач)', + user_create: 'Создан пользователь', + user_update: 'Изменён пользователь', + password_reset: 'Сброс пароля', + operation_create: 'Создана операция', + operation_update: 'Изменена операция', + contour_send_ok: 'Зоны отправлены в КОНТУР', + contour_send_failed: 'Ошибка отправки в КОНТУР', +}; + +const AuditTab = () => { + const [events, setEvents] = useState([]); + const [total, setTotal] = useState(0); + const [retention, setRetention] = useState(90); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [eventFilter, setEventFilter] = useState(''); + const [expanded, setExpanded] = useState({}); + const [offset, setOffset] = useState(0); + const PAGE = 50; + + const load = async (off = 0) => { + try { + setLoading(true); + setError(''); + const params = new URLSearchParams({ limit: String(PAGE), offset: String(off) }); + if (eventFilter) params.set('event_type', eventFilter); + const r = await apiFetch(`/api/v1/admin/users/audit?${params}`); + if (!r.ok) { + const d = await r.json().catch(() => ({})); + throw new Error(d.detail || `Ошибка ${r.status}`); + } + const d = await r.json(); + setEvents(d.items); + setTotal(d.total); + setRetention(d.retention_days); + setOffset(off); + } catch (e) { + setError(e.message); + } finally { + setLoading(false); + } + }; + + useEffect(() => { load(0); }, [eventFilter]); + + const fmtTime = (iso) => { + if (!iso) return '—'; + const d = new Date(iso); + return d.toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); + }; + + return ( +
+
+ + Хранится {retention} дней · всего {total} + +
+ + {loading &&
Загрузка...
} + {error &&
{error}
} + + {!loading && !error && ( + + + + + + {events.map((ev) => ( + + + + + + + + ))} + {events.length === 0 && } + +
ВремяПользовательСобытиеОбъектИзменения
{fmtTime(ev.created_at)}{ev.username || '—'}{EVENT_LABEL[ev.event_type] || ev.event_type}{ev.object_type || '—'} + {ev.changes?.length ? ( +
+ {ev.changes.length} поле +
+ {ev.changes.map((ch, i) => ( +
+ {ch.field}: {ch.old ?? '∅'} → {ch.new ?? '∅'} +
+ ))} +
+
+ ) : '—'} +
Событий нет
+ )} + +
+ + {offset + 1}–{Math.min(offset + PAGE, total)} из {total} + +
+
+ ); +}; + const UsersAdmin = () => { + const [activeTab, setActiveTab] = useState('users'); // 'users' | 'audit' const [users, setUsers] = useState([]); const [units, setUnits] = useState([]); const [loading, setLoading] = useState(true); @@ -147,12 +260,25 @@ const UsersAdmin = () => { return (
-

Пользователи

- +
+ + +
+ {activeTab === 'users' && ( + + )}
+ {activeTab === 'audit' && } + {activeTab === 'users' && (<> {message &&
{message}
} {error &&
{error}
} @@ -247,6 +373,7 @@ const UsersAdmin = () => { )} + )}
); };