feat: registration jounrey for parents

This commit is contained in:
Abdalhamid Alhamad
2024-12-05 11:14:18 +03:00
parent e4b69a406f
commit 2577f2dcac
97 changed files with 2269 additions and 17 deletions

View File

@ -0,0 +1 @@
export * from './otp-default.constant';

View File

@ -0,0 +1,2 @@
export const DEFAULT_OTP_LENGTH = 6;
export const DEFAULT_OTP_DIGIT = '1';

View File

@ -0,0 +1,20 @@
import { IsNotEmpty } from 'class-validator';
import { UserLocale } from '~/core/enums';
import { OtpScope, OtpType } from '../../enums';
export class GenerateOtpRequestDto {
@IsNotEmpty()
userId!: string;
@IsNotEmpty()
phoneNumber!: string;
@IsNotEmpty()
scope!: OtpScope;
@IsNotEmpty()
language?: UserLocale = UserLocale.ENGLISH;
@IsNotEmpty()
otpType!: OtpType;
}

View File

@ -0,0 +1,2 @@
export * from './generate-otp-request.request.dto';
export * from './verify-otp-request.dto';

View File

@ -0,0 +1,16 @@
import { IsNotEmpty } from 'class-validator';
import { OtpScope, OtpType } from '../../enums';
export class VerifyOtpRequestDto {
@IsNotEmpty()
userId!: string;
@IsNotEmpty()
scope!: OtpScope;
@IsNotEmpty()
otpType!: OtpType;
@IsNotEmpty()
value!: string;
}

View File

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

View File

@ -0,0 +1,33 @@
import { Column, CreateDateColumn, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { User } from '~/auth/entities';
import { OtpScope, OtpType } from '../enums';
@Entity('otp')
export class Otp {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column('varchar', { length: 255, name: 'value' })
value!: string;
@Index()
@Column('varchar', { length: 255, name: 'scope' })
scope!: OtpScope;
@Column('varchar', { length: 255, name: 'otp_type' })
otpType!: OtpType;
@Column('timestamp with time zone', { name: 'expires_at' })
expiresAt!: Date;
@Index()
@Column('varchar', { name: 'user_id' })
userId!: string;
@ManyToOne(() => User, (user) => user.otp, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user!: User;
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
createdAt!: Date;
}

View File

@ -0,0 +1,2 @@
export * from './otp-scope.enum';
export * from './otp-type.enum';

View File

@ -0,0 +1,3 @@
export enum OtpScope {
VERIFY_PHONE = 'VERIFY_PHONE',
}

View File

@ -0,0 +1,4 @@
export enum OtpType {
SMS = 'SMS',
EMAIL = 'EMAIL',
}

View File

@ -0,0 +1,13 @@
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Otp } from './entities';
import { OtpRepository } from './repositories';
import { OtpService } from './services/otp.service';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([Otp])],
providers: [OtpService, OtpRepository],
exports: [OtpService],
})
export class OtpModule {}

View File

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

View File

@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MoreThan, Repository } from 'typeorm';
import { VerifyOtpRequestDto } from '../dtos/request';
import { Otp } from '../entities';
const FIVE = 5;
const SIXTY = 60;
const ONE_THOUSAND = 1000;
const FIVE_MINUTES_IN_MILLISECONDS = FIVE * SIXTY * ONE_THOUSAND;
@Injectable()
export class OtpRepository {
constructor(@InjectRepository(Otp) private readonly otpRepository: Repository<Otp>) {}
createOtp(otp: Partial<Otp>) {
return this.otpRepository.save(
this.otpRepository.create({
userId: otp.userId,
value: otp.value,
scope: otp.scope,
otpType: otp.otpType,
expiresAt: new Date(Date.now() + FIVE_MINUTES_IN_MILLISECONDS),
}),
);
}
findOtp(otp: VerifyOtpRequestDto) {
return this.otpRepository.findOne({
where: {
userId: otp.userId,
scope: otp.scope,
value: otp.value,
otpType: otp.otpType,
expiresAt: MoreThan(new Date()),
},
order: {
createdAt: 'DESC',
},
});
}
}

View File

@ -0,0 +1 @@
export * from './otp.service';

View File

@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DEFAULT_OTP_DIGIT, DEFAULT_OTP_LENGTH } from '../constants';
import { GenerateOtpRequestDto, VerifyOtpRequestDto } from '../dtos/request';
import { OtpRepository } from '../repositories';
import { generateRandomOtp } from '../utils';
@Injectable()
export class OtpService {
constructor(private readonly configService: ConfigService, private readonly otpRepository: OtpRepository) {}
private useMock = this.configService.get<boolean>('USE_MOCK', false);
async generateAndSendOtp(sendotpRequest: GenerateOtpRequestDto) {
const otp = this.useMock ? DEFAULT_OTP_DIGIT.repeat(DEFAULT_OTP_LENGTH) : generateRandomOtp(DEFAULT_OTP_LENGTH);
await this.otpRepository.createOtp({ ...sendotpRequest, value: otp });
this.sendOtp(sendotpRequest, otp);
return sendotpRequest.phoneNumber.replace(/.(?=.{4})/g, '*');
}
async verifyOtp(verifyOtpRequest: VerifyOtpRequestDto) {
const otp = await this.otpRepository.findOtp(verifyOtpRequest);
return !!otp;
}
private sendOtp(sendotpRequest: GenerateOtpRequestDto, otp: string) {
// TODO: send OTP to the user
return;
}
}

View File

@ -0,0 +1 @@
export * from './otp-generator.util';

View File

@ -0,0 +1,9 @@
import { getRandomValues } from 'crypto';
import { shuffle } from 'lodash';
const ZERO = 0;
const ONE = 1;
export function generateRandomOtp(length: number): string {
const u32 = getRandomValues(new Uint32Array(ONE))[ZERO];
const randomOtpDigits = u32.toString().substring(ZERO, length).padEnd(length, '0');
return shuffle(randomOtpDigits).join('');
}