Перейти к основному содержимому

Уведомления

Документ canonical: false. SQL ниже является bootstrap/conceptual примером platform-owned notifications и не используется для identity migrations.

Зачем нужно

Документ описывает bootstrap-hosting platform-owned notifications внутри identity-api. Canonical owner уведомлений, правил, шаблонов и plugins — platform по ADR-035; identity здесь не получает ownership.

1. Разделение: логи и уведомления

Два разных механизма, не смешиваются:

Логи (Audit)Уведомления
Для когоАдмин, разработчикКонечный пользователь
ЦельРасследование, аудитИнформирование пользователя
ХранениеБД (значимые) + файлы (все)БД (in_app) + внешние каналы

2. Audit (логи)

2.1. Два уровня логирования

БД (audit_logs) — только значимые действия:

  • Вход / выход
  • Смена пароля
  • Бан / разбан пользователя
  • Смена ролей
  • Изменение системных настроек
  • Создание / удаление организации
  • Управление плагинами

Retention: security/business audit events хранятся 7 лет по platform baseline и не удаляются cron-задачей. Для application notification logs допускается отдельная TTL-политика, но она не применяется к audit_logs.

Файловые логи (pino) — все HTTP-запросы:

  • requestId, userId, method, path, status, duration
  • Ротация по размеру / дате
  • Для разработчиков и расследований инцидентов
  • Не в БД — не нагружают базу

2.2. Таблица audit_logs

CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
action VARCHAR(100) NOT NULL, -- "identity.login.succeeded", "identity.user.blocked", "platform.settings.updated"
resource VARCHAR(100), -- "user", "organization", "plugin"
resource_id UUID,
details JSONB, -- контекст действия (что изменено, с какого IP)
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_audit_logs_user_id ON audit_logs (user_id);
CREATE INDEX idx_audit_logs_action ON audit_logs (action);
CREATE INDEX idx_audit_logs_created_at ON audit_logs (created_at);

3. Уведомления

3.1. Поток

Событие в системе


NATS JetStream domain event


NotificationOrchestrator (consumer JetStream)


Ищет активные правила для `messageKind = event` и messageType (notification_rules)


Для каждого правила:
├── проверить conditions (условная логика)
├── определить получателей (recipientType + payload)
├── для каждого получателя:
│ ├── проверить пользовательские настройки (notification_user_settings)
│ ├── рендерить шаблон (Handlebars, locale получателя)
│ │
│ ├── in_app → сохранить в БД + WebSocket (real-time)
│ └── email/sms/push → BullMQ очередь
│ │
│ ▼
│ NotificationWorker
│ │
│ ▼
│ TransportRegistry → плагин → отправка

3.2. Правила (notification_rules)

Админ создаёт и редактирует через UI. Нет хардкода.

CREATE TABLE notification_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_kind VARCHAR(20) NOT NULL DEFAULT 'event' CHECK (message_kind = 'event'),
message_type VARCHAR(100) NOT NULL, -- "identity.login.succeeded", "identity.invitation.created"
name JSONB NOT NULL, -- { "ru": "Вход с нового IP", "en": "Login from new IP" }
description JSONB,
conditions JSONB NOT NULL DEFAULT '{}', -- условная логика: { "isNewIp": true }
template_id UUID REFERENCES notification_templates(id),
channels JSONB NOT NULL DEFAULT '["in_app"]', -- ["in_app", "email", "sms"]
recipient_type VARCHAR(50) NOT NULL, -- "event_user", "admins", "org_owner", "custom"
recipient_ids JSONB, -- для custom: конкретные userId
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_system BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IDX_notification_rules_event ON notification_rules(message_kind, message_type);

recipient_type:

ЗначениеПолучатели
event_userПользователь из payload события
adminsВсе super_admin / admin
org_ownerВладелец организации из payload
org_membersВсе участники организации
customКонкретные userId из recipient_ids

conditions:

JSON-объект с ключами из event payload и ожидаемыми значениями. Оркестратор проверяет каждый ключ: если все совпадают — правило срабатывает.

{ "isNewIp": true } — только при входе с нового IP
{ "role": "admin" } — только для админов
{ "channel": "email" } — только email-события
{} — всегда срабатывает (нет условий)

3.3. Пользовательские настройки (notification_user_settings)

Пользователь может:

  • Отключить конкретное правило для себя
  • Изменить каналы доставки для конкретного правила
CREATE TABLE notification_user_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
rule_id UUID NOT NULL REFERENCES notification_rules(id) ON DELETE CASCADE,
enabled BOOLEAN NOT NULL DEFAULT TRUE, -- отключить правило для себя
channels JSONB, -- переопределить каналы (null = использовать дефолт из правила)
UNIQUE(user_id, rule_id)
);

3.4. In-app уведомления (notifications)

CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
rule_id UUID REFERENCES notification_rules(id) ON DELETE SET NULL,
title TEXT NOT NULL,
body TEXT,
is_read BOOLEAN NOT NULL DEFAULT FALSE,
read_at TIMESTAMPTZ,
data JSONB, -- дополнительные данные (ссылка, action)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_notifications_user_unread ON notifications (user_id, is_read);

Retention: прочитанные уведомления старше 90 дней удаляются cron-задачей (ежедневно). Непрочитанные — хранятся до прочтения или удаления пользователем.

3.5. Шаблоны (notification_templates)

Handlebars с кэшем, локализация, email layouts.

CREATE TABLE notification_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_type VARCHAR(100) NOT NULL,
channel VARCHAR(30) NOT NULL, -- in_app, email, sms
locale VARCHAR(10) NOT NULL, -- ru, en
subject TEXT,
body TEXT NOT NULL,
html TEXT,
layout_id UUID REFERENCES email_layouts(id),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_system BOOLEAN NOT NULL DEFAULT FALSE,
UNIQUE(message_type, channel, locale)
);

3.5a. Email layouts (email_layouts)

Общие обёртки для email-шаблонов (header, footer, стили). Шаблон ссылается на layout через layout_id.

CREATE TABLE email_layouts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL UNIQUE, -- "default", "transactional", "marketing"
html TEXT NOT NULL, -- Handlebars: {{> content}} — место вставки body
is_default BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Layout содержит {{> content}} partial — туда подставляется отрендеренный html из шаблона. При layout_id = NULL — шаблон рендерится без обёртки.

3.6. Структура бэкенд-сервиса

apps/identity-api/src/platform/notifications/
├── notifications.module.ts
├── notifications.controller.ts
├── notifications.service.ts
├── notification-orchestrator.service.ts
├── notification-rules.service.ts
├── notification-template.service.ts
├── channels/
│ ├── email.channel.ts
│ ├── sms.channel.ts
│ ├── push.channel.ts
│ └── websocket.channel.ts
├── entities/
│ ├── notification-rule.entity.ts
│ ├── notification-user-setting.entity.ts
│ └── audit-log.entity.ts
└── dto/

3.7. BullMQ — очереди и retry

Очереди: notification-email, notification-sms, notification-push
Retry: 3 попытки с exponential backoff (1s, 5s, 25s)
Dead letter: после 3 неудач — запись в audit_logs с status: 'failed'

3.8. WebSocket (real-time)

In-app уведомления доставляются в реальном времени через Socket.IO (@nestjs/websockets):

  • Namespace: /notifications
  • Аутентификация: JWT при handshake
  • Комнаты: user:{userId}
  • События: notification:new (новое уведомление), notification:read (помечено прочитанным)
  • При создании in_app уведомления → оркестратор отправляет через WebSocket
  • Фронтенд: получает событие → обновляет счётчик и список уведомлений без перезагрузки

3.9. Event transport

Notifications получают события из платформенной шины NATS JetStream по контракту ../../../platform/events-bus.md. Локальный in-process pub/sub допустим только как адаптер внутри identity-api для bootstrap-режима и не является cross-domain транспортом.

Event transport не является частью notifications — это инфраструктурная шина, которую notifications используют как источник событий.


4. API-эндпоинты

Уведомления пользователя

HTTPПутьAuthНазначение
GET/api/v2/platform/notificationsJWTМои уведомления (пагинация)
GET/api/v2/platform/notifications/unread-countJWTСчётчик непрочитанных
PATCH/api/v2/platform/notifications/{id}/readJWTПометить прочитанным
POST/api/v2/platform/notifications/read-allJWTПометить все прочитанными
DELETE/api/v2/platform/notifications/{id}JWTУдалить уведомление
DELETE/api/v2/platform/notifications/readJWTУдалить все прочитанные
GET/api/v2/platform/notifications/settingsJWTМои настройки уведомлений
PATCH/api/v2/platform/notifications/settings/{ruleId}JWTИзменить настройку правила

Управление правилами (admin)

HTTPПутьAuthНазначение
GET/api/v2/platform/admin/notifications/rulesJWT + platform.notifications.readСписок правил
POST/api/v2/platform/admin/notifications/rulesJWT + platform.notifications.manageСоздать правило
PATCH/api/v2/platform/admin/notifications/rules/{id}JWT + platform.notifications.manageРедактировать
DELETE/api/v2/platform/admin/notifications/rules/{id}JWT + platform.notifications.manageУдалить

Шаблоны (admin)

HTTPПутьAuthНазначение
GET/api/v2/platform/admin/notifications/templatesJWT + platform.notifications.readСписок шаблонов
PATCH/api/v2/platform/admin/notifications/templates/{id}JWT + platform.notifications.manageРедактировать шаблон

Audit (admin)

HTTPПутьAuthНазначение
GET/api/v2/identity/admin/audit-logsJWT + identity.audit.readЛоги значимых действий (пагинация, фильтры)

5. События EventBus

Реестр событий, на которые подписывается оркестратор уведомлений:

ДоменСобытия
authidentity.login.succeeded, identity.login.failed, identity.password.changed, identity.password_reset.completed, identity.session.revoked
usersidentity.user.created, identity.user.updated, identity.user.blocked, identity.user.activated
rbacidentity.role.assigned, identity.role.revoked
organizationsidentity.organization.created, identity.organization.updated, identity.organization_invitation.created, identity.organization_invitation.sent, identity.organization_invitation.accepted, identity.organization_invitation.rejected, identity.organization_invitation.revoked, identity.organization_membership.activated, identity.organization_membership.suspended, identity.organization_ownership_claim.submitted, identity.organization_ownership_transfer.completed
familyidentity.family.created, identity.family_member.added, identity.family_student_profile.created, identity.child_device_authorization.created, identity.child_device_authorization.approved, identity.child_device_authorization.completed
pluginsplatform.plugin.installed, platform.plugin.enabled, platform.plugin.disabled, platform.plugin.deleted
invitationsidentity.invitation.created, identity.invitation.accepted, identity.invitation.rejected, identity.invitation.revoked только для generic family/system invitations

Каждое событие: { messageKind: 'event', messageType, payload, userId?, timestamp }. Notification rules не реагируют на commands.


6. UX/UI

6.1. NotificationCenter — колокольчик

Фиксированная позиция в правом верхнем углу внутри DashboardLayout. Компонент реализуется на CSS Modules и общих токенах из ../../../platform/ui-system.md; классы Tailwind в приложениях не используются.

Иконка:

  • Колокольчик с красным бейджем (число непрочитанных, "99+" при переполнении)
  • Нажатие → Radix DropdownMenu панель

Панель уведомлений:

  • Ширина: 380px, max-height: 400px со скроллом
  • Шапка: "Уведомления" + счётчик непрочитанных + кнопка "Прочитать все"
  • Группировка: Сегодня / Вчера / Ранее

Каждое уведомление:

  • Иконка типа: Shield (безопасность, красная) / UserPlus (приглашения, синяя) / User (аккаунт, фиолетовая) / Users (семья, зелёная) / Settings (админ, оранжевая) / Bell (дефолт)
  • Заголовок (bold если непрочитано) + сообщение (2 строки, обрезка)
  • Относительное время: "только что", "5 мин назад", "1ч назад", "вчера", или дата
  • Индикатор непрочитанного: синяя точка справа от заголовка
  • Приоритет: семантический левый индикатор через CSS Modules (critical, high)
  • Hover: кнопка удаления (иконка корзины, hover красный)

Пустое состояние: большая иконка колокольчика + "Нет уведомлений"

Loading: 3-строчный скелетон с анимацией пульсации

6.2. NotificationSettingsPage — настройки

Страница /:lang/notifications/settings внутри DashboardLayout.

Матричная таблица:

  • Строки: категории событий (Безопасность, Приглашения, Аккаунт, Семья, Админ)
  • Колонки: каналы доставки (Email, Push, Telegram, SMS, In-app)
  • Каждая ячейка: Switch тумблер
  • Безопасность / In-app: принудительно включен, лейбл "Обязательно"
  • Кнопка "Сбросить к умолчаниям" внизу справа

Поведение:

  • Оптимистичные обновления: Switch переключается мгновенно
  • При ошибке: откат + toast с ошибкой