diff --git a/src/auth/auth-controllers/actor/actor.auth.controller.ts b/src/auth/auth-controllers/actor/actor.auth.controller.ts
index 68ccb55..8028e84 100644
--- a/src/auth/auth-controllers/actor/actor.auth.controller.ts
+++ b/src/auth/auth-controllers/actor/actor.auth.controller.ts
@@ -20,9 +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 { CaptchaChallengeService } from "src/captcha/captcha-challenge.service";
import { CaptchaResponseDto } from "src/auth/dto/captcha-response.dto";
-import { GetActorCaptchaQueryDto } from "src/auth/dto/get-actor-captcha.dto";
+import { GetCaptchaImageQueryDto } from "src/auth/dto/get-captcha-image-query.dto";
import {
ForgetPasswordSendCodeDto,
ForgetPasswordVerifyCodeDto,
@@ -45,33 +45,29 @@ import { CurrentUser } from "src/decorators/user.decorator";
export class ActorAuthController {
constructor(
private readonly actorAuthService: ActorAuthService,
- private readonly captchaAccountService: CaptchaAccountService,
+ private readonly captchaChallengeService: CaptchaChallengeService,
) {}
@Get("captcha")
@ApiOperation({
- summary: "Get login captcha for an actor account",
+ summary: "Get a login captcha",
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.",
+ "Issues a new captcha challenge. Returns `captchaId`, `image`, and `expiresAt`. " +
+ "Send `captchaId` and the typed characters as `captcha` on POST /actor/login.\n\n" +
+ "Optional `format=raw` returns image/svg+xml for browser preview (same captchaId is in JSON when omitted).",
})
@ApiResponse({ status: 200, type: CaptchaResponseDto })
async getCaptcha(
- @Query() query: GetActorCaptchaQueryDto,
+ @Query() query: GetCaptchaImageQueryDto,
@Res({ passthrough: true }) res: Response,
) {
- const result = await this.captchaAccountService.issueActorCaptcha(
- query.username,
- query.role,
- );
+ const result = await this.captchaChallengeService.issue();
if (query.format === "raw") {
- const base64 = result.image.replace(/^data:image\/svg\+xml;base64,/, "");
- const svg = Buffer.from(base64, "base64").toString("utf8");
+ const svg = await this.captchaChallengeService.getImageById(
+ result.captchaId,
+ );
+ res.setHeader("X-Captcha-Id", result.captchaId);
res.type("image/svg+xml");
res.send(svg);
return;
@@ -80,6 +76,20 @@ export class ActorAuthController {
return result;
}
+ @Get("captcha/:captchaId/image")
+ @ApiOperation({
+ summary: "View captcha image by id",
+ description: "Returns raw SVG for a previously issued captcha challenge.",
+ })
+ async getCaptchaImage(
+ @Param("captchaId") captchaId: string,
+ @Res({ passthrough: true }) res: Response,
+ ) {
+ const svg = await this.captchaChallengeService.getImageById(captchaId);
+ res.type("image/svg+xml");
+ res.send(svg);
+ }
+
/**
* @deprecated Use the unified actor onboarding flow instead. This endpoint
* will be removed in a future release.
@@ -154,6 +164,7 @@ export class ActorAuthController {
role: "company",
username: "saman_insurer@gmail.com",
password: "123321",
+ captchaId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
captcha: "a7bx2",
},
},
@@ -164,6 +175,7 @@ export class ActorAuthController {
role: "expert",
username: "blame@gmail.com",
password: "123321",
+ captchaId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
captcha: "a7bx2",
},
},
@@ -174,6 +186,7 @@ export class ActorAuthController {
role: "damage_expert",
username: "claim@gmail.com",
password: "123321",
+ captchaId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
captcha: "a7bx2",
},
},
diff --git a/src/auth/auth-services/actor.auth.service.ts b/src/auth/auth-services/actor.auth.service.ts
index 9ed766e..09e4254 100644
--- a/src/auth/auth-services/actor.auth.service.ts
+++ b/src/auth/auth-services/actor.auth.service.ts
@@ -18,7 +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 { CaptchaChallengeService } from "src/captcha/captcha-challenge.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";
@@ -53,7 +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,
+ private readonly captchaChallengeService: CaptchaChallengeService,
) {}
// TODO convrt to class for dynamic controller
@@ -178,13 +178,11 @@ export class ActorAuthService {
if (typeof password !== "string" || !password) {
throw new BadRequestException("password is required");
}
+ const captchaId =
+ typeof body?.captchaId === "string" ? body.captchaId : undefined;
const captcha =
typeof body?.captcha === "string" ? body.captcha : undefined;
- await this.captchaAccountService.verifyActorCaptcha(
- username,
- role,
- captcha,
- );
+ await this.captchaChallengeService.verify(captchaId, 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
deleted file mode 100644
index 3368c19..0000000
--- a/src/auth/auth-services/captcha-account.service.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-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
index 8ea8ee2..0baac20 100644
--- a/src/auth/auth-services/captcha-auth.error.ts
+++ b/src/auth/auth-services/captcha-auth.error.ts
@@ -5,6 +5,7 @@ import {
export enum CaptchaAuthErrorCode {
CAPTCHA_REQUIRED = "CAPTCHA_REQUIRED",
+ CAPTCHA_NOT_FOUND = "CAPTCHA_NOT_FOUND",
CAPTCHA_EXPIRED = "CAPTCHA_EXPIRED",
CAPTCHA_INVALID = "CAPTCHA_INVALID",
}
@@ -12,6 +13,8 @@ export enum CaptchaAuthErrorCode {
const messages: Record = {
[CaptchaAuthErrorCode.CAPTCHA_REQUIRED]:
"Captcha is required. Request a new captcha image first.",
+ [CaptchaAuthErrorCode.CAPTCHA_NOT_FOUND]:
+ "Captcha id was not found. Request a new captcha image.",
[CaptchaAuthErrorCode.CAPTCHA_EXPIRED]:
"Captcha has expired. Request a new captcha image.",
[CaptchaAuthErrorCode.CAPTCHA_INVALID]: "Captcha is invalid.",
@@ -25,7 +28,10 @@ export function captchaAuthErrorBody(code: CaptchaAuthErrorCode) {
}
export function throwCaptchaAuthError(code: CaptchaAuthErrorCode): never {
- if (code === CaptchaAuthErrorCode.CAPTCHA_REQUIRED) {
+ if (
+ code === CaptchaAuthErrorCode.CAPTCHA_REQUIRED ||
+ code === CaptchaAuthErrorCode.CAPTCHA_NOT_FOUND
+ ) {
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 fa13737..498a66a 100644
--- a/src/auth/auth.module.ts
+++ b/src/auth/auth.module.ts
@@ -9,7 +9,6 @@ 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,
@@ -55,7 +54,6 @@ import { CaptchaModule } from "src/captcha/captcha.module";
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 a041669..0c0c471 100644
--- a/src/auth/dto/actor/login.actor.dto.ts
+++ b/src/auth/dto/actor/login.actor.dto.ts
@@ -12,9 +12,18 @@ export class LoginActorDto {
@ApiProperty({})
password: string;
+ @ApiProperty({
+ example: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
+ description: "Captcha id from GET /actor/captcha.",
+ })
+ @IsString()
+ @IsNotEmpty()
+ @MaxLength(64)
+ captchaId: string;
+
@ApiProperty({
example: "a7bx2",
- description: "Captcha text from GET /actor/captcha (same username + role).",
+ description: "Characters shown in the captcha image.",
})
@IsString()
@IsNotEmpty()
diff --git a/src/auth/dto/captcha-response.dto.ts b/src/auth/dto/captcha-response.dto.ts
index d19541f..6e4bfa3 100644
--- a/src/auth/dto/captcha-response.dto.ts
+++ b/src/auth/dto/captcha-response.dto.ts
@@ -1,11 +1,15 @@
import { ApiProperty } from "@nestjs/swagger";
export class CaptchaResponseDto {
+ @ApiProperty({
+ description: "Captcha challenge id — send back with POST /actor/login.",
+ example: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
+ })
+ captchaId: string;
+
@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.",
+ "SVG captcha as a data URI — use as `
` in the frontend.",
example: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iLi4u",
})
image: string;
diff --git a/src/auth/dto/get-actor-captcha.dto.ts b/src/auth/dto/get-actor-captcha.dto.ts
deleted file mode 100644
index 6f6074e..0000000
--- a/src/auth/dto/get-actor-captcha.dto.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-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/auth/dto/get-captcha-image-query.dto.ts b/src/auth/dto/get-captcha-image-query.dto.ts
new file mode 100644
index 0000000..94d828c
--- /dev/null
+++ b/src/auth/dto/get-captcha-image-query.dto.ts
@@ -0,0 +1,15 @@
+import { ApiPropertyOptional } from "@nestjs/swagger";
+import { IsIn, IsOptional } from "class-validator";
+
+export class GetCaptchaImageQueryDto {
+ @ApiPropertyOptional({
+ enum: ["json", "raw"],
+ default: "json",
+ description:
+ "On GET /actor/captcha, use `raw` to return image/svg+xml (for browser preview). " +
+ "The JSON response includes `captchaId` either way.",
+ })
+ @IsOptional()
+ @IsIn(["json", "raw"])
+ format?: "json" | "raw";
+}
diff --git a/src/captcha/captcha-challenge.service.ts b/src/captcha/captcha-challenge.service.ts
new file mode 100644
index 0000000..4bfd68f
--- /dev/null
+++ b/src/captcha/captcha-challenge.service.ts
@@ -0,0 +1,88 @@
+import { Injectable, NotFoundException } from "@nestjs/common";
+import { randomUUID } from "node:crypto";
+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 { CaptchaChallengeDbService } from "src/captcha/entities/db-service/captcha-challenge.db.service";
+import { HashService } from "src/utils/hash/hash.service";
+
+@Injectable()
+export class CaptchaChallengeService {
+ constructor(
+ private readonly captchaService: CaptchaService,
+ private readonly hashService: HashService,
+ private readonly captchaChallengeDbService: CaptchaChallengeDbService,
+ ) {}
+
+ async issue(): Promise {
+ const generated = this.captchaService.generate();
+ const captchaId = randomUUID();
+ const answerHash = await this.hashService.hash(
+ this.captchaService.normalizeAnswer(generated.text),
+ );
+
+ await this.captchaChallengeDbService.create({
+ captchaId,
+ answerHash,
+ image: generated.image,
+ expiresAt: generated.expiresAt,
+ expireAt: new Date(generated.expiresAt),
+ usedAt: null,
+ });
+
+ return {
+ captchaId,
+ image: generated.image,
+ expiresAt: generated.expiresAt,
+ };
+ }
+
+ async getImageById(captchaId: string): Promise {
+ const challenge =
+ await this.captchaChallengeDbService.findByCaptchaId(captchaId);
+ if (!challenge) {
+ throw new NotFoundException("Captcha not found");
+ }
+ return this.decodeImage(challenge.image);
+ }
+
+ async verify(captchaId: string | undefined, answer: string | undefined): Promise {
+ if (!captchaId?.trim()) {
+ throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_REQUIRED);
+ }
+ if (!answer?.trim()) {
+ throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_REQUIRED);
+ }
+
+ const challenge = await this.captchaChallengeDbService.findByCaptchaId(
+ captchaId.trim(),
+ );
+ if (!challenge) {
+ throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_NOT_FOUND);
+ }
+ if (challenge.usedAt) {
+ throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_INVALID);
+ }
+ if (challenge.expiresAt < Date.now()) {
+ throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_EXPIRED);
+ }
+
+ const ok = await this.hashService.compare(
+ this.captchaService.normalizeAnswer(answer),
+ challenge.answerHash,
+ );
+ if (!ok) {
+ throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_INVALID);
+ }
+
+ await this.captchaChallengeDbService.markUsed(challenge.captchaId);
+ }
+
+ private decodeImage(imageDataUri: string): string {
+ const base64 = imageDataUri.replace(/^data:image\/svg\+xml;base64,/, "");
+ return Buffer.from(base64, "base64").toString("utf8");
+ }
+}
diff --git a/src/captcha/captcha.module.ts b/src/captcha/captcha.module.ts
index 9ff3af7..1d9be6b 100644
--- a/src/captcha/captcha.module.ts
+++ b/src/captcha/captcha.module.ts
@@ -1,8 +1,26 @@
import { Module } from "@nestjs/common";
+import { MongooseModule } from "@nestjs/mongoose";
+import { HashModule } from "src/utils/hash/hash.module";
+import { CaptchaChallengeService } from "./captcha-challenge.service";
import { CaptchaService } from "./captcha.service";
+import { CaptchaChallengeDbService } from "./entities/db-service/captcha-challenge.db.service";
+import {
+ CaptchaChallenge,
+ CaptchaChallengeSchema,
+} from "./entities/schema/captcha-challenge.schema";
@Module({
- providers: [CaptchaService],
- exports: [CaptchaService],
+ imports: [
+ HashModule,
+ MongooseModule.forFeature([
+ { name: CaptchaChallenge.name, schema: CaptchaChallengeSchema },
+ ]),
+ ],
+ providers: [
+ CaptchaService,
+ CaptchaChallengeDbService,
+ CaptchaChallengeService,
+ ],
+ exports: [CaptchaChallengeService],
})
export class CaptchaModule {}
diff --git a/src/captcha/entities/db-service/captcha-challenge.db.service.ts b/src/captcha/entities/db-service/captcha-challenge.db.service.ts
new file mode 100644
index 0000000..f04e02f
--- /dev/null
+++ b/src/captcha/entities/db-service/captcha-challenge.db.service.ts
@@ -0,0 +1,33 @@
+import { Injectable } from "@nestjs/common";
+import { InjectModel } from "@nestjs/mongoose";
+import { Model } from "mongoose";
+import {
+ CaptchaChallenge,
+ CaptchaChallengeDocument,
+} from "../schema/captcha-challenge.schema";
+
+@Injectable()
+export class CaptchaChallengeDbService {
+ constructor(
+ @InjectModel(CaptchaChallenge.name)
+ private readonly captchaChallengeModel: Model,
+ ) {}
+
+ create(data: CaptchaChallenge): Promise {
+ return this.captchaChallengeModel.create(data);
+ }
+
+ findByCaptchaId(
+ captchaId: string,
+ ): Promise {
+ return this.captchaChallengeModel.findOne({ captchaId });
+ }
+
+ markUsed(captchaId: string): Promise {
+ return this.captchaChallengeModel.findOneAndUpdate(
+ { captchaId, usedAt: null },
+ { $set: { usedAt: new Date() } },
+ { new: true },
+ );
+ }
+}
diff --git a/src/captcha/entities/schema/captcha-challenge.schema.ts b/src/captcha/entities/schema/captcha-challenge.schema.ts
new file mode 100644
index 0000000..aab419a
--- /dev/null
+++ b/src/captcha/entities/schema/captcha-challenge.schema.ts
@@ -0,0 +1,28 @@
+import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
+import { HydratedDocument } from "mongoose";
+
+@Schema({ collection: "captcha-challenges", timestamps: true, versionKey: false })
+export class CaptchaChallenge {
+ @Prop({ required: true, unique: true, index: true })
+ captchaId: string;
+
+ @Prop({ required: true })
+ answerHash: string;
+
+ @Prop({ required: true })
+ image: string;
+
+ @Prop({ required: true, index: true })
+ expiresAt: number;
+
+ /** Mongo TTL — document removed shortly after this time. */
+ @Prop({ required: true, expires: 0 })
+ expireAt: Date;
+
+ @Prop({ default: null })
+ usedAt?: Date | null;
+}
+
+export type CaptchaChallengeDocument = HydratedDocument;
+export const CaptchaChallengeSchema =
+ SchemaFactory.createForClass(CaptchaChallenge);
diff --git a/src/users/entities/schema/damage-expert.schema.ts b/src/users/entities/schema/damage-expert.schema.ts
index 633a26a..84f6029 100644
--- a/src/users/entities/schema/damage-expert.schema.ts
+++ b/src/users/entities/schema/damage-expert.schema.ts
@@ -150,12 +150,6 @@ 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 21dbf33..e14fddf 100644
--- a/src/users/entities/schema/expert.schema.ts
+++ b/src/users/entities/schema/expert.schema.ts
@@ -72,13 +72,6 @@ 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 55ce585..ff7dc63 100644
--- a/src/users/entities/schema/field-expert.schema.ts
+++ b/src/users/entities/schema/field-expert.schema.ts
@@ -37,12 +37,6 @@ 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 f57f878..947f199 100644
--- a/src/users/entities/schema/insurer-expert.schema.ts
+++ b/src/users/entities/schema/insurer-expert.schema.ts
@@ -41,12 +41,6 @@ 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 ba090cb..a314f78 100644
--- a/src/users/entities/schema/registrar.schema.ts
+++ b/src/users/entities/schema/registrar.schema.ts
@@ -27,12 +27,6 @@ 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);