From 866696094fc3d51ca0b6fb36d2b293bd9e6dbe26 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sun, 24 May 2026 10:56:28 +0330 Subject: [PATCH] FIX Catcha --- .../actor/actor.auth.controller.ts | 47 ++++--- src/auth/auth-services/actor.auth.service.ts | 12 +- .../auth-services/captcha-account.service.ts | 126 ------------------ src/auth/auth-services/captcha-auth.error.ts | 8 +- src/auth/auth.module.ts | 2 - src/auth/dto/actor/login.actor.dto.ts | 11 +- src/auth/dto/captcha-response.dto.ts | 10 +- src/auth/dto/get-actor-captcha.dto.ts | 33 ----- src/auth/dto/get-captcha-image-query.dto.ts | 15 +++ src/captcha/captcha-challenge.service.ts | 88 ++++++++++++ src/captcha/captcha.module.ts | 22 ++- .../captcha-challenge.db.service.ts | 33 +++++ .../schema/captcha-challenge.schema.ts | 28 ++++ .../entities/schema/damage-expert.schema.ts | 6 - src/users/entities/schema/expert.schema.ts | 7 - .../entities/schema/field-expert.schema.ts | 6 - .../entities/schema/insurer-expert.schema.ts | 6 - src/users/entities/schema/registrar.schema.ts | 6 - 18 files changed, 243 insertions(+), 223 deletions(-) delete mode 100644 src/auth/auth-services/captcha-account.service.ts delete mode 100644 src/auth/dto/get-actor-captcha.dto.ts create mode 100644 src/auth/dto/get-captcha-image-query.dto.ts create mode 100644 src/captcha/captcha-challenge.service.ts create mode 100644 src/captcha/entities/db-service/captcha-challenge.db.service.ts create mode 100644 src/captcha/entities/schema/captcha-challenge.schema.ts 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: `\"captcha\"`\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);