Соглашение о неразглашении (NDA)
При первом входе сотрудник видит блокирующий экран с текстом соглашения и не получает данные, пока не нажмёт «Принимаю». Проверка живёт на сервере, а не только в интерфейсе: гейт стоит сразу после аутентификации и закрывает все маршруты, кроме /auth и /nda, поэтому обойти экран прямым обращением к API нельзя.
Реализовано: модуль
internal/modules/nda, миграция0136(таблицыnda_documents,nda_acceptances, первая версия1.0, кодыnda.report/nda.manage), гейт подключён вinternal/app/app.goчерезauthHTTP.AddPostAuthMiddleware(nda.GateMiddleware(ndaSvc)). Спецификация «Структура RMS», п.12.
Схема
Пользователь: "Логинюсь, получаю токен"
↓
Фронт: "GET /nda/current"
↓
Система: "accepted=false, document={version, title, body}"
↓
Пользователь: "Читаю, нажимаю «Принимаю» (POST /nda/accept) или «Отказываюсь» (POST /nda/decline)"
↓
Система: "Пишу в журнал: user, версия, действие, ip, user-agent"
↓
Гейт: "accepted=true — все остальные маршруты открыты; иначе 403 nda_not_accepted"
↓
OWNER/OPS: "POST /nda/versions — новая версия, все принимают заново"Сущности
Версия соглашения (Document, таблица nda_documents)
| Поле | Описание |
|---|---|
version | Уникальная строка версии (1.0, 2026-09, …) |
title | Заголовок; по умолчанию «Соглашение о неразглашении» |
body | Текст соглашения |
is_active | Действующая версия. Частичный уникальный индекс uq_nda_documents_active гарантирует, что активная ровно одна |
published_at | Дата публикации |
Публикация (Service.Publish) в одной транзакции снимает is_active со всех версий и вставляет новую активную. Старые версии остаются как история.
Запись журнала (Acceptance, таблица nda_acceptances)
| Поле | Описание |
|---|---|
user_id, user_name | Кто |
document_id, version | Какую версию |
action | accepted или declined |
ip | c.ClientIP() на момент действия |
user_agent | заголовок User-Agent (хранится, в ответах API не отдаётся) |
created_at | Когда |
Фиксируются оба действия: отказ означает отсутствие доступа, а не ограниченный доступ. Записи не перезаписываются — журнал append-only, статус определяется последним действием пользователя по действующей версии.
Статус для пользователя (Status)
GET /nda/current и гейт используют один расчёт (Service.StatusFor):
| Поле | Значение |
|---|---|
document | Действующая версия (без неё — null) |
accepted | Последнее действие по действующей версии — accepted |
declined | Последнее действие — declined |
accepted_at | Время принятия |
Гейт
nda.GateMiddleware регистрируется как post-auth middleware — выполняется после разбора JWT на всех маршрутах под authMW.
| Ситуация | Поведение |
|---|---|
Путь начинается с /api/v1/auth/ или /api/v1/nda/ | пропуск (иначе соглашение нельзя было бы загрузить и принять) |
В контексте нет user_id | пропуск (неаутентифицированные запросы отсекает auth middleware) |
Действующей версии нет (nda_documents без is_active) | считается принятым — пустой справочник не должен закрывать систему |
Последнее действие по действующей версии — accepted | пропуск |
Иначе (нет действия или declined) | 403 с кодом nda_not_accepted |
Что из этого следует:
- Новая версия = повторное принятие. Согласие привязано к
document_id; послеPOST /nda/versionsу всех пользователейaccepted=false, и гейт закрывается до нового «Принимаю». - Сброс одному сотруднику = повторное принятие только для него.
POST /nda/users/:id/reset(правоnda.manage, кнопка «Попросить подписать заново» на вкладке NDA карточки сотрудника) пишет в журнал действиеresetс автором (actor_id, миграция0194). Последнее действие сотрудника —reset, поэтомуaccepted=falseи при следующем запросе гейт снова показывает соглашение. Положительный ответ гейта в кеше другого процесса живёт не дольше минуты. В отчёте у строки появляютсяreset_atиreset_by_name(IP сброса — адрес администратора, в отчёт не попадает). - Отчёт и публикация тоже под гейтом? Нет: они лежат под
/api/v1/nda/, поэтому доступны и до принятия — но защищены своими permission-кодами. - WebSocket-маршруты
/ws/notificationsи/ws/chatтоже идут через auth middleware и не входят в исключения, поэтому до принятия соглашения соединение отклоняется с тем же403. - Гейт делает 1–2 SQL-запроса на каждый защищённый запрос; кэш на TTL запланирован в дорожной карте (этап 6).
Права
Принятие, отказ и своя история открыты каждому аутентифицированному пользователю. Два действия закрыты legacy permission-кодами (RequirePermissions), матрица прав их не покрывает:
| Код | Кому выдан (0136) | Что открывает |
|---|---|---|
nda.report | OWNER, OPS, admin | GET /nda/report |
nda.manage | OWNER, OPS, admin | POST /nda/versions, POST /nda/users/:id/reset |
Носитель system.global_admin проходит обе проверки без кода.
API
| Метод | Путь | Доступ | Описание |
|---|---|---|---|
GET | /api/v1/nda/current | любой | Действующая версия и статус текущего пользователя |
POST | /api/v1/nda/accept | любой | Принять действующую версию. Тело опционально: {"version": "1.0"} — только для явности намерения, на запись не влияет |
POST | /api/v1/nda/decline | любой | Отказаться. Ответ — тот же Status |
GET | /api/v1/nda/my | любой | История собственных действий по всем версиям: {"items": [Acceptance]} |
GET | /api/v1/nda/report | nda.report | Кто принял, кто отказался, кто ещё не ответил по действующей версии |
POST | /api/v1/nda/versions | nda.manage | {"version", "title"?, "body"} — выпустить новую версию, 201 |
POST | /api/v1/nda/users/:id/reset | nda.manage | Попросить сотрудника принять действующую версию заново. Ответ — Status сотрудника |
Ошибки: nda_no_active_document (404) при accept/decline без действующей версии; nda_version_required, nda_body_required (400) при публикации; nda_user_not_found (404) при сбросе несуществующему сотруднику.
Ответ отчёта:
{
"document": { "id": 1, "version": "1.0", "title": "…", "is_active": true, "published_at": "…" },
"rows": [
{ "user_id": 12, "user_name": "…", "email": "…", "accepted": true, "declined": false, "accepted_at": "…", "ip": "10.0.0.5" }
],
"total_users": 40, "accepted_users": 31, "pending_users": 8, "declined_users": 1
}Что важно для фронта
- После логина первым делом вызывайте
GET /nda/current; приaccepted=falseпоказывайте блокирующий экран сdocument.bodyи не грузите остальные данные — они всё равно вернут403 nda_not_accepted. - Обрабатывайте код
nda_not_acceptedглобально в HTTP-клиенте: любой запрос может его вернуть после публикации новой версии посреди сессии — переводите пользователя на экран соглашения, не на «нет доступа». declineне разлогинивает: пользователь остаётся с токеном, но без данных. Покажите объяснение и кнопку «Вернуться к соглашению».- Не кэшируйте
acceptedдольше сессии; сбрасывайте при logout и после публикации версии. - Экран отчёта и публикация версии — по permission-кодам
nda.report/nda.manage, которых нет в матрицеpermissionsиз/org/me. Итог сервер отдаёт вcapabilitiesтого же ответа:nda_reportиnda_manage(см. Матрица прав);403от сервера по-прежнему обрабатывайте. ipв отчёте — служебная информация для администратора; в профиле пользователя (/nda/my) он тоже есть,user_agentне отдаётся.