Skip to Content
ПродвинутыйПринцип единой ответственностиПример с AuthGuard

Пример с AuthGuard

Возьмём пример из feeldown-backend 

С первого взгляда кажется, что класс перенасыщен, проверка метаданных, hash-сервис, парсинг slug-а, однако на самом деле этот класс делегирует задачи профильным утилитам.

  1. Делегирование задач
  • AuthGuardService выполняет всю работу по валидации запроса, а наш guard просто вызывает this.service.validateRequest(request).
  • HashService выполняет роль парсера токена и всех операций, связанных с криптографией и токенами.
  • SlugPipe парсит сложные slug-и.
  1. Разделение логики
  • validateMetadata валидирует все метаданные, проверяет прав доступа, разделяем основную работу и второстепенную.
  • validateOnlyMe отдельная большая валидация OnlyMe-метаполя.
  • validateUsername просто защита от дублирования кода.
  1. Использование tryCatchThrow, чтобы избавиться от больших повторяющихся try-catch-блоков.

AuthGuard следует SRP не потому, что он маленький, а потому что его единственная ответственность — это управление потоком аутентификации. (решить: пропустить запрос или нет). Всё остальное — это детали, которые он делегирует.

Что плохо

  1. validateOnlyMe можно было вынести в отдельный Guard или в отдельный сервис, чтобы Guard занимался только пропуском
  2. Смешивание ответственности:
  • AuthGuard проверяет, авторизован ли пользователь и имеет ли он доступ к конкретному ресурсу (Authentication и Authorization), лучше всего разделить его два два Guard-а.
  1. validateMetadata валидирует все метаданные в одном месте через условия, что нарушает open-closed принцип, лучше всего сделать MetadataHandler, чтобы не изменять метод, а добавлять класс и изменять только массив всех метаданных.
  2. SlugPipe в Guard-е недопустим, потому что он трансформирует данные, что Guard делать не должен, лучше всего попробовать передать уже спрасированные данные или вынести парсер в отдельный сервис.

Однако

«Однако» — несмотря на выявленные недостатки, AuthGuard всё же демонстрирует главную идею SRP: класс должен иметь только одну причину для изменения. В текущей реализации такой причиной является управление потоком аутентификации (пропустить или отклонить запрос). Всё, что выходит за эти рамки (парсинг slug, криптография, проверка метаданных), уже делегировано, пусть и не идеально.

Настоящая проблема — не в нарушении SRP как такового, а в смешении уровней абстракции и нарушении Open/Closed Principle. Если мы выделим OnlyMeGuard и MetadataHandlerChain, то AuthGuard станет эталонным примером SRP: его единственная задача — скомпоновать результаты проверок и принять решение.

Запомните: SRP — это не про размер класса или количество методов. Это про направление изменений. Если изменение требований к аутентификации заставляет менять код парсинга slug — значит, ответственность размазана. Если же вы можете добавить новый тип авторизации, не трогая AuthGuard, — вы на верном пути.

Улучшенный код

only-me.guard.ts

import type { ExecutionContext } from "@nestjs/common"; import type { OnlyMeMetadata } from "@types"; import type { Request } from "express"; import { Injectable, CanActivate, ExecutionContext, ForbiddenException, } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { Metadata } from "@/enums"; import { OnlyMeGuardService } from "./only-me-guard.service"; @Injectable() export class OnlyMeGuard implements CanActivate { public constructor( private readonly reflector: Reflector, private readonly service: OnlyMeGuardService, private readonly serverUserService: ServerUserService, ) {} public async canActivate(context: ExecutionContext): Promise<boolean> { const onlyMeMetadata = this.reflector.get<OnlyMeMetadata>( Metadata.isOnlyMe, context.getHandler(), ); if (!onlyMeMetadata) { return true; } const request = context.switchToHttp().getRequest<Request>(); const user = this.serverUserService.getByRequest(request); if (!user) { throw new ForbiddenException("User not authenticated"); } const validated = await this.service.execute( user, onlyMeMetadata, request.params, ); if (!validated) { throw new ForbiddenException("You are not the owner of this resource"); } return true; } } export const ONLY_ME_GUARD_PROVIDERS = [ OnlyMeGuardService, ServerUserService, PrismaService, ] as const; export default OnlyMeGuard;

auth.guard.ts

import type { ExecutionContext } from "@nestjs/common"; import type { Request } from "express"; import { Injectable, CanActivate } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { Metadata } from "@/enums"; import { AuthGuardService } from "./auth-guard.service"; @Injectable() export class AuthGuard implements CanActivate { public constructor( private readonly reflector: Reflector, private readonly service: AuthGuardService, private readonly serverUserService: ServerUserService, ) {} public async canActivate(context: ExecutionContext): Promise<boolean> { const handler = context.getHandler(); const isPublic = this.reflector.get<boolean>(Metadata.isPublic, handler); if (isPublic) { return true; } const skipAuthGuard = this.reflector.get<boolean>( Metadata.skipAuthGuard, handler, ); if (skipAuthGuard) { return true; } const request = context.switchToHttp().getRequest<Request>(); const user = await this.service.execute(request); if (!user) { throw new UnauthorizedException("Invalid credentials"); } this.serverUserService.setByRequest(request, user); return true; } } export const AUTH_GUARD_PROVIDERS = [ AuthGuardService, ServerUserService, PrismaService, ] as const; export default AuthGuard;

Примечание: здесь использовано упрощение. По-хорошему для каждой метадаты использовано свой класс, однако в данном случае это может оказаться переусложнение кода.

Здесь можно было бы применить паттерн “Цепочка обязанностей”, но для трёх условий мы пока оставляем так, чтобы не переусложнять

Пример в контроллере:

@Controller("posts") export class PostsController { @Public() @Get("public") public get() { /* ... */ } @UseGuards(AuthGuard) @Post() public post(@Body() post: CreatePostDto) { /* ... */ } @UseGuards(AuthGuard, OnlyMeGuard) @Delete(ROUTES.DELETE) public delete(@Parameter("id") id: string) { /* ... */ } }

Что мы сделали и зачем

Мы взяли AuthGuard, который успел обрасти ответственностью, и аккуратно разобрали его на части. Не потому что он плохой — просто каждая деталь теперь лежит там, где ей положено.

Вот что конкретно произошло:

  1. OnlyMeGuard уехал в отдельный гараж. Вся логика с isOnlyMe, сравнением id, username и slug — всё это переехало в новый класс. Теперь он занимается ровно одним делом: проверяет, имеет ли пользователь право распоряжаться этим ресурсом. И больше ничем.

  2. AuthGuard перестал быть универсальной сущностью. Его задача теперь кристально прозрачна:

    • проверить, не публичный ли маршрут (isPublic);
    • не пропущен ли он специальным флагом (skipAuthGuard);
    • если нет — провалидировать учётные данные через AuthGuardService;
    • и положить пользователя в ServerUserService, чтобы остальные знали, кто тут главный.
  3. ServerUserService стал центральным диспетчером по пользователям. Раньше каждый Guard сам решал, как достать пользователя: кто через request.user, кто через HashService. Теперь всё идёт через один сервис. Он знает, где лежит пользователь, и умеет его доставать — хоть из request, хоть из кэша, хоть из базы. Остальным об этом думать не надо.

  4. Зависимости похудели. Из AuthGuard улетели SlugPipe и HashService — они там больше не нужны. Из OnlyMeGuard убрали всю муть с токенами. Каждый Guard знает ровно столько, сколько ему нужно, и не лезет в чужие дела.

  5. Guard-ы теперь можно смешивать как конструктор. Хотите просто проверить, что пользователь залогинен? Берите AuthGuard. Хотите ещё и проверить, что он владеет ресурсом? Добавьте OnlyMeGuard. Хотите открытый маршрут? Просто повесьте @Public() — и ни одного Guard.

Что в итоге

Теперь у каждого Guard ровно одна причина, чтобы измениться:

  • AuthGuard переписывают только тогда, когда меняют логику аутентификации — например, переходят с JWT на сессии или добавляют новый тип токена.
  • OnlyMeGuard трогают только тогда, когда меняются правила доступа — например, появляется новый способ проверки владельца (по email или по uuid).
  • ServerUserService живёт своей жизнью и отвечает за то, чтобы пользователь всегда был под рукой.

Мы не просто починили нарушение SRP — мы сделали код гибким, читаемым и удобным для тестирования. И, что важно, теперь, когда через полгода придёт новый разработчик и откроет этот файл, ему не придётся гадать, что тут за что отвечает. Всё разложено по полочкам.

А главное — мы убрали смешение уровней абстракции. Теперь AuthGuard не знает, как парсить slug, а OnlyMeGuard не знает, как работать с токенами. Каждый занимается своим делом — и делает это хорошо.

Last updated on