Skip to content

Commit

Permalink
Merge pull request #15 from Brints/access-token-guard
Browse files Browse the repository at this point in the history
feat: access token guard
  • Loading branch information
aniebietafia authored Sep 4, 2024
2 parents 6e4bd1d + d84b1e7 commit 9dffd3d
Show file tree
Hide file tree
Showing 14 changed files with 174 additions and 48 deletions.
14 changes: 14 additions & 0 deletions brints-estate-api/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';

import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { environmentValidationSchema } from './config/environment.validation';
import { AccessTokenGuard } from './auth/guards/access-token/access-token.guard';
import jwtConfig from './auth/config/jwt.config';
import { JwtModule } from '@nestjs/jwt';
import { AuthenticationGuard } from './auth/guards/authentication/authentication.guard';

@Module({
imports: [
Expand All @@ -30,8 +35,17 @@ import { environmentValidationSchema } from './config/environment.validation';
// entities: [User, UserAuth],
}),
}),
ConfigModule.forFeature(jwtConfig),
JwtModule.registerAsync(jwtConfig.asProvider()),
AuthModule,
UsersModule,
],
providers: [
{
provide: APP_GUARD,
useClass: AuthenticationGuard,
},
AccessTokenGuard,
],
})
export class AppModule {}
4 changes: 4 additions & 0 deletions brints-estate-api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import { AuthService } from './providers/auth.service';
import { CreateUserDto } from 'src/users/dto/create-user.dto';
import { CreateUserAuthDto } from 'src/users/dto/create-userauth.dto';
import { LoginUserDto } from './dto/login.dto';
import { Auth } from './decorators/auth.decorator';
import { AuthType } from './enum/auth-type.enum';

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

@Post('register')
@Auth(AuthType.None)
async registerUser(
@Body()
createUserDto: CreateUserDto,
Expand All @@ -29,6 +32,7 @@ export class AuthController {
}

@Post('login')
@Auth(AuthType.None)
@HttpCode(HttpStatus.OK)
async loginUser(@Body() loginUserDto: LoginUserDto) {
const user = await this.authService.loginUser(loginUserDto);
Expand Down
2 changes: 2 additions & 0 deletions brints-estate-api/src/auth/constants/auth.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const REQUEST_USER_TYPE = 'user';
export const AUTH_TYPE_KEY = 'authType';
6 changes: 6 additions & 0 deletions brints-estate-api/src/auth/decorators/auth.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { SetMetadata } from '@nestjs/common';
import { AuthType } from '../enum/auth-type.enum';
import { AUTH_TYPE_KEY } from '../constants/auth.constants';

export const Auth = (...authTypes: AuthType[]) =>
SetMetadata(AUTH_TYPE_KEY, authTypes);
4 changes: 4 additions & 0 deletions brints-estate-api/src/auth/enum/auth-type.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum AuthType {
Bearer,
None,
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,60 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import {
CanActivate,
ExecutionContext,
HttpStatus,
Inject,
Injectable,
} from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import jwtConfig from 'src/auth/config/jwt.config';
import { REQUEST_USER_TYPE } from 'src/auth/constants/auth.constants';
import { CustomException } from 'src/exceptions/custom.exception';

@Injectable()
export class AccessTokenGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
constructor(
private readonly jwtService: JwtService,

@Inject(jwtConfig.KEY)
private readonly jwtConfiguration: ConfigType<typeof jwtConfig>,
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractRequestFromHeader(request);

if (!token) {
throw new CustomException(HttpStatus.UNAUTHORIZED, 'Unauthorized');
}

try {
const payload = await this.jwtService.verifyAsync(
token,
this.jwtConfiguration,
);

// request.user = payload;
request[REQUEST_USER_TYPE] = payload;
} catch {
throw new CustomException(HttpStatus.UNAUTHORIZED, 'Unauthorized');
}

return true;
}

private extractRequestFromHeader(request: Request): string | null {
const authorizationHeader = request.headers.authorization;
if (!authorizationHeader) {
return null;
}

const [bearer, token] = authorizationHeader.split(' ');
if (bearer !== 'Bearer' || !token) {
return null;
}

return token;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
CanActivate,
ExecutionContext,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AccessTokenGuard } from '../access-token/access-token.guard';
import { AuthType } from 'src/auth/enum/auth-type.enum';
import { AUTH_TYPE_KEY } from 'src/auth/constants/auth.constants';
import { CustomException } from 'src/exceptions/custom.exception';

@Injectable()
export class AuthenticationGuard implements CanActivate {
private static readonly defaultAuthType = AuthType.Bearer;

private get authTypeGuardMap(): Record<
AuthType,
CanActivate | CanActivate[]
> {
return {
[AuthType.Bearer]: this.accessTokenGuard,
[AuthType.None]: { canActivate: () => true },
};
}

constructor(
private readonly reflector: Reflector,
private readonly accessTokenGuard: AccessTokenGuard,
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const authTypes = this.reflector.getAllAndOverride<AuthType[]>(
AUTH_TYPE_KEY,
[context.getHandler(), context.getClass()],
) ?? [AuthenticationGuard.defaultAuthType];

const guards = authTypes
.map((authType) => this.authTypeGuardMap[authType])
.flat();

const error = new CustomException(HttpStatus.UNAUTHORIZED, 'Unauthorized');

for (const instance of guards) {
const canActivate = await Promise.resolve(
instance.canActivate(context),
).catch((err) => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
error: err;
});

if (!canActivate) {
throw error;
}
}

return true;
}
}
12 changes: 6 additions & 6 deletions brints-estate-api/src/auth/providers/login-user.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ export class LoginUserProvider {
);
}

if (!user.isVerified) {
throw new CustomException(
HttpStatus.BAD_REQUEST,
'User account not verified',
);
}
// if (!user.isVerified) {
// throw new CustomException(
// HttpStatus.BAD_REQUEST,
// 'User account not verified',
// );
// }

const payload: JwtPayload = {
sub: user.id,
Expand Down
18 changes: 4 additions & 14 deletions brints-estate-api/src/config/environment.validation.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,5 @@
// import Joi from '@hapi/joi';
import * as Joi from 'joi';

// export const environmentValidationSchema = Joi.object({
// APP_PORT: Joi.number().default(8000),
// DB_HOST: Joi.string().required(),
// DB_PORT: Joi.number().default(5432),
// DB_USER: Joi.string().required(),
// DB_PASSWORD: Joi.string().required(),
// DB_NAME: Joi.string().required(),
// NODE_ENV: Joi.string()
// .required()
// .valid('development', 'production', 'test')
// .default('development'),
// });

export const environmentValidationSchema = Joi.object({
APP_PORT: Joi.number().port().default(8000),
DB_HOST: Joi.string().required(),
Expand All @@ -25,4 +11,8 @@ export const environmentValidationSchema = Joi.object({
.required()
.valid('development', 'production', 'test', 'staging')
.default('development'),
JWT_SECRET: Joi.string().required(),
JWT_ACCESS_TOKEN_TTL: Joi.number().required(),
JWT_TOKEN_AUDIENCE: Joi.string().required(),
JWT_TOKEN_ISSUER: Joi.string().required(),
});
3 changes: 0 additions & 3 deletions brints-estate-api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Logger, ValidationPipe } from '@nestjs/common';

import { AppModule } from './app.module';
import { swaggerInitializer } from './config/config.swagger';
import { HttpExceptionFilter } from './exceptions/http-exception.filter';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
Expand All @@ -15,8 +14,6 @@ async function bootstrap() {
}),
);

app.useGlobalFilters(new HttpExceptionFilter());

const configService = new ConfigService();

const port = configService.get('APP_PORT');
Expand Down
13 changes: 3 additions & 10 deletions brints-estate-api/src/users/http/users.endpoint.http
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
GET http://localhost:3000/users/1
GET http://localhost:3001/users/1

POST http://localhost:3000/users
Content-Type: application/json

{
"firstName": "John",
"lastName": "Doe",
"email": "test@example.com",
"age": 27
}
GET http://localhost:3001/users/all
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlZjI2N2IxMS02NmYzLTQ3MjAtYjM1MS01ZjlkNDc0OGQ3M2YiLCJmaXJzdF9uYW1lIjoiQW5pZWJpZXQiLCJsYXN0X25hbWUiOiJBZmlhIiwiZW1haWwiOiJhbmllYmlldGFmaWFAZ21haWwuY29tIiwicm9sZSI6InVzZXIiLCJ2ZXJpZmllZCI6ZmFsc2UsImlhdCI6MTcyNTQzOTQzOSwiZXhwIjoxNzI1NDQzMDM5LCJhdWQiOiJsb2NhbGhvc3Q6MzAwMSIsImlzcyI6ImxvY2FsaG9zdDozMDAxIn0.dmuK28GbSaGyQosL2DnXyEGPXRm26Nd3gnWlpaHddWg
4 changes: 4 additions & 0 deletions brints-estate-api/src/users/providers/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
constructor() {}

public async getAllUsers() {
return 'Hello World';
}
}
16 changes: 8 additions & 8 deletions brints-estate-api/src/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Controller } from '@nestjs/common';
// import { UsersService } from './providers/users.service';
// import { CreateUserDto } from './dto/create-user.dto';
import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { UsersService } from './providers/users.service';

@Controller('users')
@ApiTags('Users')
export class UsersController {
// constructor(private readonly usersService: UsersService) {}
// @Post()
// create(@Body() createUserDto: CreateUserDto) {
// return this.usersService.create(createUserDto);
// }
constructor(private readonly usersService: UsersService) {}

@Get('all')
async getUser() {
return this.usersService.getAllUsers();
}
}
8 changes: 6 additions & 2 deletions brints-estate-api/src/users/users.module.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { UsersController } from './users.controller';
import { UsersService } from './providers/users.service';
import { AuthModule } from 'src/auth/auth.module';

import { User } from './entities/user.entity';
import { UserAuth } from './entities/userAuth.entity';

@Module({
controllers: [UsersController],
providers: [UsersService],
imports: [TypeOrmModule.forFeature([User, UserAuth])],
imports: [
TypeOrmModule.forFeature([User, UserAuth]),
forwardRef(() => AuthModule),
],
exports: [TypeOrmModule, UsersService],
})
export class UsersModule {}

0 comments on commit 9dffd3d

Please sign in to comment.