ТЕХНИЧЕСКОЕ ЗАДАНИЕ (ТЗ) НА ПЛАТФОРМУ EVENTHUB Версия: 1.4 (актуальная реализация: раздельная JWT-аутентификация, версионированное админ-API, расширенная инфраструктура, аудит администраторов, расширенная статистика дашборда, улучшенная обработка ошибок) ## 1. ЦЕЛИ И НАЗНАЧЕНИЕ EventHub — платформа для управления событиями с поддержкой календарей, записи участников (включая специалистов), гибкого подтверждения, рейтингов, отзывов, модерации, встроенного баг-трекера и платной подписки. Целевая аудитория: владельцы календарей (бизнес), участники (клиенты), администраторы. ## 2. ФУНКЦИОНАЛЬНЫЕ ТРЕБОВАНИЯ ### 2.1. Календари - CRUD календаря (название, описание, теги, владелец) - Расшаривание по ссылке (публичная/приватная) - Приглашение пользователей с правами "запись" или "администрирование" - Типы календарей: personal (бесплатный, без записи), commercial (платный, запись клиентов, специалисты) - Гибкое подтверждение заявок: auto (автоматически), manual (вручную), timeout (авто через N секунд) - Теги календаря, рейтинг (средняя оценка, количество голосов) ### 2.2. События (расширенная версия с повторяющимися событиями) #### 2.2.1. Типы событий и модель хранения События делятся на два типа: - single — одиночное событие, имеет конкретные дату и время начала, длительность. - recurring — повторяющееся событие (серия), определяется мастер-записью и правилом повторения. Для эффективного хранения и быстрого поиска применяется гибридная модель с отложенной материализацией вхождений: - Каждая повторяющаяся серия представлена одной мастер-записью в таблице events с полем event_type = recurring и полем recurrence_rule, содержащим правило повторения в формате, совместимом с iCalendar RRULE (или упрощённый JSON-представление: freq, interval, byday, until и т.д.). - Конкретные вхождения (экземпляры) серии материализуются в таблице events только при необходимости: * когда на данное вхождение записался хотя бы один участник; * когда администратор вручную редактирует это вхождение (перенос времени, отмена, смена специалиста); * (опционально) при наступлении даты вхождения для архивных целей. - Материализованное вхождение имеет: * event_type = single (или специальный флаг is_instance = true), * ссылку master_id на родительскую мастер-запись, * собственные start_time и duration, которые могут отличаться от вычисленных по правилу (например, при переносе конкретного дня). - Отменённые вхождения из серии (исключения) хранятся в отдельной таблице recurrence_exceptions с указанием master_id, original_start_time и action = cancel | reschedule. #### 2.2.2. Правила генерации вхождений при поиске При поиске событий на заданный интервал дат (например, /v1/events?from=...&to=...) система: 1. Выбирает все одиночные события (event_type = single), попадающие в интервал. 2. Выбирает все активные мастер-записи (event_type = recurring), у которых: - дата первого вхождения ≤ конец интервала, - правило повторения допускает вхождения внутри интервала (учёт until или ограничения по количеству повторений). 3. Для каждой мастер-записи генерирует список вхождений в пределах запрошенного интервала (используя встроенную функцию разбора RRULE или собственный алгоритм). 4. Из сгенерированного списка исключаются вхождения, присутствующие в таблице recurrence_exceptions с действием cancel. 5. Если для какого-либо вхождения уже существует материализованная запись в таблице events (по совпадению master_id и start_time), то используются данные материализованного экземпляра (специалист, длительность, статус) вместо вычисленных по шаблону. 6. Итоговый список сортируется по времени начала и возвращается с пагинацией. Ограничение: глубина генерации вхождений для одного запроса ограничена 1000 элементов; для длительных серий применяется пагинация по датам. #### 2.2.3. Материализация при записи участника При попытке записаться на вхождение повторяющегося события (POST /v1/events/:event_id/join): - Если event_id ссылается на мастер-запись (event_type = recurring), то в теле запроса обязательно передаётся occurrence_start — конкретное время вхождения, на которое производится запись. - Система в одной транзакции Mnesia: * Проверяет, существует ли материализованное вхождение с master_id = EventId и start_time = OccurrenceStart. * Если нет — создаёт новую запись в таблице events (тип single, master_id = EventId), копируя общие атрибуты мастер-записи (название, описание, календарь, специалист). * Создаёт бронирование (booking), привязанное к идентификатору материализованного вхождения. - Дальнейшие действия по подтверждению заявки выполняются уже с материализованным экземпляром. #### 2.2.4. Изменение и удаление серий - При редактировании мастер-записи (изменение названия, описания, правила повторения) обновляется только сама мастер-запись. Уже существующие материализованные вхождения остаются неизменными (сохраняют старые значения атрибутов). Это поведение может быть изменено администратором через специальный флаг «применить ко всем будущим вхождениям». - При удалении мастер-записи все связанные материализованные вхождения также помечаются удалёнными (или удаляются каскадно), а бронирования на будущие вхождения аннулируются с уведомлением участников. #### 2.2.5. Структура записей (records.hrl) ```erlang -record(event, { id :: binary(), calendar_id :: binary(), title :: binary(), description :: binary(), event_type :: single | recurring, start_time :: calendar:datetime(), duration :: integer(), %% минуты recurrence_rule :: binary() | undefined, master_id :: binary() | undefined, is_instance :: boolean(), %% true для материализованных вхождений specialist_id :: binary() | undefined, location :: #location{} | undefined, tags :: [binary()], status :: active | cancelled | completed, created_at :: calendar:datetime(), updated_at :: calendar:datetime() }). -record(recurrence_exception, { master_id :: binary(), original_start :: calendar:datetime(), action :: cancel | reschedule, new_start :: calendar:datetime() | undefined }). ``` #### 2.2.6. Требования к реализации - Разбор и генерация RRULE должны быть реализованы в отдельном модуле logic_recurrence без внешних зависимостей (чистый Erlang). - Все операции с материализацией и записью участников должны выполняться в транзакциях Mnesia для обеспечения консистентности. - Для ускорения поиска материализованных вхождений по мастеру и дате создаётся составной индекс в Mnesia: {master_id, start_time} (через mnesia:add_table_index/2 или хранение в ets с ключом {master_id, start_time}). ### 2.3. Запись участников и подтверждение - Запись через календарь - Подтверждение согласно политике календаря - Уведомления участника и владельца - Привязка события к специалисту (отдельная запись в календаре специалиста) ### 2.4. Отзывы и рейтинги - Только участники событий могут оставлять отзывы - Оценка 1-5, текстовый комментарий - Отзывы на событие или на календарь - Модерация отзывов (администраторы могут скрывать) ### 2.5. Поиск и фильтрация - По тексту, по тегам, по гео-позиции (радиус), по дате/времени ### 2.6. Расширенные возможности - Экспорт в Google Calendar (через OAuth2) - Экспорт в Apple Calendar (ICS-файл) - Геокодирование (адрес -> координаты) через внешний API (заглушка / Nominatim) ### 2.7. Модерация и безопасность - Жалобы на календари, события, отзывы - Автоматическая модерация по ключевым словам и по порогу жалоб - Ручная заморозка / разморозка администратором - Бан-лист слов - **Аудит действий администраторов:** Ведётся детальный журнал действий всех администраторов (кто, что, когда, IP, причина). Модель аудита (admin_audit) включает: - admin_id — идентификатор администратора, - email — email администратора, - role — роль (superadmin, moderator, support), - action — действие (например, add_banned_word, delete_banned_word, update_role), - entity_type — тип сущности (banned_word, admin и т.д.), - entity_id — идентификатор сущности, - ip — IP-адрес, - reason — причина действия (опционально), - timestamp — время действия. ### 2.8. Баг-трекер (автоматический) - При ошибке создаётся тикет, группировка по хэшу ошибки - Учёт количества повторений - Уведомление пользователя при создании и при закрытии тикета - Доступ только у администраторов ### 2.9. Платная подписка - Личное использование бесплатно (personal), коммерческое — платно (commercial) - Пробный период 30 дней - Планы подписки: monthly, quarterly, biannual, annual - Заглушка платежного шлюза (для тестирования) - Автоматическое истечение подписки - Автоматическое понижение календарей до personal при истечении ### 2.10. Административная панель - Отдельный HTTPS-сервер (порт 8445) и отдельный WSS (порт 8446) - Управление календарями, событиями, отзывами, жалобами, тикетами, бан-словами - Статистика, информация о нодах кластера - WebSocket-уведомления администраторам #### 2.10.1. Ролевая модель администраторов (реализована) Реализована трехуровневая ролевая модель: superadmin, moderator, support. Проверка ролей выполняется в каждом административном обработчике через handler_auth:authenticate/1 и вспомогательную функцию is_admin/1. #### 2.10.2. Эндпоинты для управления ролями и аудитом - GET /v1/admin/me — получение текущей роли и разрешений администратора. - GET /v1/admin/admins — список всех администраторов с ролями (только для superadmin). - PUT /v1/admin/admins/:id — изменение роли администратора (только для superadmin). - POST /v1/admin/admins — приглашение нового администратора с назначением роли (только для superadmin). - GET /v1/admin/audit — журнал действий администраторов с фильтрацией по дате, пользователю, действию (только для superadmin). #### 2.10.3. Статистика для дашборда с учётом ролей Эндпоинт: GET /v1/admin/stats. Возвращает JSON, содержимое которого фильтруется в соответствии с ролью вызывающего: - superadmin — системные метрики: все пользователи, события, жалобы, баги за период, графики регистраций/событий по дням, активность администраторов. - moderator — собственные обработанные жалобы/события (количество, статусы, время реакции), общая статистика по модерации. - support — количество открытых багов и жалоб, назначенных на текущего сотрудника, персональные задачи. **Расширенная статистика (дополнение):** - GET /v1/admin/statistics — агрегированная статистика по администраторам: общее количество действий за период, распределение по типам действий. - Поддержка фильтрации: admin_id, action, date_from, date_to. ### 2.11. Real-time уведомления (WebSocket) - Пользователи: порт 8081, маршрут /ws, подписка на календарь, получение обновлений - Администраторы: порт 8446, маршрут /admin/ws, подписка на глобальные уведомления (жалобы, авто-заморозки) ### 2.12. Инфраструктура развертывания (Docker Compose) - Балансировщик Traefik с поддержкой HTTPS/WSS, WAF (Coraza), Rate Limiting и Failover - Кластер из трех нод Erlang с автоматическим обнаружением (DNS-лукап) - Мониторинг: Prometheus (метрики), Grafana (дашборды), LogLynx (аналитика логов) - Ротация логов (logrotate) - Административный SPA (EventHubFrontAdmin) как отдельный сервис - Сервис-заглушка (Fallback) для отказоустойчивости ## 3. НЕФУНКЦИОНАЛЬНЫЕ ТРЕБОВАНИЯ ### 3.1. Производительность и масштабирование - 100 000+ пользователей - Горизонтальное масштабирование (увеличение нод) - Mnesia с дисковыми копиями на нескольких нодах - Отдельные ноды для исторических данных (disc_only_copies) - Пагинация всех списков - Автоматическое обнаружение узлов через DNS-лукап (libcluster в перспективе) ### 3.2. Надёжность - Супервизорное дерево OTP - Let it crash – быстрый перезапуск процессов - Автоматическое восстановление после падения ноды (Mnesia) - **Улучшенная обработка ошибок:** - Валидация входных данных на всех критических эндпоинтах административного API. - Единый формат ошибок: {"error": "код", "message": "описание"}. ### 3.3. Безопасность - Раздельная JWT-аутентификация: - Пользовательские токены: секрет JWT_SECRET, audience = <<"user">>, эндпоинт /v1/login - Административные токены: секрет ADMIN_JWT_SECRET, audience = <<"admin">>, эндпоинт /v1/admin/login - Проверка ролей: superadmin, moderator, support - HTTPS / WSS (самоподписанный сертификат для dev, реальный для prod) - Argon2 для хеширования паролей (erlang-argon2) - Refresh token реализован (хранение в Mnesia, сессии) - OAuth2 для Google (опционально) - При блокировке пользователя или отклонении сущности обязательно сохранять причину (поле reason) - **CORS:** Все ответы API включают заголовки: - access-control-allow-origin: * - access-control-expose-headers: Content-Range ### 3.4. Наблюдаемость - Healthcheck эндпоинт (GET /health) - Логирование (JSON, ротация) - Prometheus метрики (реализованы на уровне приложения через prometheus_cowboy) - Аудит действий администраторов (запись admin_audit) ### 3.5. CI/CD - Drone CI (или GitLab CI / GitHub Actions) - EUnit + Common Test + Tsung - Сборка релиза - Docker-образ - Горячее обновление (rolling upgrade) кластера (3+ нод) - Миграции Mnesia ## 4. СТЕК ТЕХНОЛОГИЙ (С ВЕРСИЯМИ) - Бэкенд: Erlang/OTP 28.2 - Сборка: rebar3 3.27.0 - HTTP/WebSocket: Cowboy 2.10.0 - JSON: jsx 3.1.0 - JWT: jose 1.11.10 - Хеширование паролей: erlang-argon2 1.2.0 - Тестирование: meck 0.9.2, gun 2.0.0 - Мониторинг: prometheus_cowboy 0.2.0 - Нагрузочное тестирование: Tsung 1.8.0 - CI/CD: Drone 2.0+ - Контейнеризация: Docker 20.10+ - База данных: Mnesia (встроенная, распределённая) ## 5. ИЕРАРХИЧЕСКАЯ СТРУКТУРА КОДА ``` src/ test/ ├── infra/ ├── unit/ # EUnit тесты │ ├── eventhub_auth.erl # JWT └── api/ # Common Test интеграционные тесты │ └── ... ├── core/ # слой доступа к данным (DAO) ├── logic/ # бизнес-логика ├── services/ # внешние сервисы ├── handlers/ # обработчики HTTP/WebSocket │ ├── admin/ # административные обработчики │ └── ... └── middlewares/ # промежуточные слои (CORS, аутентификация) ``` **Правила:** - Модули handlers/ не содержат бизнес-логики. - Модули logic/ не знают о HTTP/WS. - Модули core/ (DAO) работают только с Mnesia. - Модуль infra/eventhub_auth.erl реализует раздельную JWT-аутентификацию (пользователи и администраторы). - Модули services/ могут быть заменены на реальные реализации без изменения остального кода. - Заголовочный файл include/records.hrl содержит все записи таблиц Mnesia. ## 6. ОСНОВНЫЕ API (КРАТКО) ### Пользовательские (порт 8080) - **Публичные:** GET /health, POST /v1/register, POST /v1/login, POST /v1/refresh - **Авторизованные:** GET /v1/user/me, GET /v1/user/bookings, GET /v1/user/reviews, GET /v1/search - **Календари:** POST /v1/calendars + CRUD, GET /v1/calendars/:id, PUT /v1/calendars/:id, DELETE /v1/calendars/:id - **События:** POST /v1/calendars/:calendar_id/events, GET /v1/events/:id, PUT /v1/events/:id, DELETE /v1/events/:id, GET /v1/events/:id/occurrences, POST /v1/events/:id/join, POST /v1/events/:id/confirm/:user_id, GET /v1/events/:id/bookings - **Бронирования:** GET /v1/bookings/:id, PUT /v1/bookings/:id, DELETE /v1/bookings/:id - **Отзывы:** POST /v1/reviews, GET /v1/reviews/:id, PUT /v1/reviews/:id, DELETE /v1/reviews/:id - **Жалобы:** POST /v1/reports - **Тикеты:** POST /v1/tickets, GET /v1/tickets, GET /v1/tickets/:id, PUT /v1/tickets/:id, DELETE /v1/tickets/:id - **Подписки:** GET /v1/subscription, POST /v1/subscription - **Метрики:** GET /metrics/[:registry] WebSocket (порт 8081): ws://localhost:8081/ws?token= ### Административные (порт 8445) - **Версионирование:** Все административные эндпоинты имеют префикс /v1/admin/. - **Базовые:** GET /v1/admin/health, GET /v1/admin/stats, POST /v1/admin/login - **Пользователи:** GET /v1/admin/users, GET /v1/admin/users/:id, PUT /v1/admin/users/:id, DELETE /v1/admin/users/:id - **Отчёты (жалобы):** GET /v1/admin/reports, GET /v1/admin/reports/:id, PUT /v1/admin/reports/:id - **Отзывы:** GET /v1/admin/reviews/:id, PUT /v1/admin/reviews/:id (hide/show) - **Бан-слова:** GET /v1/admin/banned-words, POST /v1/admin/banned-words, DELETE /v1/admin/banned-words/:word - **Тикеты:** GET /v1/admin/tickets, POST /v1/admin/tickets, GET /v1/admin/tickets/:id, PUT /v1/admin/tickets/:id, DELETE /v1/admin/tickets/:id, GET /v1/admin/tickets/stats - **Подписки:** GET /v1/admin/subscriptions, POST /v1/admin/subscriptions, GET /v1/admin/subscriptions/:id, PUT /v1/admin/subscriptions/:id, DELETE /v1/admin/subscriptions/:id *- *Модерация:** PUT /v1/admin/:target_type/:id (target_type: calendar, event, review, user; action: freeze/unfreeze, hide/show, block/unblock) Административный WebSocket (порт 8446): wss://localhost:8446/admin/ws?token= ## 6.1. Аутентификация и авторизация Модуль infra/eventhub_auth.erl реализует раздельную JWT-аутентификацию: - Пользовательские токены выпускаются через POST /v1/login с audience <<"user">> и секретом JWT_SECRET. Проверяются обработчиками через handler_auth:authenticate/1 → logic_auth:verify_jwt/1. - Административные токены выпускаются через POST /v1/admin/login с audience <<"admin">> и секретом ADMIN_JWT_SECRET. Проверяются обработчиками через handler_auth:authenticate/1 с последующей проверкой роли is_admin/1. Обработчики порта 8445 используют единый middleware handler_auth:authenticate/1, который извлекает токен из заголовка Authorization: Bearer ..., верифицирует его через logic_auth:verify_jwt/1 и возвращает {ok, UserId, Req} или {error, Code, Message, Req}. ## 7. ВЕРСИОНИРОВАНИЕ И СТАТУС Текущая версия: 1.4 (MVP, альфа). Включает: - Гибридную модель повторяющихся событий - Раздельную JWT-аутентификацию (пользователи/администраторы) - Полноценную ролевую модель администрирования - Версионированное админ-API (/v1/admin/...) - Расширенную инфраструктуру (Traefik, WAF, мониторинг, failover) - Аудит действий администраторов - Расширенную статистику для дашборда - Улучшенную обработку ошибок и валидацию ## 8. ОГРАНИЧЕНИЯ И ДОПУЩЕНИЯ - В разработке используются самоподписанные SSL-сертификаты. - Для production требуется реальный сертификат и настройка OAuth2 для Google. - Заглушки внешних сервисов должны быть заменены перед запуском. - Автоматическое обнаружение узлов через libcluster находится в перспективе; текущая реализация использует статический JOIN_NODES с возможностью подключения через DNS-лукап. ## 9. ТРЕБОВАНИЯ К ОКРУЖЕНИЮ - Erlang/OTP 28.2 - rebar3 3.27.0 - openssl - Docker (опционально) - Drone (опционально, для CI/CD)