Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке guards у NestJS і як реалізувати аутентифікацію?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Guards у NestJS** реалізують `CanActivate` і виконуються до обробників маршрутів, щоб дозволити або заблокувати запит на основі автентифікації. Стандартний патерн: перевірити JWT, прикріпити payload до запиту, повернути `true`. ```typescript @Injectable() export class JwtAuthGuard implements CanActivate { constructor(private jwtService: JwtService) {} async canActivate(context: ExecutionContext): Promise<boolean> { const req = context.switchToHttp().getRequest(); req.user = await this.jwtService.verifyAsync(extractToken(req)); return true; } } ``` **Ключове:** Guards виконуються після middleware, але до interceptors і pipes. Застосовуються через `@UseGuards()` на методі, контролері або глобально через `APP_GUARD`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Guards у NestJS** - це класи, що реалізують `CanActivate` і виконуються перед обробниками маршрутів, щоб вирішити, чи може запит продовжити свій шлях на основі автентифікації або прав користувача. ## Теорія ### TL;DR - Guard - це охоронець на вході: він перевіряє токен перш ніж запит потрапить до контролера; немає дійсного токена - немає доступу - `canActivate` повертає `true`, щоб пропустити запит, або кидає виняток, щоб заблокувати; повернення `false` дає 403 - Guards виконуються після middleware, але до interceptors, pipes і обробника маршруту - Застосовуй через `@UseGuards()` на методі, класі контролера або глобально через `APP_GUARD` - Автентифікація (хто ти?) і авторизація (що тобі дозволено?) - різні задачі; для обох використовуй ланцюжок з двох guards ### Швидкий приклад ```typescript import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; @Injectable() export class AuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); if (!request.headers.authorization) { throw new UnauthorizedException('Токен відсутній'); } return true; // запит проходить до обробника } } // Захист одного маршруту @Get('profile') @UseGuards(AuthGuard) getProfile() { return 'захищені дані'; } ``` GET `/profile` без заголовка `Authorization` повертає 401. З заголовком контролер виконується. ### Місце guards у pipeline NestJS обробляє кожен запит у такому порядку: middleware, guards, interceptors, pipes, обробник маршруту, фільтри винятків. Guards стоять на другому місці. Вони бачать запит після парсингу middleware і зупиняють його до того, як починається бізнес-логіка. Якщо `canActivate` повертає `false` або кидає виняток, pipeline зупиняється тут. Фільтр винятків перехоплює помилку і формує HTTP-відповідь. Код контролера не виконується взагалі. Об'єкт `ExecutionContext` дає guards більше можливостей ніж middleware. Він обгортає платформу (Express або Fastify) і надає доступ до `context.switchToHttp().getRequest()` для отримання запиту, а також `context.getHandler()` і `context.getClass()` для читання метаданих, встановлених декораторами на кшталт `@Roles('admin')`. ### Коли використовувати guards - Один маршрут потребує автентифікації: `@UseGuards(JwtAuthGuard)` на методі - Весь контролер потребує автентифікації: `@UseGuards(JwtAuthGuard)` на класі - Весь застосунок захищено за замовчуванням: реєструй через `APP_GUARD` у модулі (підтримує DI, на відміну від `app.useGlobalGuards()` у `main.ts`) - Деякі маршрути мають бути відкритими у глобально захищеному застосунку: для цього є патерн із декоратором `@Public()` - Перевірка ролей поверх перевірки токена: `@UseGuards(JwtAuthGuard, RolesGuard)` ### Guard vs Middleware vs Interceptor | | Guard | Middleware | Interceptor | |---|---|---|---| | Має `ExecutionContext` | Так | Ні | Так | | Читає метадані маршруту | Так | Ні | Так | | Виконується до обробника | Так | Так | Так (до обробника) | | Може заблокувати запит | Так | Так | Так | | Основна задача | Автентифікація, авторизація | Парсинг, CORS, логування | Трансформація відповіді | Middleware працює на рівні Express/Fastify і нічого не знає про маршрути та метадані NestJS. Guards виконуються всередині NestJS і можуть бачити, який саме контролер і метод викликається. Middleware - для глобальних HTTP-задач, як парсинг cookie. Guards - для перевірки доступу. ### Типові помилки **Помилка 1: Забути `async` при використанні `jwtService.verifyAsync`** ```typescript // Неправильно - Promise ігнорується, guard завжди повертає true canActivate(context: ExecutionContext): boolean { this.jwtService.verifyAsync(token); // ніколи не await-ується return true; } // Правильно async canActivate(context: ExecutionContext): Promise<boolean> { const payload = await this.jwtService.verifyAsync(token); request.user = payload; return true; } ``` **Помилка 2: Не прикріплювати `payload` до `request.user`** Guard перевіряє токен, але контролер все одно потребує даних користувача. Без `request.user = payload` кожен обробник, що звертається до `req.user`, отримає `undefined`. Це тихий баг, який часто спливає лише в продакшені. **Помилка 3: Забути `@Injectable()` на класі guard** Без `@Injectable()` NestJS не може створити екземпляр guard через DI-контейнер. Застосунок падає при старті з незрозумілою помилкою. **Помилка 4: Кидати `new Error()` замість `UnauthorizedException`** `new Error()` обходить фільтр винятків NestJS. У відповіді повертається 500 з HTML-тілом замість чистого JSON з кодом 401. Використовуй `UnauthorizedException` для 401 і `ForbiddenException` для 403. **Помилка 5: Глобальний guard блокує публічні маршрути** Глобально зареєстрований JWT guard вимагатиме токен навіть на `/health`, `/auth/login` і `/auth/register`. Додай патерн `@Public()`, щоб ці маршрути пропускали перевірку токена. ### Де зустрічається у реальних проєктах - NestJS + Prisma: guard перевіряє, чи `req.user.id` збігається з власником ресурсу перед записом або видаленням - GraphQL: `@UseGuards()` на resolver-ах працює так само, як і на REST-контролерах; використовується `context.switchToGraphql()` замість `switchToHttp()` - Мікросервіси: `JwtAuthGuard` реєструється глобально, `@Public()` ставиться тільки на auth-ендпоїнти - Fastify-адаптер: код guard залишається ідентичним; `switchToHttp()` сам розбирається з різницею платформ ### Питання на співбесіді **Q:** Який повний порядок виконання у NestJS? **A:** Спочатку middleware, потім guards, потім interceptors (до обробника), потім pipes, потім сам обробник, потім interceptors знову (після обробника). Фільтри винятків спрацьовують в кінці, якщо щось кинуло помилку. **Q:** Як поєднати кілька guards у ланцюжок? **A:** `@UseGuards(Guard1, Guard2)` - обидва мають повернути `true`. NestJS зупиняється на першому guard, який повернув `false` або кинув виняток. **Q:** Яка різниця між auth guard і roles guard? **A:** Auth guard перевіряє особу: чи дійсний цей токен і чи існує такий користувач? Roles guard перевіряє права: чи є у цього користувача потрібна роль? Вони вирішують різні задачі і зазвичай використовуються разом у ланцюжку. **Q:** Як тестувати guard ізольовано? **A:** Створи тестовий модуль із guard як провайдером. Замокай `ExecutionContext` як простий об'єкт: `{ switchToHttp: () => ({ getRequest: () => mockReq }) }`. Перевір, що `canActivate` повертає `true` для коректних даних і кидає виняток для некоректних. **Q (Senior):** Як зробити `/auth/login` доступним без токена, якщо `JwtAuthGuard` зареєстрований глобально? **A:** Створи декоратор `@Public()` через `SetMetadata('isPublic', true)`. У guard-і читай цей прапор через `Reflector.getAllAndOverride` перед перевіркою токена. Якщо маршрут помічений як публічний, одразу повертай `true` і пропускай перевірку JWT. ## Приклади ### Базовий AuthGuard Найпростіший guard перевіряє наявність будь-якого заголовка `Authorization`. Добре підходить для розуміння структури перед додаванням JWT-логіки. ```typescript import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, } from '@nestjs/common'; @Injectable() export class AuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); const authHeader = request.headers.authorization; if (!authHeader) { throw new UnauthorizedException('Заголовок Authorization відсутній'); } return true; // будь-яке значення заголовка проходить; у реальному застосунку тут валідується токен } } ``` `@Injectable()` обов'язковий. Guard кидає `UnauthorizedException` (401), коли заголовка немає, і повертає `true` інакше. Наступний guard у ланцюжку виконується тільки якщо цей пропустив запит. ### JWT Auth Guard (продакшн-патерн) Цей guard перевіряє Bearer-токен через `@nestjs/jwt`, прикріплює декодований payload до `request.user` і обробляє прострочені або некоректні токени чистим 401. ```typescript // guards/jwt-auth.guard.ts import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { Request } from 'express'; interface AuthRequest extends Request { user?: Record<string, unknown>; } @Injectable() export class JwtAuthGuard implements CanActivate { constructor(private jwtService: JwtService) {} async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest<AuthRequest>(); const token = this.extractToken(request); if (!token) throw new UnauthorizedException('Bearer-токен відсутній'); try { const payload = await this.jwtService.verifyAsync(token, { secret: process.env.JWT_SECRET, }); request.user = payload; // доступно як req.user у будь-якому контролері } catch { throw new UnauthorizedException('Недійсний або прострочений токен'); } return true; } private extractToken(request: AuthRequest): string | undefined { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : undefined; } } // users.controller.ts @Controller('users') @UseGuards(JwtAuthGuard) export class UsersController { @Get('profile') getProfile(@Req() req: AuthRequest) { return req.user; // { sub: 'userId', email: '...', iat: ... } } } ``` Дійсний Bearer-токен повертає 200 з декодованим payload. Прострочений або підмінений токен повертає 401. `async/await` на `verifyAsync` не є опціональним: без нього guard завжди пропускає запит незалежно від коректності токена. ### Roles Guard із декоратором `@Public()` Два продакшн-патерни в одному прикладі: перевірка ролей через `Reflector` (клас із `@nestjs/core`) і позначення маршрутів публічними у глобально захищеному застосунку. Відсутній `@Public()` на маршруті логіну - одна з найпоширеніших причин плутанини при переході з рівня методів на глобальний рівень. ```typescript // decorators/roles.decorator.ts import { SetMetadata } from '@nestjs/common'; export const ROLES_KEY = 'roles'; export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); // decorators/public.decorator.ts import { SetMetadata } from '@nestjs/common'; export const IS_PUBLIC_KEY = 'isPublic'; export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); // guards/roles.guard.ts import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ROLES_KEY } from '../decorators/roles.decorator'; @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [ context.getHandler(), // спочатку метадані методу context.getClass(), // потім метадані контролера ]); if (!requiredRoles) return true; // @Roles() не вказано - пропускаємо const { user } = context.switchToHttp().getRequest(); return requiredRoles.some((role) => user.roles?.includes(role)); } } // admin.controller.ts @Controller('admin') @UseGuards(JwtAuthGuard, RolesGuard) // спочатку перевірка токена, потім ролей export class AdminController { @Get('users') @Roles('admin') getAllUsers() { return 'тільки для адміністраторів'; } } // auth.controller.ts - логін має бути публічним при глобальному guard @Public() @Post('auth/login') login(@Body() loginDto: LoginDto) { return this.authService.login(loginDto); } // app.module.ts - глобальна реєстрація з підтримкою DI @Module({ providers: [ { provide: APP_GUARD, useClass: JwtAuthGuard }, { provide: APP_GUARD, useClass: RolesGuard }, ], }) export class AppModule {} ``` Користувач із `roles: ['admin']` у JWT payload проходить обидва guards. Користувач із `roles: ['viewer']` проходить перевірку токена, але отримує 403 від `RolesGuard`. Декоратор `@Public()` на маршруті логіну повністю пропускає перевірку JWT - неавтентифіковані користувачі можуть отримати токен.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.