forked from Yara724/api
Merge pull request 'YARA-913' (#81) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#81
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",
|
"short-unique-id": "^5.2.0",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
"standardjs": "^1.0.0-alpha",
|
"standardjs": "^1.0.0-alpha",
|
||||||
|
"svg-captcha": "^1.4.0",
|
||||||
"uuid": "^11.0.3",
|
"uuid": "^11.0.3",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
@@ -14289,6 +14290,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.3",
|
"version": "0.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
||||||
@@ -16894,6 +16907,18 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/swagger-ui-dist": {
|
||||||
"version": "5.17.14",
|
"version": "5.17.14",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
|
"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": {
|
"node_modules/tlds": {
|
||||||
"version": "1.255.0",
|
"version": "1.255.0",
|
||||||
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.255.0.tgz",
|
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.255.0.tgz",
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
"short-unique-id": "^5.2.0",
|
"short-unique-id": "^5.2.0",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
"standardjs": "^1.0.0-alpha",
|
"standardjs": "^1.0.0-alpha",
|
||||||
|
"svg-captcha": "^1.4.0",
|
||||||
"uuid": "^11.0.3",
|
"uuid": "^11.0.3",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -77,14 +77,14 @@ dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
|
|||||||
provide: APP_INTERCEPTOR,
|
provide: APP_INTERCEPTOR,
|
||||||
useClass: UnicodeDigitsNormalizeInterceptor,
|
useClass: UnicodeDigitsNormalizeInterceptor,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
provide: APP_PIPE,
|
// provide: APP_PIPE,
|
||||||
useValue: new ValidationPipe({
|
// useValue: new ValidationPipe({
|
||||||
transform: true,
|
// transform: true,
|
||||||
whitelist: true,
|
// whitelist: true,
|
||||||
forbidNonWhitelisted: false,
|
// forbidNonWhitelisted: false,
|
||||||
}),
|
// }),
|
||||||
},
|
// },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import {
|
|||||||
Param,
|
Param,
|
||||||
Patch,
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
|
Query,
|
||||||
Req,
|
Req,
|
||||||
|
Res,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
|
import type { Response } from "express";
|
||||||
import {
|
import {
|
||||||
ApiBody,
|
ApiBody,
|
||||||
ApiAcceptedResponse,
|
ApiAcceptedResponse,
|
||||||
@@ -17,6 +20,9 @@ import {
|
|||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { ActorAuthService } from "src/auth/auth-services/actor.auth.service";
|
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 {
|
import {
|
||||||
ForgetPasswordSendCodeDto,
|
ForgetPasswordSendCodeDto,
|
||||||
ForgetPasswordVerifyCodeDto,
|
ForgetPasswordVerifyCodeDto,
|
||||||
@@ -37,7 +43,42 @@ import { CurrentUser } from "src/decorators/user.decorator";
|
|||||||
@Controller("actor")
|
@Controller("actor")
|
||||||
@ApiTags("actor")
|
@ApiTags("actor")
|
||||||
export class ActorAuthController {
|
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
|
* @deprecated Use the unified actor onboarding flow instead. This endpoint
|
||||||
@@ -113,6 +154,7 @@ export class ActorAuthController {
|
|||||||
role: "company",
|
role: "company",
|
||||||
username: "saman_insurer@gmail.com",
|
username: "saman_insurer@gmail.com",
|
||||||
password: "123321",
|
password: "123321",
|
||||||
|
captcha: "a7bx2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expert: {
|
expert: {
|
||||||
@@ -122,6 +164,7 @@ export class ActorAuthController {
|
|||||||
role: "expert",
|
role: "expert",
|
||||||
username: "blame@gmail.com",
|
username: "blame@gmail.com",
|
||||||
password: "123321",
|
password: "123321",
|
||||||
|
captcha: "a7bx2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
damage_expert: {
|
damage_expert: {
|
||||||
@@ -131,6 +174,7 @@ export class ActorAuthController {
|
|||||||
role: "damage_expert",
|
role: "damage_expert",
|
||||||
username: "claim@gmail.com",
|
username: "claim@gmail.com",
|
||||||
password: "123321",
|
password: "123321",
|
||||||
|
captcha: "a7bx2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
RegisterDtoRs,
|
RegisterDtoRs,
|
||||||
} from "src/auth/dto/actor/register.actor.dto";
|
} from "src/auth/dto/actor/register.actor.dto";
|
||||||
import { StateListDtoRs } from "src/auth/dto/actor/states.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 { ClientDbService } from "src/client/entities/db-service/client.db.service";
|
||||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
import { UserType } from "src/Types&Enums/userType.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 mailService: MailService, // Mailer disabled – not used
|
||||||
private readonly clientDbService: ClientDbService,
|
private readonly clientDbService: ClientDbService,
|
||||||
private readonly otpService: OtpGeneratorService,
|
private readonly otpService: OtpGeneratorService,
|
||||||
|
private readonly captchaAccountService: CaptchaAccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// TODO convrt to class for dynamic controller
|
// TODO convrt to class for dynamic controller
|
||||||
@@ -176,6 +178,13 @@ export class ActorAuthService {
|
|||||||
if (typeof password !== "string" || !password) {
|
if (typeof password !== "string" || !password) {
|
||||||
throw new BadRequestException("password is required");
|
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);
|
const actor = await this.validateActor(username, password, role);
|
||||||
return this.issueActorTokens(actor);
|
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 { ActorAuthService } from "src/auth/auth-services/actor.auth.service";
|
||||||
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
|
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
|
||||||
import { UserLinkAccessService } from "src/auth/auth-services/user-link-access.service";
|
import { UserLinkAccessService } from "src/auth/auth-services/user-link-access.service";
|
||||||
|
import { CaptchaAccountService } from "src/auth/auth-services/captcha-account.service";
|
||||||
import {
|
import {
|
||||||
ClaimRequestManagementModel,
|
ClaimRequestManagementModel,
|
||||||
ClaimRequestManagementSchema,
|
ClaimRequestManagementSchema,
|
||||||
@@ -26,6 +27,7 @@ import { UsersModule } from "src/users/users.module";
|
|||||||
import { HashModule } from "src/utils/hash/hash.module";
|
import { HashModule } from "src/utils/hash/hash.module";
|
||||||
// import { MailModule } from "src/utils/mail/mail.module";
|
// import { MailModule } from "src/utils/mail/mail.module";
|
||||||
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
|
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
|
||||||
|
import { CaptchaModule } from "src/captcha/captcha.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -33,6 +35,7 @@ import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.
|
|||||||
UsersModule,
|
UsersModule,
|
||||||
ClientModule,
|
ClientModule,
|
||||||
HashModule,
|
HashModule,
|
||||||
|
CaptchaModule,
|
||||||
PassportModule,
|
PassportModule,
|
||||||
SmsOrchestrationModule,
|
SmsOrchestrationModule,
|
||||||
MongooseModule.forFeature([
|
MongooseModule.forFeature([
|
||||||
@@ -52,6 +55,7 @@ import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.
|
|||||||
providers: [
|
providers: [
|
||||||
UserAuthService,
|
UserAuthService,
|
||||||
UserLinkAccessService,
|
UserLinkAccessService,
|
||||||
|
CaptchaAccountService,
|
||||||
ActorAuthService,
|
ActorAuthService,
|
||||||
LocalStrategy,
|
LocalStrategy,
|
||||||
LocalActorStrategy,
|
LocalActorStrategy,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsNotEmpty, IsString, MaxLength } from "class-validator";
|
||||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
|
||||||
export class LoginActorDto {
|
export class LoginActorDto {
|
||||||
@@ -10,6 +11,15 @@ export class LoginActorDto {
|
|||||||
|
|
||||||
@ApiProperty({})
|
@ApiProperty({})
|
||||||
password: string;
|
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 {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ export interface MediaLimits {
|
|||||||
* receive. Per-client `maxBytes` cannot legitimately go above its route's
|
* receive. Per-client `maxBytes` cannot legitimately go above its route's
|
||||||
* multer ceiling, so the defaults below are kept at-or-below those.
|
* 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_MAX_BYTES = 100 * 1024 * 1024; // 100MB
|
||||||
|
|
||||||
export const DEFAULT_MEDIA_LIMITS: Record<MediaKind, MediaLimits> = {
|
export const DEFAULT_MEDIA_LIMITS: Record<MediaKind, MediaLimits> = {
|
||||||
|
|||||||
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" })
|
@Prop({ type: "string" })
|
||||||
otp: string;
|
otp: string;
|
||||||
|
|
||||||
|
@Prop({ default: null })
|
||||||
|
captcha?: string | null;
|
||||||
|
|
||||||
|
@Prop({ default: 0 })
|
||||||
|
captchaExpire?: number;
|
||||||
|
|
||||||
@Prop({ type: "string" })
|
@Prop({ type: "string" })
|
||||||
address: string;
|
address: string;
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ export class ExpertModel {
|
|||||||
@Prop({ type: "string" })
|
@Prop({ type: "string" })
|
||||||
otp: string;
|
otp: string;
|
||||||
|
|
||||||
|
@Prop({ default: null })
|
||||||
|
captcha?: string | null;
|
||||||
|
|
||||||
|
@Prop({ default: 0 })
|
||||||
|
captchaExpire?: number;
|
||||||
|
|
||||||
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ export class FieldExpertModel {
|
|||||||
@Prop({ type: "string", default: "" })
|
@Prop({ type: "string", default: "" })
|
||||||
otp: string;
|
otp: string;
|
||||||
|
|
||||||
|
@Prop({ default: null })
|
||||||
|
captcha?: string | null;
|
||||||
|
|
||||||
|
@Prop({ default: 0 })
|
||||||
|
captchaExpire?: number;
|
||||||
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ export class InsurerExpertModel {
|
|||||||
@Prop()
|
@Prop()
|
||||||
address?: string;
|
address?: string;
|
||||||
|
|
||||||
|
@Prop({ default: null })
|
||||||
|
captcha?: string | null;
|
||||||
|
|
||||||
|
@Prop({ default: 0 })
|
||||||
|
captchaExpire?: number;
|
||||||
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
export const InsurerExpertDbSchema =
|
export const InsurerExpertDbSchema =
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ export class RegistrarModel {
|
|||||||
|
|
||||||
@Prop({ type: "string", default: "" })
|
@Prop({ type: "string", default: "" })
|
||||||
otp: string;
|
otp: string;
|
||||||
|
|
||||||
|
@Prop({ default: null })
|
||||||
|
captcha?: string | null;
|
||||||
|
|
||||||
|
@Prop({ default: 0 })
|
||||||
|
captchaExpire?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RegistrarDbSchema = SchemaFactory.createForClass(RegistrarModel);
|
export const RegistrarDbSchema = SchemaFactory.createForClass(RegistrarModel);
|
||||||
|
|||||||
Reference in New Issue
Block a user