forked from Yara724/api
Merge pull request 'Fix 404 for invalid email of actors, added APIs for client settings' (#70) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#70
This commit is contained in:
@@ -31,7 +31,6 @@ import {
|
|||||||
LegalRegisterDto,
|
LegalRegisterDto,
|
||||||
} from "src/auth/dto/actor/register.actor.dto";
|
} from "src/auth/dto/actor/register.actor.dto";
|
||||||
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
import { ClientKey } from "src/decorators/clientKey.decorator";
|
|
||||||
import { Roles } from "src/decorators/roles.decorator";
|
import { Roles } from "src/decorators/roles.decorator";
|
||||||
import { CurrentUser } from "src/decorators/user.decorator";
|
import { CurrentUser } from "src/decorators/user.decorator";
|
||||||
|
|
||||||
@@ -136,9 +135,17 @@ export class ActorAuthController {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 404,
|
||||||
|
description: "No actor account exists for the given email and role",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 401,
|
||||||
|
description: "Wrong password or role mismatch",
|
||||||
|
})
|
||||||
@ApiAcceptedResponse()
|
@ApiAcceptedResponse()
|
||||||
async login(@Body() body, @Req() req, @ClientKey() client) {
|
async login(@Req() req: { user: Record<string, unknown> }) {
|
||||||
return await this.actorAuthService.loginActors(req.user);
|
return req.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("forget-password")
|
@Post("forget-password")
|
||||||
|
|||||||
@@ -97,48 +97,95 @@ export class ActorAuthService {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateActor(username: string, pass: string, role): Promise<any> {
|
/** Normalizes `role` from login body (string or single-element array). */
|
||||||
const user = await this.dynamicDbController(role, username);
|
parseActorLoginRole(role: unknown): RoleEnum {
|
||||||
if (user) {
|
const raw = Array.isArray(role) ? role[0] : role;
|
||||||
if (user.role !== role) {
|
if (
|
||||||
throw new UnauthorizedException("user not assigned to this role");
|
typeof raw !== "string" ||
|
||||||
}
|
!Object.values(RoleEnum).includes(raw as RoleEnum)
|
||||||
if (!(await this.hashService.compare(pass, user.password))) {
|
) {
|
||||||
throw new UnauthorizedException(
|
throw new BadRequestException(
|
||||||
"password is incorrect or access Denied",
|
`Invalid role. Expected one of: ${Object.values(RoleEnum).join(", ")}`,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return raw as RoleEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loginActors(user: any) {
|
parseActorLoginUsername(body: Record<string, unknown>): string {
|
||||||
let foundedUser = await this.dynamicDbController(user.role, user.username);
|
const username = body?.username ?? body?.email;
|
||||||
if (foundedUser) {
|
if (typeof username !== "string" || !username.trim()) {
|
||||||
const payload = {
|
throw new BadRequestException("username (email) is required");
|
||||||
username: foundedUser.username || foundedUser.email,
|
|
||||||
sub: foundedUser._id,
|
|
||||||
fullName:
|
|
||||||
`${foundedUser.firstName || ""} ${foundedUser.lastName || ""}`.trim(),
|
|
||||||
role: foundedUser.role || "User",
|
|
||||||
userType: foundedUser.userType || "UserType",
|
|
||||||
clientKey: foundedUser.clientKey || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const accToken = this.jwtService.sign(payload, {
|
|
||||||
secret: `${process.env.SECRET}`,
|
|
||||||
expiresIn: "1h",
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...payload,
|
|
||||||
access_token: accToken,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw new UnauthorizedException("expert or damage_expert not found");
|
|
||||||
}
|
}
|
||||||
|
return username.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
issueActorTokens(actor: {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
username?: string;
|
||||||
|
email?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
role?: string;
|
||||||
|
userType?: string;
|
||||||
|
clientKey?: Types.ObjectId | string | null;
|
||||||
|
}) {
|
||||||
|
const payload = {
|
||||||
|
username: actor.username || actor.email,
|
||||||
|
sub: actor._id,
|
||||||
|
fullName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(),
|
||||||
|
role: actor.role || "User",
|
||||||
|
userType: actor.userType || "UserType",
|
||||||
|
clientKey: actor.clientKey ?? null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const access_token = this.jwtService.sign(payload, {
|
||||||
|
secret: `${process.env.SECRET}`,
|
||||||
|
expiresIn: "1h",
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...payload, access_token };
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateActor(
|
||||||
|
username: string,
|
||||||
|
pass: string,
|
||||||
|
role: RoleEnum,
|
||||||
|
): Promise<any> {
|
||||||
|
const user = await this.dynamicDbController(role, username);
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException("Actor account not found");
|
||||||
|
}
|
||||||
|
if (user.role !== role) {
|
||||||
|
throw new UnauthorizedException("user not assigned to this role");
|
||||||
|
}
|
||||||
|
if (!(await this.hashService.compare(pass, user.password))) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
"password is incorrect or access Denied",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticates an actor from a login request body (role, username/email, password).
|
||||||
|
*/
|
||||||
|
async loginFromCredentials(body: Record<string, unknown>) {
|
||||||
|
const role = this.parseActorLoginRole(body?.role);
|
||||||
|
const username = this.parseActorLoginUsername(body);
|
||||||
|
const password = body?.password;
|
||||||
|
if (typeof password !== "string" || !password) {
|
||||||
|
throw new BadRequestException("password is required");
|
||||||
|
}
|
||||||
|
const actor = await this.validateActor(username, password, role);
|
||||||
|
return this.issueActorTokens(actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Prefer {@link loginFromCredentials}. Kept for internal callers. */
|
||||||
|
async loginActors(user: any) {
|
||||||
|
if (user?.access_token && user?.sub) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
return this.loginFromCredentials(user as Record<string, unknown>);
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerActors(
|
async registerActors(
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ export class LocalActorAuthGuard extends AuthGuard("actor") {
|
|||||||
const path = request.url;
|
const path = request.url;
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
if (path === "/actor/login") {
|
if (this.isActorLoginRequest(request)) {
|
||||||
const loginData = await this.actorAuthService.loginActors(request.body);
|
const loginData = await this.actorAuthService.loginFromCredentials(
|
||||||
|
request.body ?? {},
|
||||||
|
);
|
||||||
request.user = loginData;
|
request.user = loginData;
|
||||||
request.identity = request;
|
request.identity = loginData;
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new UnauthorizedException("Token not found");
|
throw new UnauthorizedException("Token not found");
|
||||||
@@ -59,6 +61,17 @@ export class LocalActorAuthGuard extends AuthGuard("actor") {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isActorLoginRequest(request: {
|
||||||
|
url?: string;
|
||||||
|
path?: string;
|
||||||
|
route?: { path?: string };
|
||||||
|
}): boolean {
|
||||||
|
const path = (request.route?.path ?? request.url ?? request.path ?? "")
|
||||||
|
.split("?")[0]
|
||||||
|
.replace(/\/+$/, "");
|
||||||
|
return path === "/actor/login" || path.endsWith("/actor/login");
|
||||||
|
}
|
||||||
|
|
||||||
private extractTokenFromHeader(request: Request): string | undefined {
|
private extractTokenFromHeader(request: Request): string | undefined {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const [type, token] = request.headers.authorization?.split(" ") ?? [];
|
const [type, token] = request.headers.authorization?.split(" ") ?? [];
|
||||||
|
|||||||
71
src/client/client-panel.controller.ts
Normal file
71
src/client/client-panel.controller.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Patch,
|
||||||
|
UnauthorizedException,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiOperation,
|
||||||
|
ApiResponse,
|
||||||
|
ApiTags,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
|
import { Roles } from "src/decorators/roles.decorator";
|
||||||
|
import { CurrentUser } from "src/decorators/user.decorator";
|
||||||
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
import { ClientService } from "./client.service";
|
||||||
|
import {
|
||||||
|
ClientSettingsPanelResponseDto,
|
||||||
|
UpdateClientSettingsDto,
|
||||||
|
} from "./dto/client-settings.dto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insurer (company) panel: per-tenant upload limits and CAR_BODY accident window.
|
||||||
|
* Scoped to the JWT `clientKey` — tenants cannot edit other clients.
|
||||||
|
*/
|
||||||
|
@Controller("client-panel")
|
||||||
|
@ApiTags("insurer-client-panel")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
|
@Roles(RoleEnum.COMPANY)
|
||||||
|
export class ClientPanelController {
|
||||||
|
constructor(private readonly clientService: ClientService) {}
|
||||||
|
|
||||||
|
@Get("settings")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get tenant settings (media limits & CAR_BODY accident window)",
|
||||||
|
description:
|
||||||
|
"Returns configured overrides, effective values (with system defaults), and route upload ceilings for the authenticated insurer's client.",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, type: ClientSettingsPanelResponseDto })
|
||||||
|
async getSettings(
|
||||||
|
@CurrentUser() insurer: { clientKey?: string },
|
||||||
|
): Promise<ClientSettingsPanelResponseDto> {
|
||||||
|
if (!insurer?.clientKey) {
|
||||||
|
throw new UnauthorizedException("Insurer client key is missing.");
|
||||||
|
}
|
||||||
|
return this.clientService.getPanelSettings(insurer.clientKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("settings")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Update tenant settings",
|
||||||
|
description:
|
||||||
|
"Partial update of `settings.carBodyAccidentMaxAgeDays` and/or `settings.media` (video, image, voice). " +
|
||||||
|
"Only fields sent in the body are changed. `maxBytes` cannot exceed the route ceiling returned by GET.",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, type: ClientSettingsPanelResponseDto })
|
||||||
|
async updateSettings(
|
||||||
|
@CurrentUser() insurer: { clientKey?: string },
|
||||||
|
@Body() body: UpdateClientSettingsDto,
|
||||||
|
): Promise<ClientSettingsPanelResponseDto> {
|
||||||
|
if (!insurer?.clientKey) {
|
||||||
|
throw new UnauthorizedException("Insurer client key is missing.");
|
||||||
|
}
|
||||||
|
return this.clientService.updatePanelSettings(insurer.clientKey, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { MongooseModule } from "@nestjs/mongoose";
|
import { MongooseModule } from "@nestjs/mongoose";
|
||||||
import { ClientController } from "./client.controller";
|
import { ClientController } from "./client.controller";
|
||||||
|
import { ClientPanelController } from "./client-panel.controller";
|
||||||
import { ClientService } from "./client.service";
|
import { ClientService } from "./client.service";
|
||||||
import { BranchDbService } from "./entities/db-service/branch.db.service";
|
import { BranchDbService } from "./entities/db-service/branch.db.service";
|
||||||
import { ClientDbService } from "./entities/db-service/client.db.service";
|
import { ClientDbService } from "./entities/db-service/client.db.service";
|
||||||
@@ -17,7 +18,7 @@ import { ClientDbSchema, ClientModel } from "./entities/schema/client.schema";
|
|||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
controllers: [ClientController],
|
controllers: [ClientController, ClientPanelController],
|
||||||
providers: [ClientService, ClientDbService, BranchDbService],
|
providers: [ClientService, ClientDbService, BranchDbService],
|
||||||
exports: [ClientService, ClientDbService, BranchDbService],
|
exports: [ClientService, ClientDbService, BranchDbService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import { BadGatewayException, GoneException, Injectable } from "@nestjs/common";
|
import {
|
||||||
|
BadGatewayException,
|
||||||
|
BadRequestException,
|
||||||
|
GoneException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from "@nestjs/common";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
ClientDto,
|
ClientDto,
|
||||||
ClientDtoRs,
|
ClientDtoRs,
|
||||||
ClientLists,
|
ClientLists,
|
||||||
} from "src/client/dto/create-client.dto";
|
} from "src/client/dto/create-client.dto";
|
||||||
|
import {
|
||||||
|
type ClientSettingsPanelResponseDto,
|
||||||
|
UpdateClientSettingsDto,
|
||||||
|
} from "src/client/dto/client-settings.dto";
|
||||||
|
import type { ClientMediaLimits } from "./entities/schema/client.schema";
|
||||||
import { ClientDbService } from "./entities/db-service/client.db.service";
|
import { ClientDbService } from "./entities/db-service/client.db.service";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,6 +53,18 @@ export const DEFAULT_MEDIA_LIMITS: Record<MediaKind, MediaLimits> = {
|
|||||||
voice: { minBytes: 5 * 1024, maxBytes: 8 * 1024 * 1024 },
|
voice: { minBytes: 5 * 1024, maxBytes: 8 * 1024 * 1024 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Highest multer `fileSize` used on any route for each kind (policy cannot exceed this). */
|
||||||
|
export const MEDIA_ROUTE_MAX_BYTES: Record<MediaKind, number> = {
|
||||||
|
video: 50 * 1024 * 1024,
|
||||||
|
image: 10 * 1024 * 1024,
|
||||||
|
voice: 10 * 1024 * 1024,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CAR_BODY_ACCIDENT_MAX_AGE_DAYS_MIN = 1;
|
||||||
|
export const CAR_BODY_ACCIDENT_MAX_AGE_DAYS_MAX = 365;
|
||||||
|
|
||||||
|
const MEDIA_KINDS: MediaKind[] = ["video", "image", "voice"];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ClientService {
|
export class ClientService {
|
||||||
constructor(private readonly clientDbService: ClientDbService) {}
|
constructor(private readonly clientDbService: ClientDbService) {}
|
||||||
@@ -155,4 +178,138 @@ export class ClientService {
|
|||||||
}
|
}
|
||||||
return DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS;
|
return DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveClientObjectId(clientKey: string | Types.ObjectId): Types.ObjectId {
|
||||||
|
const raw = String(clientKey ?? "").trim();
|
||||||
|
if (!Types.ObjectId.isValid(raw)) {
|
||||||
|
throw new BadRequestException("Invalid client key");
|
||||||
|
}
|
||||||
|
return new Types.ObjectId(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadClientOrThrow(clientKey: string | Types.ObjectId) {
|
||||||
|
const id = this.resolveClientObjectId(clientKey);
|
||||||
|
const client = await this.clientDbService.findOne({ _id: id });
|
||||||
|
if (!client) {
|
||||||
|
throw new NotFoundException("Client not found");
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private configuredMediaLimits(
|
||||||
|
stored: ClientMediaLimits | undefined,
|
||||||
|
): { minBytes?: number; maxBytes?: number } | null {
|
||||||
|
if (!stored) return null;
|
||||||
|
const hasMin = typeof stored.minBytes === "number";
|
||||||
|
const hasMax = typeof stored.maxBytes === "number";
|
||||||
|
if (!hasMin && !hasMax) return null;
|
||||||
|
return {
|
||||||
|
...(hasMin ? { minBytes: stored.minBytes } : {}),
|
||||||
|
...(hasMax ? { maxBytes: stored.maxBytes } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateMediaLimitsPatch(
|
||||||
|
kind: MediaKind,
|
||||||
|
patch: { minBytes?: number; maxBytes?: number },
|
||||||
|
): { minBytes?: number; maxBytes?: number } {
|
||||||
|
const defaults = DEFAULT_MEDIA_LIMITS[kind];
|
||||||
|
const routeMax = MEDIA_ROUTE_MAX_BYTES[kind];
|
||||||
|
const minBytes =
|
||||||
|
patch.minBytes !== undefined ? patch.minBytes : defaults.minBytes;
|
||||||
|
const maxBytes =
|
||||||
|
patch.maxBytes !== undefined ? patch.maxBytes : defaults.maxBytes;
|
||||||
|
|
||||||
|
if (maxBytes > routeMax) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`${kind} maxBytes (${maxBytes}) cannot exceed the route upload limit of ${routeMax} bytes.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (minBytes > maxBytes) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`${kind} minBytes (${minBytes}) cannot be greater than maxBytes (${maxBytes}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const out: { minBytes?: number; maxBytes?: number } = {};
|
||||||
|
if (patch.minBytes !== undefined) out.minBytes = patch.minBytes;
|
||||||
|
if (patch.maxBytes !== undefined) out.maxBytes = patch.maxBytes;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPanelSettings(
|
||||||
|
clientKey: string | Types.ObjectId,
|
||||||
|
): Promise<ClientSettingsPanelResponseDto> {
|
||||||
|
const client = await this.loadClientOrThrow(clientKey);
|
||||||
|
const clientId = client._id.toString();
|
||||||
|
|
||||||
|
const configuredDays = client.settings?.carBodyAccidentMaxAgeDays;
|
||||||
|
const effectiveDays = await this.getCarBodyAccidentMaxAgeDays(client._id);
|
||||||
|
|
||||||
|
const media = {} as ClientSettingsPanelResponseDto["media"];
|
||||||
|
for (const kind of MEDIA_KINDS) {
|
||||||
|
const effective = await this.getMediaLimits(client._id, kind);
|
||||||
|
media[kind] = {
|
||||||
|
configured: this.configuredMediaLimits(client.settings?.media?.[kind]),
|
||||||
|
effective,
|
||||||
|
systemDefault: { ...DEFAULT_MEDIA_LIMITS[kind] },
|
||||||
|
routeMaxBytes: MEDIA_ROUTE_MAX_BYTES[kind],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
clientId,
|
||||||
|
carBodyAccidentMaxAgeDays: {
|
||||||
|
configured:
|
||||||
|
typeof configuredDays === "number" && Number.isFinite(configuredDays)
|
||||||
|
? configuredDays
|
||||||
|
: null,
|
||||||
|
effective: effectiveDays,
|
||||||
|
systemDefault: DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS,
|
||||||
|
minDays: CAR_BODY_ACCIDENT_MAX_AGE_DAYS_MIN,
|
||||||
|
maxDays: CAR_BODY_ACCIDENT_MAX_AGE_DAYS_MAX,
|
||||||
|
},
|
||||||
|
media,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePanelSettings(
|
||||||
|
clientKey: string | Types.ObjectId,
|
||||||
|
body: UpdateClientSettingsDto,
|
||||||
|
): Promise<ClientSettingsPanelResponseDto> {
|
||||||
|
const client = await this.loadClientOrThrow(clientKey);
|
||||||
|
const $set: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (body.carBodyAccidentMaxAgeDays !== undefined) {
|
||||||
|
$set["settings.carBodyAccidentMaxAgeDays"] = body.carBodyAccidentMaxAgeDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.media) {
|
||||||
|
for (const kind of MEDIA_KINDS) {
|
||||||
|
const patch = body.media[kind];
|
||||||
|
if (!patch) continue;
|
||||||
|
const validated = this.validateMediaLimitsPatch(kind, patch);
|
||||||
|
if (validated.minBytes !== undefined) {
|
||||||
|
$set[`settings.media.${kind}.minBytes`] = validated.minBytes;
|
||||||
|
}
|
||||||
|
if (validated.maxBytes !== undefined) {
|
||||||
|
$set[`settings.media.${kind}.maxBytes`] = validated.maxBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys($set).length === 0) {
|
||||||
|
throw new BadRequestException("No settings fields to update.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await this.clientDbService.findByIdAndUpdate(
|
||||||
|
client._id.toString(),
|
||||||
|
{ $set },
|
||||||
|
);
|
||||||
|
if (!updated) {
|
||||||
|
throw new NotFoundException("Client not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getPanelSettings(client._id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
131
src/client/dto/client-settings.dto.ts
Normal file
131
src/client/dto/client-settings.dto.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
Max,
|
||||||
|
Min,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator";
|
||||||
|
import type { MediaKind } from "../client.service";
|
||||||
|
|
||||||
|
export class ClientMediaLimitsDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Minimum upload size in bytes (inclusive). Omit to keep default.",
|
||||||
|
example: 5120,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
minBytes?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Maximum upload size in bytes (inclusive). Cannot exceed route ceiling.",
|
||||||
|
example: 8388608,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
maxBytes?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClientMediaSettingsDto {
|
||||||
|
@ApiPropertyOptional({ type: ClientMediaLimitsDto })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => ClientMediaLimitsDto)
|
||||||
|
video?: ClientMediaLimitsDto;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: ClientMediaLimitsDto })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => ClientMediaLimitsDto)
|
||||||
|
image?: ClientMediaLimitsDto;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: ClientMediaLimitsDto })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => ClientMediaLimitsDto)
|
||||||
|
voice?: ClientMediaLimitsDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateClientSettingsDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
"Max days between CAR_BODY accident date and submission. Omit to leave unchanged.",
|
||||||
|
example: 7,
|
||||||
|
minimum: 1,
|
||||||
|
maximum: 365,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(365)
|
||||||
|
carBodyAccidentMaxAgeDays?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: ClientMediaSettingsDto })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => ClientMediaSettingsDto)
|
||||||
|
media?: ClientMediaSettingsDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MediaLimitsResolvedDto {
|
||||||
|
@ApiProperty({ example: 5120 })
|
||||||
|
minBytes: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 8388608 })
|
||||||
|
maxBytes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MediaKindSettingsViewDto {
|
||||||
|
@ApiPropertyOptional({ type: ClientMediaLimitsDto })
|
||||||
|
configured: { minBytes?: number; maxBytes?: number } | null;
|
||||||
|
|
||||||
|
@ApiProperty({ type: MediaLimitsResolvedDto })
|
||||||
|
effective: MediaLimitsResolvedDto;
|
||||||
|
|
||||||
|
@ApiProperty({ type: MediaLimitsResolvedDto })
|
||||||
|
systemDefault: MediaLimitsResolvedDto;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: "Hard multer ceiling for this kind on upload routes (bytes)",
|
||||||
|
example: 10485760,
|
||||||
|
})
|
||||||
|
routeMaxBytes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CarBodyAccidentWindowViewDto {
|
||||||
|
@ApiPropertyOptional({ example: 14, nullable: true })
|
||||||
|
configured: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 14 })
|
||||||
|
effective: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 7 })
|
||||||
|
systemDefault: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 1 })
|
||||||
|
minDays: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 365 })
|
||||||
|
maxDays: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClientSettingsPanelResponseDto {
|
||||||
|
@ApiProperty({ example: "507f1f77bcf86cd799439011" })
|
||||||
|
clientId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: CarBodyAccidentWindowViewDto })
|
||||||
|
carBodyAccidentMaxAgeDays: CarBodyAccidentWindowViewDto;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: "Per-kind upload bounds (video / image / voice)",
|
||||||
|
example: {
|
||||||
|
video: {},
|
||||||
|
image: {},
|
||||||
|
voice: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
media: Record<MediaKind, MediaKindSettingsViewDto>;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { InjectModel } from "@nestjs/mongoose";
|
import { InjectModel } from "@nestjs/mongoose";
|
||||||
import { FilterQuery, Model } from "mongoose";
|
import { FilterQuery, Model, UpdateQuery } from "mongoose";
|
||||||
import { ClientModel, ClientDocument } from "../schema/client.schema";
|
import { ClientModel, ClientDocument } from "../schema/client.schema";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -25,4 +25,14 @@ export class ClientDbService {
|
|||||||
async findAll(): Promise<ClientModel[]> {
|
async findAll(): Promise<ClientModel[]> {
|
||||||
return await this.clientModel.find();
|
return await this.clientModel.find();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findByIdAndUpdate(
|
||||||
|
id: string,
|
||||||
|
update: UpdateQuery<ClientModel>,
|
||||||
|
): Promise<ClientModel | null> {
|
||||||
|
return this.clientModel
|
||||||
|
.findByIdAndUpdate(id, update, { new: true })
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user