diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index bf4757e..7963170 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -38,7 +38,12 @@ async function bootstrap() { allowedHeaders: ['Content-Type', 'Accept', 'Authorization'], }); - app.useGlobalPipes(new ValidationPipe()); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + }) + ); app.setGlobalPrefix('api/v1', { exclude: ['/'], }); diff --git a/apps/backend/src/modules/auth/auth.controller.ts b/apps/backend/src/modules/auth/auth.controller.ts index 240a65e..7f26a93 100644 --- a/apps/backend/src/modules/auth/auth.controller.ts +++ b/apps/backend/src/modules/auth/auth.controller.ts @@ -1,59 +1,42 @@ -import { Body, Controller, Get, Post, Req, Res, UseGuards } from '@nestjs/common'; +import { Body, Controller, HttpCode, Post, Res } from '@nestjs/common'; import { AuthService } from './auth.service'; -import { ApiResponse } from '../../shared/types/api-response.type'; -import { UserDto } from '../user/dto/user.dto'; -import { CreateUserDto } from './dto/create-user.dto'; -import { LocalAuthGuard } from './guards/local-auth.guard'; -import { LoginUserDto } from './dto/login-user.dto'; -import { JwtAuthGuard } from './guards/jwt-auth.guard'; +import { ApiResponse } from '../../shared/types'; +import { PrivateUserDto } from '../user/dto/user.dto'; +import { CreateUserDto } from '../user/dto/create-user.dto'; import { isProd } from '../../env'; -import { JwtPayload } from './types/jwt-payload.type'; +import { type FastifyReply } from 'fastify'; +import { LoginRequestDto } from './dto/login.dto'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} - @UseGuards(LocalAuthGuard) @Post('login') + @HttpCode(200) async login( - @Req() req: Request & { user: UserDto }, - @Res({ passthrough: true }) res - ): Promise> { - const { access_token } = await this.authService.login(req.user); + @Body() loginUserDto: LoginRequestDto, + @Res({ passthrough: true }) res: FastifyReply + ): Promise> { + const { accessToken, user } = await this.authService.login(loginUserDto); - res.setCookie('access_token', access_token, { + res.setCookie('access_token', accessToken, { httpOnly: true, secure: isProd, sameSite: 'strict', path: '/', }); - return { data: { success: true } }; + return { data: user }; } - @Post('registration') - async registration( - @Body() createUserDto: CreateUserDto, - @Res({ passthrough: true }) res - ): Promise> { - const { access_token } = await this.authService.registration(createUserDto); - - res.setCookie('access_token', access_token, { - httpOnly: true, - secure: isProd, - sameSite: 'strict', - path: '/', - }); - - return { data: { success: true } }; + @Post('logout') + @HttpCode(204) + async logout(@Res({ passthrough: true }) res: FastifyReply) { + res.clearCookie('access_token', { path: '/' }); } - @UseGuards(JwtAuthGuard) - @Get('me') - async me(@Req() req: Request & { user: JwtPayload }): Promise> { - const { email } = req.user; - const user = await this.authService.me(email); - - return { data: user }; + @Post('registration') + async registration(@Body() createUserDto: CreateUserDto): Promise { + await this.authService.registration(createUserDto); } } diff --git a/apps/backend/src/modules/auth/auth.module.ts b/apps/backend/src/modules/auth/auth.module.ts index 1334403..ffaafa0 100644 --- a/apps/backend/src/modules/auth/auth.module.ts +++ b/apps/backend/src/modules/auth/auth.module.ts @@ -4,7 +4,6 @@ import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; import { env } from '../../env'; import { JwtStrategy } from './strategies/jwt.strategy'; -import { LocalStrategy } from './strategies/local.strategy'; import { PassportModule } from '@nestjs/passport'; import { UserModule } from '../user/user.module'; @@ -18,6 +17,6 @@ import { UserModule } from '../user/user.module'; UserModule, ], controllers: [AuthController], - providers: [AuthService, JwtStrategy, LocalStrategy], + providers: [AuthService, JwtStrategy], }) export class AuthModule {} diff --git a/apps/backend/src/modules/auth/auth.service.ts b/apps/backend/src/modules/auth/auth.service.ts index 8fc0f8e..bb184e0 100644 --- a/apps/backend/src/modules/auth/auth.service.ts +++ b/apps/backend/src/modules/auth/auth.service.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; -import { UserDto } from '../user/dto/user.dto'; import bcrypt from 'bcrypt'; -import { CreateUserDto } from './dto/create-user.dto'; +import { CreateUserDto } from '../user/dto/create-user.dto'; import { hashPassword } from './utils/hashPassword/hashPassword'; import { JwtService } from '@nestjs/jwt'; import { UserService } from '../user/user.service'; import { JwtPayload } from './types/jwt-payload.type'; import { DomainError } from '../../shared/errors'; +import { LoginRequestDto } from './dto/login.dto'; +import { PrivateUserDto } from '../user/dto/user.dto'; @Injectable() export class AuthService { @@ -15,34 +16,29 @@ export class AuthService { private readonly jwtService: JwtService ) {} - async validateUser(email: string, password: string): Promise { - const user = await this.userService.findByEmailOrNull(email); - - if (!user) return null; - - const isValidPassword = await bcrypt.compare(password, user.password); - - if (!isValidPassword) return null; - - return { - id: user.id, - email: user.email, - name: user.name, - role: user.role, - }; - } - - async registration(createUserDto: CreateUserDto) { + async registration(createUserDto: CreateUserDto): Promise { const hashedPassword = await hashPassword(createUserDto.password); - const user = await this.userService.create({ + await this.userService.create({ ...createUserDto, password: hashedPassword, }); - - return this.login(user); } - async login(user: UserDto): Promise<{ access_token: string }> { + async login( + loginUserDto: LoginRequestDto + ): Promise<{ user: PrivateUserDto; accessToken: string }> { + const user = await this.userService.findByEmailWithPasswordOrNull(loginUserDto.email); + if (!user) { + throw DomainError.Unauthorized('Invalid email or password'); + } + + const isValidPassword = await bcrypt.compare(loginUserDto.password, user.password); + if (!isValidPassword) { + throw DomainError.Unauthorized('Invalid email or password'); + } + + const { password: _, ...userDto } = user; + const payload: JwtPayload = { sub: user.id, email: user.email, @@ -50,21 +46,8 @@ export class AuthService { }; return { - access_token: this.jwtService.sign(payload), - }; - } - - async me(email: string): Promise { - const user = await this.userService.findByEmailOrNull(email); - if (!user) { - throw DomainError.Unauthorized(); - } - - return { - id: user.id, - email: user.email, - name: user.name, - role: user.role, + accessToken: this.jwtService.sign(payload), + user: userDto, }; } } diff --git a/apps/backend/src/modules/auth/dto/login-user.dto.ts b/apps/backend/src/modules/auth/dto/login-user.dto.ts deleted file mode 100644 index ff87bd3..0000000 --- a/apps/backend/src/modules/auth/dto/login-user.dto.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface LoginUserDto { - success: boolean; -} diff --git a/apps/backend/src/modules/auth/dto/login.dto.ts b/apps/backend/src/modules/auth/dto/login.dto.ts new file mode 100644 index 0000000..1c4211f --- /dev/null +++ b/apps/backend/src/modules/auth/dto/login.dto.ts @@ -0,0 +1,9 @@ +import { IsEmail, IsString } from 'class-validator'; + +export class LoginRequestDto { + @IsEmail() + email: string; + + @IsString() + password: string; +} diff --git a/apps/backend/src/modules/auth/guards/local-auth.guard.ts b/apps/backend/src/modules/auth/guards/local-auth.guard.ts deleted file mode 100644 index 26a1d73..0000000 --- a/apps/backend/src/modules/auth/guards/local-auth.guard.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { AuthGuard } from '@nestjs/passport'; - -export class LocalAuthGuard extends AuthGuard('local') {} diff --git a/apps/backend/src/modules/auth/strategies/jwt.strategy.ts b/apps/backend/src/modules/auth/strategies/jwt.strategy.ts index 0a7efb4..91feac5 100644 --- a/apps/backend/src/modules/auth/strategies/jwt.strategy.ts +++ b/apps/backend/src/modules/auth/strategies/jwt.strategy.ts @@ -2,24 +2,22 @@ import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { env } from '../../../env'; import { Injectable } from '@nestjs/common'; -import { JwtPayload } from '../types/jwt-payload.type'; -import { UserService } from '../../user/user.service'; -import { DomainError } from '../../../shared/errors'; +import type { CurrentUserType, JwtPayload } from '../types/jwt-payload.type'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { - constructor(private readonly userService: UserService) { + constructor() { super({ jwtFromRequest: ExtractJwt.fromExtractors([(req) => req?.cookies?.access_token]), secretOrKey: env.JWT_SECRET, }); } - async validate(payload: JwtPayload): Promise { - const user = await this.userService.findByEmailOrNull(payload.email); - if (!user) { - throw DomainError.Unauthorized(); - } - return payload; + async validate(payload: JwtPayload): Promise { + return { + id: payload.sub, + email: payload.email, + role: payload.role, + }; } } diff --git a/apps/backend/src/modules/auth/strategies/local.strategy.ts b/apps/backend/src/modules/auth/strategies/local.strategy.ts deleted file mode 100644 index cb8163a..0000000 --- a/apps/backend/src/modules/auth/strategies/local.strategy.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { PassportStrategy } from '@nestjs/passport'; -import { Strategy } from 'passport-local'; -import { AuthService } from '../auth.service'; -import { Injectable } from '@nestjs/common'; -import { UserDto } from '../../user/dto/user.dto'; -import { DomainError } from '../../../shared/errors'; - -@Injectable() -export class LocalStrategy extends PassportStrategy(Strategy) { - constructor(private authService: AuthService) { - super({ - usernameField: 'email', - passwordField: 'password', - }); - } - - async validate(email: string, password: string): Promise { - const user = await this.authService.validateUser(email, password); - if (!user) { - throw DomainError.Unauthorized(); - } - return user; - } -} diff --git a/apps/backend/src/modules/auth/types/jwt-payload.type.ts b/apps/backend/src/modules/auth/types/jwt-payload.type.ts index 4a216af..dae2469 100644 --- a/apps/backend/src/modules/auth/types/jwt-payload.type.ts +++ b/apps/backend/src/modules/auth/types/jwt-payload.type.ts @@ -1,7 +1,13 @@ import type { UserRole } from '@prisma/client'; -export interface JwtPayload { +export type JwtPayload = { sub: string; email: string; role: UserRole; -} +}; + +export type CurrentUserType = { + id: string; + email: string; + role: UserRole; +}; diff --git a/apps/backend/src/modules/auth/dto/create-user.dto.ts b/apps/backend/src/modules/user/dto/create-user.dto.ts similarity index 100% rename from apps/backend/src/modules/auth/dto/create-user.dto.ts rename to apps/backend/src/modules/user/dto/create-user.dto.ts diff --git a/apps/backend/src/modules/user/dto/update-auth-user-password.dto.ts b/apps/backend/src/modules/user/dto/update-auth-user-password.dto.ts new file mode 100644 index 0000000..a82dee2 --- /dev/null +++ b/apps/backend/src/modules/user/dto/update-auth-user-password.dto.ts @@ -0,0 +1,9 @@ +import { IsString } from 'class-validator'; + +export class UpdateAuthUserPasswordDto { + @IsString() + oldPassword: string; + + @IsString() + newPassword: string; +} diff --git a/apps/backend/src/modules/user/dto/update-user.dto.ts b/apps/backend/src/modules/user/dto/update-user.dto.ts index ad44dea..e61a2b6 100644 --- a/apps/backend/src/modules/user/dto/update-user.dto.ts +++ b/apps/backend/src/modules/user/dto/update-user.dto.ts @@ -1,4 +1,4 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateUserDto } from '../../auth/dto/create-user.dto'; +import { OmitType, PartialType } from '@nestjs/swagger'; +import { CreateUserDto } from './create-user.dto'; -export class UpdateUserDto extends PartialType(CreateUserDto) {} +export class UpdateUserDto extends PartialType(OmitType(CreateUserDto, ['password'] as const)) {} diff --git a/apps/backend/src/modules/user/dto/user.dto.ts b/apps/backend/src/modules/user/dto/user.dto.ts index cf9d7bf..4d1988a 100644 --- a/apps/backend/src/modules/user/dto/user.dto.ts +++ b/apps/backend/src/modules/user/dto/user.dto.ts @@ -1,8 +1,14 @@ import type { UserRole } from '@prisma/client'; +import { PrivateUserPayload, PublicUserPayload } from '../selectors/user.selectors'; -export interface UserDto { +export class PrivateUserDto implements PrivateUserPayload { id: string; name: string; email: string; role: UserRole; } + +export class PublicUserDto implements PublicUserPayload { + id: string; + name: string; +} diff --git a/apps/backend/src/modules/user/selectors/user.selectors.ts b/apps/backend/src/modules/user/selectors/user.selectors.ts new file mode 100644 index 0000000..0020d2b --- /dev/null +++ b/apps/backend/src/modules/user/selectors/user.selectors.ts @@ -0,0 +1,15 @@ +import { Prisma } from '@prisma/client'; + +export const publicUserSelect = { + id: true, + name: true, +} satisfies Prisma.UserSelect; + +export const privateUserSelect = { + ...publicUserSelect, + email: true, + role: true, +} satisfies Prisma.UserSelect; + +export type PrivateUserPayload = Prisma.UserGetPayload<{ select: typeof privateUserSelect }>; +export type PublicUserPayload = Prisma.UserGetPayload<{ select: typeof publicUserSelect }>; diff --git a/apps/backend/src/modules/user/user.controller.ts b/apps/backend/src/modules/user/user.controller.ts index 59a9196..adfa35d 100644 --- a/apps/backend/src/modules/user/user.controller.ts +++ b/apps/backend/src/modules/user/user.controller.ts @@ -1,71 +1,57 @@ -import { - Controller, - Get, - Body, - Patch, - Param, - Delete, - HttpCode, - UseGuards, - Req, - ForbiddenException, -} from '@nestjs/common'; +import { Controller, Get, Body, Patch, Param, Delete, HttpCode, UseGuards } from '@nestjs/common'; import { UserService } from './user.service'; import { UpdateUserDto } from './dto/update-user.dto'; -import { ApiResponse } from '../../shared/types/api-response.type'; -import { UserDto } from './dto/user.dto'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -import { UserRole } from '@prisma/client'; -import { JwtPayload } from '../auth/types/jwt-payload.type'; -import { ParseCuidPipe } from '../../shared/pipes/parse-cuid.pipe'; - -@Controller('user') +import { ApiResponse } from '../../shared/types'; +import type { CurrentUserType } from '../auth/types/jwt-payload.type'; +import { ParseCuidPipe } from '../../shared/pipes'; +import { JwtAuthGuard } from '../../shared/guards'; +import { CurrentUser } from '../../shared/decorators'; +import { PrivateUserDto, PublicUserDto } from './dto/user.dto'; +import type { UpdateAuthUserPasswordDto } from './dto/update-auth-user-password.dto'; + +@Controller('users') export class UserController { constructor(private readonly userService: UserService) {} @UseGuards(JwtAuthGuard) - @Get() - async findAll(@Req() req: Request & { user: JwtPayload }): Promise> { - if (req.user.role !== UserRole.ADMIN && req.user.role !== UserRole.MODERATOR) { - throw new ForbiddenException(); - } - const users = await this.userService.findAll(); + @Get('me') + async me(@CurrentUser() user: CurrentUserType): Promise> { + const userData = await this.userService.me(user.id); - return { data: users }; + return { data: userData }; } - @Get(':id') - async findById(@Param('id', ParseCuidPipe) id: string): Promise> { - const user = await this.userService.findById(id); + @UseGuards(JwtAuthGuard) + @HttpCode(204) + @Patch('me/password') + async updatePassword( + @Body() updatePasswordDto: UpdateAuthUserPasswordDto, + @CurrentUser() user: CurrentUserType + ): Promise { + await this.userService.updatePassword(user, updatePasswordDto); + } + @UseGuards(JwtAuthGuard) + @Get(':id') + async findById(@Param('id', ParseCuidPipe) id: string): Promise> { + const user = await this.userService.findPublicById(id); return { data: user }; } @UseGuards(JwtAuthGuard) - @Patch(':id') + @Patch('/me') async update( - @Param('id', ParseCuidPipe) id: string, @Body() updateUserDto: UpdateUserDto, - @Req() req: Request & { user: JwtPayload } - ): Promise> { - // TODO: password update? - - if (req.user.role !== UserRole.ADMIN && req.user.sub !== id) { - throw new ForbiddenException(); - } - - const user = await this.userService.update(id, updateUserDto); - - return { data: user }; + @CurrentUser() user: CurrentUserType + ): Promise> { + const updatedUser = await this.userService.update(user.id, updateUserDto); + return { data: updatedUser }; } @UseGuards(JwtAuthGuard) - @Delete(':id') + @Delete('/me') @HttpCode(204) - remove(@Param('id', ParseCuidPipe) id: string, @Req() req: Request & { user: JwtPayload }) { - if (req.user.role !== UserRole.ADMIN && req.user.sub !== id) { - throw new ForbiddenException(); - } - return this.userService.remove(id); + async remove(@CurrentUser() user: CurrentUserType) { + await this.userService.remove(user.id); } } diff --git a/apps/backend/src/modules/user/user.service.ts b/apps/backend/src/modules/user/user.service.ts index 66fd438..b2564d3 100644 --- a/apps/backend/src/modules/user/user.service.ts +++ b/apps/backend/src/modules/user/user.service.ts @@ -1,75 +1,91 @@ import { Injectable } from '@nestjs/common'; import { UpdateUserDto } from './dto/update-user.dto'; import { PrismaService } from '../../prisma/prisma.service'; -import { UserDto } from './dto/user.dto'; -import { CreateUserDto } from '../auth/dto/create-user.dto'; -import { User } from '@prisma/client'; +import { CreateUserDto } from './dto/create-user.dto'; import { DomainError } from '../../shared/errors'; +import { privateUserSelect, publicUserSelect } from './selectors/user.selectors'; +import { PrivateUserDto, PublicUserDto } from './dto/user.dto'; +import bcrypt from 'bcrypt'; +import { UpdateAuthUserPasswordDto } from './dto/update-auth-user-password.dto'; +import { CurrentUserType } from '../auth/types/jwt-payload.type'; +import { hashPassword } from '../auth/utils/hashPassword/hashPassword'; @Injectable() export class UserService { constructor(private readonly prisma: PrismaService) {} - async findAll(): Promise { - const users = await this.prisma.user.findMany(); + async me(id: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { id }, + select: privateUserSelect, + }); + if (!user) { + throw DomainError.Unauthorized(); + } + + return user; + } + + async updatePassword( + currentUser: CurrentUserType, + updatePasswordDto: UpdateAuthUserPasswordDto + ): Promise { + const user = await this.findByEmailWithPasswordOrNull(currentUser.email); + if (!user) { + throw DomainError.NotFound('User not found'); + } + + const isValidPassword = await bcrypt.compare(updatePasswordDto.oldPassword, user.password); + if (!isValidPassword) { + throw DomainError.BadRequest('Invalid password'); + } + + const hashedNewPassword = await hashPassword(updatePasswordDto.newPassword); - return users.map((user) => { - return { - id: user.id, - name: user.name, - email: user.email, - role: user.role, - }; + await this.prisma.user.update({ + where: { id: currentUser.id }, + data: { + password: hashedNewPassword, + }, }); } - async findById(id: string): Promise { + async findPublicById(id: string): Promise { const user = await this.prisma.user.findUnique({ where: { id }, + select: publicUserSelect, }); if (!user) { throw DomainError.NotFound('User not found'); } - return { - id: user.id, - name: user.name, - email: user.email, - role: user.role, - }; + return user; } - async findByEmailOrNull(email: string): Promise { + async findByEmailWithPasswordOrNull( + email: string + ): Promise<(PrivateUserDto & { password: string }) | null> { return this.prisma.user.findUnique({ where: { email }, + select: { + ...privateUserSelect, + password: true, + }, }); } - async create(createUserDto: CreateUserDto): Promise { - const user = await this.prisma.user.create({ + async create(createUserDto: CreateUserDto): Promise { + await this.prisma.user.create({ data: createUserDto, }); - - return { - id: user.id, - name: user.name, - email: user.email, - role: user.role, - }; } - async update(id: string, updateUserDto: UpdateUserDto): Promise { - const updatedUser = await this.prisma.user.update({ + async update(id: string, updateUserDto: UpdateUserDto): Promise { + return await this.prisma.user.update({ where: { id }, data: updateUserDto, + select: privateUserSelect, }); - - return { - id: updatedUser.id, - name: updatedUser.name, - email: updatedUser.email, - role: updatedUser.role, - }; } async remove(id: string): Promise { diff --git a/apps/backend/src/shared/decorators/current-user.decorator.ts b/apps/backend/src/shared/decorators/current-user.decorator.ts new file mode 100644 index 0000000..a9d7963 --- /dev/null +++ b/apps/backend/src/shared/decorators/current-user.decorator.ts @@ -0,0 +1,16 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { FastifyRequest } from 'fastify'; +import { type CurrentUserType } from '../../modules/auth/types/jwt-payload.type'; + +export const CurrentUser = createParamDecorator( + (data: keyof CurrentUserType | undefined, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + const user = request.user; + + if (!user) { + return null; + } + + return data ? user[data] : user; + } +); diff --git a/apps/backend/src/shared/decorators/index.ts b/apps/backend/src/shared/decorators/index.ts new file mode 100644 index 0000000..81b4330 --- /dev/null +++ b/apps/backend/src/shared/decorators/index.ts @@ -0,0 +1,2 @@ +export { CurrentUser } from './current-user.decorator'; +export { Roles } from './roles.decorator'; diff --git a/apps/backend/src/shared/decorators/roles.decorator.ts b/apps/backend/src/shared/decorators/roles.decorator.ts new file mode 100644 index 0000000..ec0c377 --- /dev/null +++ b/apps/backend/src/shared/decorators/roles.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; +import { UserRole } from '@prisma/client'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles); diff --git a/apps/backend/src/shared/guards/index.ts b/apps/backend/src/shared/guards/index.ts new file mode 100644 index 0000000..79af697 --- /dev/null +++ b/apps/backend/src/shared/guards/index.ts @@ -0,0 +1,2 @@ +export { JwtAuthGuard } from './jwt-auth.guard'; +export { RolesGuard } from './roles.guard'; diff --git a/apps/backend/src/modules/auth/guards/jwt-auth.guard.ts b/apps/backend/src/shared/guards/jwt-auth.guard.ts similarity index 100% rename from apps/backend/src/modules/auth/guards/jwt-auth.guard.ts rename to apps/backend/src/shared/guards/jwt-auth.guard.ts diff --git a/apps/backend/src/shared/guards/roles.guard.ts b/apps/backend/src/shared/guards/roles.guard.ts new file mode 100644 index 0000000..6b5fd80 --- /dev/null +++ b/apps/backend/src/shared/guards/roles.guard.ts @@ -0,0 +1,40 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ROLES_KEY } from '../decorators/roles.decorator'; +import { UserRole } from '@prisma/client'; +import { FastifyRequest } from 'fastify'; +import { CurrentUserType } from '../../modules/auth/types/jwt-payload.type'; +import { DomainError } from '../errors'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (!requiredRoles) { + return true; + } + + const request = context + .switchToHttp() + .getRequest(); + const user = request.user; + + if (!user) { + throw DomainError.Unauthorized(); + } + + const hasRole = requiredRoles.includes(user.role); + + if (!hasRole) { + throw DomainError.Forbidden(); + } + + return true; + } +} diff --git a/apps/backend/src/shared/pipes/index.ts b/apps/backend/src/shared/pipes/index.ts new file mode 100644 index 0000000..1ce76b3 --- /dev/null +++ b/apps/backend/src/shared/pipes/index.ts @@ -0,0 +1 @@ +export { ParseCuidPipe } from './parse-cuid.pipe'; diff --git a/apps/backend/src/shared/types/index.ts b/apps/backend/src/shared/types/index.ts new file mode 100644 index 0000000..535d36b --- /dev/null +++ b/apps/backend/src/shared/types/index.ts @@ -0,0 +1 @@ +export { type ApiResponse } from './api-response.type'; diff --git a/eslint.config.mjs b/eslint.config.mjs index ae40151..d4ce4f1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,4 +3,15 @@ import { defineConfig } from 'eslint/config'; import tseslint from 'typescript-eslint'; import prettier from 'eslint-config-prettier'; -export default defineConfig(eslint.configs.recommended, tseslint.configs.recommended, prettier); +export default defineConfig(eslint.configs.recommended, tseslint.configs.recommended, prettier, { + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + }, +});