mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 13:49:40 +00:00
feat: onbard junior by qrcode
This commit is contained in:
2953
package-lock.json
generated
2953
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -64,6 +64,7 @@
|
|||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
"pino-http": "^10.3.0",
|
"pino-http": "^10.3.0",
|
||||||
"pino-pretty": "^13.0.0",
|
"pino-pretty": "^13.0.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"typeorm": "^0.3.20",
|
"typeorm": "^0.3.20",
|
||||||
@ -83,6 +84,7 @@
|
|||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/nodemailer": "^6.4.16",
|
"@types/nodemailer": "^6.4.16",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||||
@ -91,8 +93,10 @@
|
|||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-security": "^1.7.1",
|
"eslint-plugin-security": "^1.7.1",
|
||||||
|
"i": "^0.3.7",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"lint-staged": "^13.2.2",
|
"lint-staged": "^13.2.2",
|
||||||
|
"npm": "^10.9.2",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
|
@ -2,6 +2,7 @@ import { forwardRef, Module } from '@nestjs/common';
|
|||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { CustomerModule } from '~/customer/customer.module';
|
import { CustomerModule } from '~/customer/customer.module';
|
||||||
|
import { JuniorModule } from '~/junior/junior.module';
|
||||||
import { AuthController } from './controllers';
|
import { AuthController } from './controllers';
|
||||||
import { Device, User } from './entities';
|
import { Device, User } from './entities';
|
||||||
import { DeviceRepository, UserRepository } from './repositories';
|
import { DeviceRepository, UserRepository } from './repositories';
|
||||||
@ -10,7 +11,12 @@ import { UserService } from './services/user.service';
|
|||||||
import { AccessTokenStrategy } from './strategies';
|
import { AccessTokenStrategy } from './strategies';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([User, Device]), JwtModule.register({}), forwardRef(() => CustomerModule)],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([User, Device]),
|
||||||
|
JwtModule.register({}),
|
||||||
|
forwardRef(() => CustomerModule),
|
||||||
|
forwardRef(() => JuniorModule),
|
||||||
|
],
|
||||||
providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy],
|
providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
exports: [UserService],
|
exports: [UserService],
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Body, Controller, Headers, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Headers, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { DEVICE_ID_HEADER } from '~/common/constants';
|
import { DEVICE_ID_HEADER } from '~/common/constants';
|
||||||
import { AuthenticatedUser } from '~/common/decorators';
|
import { AuthenticatedUser, Public } from '~/common/decorators';
|
||||||
import { AccessTokenGuard } from '~/common/guards';
|
import { AccessTokenGuard } from '~/common/guards';
|
||||||
import { ResponseFactory } from '~/core/utils';
|
import { ResponseFactory } from '~/core/utils';
|
||||||
import {
|
import {
|
||||||
@ -12,6 +12,7 @@ import {
|
|||||||
LoginRequestDto,
|
LoginRequestDto,
|
||||||
SendForgetPasswordOtpRequestDto,
|
SendForgetPasswordOtpRequestDto,
|
||||||
SetEmailRequestDto,
|
SetEmailRequestDto,
|
||||||
|
setJuniorPasswordRequestDto,
|
||||||
SetPasscodeRequestDto,
|
SetPasscodeRequestDto,
|
||||||
VerifyUserRequestDto,
|
VerifyUserRequestDto,
|
||||||
} from '../dtos/request';
|
} from '../dtos/request';
|
||||||
@ -77,6 +78,13 @@ export class AuthController {
|
|||||||
return this.authService.verifyForgetPasswordOtp(forgetPasswordDto);
|
return this.authService.verifyForgetPasswordOtp(forgetPasswordDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('junior/set-passcode')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@Public()
|
||||||
|
setJuniorPasscode(@Body() setPasscodeDto: setJuniorPasswordRequestDto) {
|
||||||
|
return this.authService.setJuniorPasscode(setPasscodeDto);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
async login(@Body() loginDto: LoginRequestDto, @Headers(DEVICE_ID_HEADER) deviceId: string) {
|
async login(@Body() loginDto: LoginRequestDto, @Headers(DEVICE_ID_HEADER) deviceId: string) {
|
||||||
const [res, user] = await this.authService.login(loginDto, deviceId);
|
const [res, user] = await this.authService.login(loginDto, deviceId);
|
||||||
|
@ -5,5 +5,6 @@ export * from './forget-password.request.dto';
|
|||||||
export * from './login.request.dto';
|
export * from './login.request.dto';
|
||||||
export * from './send-forget-password-otp.request.dto';
|
export * from './send-forget-password-otp.request.dto';
|
||||||
export * from './set-email.request.dto';
|
export * from './set-email.request.dto';
|
||||||
|
export * from './set-junior-password.request.dto';
|
||||||
export * from './set-passcode.request.dto';
|
export * from './set-passcode.request.dto';
|
||||||
export * from './verify-user.request.dto';
|
export * from './verify-user.request.dto';
|
||||||
|
10
src/auth/dtos/request/set-junior-password.request.dto.ts
Normal file
10
src/auth/dtos/request/set-junior-password.request.dto.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
import { SetPasscodeRequestDto } from './set-passcode.request.dto';
|
||||||
|
export class setJuniorPasswordRequestDto extends SetPasscodeRequestDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString({ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.qrToken' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.qrToken' }) })
|
||||||
|
qrToken!: string;
|
||||||
|
}
|
@ -4,6 +4,7 @@ import { JwtService } from '@nestjs/jwt';
|
|||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
|
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
|
||||||
import { OtpService } from '~/common/modules/otp/services';
|
import { OtpService } from '~/common/modules/otp/services';
|
||||||
|
import { JuniorTokenService } from '~/junior/services';
|
||||||
import { PASSCODE_REGEX, PASSWORD_REGEX } from '../constants';
|
import { PASSCODE_REGEX, PASSWORD_REGEX } from '../constants';
|
||||||
import {
|
import {
|
||||||
CreateUnverifiedUserRequestDto,
|
CreateUnverifiedUserRequestDto,
|
||||||
@ -13,6 +14,7 @@ import {
|
|||||||
LoginRequestDto,
|
LoginRequestDto,
|
||||||
SendForgetPasswordOtpRequestDto,
|
SendForgetPasswordOtpRequestDto,
|
||||||
SetEmailRequestDto,
|
SetEmailRequestDto,
|
||||||
|
setJuniorPasswordRequestDto,
|
||||||
} from '../dtos/request';
|
} from '../dtos/request';
|
||||||
import { VerifyUserRequestDto } from '../dtos/request/verify-user.request.dto';
|
import { VerifyUserRequestDto } from '../dtos/request/verify-user.request.dto';
|
||||||
import { User } from '../entities';
|
import { User } from '../entities';
|
||||||
@ -32,6 +34,7 @@ export class AuthService {
|
|||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly deviceService: DeviceService,
|
private readonly deviceService: DeviceService,
|
||||||
|
private readonly juniorTokenService: JuniorTokenService,
|
||||||
) {}
|
) {}
|
||||||
async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
|
async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
|
||||||
const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode });
|
const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode });
|
||||||
@ -186,6 +189,14 @@ export class AuthService {
|
|||||||
return [tokens, user];
|
return [tokens, user];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setJuniorPasscode(body: setJuniorPasswordRequestDto) {
|
||||||
|
const juniorId = await this.juniorTokenService.validateToken(body.qrToken);
|
||||||
|
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
|
||||||
|
const hashedPasscode = bcrypt.hashSync(body.passcode, salt);
|
||||||
|
await this.userService.setPasscode(juniorId, hashedPasscode, salt);
|
||||||
|
await this.juniorTokenService.invalidateToken(body.qrToken);
|
||||||
|
}
|
||||||
|
|
||||||
private async loginWithPassword(loginDto: LoginRequestDto, user: User): Promise<ILoginResponse> {
|
private async loginWithPassword(loginDto: LoginRequestDto, user: User): Promise<ILoginResponse> {
|
||||||
const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password);
|
const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password);
|
||||||
|
|
||||||
|
1
src/core/types/index.ts
Normal file
1
src/core/types/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './object-values.type';
|
13
src/core/types/object-values.type.ts
Normal file
13
src/core/types/object-values.type.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Converts an object's values to a union type.
|
||||||
|
* Should be used in conjunction with constant objects to define enums.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const Status = { SUCCEEDED: 'Success', FAILED: 'Failure' } as const;
|
||||||
|
* type Status = ObjectValues<typeof Status>; // 'Success' | 'Failure'
|
||||||
|
* const foo: Status = Status.SUCCEEDED;
|
||||||
|
* const bar: Status = 'Failure';
|
||||||
|
*
|
||||||
|
* @see https://youtu.be/jjMbPt_H3RQ
|
||||||
|
*/
|
||||||
|
export type ObjectValues<T> = T[keyof T];
|
@ -0,0 +1,32 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class CreateJuniorRegistrationTokenTable1734262619426 implements MigrationInterface {
|
||||||
|
name = 'CreateJuniorRegistrationTokenTable1734262619426';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "junior_registration_tokens"
|
||||||
|
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
|
"token" character varying(255) NOT NULL,
|
||||||
|
"is_used" boolean NOT NULL DEFAULT false,
|
||||||
|
"expiry_date" TIMESTAMP NOT NULL,
|
||||||
|
"junior_id" uuid NOT NULL,
|
||||||
|
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT "UQ_e6a3e23d5a63be76812dc5d7728" UNIQUE ("token"),
|
||||||
|
CONSTRAINT "PK_610992ebec8f664113ae48b946f" PRIMARY KEY ("id"))`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_e6a3e23d5a63be76812dc5d772" ON "junior_registration_tokens" ("token") `);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "junior_registration_tokens" ADD CONSTRAINT "FK_19c52317b5b7aeecc0a11789b53" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "junior_registration_tokens" DROP CONSTRAINT "FK_19c52317b5b7aeecc0a11789b53"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_e6a3e23d5a63be76812dc5d772"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "junior_registration_tokens"`);
|
||||||
|
}
|
||||||
|
}
|
@ -12,3 +12,4 @@ export * from './1733990253208-seeds-default-tasks-logo';
|
|||||||
export * from './1733993920226-create-customer-notifications-settings-table';
|
export * from './1733993920226-create-customer-notifications-settings-table';
|
||||||
export * from './1734246386471-create-saving-goals-entities';
|
export * from './1734246386471-create-saving-goals-entities';
|
||||||
export * from './1734247702310-seeds-goals-categories';
|
export * from './1734247702310-seeds-goals-categories';
|
||||||
|
export * from './1734262619426-create-junior-registration-token-table';
|
||||||
|
@ -2,14 +2,14 @@ import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/co
|
|||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { Roles } from '~/auth/enums';
|
import { Roles } from '~/auth/enums';
|
||||||
import { IJwtPayload } from '~/auth/interfaces';
|
import { IJwtPayload } from '~/auth/interfaces';
|
||||||
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
|
import { AllowedRoles, AuthenticatedUser, Public } from '~/common/decorators';
|
||||||
import { RolesGuard } from '~/common/guards';
|
import { RolesGuard } from '~/common/guards';
|
||||||
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
|
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
|
||||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
import { CustomParseUUIDPipe } from '~/core/pipes';
|
import { CustomParseUUIDPipe } from '~/core/pipes';
|
||||||
import { ResponseFactory } from '~/core/utils';
|
import { ResponseFactory } from '~/core/utils';
|
||||||
import { CreateJuniorRequestDto, SetThemeRequestDto } from '../dtos/request';
|
import { CreateJuniorRequestDto, SetThemeRequestDto } from '../dtos/request';
|
||||||
import { JuniorResponseDto, ThemeResponseDto } from '../dtos/response';
|
import { JuniorResponseDto, QrCodeValidationResponseDto, ThemeResponseDto } from '../dtos/response';
|
||||||
import { JuniorService } from '../services';
|
import { JuniorService } from '../services';
|
||||||
|
|
||||||
@Controller('juniors')
|
@Controller('juniors')
|
||||||
@ -23,9 +23,9 @@ export class JuniorController {
|
|||||||
@AllowedRoles(Roles.GUARDIAN)
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
@ApiDataResponse(JuniorResponseDto)
|
@ApiDataResponse(JuniorResponseDto)
|
||||||
async createJunior(@Body() body: CreateJuniorRequestDto, @AuthenticatedUser() user: IJwtPayload) {
|
async createJunior(@Body() body: CreateJuniorRequestDto, @AuthenticatedUser() user: IJwtPayload) {
|
||||||
const junior = await this.juniorService.createJuniors(body, user.sub);
|
const token = await this.juniorService.createJuniors(body, user.sub);
|
||||||
|
|
||||||
return ResponseFactory.data(new JuniorResponseDto(junior));
|
return ResponseFactory.data(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ -53,7 +53,7 @@ export class JuniorController {
|
|||||||
@AuthenticatedUser() user: IJwtPayload,
|
@AuthenticatedUser() user: IJwtPayload,
|
||||||
@Param('juniorId', CustomParseUUIDPipe) juniorId: string,
|
@Param('juniorId', CustomParseUUIDPipe) juniorId: string,
|
||||||
) {
|
) {
|
||||||
const junior = await this.juniorService.findJuniorById(juniorId, user.sub);
|
const junior = await this.juniorService.findJuniorById(juniorId, false, user.sub);
|
||||||
|
|
||||||
return ResponseFactory.data(new JuniorResponseDto(junior));
|
return ResponseFactory.data(new JuniorResponseDto(junior));
|
||||||
}
|
}
|
||||||
@ -66,4 +66,23 @@ export class JuniorController {
|
|||||||
const theme = await this.juniorService.setTheme(body, user.sub);
|
const theme = await this.juniorService.setTheme(body, user.sub);
|
||||||
return ResponseFactory.data(new ThemeResponseDto(theme));
|
return ResponseFactory.data(new ThemeResponseDto(theme));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(':juniorId/qr-code')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataResponse('string')
|
||||||
|
async generateQrCode(@Param('juniorId', CustomParseUUIDPipe) juniorId: string) {
|
||||||
|
const qrCode = await this.juniorService.generateToken(juniorId);
|
||||||
|
|
||||||
|
return ResponseFactory.data(qrCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('qr-code/:token/validate')
|
||||||
|
@Public()
|
||||||
|
@ApiDataResponse(QrCodeValidationResponseDto)
|
||||||
|
async validateToken(@Param('token') token: string) {
|
||||||
|
const junior = await this.juniorService.validateToken(token);
|
||||||
|
|
||||||
|
return ResponseFactory.data(new QrCodeValidationResponseDto(junior));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
export * from './junior.response.dto';
|
export * from './junior.response.dto';
|
||||||
|
export * from './qr-code-validation-details.response.dto';
|
||||||
|
export * from './qr-code-validation.response.dto';
|
||||||
export * from './theme.response.dto';
|
export * from './theme.response.dto';
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||||
|
import { Junior } from '~/junior/entities';
|
||||||
|
import { GuardianRelationship } from '~/junior/enums';
|
||||||
|
|
||||||
|
export class QrCodeValidationDetailsResponse {
|
||||||
|
@ApiProperty()
|
||||||
|
fullname!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
phoneNumber!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
relationship?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
dateOfBirth!: Date;
|
||||||
|
|
||||||
|
constructor(junior: Junior, guardian?: Guardian) {
|
||||||
|
const person = guardian ? guardian : junior;
|
||||||
|
|
||||||
|
this.fullname = `${person.customer.firstName} ${person.customer.lastName}`;
|
||||||
|
this.phoneNumber = person.customer.user.phoneNumber;
|
||||||
|
this.email = person.customer.user.email;
|
||||||
|
this.dateOfBirth = person.customer.dateOfBirth;
|
||||||
|
this.relationship = guardian ? junior.relationship : GuardianRelationship[junior.relationship];
|
||||||
|
}
|
||||||
|
}
|
16
src/junior/dtos/response/qr-code-validation.response.dto.ts
Normal file
16
src/junior/dtos/response/qr-code-validation.response.dto.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Junior } from '~/junior/entities';
|
||||||
|
import { QrCodeValidationDetailsResponse } from './qr-code-validation-details.response.dto';
|
||||||
|
|
||||||
|
export class QrCodeValidationResponseDto {
|
||||||
|
@ApiProperty({ type: QrCodeValidationDetailsResponse })
|
||||||
|
parentDetails: any;
|
||||||
|
|
||||||
|
@ApiProperty({ type: QrCodeValidationDetailsResponse })
|
||||||
|
childDetails: any;
|
||||||
|
|
||||||
|
constructor(junior: Junior) {
|
||||||
|
this.parentDetails = new QrCodeValidationDetailsResponse(junior, junior.guardian);
|
||||||
|
this.childDetails = new QrCodeValidationDetailsResponse(junior);
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
|
export * from './junior-registration-token.entity';
|
||||||
export * from './junior.entity';
|
export * from './junior.entity';
|
||||||
export * from './theme.entity';
|
export * from './theme.entity';
|
||||||
|
41
src/junior/entities/junior-registration-token.entity.ts
Normal file
41
src/junior/entities/junior-registration-token.entity.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Junior } from './junior.entity';
|
||||||
|
|
||||||
|
@Entity('junior_registration_tokens')
|
||||||
|
export class JuniorRegistrationToken extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, name: 'token', unique: true })
|
||||||
|
@Index()
|
||||||
|
token!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false, name: 'is_used' })
|
||||||
|
isUsed!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp', name: 'expiry_date' })
|
||||||
|
expiryDate!: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'junior_id' })
|
||||||
|
juniorId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Junior, (junior) => junior.registrationTokens)
|
||||||
|
@JoinColumn({ name: 'junior_id' })
|
||||||
|
junior!: Junior;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', name: 'updated_at' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
|
||||||
|
createdAt!: Date;
|
||||||
|
}
|
@ -16,6 +16,7 @@ import { Guardian } from '~/guardian/entities/guradian.entity';
|
|||||||
import { Category, SavingGoal } from '~/saving-goals/entities';
|
import { Category, SavingGoal } from '~/saving-goals/entities';
|
||||||
import { Task } from '~/task/entities';
|
import { Task } from '~/task/entities';
|
||||||
import { Relationship } from '../enums';
|
import { Relationship } from '../enums';
|
||||||
|
import { JuniorRegistrationToken } from './junior-registration-token.entity';
|
||||||
import { Theme } from './theme.entity';
|
import { Theme } from './theme.entity';
|
||||||
|
|
||||||
@Entity('juniors')
|
@Entity('juniors')
|
||||||
@ -58,13 +59,16 @@ export class Junior extends BaseEntity {
|
|||||||
guardian!: Guardian;
|
guardian!: Guardian;
|
||||||
|
|
||||||
@OneToMany(() => Task, (task) => task.assignedTo)
|
@OneToMany(() => Task, (task) => task.assignedTo)
|
||||||
tasks?: Task[];
|
tasks!: Task[];
|
||||||
|
|
||||||
@OneToMany(() => SavingGoal, (savingGoal) => savingGoal.junior)
|
@OneToMany(() => SavingGoal, (savingGoal) => savingGoal.junior)
|
||||||
goals?: SavingGoal[];
|
goals!: SavingGoal[];
|
||||||
|
|
||||||
@OneToMany(() => Category, (category) => category.junior)
|
@OneToMany(() => Category, (category) => category.junior)
|
||||||
categories?: Category[];
|
categories!: Category[];
|
||||||
|
|
||||||
|
@OneToMany(() => JuniorRegistrationToken, (token) => token.junior)
|
||||||
|
registrationTokens!: JuniorRegistrationToken[];
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
9
src/junior/enums/guardian-relationship.enum.ts
Normal file
9
src/junior/enums/guardian-relationship.enum.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ObjectValues } from '~/core/types';
|
||||||
|
import { Relationship } from './relationship.enum';
|
||||||
|
|
||||||
|
export const GuardianRelationship = {
|
||||||
|
[Relationship.PARENT]: 'CHILD',
|
||||||
|
[Relationship.GUARDIAN]: 'WARD',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type GuardianRelationship = ObjectValues<typeof GuardianRelationship>;
|
@ -1,2 +1,3 @@
|
|||||||
|
export * from './guardian-relationship.enum';
|
||||||
export * from './relationship.enum';
|
export * from './relationship.enum';
|
||||||
export * from './theme-color.enum';
|
export * from './theme-color.enum';
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { forwardRef, Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AuthModule } from '~/auth/auth.module';
|
import { AuthModule } from '~/auth/auth.module';
|
||||||
import { CustomerModule } from '~/customer/customer.module';
|
import { CustomerModule } from '~/customer/customer.module';
|
||||||
import { JuniorController } from './controllers';
|
import { JuniorController } from './controllers';
|
||||||
import { Junior, Theme } from './entities';
|
import { Junior, JuniorRegistrationToken, Theme } from './entities';
|
||||||
import { JuniorRepository } from './repositories';
|
import { JuniorRepository, JuniorTokenRepository } from './repositories';
|
||||||
import { JuniorService } from './services';
|
import { JuniorService, JuniorTokenService, QrcodeService } from './services';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [JuniorController],
|
controllers: [JuniorController],
|
||||||
providers: [JuniorService, JuniorRepository],
|
providers: [JuniorService, JuniorRepository, JuniorTokenService, JuniorTokenRepository, QrcodeService],
|
||||||
imports: [TypeOrmModule.forFeature([Junior, Theme]), AuthModule, CustomerModule],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Junior, Theme, JuniorRegistrationToken]),
|
||||||
|
forwardRef(() => AuthModule),
|
||||||
|
CustomerModule,
|
||||||
|
],
|
||||||
|
exports: [JuniorTokenService],
|
||||||
})
|
})
|
||||||
export class JuniorModule {}
|
export class JuniorModule {}
|
||||||
|
@ -1 +1,2 @@
|
|||||||
|
export * from './junior-token.repository';
|
||||||
export * from './junior.repository';
|
export * from './junior.repository';
|
||||||
|
31
src/junior/repositories/junior-token.repository.ts
Normal file
31
src/junior/repositories/junior-token.repository.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { JuniorRegistrationToken } from '../entities';
|
||||||
|
const TOKEN_LENGTH = 16;
|
||||||
|
@Injectable()
|
||||||
|
export class JuniorTokenRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(JuniorRegistrationToken)
|
||||||
|
private readonly juniorTokenRepository: Repository<JuniorRegistrationToken>,
|
||||||
|
) {}
|
||||||
|
generateToken(juniorId: string) {
|
||||||
|
return this.juniorTokenRepository.save(
|
||||||
|
this.juniorTokenRepository.create({
|
||||||
|
juniorId,
|
||||||
|
expiryDate: moment().add('15', 'minutes').toDate(),
|
||||||
|
token: `${moment().unix()}-${crypto.randomBytes(TOKEN_LENGTH).toString('hex')}`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
findByToken(token: string) {
|
||||||
|
return this.juniorTokenRepository.findOne({ where: { token, isUsed: false } });
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateToken(token: string) {
|
||||||
|
return this.juniorTokenRepository.update({ token }, { isUsed: true });
|
||||||
|
}
|
||||||
|
}
|
@ -19,10 +19,14 @@ export class JuniorRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
findJuniorById(juniorId: string, guardianId?: string) {
|
findJuniorById(juniorId: string, withGuardianRelation = false, guardianId?: string) {
|
||||||
|
const relations = ['customer', 'customer.user', 'theme', 'theme.avatar'];
|
||||||
|
if (withGuardianRelation) {
|
||||||
|
relations.push('guardian', 'guardian.customer', 'guardian.customer.user');
|
||||||
|
}
|
||||||
return this.juniorRepository.findOne({
|
return this.juniorRepository.findOne({
|
||||||
where: { id: juniorId, guardianId },
|
where: { id: juniorId, guardianId },
|
||||||
relations: ['customer', 'customer.user', 'theme'],
|
relations,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1 +1,3 @@
|
|||||||
|
export * from './junior-token.service';
|
||||||
export * from './junior.service';
|
export * from './junior.service';
|
||||||
|
export * from './qrcode.service';
|
||||||
|
33
src/junior/services/junior-token.service.ts
Normal file
33
src/junior/services/junior-token.service.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { JuniorTokenRepository } from '../repositories';
|
||||||
|
import { QrcodeService } from './qrcode.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JuniorTokenService {
|
||||||
|
constructor(
|
||||||
|
private readonly juniorTokenRepository: JuniorTokenRepository,
|
||||||
|
private readonly qrCodeService: QrcodeService,
|
||||||
|
) {}
|
||||||
|
async generateToken(juniorId: string): Promise<string> {
|
||||||
|
const tokenEntity = await this.juniorTokenRepository.generateToken(juniorId);
|
||||||
|
return this.qrCodeService.generateQrCode(tokenEntity.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateToken(token: string) {
|
||||||
|
const tokenEntity = await this.juniorTokenRepository.findByToken(token);
|
||||||
|
|
||||||
|
if (!tokenEntity) {
|
||||||
|
throw new BadRequestException('TOKEN.INVALID');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenEntity.expiryDate < new Date()) {
|
||||||
|
throw new BadRequestException('TOKEN.EXPIRED');
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenEntity.juniorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateToken(token: string) {
|
||||||
|
return this.juniorTokenRepository.invalidateToken(token);
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ import { CustomerService } from '~/customer/services';
|
|||||||
import { CreateJuniorRequestDto, SetThemeRequestDto } from '../dtos/request';
|
import { CreateJuniorRequestDto, SetThemeRequestDto } from '../dtos/request';
|
||||||
import { Junior } from '../entities';
|
import { Junior } from '../entities';
|
||||||
import { JuniorRepository } from '../repositories';
|
import { JuniorRepository } from '../repositories';
|
||||||
|
import { JuniorTokenService } from './junior-token.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JuniorService {
|
export class JuniorService {
|
||||||
@ -15,6 +16,7 @@ export class JuniorService {
|
|||||||
private readonly juniorRepository: JuniorRepository,
|
private readonly juniorRepository: JuniorRepository,
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly customerService: CustomerService,
|
private readonly customerService: CustomerService,
|
||||||
|
private readonly juniorTokenService: JuniorTokenService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Transactional()
|
@Transactional()
|
||||||
@ -49,11 +51,11 @@ export class JuniorService {
|
|||||||
user,
|
user,
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.findJuniorById(user.id, guardianId);
|
return this.juniorTokenService.generateToken(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findJuniorById(juniorId: string, guardianId?: string) {
|
async findJuniorById(juniorId: string, withGuardianRelation = false, guardianId?: string) {
|
||||||
const junior = await this.juniorRepository.findJuniorById(juniorId, guardianId);
|
const junior = await this.juniorRepository.findJuniorById(juniorId, withGuardianRelation, guardianId);
|
||||||
|
|
||||||
if (!junior) {
|
if (!junior) {
|
||||||
throw new BadRequestException('JUNIOR.NOT_FOUND');
|
throw new BadRequestException('JUNIOR.NOT_FOUND');
|
||||||
@ -74,4 +76,14 @@ export class JuniorService {
|
|||||||
findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto) {
|
findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto) {
|
||||||
return this.juniorRepository.findJuniorsByGuardianId(guardianId, pageOptions);
|
return this.juniorRepository.findJuniorsByGuardianId(guardianId, pageOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async validateToken(token: string) {
|
||||||
|
const juniorId = await this.juniorTokenService.validateToken(token);
|
||||||
|
|
||||||
|
return this.findJuniorById(juniorId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateToken(juniorId: string) {
|
||||||
|
return this.juniorTokenService.generateToken(juniorId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
9
src/junior/services/qrcode.service.ts
Normal file
9
src/junior/services/qrcode.service.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import * as qrcode from 'qrcode';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class QrcodeService {
|
||||||
|
generateQrCode(token: string): Promise<string> {
|
||||||
|
return qrcode.toDataURL(token);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user