forked from Yara724/api
YARA-913
This commit is contained in:
31
package-lock.json
generated
31
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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: `<img src={response.image} alt=\"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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
126
src/auth/auth-services/captcha-account.service.ts
Normal file
126
src/auth/auth-services/captcha-account.service.ts
Normal file
@@ -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<CaptchaResponseDto> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/auth/auth-services/captcha-auth.error.ts
Normal file
32
src/auth/auth-services/captcha-auth.error.ts
Normal file
@@ -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, string> = {
|
||||
[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));
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
18
src/auth/dto/captcha-response.dto.ts
Normal file
18
src/auth/dto/captcha-response.dto.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class CaptchaResponseDto {
|
||||
@ApiProperty({
|
||||
description:
|
||||
"SVG captcha as a data URI — use as `<img src={image} />` 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;
|
||||
}
|
||||
33
src/auth/dto/get-actor-captcha.dto.ts
Normal file
33
src/auth/dto/get-actor-captcha.dto.ts
Normal file
@@ -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";
|
||||
}
|
||||
8
src/captcha/captcha.module.ts
Normal file
8
src/captcha/captcha.module.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { CaptchaService } from "./captcha.service";
|
||||
|
||||
@Module({
|
||||
providers: [CaptchaService],
|
||||
exports: [CaptchaService],
|
||||
})
|
||||
export class CaptchaModule {}
|
||||
42
src/captcha/captcha.service.ts
Normal file
42
src/captcha/captcha.service.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
21
src/types/svg-captcha.d.ts
vendored
Normal file
21
src/types/svg-captcha.d.ts
vendored
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
|
||||
21
test.html
Normal file
21
test.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="56" viewBox="0,0,160,56">
|
||||
<rect width="100%" height="100%" fill="#f8fafc"/>
|
||||
<path fill="#5858e3" d="M131.17 34.59L122.57 25.29L122.69 25.40Q123.38 28.75 123.38 32.21L123.34 32.17L123.34 32.17Q123.40 37.78 121.49 42.94L121.46 42.91L121.47 42.92Q120.47 43.16 118.39 43.87L118.49 43.97L118.52 43.99Q121.06 38.55 121.06 32.33L121.10 32.37L120.98 32.25Q121.01 24.25 116.84 17.36L116.78 17.31L116.90 17.42Q117.26 17.53 117.89 17.92L117.95 17.98L117.89 17.92Q129.18 28.40 136.46 36.99L136.45 36.97L136.52 37.04Q136.13 34.68 136.13 32.41L136.06 32.34L136.14 32.42Q136.11 25.67 138.44 19.91L138.48 19.95L138.55 20.02Q140.71 19.52 142.19 18.89L142.02 18.71L142.16 18.86Q139.05 24.55 138.77 31.20L138.67 31.09L138.70 31.13Q138.42 37.95 141.17 43.68L141.19 43.69L141.27 43.77Q140.79 43.75 140.40 43.57L140.27 43.44L140.34 43.51Q135.99 39.73 131.15 34.57L131.26 34.68ZM143.71 46.74L143.72 46.75L143.57 46.60Q144.01 46.76 144.40 46.94L144.44 46.98L145.22 47.30L145.28 47.36Q140.56 40.98 140.56 32.28L140.43 32.15L140.56 32.28Q140.40 25.40 143.47 19.60L143.51 19.64L143.47 19.61Q142.93 19.87 141.62 20.44L141.61 20.43L141.62 20.43Q141.93 19.61 142.70 18.16L142.70 18.16L142.76 18.22Q140.53 19.10 138.27 19.63L138.28 19.64L138.18 19.54Q135.78 25.59 135.78 32.49L135.84 32.55L135.79 32.49Q135.73 34.17 135.91 36.01L135.97 36.06L136.05 36.15Q131.71 30.85 127.29 26.57L127.23 26.51L117.79 17.43L117.93 17.57Q117.29 17.18 116.16 16.65L116.25 16.74L116.11 16.60Q120.63 23.77 120.63 32.22L120.77 32.36L120.73 32.32Q120.74 38.93 117.87 44.52L117.93 44.58L117.78 44.43Q118.05 44.31 119.57 43.75L119.72 43.89L119.69 43.86Q119.43 44.62 118.65 46.00L118.60 45.95L118.51 45.86Q119.88 45.51 123.10 44.76L123.02 44.68L123.05 44.71Q125.13 38.94 125.13 32.26L125.22 32.36L125.12 32.25Q125.14 30.32 124.92 28.34L125.04 28.46L124.95 28.37Q135.64 40.30 143.63 46.66Z"/>
|
||||
<path d="M3 11 C83 13,77 16,151 28" stroke="#e8bc90" fill="none"/>
|
||||
<path fill="#e38362" d="M53.88 33.40L53.96 33.48L54.04 33.56Q54.09 32.20 53.33 31.35L53.27 31.29L53.29 31.31Q52.49 30.42 51.15 30.46L51.18 30.49L51.14 30.45Q49.04 30.58 48.27 32.56L48.24 32.54L48.31 32.60Q47.94 33.22 47.90 33.86L47.89 33.85L47.99 33.94Q47.81 38.46 46.96 42.49L46.79 42.33L46.95 42.49Q45.29 42.88 43.56 43.79L43.51 43.75L43.69 43.93Q45.75 37.40 45.51 30.61L45.44 30.55L45.55 30.66Q45.17 23.70 42.80 17.37L42.89 17.46L42.89 17.46Q44.51 18.66 46.38 19.29L46.39 19.30L46.41 19.33Q47.74 24.68 47.92 30.13L47.81 30.02L47.85 30.06Q49.16 28.15 51.67 28.22L51.61 28.16L51.71 28.26Q56.36 28.28 56.47 33.27L56.41 33.21L56.46 33.26Q56.68 38.64 57.95 42.95L57.86 42.86L57.82 42.83Q56.16 42.12 54.54 41.94L54.61 42.02L54.50 41.91Q54.15 39.01 54.04 33.56ZM54.23 42.23L54.20 42.20L54.15 42.16Q55.29 42.45 56.32 42.70L56.34 42.72L56.23 42.61Q56.35 42.84 56.85 44.68L56.77 44.59L56.83 44.66Q59.51 45.47 61.39 46.81L61.40 46.83L61.39 46.82Q58.94 41.50 58.44 35.63L58.49 35.68L58.35 35.54Q58.09 32.13 56.78 30.68L56.75 30.65L56.74 30.64Q56.82 30.66 56.22 30.23L56.23 30.24L56.18 30.19Q56.04 29.80 55.44 29.09L55.43 29.08L55.46 29.04L55.42 29.01Q54.57 28.15 51.67 27.90L51.69 27.93L51.59 27.83Q51.11 28.01 49.80 28.33L49.67 28.20L49.80 28.33Q49.42 23.63 49.10 21.30L49.11 21.31L49.13 21.34Q48.50 21.31 47.12 21.02L47.14 21.05L47.11 21.02Q46.92 20.29 46.67 18.91L46.74 18.98L46.66 18.90Q43.87 17.95 42.14 16.50L42.22 16.58L42.18 16.55Q44.85 23.10 45.09 30.34L45.17 30.42L45.27 30.52Q45.39 37.60 43.02 44.42L43.19 44.59L43.06 44.47Q43.94 43.93 45.00 43.47L45.08 43.55L44.31 45.58L44.35 45.61Q47.14 44.27 48.98 44.09L49.03 44.14L49.06 44.17Q49.65 39.21 49.83 35.54L49.74 35.45L49.83 35.54Q49.89 33.80 51.70 32.78L51.61 32.69L51.59 32.67Q52.13 32.50 52.62 32.53L52.73 32.64L52.75 32.66Q52.72 32.49 53.04 32.52L53.11 32.60L53.59 32.76L53.44 32.61Q53.59 33.11 53.70 33.54L53.68 33.52L53.55 33.39Q53.80 39.08 54.26 42.26Z"/>
|
||||
<path fill="#de2f2f" d="M104.83 34.59L96.22 25.27L96.35 25.40Q97.00 28.70 97.00 32.16L97.08 32.24L97.06 32.23Q97.09 37.80 95.18 42.96L95.07 42.86L95.17 42.95Q94.27 43.29 92.18 43.99L92.18 43.99L92.10 43.91Q94.75 38.58 94.75 32.35L94.61 32.22L94.63 32.23Q94.70 24.28 90.53 17.38L90.54 17.39L90.59 17.45Q91.00 17.61 91.64 18.00L91.58 17.94L91.53 17.89Q102.74 28.29 110.03 36.88L110.05 36.90L110.03 36.89Q109.79 34.66 109.79 32.40L109.81 32.43L109.73 32.34Q109.75 25.65 112.09 19.89L112.23 20.04L112.20 20.00Q114.28 19.43 115.76 18.79L115.86 18.89L115.72 18.75Q112.70 24.53 112.42 31.18L112.37 31.13L112.40 31.16Q112.17 38.04 114.93 43.77L114.88 43.71L114.88 43.71Q114.41 43.70 114.02 43.52L113.94 43.45L114.00 43.50Q109.67 39.74 104.82 34.58L104.88 34.64ZM117.42 46.79L117.32 46.69L117.25 46.61Q117.81 46.89 118.20 47.07L118.15 47.02L118.96 47.38L118.97 47.38Q114.08 40.83 114.08 32.13L114.08 32.13L114.20 32.25Q114.18 25.52 117.25 19.72L117.20 19.67L117.25 19.71Q116.70 19.98 115.40 20.55L115.40 20.55L115.24 20.39Q115.70 19.72 116.48 18.27L116.54 18.33L116.44 18.24Q114.10 19.01 111.84 19.54L111.98 19.68L111.94 19.63Q109.38 25.53 109.38 32.42L109.44 32.48L109.40 32.44Q109.37 34.14 109.54 35.97L109.70 36.13L109.67 36.11Q105.31 30.79 100.89 26.51L100.80 26.42L91.49 17.46L91.49 17.46Q91.08 17.30 89.95 16.77L89.82 16.65L89.93 16.75Q94.43 23.90 94.43 32.35L94.47 32.40L94.46 32.38Q94.45 38.98 91.59 44.57L91.51 44.49L91.49 44.47Q91.72 44.32 93.25 43.75L93.32 43.82L93.33 43.84Q93.03 44.56 92.25 45.93L92.20 45.88L92.31 46.00Q93.60 45.56 96.82 44.81L96.66 44.65L96.73 44.73Q98.92 39.07 98.92 32.39L98.86 32.33L98.92 32.38Q98.89 30.41 98.67 28.43L98.66 28.41L98.68 28.43Q109.27 40.26 117.25 46.62Z"/>
|
||||
<path d="M9 38 C84 22,99 10,152 22" stroke="#dc4b4b" fill="none"/>
|
||||
<path d="M17 8 C74 14,100 43,158 36" stroke="#e051bc" fill="none"/>
|
||||
<path fill="#e269c4" d="M80.53 43.18L80.36 43.01L77.74 43.04L77.68 42.97Q75.40 42.82 74.66 42.71L74.66 42.71L74.63 42.68Q73.01 42.44 71.92 41.63L71.94 41.65L71.81 41.52Q70.63 40.24 70.42 37.13L70.56 37.28L70.49 37.20Q70.38 36.67 70.31 35.54L70.38 35.61L70.40 35.62Q70.40 35.24 70.33 34.22L70.22 34.10L70.38 34.26Q70.36 33.50 70.39 32.83L70.34 32.78L70.37 32.81Q70.41 30.86 71.36 30.16L71.47 30.27L71.52 30.31Q73.25 29.18 77.74 28.97L77.77 29.00L77.69 28.91Q79.57 28.82 80.31 28.82L80.31 28.81L80.28 28.78Q81.75 28.88 83.02 29.16L82.90 29.04L82.96 29.10Q82.48 30.31 81.98 32.01L81.88 31.91L81.89 31.92Q81.60 32.16 81.14 32.12L81.03 32.01L81.11 32.09Q81.04 32.09 80.55 31.98L80.38 31.82L79.50 31.61L79.51 31.62Q78.62 31.47 78.05 31.50L78.02 31.48L78.11 31.56Q73.54 31.45 72.98 34.07L73.07 34.16L72.96 34.05Q72.76 35.16 72.83 36.04L72.87 36.08L72.82 36.03Q73.11 38.58 74.25 39.59L74.22 39.55L74.20 39.53Q75.38 40.57 77.79 40.61L77.63 40.46L77.69 40.51Q79.17 40.65 79.84 40.36L79.85 40.37L80.71 39.82L80.78 39.89Q81.17 39.58 81.70 39.51L81.74 39.54L81.77 39.57Q81.93 41.19 82.29 42.39L82.23 42.33L82.40 42.50Q82.12 42.79 80.46 43.11ZM83.38 45.74L83.38 45.74L83.40 45.77Q83.72 45.76 84.49 45.83L84.67 46.00L84.49 45.83Q85.10 45.87 85.41 45.34L85.56 45.49L85.58 45.51Q84.31 43.42 83.85 41.47L83.92 41.55L83.83 41.45Q83.68 41.59 83.26 41.59L83.27 41.60L83.08 41.41Q82.77 41.49 82.42 41.67L82.48 41.73L82.44 41.69Q82.02 40.42 81.87 39.11L81.95 39.18L81.92 39.16Q81.34 39.07 80.21 39.60L80.31 39.70L80.37 39.76Q79.17 40.22 78.71 40.22L78.76 40.27L78.60 40.11Q78.27 40.24 77.70 40.21L77.62 40.12L77.65 40.15Q76.89 40.21 75.05 39.68L74.92 39.55L75.10 39.72Q74.63 38.90 74.66 37.84L74.77 37.94L74.73 37.91Q75.00 34.07 79.59 33.58L79.42 33.40L79.59 33.57Q80.22 33.57 81.49 33.88L81.36 33.75L81.42 33.81Q82.68 34.12 83.35 34.08L83.38 34.11L83.29 34.02Q83.75 32.01 84.38 30.31L84.28 30.20L84.47 30.39Q83.93 30.10 83.57 30.03L83.63 30.09L82.88 29.97L83.03 30.12Q83.10 29.56 83.46 28.74L83.49 28.78L83.51 28.79Q82.02 28.41 80.57 28.41L80.63 28.46L80.61 28.44Q78.33 28.39 74.26 28.74L74.31 28.79L74.30 28.78Q72.59 29.01 71.14 29.93L71.03 29.82L71.05 29.84Q69.88 30.68 69.88 32.45L69.99 32.56L69.96 32.53Q70.02 32.99 70.13 34.12L70.10 34.09L70.16 34.14Q70.05 34.92 70.01 35.49L70.10 35.58L70.05 35.52Q70.18 37.38 70.85 40.85L70.77 40.77L70.85 40.85Q71.24 41.66 71.70 42.12L71.60 42.02L71.77 42.19L71.96 42.35L71.76 42.15Q72.88 44.30 76.52 44.86L76.44 44.78L76.48 44.82Q79.48 45.34 81.96 45.59L81.88 45.52L81.92 45.56Q82.53 45.50 83.38 45.74Z"/>
|
||||
<path fill="#7cdead" d="M17.97 48.55L18.01 48.59L17.83 48.41Q17.00 48.64 15.23 48.92L15.24 48.93L15.21 48.91Q16.47 46.95 17.43 45.75L17.49 45.81L17.33 45.65Q19.06 46.03 19.20 46.03L19.15 45.98L19.17 46.00Q19.54 46.16 20.03 46.05L19.97 45.99L20.05 46.07Q20.64 45.96 20.82 45.96L20.76 45.90L20.78 45.92Q23.19 45.71 23.12 43.70L23.16 43.74L23.13 43.71Q23.01 42.25 22.91 36.45L22.85 36.39L22.94 36.49Q22.88 33.17 22.95 29.32L22.90 29.27L24.37 29.25L24.33 29.22Q24.95 29.12 25.58 28.91L25.53 28.86L25.62 28.94Q25.39 33.03 25.43 37.10L25.35 37.02L25.43 37.10Q25.28 40.98 25.78 45.05L25.79 45.06L25.79 45.06Q26.01 45.49 26.01 45.74L25.87 45.60L25.86 45.59Q25.90 47.15 24.34 47.50L24.40 47.56L24.37 47.53Q21.46 47.87 17.89 48.47ZM24.12 25.93L22.21 23.10L22.19 23.08Q23.13 21.76 24.83 19.18L24.85 19.20L24.88 19.23Q25.53 20.09 26.77 21.79L26.75 21.77L24.15 25.96ZM27.08 50.80L26.95 50.68L26.96 50.68Q27.46 50.73 28.17 50.41L28.15 50.39L28.18 50.42Q28.46 49.75 28.46 49.32L28.59 49.45L28.59 49.45Q28.41 48.96 28.31 48.39L28.47 48.56L28.37 48.45Q27.12 42.50 27.12 35.15L27.05 35.08L27.16 35.19Q27.07 32.45 27.21 29.76L27.24 29.79L27.28 29.84Q26.77 29.99 25.71 30.27L25.73 30.30L25.73 30.29Q25.76 29.86 25.79 29.37L25.86 29.43L25.83 28.31L25.82 28.30Q24.35 29.02 22.69 29.06L22.70 29.07L22.63 28.99Q22.57 33.01 22.61 36.33L22.62 36.34L22.78 43.68L22.71 43.60Q22.86 45.53 20.85 45.63L20.70 45.49L20.85 45.64Q20.51 45.51 20.03 45.56L20.17 45.70L20.05 45.58Q19.71 45.77 19.46 45.77L19.46 45.77L19.38 45.69Q18.85 45.58 17.40 45.19L17.54 45.33L17.51 45.30Q17.01 45.79 15.98 47.34L15.99 47.35L14.58 49.51L14.70 49.63Q15.40 49.34 16.92 48.99L16.92 48.98L16.46 49.62L16.09 50.34L16.08 50.34Q18.06 50.06 19.30 50.13L19.30 50.13L19.30 50.13Q21.39 50.25 26.91 50.63ZM24.23 26.75L24.11 26.62L24.49 26.19L24.52 26.22Q24.84 26.61 25.72 28.10L25.74 28.11L25.72 28.09Q27.15 25.21 28.60 22.35L28.60 22.34L28.51 22.26Q27.74 20.92 26.85 19.72L26.85 19.72L26.77 19.63Q26.56 19.81 26.24 20.30L26.38 20.45L26.35 20.42Q25.98 19.84 25.06 18.63L24.95 18.52L25.08 18.65Q23.90 20.06 21.75 23.17L21.83 23.25L21.82 23.24Q22.53 24.23 24.12 26.63Z"/>
|
||||
</svg>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user