authentication module done

This commit is contained in:
VirajBrainvire
2024-02-26 11:23:44 +05:30
parent 4ffd94ec78
commit 0764793874
75 changed files with 2111 additions and 89 deletions

View File

@ -0,0 +1,28 @@
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Module } from '@nestjs/common';
import { HelperModule } from '../helper/helper.module';
import { JwtStrategy } from './strategies/jwt.strategy';
import { UserSessionRepository } from '../modules/session/repositories/session.repository';
import { AuthService } from './services/auth.service';
import { UserRepository } from '../modules/user/repositories';
@Module({
imports: [
ConfigModule.forRoot(),
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: { expiresIn: configService.get('JWT_EXPIRE_TIME') },
}),
}),
HelperModule,
],
providers: [JwtStrategy, UserSessionRepository,AuthService,UserRepository],
exports: [AuthService],
})
export class AuthModule {}

View File

@ -0,0 +1,7 @@
export class AuthInterface {
email: string;
userId: number;
uuid: string;
sessionId: string;
id: number;
}

View File

@ -0,0 +1,55 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { HelperHashService } from '../../helper/services';
import { UserRepository } from '@app/common/modules/user/repositories';
import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository';
import { UserSessionEntity } from '@app/common/modules/session/entities';
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private readonly userRepository: UserRepository,
private readonly sessionRepository: UserSessionRepository,
private readonly helperHashService: HelperHashService,
) {}
async validateUser(email: string, pass: string): Promise<any> {
const user = await this.userRepository.findOne({
where: {
email,
},
});
if (!user.isUserVerified) {
throw new BadRequestException('User is not verified');
}
if (user) {
const passwordMatch = this.helperHashService.bcryptCompare(
pass,
user.password,
);
if (passwordMatch) {
const { ...result } = user;
return result;
}
}
return null;
}
async createSession(data): Promise<UserSessionEntity> {
return await this.sessionRepository.save(data);
}
async login(user: any) {
const payload = {
email: user.email,
userId: user.userId,
uuid: user.uuid,
type: user.type,
sessionId: user.sessionId,
};
return {
access_token: this.jwtService.sign(payload),
};
}
}

View File

@ -0,0 +1,39 @@
import { ConfigService } from '@nestjs/config';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { BadRequestException, Injectable } from '@nestjs/common';
import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository';
import { AuthInterface } from '../interfaces/auth.interface';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly sessionRepository: UserSessionRepository,
private readonly configService: ConfigService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('JWT_SECRET'),
});
}
async validate(payload: AuthInterface) {
const validateUser = await this.sessionRepository.findOne({
where: {
uuid: payload.sessionId,
isLoggedOut: false,
},
});
if (validateUser) {
return {
email: payload.email,
userId: payload.id,
uuid: payload.uuid,
sessionId: payload.sessionId,
};
} else {
throw new BadRequestException('Unauthorized');
}
}
}

View File

@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { CommonService } from './common.service';
import { DatabaseModule } from './database/database.module';
import { HelperModule } from './helper/helper.module';
import { AuthModule } from './auth/auth.module';
import { ConfigModule } from '@nestjs/config';
import config from './config';
import { EmailService } from './util/email.service';
@Module({
providers: [CommonService,EmailService],
exports: [CommonService, HelperModule, AuthModule,EmailService],
imports: [
ConfigModule.forRoot({
load: config,
}),
DatabaseModule,
HelperModule,
AuthModule,
],
})
export class CommonModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CommonService } from './common.service';
describe('CommonService', () => {
let service: CommonService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CommonService],
}).compile();
service = module.get<CommonService>(CommonService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class CommonService {}

View File

@ -0,0 +1,9 @@
import { registerAs } from '@nestjs/config';
export default registerAs(
'email-config',
(): Record<string, any> => ({
EMAIL_ID: process.env.EMAIL_USER,
PASSWORD: process.env.EMAIL_PASSWORD,
}),
);

View File

@ -0,0 +1,2 @@
import emailConfig from './email.config';
export default [emailConfig];

View File

@ -0,0 +1,4 @@
export enum OtpType {
VERIFICATION = 'VERIFICATION',
PASSWORD = 'PASSWORD',
}

View File

@ -0,0 +1,38 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SnakeNamingStrategy } from './strategies';
import { UserEntity } from '../modules/user/entities/user.entity';
import { UserSessionEntity } from '../modules/session/entities/session.entity';
import { UserOtpEntity } from '../modules/user-otp/entities';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
name: 'default',
type: 'postgres',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USER'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
entities: [UserEntity, UserSessionEntity,UserOtpEntity],
namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
logging: true,
extra: {
charset: 'utf8mb4',
max: 20, // set pool max size
idleTimeoutMillis: 5000, // close idle clients after 5 second
connectionTimeoutMillis: 11_000, // return an error after 11 second if connection could not be established
maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion)
},
continuationLocalStorage: true,
}),
}),
],
})
export class DatabaseModule {}

View File

@ -0,0 +1 @@
export * from './snack-naming.strategy';

View File

@ -0,0 +1,62 @@
import { DefaultNamingStrategy, NamingStrategyInterface } from 'typeorm';
import { snakeCase } from 'typeorm/util/StringUtils';
export class SnakeNamingStrategy
extends DefaultNamingStrategy
implements NamingStrategyInterface
{
tableName(className: string, customName: string): string {
return customName ? customName : snakeCase(className);
}
columnName(
propertyName: string,
customName: string,
embeddedPrefixes: string[],
): string {
return (
snakeCase(embeddedPrefixes.join('_')) +
(customName ? customName : snakeCase(propertyName))
);
}
relationName(propertyName: string): string {
return snakeCase(propertyName);
}
joinColumnName(relationName: string, referencedColumnName: string): string {
return snakeCase(relationName + '_' + referencedColumnName);
}
joinTableName(
firstTableName: string,
secondTableName: string,
firstPropertyName: any,
_secondPropertyName: string,
): string {
return snakeCase(
firstTableName +
'_' +
firstPropertyName.replaceAll(/\./gi, '_') +
'_' +
secondTableName,
);
}
joinTableColumnName(
tableName: string,
propertyName: string,
columnName?: string,
): string {
return snakeCase(
tableName + '_' + (columnName ? columnName : propertyName),
);
}
classTableInheritanceParentColumnName(
parentTableName: string,
parentTableIdPropertyName: string,
): string {
return snakeCase(`${parentTableName}_${parentTableIdPropertyName}`);
}
}

View File

@ -0,0 +1,11 @@
import { UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
export class JwtAuthGuard extends AuthGuard('jwt') {
handleRequest(err, user, info) {
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}

View File

@ -0,0 +1,11 @@
import { Global, Module } from '@nestjs/common';
import { HelperHashService } from './services';
@Global()
@Module({
providers: [HelperHashService],
exports: [HelperHashService],
controllers: [],
imports: [],
})
export class HelperModule {}

View File

@ -0,0 +1,63 @@
import { Injectable } from '@nestjs/common';
import { compareSync, genSaltSync, hashSync } from 'bcryptjs';
import { AES, enc, mode, pad, SHA256 } from 'crypto-js';
@Injectable()
export class HelperHashService {
randomSalt(length: number): string {
return genSaltSync(length);
}
bcrypt(passwordString: string, salt: string): string {
return hashSync(passwordString, salt);
}
bcryptCompare(passwordString: string, passwordHashed: string): boolean {
return compareSync(passwordString, passwordHashed);
}
sha256(string: string): string {
return SHA256(string).toString(enc.Hex);
}
sha256Compare(hashOne: string, hashTwo: string): boolean {
return hashOne === hashTwo;
}
// Encryption function
encryptPassword(password, secretKey) {
return AES.encrypt('trx8g6gi', secretKey).toString();
}
// Decryption function
decryptPassword(encryptedPassword, secretKey) {
const bytes = AES.decrypt(encryptedPassword, secretKey);
return bytes.toString(enc.Utf8);
}
aes256Encrypt(
data: string | Record<string, any> | Record<string, any>[],
key: string,
iv: string,
): string {
const cIv = enc.Utf8.parse(iv);
const cipher = AES.encrypt(JSON.stringify(data), enc.Utf8.parse(key), {
mode: mode.CBC,
padding: pad.Pkcs7,
iv: cIv,
});
return cipher.toString();
}
aes256Decrypt(encrypted: string, key: string, iv: string) {
const cIv = enc.Utf8.parse(iv);
const cipher = AES.decrypt(encrypted, enc.Utf8.parse(key), {
mode: mode.CBC,
padding: pad.Pkcs7,
iv: cIv,
});
return cipher.toString(enc.Utf8);
}
}

View File

@ -0,0 +1 @@
export * from './helper.hash.service'

2
libs/common/src/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './common.module';
export * from './common.service';

View File

@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { AbstractEntity } from '../entities/abstract.entity';
export class AbstractDto {
@ApiProperty({ format: 'uuid' })
readonly uuid: string;
constructor(abstract: AbstractEntity, options?: { excludeFields?: boolean }) {
if (!options?.excludeFields) {
this.uuid = abstract.uuid;
}
}
}

View File

@ -0,0 +1 @@
export * from './abstract.dto'

View File

@ -0,0 +1,47 @@
import { Exclude } from 'class-transformer';
import {
Column,
CreateDateColumn,
Generated,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { AbstractDto } from '../dtos';
import { Constructor } from '@app/common/util/types';
export abstract class AbstractEntity<
T extends AbstractDto = AbstractDto,
O = never
> {
@PrimaryGeneratedColumn('increment')
@Exclude()
public id: number;
@Column()
@Generated('uuid')
public uuid: string;
@CreateDateColumn({ type: 'timestamp' })
@Exclude()
public createdAt: Date;
@UpdateDateColumn({ type: 'timestamp' })
@Exclude()
public updatedAt: Date;
private dtoClass: Constructor<T, [AbstractEntity, O?]>;
toDto(options?: O): T {
const dtoClass = this.dtoClass;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!dtoClass) {
throw new Error(
`You need to use @UseDto on class (${this.constructor.name}) be able to call toDto function`
);
}
return new this.dtoClass(this, options);
}
}

View File

@ -0,0 +1,26 @@
import {
IsBoolean,
IsDate,
IsNotEmpty,
IsNumber,
IsString,
} from 'class-validator';
export class SessionDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsNumber()
@IsNotEmpty()
userId: number;
@IsDate()
@IsNotEmpty()
public loginTime: Date;
@IsBoolean()
@IsNotEmpty()
public isLoggedOut: boolean;
}

View File

@ -0,0 +1 @@
export * from './session.entity'

View File

@ -0,0 +1,31 @@
import { Column, Entity } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SessionDto } from '../dtos/session.dto';
@Entity({ name: 'userSession' })
export class UserSessionEntity extends AbstractEntity<SessionDto> {
@Column({
nullable: false,
})
public uuid: string;
@Column({
nullable: false,
})
userId: string;
@Column({
nullable: false,
})
public loginTime: Date;
@Column({
nullable: false,
})
public isLoggedOut: boolean;
constructor(partial: Partial<UserSessionEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { UserSessionEntity } from '../entities';
@Injectable()
export class UserSessionRepository extends Repository<UserSessionEntity> {
constructor(private dataSource: DataSource) {
super(UserSessionEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserSessionEntity } from './entities';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([UserSessionEntity])],
})
export class UserSessionRepositoryModule {}

View File

@ -0,0 +1 @@
export * from './user-otp.dto'

View File

@ -0,0 +1,19 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class UserOtpDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public email: string;
@IsString()
@IsNotEmpty()
public otpCode: string;
@IsString()
@IsNotEmpty()
public expiryTime: string;
}

View File

@ -0,0 +1 @@
export * from './user-otp.entity'

View File

@ -0,0 +1,32 @@
import { Column, Entity } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { UserOtpDto } from '../dtos';
import { OtpType } from '@app/common/constants/otp-type.enum';
@Entity({ name: 'user-otp' })
export class UserOtpEntity extends AbstractEntity<UserOtpDto> {
@Column({
nullable: false,
})
public uuid: string;
@Column({ nullable: false })
email: string;
@Column({ nullable: false })
otpCode: string;
@Column({ nullable: false })
expiryTime: Date;
@Column({
type: 'enum',
enum: Object.values(OtpType),
})
type: OtpType;
constructor(partial: Partial<UserOtpEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { UserOtpEntity } from '../entities';
@Injectable()
export class UserOtpRepository extends Repository<UserOtpEntity> {
constructor(private dataSource: DataSource) {
super(UserOtpEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserOtpEntity } from './entities';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([UserOtpEntity])],
})
export class UserOtpRepositoryModule {}

View File

@ -0,0 +1 @@
export * from './user.dto';

View File

@ -0,0 +1,23 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class UserDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public email: string;
@IsString()
@IsNotEmpty()
public password: string;
@IsString()
@IsNotEmpty()
public firstName: string;
@IsString()
@IsNotEmpty()
public lastName: string;
}

View File

@ -0,0 +1,41 @@
import { Column, Entity } from 'typeorm';
import { UserDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
@Entity({ name: 'user' })
export class UserEntity extends AbstractEntity<UserDto> {
@Column({
nullable: false,
})
public uuid: string;
@Column({
nullable: false,
unique: true,
})
email: string;
@Column({
nullable: false,
})
public password: string;
@Column()
public firstName: string;
@Column({
nullable: false,
})
public lastName: string;
@Column({
nullable: true,
default: false,
})
public isUserVerified: boolean;
constructor(partial: Partial<UserEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1 @@
export * from './user.repository';

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { UserEntity } from '../entities/user.entity';
@Injectable()
export class UserRepository extends Repository<UserEntity> {
constructor(private dataSource: DataSource) {
super(UserEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from './entities/user.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([UserEntity])],
})
export class UserRepositoryModule {}

View File

@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const ResponseMessage = (message: string) =>
SetMetadata('response_message', message);

View File

@ -0,0 +1,5 @@
export interface Response<T> {
statusCode: number;
message: string;
data?: T;
}

View File

@ -0,0 +1,38 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as nodemailer from 'nodemailer';
@Injectable()
export class EmailService {
private user: string;
private pass: string;
constructor(private readonly configService: ConfigService) {
this.user = this.configService.get<string>('email-config.EMAIL_ID');
this.pass = this.configService.get<string>('email-config.PASSWORD');
}
async sendOTPEmail(
email: string,
subject: string,
message: string,
): Promise<void> {
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: this.user,
pass: this.pass,
},
});
const mailOptions = {
from: this.user,
to: email,
subject,
text: message,
};
await transporter.sendMail(mailOptions);
}
}

View File

@ -0,0 +1,46 @@
export type Constructor<T, Arguments extends unknown[] = undefined[]> = new (
...arguments_: Arguments
) => T;
export type Plain<T> = T;
export type Optional<T> = T | undefined;
export type Nullable<T> = T | null;
export type PathImpl<T, Key extends keyof T> = Key extends string
? T[Key] extends Record<string, any>
?
| `${Key}.${PathImpl<T[Key], Exclude<keyof T[Key], keyof any[]>> &
string}`
| `${Key}.${Exclude<keyof T[Key], keyof any[]> & string}`
: never
: never;
export type PathImpl2<T> = PathImpl<T, keyof T> | keyof T;
export type Path<T> = keyof T extends string
? PathImpl2<T> extends string | keyof T
? PathImpl2<T>
: keyof T
: never;
export type PathValue<
T,
P extends Path<T>
> = P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? Rest extends Path<T[Key]>
? PathValue<T[Key], Rest>
: never
: never
: P extends keyof T
? T[P]
: never;
export type KeyOfType<Entity, U> = {
[P in keyof Required<Entity>]: Required<Entity>[P] extends U
? P
: Required<Entity>[P] extends U[]
? P
: never;
}[keyof Entity];

View File

@ -0,0 +1,21 @@
import type { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
export function setupSwaggerAuthentication(app: INestApplication): void {
const options = new DocumentBuilder()
.setTitle('Authentication-Service')
.addBearerAuth({
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
in: 'header',
})
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api/authentication/documentation', app, document, {
swaggerOptions: {
persistAuthorization: true,
},
});
}