Skip to content

Commit 708b308

Browse files
author
Ángel De La Cruz
committed
Finish Signin, SignUp, Refresh Token and validate all tokens with Guards
1 parent 4e30298 commit 708b308

File tree

13 files changed

+135
-60
lines changed

13 files changed

+135
-60
lines changed

src/auth/abstract/auth-jwt.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11

2-
import { ConflictException, ForbiddenException, InternalServerErrorException } from "@nestjs/common";
2+
import { ConflictException, ForbiddenException, HttpException, HttpStatus, InternalServerErrorException } from "@nestjs/common";
33
import { JwtService } from "@nestjs/jwt";
44
import * as bcrypt from 'bcrypt';
55
import { Tokens } from "../types";
66
import { PrismaService } from '../../prisma/prisma.service';
77
import { HashingException, UpdateHashException } from './../../exceptions';
8+
import { User } from "@prisma/client";
89

910

1011
export abstract class AuthJwt {
@@ -110,14 +111,16 @@ export abstract class AuthJwt {
110111
* user's ID or subject identifier. It is used to identify the user whose refresh token hash needs to
111112
* be updated to `null` during the logout process.
112113
*/
113-
async logout(sub: number) {
114+
async logout(sub: number): Promise<boolean> {
114115
try {
115-
await this.prismaService.user.update({
116+
const result = await this.prismaService.user.update({
116117
where: { id: sub },
117118
data: {
118119
refreshTokenHash: null,
119120
},
120121
});
122+
123+
return !!result;
121124
} catch (error) {
122125
throw new UpdateHashException(sub, error.message);
123126
}
@@ -172,4 +175,54 @@ export abstract class AuthJwt {
172175
throw new InternalServerErrorException('Error al procesar el token de actualización.');
173176
}
174177
}
178+
179+
/**
180+
* This TypeScript function asynchronously finds a user by their email using Prisma.
181+
* @param {string} email - The `findUserEmail` function is an asynchronous function that takes an
182+
* email address as a parameter and returns a Promise that resolves to a `User` object. The function
183+
* uses the `prismaService` to query the database and find the first user with the specified email
184+
* address.
185+
* @returns The `findUserEmail` function is returning a Promise that resolves to a `User` object.
186+
*/
187+
async findUserEmail(email: string): Promise<User> {
188+
const user = await this.prismaService.user.findFirst({
189+
where: {
190+
email
191+
}
192+
})
193+
return user;
194+
}
195+
196+
197+
/**
198+
* The function `compareUserPassword` asynchronously compares an authentication password with a hashed
199+
* password using bcrypt and returns a boolean indicating whether they match.
200+
* @param {string} authPassword - The `authPassword` parameter is the password provided by the user
201+
* during authentication, typically entered through a login form or API request. This password needs to
202+
* be compared with the hashed password stored in the database (retrieved as `prismaPassword`) to
203+
* verify the user's identity. The `compare
204+
* @param {string} prismaPassword - The `prismaPassword` parameter in the `compareUserPassword`
205+
* function refers to the hashed password stored in your database, typically hashed using a secure
206+
* hashing algorithm like bcrypt. When a user tries to authenticate, their input password
207+
* (`authPassword`) is compared with the hashed password retrieved from the database to
208+
* @returns A boolean value is being returned. If the authentication password matches the Prisma
209+
* password, the function returns `true`.
210+
*/
211+
async compareUserPassword(authPassword: string, prismaPassword: string): Promise<boolean> {
212+
try {
213+
214+
const rtMatches = await bcrypt.compare(authPassword, prismaPassword);
215+
if (!rtMatches) {
216+
throw new ForbiddenException('Authentication failed. Please check your credentials.');
217+
}
218+
219+
return rtMatches;
220+
} catch (error) {
221+
if (error instanceof ForbiddenException) {
222+
throw error
223+
}
224+
throw new HttpException("An unexpected error occurred. Please try again later.", HttpStatus.INTERNAL_SERVER_ERROR);
225+
}
226+
}
227+
175228
}

src/auth/auth.controller.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { AuthService } from './auth.service';
22
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
3-
import { AuthDto } from './dto';
3+
import { AuthDto, SigninDto } from './dto';
44
import { Tokens } from './types';
55
import { AtGuard } from './guards';
66
import { RtGuard } from './guards/rt-guards';
7-
import { GetCurrentUser, GetCurrentUserId, Public } from './decorators';
7+
import { GetCurrentUser, GetCurrentUserId, GetRefreshToken, Public } from './decorators';
88

99
@Controller({
1010
path: 'auth',
@@ -22,14 +22,15 @@ export class AuthController {
2222

2323
@Public()
2424
@Post('signin')
25-
async signin() {
26-
await this.authService.signin()
25+
async signin(@Body() dto: SigninDto) {
26+
return await this.authService.signin(dto)
2727
}
2828

2929
@UseGuards(AtGuard)
3030
@Post('logout')
3131
async logout(@GetCurrentUserId() userId: number) {
32-
await this.authService.logout(userId)
32+
const result = await this.authService.logout(userId);
33+
return result
3334
}
3435

3536

@@ -38,10 +39,9 @@ export class AuthController {
3839
@Post('refresh')
3940
async refreshToken(
4041
@GetCurrentUserId() userId: number,
41-
@GetCurrentUser('refreshToken') refreshToken: string
42+
@GetRefreshToken() refreshToken: string
4243
) {
43-
console.log(refreshToken)
44-
await this.authService.refreshToken(userId, refreshToken)
44+
return await this.authService.refreshToken(userId, refreshToken)
4545
}
4646

4747
}

src/auth/auth.service.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { ConflictException, HttpException, HttpStatus, Injectable } from '@nestjs/common';
1+
import { ConflictException, ForbiddenException, HttpException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common';
22
import { JwtService } from '@nestjs/jwt';
33
import { PrismaService } from '../prisma/prisma.service';
4-
import { AuthDto } from './dto';
4+
import { AuthDto, SigninDto } from './dto';
55
import { Tokens } from './types';
66

77
import { AuthJwt } from './abstract/auth-jwt';
8+
import { UpdateHashException } from '../exceptions/UpdateHashException';
9+
import { HashingException } from '../exceptions/HashingException';
810

911
@Injectable()
1012
export class AuthService extends AuthJwt {
@@ -30,17 +32,42 @@ export class AuthService extends AuthJwt {
3032

3133
return tokens
3234
} catch (error) {
33-
if (error instanceof ConflictException) {
35+
const allowedExceptions = [UnauthorizedException, UpdateHashException, ConflictException, HashingException];
36+
37+
if (allowedExceptions.some((exception) => error instanceof exception)) {
3438
throw error;
3539
}
3640
throw new HttpException("An unexpected error occurred. Please try again later.", HttpStatus.INTERNAL_SERVER_ERROR);
3741
}
3842

3943
}
4044

41-
async signin() {
45+
async signin(dto: SigninDto): Promise<Tokens> {
46+
try {
47+
if (!await this.validateEmailExiste(dto.email)) throw new UnauthorizedException("Authentication failed. Please check your credentials.");
48+
49+
const user = await this.findUserEmail(dto.email);
50+
51+
await this.compareUserPassword(dto.password, user.password);
52+
53+
const tokens = await this.getToken(user.id, user.email);
54+
55+
await this.updatedRtHash(user.id, tokens.refresh_token);
4256

57+
return tokens;
4358

59+
} catch (error) {
60+
const allowedExceptions = [UnauthorizedException, ForbiddenException];
61+
62+
if (allowedExceptions.some((exception) => error instanceof exception)) {
63+
throw error;
64+
}
65+
66+
throw new HttpException(
67+
"An unexpected error occurred. Please try again later.",
68+
HttpStatus.INTERNAL_SERVER_ERROR
69+
);
70+
}
4471
}
4572

4673
private async validateEmailExiste(email: string): Promise<boolean> {

src/auth/decorators/get-current-user-id.decorator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createParamDecorator, ExecutionContext } from "@nestjs/common";
22

33
export const GetCurrentUserId = createParamDecorator((data: string | undefined, context: ExecutionContext) => {
44
const request = context.switchToHttp().getRequest();
5+
56
if (!data) return request.user.sub;
6-
return request.user['user'].sub
7+
return request.user?.sub
78
})
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
2+
3+
export const GetRefreshToken = createParamDecorator((data: string | undefined, context: ExecutionContext) => {
4+
const request = context.switchToHttp().getRequest();
5+
return request?.refreshToken
6+
})

src/auth/decorators/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './get-current-user.decorator';
22
export * from './get-current-user-id.decorator';
3+
export * from './get-refresh-token.decorator';
34
export * from './public.decorator';

src/auth/guards/at-guards.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export class AtGuard extends AuthGuard('jwt') {
1717

1818
async canActivate(context: ExecutionContext) {
1919

20-
2120
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
2221
context.getHandler(),
2322
context.getClass(),
@@ -34,15 +33,11 @@ export class AtGuard extends AuthGuard('jwt') {
3433
}
3534

3635
try {
37-
// Validar el token (puedes usar JwtService u otro mecanismo)
3836
const payload = await this.validateToken(token);
39-
40-
request.user = payload; // Adjuntar usuario decodificado a la solicitud
37+
request.user = payload;
4138
return true;
4239
} catch (err) {
43-
console.error(err)
44-
console.error('Error al validar el token:', err.message);
45-
return false;
40+
throw err;
4641
}
4742
}
4843

@@ -51,13 +46,13 @@ export class AtGuard extends AuthGuard('jwt') {
5146
try {
5247
return await this.jwService.verifyAsync(token, { secret });
5348
} catch (error) {
54-
if (error.name === 'TokenExpiredError') {
55-
throw new UnauthorizedException('El token ha expirado.');
56-
} else if (error.name === 'JsonWebTokenError') {
57-
throw new UnauthorizedException('Token inválido o firma incorrecta.');
58-
} else {
59-
throw new UnauthorizedException('Error al procesar el token.');
60-
}
49+
const errorMessages: Record<string, string> = {
50+
TokenExpiredError: 'El token ha expirado.',
51+
JsonWebTokenError: 'Token inválido o firma incorrecta.',
52+
};
53+
54+
const message = errorMessages[error.name] || 'Error al procesar el token.';
55+
throw new UnauthorizedException(message);
6156
}
6257
}
6358
}

src/auth/guards/rt-guards.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,35 @@
1-
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
1+
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
22
import { JwtService } from '@nestjs/jwt';
33
import { AuthGuard } from '@nestjs/passport';
4-
import { Observable } from 'rxjs';
5-
4+
@Injectable()
65
export class RtGuard extends AuthGuard('jwt-refresh') {
76
constructor(private readonly jwService: JwtService) {
87
super()
98
}
109

11-
12-
anActivate(
10+
async canActivate(
1311
context: ExecutionContext,
14-
): boolean | Promise<boolean> | Observable<boolean> {
12+
) {
1513
const request = context.switchToHttp().getRequest();
16-
const token = request.headers?.refreshtoken?.split(' ')[1]
17-
console.log(token)
18-
const payload = this.validateToken(token);
19-
console.log(payload)
20-
return false
14+
const token = request.headers?.refreshtoken?.split(' ')[1];
15+
const payload = await this.validateToken(token);
16+
request.user = payload;
17+
request.refreshToken = token;
18+
return true
2119
}
2220

2321
private async validateToken(token: string) {
2422
const secret = process.env.RT;
2523
try {
2624
return await this.jwService.verifyAsync(token, { secret });
2725
} catch (error) {
28-
if (error.name === 'TokenExpiredError') {
29-
throw new UnauthorizedException('El token ha expirado.');
30-
} else if (error.name === 'JsonWebTokenError') {
31-
throw new UnauthorizedException('Token inválido o firma incorrecta.');
32-
} else {
33-
throw new UnauthorizedException('Error al procesar el token.');
34-
}
26+
const errorMessages: Record<string, string> = {
27+
TokenExpiredError: 'El token ha expirado.',
28+
JsonWebTokenError: 'Token inválido o firma incorrecta.',
29+
};
30+
31+
const message = errorMessages[error.name] || 'Error al procesar el token.';
32+
throw new UnauthorizedException(message);
3533
}
3634
}
3735
}

src/auth/strategies/at.trategy.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export class AtStrategy extends PassportStrategy(Strategy, 'jwt') {
1818
}
1919

2020
validate(payload: JwtPayload) {
21-
console.log(payload, 'Hola')
2221
return payload;
2322
}
2423
}

src/auth/strategies/rt.strategy.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,8 @@ export class RtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
1313
})
1414
}
1515
async validate(req: FastifyRequest, payload: any) {
16-
console.log(req, 'Hola')
17-
// const refreshToken = req.get('authorization').replace('Bearer', '').trim();
1816
return {
1917
...payload,
20-
// refreshToken,
2118
};
2219
}
2320
}

src/auth/types/tokens.tyoe.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export type Tokens = {
22
access_token: string;
33
refresh_token: string;
4+
role?: string;
45
}

src/main.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { NestFactory } from '@nestjs/core';
1+
import { NestFactory, Reflector } from '@nestjs/core';
22
import { AppModule } from './app.module';
33
import { ValidationPipe } from '@nestjs/common';
44
import helmet from 'helmet';
55
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
66

7+
78
declare const module: any;
89

910
async function bootstrap() {
@@ -17,10 +18,6 @@ async function bootstrap() {
1718
whitelist: true,
1819
}),
1920
);
20-
21-
22-
23-
2421
app.setGlobalPrefix('api');
2522

2623
app.use(helmet())

webpack-hmr.config.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');
55
module.exports = function (options, webpack) {
66
return {
77
...options,
8-
entry: ['webpack/hot/poll?100', options.entry],
8+
entry: ['webpack/hot/poll?20', options.entry],
99
externals: [
1010
nodeExternals({
11-
allowlist: ['webpack/hot/poll?100'],
11+
allowlist: ['webpack/hot/poll?20'],
1212
}),
1313
],
1414
plugins: [
@@ -17,7 +17,7 @@ module.exports = function (options, webpack) {
1717
new webpack.WatchIgnorePlugin({
1818
paths: [/\.js$/, /\.d\.ts$/],
1919
}),
20-
new RunScriptWebpackPlugin({ name: options.output.filename, autoRestart: false }),
20+
new RunScriptWebpackPlugin({ name: options.output.filename, autoRestart: true }),
2121
],
2222
};
2323
};

0 commit comments

Comments
 (0)