Skip to content

Commit

Permalink
chore: add auth module and jwt service
Browse files Browse the repository at this point in the history
  • Loading branch information
andrea-acampora committed Feb 21, 2025
1 parent 6365bb6 commit 1e99946
Show file tree
Hide file tree
Showing 14 changed files with 231 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ DATABASE_PORT=15432
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres

JWT_SECRET=nestjs-ddd-devops
JWT_REFRESH_SECRET=nestjs-ddd-devops-refresh
JWT_EXPIRES_IN=3600
JET_REFRESH_EXPIRES_IN=360000

PORT=3000
12 changes: 4 additions & 8 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { CacheModule } from '@nestjs/cache-manager';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import mikroOrmConfig from './config/database/mikro-orm.config';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerModule } from '@nestjs/throttler';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { ScheduleModule } from '@nestjs/schedule';
import { APP_GUARD } from '@nestjs/core';
import { UserModule } from './modules/user/user.module';
import { HealthModule } from './modules/health/health.module';
import { AuthModule } from './modules/auth/auth.module';

@Module({
imports: [
Expand Down Expand Up @@ -42,13 +42,9 @@ import { HealthModule } from './modules/health/health.module';
ScheduleModule.forRoot(),
HealthModule,
UserModule,
AuthModule,
],
controllers: [],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
providers: [],
})
export class AppModule {}
5 changes: 5 additions & 0 deletions src/config/env/configuration.constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const JWT_SECRET = 'JWT_SECRET';
export const JWT_REFRESH_SECRET = 'JWT_REFRESH_SECRET';

export const JWT_EXPIRES_IN = 'JWT_EXPIRES_IN';
export const JWT_REFRESH_EXPIRES_IN = 'JWT_REFRESH_EXPIRES_IN';
13 changes: 13 additions & 0 deletions src/libs/util/config.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { fromNullable, getOrThrowWith } from 'effect/Option';
import { ConfigService } from '@nestjs/config';

export const getConfigValue = <T>(
configService: ConfigService,
key: string,
): T => {
console.log('AAA');
return getOrThrowWith<T>(
fromNullable(configService.get<T>(key)),
() => new Error(`Missing ${key}`),
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { AuthUser } from '../presentation/dto/auth-user.dto';
import { Option } from 'effect/Option';
import { JwtUser } from '../presentation/dto/jwt-user.dto';

export interface JwtAuthentication {
verifyToken(token: string): Promise<Option<AuthUser>>;

verifyRefreshToken(refreshToken: string): Promise<Option<AuthUser>>;

generateToken(user: AuthUser): Promise<string>;

generateRefreshToken(user: AuthUser): Promise<string>;

generateJwtUser(user: AuthUser): Promise<JwtUser>;

generateJwtUserFromRefresh(token: string): Promise<JwtUser>;
}
29 changes: 29 additions & 0 deletions src/modules/auth/application/presentation/body/login.body.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
IsEmail,
IsNotEmpty,
IsString,
IsStrongPassword,
MaxLength,
} from 'class-validator';

export class LoginBody {
@IsNotEmpty()
@IsEmail()
@IsString()
email!: string;

@IsNotEmpty()
@IsString()
@MaxLength(20)
@IsStrongPassword(
{
minLength: 8,
minSymbols: 1,
},
{
message:
'Password must be at least 8 characters long and contain at least one symbol',
},
)
password!: string;
}
12 changes: 12 additions & 0 deletions src/modules/auth/application/presentation/body/signup.body.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { LoginBody } from './login.body';
import { IsNotEmpty, IsString } from 'class-validator';

export class SignupBody extends LoginBody {
@IsNotEmpty()
@IsString()
firstName!: string;

@IsNotEmpty()
@IsString()
lastName!: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface AuthUser {
id: string;
email: string;
role: number;
}
9 changes: 9 additions & 0 deletions src/modules/auth/application/presentation/dto/jwt-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AuthUser } from './auth-user.dto';

export interface JwtUser {
token: string;
expiresIn: number;
refreshToken: string;
refreshExpiresIn: number;
user: AuthUser;
}
95 changes: 95 additions & 0 deletions src/modules/auth/application/service/jwt.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
JWT_EXPIRES_IN,
JWT_REFRESH_EXPIRES_IN,
JWT_REFRESH_SECRET,
JWT_SECRET,
} from '../../../../config/env/configuration.constant';
import { isNone, liftThrowable, Option } from 'effect/Option';
import { JwtAuthentication } from '../interface/jwt-authentication.interface';
import { AuthUser } from '../presentation/dto/auth-user.dto';
import * as jwt from 'jsonwebtoken';
import { getConfigValue } from '../../../../libs/util/config.util';
import { JwtUser } from '../presentation/dto/jwt-user.dto';

@Injectable()
export class JwtService implements JwtAuthentication {
private readonly tokenSecret: string;
private readonly refreshTokenSecret: string;
private readonly tokenExpiration: number;
private readonly refreshTokenExpiration: number;

constructor(private readonly configService: ConfigService) {
this.tokenSecret = getConfigValue(this.configService, JWT_SECRET);
this.refreshTokenSecret = getConfigValue<string>(
this.configService,
JWT_REFRESH_SECRET,
);
this.tokenExpiration = getConfigValue<number>(
this.configService,
JWT_EXPIRES_IN,
);
this.refreshTokenExpiration = getConfigValue<number>(
this.configService,
JWT_REFRESH_EXPIRES_IN,
);
}

async generateToken(user: AuthUser): Promise<string> {
return jwt.sign(user, this.tokenSecret, {
expiresIn: this.tokenExpiration,
});
}

async generateRefreshToken(user: AuthUser): Promise<string> {
return jwt.sign(user, this.refreshTokenSecret, {
expiresIn: this.refreshTokenExpiration,
});
}

async generateJwtUser(authUser: AuthUser): Promise<JwtUser> {
const token = await this.generateToken(authUser);
const refreshToken = await this.generateRefreshToken(authUser);
return {
token,
expiresIn: this.tokenExpiration,
refreshToken,
refreshExpiresIn: this.refreshTokenExpiration,
user: authUser,
};
}

async generateJwtUserFromRefresh(refreshToken: string): Promise<JwtUser> {
const authUser = this.verifyJwt<AuthUser>(
refreshToken,
this.refreshTokenSecret,
);
if (isNone(authUser)) throw new UnauthorizedException('Invalid Token!');
return this.generateJwtUser(this.convertToAuthUser(authUser.value));
}

async verifyToken(token: string): Promise<Option<AuthUser>> {
return this.verifyJwt<AuthUser>(token, this.tokenSecret);
}

async verifyRefreshToken(refreshToken: string): Promise<Option<AuthUser>> {
return this.verifyJwt<AuthUser>(refreshToken, this.refreshTokenSecret);
}

/**
* Generic JWT verification method.
*/
private verifyJwt<T>(token: string, secret: string): Option<T> {
return liftThrowable(() => jwt.verify(token, secret) as T)();
}

/**
* Helper method to clean the AuthUser object.
*/
convertToAuthUser = (authUser: AuthUser): AuthUser => ({
id: authUser.id,
email: authUser.email,
role: authUser.role,
});
}
18 changes: 18 additions & 0 deletions src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerGuard } from '@nestjs/throttler';
import { JwtService } from './application/service/jwt.service';

@Module({
imports: [],
controllers: [],
providers: [
JwtService,
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
exports: [],
})
export class AuthModule {}
14 changes: 11 additions & 3 deletions src/modules/user/domain/entity/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Entity, PrimaryKey, Property } from '@mikro-orm/core';
import { Entity, Enum, PrimaryKey, Property } from '@mikro-orm/core';
import { v4 } from 'uuid';
import { UserRole } from '../value-object/user-role.enum';
import { UserState } from '../value-object/user-state.enum';

@Entity({
tableName: 'Users',
Expand All @@ -15,10 +17,16 @@ export class User {
password!: string;

@Property({ nullable: true })
firstname?: string;
firstName?: string;

@Property({ nullable: true })
lastname?: string;
lastName?: string;

@Enum({ items: () => UserRole })
role!: UserRole;

@Enum({ items: () => UserState })
state!: UserState;

@Property({ onCreate: () => new Date() })
createdAt: Date = new Date();
Expand Down
4 changes: 4 additions & 0 deletions src/modules/user/domain/value-object/user-role.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum UserRole {
ADMIN = 0,
USER = 1,
}
4 changes: 4 additions & 0 deletions src/modules/user/domain/value-object/user-state.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum UserState {
ACTIVE = 'ACTIVE',
DISABLED = 'DISABLED',
}

0 comments on commit 1e99946

Please sign in to comment.