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

Admin API — админ-панель

Слой: libs/apis/providers/admin-api · Приложение: apps/admin-api

Admin API — отдельное NestJS-приложение (backend бэк-офиса CrewsForge). Оно обслуживает три рабочих места платформы — оператора, администратора и арбитра — и публичные формы лендинга.

Администраторская поверхность:

  • просмотр и поиск пользователей, блокировка/разблокировка;
  • просмотр и поиск проектов, редактирование, мягкое удаление и восстановление;
  • просмотр команд-исполнителей с их составом и верификация команды, проходящей онбординг.

Рабочие места оператора и арбитра пока представлены только точкой входа /{роль}/me: она отвечает, кто вошёл и в какую поверхность его пустили.

Публичные формы лендинга (/contact/*) живут в том же приложении и авторизации не требуют.

Приложение поднимается на своём порту, имеет собственную пару JWT-секретов (jwt-admin) и собственный набор фич в слое libs/apis/providers/admin-api. Бизнес-логика вынесена в фичи-библиотеки, а само приложение (apps/admin-api) выполняет только bootstrap.

Технические параметры bootstrap (apps/admin-api/src/main.ts):

  • порт: process.env.ADMIN_API_PORT или 3002 по умолчанию;
  • глобальный префикс: api/v1, версионирование через URI (VersioningType.URI);
  • Swagger: api/v1/docs, заголовок «Admin API», bearer-схема jwt-admin-access;
  • глобальный ValidationPipe (whitelist, forbidNonWhitelisted, transform);
  • глобальный PrismaExceptionFilter, cookie-parser, CORS с credentials: true;
  • транзакции через nestjs-cls + ClsPluginTransactional (адаптер Prisma).

AppController / AppService содержат только заглушку GET /{ message: 'Hello API' } и не относятся к админ-функциональности.

Авторизация двухслойная: AdminAccessTokenGuard отвечает на вопрос «кто это», платформенный guard — «в какую поверхность его пускают». Оба ставятся на уровне класса контроллера: @UseGuards(AdminAccessTokenGuard, AdministratorGuard) и так далее.

AdminAccessTokenGuard (libs/apis/shared/.../guards/admin-access-token.guard.ts) — passport-AuthGuard поверх стратегии ADMIN_ACCESS_TOKEN_STRATEGY_NAME = 'jwt-admin-access'.

Стратегия AdminAccessTokenStrategy (auth-api/features/admin-auth) при каждом запросе:

  1. извлекает Bearer-токен из заголовка Authorization;
  2. проверяет подпись секретом jwt-admin.accessSecretKey;
  3. проверяет, что access-токен зарегистрирован (tokenService.findOneByAccessTokenId);
  4. проверяет, что пользователь существует и не заблокирован (isBlocked);
  5. кладёт TokenDecodePayload (в т.ч. userId) в request.user.

В admin-api пускают три платформенные роли — ADMIN, OPERATOR, ARBITER — и ровно одну на человека. Правило проходит через всю цепочку:

  1. Логин (AdminAuthService.login) требует e-mail в разрешённом домене (ADMIN_ALLOWED_DOMAINS), незаблокированный аккаунт и ровно одну платформенную роль. Отсутствие роли и совмещение двух — 403 Forbidden, в лог уходит warn без секретов.
  2. Выдача токена (AdminAuthVerificationService) кладёт в payload claim platformRole со значением ADMIN | OPERATOR | ARBITER. Значение выводится из UserOnRole, а не подставляется литералом: содержимое токена и содержимое базы расходиться не должны.
  3. Refresh (AdminAuthRefreshTokenService) перевыводит и roles, и platformRole на каждой ротации — иначе claim исчезал бы после первого обновления, а снятая роль продолжала бы жить в токене до истечения.
  4. Guard рабочего места (OperatorGuard / AdministratorGuard / ArbiterGuard) сверяет claim с требуемой ролью и отдаёт 403 при несовпадении.
  5. База отвергает вторую платформенную роль частичным уникальным индексом users_on_roles_single_platform_role — см. database-schema.

Вывод роли живёт в одной функции resolvePlatformRole / resolvePlatformRoleOrFail (auth-api/features/token), а не размазан по точкам выпуска токена.

Совмещение ролей запрещено не из аккуратности: оператор «по совместительству администратор» видел бы деньги, и разделение обязанностей (оператор готовит решение — администратор его утверждает) перестало бы что-либо значить.

Регистрация в admin-api (POST /auth/admin/register) заводит только роль ADMIN. Оператор и арбитр создаются сидом или назначением роли существующему пользователю (см. seeds).

RoleGuard из shared и enum Role работают только с CUSTOMER/EMPLOYEE и в admin-api не используются.

Чем отличается от customer / employee:

  • Отдельная пара секретов. Токены подписываются JWT_ADMIN_*-секретами, а не customer/employee-секретами — customer- или employee-токен не пройдёт AdminAccessTokenGuard.
  • Отдельные гварды/стратегии. AdminAccessTokenGuard (jwt-admin-access) против CustomerAccessTokenGuard / EmployeeAccessTokenGuard.
  • Ограничение по домену. Для админов действует allow-list доменов e-mail (ADMIN_ALLOWED_DOMAINS), которого нет у обычных пользователей.
  • Платформенные guard’ы. Разграничение внутри бэк-офиса даёт claim platformRole, которого нет ни у customer-, ни у employee-токена.

Переменные окружения jwt-admin (libs/apis/configs/auth-api/jwt-admin):

ПеременнаяНазначениеПо умолчанию / валидация
JWT_ADMIN_ACCESS_SECRET_KEYсекрет подписи admin access-токенаrequired (Joi)
JWT_ADMIN_ACCESS_EXPIRES_INTTL access-токена, сек.3600, если не число
JWT_ADMIN_REFRESH_SECRET_KEYсекрет подписи admin refresh-токенаrequired (Joi)
JWT_ADMIN_REFRESH_EXPIRES_INTTL refresh-токена, сек.7200, если не число
ADMIN_ALLOWED_DOMAINSсписок разрешённых доменов e-mail админов (через запятую)required (Joi); парсится в массив в нижнем регистре

Конфиг регистрируется через registerAs('jwt-admin', ...), доступ — через JwtAdminConfigService.

Фича (библиотека)Контроллер / базовый путьНазначение
admin-api/features/workspaceOperatorWorkspaceControlleroperator, AdministratorWorkspaceControlleradministrator, ArbiterWorkspaceControllerarbiterТочки входа трёх рабочих мест: GET /{роль}/me
admin-api/features/admin-userAdminUserControllerusersСписок/поиск пользователей, карточка пользователя, профиль текущего админа, блокировка/разблокировка
admin-api/features/admin-projectAdminProjectControllerprojectsСписок/поиск проектов, карточка, редактирование, мягкое удаление/восстановление
admin-api/features/admin-teamAdminTeamControlleradministrator/teamsСписок команд, карточка с составом, верификация онбординга
admin-api/features/admin-contactAdminContactControllercontactПубличные формы лендинга: заявка и обращение в поддержку
admin-api/data-accessОбщие константы (пагинация, префикс) и query/командные DTO

Каждая фича — модуль NestJS с тройкой Controller → Service → Repository (Prisma). Исключение — admin-team: своего репозитория у неё нет, она работает поверх TeamCoreModule из user-api и добавляет только форму ответа бэк-офиса.

Все пути ниже даны без глобального префикса. Полный URL = /api/v1/<path>. Всё, кроме /contact/*, защищено парой guard’ов: AdminAccessTokenGuard (Bearer jwt-admin-access) + guard рабочего места.

МетодПутьGuardОписаниеDTO
GET/operator/meAdminAccessTokenGuard, OperatorGuardКто вошёл в рабочее место оператораWorkspaceMeResponseDto
GET/administrator/meAdminAccessTokenGuard, AdministratorGuardТо же для администратораWorkspaceMeResponseDto
GET/arbiter/meAdminAccessTokenGuard, ArbiterGuardТо же для арбитраWorkspaceMeResponseDto

WorkspaceMeResponseDto — три поля: userId, email, platformRole. Больше здесь быть не должно: рабочее место спрашивает «кто я и куда меня пустили», а не карточку пользователя.

Роль в ответе берётся из базы, а не из claim’а: токен уже проверен guard’ом, и второй источник истины только развёл бы ответ с реальными правами. Отсутствие платформенной роли и совмещение двух — 403.

Чужое рабочее место закрыто в обе стороны: оператор на /administrator/me и /arbiter/me получает 403, администратор на /operator/me — тоже, и так для всех трёх ролей.

МетодПутьGuardОписаниеDTO
POST/contact/leadЗаявка с лендинга, 204 No ContentLeaveRequestDto
POST/contact/supportОбращение в поддержку с лендинга, 204 No ContentSupportRequestDto

Эндпоинты публичные намеренно: их вызывает лендинг, у посетителя которого нет и не может быть токена. Детальный контракт — в README библиотеки.

Оператор и арбитр на /users, /projects и /administrator/teams получают 403: это администраторская поверхность.

МетодПутьGuardОписаниеDTO
GET/usersAdminAccessTokenGuard, AdministratorGuardСписок пользователей с пагинацией и фильтрамиQuery: AdminUserQueryDto → отдаёт { data, total, page, perPage } (AdminUserResponseDto)
GET/users/meAdminAccessTokenGuard, AdministratorGuardПрофиль текущего админа (по userId из токена)AdminMeResponseDto
GET/users/:idAdminAccessTokenGuard, AdministratorGuardКарточка пользователя по UUID (с профилем, ролями, проектами)id: ParseUUIDPipe; AdminUserDetailResponseDto
PATCH/users/:id/blockAdminAccessTokenGuard, AdministratorGuardЗаблокировать пользователяid: ParseUUIDPipe; тела нет
PATCH/users/:id/unblockAdminAccessTokenGuard, AdministratorGuardРазблокировать пользователяid: ParseUUIDPipe; тела нет

Фильтры/пагинация AdminUserQueryDto:

  • page (int ≥ 1, по умолчанию 1);
  • perPage (int 1..100, по умолчанию DEFAULT_PAGE_SIZE = 10);
  • role (enum RoleType) — фильтр по роли (userOnRole.some.role.type);
  • isBlocked (boolean, из строки 'true') — фильтр по блокировке;
  • search (строка ≤ 255, @Trim) — поиск по email, регистронезависимо.
МетодПутьGuardОписаниеDTO
GET/projectsAdminAccessTokenGuard, AdministratorGuardСписок проектов с пагинацией и фильтрами (только не удалённые)Query: AdminProjectQueryDto{ data, total, page, perPage } (AdminProjectResponseDto)
GET/projects/:idAdminAccessTokenGuard, AdministratorGuardКарточка проекта по UUID (только не удалённый)id: ParseUUIDPipe; AdminProjectResponseDto
PATCH/projects/:idAdminAccessTokenGuard, AdministratorGuardРедактирование проектаid: ParseUUIDPipe; Body: AdminProjectUpdateDto
DELETE/projects/:idAdminAccessTokenGuard, AdministratorGuardМягкое удаление (204 No Content)id: ParseUUIDPipe; adminId из токена
PATCH/projects/:id/restoreAdminAccessTokenGuard, AdministratorGuardВосстановление мягко удалённого проектаid: ParseUUIDPipe

Фильтры/пагинация AdminProjectQueryDto:

  • page (int ≥ 1, по умолчанию 1);
  • perPage (int 1..100, по умолчанию 10);
  • status (enum ProjectStatus);
  • category (enum ProjectCategoryType);
  • search (строка ≤ 255, @Trim) — поиск по title и description, регистронезависимо.

AdminProjectUpdateDto (тело PATCH /projects/:id): title? (3..255), description? (≥ 3), category? (ProjectCategoryType) — все поля опциональны. Поля status в теле нет.

МетодПутьGuardОписаниеDTO
GET/administrator/teamsAdminAccessTokenGuard, AdministratorGuardСписок команд с пагинацией, новые сверхуQuery: AdminTeamQueryDto{ data, meta } (AdminTeamResponseDto)
GET/administrator/teams/:teamIdAdminAccessTokenGuard, AdministratorGuardКарточка команды вместе с составомteamId: ParseUUIDPipe; AdminTeamDetailResponseDto
POST/administrator/teams/:teamId/verifyAdminAccessTokenGuard, AdministratorGuardВерифицировать команду, проходящую онбординг (201)teamId: ParseUUIDPipe; тела нет; AdminTeamResponseDto

Базовый путь контроллера начинается с administrator/ — маршруты скоупятся рабочим местом, как и остальные его поверхности; оператор и арбитр получают здесь 403.

Фильтры/пагинация AdminTeamQueryDto:

  • page (int ≥ 1);
  • perPage (int 1..MAX_PAGE_SIZE);
  • status (enum TeamStatus).

Значения по умолчанию (page = 1, perPage = 10) ставит домен в TeamService, второго набора дефолтов в admin-api нет. Ответ списка — общий пагинационный конверт { data, meta } (MapperService.toPaginateResponse), а не { data, total, page, perPage }, как у users и projects.

AdminTeamResponseDto: id, createdAt, updatedAt, name, slug, bio, status, externalExperience, verifiedAt. AdminTeamDetailResponseDto добавляет membersAdminTeamMemberResponseDto с employeeId, role, canSign, canSubmit, status, removedAt. Карточки сотрудников в составе нет: их бэк-офис берёт в своей поверхности по employeeId.

Блокировка пользователя (PATCH /users/:id/block, .../unblock)

Заголовок раздела «Блокировка пользователя (PATCH /users/:id/block, .../unblock)»

AdminUserService.blockUser(id, adminId):

  1. загружает пользователя вместе с ролями (findOneById(id, true)); если нет — 404 NotFound;
  2. если у пользователя есть роль ADMIN — операция запрещена (403 Forbidden, «Cannot block a user with ADMIN role»), в лог пишется admin.user.block_admin_rejected;
  3. если пользователь уже заблокирован — 400 BadRequest;
  4. иначе update(id, { isBlocked: true }), лог admin.user.blocked, ответ { message: 'User has been blocked' }.

unblockUser(id) — зеркально: 404, если пользователя нет; 400, если пользователь не заблокирован; иначе isBlocked: false и { message: 'User has been unblocked' }. Флаг isBlocked затем блокирует вход и проверяется в token-стратегиях (заблокированный пользователь получает 401).

Примечание: BlockUserDto (data-access/.../block-user.dto.ts) с полем isBlocked определён, но текущие эндпоинты его не используют — блокировка задаётся самим путём (/block vs /unblock) без тела.

Смены статуса проекта в API нет — ни отдельным маршрутом, ни полем в теле PATCH /projects/:id. Прежняя матрица переходов PROJECT_STATUS_TRANSITIONS и поле status в AdminProjectUpdateDto сняты вместе с маршрутом PATCH /api/v1/customers/me/projects/:projectId/status в project-api.

Это ломающее изменение API: клиент, дёргавший смену статуса, получит 400 на неизвестное поле (forbidNonWhitelisted) либо 404 на снятом маршруте.

Причина: ProjectStatus переработан (10 значений вместо 8, плюс подстадия contractingStage), и переход между ними — не свойство одного сервиса, а машина состояний с журналом StateTransition, актором и основанием. Проектная модель переходов — step-3-state-machines.md. AdminProjectService.updateOneById теперь только проверяет существование проекта и сохраняет переданные поля.

Верификация команды (POST /administrator/teams/:teamId/verify)

Заголовок раздела «Верификация команды (POST /administrator/teams/:teamId/verify)»

Команда создаётся в статусе ONBOARDING и до проверки администратором остаётся в нём. Верификация переводит её в ACTIVE и штампует verifiedAt — это отметка платформы «команду посмотрели», по которой команда становится видимой дальнейшим сценариям.

Правила — в домене (TeamService.verifyOneByIdOrFail в user-api), admin-api их не дублирует:

  • команды нет — 404;
  • команда не в ONBOARDING409: повторная верификация затёрла бы дату первой, а другого источника verifiedAt нет;
  • иначе status = ACTIVE, verifiedAt = now, в лог уходит team verified.

Статус пишется напрямую: журнала StateTransition, outbox и обратных переходов у команды пока нет. ONBOARDING → ACTIVE — единственный реализованный переход, SUSPENDED и ARCHIVED недостижимы. Подробности домена — в user-api.

Границы бэк-офиса по командам на сегодня:

  • верификация онбординга — единственная операция админ-панели над командой; состав команды доступен только на чтение (GET /administrator/teams/:teamId);
  • передачи роли администратора команды в бэк-офисе нет. Домен не даёт уйти последнему администратору (409), но если единственный админ недоступен — аккаунт заблокирован или просто брошен, — назначить нового некому: все операции над составом закрыты ролью ADMIN внутри самой команды, и обойти это из админ-панели нельзя. Операция придёт с единицей T2.
  • DELETE /projects/:idsoftDeleteOneById(id, adminId): проверка существования, затем deletedAt = new Date(), лог admin.project.soft_deleted, ответ 204 No Content.
  • PATCH /projects/:id/restorerestoreOneById(id): ищет проект, включая удалённые (findOneByIdIncludingDeleted); если нет — 404; иначе deletedAt = null. Все списки/карточки по умолчанию фильтруют deletedAt: null.
  • NestJS (@nestjs/common, @nestjs/core, @nestjs/platform-express, @nestjs/swagger, @nestjs/passport, passport-jwt).
  • Prisma@crewsforge-back/apis/utils/prisma-client (PrismaClientService, PrismaClientModule); модели User, Project, роли userOnRole/role.
  • Транзакции/контекстnestjs-cls, @nestjs-cls/transactional, @nestjs-cls/transactional-adapter-prisma.
  • Общие утилиты@crewsforge-back/apis/shared: AdminAccessTokenGuard, AdministratorGuard, OperatorGuard, ArbiterGuard, GetTokenPayload, AdminTokenPayload, MapperService, ADMIN_ACCESS_TOKEN_STRATEGY_NAME, PrismaExceptionFilter, декоратор @Trim, константы платформенных ролей (PLATFORM_ROLE_TYPES, ADMIN_ROLE_ID, OPERATOR_ROLE_ID, ARBITER_ROLE_ID).
  • Конфиг@crewsforge-back/apis/configs/shared/app (AppConfigModule, порт) и @crewsforge-back/apis/configs/auth-api/jwt-admin (JwtAdminConfigService, используется в стратегиях admin-auth).
  • Auth — стратегии jwt-admin-access/jwt-admin-refresh из auth-api/features/admin-auth; тип TokenDecodePayload из auth-api/features/token.
  • Внутренние фичиadmin-api/data-access (DTO и константы), admin-project (используется в AdminUserDetailResponseDto).
  • user-apiapis/providers/user-api/features/team (TeamCoreModule, TeamService) и apis/providers/user-api/data-access (TeamExternalCaseDto): домен команд admin-api не повторяет, а переиспользует.
  • apps/admin-api/src/main.ts — bootstrap, порт 3002, префикс api/v1, Swagger, пайпы/фильтры.
  • apps/admin-api/src/app/app.module.ts — сборка приложения: AppConfigModule, CLS/транзакции, AdminAuthStrategiesModule, AdminContactModule, AdminProjectModule, AdminTeamModule, AdminUserModule, WorkspaceModule.
  • libs/apis/providers/admin-api/features/workspace/src/lib/{operator,administrator,arbiter}-workspace.controller.ts — три точки входа /{роль}/me.
  • .../workspace/src/lib/workspace.service.ts — общий для трёх контроллеров: выводит платформенную роль пользователя из базы.
  • .../workspace/src/lib/dto/workspace-me-response.dto.ts — контракт ответа { userId, email, platformRole }.
  • libs/apis/providers/admin-api/features/admin-user/src/lib/admin-user.controller.ts — маршруты users, гвард.
  • .../admin-user/src/lib/admin-user.service.ts — блокировка/разблокировка, поиск, getAdminMe.
  • .../admin-user/src/lib/admin-user.repository.ts — Prisma-запросы по пользователям.
  • .../admin-user/src/lib/dto/*admin-me-response, admin-user-response, admin-user-detail-response, admin-user-role.
  • libs/apis/providers/admin-api/features/admin-project/src/lib/admin-project.controller.ts — маршруты projects.
  • .../admin-project/src/lib/admin-project.service.ts — редактирование, soft-delete/restore.
  • .../admin-project/src/lib/admin-project.repository.ts — Prisma-запросы по проектам (фильтр deletedAt).
  • .../admin-project/src/lib/dto/*admin-project-response, admin-project-owner, admin-project-update.
  • libs/apis/providers/admin-api/features/admin-team/src/lib/admin-team.controller.ts — маршруты administrator/teams.
  • .../admin-team/src/lib/admin-team.service.ts — обёртка над TeamService: список, карточка с составом, верификация.
  • .../admin-team/src/lib/dto/*admin-team-query, admin-team-response (карточка, состав, карточка с составом).
  • libs/apis/providers/admin-api/data-access/src/lib/constants/admin-api.constants.tsDEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, ADMIN_ROUTE_PREFIX.
  • .../data-access/src/lib/dto/*admin-user-query, admin-project-query, block-user, update-project-status.
  • libs/apis/shared/src/lib/guards/admin-access-token.guard.ts — аутентификация админ-эндпоинтов.
  • libs/apis/shared/src/lib/guards/{platform-role,operator,administrator,arbiter}.guard.ts — guard’ы рабочих мест.
  • libs/apis/configs/auth-api/jwt-admin/** — env, конфиг и сервис доступа к jwt-admin-секретам.
  • Новая поверхность рабочего места (очередь задач, разбор, спор): добавить фичу admin-api/features/<name> (Controller → Service → Repository), навесить @UseGuards(AdminAccessTokenGuard, <Operator|Administrator|Arbiter>Guard) и подключить модуль в apps/admin-api/src/app/app.module.ts. Базовый путь контроллера и есть скоуп: operator/..., administrator/..., arbiter/....
  • Новая платформенная роль: значение в RoleType (миграция), строка в PLATFORM_ROLE_TYPES и id в roles.constants.ts, пересоздание частичного индекса users_on_roles_single_platform_role миграцией, новый наследник PlatformRoleGuard, роль в сиде platform-role.seed.ts.
  • Тело для block/unblock. Готовый BlockUserDto позволяет при необходимости заменить два пути (/block, /unblock) одним PATCH /users/:id с полем isBlocked.
  • Пагинация. Все списки строятся на page/perPage из data-access; общий формат ответа { data, total, page, perPage } — удобная точка для единого пагинационного слоя. Константа MAX_PAGE_SIZE = 100 уже задана в data-access (в DTO лимит продублирован через @Max(100)).
  • Фильтры/поиск. Расширяются в service.findAll через сборку Prisma.*WhereInput — новые поля добавляются в query-DTO и в маппинг where.

  • Приложение: apps/admin-api, порт 3002 (env ADMIN_API_PORT), префикс api/v1, Swagger на api/v1/docs; логика — в фичах libs/apis/providers/admin-api.
  • Эндпоинты (users): GET /users, GET /users/me, GET /users/:id, PATCH /users/:id/block, PATCH /users/:id/unblock.
  • Эндпоинты (projects): GET /projects, GET /projects/:id, PATCH /projects/:id, DELETE /projects/:id, PATCH /projects/:id/restore; списки поддерживают page/perPage и фильтры (users: role, isBlocked, search; projects: status, category, search). Обе поверхности закрыты AdministratorGuard.
  • Авторизация: аутентификация — AdminAccessTokenGuard (passport-стратегия jwt-admin-access, отдельные секреты JWT_ADMIN_*, проверка регистрации токена и блокировки пользователя); авторизация — платформенный guard поверх claim’а platformRole.
  • Эндпоинты (рабочие места): GET /operator/me, GET /administrator/me, GET /arbiter/me — ответ { userId, email, platformRole }, чужое рабочее место даёт 403.
  • Эндпоинты (команды): GET /administrator/teams, GET /administrator/teams/:teamId, POST /administrator/teams/:teamId/verify — под AdministratorGuard; логика в TeamService из user-api, повторная верификация даёт 409.
  • Эндпоинты (лендинг): POST /contact/lead, POST /contact/support — публичные, без guard’ов.
  • Платформенная роль: claim platformRole (ADMIN | OPERATOR | ARBITER) выводится из UserOnRole при каждом выпуске пары токенов, включая refresh; совмещение двух ролей отвергается и на логине (403), и уникальным индексом БД.
  • Ключевые операции: блокировка пользователя (запрет блокировать ADMIN, защита от повторной блокировки), редактирование проекта, мягкое удаление и восстановление (deletedAt). Смены статуса проекта в API нет.
  • Заготовка: BlockUserDto определён в data-access, но текущими контроллерами не используется. UpdateProjectStatusDto там же остался без потребителя.