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

Shared-библиотека (libs/apis/shared)

libs/apis/shared — общая библиотека, переиспользуемая всеми backend-приложениями монорепозитория (auth-api, user-api, admin-api, project-api, ai-agent-service). Содержит кросс-сервисные строительные блоки уровня NestJS/HTTP: guards аутентификации и авторизации, param-декораторы, глобальный exception-фильтр Prisma, чистые helper-функции (пагинация, хеширование, пароли, cookie, токены, slug и т.д.), сервис-маппер на базе class-transformer, кастомный class-validator и общие типы/константы.

Импортируется через алиас @crewsforge-back/apis/shared (barrel-экспорт из src/index.ts).

Структура:

libs/apis/shared/src/lib/
├── constants/ # имена стратегий/cookie токенов, роли (включая платформенные)
├── decorators/ # param-декораторы (@GetTokenPayload, @HeaderFingerprint, ...) и @Trim
├── filters/ # PrismaExceptionFilter
├── guards/ # customer/employee/admin access+refresh, role, employee-role, платформенные роли
├── helpers/ # чистые функции: cookie, date, hash, json, paginate, password, random, slug, token
├── services/ # MapperService (class-transformer)
├── types/ # Role, AdminTokenPayload, Any, Dictionary, Env
└── validators/ # @Match (class-validator)

В библиотеке четыре вида guard’ов:

  1. Passport-обёртки — тонкие классы, наследующие AuthGuard(STRATEGY_NAME) из @nestjs/passport. Вся логика проверки JWT вынесена в passport-стратегии соответствующих приложений; guard лишь указывает имя стратегии (константа из constants/token.constants.ts).
  2. Самостоятельные guard’ы (CanActivate) — CustomerGuard, EmployeeGuard — вручную извлекают Bearer-токен, верифицируют его через JwtService и проверяют наличие сущности/токена в БД.
  3. Фабрики-миксины для проверки ролей — RoleGuard(...), EmployeeRoleGuard(...).
  4. Платформенные guard’ыOperatorGuard, AdministratorGuard, ArbiterGuard — проверяют claim platformRole в уже разобранном payload’е.
GuardЧто защищаетКак работает
CustomerAccessTokenGuardЭндпоинты клиента (customer), доступ по access-токенуAuthGuard('CustomerAccessToken') — делегирует passport-стратегии
CustomerRefreshTokenGuardОбновление токенов клиентаAuthGuard('CustomerRefreshToken')
EmployeeAccessTokenGuardЭндпоинты сотрудника (employee) по access-токенуAuthGuard('EmployeeAccessToken')
EmployeeRefreshTokenGuardОбновление токенов сотрудникаAuthGuard('EmployeeRefreshToken')
AdminAccessTokenGuardЭндпоинты админа по access-токенуAuthGuard('jwt-admin-access')
AdminRefreshTokenGuardОбновление токенов админаAuthGuard('jwt-admin-refresh')
CustomerGuardАльтернативная ручная проверка клиентаИзвлекает Bearer через ExtractJwt, jwtService.verify секретом JwtCustomerConfigService.accessSecretKey, требует payload.customerId, проверяет наличие customer + связанного user в БД (PrismaClientService), кладёт payload в request.user; иначе UnauthorizedException
EmployeeGuardАльтернативная ручная проверка сотрудникаАналогично: verify секретом JwtEmployeeConfigService.accessSecretKey, требует payload.employeeId, дополнительно проверяет существование токена в БД через TokenService.findOneByAccessTokenId(jti, userId)
RoleGuard(...roles)Ролевая авторизация (миксин)Фабрика возвращает CanActivate, который читает request.user.roles и требует пересечения с переданными ролями. Не проверяет сам токен — ставится после access-guard’а
EmployeeRoleGuard(...roles)Ролевая авторизация сотрудникаМиксин наследует EmployeeAccessTokenGuard: сначала super.canActivate (проверка токена), затем roles.some(r => request.user.roles.includes(r))
OperatorGuard / AdministratorGuard / ArbiterGuardРабочие места бэк-офисаНаследники PlatformRoleGuard: сверяют request.user.platformRole с требуемой ролью, иначе 403. Ставятся после AdminAccessTokenGuard

Типы ролей для фабрик берутся из enum Role { CUSTOMER, EMPLOYEE } (types/role.type.ts).

guards/platform-role.guard.ts объявляет абстрактный PlatformRoleGuard, а три наследника задают только своё значение роли:

GuardТребуемый platformRoleРабочее место
OperatorGuardOPERATORоператор: подготовка решений, верификация сдач, первая линия консьержа
AdministratorGuardADMINадминистратор: утверждение решений, деньги, мэтчинг, KYC
ArbiterGuardARBITERарбитр: разбор споров

Как устроены:

  • Аутентификацию не делают. Ставятся строго в паре и после AdminAccessTokenGuard, который кладёт payload токена в request.user. Отсутствие request.user трактуется как неправильно собранная цепочка guard’ов и даёт 403, а не падение на чтении поля.
  • Сравнивают ровно одно значениеrequest.user.platformRole с требуемым. Несовпадение — 403 Forbidden с текстом Platform role <ROLE> is required.
  • Тип PlatformRole ('ADMIN' | 'OPERATOR' | 'ARBITER') объявлен локально, а не импортирован из TokenPayload auth-api: shared не имеет права зависеть от провайдерной библиотеки — обратный импорт дал бы цикл.

Совмещение двух платформенных ролей одним человеком запрещено, и guard об этом не знает: отказ происходит раньше — на выдаче токена (resolvePlatformRoleOrFail в auth-api/features/token) и на уровне БД (частичный уникальный индекс, см. database-schema). Значения claim’а и адресаты задач очереди (PlatformRole из Prisma, где администратор называется ADMINISTRATOR) — разные множества; мост между ними — карта PLATFORM_ROLE_BY_ROLE_TYPE в constants/roles.constants.ts.

Применение — в admin-api.

ДекораторНазначение
@GetTokenPayload()Param-декоратор: возвращает request.user (payload токена, положенный guard’ом/стратегией)
@HeaderFingerprint()Param-декоратор: читает заголовок x-fingerprint; если отсутствует или не строка — BadRequestException
@HeaderSessionId()Param-декоратор: читает заголовок x-session-id; при отсутствии/неверном типе — BadRequestException
@Trim(options?, transformOptions?)Обёртка над class-transformer’s @Transform: тримит строковое значение DTO (start / end / both, по умолчанию both); нестроковые значения возвращает как есть

filters/prisma-exception.filter.ts — глобальный фильтр, наследующий BaseExceptionFilter. Ловит Prisma.PrismaClientKnownRequestError и Prisma.PrismaClientValidationError (декоратор @Catch(...)) и превращает низкоуровневые ошибки Prisma в корректные HTTP-ответы. Из текста ошибки удаляются переносы строк.

Код PrismaHTTP-статусОтвет
P2002 (нарушение уникальности)409 Conflict{ statusCode, message, meta }
P2025 (запись не найдена)404 Not Found{ statusCode, message: meta.cause ?? 'Record not found', error: 'Not Found' }
прочие known-request / validation ошибки400 Bad Request{ statusCode, message, meta }

Чистые функции без состояния NestJS. Экспорт через helpers/index.ts.

HelperЧто делает
getCookieOptions(maxAge)Формирует CookieOptions (httpOnly, secure, sameSite: 'none', expires через dayjs)
getAuthCookieOptions(maxAge)Обёртка над getCookieOptions для auth-cookie (refresh-токены)
createDate(date?)Обёртка dayjs(date) — единая точка работы с датами
createHashSync(data, algorithm='md5')Синхронный hex-хеш через node:crypto (по умолчанию md5)
parseJson(value, defaultValue=null)Безопасный JSON.parse; при ошибке возвращает defaultValue
getPaginate({ page, perPage })Считает { take, skip } для Prisma-запросов из page/perPage
toPaginateResponse({ data, count, paginate })Оборачивает выборку в { data, meta } с currentPage, isFirstPage, isLastPage, pageCount, totalCount
generatePasswordHashBy(password)bcryptjs: генерирует соль (rounds=10) и хеш, возвращает { passwordHash, passwordSalt }
getRandomCode(length)Случайный код из [0-9A-Z] заданной длины (коды верификации)
getRandomNumber(min, max)Случайное целое в диапазоне (нормализует перепутанные границы)
createSlug(string)slugify (lower, -, trim, strict)
extractTokenFromAuthHeader(req)Достаёт токен из заголовка Authorization, срезая Bearer
extractEmployeeAccessTokenFromCookieOrAuthHeader(req)Токен из cookie EmployeeAccessToken, иначе из заголовка
extractEmployeeRefreshTokenFromCookieOrAuthHeader(req)Из cookie EmployeeRefreshToken, иначе из заголовка
extractCustomerAccessTokenFromCookieOrAuthHeader(req)Из cookie CustomerAccessToken, иначе из заголовка
extractCustomerRefreshTokenFromCookieOrAuthHeader(req)Из cookie CustomerRefreshToken, иначе из заголовка
extractAdminRefreshTokenFromCookieOrAuthHeader(req)Из cookie AdminRefreshToken, иначе из заголовка

services/mapper.service.ts — инъектируемый сервис, инкапсулирующий паттерн маппинга сущность → response-DTO через class-transformer.

  • toResponse(source, DtoClass) — сначала «схлопывает» источник через JSON.parse(JSON.stringify(...)) (убирает прокси/инстансы Prisma), затем plainToInstance с strategy: 'excludeAll' и excludeExtraneousValues: true. То есть в DTO попадают только поля, помеченные @Expose() — по умолчанию всё исключается. Это защищает от утечки внутренних полей (паролей, соли и т.п.).
  • toArrayResponse(sources[], DtoClass) — то же для массива.
  • toPaginateResponse(paginated, DtoClass) — маппит data и прокидывает meta из toPaginateResponse-helper’а.

validators/match.validator.ts — кастомный class-validator декоратор @Match(property, options?) с MatchConstraint. Проверяет, что значение поля равно значению другого свойства того же DTO (value === object[property]). Типичное применение — подтверждение пароля (password / passwordConfirm).

  • Guards навешиваются на контроллеры приложений через @UseGuards(...). Passport-обёртки работают в паре со стратегиями, зарегистрированными в конкретном приложении; секреты приходят из соответствующих jwt-конфигов (JwtCustomerConfigService, JwtEmployeeConfigService, JwtAdminConfigService).
  • @GetTokenPayload(), @HeaderFingerprint(), @HeaderSessionId() используются в контроллерах auth/user для получения payload’а и идентификации устройства/сессии.
  • MapperService используется в feature-сервисах для преобразования Prisma-сущностей в безопасные response-DTO.
  • Helpers (getPaginate / toPaginateResponse, generatePasswordHashBy, getRandomCode, cookie- и token-extract функции) вызываются в auth- и user-фичах.
  • PrismaExceptionFilter регистрируется как глобальный фильтр в bootstrap приложений, централизуя обработку ошибок БД.

Константы имён стратегий/cookie (constants/token.constants.ts) и роли (constants/roles.constants.ts) — единый источник истины, общий для guard’ов, helper’ов, стратегий и сидов. В roles.constants.ts лежат детерминированные id платформенных ролей (ADMIN_ROLE_ID, OPERATOR_ROLE_ID, ARBITER_ROLE_ID), набор PLATFORM_ROLE_TYPES и карта PLATFORM_ROLE_BY_ROLE_TYPE. Те же id зашиты в предикат частичного индекса baseline-миграции — менять их можно только вместе с миграцией.