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’ов:
- Passport-обёртки — тонкие классы, наследующие
AuthGuard(STRATEGY_NAME)из@nestjs/passport. Вся логика проверки JWT вынесена в passport-стратегии соответствующих приложений; guard лишь указывает имя стратегии (константа изconstants/token.constants.ts). - Самостоятельные guard’ы (
CanActivate) —CustomerGuard,EmployeeGuard— вручную извлекают Bearer-токен, верифицируют его черезJwtServiceи проверяют наличие сущности/токена в БД. - Фабрики-миксины для проверки ролей —
RoleGuard(...),EmployeeRoleGuard(...). - Платформенные guard’ы —
OperatorGuard,AdministratorGuard,ArbiterGuard— проверяют claimplatformRoleв уже разобранном 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).
Платформенные guard’ы
Заголовок раздела «Платформенные guard’ы»guards/platform-role.guard.ts объявляет абстрактный PlatformRoleGuard, а три наследника задают
только своё значение роли:
| Guard | Требуемый platformRole | Рабочее место |
|---|---|---|
OperatorGuard | OPERATOR | оператор: подготовка решений, верификация сдач, первая линия консьержа |
AdministratorGuard | ADMIN | администратор: утверждение решений, деньги, мэтчинг, KYC |
ArbiterGuard | ARBITER | арбитр: разбор споров |
Как устроены:
- Аутентификацию не делают. Ставятся строго в паре и после
AdminAccessTokenGuard, который кладёт payload токена вrequest.user. Отсутствиеrequest.userтрактуется как неправильно собранная цепочка guard’ов и даёт403, а не падение на чтении поля. - Сравнивают ровно одно значение —
request.user.platformRoleс требуемым. Несовпадение —403 Forbiddenс текстомPlatform role <ROLE> is required. - Тип
PlatformRole('ADMIN' | 'OPERATOR' | 'ARBITER') объявлен локально, а не импортирован изTokenPayloadauth-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.
Decorators
Заголовок раздела «Decorators»| Декоратор | Назначение |
|---|---|
@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 (PrismaExceptionFilter)
Заголовок раздела «Filters (PrismaExceptionFilter)»filters/prisma-exception.filter.ts — глобальный фильтр, наследующий BaseExceptionFilter. Ловит Prisma.PrismaClientKnownRequestError и Prisma.PrismaClientValidationError (декоратор @Catch(...)) и превращает низкоуровневые ошибки Prisma в корректные HTTP-ответы. Из текста ошибки удаляются переносы строк.
| Код Prisma | HTTP-статус | Ответ |
|---|---|---|
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 } |
Helpers
Заголовок раздела «Helpers»Чистые функции без состояния 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 (MapperService)
Заголовок раздела «Services (MapperService)»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
Заголовок раздела «Validators»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-миграции — менять их можно только вместе с миграцией.