Ролевая модель и аудит #6

Closed
opened 2026-04-26 22:38:01 +03:00 by aleksey · 1 comment
Owner
  • Автосоздание суперадмина при первой инициализации базы
    При старте приложения проверять, есть ли в таблице users хотя бы одна запись. Если пользователей нет — автоматически создавать учётную запись с ролью superadmin, используя параметры из .env:

    • ADMIN_EMAIL
    • ADMIN_PASSWORD
    • (опционально) ADMIN_USERNAME (по умолчанию admin).
      Никогда не перезаписывать существующего пользователя.
  • Добавить миграцию для поля role в таблице users
    Сейчас роль хранится как <<"admin">>. Нужно зарезервировать значения superadmin, moderator, support и обновить существующих пользователей.

  • Реализовать middleware проверки ролей
    В каждом админском обработчике проверять JWT и извлекать роль. Возвращать 403 при недостаточности прав (например, support не может менять роли).

  • Эндпоинт GET /v1/admin/me
    Возвращает { id, email, role, permissions } для текущего администратора. Нужен для динамического скрытия элементов в UI.

  • Управление ролями (только для superadmin)

    • GET /v1/admin/admins – список всех админов с ролями.
    • PUT /v1/admin/admins/:id – изменение роли.
    • POST /v1/admin/admins – приглашение нового админа (с отправкой email).
  • Аудит действий администраторов

    • Таблица admin_audit (admin_id, email, role, action, entity_type, entity_id, timestamp, ip, reason).
    • Запись при каждом изменении (блокировка, модерация, смена роли).
    • GET /v1/admin/audit с фильтрами (по дате, админу, действию) – доступен только superadmin.

https://git.sabilin.com/EventHub/EventHubBack/raw/branch/master/src/eventhub_app.erl

- [x] **Автосоздание суперадмина при первой инициализации базы** При старте приложения проверять, есть ли в таблице `users` хотя бы одна запись. Если пользователей нет — автоматически создавать учётную запись с ролью `superadmin`, используя параметры из `.env`: - `ADMIN_EMAIL` - `ADMIN_PASSWORD` - (опционально) `ADMIN_USERNAME` (по умолчанию `admin`). Никогда не перезаписывать существующего пользователя. - [x] **Добавить миграцию для поля `role` в таблице `users`** Сейчас роль хранится как `<<"admin">>`. Нужно зарезервировать значения `superadmin`, `moderator`, `support` и обновить существующих пользователей. - [x] **Реализовать middleware проверки ролей** В каждом админском обработчике проверять JWT и извлекать роль. Возвращать `403` при недостаточности прав (например, `support` не может менять роли). - [x] **Эндпоинт `GET /v1/admin/me`** Возвращает `{ id, email, role, permissions }` для текущего администратора. Нужен для динамического скрытия элементов в UI. - [x] **Управление ролями (только для `superadmin`)** - `GET /v1/admin/admins` – список всех админов с ролями. - `PUT /v1/admin/admins/:id` – изменение роли. - `POST /v1/admin/admins` – приглашение нового админа (с отправкой email). - [x] **Аудит действий администраторов** - Таблица `admin_audit` (admin_id, email, role, action, entity_type, entity_id, timestamp, ip, reason). - Запись при каждом изменении (блокировка, модерация, смена роли). - `GET /v1/admin/audit` с фильтрами (по дате, админу, действию) – доступен только `superadmin`. [https://git.sabilin.com/EventHub/EventHubBack/raw/branch/master/src/eventhub_app.erl](https://git.sabilin.com/EventHub/EventHubBack/raw/branch/master/src/eventhub_app.erl)
aleksey added the Future label 2026-04-26 22:38:01 +03:00
Author
Owner

🧩 Этап 0: Вынесение администраторов в отдельную таблицу
Цели:

Изолировать учётные записи администраторов от обычных пользователей.

Создать отдельную таблицу admin_session для административных сессий (refresh-токены).

Ограничить возможные роли обычных пользователей до user и bot.

Обеспечить обратную совместимость с существующим API регистрации и входа.

Локализация: include/records.hrl, infra_mnesia.erl, core_user.erl, core_admin.erl, eventhub_auth.erl, handler_login.erl, admin_handler_login.erl, handler_register.erl.

🧩 Этап 1: Обновление records.hrl
Добавляем две новые записи и изменяем поле role в #user{}.

erlang
% Вместо старого #user{role :: atom()}
-record(user, {
id :: binary(),
email :: binary(),
password_hash :: binary(),
role :: user | bot, % теперь только две роли
status :: active | blocked | deleted,
created_at :: calendar:datetime(),
updated_at :: calendar:datetime()
}).

% Новая запись для администраторов
-record(admin, {
id :: binary(),
email :: binary(),
password_hash :: binary(),
role :: superadmin | moderator | support,
status :: active | blocked,
created_at :: calendar:datetime(),
updated_at :: calendar:datetime()
}).

% Новая запись для сессий администраторов
-record(admin_session, {
token :: binary(), % refresh-токен
admin_id :: binary(),
expires_at :: calendar:datetime(),
type :: refresh
}).
🧩 Этап 2: Миграция данных при старте приложения
В infra_mnesia:init_tables/0:

Создаём таблицы admin и admin_session (как ram_copies + disc_copies).

Добавляем функцию миграции:

Если таблица users существует и в ней есть записи с role = admin (или superadmin, moderator, support), то переносим их в таблицу admin, а из users удаляем.

У обычных пользователей, у которых роль была admin (ошибочно), заменяем на user.

Логируем количество перенесённых записей.

Код миграции (псевдокод):

erlang
migrate_admins() ->
Users = mnesia:dirty_match_object(#user{_ = '_'}),
lists:foreach(fun(U) ->
case lists:member(U#user.role, [admin, superadmin, moderator, support]) of
true ->
Admin = #admin{...}, % копируем поля
mnesia:dirty_write(Admin),
mnesia:dirty_delete({user, U#user.id});
false ->
ok
end
end, Users).
🧩 Этап 3: Создание модуля core_admin.erl
Функции, аналогичные core_user, но работающие с таблицей admin:

erlang
-module(core_admin).
-export([create/3, get_by_email/1, get_by_id/1, list_all/0,
update_role/2, block/1, unblock/1]).
Реализация повторяет core_user, но использует #admin{}.

🧩 Этап 4: Изменение core_user.erl
Удаляем функции, связанные с админами (если они были, например, list_admins).

В create/3 принимаем только роли user или bot.

Функция get_by_email/1 остаётся без изменений — она нужна для пользовательского входа.

🧩 Этап 5: Адаптация аутентификации
Модуль eventhub_auth.erl:

authenticate_user_request/3 продолжает работать с core_user и logic_auth.

authenticate_admin_request/3 теперь вызывает core_admin:get_by_email(Email) вместо core_user:get_by_email.

generate_admin_token/2 использует AdminId и Role из #admin{}.

Модуль logic_auth.erl:

authenticate_user/2 теперь ищет пользователя через core_user:get_by_email, а если не найден — через core_admin:get_by_email (для обратной совместимости, пока все админы не перенесены). После полной миграции можно будет убрать этот запасной вариант.

🧩 Этап 6: Обновление обработчиков
handler_register.erl — разрешаем регистрацию только с ролями user и bot.

admin_handler_login.erl — теперь вызывает eventhub_auth:authenticate_admin_request, которая ходит в core_admin.

Все административные обработчики, которые проверяют is_admin/1, теперь должны использовать core_admin:get_by_id/1 вместо core_user. Выносим функцию is_admin/1 в admin_utils.erl, которая проверяет наличие записи в admin.

🧩 Этап 7: Обработка refresh-токенов
Для пользователей refresh-токены хранятся в таблице session (уже реализовано).

Для администраторов создаём отдельную таблицу admin_session и модуль core_admin_session.erl с функциями create/2, validate/1, delete/1.

handler_refresh.erl остаётся общим — он определяет, к кому относится токен (пользователь или админ), проверяя наличие в соответствующей таблице.

🧩 Этап 8: Тестирование
Обновляем все юнит-тесты, которые используют #user{role = admin} — заменяем на моки core_admin.

Добавляем новые тесты для core_admin, admin_session, миграции.

Интеграционные тесты (api_SUITE) должны продолжать работать без изменений, так как они используют HTTP-клиент.

Проверка: make eunit && rebar3 ct — все 526+ тестов должны остаться зелёными.

🧩 Этап 0: Вынесение администраторов в отдельную таблицу Цели: Изолировать учётные записи администраторов от обычных пользователей. Создать отдельную таблицу admin_session для административных сессий (refresh-токены). Ограничить возможные роли обычных пользователей до user и bot. Обеспечить обратную совместимость с существующим API регистрации и входа. Локализация: include/records.hrl, infra_mnesia.erl, core_user.erl, core_admin.erl, eventhub_auth.erl, handler_login.erl, admin_handler_login.erl, handler_register.erl. 🧩 Этап 1: Обновление records.hrl Добавляем две новые записи и изменяем поле role в #user{}. erlang % Вместо старого #user{role :: atom()} -record(user, { id :: binary(), email :: binary(), password_hash :: binary(), role :: user | bot, % теперь только две роли status :: active | blocked | deleted, created_at :: calendar:datetime(), updated_at :: calendar:datetime() }). % Новая запись для администраторов -record(admin, { id :: binary(), email :: binary(), password_hash :: binary(), role :: superadmin | moderator | support, status :: active | blocked, created_at :: calendar:datetime(), updated_at :: calendar:datetime() }). % Новая запись для сессий администраторов -record(admin_session, { token :: binary(), % refresh-токен admin_id :: binary(), expires_at :: calendar:datetime(), type :: refresh }). 🧩 Этап 2: Миграция данных при старте приложения В infra_mnesia:init_tables/0: Создаём таблицы admin и admin_session (как ram_copies + disc_copies). Добавляем функцию миграции: Если таблица users существует и в ней есть записи с role = admin (или superadmin, moderator, support), то переносим их в таблицу admin, а из users удаляем. У обычных пользователей, у которых роль была admin (ошибочно), заменяем на user. Логируем количество перенесённых записей. Код миграции (псевдокод): erlang migrate_admins() -> Users = mnesia:dirty_match_object(#user{_ = '_'}), lists:foreach(fun(U) -> case lists:member(U#user.role, [admin, superadmin, moderator, support]) of true -> Admin = #admin{...}, % копируем поля mnesia:dirty_write(Admin), mnesia:dirty_delete({user, U#user.id}); false -> ok end end, Users). 🧩 Этап 3: Создание модуля core_admin.erl Функции, аналогичные core_user, но работающие с таблицей admin: erlang -module(core_admin). -export([create/3, get_by_email/1, get_by_id/1, list_all/0, update_role/2, block/1, unblock/1]). Реализация повторяет core_user, но использует #admin{}. 🧩 Этап 4: Изменение core_user.erl Удаляем функции, связанные с админами (если они были, например, list_admins). В create/3 принимаем только роли user или bot. Функция get_by_email/1 остаётся без изменений — она нужна для пользовательского входа. 🧩 Этап 5: Адаптация аутентификации Модуль eventhub_auth.erl: authenticate_user_request/3 продолжает работать с core_user и logic_auth. authenticate_admin_request/3 теперь вызывает core_admin:get_by_email(Email) вместо core_user:get_by_email. generate_admin_token/2 использует AdminId и Role из #admin{}. Модуль logic_auth.erl: authenticate_user/2 теперь ищет пользователя через core_user:get_by_email, а если не найден — через core_admin:get_by_email (для обратной совместимости, пока все админы не перенесены). После полной миграции можно будет убрать этот запасной вариант. 🧩 Этап 6: Обновление обработчиков handler_register.erl — разрешаем регистрацию только с ролями user и bot. admin_handler_login.erl — теперь вызывает eventhub_auth:authenticate_admin_request, которая ходит в core_admin. Все административные обработчики, которые проверяют is_admin/1, теперь должны использовать core_admin:get_by_id/1 вместо core_user. Выносим функцию is_admin/1 в admin_utils.erl, которая проверяет наличие записи в admin. 🧩 Этап 7: Обработка refresh-токенов Для пользователей refresh-токены хранятся в таблице session (уже реализовано). Для администраторов создаём отдельную таблицу admin_session и модуль core_admin_session.erl с функциями create/2, validate/1, delete/1. handler_refresh.erl остаётся общим — он определяет, к кому относится токен (пользователь или админ), проверяя наличие в соответствующей таблице. 🧩 Этап 8: Тестирование Обновляем все юнит-тесты, которые используют #user{role = admin} — заменяем на моки core_admin. Добавляем новые тесты для core_admin, admin_session, миграции. Интеграционные тесты (api_SUITE) должны продолжать работать без изменений, так как они используют HTTP-клиент. Проверка: make eunit && rebar3 ct — все 526+ тестов должны остаться зелёными.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: EventHub/EventHubBack#6