Skip to content

Commit 4e30298

Browse files
author
Ángel De La Cruz
committed
Token with Fastify
1 parent afde09f commit 4e30298

22 files changed

+449
-31
lines changed

src/app.module.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { APP_GUARD } from '@nestjs/core';
12
import { Module } from '@nestjs/common';
3+
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
4+
5+
26
import { ConfigModule } from '@nestjs/config';
37
import { PrismaModule } from './prisma/prisma.module';
48
import { AuthModule } from './auth/auth.module';
5-
import { APP_GUARD } from '@nestjs/core';
6-
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
7-
9+
import { AtGuard } from './auth/guards';
810

911
@Module({
1012
imports: [ConfigModule.forRoot({
@@ -17,7 +19,11 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
1719
{
1820
provide: APP_GUARD,
1921
useClass: ThrottlerGuard
20-
}
22+
},
23+
{
24+
provide: APP_GUARD,
25+
useClass: AtGuard,
26+
},
2127
]
2228
})
2329
export class AppModule { }

src/auth/abstract/auth-jwt.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
2+
import { ConflictException, ForbiddenException, InternalServerErrorException } from "@nestjs/common";
3+
import { JwtService } from "@nestjs/jwt";
4+
import * as bcrypt from 'bcrypt';
5+
import { Tokens } from "../types";
6+
import { PrismaService } from '../../prisma/prisma.service';
7+
import { HashingException, UpdateHashException } from './../../exceptions';
8+
9+
10+
export abstract class AuthJwt {
11+
constructor(protected readonly jwtService: JwtService, protected readonly prismaService: PrismaService) { }
12+
13+
/**
14+
* This TypeScript function generates access and refresh tokens for a user based on their user ID and
15+
* email.
16+
* @param {number} userId - The `userId` parameter in the `getToken` function represents the unique
17+
* identifier of a user for whom the tokens are being generated. It is of type `number` and is used
18+
* to associate the tokens with a specific user in the system.
19+
* @param {string} email - The `email` parameter in the `getToken` function is a string that
20+
* represents the email address of a user. It is used as one of the payload properties when
21+
* generating the access token and refresh token for the user identified by the `userId`.
22+
* @returns The `getToken` function returns a Promise that resolves to an object containing two
23+
* properties: `access_token` and `refresh_token`. These tokens are generated using the `signAsync`
24+
* method from the `jwtService` with specific configurations such as `JWT_SECRET` for access token
25+
* and `RT` for refresh token. The access token expires in 10 minutes, while the refresh token
26+
* expires in 7
27+
*/
28+
async getToken(userId: number, email: string): Promise<Tokens> {
29+
try {
30+
const [at, rt] = await Promise.all([
31+
this.jwtService.signAsync(
32+
{
33+
sub: userId,
34+
email
35+
},
36+
{
37+
secret: process.env.JWT_SECRET,
38+
expiresIn: 60 * 10,
39+
}
40+
),
41+
this.jwtService.signAsync(
42+
{
43+
sub: userId,
44+
email
45+
},
46+
{
47+
secret: process.env.RT,
48+
expiresIn: 60 * 60 * 24 * 7,
49+
},
50+
)
51+
])
52+
53+
return {
54+
access_token: at,
55+
refresh_token: rt
56+
}
57+
} catch (error) {
58+
throw new ConflictException('Something bad happened to get all token.')
59+
}
60+
}
61+
62+
/**
63+
* The `updatedRtHash` function updates the refresh token hash for a user in a database using Prisma
64+
* ORM in TypeScript.
65+
* @param {number} userId - The `userId` parameter is a number that represents the unique identifier of
66+
* a user in the system.
67+
* @param {string} rt - The `rt` parameter in the `updatedRtHash` function is a string representing a
68+
* refresh token. It is used to generate a hash value that will be stored in the database for the
69+
* corresponding user identified by the `userId`.
70+
*/
71+
72+
async updatedRtHash(userId: number, rt: string): Promise<void> {
73+
try {
74+
const hash = await this.hash(rt);
75+
76+
await this.prismaService.user.update({
77+
where: { id: userId },
78+
data: {
79+
refreshTokenHash: hash,
80+
},
81+
});
82+
} catch (error) {
83+
throw new UpdateHashException(userId, error.message);
84+
}
85+
}
86+
87+
/**
88+
* The function asynchronously hashes a given string using bcrypt with a specified salt or number of
89+
* rounds.
90+
* @param {string} data - The `data` parameter in the `hash` function is a string that represents the
91+
* data that you want to hash using the bcrypt algorithm. This data will be securely hashed using a
92+
* salt and the specified number of rounds before being returned as a hashed string.
93+
* @returns The `hash` function is returning a Promise that resolves to a string, which is the hashed
94+
* value of the input `data` string after using the bcrypt hashing algorithm with the specified salt
95+
* or rounds.
96+
*/
97+
async hash(data: string): Promise<string> {
98+
try {
99+
const saltOrRounds = 10;
100+
const hash = await bcrypt.hash(data, saltOrRounds);
101+
return hash;
102+
} catch (error) {
103+
throw new HashingException(error.message);
104+
}
105+
}
106+
107+
/**
108+
* The `logout` function updates the `refreshTokenHash` field to null for a user with a specific ID.
109+
* @param {number} sub - The `sub` parameter in the `logout` function is a number that represents the
110+
* user's ID or subject identifier. It is used to identify the user whose refresh token hash needs to
111+
* be updated to `null` during the logout process.
112+
*/
113+
async logout(sub: number) {
114+
try {
115+
await this.prismaService.user.update({
116+
where: { id: sub },
117+
data: {
118+
refreshTokenHash: null,
119+
},
120+
});
121+
} catch (error) {
122+
throw new UpdateHashException(sub, error.message);
123+
}
124+
}
125+
126+
127+
/**
128+
* The function `refreshToken` in TypeScript checks and refreshes a user's token based on their ID
129+
* and refresh token hash stored in the database.
130+
* @param {number} sub - The `sub` parameter in the `refreshToken` function likely stands for
131+
* "subject" and is used to identify the user for whom the token is being refreshed. It is typically
132+
* a unique identifier for the user in the system, such as a user ID.
133+
* @param {string} token - The `token` parameter in the `refreshToken` function is a string that
134+
* represents the refresh token provided by the user for refreshing their authentication. This token
135+
* is used to verify the user's identity and generate new access tokens for continued access to the
136+
* application.
137+
* @returns The `refreshToken` function is returning a set of tokens after successfully refreshing
138+
* the user's refresh token. The tokens are generated using the `getToken` method and then a new
139+
* refresh token hash is generated and updated for the user. Finally, the function returns the newly
140+
* generated tokens.
141+
*/
142+
async refreshToken(sub: number, token: string) {
143+
try {
144+
145+
const user = await this.prismaService.user.findFirst({
146+
where: {
147+
id: sub,
148+
},
149+
});
150+
151+
152+
if (!user || !user.refreshTokenHash) {
153+
throw new ForbiddenException('Authentication failed. Please check your credentials.');
154+
}
155+
156+
const rtMatches = await bcrypt.compare(token, user.refreshTokenHash);
157+
if (!rtMatches) {
158+
throw new ForbiddenException('El token ha expirado.');
159+
}
160+
161+
const tokens = await this.getToken(user.id, user.email);
162+
163+
const hashToken = await this.hash(tokens.refresh_token);
164+
await this.updatedRtHash(user.id, hashToken);
165+
166+
return tokens;
167+
} catch (error) {
168+
169+
if (error instanceof ForbiddenException) {
170+
throw error;
171+
}
172+
throw new InternalServerErrorException('Error al procesar el token de actualización.');
173+
}
174+
}
175+
}

src/auth/auth.controller.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { AuthService } from './auth.service';
2-
import { Body, Controller, Post } from '@nestjs/common';
2+
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
33
import { AuthDto } from './dto';
44
import { Tokens } from './types';
5+
import { AtGuard } from './guards';
6+
import { RtGuard } from './guards/rt-guards';
7+
import { GetCurrentUser, GetCurrentUserId, Public } from './decorators';
58

69
@Controller({
710
path: 'auth',
@@ -11,24 +14,34 @@ export class AuthController {
1114

1215
constructor(private authService: AuthService) { }
1316

17+
@Public()
1418
@Post('signup')
1519
async signup(@Body() dto: AuthDto): Promise<Tokens> {
1620
return await this.authService.signup(dto)
1721
}
1822

23+
@Public()
1924
@Post('signin')
2025
async signin() {
2126
await this.authService.signin()
2227
}
2328

29+
@UseGuards(AtGuard)
2430
@Post('logout')
25-
async logout() {
26-
await this.authService.logout()
31+
async logout(@GetCurrentUserId() userId: number) {
32+
await this.authService.logout(userId)
2733
}
2834

29-
@Post('refresh-token')
30-
async refreshToken() {
31-
await this.authService.refreshToken()
35+
36+
@Public()
37+
@UseGuards(RtGuard)
38+
@Post('refresh')
39+
async refreshToken(
40+
@GetCurrentUserId() userId: number,
41+
@GetCurrentUser('refreshToken') refreshToken: string
42+
) {
43+
console.log(refreshToken)
44+
await this.authService.refreshToken(userId, refreshToken)
3245
}
3346

3447
}

src/auth/auth.module.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
import { Module } from '@nestjs/common';
22
import { AuthController } from './auth.controller';
33
import { AuthService } from './auth.service';
4+
import { AtStrategy, RtStrategy } from './strategies';
5+
import { JwtModule } from '@nestjs/jwt';
6+
import { jwtConstants } from './constants';
47

58
@Module({
9+
imports: [
10+
11+
JwtModule.register({
12+
global: true,
13+
secret: jwtConstants.secret,
14+
signOptions: { expiresIn: '60s' },
15+
}),
16+
],
617
controllers: [AuthController],
7-
providers: [AuthService],
18+
providers: [AuthService, AtStrategy, RtStrategy],
819

920
})
1021
export class AuthModule { }

src/auth/auth.service.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
1-
import { BadRequestException, ConflictException, HttpException, HttpStatus, Injectable } from '@nestjs/common';
2-
import * as bcrypt from 'bcrypt';
1+
import { ConflictException, HttpException, HttpStatus, Injectable } from '@nestjs/common';
2+
import { JwtService } from '@nestjs/jwt';
33
import { PrismaService } from '../prisma/prisma.service';
44
import { AuthDto } from './dto';
55
import { Tokens } from './types';
66

7+
import { AuthJwt } from './abstract/auth-jwt';
8+
79
@Injectable()
8-
export class AuthService {
10+
export class AuthService extends AuthJwt {
911

10-
constructor(private readonly prismaService: PrismaService) { }
12+
constructor(public prismaService: PrismaService, public jwtService: JwtService) {
13+
super(jwtService, prismaService);
14+
}
1115

1216
async signup(dto: AuthDto): Promise<Tokens> {
1317
try {
14-
15-
if (this.validateEmailExiste(dto.email)) throw new ConflictException("A user with this email already exists.")
16-
17-
const saltOrRounds = 10;
18-
const hash = await bcrypt.hash(dto.password, saltOrRounds);
19-
18+
if (await this.validateEmailExiste(dto.email)) throw new ConflictException("A user with this email already exists.")
19+
const hash = await this.hash(dto.password);
2020
const created = await this.prismaService.user.create({
2121
data: {
2222
name: dto.name,
@@ -25,7 +25,10 @@ export class AuthService {
2525
}
2626
})
2727

28-
return
28+
const tokens = await this.getToken(created.id, created.email);
29+
await this.updatedRtHash(created.id, tokens.refresh_token);
30+
31+
return tokens
2932
} catch (error) {
3033
if (error instanceof ConflictException) {
3134
throw error;
@@ -34,9 +37,11 @@ export class AuthService {
3437
}
3538

3639
}
37-
async signin() { }
38-
async logout() { }
39-
async refreshToken() { }
40+
41+
async signin() {
42+
43+
44+
}
4045

4146
private async validateEmailExiste(email: string): Promise<boolean> {
4247
try {
@@ -51,5 +56,4 @@ export class AuthService {
5156
}
5257
}
5358

54-
5559
}

src/auth/constants.ts

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

src/auth/decorators/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './get-current-user.decorator';
2+
export * from './get-current-user-id.decorator';
3+
export * from './public.decorator';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { SetMetadata } from '@nestjs/common';
2+
3+
export const IS_PUBLIC_KEY = 'isPublic';
4+
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

0 commit comments

Comments
 (0)