Skip to content

Commit

Permalink
feat: jwt strategy implementation and jwt update strategy (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
nathan2slime authored Jul 12, 2024
1 parent 9c4f629 commit 56c5480
Show file tree
Hide file tree
Showing 28 changed files with 572 additions and 75 deletions.
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pnpm format
pnpm lint
pnpm test
git add .
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@
"@nestjs/terminus": "^10.2.3",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"commitizen": "^4.3.0",
"cookie-parser": "^1.4.6",
"dotenv": "^16.4.5",
"husky": "^9.0.11",
"lint-staged": "^15.2.7",
Expand All @@ -53,6 +55,8 @@
"@nestjs/schematics": "^10.0.0",
"@nestjs/swagger": "^7.4.0",
"@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
Expand Down
264 changes: 264 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

12 changes: 0 additions & 12 deletions src/app/app.controller.ts

This file was deleted.

9 changes: 5 additions & 4 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from '~/app/auth/auth.module';

import { env } from '~/env';

@Module({
Expand All @@ -14,8 +14,9 @@ import { env } from '~/env';
return connection;
},
}),
AuthModule,
],
controllers: [AppController],
providers: [AppService],
controllers: [],
providers: [],
})
export class AppModule {}
8 changes: 0 additions & 8 deletions src/app/app.service.ts

This file was deleted.

17 changes: 17 additions & 0 deletions src/app/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Body, Controller, HttpStatus, Post, Res } from '@nestjs/common';
import { Response } from 'express';

import { AuthService } from '~/app/auth/auth.service';
import { SignUpDto } from '~/app/auth/auth.dto';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Post('signup')
async signUp(@Body() body: SignUpDto, @Res() res: Response) {
const data = await this.authService.signUp(body);

return res.status(HttpStatus.CREATED).json(data);
}
}
3 changes: 3 additions & 0 deletions src/app/auth/auth.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Reflector } from '@nestjs/core';

export const Roles = Reflector.createDecorator<string[]>();
17 changes: 17 additions & 0 deletions src/app/auth/auth.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';

export class SignUpDto {
@ApiProperty()
@IsEmail()
@IsNotEmpty()
email: string;

@ApiProperty()
@IsNotEmpty()
name: string;

@ApiProperty()
@MinLength(6)
password: string;
}
2 changes: 1 addition & 1 deletion src/auth/auth.guard.ts → src/app/auth/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ export class JwtAuthGuard extends AuthGuard('jwt') {

return user;
}
}
}
28 changes: 28 additions & 0 deletions src/app/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';

import { AuthService } from '~/app/auth/auth.service';
import { AuthController } from '~/app/auth/auth.controller';
import { User, UserSchema } from '~/schemas/user.schema';
import { JwtStrategy } from '~/app/auth/jwt.strategy';
import { JwtRefreshStrategy } from '~/app/auth/refresh.strategy';
import { ACCESS_TOKEN_EXPIRES_IN } from '~/constants';
import { env } from '~/env';

@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
JwtModule.register({
global: true,
secret: env.SECRET_KEY,
signOptions: { expiresIn: ACCESS_TOKEN_EXPIRES_IN },
}),
PassportModule,
],
controllers: [AuthController],
providers: [AuthService, JwtService, JwtStrategy, JwtRefreshStrategy],
exports: [AuthService],
})
export class AuthModule {}
42 changes: 42 additions & 0 deletions src/app/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { JwtService } from '@nestjs/jwt';

import { ACCESS_TOKEN_EXPIRES_IN, REFRESH_TOKEN_EXPIRES_IN } from '~/constants';
import { SignUpDto } from '~/app/auth/auth.dto';
import { User } from '~/schemas/user.schema';
import { env } from '~/env';

@Injectable()
export class AuthService {
constructor(
@InjectModel(User.name) private readonly userModel: Model<User>,
private readonly jwtService: JwtService,
) {}

async signUp(data: SignUpDto) {
const emailIsAlreadyInUse = await this.userModel.findOne({
email: data.email,
});
if (!!emailIsAlreadyInUse)
throw new HttpException('Email is already in use', HttpStatus.CONFLICT);

const user = await this.userModel.create(data);

const accessToken = await this.jwtService.signAsync(
{ user: user._id },
{ secret: env.SECRET_KEY, expiresIn: ACCESS_TOKEN_EXPIRES_IN },
);

const refreshToken = await this.jwtService.signAsync(
{ user: user._id },
{
secret: env.SECRET_KEY,
expiresIn: REFRESH_TOKEN_EXPIRES_IN,
},
);

return { user, refreshToken, accessToken };
}
}
39 changes: 39 additions & 0 deletions src/app/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { Model } from 'mongoose';
import { Request } from 'express';
import { InjectModel } from '@nestjs/mongoose';

import { env } from '~/env';
import { AUTH_COOKIE } from '~/constants';
import { User } from '~/schemas/user.schema';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(@InjectModel(User.name) private readonly userModel: Model<User>) {
super({
ignoreExpiration: false,
jwtFromRequest: ExtractJwt.fromExtractors([
(req: Request) => {
if (req) {
const data = req.cookies[AUTH_COOKIE];

if (data && data.accessToken) return data.accessToken;
}

return null;
},
]),
secretOrKey: env.SECRET_KEY,
});
}

async validate(payload: Record<string, string>) {
const user = this.userModel.findById(payload.user);

if (user) return user;

throw new UnauthorizedException();
}
}
49 changes: 49 additions & 0 deletions src/app/auth/refresh.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { JwtService } from '@nestjs/jwt';

import { env } from '~/env';
import { ACCESS_TOKEN_EXPIRES_IN, AUTH_COOKIE } from '~/constants';
import { User } from '~/schemas/user.schema';

@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'refresh') {
constructor(
@InjectModel(User.name) private readonly userModel: Model<User>,
private readonly jwtService: JwtService,
) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(req: Request) => {
if (req) {
const data = req.cookies[AUTH_COOKIE];

if (data && data.refreshToken) return data.refreshToken;
}

return null;
},
]),
ignoreExpiration: false,
secretOrKey: env.SECRET_KEY,
});
}

async validate(payload: Record<string, string>) {
const user = await this.userModel.findById(payload.user);

if (user) {
const accessToken = await this.jwtService.signAsync(
{ user: user._id },
{ secret: env.SECRET_KEY, expiresIn: ACCESS_TOKEN_EXPIRES_IN },
);

return accessToken;
}
throw new UnauthorizedException();
}
}
18 changes: 2 additions & 16 deletions src/app/tests/app.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
import { Test, TestingModule } from '@nestjs/testing';

import { AppController } from '../app.controller';
import { AppService } from '../app.service';

describe('AppController', () => {
let appController: AppController;

beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();

appController = app.get<AppController>(AppController);
});
beforeEach(async () => {});

describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
expect(true).toBe(true);
});
});
});
Empty file removed src/auth/auth.controller.ts
Empty file.
3 changes: 0 additions & 3 deletions src/auth/auth.decorator.ts

This file was deleted.

Empty file removed src/auth/auth.module.ts
Empty file.
Empty file removed src/auth/auth.service.ts
Empty file.
29 changes: 0 additions & 29 deletions src/auth/jwt.strategy.ts

This file was deleted.

Empty file removed src/auth/refresh.strategy.ts
Empty file.
3 changes: 3 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const AUTH_COOKIE = 'auth';
export const ACCESS_TOKEN_EXPIRES_IN = '3d';
export const REFRESH_TOKEN_EXPIRES_IN = '30d';
Empty file.
25 changes: 25 additions & 0 deletions src/filters/http-exception.filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
const data = exception.getResponse();

response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
data,
});
}
}
Loading

0 comments on commit 56c5480

Please sign in to comment.