diff --git a/package-lock.json b/package-lock.json
index ad90e3b..d500914 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -50,6 +50,7 @@
"short-unique-id": "^5.2.0",
"standard": "^17.1.2",
"standardjs": "^1.0.0-alpha",
+ "svg-captcha": "^1.4.0",
"uuid": "^11.0.3",
"yargs": "^17.7.2"
},
@@ -14289,6 +14290,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/opentype.js": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-0.7.3.tgz",
+ "integrity": "sha512-Veui5vl2bLonFJ/SjX/WRWJT3SncgiZNnKUyahmXCc2sa1xXW15u3R/3TN5+JFiP7RsjK5ER4HA5eWaEmV9deA==",
+ "license": "MIT",
+ "dependencies": {
+ "tiny-inflate": "^1.0.2"
+ },
+ "bin": {
+ "ot": "bin/ot"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -16894,6 +16907,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/svg-captcha": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/svg-captcha/-/svg-captcha-1.4.0.tgz",
+ "integrity": "sha512-/fkkhavXPE57zRRCjNqAP3txRCSncpMx3NnNZL7iEoyAtYwUjPhJxW6FQTQPG5UPEmCrbFoXS10C3YdJlW7PDg==",
+ "license": "MIT",
+ "dependencies": {
+ "opentype.js": "^0.7.3"
+ },
+ "engines": {
+ "node": ">=4.x"
+ }
+ },
"node_modules/swagger-ui-dist": {
"version": "5.17.14",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz",
@@ -17166,6 +17191,12 @@
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
},
+ "node_modules/tiny-inflate": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
+ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
+ "license": "MIT"
+ },
"node_modules/tlds": {
"version": "1.255.0",
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.255.0.tgz",
diff --git a/package.json b/package.json
index 6fb19e2..3241ecf 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,7 @@
"short-unique-id": "^5.2.0",
"standard": "^17.1.2",
"standardjs": "^1.0.0-alpha",
+ "svg-captcha": "^1.4.0",
"uuid": "^11.0.3",
"yargs": "^17.7.2"
},
diff --git a/src/app.module.ts b/src/app.module.ts
index 6389d4f..1274c36 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -77,14 +77,14 @@ dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
provide: APP_INTERCEPTOR,
useClass: UnicodeDigitsNormalizeInterceptor,
},
- {
- provide: APP_PIPE,
- useValue: new ValidationPipe({
- transform: true,
- whitelist: true,
- forbidNonWhitelisted: false,
- }),
- },
+ // {
+ // provide: APP_PIPE,
+ // useValue: new ValidationPipe({
+ // transform: true,
+ // whitelist: true,
+ // forbidNonWhitelisted: false,
+ // }),
+ // },
],
})
export class AppModule {}
diff --git a/src/auth/auth-controllers/actor/actor.auth.controller.ts b/src/auth/auth-controllers/actor/actor.auth.controller.ts
index 3434a0f..68ccb55 100644
--- a/src/auth/auth-controllers/actor/actor.auth.controller.ts
+++ b/src/auth/auth-controllers/actor/actor.auth.controller.ts
@@ -5,9 +5,12 @@ import {
Param,
Patch,
Post,
+ Query,
Req,
+ Res,
UseGuards,
} from "@nestjs/common";
+import type { Response } from "express";
import {
ApiBody,
ApiAcceptedResponse,
@@ -17,6 +20,9 @@ import {
ApiBearerAuth,
} from "@nestjs/swagger";
import { ActorAuthService } from "src/auth/auth-services/actor.auth.service";
+import { CaptchaAccountService } from "src/auth/auth-services/captcha-account.service";
+import { CaptchaResponseDto } from "src/auth/dto/captcha-response.dto";
+import { GetActorCaptchaQueryDto } from "src/auth/dto/get-actor-captcha.dto";
import {
ForgetPasswordSendCodeDto,
ForgetPasswordVerifyCodeDto,
@@ -37,7 +43,42 @@ import { CurrentUser } from "src/decorators/user.decorator";
@Controller("actor")
@ApiTags("actor")
export class ActorAuthController {
- constructor(private readonly actorAuthService: ActorAuthService) {}
+ constructor(
+ private readonly actorAuthService: ActorAuthService,
+ private readonly captchaAccountService: CaptchaAccountService,
+ ) {}
+
+ @Get("captcha")
+ @ApiOperation({
+ summary: "Get login captcha for an actor account",
+ description:
+ "Returns an SVG captcha stored on the actor record.\n\n" +
+ "**Viewing the image:** Swagger displays `image` as plain text. To see the captcha:\n" +
+ "- Add `format=raw` and open the request URL in a new browser tab, or\n" +
+ "- Copy the full `image` value from the JSON response and paste it into the browser address bar, or\n" +
+ "- In your app: `
`\n\n" +
+ "Type the characters shown in the image into the `captcha` field on POST /actor/login.",
+ })
+ @ApiResponse({ status: 200, type: CaptchaResponseDto })
+ async getCaptcha(
+ @Query() query: GetActorCaptchaQueryDto,
+ @Res({ passthrough: true }) res: Response,
+ ) {
+ const result = await this.captchaAccountService.issueActorCaptcha(
+ query.username,
+ query.role,
+ );
+
+ if (query.format === "raw") {
+ const base64 = result.image.replace(/^data:image\/svg\+xml;base64,/, "");
+ const svg = Buffer.from(base64, "base64").toString("utf8");
+ res.type("image/svg+xml");
+ res.send(svg);
+ return;
+ }
+
+ return result;
+ }
/**
* @deprecated Use the unified actor onboarding flow instead. This endpoint
@@ -113,6 +154,7 @@ export class ActorAuthController {
role: "company",
username: "saman_insurer@gmail.com",
password: "123321",
+ captcha: "a7bx2",
},
},
expert: {
@@ -122,6 +164,7 @@ export class ActorAuthController {
role: "expert",
username: "blame@gmail.com",
password: "123321",
+ captcha: "a7bx2",
},
},
damage_expert: {
@@ -131,6 +174,7 @@ export class ActorAuthController {
role: "damage_expert",
username: "claim@gmail.com",
password: "123321",
+ captcha: "a7bx2",
},
},
},
diff --git a/src/auth/auth-services/actor.auth.service.ts b/src/auth/auth-services/actor.auth.service.ts
index 53fd3c5..9ed766e 100644
--- a/src/auth/auth-services/actor.auth.service.ts
+++ b/src/auth/auth-services/actor.auth.service.ts
@@ -18,6 +18,7 @@ import {
RegisterDtoRs,
} from "src/auth/dto/actor/register.actor.dto";
import { StateListDtoRs } from "src/auth/dto/actor/states.dto";
+import { CaptchaAccountService } from "src/auth/auth-services/captcha-account.service";
import { ClientDbService } from "src/client/entities/db-service/client.db.service";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { UserType } from "src/Types&Enums/userType.enum";
@@ -52,6 +53,7 @@ export class ActorAuthService {
// private readonly mailService: MailService, // Mailer disabled – not used
private readonly clientDbService: ClientDbService,
private readonly otpService: OtpGeneratorService,
+ private readonly captchaAccountService: CaptchaAccountService,
) {}
// TODO convrt to class for dynamic controller
@@ -176,6 +178,13 @@ export class ActorAuthService {
if (typeof password !== "string" || !password) {
throw new BadRequestException("password is required");
}
+ const captcha =
+ typeof body?.captcha === "string" ? body.captcha : undefined;
+ await this.captchaAccountService.verifyActorCaptcha(
+ username,
+ role,
+ captcha,
+ );
const actor = await this.validateActor(username, password, role);
return this.issueActorTokens(actor);
}
diff --git a/src/auth/auth-services/captcha-account.service.ts b/src/auth/auth-services/captcha-account.service.ts
new file mode 100644
index 0000000..3368c19
--- /dev/null
+++ b/src/auth/auth-services/captcha-account.service.ts
@@ -0,0 +1,126 @@
+import { Injectable, NotFoundException } from "@nestjs/common";
+import {
+ CaptchaAuthErrorCode,
+ throwCaptchaAuthError,
+} from "src/auth/auth-services/captcha-auth.error";
+import { CaptchaResponseDto } from "src/auth/dto/captcha-response.dto";
+import { CaptchaService } from "src/captcha/captcha.service";
+import { RoleEnum } from "src/Types&Enums/role.enum";
+import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
+import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
+import { FieldExpertDbService } from "src/users/entities/db-service/field-expert.db.service";
+import { InsurerExpertDbService } from "src/users/entities/db-service/insurer-expert.db.service";
+import { RegistrarDbService } from "src/users/entities/db-service/registrar.db.service";
+import { HashService } from "src/utils/hash/hash.service";
+
+type CaptchaRecord = {
+ captcha?: string | null;
+ captchaExpire?: number | null;
+};
+
+@Injectable()
+export class CaptchaAccountService {
+ constructor(
+ private readonly captchaService: CaptchaService,
+ private readonly hashService: HashService,
+ private readonly expertDbService: ExpertDbService,
+ private readonly damageExpertDbService: DamageExpertDbService,
+ private readonly fieldExpertDbService: FieldExpertDbService,
+ private readonly registrarDbService: RegistrarDbService,
+ private readonly insurerExpertDbService: InsurerExpertDbService,
+ ) {}
+
+ async issueActorCaptcha(
+ username: string,
+ role: RoleEnum,
+ ): Promise {
+ const actor = await this.findActor(role, username);
+ if (!actor) {
+ throw new NotFoundException("Actor account not found");
+ }
+
+ const generated = this.captchaService.generate();
+ const captchaHash = await this.hashService.hash(
+ this.captchaService.normalizeAnswer(generated.text),
+ );
+ await this.updateActorCaptcha(role, username, captchaHash, generated.expiresAt);
+
+ return { image: generated.image, expiresAt: generated.expiresAt };
+ }
+
+ async verifyActorCaptcha(
+ username: string,
+ role: RoleEnum,
+ answer: string | undefined,
+ ): Promise {
+ const actor = await this.findActor(role, username);
+ if (!actor) {
+ throw new NotFoundException("Actor account not found");
+ }
+ await this.assertCaptcha(actor, answer);
+ await this.updateActorCaptcha(role, username, null, 0);
+ }
+
+ private async assertCaptcha(
+ record: CaptchaRecord | null | undefined,
+ answer: string | undefined,
+ ): Promise {
+ if (!answer?.trim()) {
+ throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_REQUIRED);
+ }
+ if (!record?.captcha) {
+ throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_REQUIRED);
+ }
+ const now = Date.now();
+ if (!record.captchaExpire || record.captchaExpire < now) {
+ throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_EXPIRED);
+ }
+ const ok = await this.hashService.compare(
+ this.captchaService.normalizeAnswer(answer),
+ record.captcha,
+ );
+ if (!ok) {
+ throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_INVALID);
+ }
+ }
+
+ private async findActor(role: RoleEnum, username: string) {
+ switch (role) {
+ case RoleEnum.EXPERT:
+ return this.expertDbService.findOne({ email: username });
+ case RoleEnum.DAMAGE_EXPERT:
+ return this.damageExpertDbService.findOne({ email: username });
+ case RoleEnum.FIELD_EXPERT:
+ return this.fieldExpertDbService.findOne({ email: username });
+ case RoleEnum.REGISTRAR:
+ return this.registrarDbService.findOne({ email: username });
+ case RoleEnum.COMPANY:
+ return this.insurerExpertDbService.findOne({ email: username });
+ default:
+ return null;
+ }
+ }
+
+ private async updateActorCaptcha(
+ role: RoleEnum,
+ username: string,
+ captcha: string | null,
+ captchaExpire: number,
+ ) {
+ const update = { captcha, captchaExpire };
+ switch (role) {
+ case RoleEnum.EXPERT:
+ return this.expertDbService.findOneAndUpdate({ email: username }, update);
+ case RoleEnum.DAMAGE_EXPERT:
+ return this.damageExpertDbService.findOneAndUpdate({ email: username }, update);
+ case RoleEnum.FIELD_EXPERT:
+ return this.fieldExpertDbService.findOneAndUpdate({ email: username }, update);
+ case RoleEnum.REGISTRAR:
+ return this.registrarDbService.findOneAndUpdate({ email: username }, update);
+ case RoleEnum.COMPANY:
+ return this.insurerExpertDbService.updateOne({ email: username }, update);
+ default:
+ return null;
+ }
+ }
+}
diff --git a/src/auth/auth-services/captcha-auth.error.ts b/src/auth/auth-services/captcha-auth.error.ts
new file mode 100644
index 0000000..8ea8ee2
--- /dev/null
+++ b/src/auth/auth-services/captcha-auth.error.ts
@@ -0,0 +1,32 @@
+import {
+ BadRequestException,
+ UnauthorizedException,
+} from "@nestjs/common";
+
+export enum CaptchaAuthErrorCode {
+ CAPTCHA_REQUIRED = "CAPTCHA_REQUIRED",
+ CAPTCHA_EXPIRED = "CAPTCHA_EXPIRED",
+ CAPTCHA_INVALID = "CAPTCHA_INVALID",
+}
+
+const messages: Record = {
+ [CaptchaAuthErrorCode.CAPTCHA_REQUIRED]:
+ "Captcha is required. Request a new captcha image first.",
+ [CaptchaAuthErrorCode.CAPTCHA_EXPIRED]:
+ "Captcha has expired. Request a new captcha image.",
+ [CaptchaAuthErrorCode.CAPTCHA_INVALID]: "Captcha is invalid.",
+};
+
+export function captchaAuthErrorBody(code: CaptchaAuthErrorCode) {
+ return {
+ code,
+ message: messages[code],
+ };
+}
+
+export function throwCaptchaAuthError(code: CaptchaAuthErrorCode): never {
+ if (code === CaptchaAuthErrorCode.CAPTCHA_REQUIRED) {
+ throw new BadRequestException(captchaAuthErrorBody(code));
+ }
+ throw new UnauthorizedException(captchaAuthErrorBody(code));
+}
diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts
index 5bd17a0..fa13737 100644
--- a/src/auth/auth.module.ts
+++ b/src/auth/auth.module.ts
@@ -9,6 +9,7 @@ import { UserAuthController } from "src/auth/auth-controllers/user/user.auth.con
import { ActorAuthService } from "src/auth/auth-services/actor.auth.service";
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
import { UserLinkAccessService } from "src/auth/auth-services/user-link-access.service";
+import { CaptchaAccountService } from "src/auth/auth-services/captcha-account.service";
import {
ClaimRequestManagementModel,
ClaimRequestManagementSchema,
@@ -26,6 +27,7 @@ import { UsersModule } from "src/users/users.module";
import { HashModule } from "src/utils/hash/hash.module";
// import { MailModule } from "src/utils/mail/mail.module";
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
+import { CaptchaModule } from "src/captcha/captcha.module";
@Module({
imports: [
@@ -33,6 +35,7 @@ import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.
UsersModule,
ClientModule,
HashModule,
+ CaptchaModule,
PassportModule,
SmsOrchestrationModule,
MongooseModule.forFeature([
@@ -52,6 +55,7 @@ import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.
providers: [
UserAuthService,
UserLinkAccessService,
+ CaptchaAccountService,
ActorAuthService,
LocalStrategy,
LocalActorStrategy,
diff --git a/src/auth/dto/actor/login.actor.dto.ts b/src/auth/dto/actor/login.actor.dto.ts
index 75ca1d1..a041669 100644
--- a/src/auth/dto/actor/login.actor.dto.ts
+++ b/src/auth/dto/actor/login.actor.dto.ts
@@ -1,4 +1,5 @@
import { ApiProperty } from "@nestjs/swagger";
+import { IsNotEmpty, IsString, MaxLength } from "class-validator";
import { RoleEnum } from "src/Types&Enums/role.enum";
export class LoginActorDto {
@@ -10,6 +11,15 @@ export class LoginActorDto {
@ApiProperty({})
password: string;
+
+ @ApiProperty({
+ example: "a7bx2",
+ description: "Captcha text from GET /actor/captcha (same username + role).",
+ })
+ @IsString()
+ @IsNotEmpty()
+ @MaxLength(16)
+ captcha: string;
}
export class LoginActorDtoRs extends LoginActorDto {
diff --git a/src/auth/dto/captcha-response.dto.ts b/src/auth/dto/captcha-response.dto.ts
new file mode 100644
index 0000000..d19541f
--- /dev/null
+++ b/src/auth/dto/captcha-response.dto.ts
@@ -0,0 +1,18 @@
+import { ApiProperty } from "@nestjs/swagger";
+
+export class CaptchaResponseDto {
+ @ApiProperty({
+ description:
+ "SVG captcha as a data URI — use as `
` in the frontend. " +
+ "Swagger shows this as text only; paste the full string into a browser tab, " +
+ "or call GET /actor/captcha?format=raw to view the image directly.",
+ example: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iLi4u",
+ })
+ image: string;
+
+ @ApiProperty({
+ description: "Unix timestamp (ms) when this captcha expires.",
+ example: 1710000000000,
+ })
+ expiresAt: number;
+}
diff --git a/src/auth/dto/get-actor-captcha.dto.ts b/src/auth/dto/get-actor-captcha.dto.ts
new file mode 100644
index 0000000..6f6074e
--- /dev/null
+++ b/src/auth/dto/get-actor-captcha.dto.ts
@@ -0,0 +1,33 @@
+import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
+import { IsEnum, IsIn, IsNotEmpty, IsOptional, IsString, MaxLength } from "class-validator";
+import { RoleEnum } from "src/Types&Enums/role.enum";
+
+export class GetActorCaptchaQueryDto {
+ @ApiProperty({
+ example: "expert@gmail.com",
+ description: "Actor email / username",
+ })
+ @IsString()
+ @IsNotEmpty()
+ @MaxLength(128)
+ username: string;
+
+ @ApiProperty({
+ enum: RoleEnum,
+ example: RoleEnum.EXPERT,
+ description: "Actor role",
+ })
+ @IsEnum(RoleEnum)
+ role: RoleEnum;
+
+ @ApiPropertyOptional({
+ enum: ["json", "raw"],
+ default: "json",
+ description:
+ "Use `raw` to open the captcha directly in the browser (image/svg+xml). " +
+ "Default `json` returns `{ image, expiresAt }` for the frontend.",
+ })
+ @IsOptional()
+ @IsIn(["json", "raw"])
+ format?: "json" | "raw";
+}
diff --git a/src/captcha/captcha.module.ts b/src/captcha/captcha.module.ts
new file mode 100644
index 0000000..9ff3af7
--- /dev/null
+++ b/src/captcha/captcha.module.ts
@@ -0,0 +1,8 @@
+import { Module } from "@nestjs/common";
+import { CaptchaService } from "./captcha.service";
+
+@Module({
+ providers: [CaptchaService],
+ exports: [CaptchaService],
+})
+export class CaptchaModule {}
diff --git a/src/captcha/captcha.service.ts b/src/captcha/captcha.service.ts
new file mode 100644
index 0000000..6088199
--- /dev/null
+++ b/src/captcha/captcha.service.ts
@@ -0,0 +1,42 @@
+import { Injectable } from "@nestjs/common";
+import * as svgCaptcha from "svg-captcha";
+
+export interface GeneratedCaptcha {
+ text: string;
+ image: string;
+ expiresAt: number;
+}
+
+@Injectable()
+export class CaptchaService {
+ generate(): GeneratedCaptcha {
+ const captcha = svgCaptcha.create({
+ size: 5,
+ ignoreChars: "0oO1ilI",
+ noise: 3,
+ color: true,
+ background: "#f8fafc",
+ width: 160,
+ height: 56,
+ fontSize: 52,
+ });
+
+ return {
+ text: captcha.text,
+ image: `data:image/svg+xml;base64,${Buffer.from(captcha.data, "utf8").toString("base64")}`,
+ expiresAt: this.buildExpireAt(),
+ };
+ }
+
+ normalizeAnswer(input: string): string {
+ return input.trim().toLowerCase();
+ }
+
+ buildExpireAt(): number {
+ const raw = Number(
+ process.env.EXP_CAPTCHA_TIME ?? process.env.EXP_OTP_TIME ?? "2",
+ );
+ const minutes = Number.isFinite(raw) && raw > 0 ? raw : 2;
+ return Date.now() + minutes * 60 * 1000;
+ }
+}
diff --git a/src/types/svg-captcha.d.ts b/src/types/svg-captcha.d.ts
new file mode 100644
index 0000000..358723c
--- /dev/null
+++ b/src/types/svg-captcha.d.ts
@@ -0,0 +1,21 @@
+declare module "svg-captcha" {
+ export interface CaptchaOptions {
+ size?: number;
+ ignoreChars?: string;
+ noise?: number;
+ color?: boolean;
+ background?: string;
+ width?: number;
+ height?: number;
+ fontSize?: number;
+ charPreset?: string;
+ }
+
+ export interface CaptchaResult {
+ data: string;
+ text: string;
+ }
+
+ export function create(options?: CaptchaOptions): CaptchaResult;
+ export function createMathExpr(options?: CaptchaOptions): CaptchaResult;
+}
diff --git a/src/users/entities/schema/damage-expert.schema.ts b/src/users/entities/schema/damage-expert.schema.ts
index 84f6029..633a26a 100644
--- a/src/users/entities/schema/damage-expert.schema.ts
+++ b/src/users/entities/schema/damage-expert.schema.ts
@@ -150,6 +150,12 @@ export class DamageExpertModel {
@Prop({ type: "string" })
otp: string;
+ @Prop({ default: null })
+ captcha?: string | null;
+
+ @Prop({ default: 0 })
+ captchaExpire?: number;
+
@Prop({ type: "string" })
address: string;
diff --git a/src/users/entities/schema/expert.schema.ts b/src/users/entities/schema/expert.schema.ts
index d328f9d..21dbf33 100644
--- a/src/users/entities/schema/expert.schema.ts
+++ b/src/users/entities/schema/expert.schema.ts
@@ -72,6 +72,12 @@ export class ExpertModel {
@Prop({ type: "string" })
otp: string;
+ @Prop({ default: null })
+ captcha?: string | null;
+
+ @Prop({ default: 0 })
+ captchaExpire?: number;
+
createdAt: Date;
}
diff --git a/src/users/entities/schema/field-expert.schema.ts b/src/users/entities/schema/field-expert.schema.ts
index ff7dc63..55ce585 100644
--- a/src/users/entities/schema/field-expert.schema.ts
+++ b/src/users/entities/schema/field-expert.schema.ts
@@ -37,6 +37,12 @@ export class FieldExpertModel {
@Prop({ type: "string", default: "" })
otp: string;
+ @Prop({ default: null })
+ captcha?: string | null;
+
+ @Prop({ default: 0 })
+ captchaExpire?: number;
+
createdAt: Date;
}
diff --git a/src/users/entities/schema/insurer-expert.schema.ts b/src/users/entities/schema/insurer-expert.schema.ts
index 947f199..f57f878 100644
--- a/src/users/entities/schema/insurer-expert.schema.ts
+++ b/src/users/entities/schema/insurer-expert.schema.ts
@@ -41,6 +41,12 @@ export class InsurerExpertModel {
@Prop()
address?: string;
+ @Prop({ default: null })
+ captcha?: string | null;
+
+ @Prop({ default: 0 })
+ captchaExpire?: number;
+
createdAt: Date;
}
export const InsurerExpertDbSchema =
diff --git a/src/users/entities/schema/registrar.schema.ts b/src/users/entities/schema/registrar.schema.ts
index a314f78..ba090cb 100644
--- a/src/users/entities/schema/registrar.schema.ts
+++ b/src/users/entities/schema/registrar.schema.ts
@@ -27,6 +27,12 @@ export class RegistrarModel {
@Prop({ type: "string", default: "" })
otp: string;
+
+ @Prop({ default: null })
+ captcha?: string | null;
+
+ @Prop({ default: 0 })
+ captchaExpire?: number;
}
export const RegistrarDbSchema = SchemaFactory.createForClass(RegistrarModel);
diff --git a/test.html b/test.html
new file mode 100644
index 0000000..8912c03
--- /dev/null
+++ b/test.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Document
+
+
+
+
+
\ No newline at end of file