Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: ['/'],
});
Expand Down
57 changes: 20 additions & 37 deletions apps/backend/src/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<LoginUserDto>> {
const { access_token } = await this.authService.login(req.user);
@Body() loginUserDto: LoginRequestDto,
@Res({ passthrough: true }) res: FastifyReply
): Promise<ApiResponse<PrivateUserDto>> {
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<ApiResponse<LoginUserDto>> {
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<ApiResponse<UserDto>> {
const { email } = req.user;
const user = await this.authService.me(email);

return { data: user };
@Post('registration')
async registration(@Body() createUserDto: CreateUserDto): Promise<void> {
await this.authService.registration(createUserDto);
}
}
3 changes: 1 addition & 2 deletions apps/backend/src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -18,6 +17,6 @@ import { UserModule } from '../user/user.module';
UserModule,
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy, LocalStrategy],
providers: [AuthService, JwtStrategy],
})
export class AuthModule {}
61 changes: 22 additions & 39 deletions apps/backend/src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -15,56 +16,38 @@ export class AuthService {
private readonly jwtService: JwtService
) {}

async validateUser(email: string, password: string): Promise<UserDto | null> {
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<void> {
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,
role: user.role,
};

return {
access_token: this.jwtService.sign(payload),
};
}

async me(email: string): Promise<UserDto> {
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,
};
}
}
3 changes: 0 additions & 3 deletions apps/backend/src/modules/auth/dto/login-user.dto.ts

This file was deleted.

9 changes: 9 additions & 0 deletions apps/backend/src/modules/auth/dto/login.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IsEmail, IsString } from 'class-validator';

export class LoginRequestDto {
@IsEmail()
email: string;

@IsString()
password: string;
}
3 changes: 0 additions & 3 deletions apps/backend/src/modules/auth/guards/local-auth.guard.ts

This file was deleted.

18 changes: 8 additions & 10 deletions apps/backend/src/modules/auth/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<JwtPayload> {
const user = await this.userService.findByEmailOrNull(payload.email);
if (!user) {
throw DomainError.Unauthorized();
}
return payload;
async validate(payload: JwtPayload): Promise<CurrentUserType> {
return {
id: payload.sub,
email: payload.email,
role: payload.role,
};
}
}
24 changes: 0 additions & 24 deletions apps/backend/src/modules/auth/strategies/local.strategy.ts

This file was deleted.

10 changes: 8 additions & 2 deletions apps/backend/src/modules/auth/types/jwt-payload.type.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IsString } from 'class-validator';

export class UpdateAuthUserPasswordDto {
@IsString()
oldPassword: string;

@IsString()
newPassword: string;
}
6 changes: 3 additions & 3 deletions apps/backend/src/modules/user/dto/update-user.dto.ts
Original file line number Diff line number Diff line change
@@ -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)) {}
8 changes: 7 additions & 1 deletion apps/backend/src/modules/user/dto/user.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
15 changes: 15 additions & 0 deletions apps/backend/src/modules/user/selectors/user.selectors.ts
Original file line number Diff line number Diff line change
@@ -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 }>;
Loading
Loading