История сущностей и Audit Timeline
Audit timeline хранит продуктовую историю действий по ключевым сущностям и auth/security-событиям. V1 реализован как модуль internal/modules/audit внутри текущего Go API, без отдельного HTTP-сервиса.
Архитектура
- API пишет событие в
audit_outboxчерезaudit.Emitterиз service layer. - Запись аудита best-effort: бизнес-операция не откатывается, если outbox недоступен; ошибка только логируется.
- Фоновый worker в существующем worker-процессе забирает pending rows через
FOR UPDATE SKIP LOCKEDи вставляет их вaudit_events. - Вставка в
audit_eventsидемпотентная поevent_id:ON CONFLICT (event_id) DO NOTHING. audit_eventsхранится бессрочно.audit_outboxявляется технической очередью со статусами, retry иlast_error.
Связанные таблицы:
audit_outbox: raw payload, статус, attempts,next_attempt_at,last_error.audit_events: нормализованная timeline-запись с entity, actor, request metadata, изменёнными полями, changes и metadata.
API
Entity Timeline
GET /api/v1/entities/:type/:id/timeline
Поддержанные type в v1:
projecttaskcompanycontactdocumentlogistic_operationequipment_request
Фильтры:
event_typeactor_idfromtolimitoffset
Ответ возвращается через стандартный list envelope: items и meta.
Admin Audit Events
GET /api/v1/audit/events
Доступ: admin, system.global_admin или permission audit.view.
Фильтры:
categoryentity_typeentity_idactor_idevent_typerequest_idfromtolimitoffset
Поля события
AuditEventDTO содержит:
id,event_id,occurred_at,category,event_type,actionentity_type,entity_idactor_id, nullableactor { id, full_name, email }client_ip,user_agent,request_id,method,pathchanged_fields,changes,metadata
Для update-событий изменения хранятся только по whitelisted полям:
{
"changed_fields": ["status"],
"changes": {
"status": {
"from": "todo",
"to": "done"
}
}
}События v1
project.*: create, update, archive, status/spec submit/approve/reject.task.*: create, update, delete, status/start/finish/comment added.company.*,contact.*: create, update, deactivate.document.*: created/status changed через существующий document EventBus.logistic_operation.*: create, update, delete, confirm, close, finance approve/reject.equipment_request.*: create, update, cancel, submit, finance approve/reject, procurement and plan transitions.auth.*: login success/failure, refresh success/failure, logout.
Существующие project_team_history и logistic_operation_events не мигрируются и не ломаются. Новые релевантные события дополнительно дублируются в общий audit timeline.
Безопасность данных
Нельзя сохранять в audit:
- пароли;
- access/refresh JWT;
- reset/verification tokens;
Authorizationheaders;- бинарные payload файлов и документов;
- raw request body.
Failed auth-события имеют nullable actor. В metadata допускаются только masked principal и reason code без секретов.
IP определяется через gin.Context.ClientIP() и список доверенных прокси APP_TRUSTED_PROXIES. Raw X-Forwarded-For не используется без trusted proxy.
Конфигурация
| Env | Default | Назначение |
|---|---|---|
APP_TRUSTED_PROXIES | 127.0.0.1,::1,172.16.0.0/12 | Доверенные прокси для ClientIP() |
APP_AUDIT_ENABLED | true | Включает emission событий в outbox |
APP_AUDIT_WORKER_ENABLED | true | Включает worker обработки outbox |
APP_AUDIT_WORKER_INTERVAL | 2s | Интервал polling worker |
APP_AUDIT_WORKER_BATCH_SIZE | 100 | Batch size для claim pending rows |
Worker использует max attempts 10. После исчерпания попыток событие остаётся в audit_outbox со статусом failed.
Интерфейс
С 2026-09-30 журнал виден в интерфейсе:
- «Администрирование → Журнал действий» (
GET /audit/events, capabilityaudit_viewв/org/me: администратор,audit.viewилиsystem.global_admin). Разделы «Действия», «Входы», «Безопасность»; фильтры по периоду, объекту, событию и сотруднику; серверная пагинация. Строка — кто, когда, что произошло и что поменялось («Статус: В работе → Сдан»); подробности события — в боковой панели со ссылкой на объект. - Вкладка «История» в карточках проекта, компании и задачи (
GET /entities/:type/:id/timeline) — видит тот, кто видит сам объект.
Подписи событий, объектов и полей — rms_crm_front/src/data/auditLabels.ts и справочники auditCategory, auditEntity. Незнакомый код события получает подпись по действию («Изменение»), незнакомое поле — «Другое поле»: сырые коды в интерфейс не попадают. Значения показываются словами интерфейса: статусы — по справочнику статусов, суммы — в рублях, даты — ДД.ММ.ГГГГ; изменения без разницы (было = стало) скрыты.