Пошук уроків, статей та іншого контенту
Створите власний декоратор для отримання поточного користувача з HTTP-запиту в контролерах.
Після автентифікації інформація про поточного користувача зазвичай зберігається в request.user. Наприклад, guard може перевірити JWT і додати до запиту такі дані:
request.user = {
id: 'user-123',
email: 'anna@example.com',
role: 'admin',
};Без власного декоратора контролер звертався б до користувача так:
@Get('profile')
getProfile(@Req() request: Request) {
return request.user;
}Такий підхід має кілька недоліків:
контролер залежить від структури HTTP-запиту;
тип request.user часто доводиться описувати вручну;
логіка отримання користувача дублюється в різних методах;
сигнатура контролера стає менш зрозумілою.
Власний параметр-декоратор дозволяє писати компактніше:
@Get('profile')
getProfile(@CurrentUser() user: AuthenticatedUser) {
return user;
}А якщо потрібне лише одне поле:
@Get('email')
getEmail(@CurrentUser('email') email: string) {
return { email };
}createParamDecoratorNestJS надає функцію createParamDecorator, за допомогою якої можна створювати декоратори параметрів методів контролера.
Базова структура має такий вигляд:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest();
return request.user;
},
);Функція всередині createParamDecorator отримує:
data — додатковий аргумент, переданий у декоратор;
ExecutionContext — контекст поточного виконання;
результат функції — значення, яке буде передане в параметр методу контролера.
В HTTP-контексті потрібно викликати:
context.switchToHttp().getRequest();Після цього можна отримати request.user.
Декоратор не створює користувача і не виконує автентифікацію. Він лише читає користувача з HTTP-запиту. Тому до виконання декоратора guard або інший компонент уже має додати
userдо запиту.
Створимо тип користувача та декоратор CurrentUser.
// src/authenticated-user.type.ts
export type AuthenticatedUser = {
id: string;
email: string;
role: 'user' | 'admin';
};// src/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';
import { AuthenticatedUser } from './authenticated-user.type';
type RequestWithUser = Request & {
user?: AuthenticatedUser;
};
export const CurrentUser = createParamDecorator(
(
_data: unknown,
context: ExecutionContext,
): AuthenticatedUser | undefined => {
const request = context
.switchToHttp()
.getRequest<RequestWithUser>();
return request.user;
},
);Префікс _ у назві _data показує, що цей аргумент поки не використовується.
Тепер декоратор можна застосовувати в контролері:
@Get('profile')
getProfile(@CurrentUser() user: AuthenticatedUser | undefined) {
return user;
}Якщо guard гарантовано встановлює request.user, параметр можна типізувати без undefined:
@Get('profile')
getProfile(@CurrentUser() user: AuthenticatedUser) {
return user;
}Однак це припущення має бути обґрунтованим. Якщо endpoint доступний без guard, користувач може бути відсутнім.
Часто контролеру потрібен не весь об’єкт користувача, а лише його ідентифікатор або роль.
Декоратор може приймати назву поля через параметр data:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';
import { AuthenticatedUser } from './authenticated-user.type';
type RequestWithUser = Request & {
user?: AuthenticatedUser;
};
export const CurrentUser = createParamDecorator(
(
field: keyof AuthenticatedUser | undefined,
context: ExecutionContext,
) => {
const request = context
.switchToHttp()
.getRequest<RequestWithUser>();
const user = request.user;
if (!field) {
return user;
}
return user?.[field];
},
);Тепер доступні обидва варіанти:
@Get('profile')
getProfile(@CurrentUser() user: AuthenticatedUser) {
return user;
}
@Get('profile/id')
getProfileId(@CurrentUser('id') userId: string) {
return { userId };
}
@Get('profile/role')
getProfileRole(@CurrentUser('role') role: AuthenticatedUser['role']) {
return { role };
}Тип keyof AuthenticatedUser обмежує аргумент декоратора допустимими полями. Наприклад, TypeScript не дозволить написати:
@CurrentUser('username')оскільки поля username немає в типі AuthenticatedUser.
Нижче наведено приклад, який можна додати до стандартного NestJS-проєкту. DemoAuthGuard імітує guard автентифікації: у реальному застосунку замість нього використовуватиметься guard, який перевіряє токен або сесію.
// src/authenticated-user.type.ts
export type AuthenticatedUser = {
id: string;
email: string;
role: 'user' | 'admin';
};// src/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';
import { AuthenticatedUser } from './authenticated-user.type';
type RequestWithUser = Request & {
user?: AuthenticatedUser;
};
export const CurrentUser = createParamDecorator(
(
field: keyof AuthenticatedUser | undefined,
context: ExecutionContext,
) => {
const request = context
.switchToHttp()
.getRequest<RequestWithUser>();
const user = request.user;
if (!field) {
return user;
}
return user?.[field];
},
);// src/demo-auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
import { Request } from 'express';
import { AuthenticatedUser } from './authenticated-user.type';
type RequestWithUser = Request & {
user?: AuthenticatedUser;
};
@Injectable()
export class DemoAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context
.switchToHttp()
.getRequest<RequestWithUser>();
// У реальному застосунку користувач визначається після перевірки токена.
request.user = {
id: 'user-123',
email: 'anna@example.com',
role: 'admin',
};
return true;
}
}// src/app.controller.ts
import {
Controller,
Get,
UseGuards,
} from '@nestjs/common';
import { AuthenticatedUser } from './authenticated-user.type';
import { CurrentUser } from './current-user.decorator';
import { DemoAuthGuard } from './demo-auth.guard';
@Controller('profile')
@UseGuards(DemoAuthGuard)
export class AppController {
@Get()
getProfile(@CurrentUser() user: AuthenticatedUser) {
return user;
}
@Get('id')
getProfileId(@CurrentUser('id') userId: string) {
return { userId };
}
@Get('email')
getProfileEmail(@CurrentUser('email') email: string) {
return { email };
}
@Get('role')
getProfileRole(@CurrentUser('role') role: AuthenticatedUser['role']) {
return { role };
}
}// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { DemoAuthGuard } from './demo-auth.guard';
@Module({
controllers: [AppController],
providers: [DemoAuthGuard],
})
export class AppModule {}Після запуску застосунку запити матимуть такий результат:
GET /profile
{
"id": "user-123",
"email": "anna@example.com",
"role": "admin"
}GET /profile/id
{
"userId": "user-123"
}GET /profile/email
{
"email": "anna@example.com"
}GET /profile/role
{
"role": "admin"
}request.userДекоратор очікує, що властивість user уже існує в запиті. Її можуть додати:
guard;
Passport strategy;
middleware;
інший компонент, який відповідає за автентифікацію.
Спрощена схема обробки запиту виглядає так:
клієнт надсилає HTTP-запит;
guard перевіряє автентифікаційні дані;
guard або стратегія визначає користувача;
користувач записується в request.user;
NestJS викликає метод контролера;
@CurrentUser() читає значення з request.user.
Сам декоратор не повинен містити логіку перевірки токена, пошуку користувача в базі даних або перевірки ролей. Його відповідальність — отримати вже підготовлене значення.
Один і той самий декоратор можна застосовувати в багатьох контролерах:
@Controller('orders')
@UseGuards(DemoAuthGuard)
export class OrdersController {
@Get()
findUserOrders(@CurrentUser('id') userId: string) {
return {
userId,
orders: [],
};
}
}У цьому випадку endpoint працює з ідентифікатором користувача, але не знає, як саме він був отриманий із токена або сесії.
Це робить код контролера зрозумілішим:
findUserOrders(@CurrentUser('id') userId: string)замість:
findUserOrders(@Req() request: Request) {
const userId = request.user.id;
}Якщо endpoint не захищений guard, request.user може бути undefined.
Декоратор повертає undefined у таких випадках:
@Get('optional-profile')
getOptionalProfile(@CurrentUser() user: AuthenticatedUser | undefined) {
if (!user) {
return { authenticated: false };
}
return {
authenticated: true,
user,
};
}Для захищених endpoint краще додавати guard на рівні методу або контролера:
@UseGuards(DemoAuthGuard)
@Get('profile')
getProfile(@CurrentUser() user: AuthenticatedUser) {
return user;
}Тоді контролер може розраховувати, що користувач існує.
@Get('profile')
getProfile(@CurrentUser() user: AuthenticatedUser) {
return user.email;
}Якщо guard не встановлює request.user, значення буде undefined, а звернення до user.email спричинить помилку.
Потрібно або додати guard, або обробити відсутнього користувача.
Декоратор не повинен самостійно:
читати та перевіряти JWT;
шукати користувача в базі даних;
перевіряти пароль;
визначати права доступу.
Для цього призначені guard, стратегії та сервіси автентифікації.
Для HTTP-запиту потрібно використовувати:
const request = context.switchToHttp().getRequest();Якщо одразу намагатися працювати з context.getArgs(), код стає залежним від внутрішнього порядку аргументів handler-а і втрачає зрозумілість.
Якщо декоратор приймає назву поля як string, TypeScript не перевіряє її:
@CurrentUser('emial')Типізація через keyof AuthenticatedUser допомагає виявляти такі помилки ще до запуску застосунку.
Усі компоненти застосунку повинні погоджено використовувати структуру request.user. Якщо guard записує:
request.user = {
userId: 'user-123',
};а контролер очікує:
user.idдекоратор не зможе виправити цю невідповідність. Структура користувача має бути описана спільним типом.
Власний декоратор параметра створюється через createParamDecorator.
HTTP-запит отримується з ExecutionContext за допомогою switchToHttp().getRequest().
@CurrentUser() повертає весь об’єкт із request.user.
@CurrentUser('id') може повертати окреме поле користувача.
Декоратор не виконує автентифікацію, а лише читає результат роботи guard або іншого компонента.
Для типобезпечного доступу до полів використовуйте keyof AuthenticatedUser.
Якщо користувач може бути відсутнім, враховуйте undefined у типі та логіці контролера.