From af875a477368fcf864ce495b51f16ebc63ea2d29 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sun, 24 May 2026 10:10:17 +0330 Subject: [PATCH 1/2] YARA-913 --- package-lock.json | 31 +++++ package.json | 1 + src/app.module.ts | 16 +-- .../actor/actor.auth.controller.ts | 46 ++++++- src/auth/auth-services/actor.auth.service.ts | 9 ++ .../auth-services/captcha-account.service.ts | 126 ++++++++++++++++++ src/auth/auth-services/captcha-auth.error.ts | 32 +++++ src/auth/auth.module.ts | 4 + src/auth/dto/actor/login.actor.dto.ts | 10 ++ src/auth/dto/captcha-response.dto.ts | 18 +++ src/auth/dto/get-actor-captcha.dto.ts | 33 +++++ src/captcha/captcha.module.ts | 8 ++ src/captcha/captcha.service.ts | 42 ++++++ src/types/svg-captcha.d.ts | 21 +++ .../entities/schema/damage-expert.schema.ts | 6 + src/users/entities/schema/expert.schema.ts | 6 + .../entities/schema/field-expert.schema.ts | 6 + .../entities/schema/insurer-expert.schema.ts | 6 + src/users/entities/schema/registrar.schema.ts | 6 + test.html | 21 +++ 20 files changed, 439 insertions(+), 9 deletions(-) create mode 100644 src/auth/auth-services/captcha-account.service.ts create mode 100644 src/auth/auth-services/captcha-auth.error.ts create mode 100644 src/auth/dto/captcha-response.dto.ts create mode 100644 src/auth/dto/get-actor-captcha.dto.ts create mode 100644 src/captcha/captcha.module.ts create mode 100644 src/captcha/captcha.service.ts create mode 100644 src/types/svg-captcha.d.ts create mode 100644 test.html 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: `\"captcha\"`\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 From 4d4106a8ab91258be199b835254ae0e4c1e2405c Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sun, 24 May 2026 10:11:52 +0330 Subject: [PATCH 2/2] Change minimum size of files --- src/client/client.service.ts | 2 +- test.html | 21 --------------------- 2 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 test.html diff --git a/src/client/client.service.ts b/src/client/client.service.ts index f8be153..6d8abc4 100644 --- a/src/client/client.service.ts +++ b/src/client/client.service.ts @@ -47,7 +47,7 @@ export interface MediaLimits { * receive. Per-client `maxBytes` cannot legitimately go above its route's * multer ceiling, so the defaults below are kept at-or-below those. */ -export const DEFAULT_MEDIA_MIN_BYTES = 100 * 1024; // 100KB +export const DEFAULT_MEDIA_MIN_BYTES = 20 * 1024; // 100KB export const DEFAULT_MEDIA_MAX_BYTES = 100 * 1024 * 1024; // 100MB export const DEFAULT_MEDIA_LIMITS: Record = { diff --git a/test.html b/test.html deleted file mode 100644 index 8912c03..0000000 --- a/test.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - Document - - - - - - - - - - - - - - - \ No newline at end of file