diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 2a731b7b..0e59a3ad 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -1,14 +1,47 @@ -import { Controller, Post, Body } from '@nestjs/common'; +import { + Controller, + Post, + Body, + HttpCode, + HttpStatus, + Res, + UseGuards, +} from '@nestjs/common'; import { AuthService } from './providers/auth.service'; import { CreateUserDto } from '../users/dto/createUser.dto'; import { User } from '../users/entities/user.entity'; +import { Public } from './decorators/public.decorator'; +import { Response } from 'express'; +import { AuthResponse } from './interfaces/authResponse.interface'; +import { LoginUserDto } from 'src/users/dto/loginUser.dto'; +import { GetCurrentUser } from './decorators/getCurrentUser.decorator'; +import { LocalAuthGuard } from './guards/local.guard'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} + // CREATE A NEW USER + @Public() @Post('register') - async register(@Body() createUserDto: CreateUserDto): Promise { - return await this.authService.createUser(createUserDto); + @HttpCode(HttpStatus.CREATED) + async register( + @Body() createUserDto: CreateUserDto, + @Res({ passthrough: true }) response: Response, + ): Promise { + return await this.authService.createUser(createUserDto, response); + } + + // LOGIN USER + @Public() + @Post('login') + @UseGuards(LocalAuthGuard) + @HttpCode(HttpStatus.OK) + public async loginUser( + @Body() loginUserDto: LoginUserDto, + @GetCurrentUser() user: User, + @Res({ passthrough: true }) response: Response, + ): Promise { + return await this.authService.loginUser(user, response); } } diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 1ec3ef11..ff5bc0b4 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -9,11 +9,29 @@ import { BcryptProvider } from './providers/bcrypt.provider'; import { LocalStrategy } from './strategies/local.strategy'; import { JwtStrategy } from './strategies/jwt.strategy'; import { JwtRefreshStrategy } from './strategies/jwtRefresh.strategy'; +import { LoginUserProvider } from './providers/loginUser.provider'; +import { GenerateTokensProvider } from './providers/generateTokens.provider'; +import { RefreshTokensProvider } from './providers/refreshTokens.provider'; +import { RefreshTokenRepositoryOperations } from './providers/RefreshTokenCrud.repository'; +import { FindOneRefreshTokenProvider } from './providers/findOneRefreshToken.provider'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; @Module({ imports: [ TypeOrmModule.forFeature([RefreshToken]), - forwardRef(() => UsersModule) + forwardRef(() => UsersModule), + ConfigModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET'), + signOptions: { + expiresIn: configService.get('JWT_ACCESS_EXPIRATION'), + }, + }), + }), ], controllers: [AuthController], providers: [ @@ -22,10 +40,22 @@ import { JwtRefreshStrategy } from './strategies/jwtRefresh.strategy'; provide: HashingProvider, useClass: BcryptProvider, }, - LocalStrategy, + LocalStrategy, JwtStrategy, JwtRefreshStrategy, + LoginUserProvider, + GenerateTokensProvider, + RefreshTokensProvider, + RefreshTokenRepositoryOperations, + FindOneRefreshTokenProvider, + ], + exports: [ + AuthService, + HashingProvider, + GenerateTokensProvider, + RefreshTokensProvider, + RefreshTokenRepositoryOperations, + FindOneRefreshTokenProvider, ], - exports: [AuthService, HashingProvider] }) export class AuthModule {} diff --git a/backend/src/auth/http/post.auth.endpoints.http b/backend/src/auth/http/post.auth.endpoints.http new file mode 100644 index 00000000..30103824 --- /dev/null +++ b/backend/src/auth/http/post.auth.endpoints.http @@ -0,0 +1,20 @@ + +// CREATE NEW USER +POST http://localhost:6000/auth/register +Content-Type: application/json + +{ + "firstname": "john", + "lastname": "doe", + "email": "johndoe@gmail.com", + "password": "Adetomi.54" +} + +// LOGIN USER +POST http://localhost:6000/auth/login +Content-Type: application/json + +{ + "email": "johndoe", + "password": "Adetomi.54" +} \ No newline at end of file diff --git a/backend/src/auth/providers/RefreshTokenCrud.repository.ts b/backend/src/auth/providers/RefreshTokenCrud.repository.ts new file mode 100644 index 00000000..6324fe3c --- /dev/null +++ b/backend/src/auth/providers/RefreshTokenCrud.repository.ts @@ -0,0 +1,139 @@ +import { + BadRequestException, + Inject, + Injectable, + RequestTimeoutException, + UnauthorizedException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { RefreshToken } from '../entities/refreshToken.entity'; +import { Repository } from 'typeorm'; +import { User } from 'src/users/entities/user.entity'; +import { Request } from 'express'; +import { ConfigService } from '@nestjs/config'; +import { HashingProvider } from './hashing.provider'; +import { FindOneRefreshTokenProvider } from './findOneRefreshToken.provider'; + +@Injectable() +export class RefreshTokenRepositoryOperations { + constructor( + @InjectRepository(RefreshToken) + private readonly refreshTokenEntity: Repository, + + private readonly configService: ConfigService, + + private readonly hashingProvider: HashingProvider, + + private readonly findOneRefreshTokenProvider: FindOneRefreshTokenProvider, + ) {} + + // [1] save the refresh token to the databse + public async saveRefreshToken( + user: User, + token: string, + req?: Request, + ): Promise { + let savedRefreshTokenEntity: RefreshToken; + + let raw = this.configService.get('JWT_REFRESH_EXPIRATION'); // e.g. "604800" or "604800000" + let expirationSeconds: number; + + if (parseInt(raw) > 1000000) { + // looks like milliseconds + expirationSeconds = Math.floor(parseInt(raw) / 1000); + } else { + // already seconds + expirationSeconds = parseInt(raw); + } + + const expiresAt = new Date(Date.now() + expirationSeconds * 1000); + + const refreshTokenEntityData: Partial = { + user, + token: await this.hashingProvider.hash(token), + expiresAt, + }; + + // conditionally add the metadata fileds + if (req) { + refreshTokenEntityData.userAgent = req.headers['user-agent'] || 'unknown'; + refreshTokenEntityData.ipAddress = req.ip || 'unknown'; + } + + // create a new refresh token entity + savedRefreshTokenEntity = this.refreshTokenEntity.create( + refreshTokenEntityData, + ); + + try { + savedRefreshTokenEntity = await this.refreshTokenEntity.save( + savedRefreshTokenEntity, + ); + } catch (error) { + throw new RequestTimeoutException('Error connecting to the database'); + } + + if (!savedRefreshTokenEntity) { + throw new BadRequestException('Error saving refresh token to database'); + } + + return savedRefreshTokenEntity; + } + + // [2] find one refresh token from the database and return it + public async findOneRefreshToken(userId: string, userToken: string) { + const refreshToken = + await this.findOneRefreshTokenProvider.findRefreshToken( + userId, + userToken, + ); + + return refreshToken; + } + + // [3] invalidate/revoke a refresh token entity + public async revokeSingleRefreshToken(userId: string, userToken: string) { + let refreshToken: RefreshToken; + + refreshToken = await this.findOneRefreshTokenProvider.findRefreshToken( + userId, + userToken, + ); + + const now = new Date(); + + refreshToken.revoked = true; + refreshToken.revokedAt = now; + await this.refreshTokenEntity.save(refreshToken); + + return { + loggedOut: true, + refreshToken, + }; + } + + // [4] invalidate/revoke all refresh token entity of the user + public async revokeAllRefreshTokens(userId: string) { + const allRefreshTokenEntities = await this.refreshTokenEntity.find({ + where: { + user: { + id: userId, + }, + revoked: false, + }, + }); + + const now = new Date(); + + const revokedTokens = allRefreshTokenEntities.map((token) => { + ((token.revoked = true), (token.revokedAt = now)); + return token; + }); + + await this.refreshTokenEntity.save(revokedTokens); + + return { + revokedAllSessions: true, + }; + } +} diff --git a/backend/src/auth/providers/auth.service.ts b/backend/src/auth/providers/auth.service.ts index 08b9988a..51ad7c28 100644 --- a/backend/src/auth/providers/auth.service.ts +++ b/backend/src/auth/providers/auth.service.ts @@ -2,12 +2,39 @@ import { Injectable } from '@nestjs/common'; import { UsersService } from '../../users/providers/users.service'; import { CreateUserDto } from '../../users/dto/createUser.dto'; import { User } from '../../users/entities/user.entity'; +import { LoginUserProvider } from './loginUser.provider'; +import { AuthResponse } from '../interfaces/authResponse.interface'; +import { Response } from 'express'; @Injectable() export class AuthService { - constructor(private readonly usersService: UsersService) {} + constructor( + private readonly usersService: UsersService, - async createUser(createUserDto: CreateUserDto): Promise { - return await this.usersService.createUser(createUserDto); + private readonly loginUserProvider: LoginUserProvider, + ) {} + + // CREATE USER + async createUser( + createUserDto: CreateUserDto, + response: Response, + ): Promise { + return await this.usersService.createUser(createUserDto, response); + } + + // VALIDATE USER + public async validateUser( + email: string, + password: string, + ): Promise> { + return await this.usersService.validateUser(email, password); + } + + // LOGIN USER + public async loginUser( + user: User, + response: Response, + ): Promise { + return await this.loginUserProvider.loginUser(user, response); } } diff --git a/backend/src/auth/providers/findOneRefreshToken.provider.ts b/backend/src/auth/providers/findOneRefreshToken.provider.ts new file mode 100644 index 00000000..f85481e9 --- /dev/null +++ b/backend/src/auth/providers/findOneRefreshToken.provider.ts @@ -0,0 +1,58 @@ +import { + Injectable, + RequestTimeoutException, + UnauthorizedException, +} from '@nestjs/common'; +import { RefreshToken } from '../entities/refreshToken.entity'; +import { HashingProvider } from './hashing.provider'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +@Injectable() +export class FindOneRefreshTokenProvider { + constructor( + @InjectRepository(RefreshToken) + private readonly refreshTokenEntity: Repository, + private readonly hashingProvider: HashingProvider, + ) {} + + public async findRefreshToken(userId: string, userRefreshToken: string) { + let userTokens: RefreshToken[]; + + try { + userTokens = await this.refreshTokenEntity.find({ + where: { + user: { + id: userId, + }, + }, + relations: ['user'], + }); + } catch (error) { + throw new RequestTimeoutException('Error connecting to the database'); + } + + if (!userTokens || userTokens.length === 0) { + throw new UnauthorizedException('Invalid refresh token'); + } + + // compare provided token with each stored one + for (const tokenEntity of userTokens) { + const isMatch = await this.hashingProvider.compare( + userRefreshToken, + tokenEntity.token, + ); + + if (isMatch) { + //checkif the token is already revoked + if (tokenEntity.revoked) { + throw new UnauthorizedException('Refresh token is already revoked'); + } + + return tokenEntity; + } + } + + throw new UnauthorizedException('Invalid refresh token'); + } +} diff --git a/backend/src/auth/providers/generateTokens.provider.ts b/backend/src/auth/providers/generateTokens.provider.ts new file mode 100644 index 00000000..ef212f74 --- /dev/null +++ b/backend/src/auth/providers/generateTokens.provider.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; +import { User } from 'src/users/entities/user.entity'; + +@Injectable() +export class GenerateTokensProvider { + constructor( + private readonly jwtService: JwtService, + + private readonly configService: ConfigService, + ) {} + + public async signSingleToken( + userId: string, + expiresIn: number, + userRole: string, + payload?: any, + ) { + return await this.jwtService.signAsync( + { + sub: userId, + role: userRole, + ...payload, + }, + { + secret: this.configService.get('JWT_SECRET'), + expiresIn, + }, + ); + } + + public async generateBothTokens(user: User) { + const [accessToken, refreshToken] = await Promise.all([ + this.signSingleToken( + user.id, + this.configService.get('JWT_ACCESS_EXPIRATION'), + user.role, + { + email: user.email, + }, + ), + this.signSingleToken( + user.id, + this.configService.get('JWT_REFRESH_EXPIRATION'), + user.role, + ), + ]); + + return { + accessToken, + refreshToken, + }; + } +} diff --git a/backend/src/auth/providers/loginUser.provider.ts b/backend/src/auth/providers/loginUser.provider.ts new file mode 100644 index 00000000..1ede4434 --- /dev/null +++ b/backend/src/auth/providers/loginUser.provider.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import { User } from '../../users/entities/user.entity'; +import { Response } from 'express'; +import { ConfigService } from '@nestjs/config'; +import { GenerateTokensProvider } from './generateTokens.provider'; +import { RefreshTokenRepositoryOperations } from './RefreshTokenCrud.repository'; +import { AuthResponse } from '../interfaces/authResponse.interface'; + +@Injectable() +export class LoginUserProvider { + constructor( + private readonly configService: ConfigService, + + private readonly generateTokensProvider: GenerateTokensProvider, + + private readonly refreshTokenRepositoryOperations: RefreshTokenRepositoryOperations, + ) {} + + public async loginUser( + user: User, + response: Response, + ): Promise { + const { accessToken, refreshToken } = + await this.generateTokensProvider.generateBothTokens(user); + + await this.refreshTokenRepositoryOperations.saveRefreshToken( + user, + refreshToken, + ); + + const jwtExpirationMs = parseInt( + this.configService.get('JWT_REFRESH_EXPIRATION') || '604800000', + ); // 7 DAYS in milliseconds + const expires = new Date(Date.now() + jwtExpirationMs); + + response.cookie('authRefreshToken', refreshToken, { + secure: true, + httpOnly: true, + expires, + path: '/auth/refresh-token', + sameSite: 'none', + }); + + return { user, accessToken }; + } +} diff --git a/backend/src/auth/providers/refreshTokens.provider.ts b/backend/src/auth/providers/refreshTokens.provider.ts new file mode 100644 index 00000000..87e9190a --- /dev/null +++ b/backend/src/auth/providers/refreshTokens.provider.ts @@ -0,0 +1,145 @@ +import { + ForbiddenException, + forwardRef, + Inject, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Request } from 'express'; +import { UsersService } from 'src/users/providers/users.service'; +import { RefreshToken } from '../entities/refreshToken.entity'; +import { Repository } from 'typeorm'; +import { HashingProvider } from './hashing.provider'; +import { GenerateTokensProvider } from './generateTokens.provider'; +import { RefreshTokenRepositoryOperations } from './RefreshTokenCrud.repository'; +import { ConfigService } from '@nestjs/config'; +import { AuthResponse } from '../interfaces/authResponse.interface'; + +@Injectable() +export class RefreshTokensProvider { + constructor( + @Inject(forwardRef(() => UsersService)) + private readonly usersService: UsersService, + + @InjectRepository(RefreshToken) + private readonly refreshTokenRepository: Repository, + + private readonly hashingProvider: HashingProvider, + + private readonly generateTokensProvider: GenerateTokensProvider, + + private readonly refreshTokenRepositoryOperations: RefreshTokenRepositoryOperations, + + private readonly configService: ConfigService, + ) {} + + public async refreshTokens( + userId: string, + refreshToken: string, + ): Promise { + const user = await this.usersService.findUserById(userId); + + // find all the tokens of the user in the database + const allTokens = await this.refreshTokenRepository.find({ + where: { + user: { + id: userId, + }, + }, + relations: ['user'], + }); + + let matchingExistingToken: RefreshToken = null; + + for (const token of allTokens) { + const isMatch = await this.hashingProvider.compare( + refreshToken, + token.token, + ); + + if (isMatch) { + matchingExistingToken = token; + break; + } + } + + if (!matchingExistingToken) { + throw new ForbiddenException('Access Denied'); + } + + // verify refresh token matches + const isMatch = await this.hashingProvider.compare( + refreshToken, + matchingExistingToken.token, + ); + + if (!isMatch) { + throw new ForbiddenException('Invalid refresh token'); + } + + const now = new Date(); + + // if token has already been revoked + if (matchingExistingToken.revoked) { + throw new UnauthorizedException( + 'Token already revoked. Kindly login to get a new refresh token', + ); + } + + // if refresh token has expired, mark it has revoked and let the user log in again to get a new access and refresh token + if (matchingExistingToken.expiresAt < now) { + matchingExistingToken.revoked = true; + matchingExistingToken.revokedAt = now; + await this.refreshTokenRepository.save(matchingExistingToken); + + throw new UnauthorizedException( + 'Your session has expired. Please login again', + ); + + // Then in the frontend, when you get a 401 Unauthorized from /refresh-token, redirect user to login. + } + + // if refresh token is still valid, generate only access token + if (matchingExistingToken.expiresAt > now) { + const newAccessToken = await this.generateTokensProvider.signSingleToken( + user.id, + this.configService.get('JWT_ACCESS_EXPIRATION'), + user.role, + { + email: user.email, + }, + ); + + return { + user, + accessToken: newAccessToken, + }; + } + + // generate new tokens if the refresh token has expired + // const newTokens = + // await this.generateTokensProvider.generateBothTokens(user); + + // const hashedNewRefreshToken = await this.hashingProvider.hashPassword( + // newTokens.refreshToken, + // ); + + // const newRefreshTokenEntity = this.refreshTokenRepository.create({ + // token: hashedNewRefreshToken, + // expiresAt: new Date(Date.now() + this.jwtConfiguration.refreshTokenTTL), + // user: user, + // userId: userId, + // userAgent: req.headers['user-agent'] || '', + // ipAddress: req.ip, + // }); + + // await this.refreshTokenRepository.save(newRefreshTokenEntity); + + // return { + // user, + // accessToken: newTokens.accessToken, + // refreshToken: newTokens.refreshToken, + // }; + } +} diff --git a/backend/src/users/providers/createUser.provider.ts b/backend/src/users/providers/createUser.provider.ts index 623ba1ea..9d1df2df 100644 --- a/backend/src/users/providers/createUser.provider.ts +++ b/backend/src/users/providers/createUser.provider.ts @@ -1,39 +1,78 @@ -import { Injectable, ConflictException } from '@nestjs/common'; +import { ConflictException, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { User } from '../entities/user.entity'; +import { Repository } from 'typeorm'; import { CreateUserDto } from '../dto/createUser.dto'; -import { HashingProvider } from '../../auth/providers/hashing.provider'; -import { handleError } from '../../utils/error'; +import { ErrorCatch } from '../../utils/error'; +import { HashingProvider } from 'src/auth/providers/hashing.provider'; +import { AuthResponse } from 'src/auth/interfaces/authResponse.interface'; +import { Response } from 'express'; +import { ConfigService } from '@nestjs/config'; +import { GenerateTokensProvider } from 'src/auth/providers/generateTokens.provider'; +import { RefreshTokenRepositoryOperations } from 'src/auth/providers/RefreshTokenCrud.repository'; @Injectable() export class CreateUserProvider { constructor( @InjectRepository(User) private readonly userRepository: Repository, + private readonly hashingProvider: HashingProvider, + + private readonly configService: ConfigService, + + private readonly generateTokensProvider: GenerateTokensProvider, + + private readonly refreshTokenRepositoryOperations: RefreshTokenRepositoryOperations, ) {} - async createUser(createUserDto: CreateUserDto): Promise { + public async createUser( + createUserDto: CreateUserDto, + response: Response, + ): Promise { try { const existingUser = await this.userRepository.findOne({ - where: { email: createUserDto.email }, + where: { + email: createUserDto.email, + }, }); if (existingUser) { - throw new ConflictException('User with this email already exists'); + throw new ConflictException('User already exists.'); } - const hashedPassword = await this.hashingProvider.hash(createUserDto.password); + let password = await this.hashingProvider.hash(createUserDto.password); + + createUserDto.password = password; + + let user = this.userRepository.create(createUserDto); + + user = await this.userRepository.save(user); + + const { accessToken, refreshToken } = + await this.generateTokensProvider.generateBothTokens(user); + + await this.refreshTokenRepositoryOperations.saveRefreshToken( + user, + refreshToken, + ); + + const jwtExpirationMs = parseInt( + this.configService.get('JWT_REFRESH_EXPIRATION') || '604800000', + ); // 7 DAYS in milliseconds + const expires = new Date(Date.now() + jwtExpirationMs); - const newUser = this.userRepository.create({ - ...createUserDto, - password: hashedPassword, + response.cookie('authRefreshToken', refreshToken, { + secure: true, + httpOnly: true, + expires, + path: '/auth/refresh-token', + sameSite: 'none', }); - return await this.userRepository.save(newUser); + return { user, accessToken }; } catch (error) { - handleError(error, 'Failed to create user'); + ErrorCatch(error, 'Failed to create user'); } } -} \ No newline at end of file +} diff --git a/backend/src/users/providers/users.service.ts b/backend/src/users/providers/users.service.ts index 1f30f377..6a0c7a34 100644 --- a/backend/src/users/providers/users.service.ts +++ b/backend/src/users/providers/users.service.ts @@ -2,12 +2,43 @@ import { Injectable } from '@nestjs/common'; import { CreateUserDto } from '../dto/createUser.dto'; import { User } from '../entities/user.entity'; import { CreateUserProvider } from './createUser.provider'; +import { FindOneUserByIdProvider } from './findOneUserById.provider'; +import { FindOneUserByEmailProvider } from './findOneUserByEmail.provider'; +import { ValidateUserProvider } from './validateUser.provider'; +import { AuthResponse } from 'src/auth/interfaces/authResponse.interface'; +import { Response } from 'express'; @Injectable() export class UsersService { - constructor(private readonly createUserProvider: CreateUserProvider) {} + constructor( + private readonly createUserProvider: CreateUserProvider, + private readonly findOneUserByIdProvider: FindOneUserByIdProvider, + private readonly findOneUserByEmailProvider: FindOneUserByEmailProvider, + private readonly validateUserProvider: ValidateUserProvider, + ) {} - async createUser(createUserDto: CreateUserDto): Promise { - return await this.createUserProvider.createUser(createUserDto); + async createUser( + createUserDto: CreateUserDto, + response: Response, + ): Promise { + return await this.createUserProvider.createUser(createUserDto, response); + } + + // FIND USER BY ID + public async findUserById(id: string): Promise { + return await this.findOneUserByIdProvider.getUser(id); + } + + // FIND USER BY EMAIL + public async findUserByEmail(email: string): Promise { + return await this.findOneUserByEmailProvider.getUser(email); + } + + // VALIDATE USER + public async validateUser( + email: string, + password: string, + ): Promise> { + return await this.validateUserProvider.validateUser(email, password); } } diff --git a/backend/src/users/providers/validateUser.provider.ts b/backend/src/users/providers/validateUser.provider.ts new file mode 100644 index 00000000..9a63a6f9 --- /dev/null +++ b/backend/src/users/providers/validateUser.provider.ts @@ -0,0 +1,44 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { FindOneUserByEmailProvider } from './findOneUserByEmail.provider'; +import { User } from '../entities/user.entity'; +import { ErrorCatch } from '../../utils/error'; +import { HashingProvider } from 'src/auth/providers/hashing.provider'; + +@Injectable() +export class ValidateUserProvider { + constructor( + private readonly findOneUserByEmail: FindOneUserByEmailProvider, + + private readonly hashingProvider: HashingProvider, + ) {} + + public async validateUser( + email: string, + userPassword: string, + ): Promise> { + try { + const user = await this.findOneUserByEmail.getUser(email); + + const isPasswordValid = await this.hashingProvider.compare( + userPassword, + user.password, + ); + + if (!isPasswordValid) { + throw new UnauthorizedException('Invalid email/password'); + } + + const { + password, + passwordResetExpiresIn, + passwordResetToken, + refreshTokens, + ...result + } = user; + + return result; + } catch (error) { + ErrorCatch(error, 'Error validating user'); + } + } +} diff --git a/backend/src/users/users.module.ts b/backend/src/users/users.module.ts index e4a504d4..9fb72354 100644 --- a/backend/src/users/users.module.ts +++ b/backend/src/users/users.module.ts @@ -7,20 +7,18 @@ import { User } from './entities/user.entity'; import { FindOneUserByEmailProvider } from './providers/findOneUserByEmail.provider'; import { FindOneUserByIdProvider } from './providers/findOneUserById.provider'; import { CreateUserProvider } from './providers/createUser.provider'; - +import { ValidateUserProvider } from './providers/validateUser.provider'; @Module({ - imports: [ - TypeOrmModule.forFeature([User]), - forwardRef(() => AuthModule) - ], + imports: [TypeOrmModule.forFeature([User]), forwardRef(() => AuthModule)], controllers: [UsersController], providers: [ UsersService, FindOneUserByEmailProvider, FindOneUserByIdProvider, CreateUserProvider, + ValidateUserProvider, ], - exports: [UsersService] + exports: [UsersService], }) export class UsersModule {} diff --git a/backend/src/utils/error.ts b/backend/src/utils/error.ts index 81c5c379..6794118e 100644 --- a/backend/src/utils/error.ts +++ b/backend/src/utils/error.ts @@ -1,16 +1,9 @@ import { HttpException, InternalServerErrorException } from '@nestjs/common'; -export function handleError(error: any, message?: string): never { +export function ErrorCatch(error: any, message: string) { if (error instanceof HttpException) { throw error; } - throw new InternalServerErrorException( - message || 'An unexpected error occurred. Please try again later.' - ); + throw new InternalServerErrorException(`${message}: Internal server error`); } - -// Alias for compatibility with existing code -export function ErrorCatch(error: any, message?: string): never { - return handleError(error, message); -} \ No newline at end of file