feat: onbard junior by qrcode

This commit is contained in:
Abdalhamid Alhamad
2024-12-15 16:46:49 +03:00
parent 7437403756
commit b0972f1a0a
28 changed files with 3274 additions and 29 deletions

2953
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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],

View File

@ -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);

View File

@ -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';

View 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;
}

View File

@ -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
View File

@ -0,0 +1 @@
export * from './object-values.type';

View 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];

View File

@ -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"`);
}
}

View File

@ -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';

View File

@ -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));
}
} }

View File

@ -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';

View File

@ -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];
}
}

View 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);
}
}

View File

@ -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';

View 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;
}

View File

@ -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;

View 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>;

View File

@ -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';

View File

@ -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 {}

View File

@ -1 +1,2 @@
export * from './junior-token.repository';
export * from './junior.repository'; export * from './junior.repository';

View 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 });
}
}

View File

@ -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,
}); });
} }

View File

@ -1 +1,3 @@
export * from './junior-token.service';
export * from './junior.service'; export * from './junior.service';
export * from './qrcode.service';

View 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);
}
}

View File

@ -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);
}
} }

View 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);
}
}