forked from Yara724/api
YARA-898
This commit is contained in:
@@ -46,6 +46,7 @@ import { PublicIdModule } from "src/utils/public-id/public-id.module";
|
||||
import { ClientModule } from "src/client/client.module";
|
||||
import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { MediaPolicyModule } from "src/media-policy/media-policy.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -55,6 +56,7 @@ import { JwtModule } from "@nestjs/jwt";
|
||||
AiModule,
|
||||
SandHubModule,
|
||||
ClientModule,
|
||||
MediaPolicyModule,
|
||||
JwtModule.register({}),
|
||||
MongooseModule.forFeature([
|
||||
{ name: ClaimCase.name, schema: ClaimCaseSchema },
|
||||
|
||||
@@ -24,6 +24,7 @@ import { GlobalGuard } from "src/auth/guards/global.guard";
|
||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||
import { Roles } from "src/decorators/roles.decorator";
|
||||
import { CurrentUser } from "src/decorators/user.decorator";
|
||||
import { MediaPolicyService } from "src/media-policy/media-policy.service";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||
import {
|
||||
@@ -53,6 +54,7 @@ import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
||||
export class ClaimRequestManagementV2Controller {
|
||||
constructor(
|
||||
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||
private readonly mediaPolicyService: MediaPolicyService,
|
||||
) {}
|
||||
|
||||
@Get("requests")
|
||||
@@ -300,6 +302,7 @@ export class ClaimRequestManagementV2Controller {
|
||||
if (!Types.ObjectId.isValid(claimRequestId)) {
|
||||
throw new BadRequestException("Invalid claim request id");
|
||||
}
|
||||
await this.mediaPolicyService.assertForClaim(sign, claimRequestId, "image");
|
||||
const agreed =
|
||||
typeof agree === "string" ? agree === "true" || agree === "1" : Boolean(agree);
|
||||
try {
|
||||
@@ -612,6 +615,8 @@ Optional: upload car green card file in the same step.
|
||||
@CurrentUser() user: any,
|
||||
@UploadedFile() file?: Express.Multer.File,
|
||||
): Promise<SelectOtherPartsV2ResponseDto> {
|
||||
// Green-card photo is optional here — the helper no-ops on missing file.
|
||||
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
|
||||
try {
|
||||
return await this.claimRequestManagementService.selectOtherPartsV2(
|
||||
claimRequestId,
|
||||
@@ -776,6 +781,7 @@ Returns status of each item (uploaded/captured or not).
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@CurrentUser() user: any,
|
||||
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
||||
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
|
||||
try {
|
||||
return await this.claimRequestManagementService.uploadRequiredDocumentV2(
|
||||
claimRequestId,
|
||||
@@ -870,6 +876,7 @@ Returns status of each item (uploaded/captured or not).
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@CurrentUser() user: any,
|
||||
): Promise<CapturePartV2ResponseDto> {
|
||||
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
|
||||
try {
|
||||
return await this.claimRequestManagementService.capturePartV2(
|
||||
claimRequestId,
|
||||
@@ -927,6 +934,7 @@ Returns status of each item (uploaded/captured or not).
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@CurrentUser() user: any,
|
||||
) {
|
||||
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
|
||||
try {
|
||||
return await this.claimRequestManagementService.uploadClaimFactorV2(
|
||||
claimRequestId,
|
||||
@@ -996,6 +1004,7 @@ Returns status of each item (uploaded/captured or not).
|
||||
@UploadedFile("file") file: Express.Multer.File,
|
||||
@CurrentUser() user: any,
|
||||
): Promise<VideoCaptureV2ResponseDto> {
|
||||
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "video");
|
||||
try {
|
||||
return await this.claimRequestManagementService.setVideoCaptureV2(
|
||||
claimRequestId,
|
||||
|
||||
@@ -14,6 +14,34 @@ import { ClientDbService } from "./entities/db-service/client.db.service";
|
||||
*/
|
||||
export const DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS = 7;
|
||||
|
||||
/**
|
||||
* Media kinds we enforce upload-size bounds for. Each kind has its own
|
||||
* default window (see {@link DEFAULT_MEDIA_LIMITS}) and an optional
|
||||
* per-client override under `settings.media.<kind>`.
|
||||
*/
|
||||
export type MediaKind = "video" | "image" | "voice";
|
||||
|
||||
export interface MediaLimits {
|
||||
/** Inclusive lower bound. `0` allows any size from zero up. */
|
||||
minBytes: number;
|
||||
/** Inclusive upper bound. */
|
||||
maxBytes: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* System defaults applied when a client (or kind) has no explicit override.
|
||||
*
|
||||
* Important: each upload route also has a `multer.limits.fileSize` hard
|
||||
* ceiling — those route ceilings are the absolute maximum the API can
|
||||
* 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_LIMITS: Record<MediaKind, MediaLimits> = {
|
||||
video: { minBytes: 256 * 1024, maxBytes: 20 * 1024 * 1024 },
|
||||
image: { minBytes: 5 * 1024, maxBytes: 8 * 1024 * 1024 },
|
||||
voice: { minBytes: 5 * 1024, maxBytes: 8 * 1024 * 1024 },
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ClientService {
|
||||
constructor(private readonly clientDbService: ClientDbService) {}
|
||||
@@ -61,6 +89,43 @@ export class ClientService {
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the per-client byte bounds for a media kind. Missing bounds fall
|
||||
* back to {@link DEFAULT_MEDIA_LIMITS}. If no clientId is supplied (or it
|
||||
* doesn't resolve to a client document) the defaults are returned.
|
||||
*
|
||||
* The returned object is always fully populated — callers can compare
|
||||
* directly against `file.size`.
|
||||
*/
|
||||
async getMediaLimits(
|
||||
clientId: string | Types.ObjectId | null | undefined,
|
||||
kind: MediaKind,
|
||||
): Promise<MediaLimits> {
|
||||
const defaults = DEFAULT_MEDIA_LIMITS[kind];
|
||||
if (!clientId) return { ...defaults };
|
||||
|
||||
const idString = String(clientId);
|
||||
if (!Types.ObjectId.isValid(idString)) return { ...defaults };
|
||||
|
||||
const client = await this.clientDbService.findOne({
|
||||
_id: new Types.ObjectId(idString),
|
||||
});
|
||||
const override = client?.settings?.media?.[kind];
|
||||
const minBytes =
|
||||
typeof override?.minBytes === "number" &&
|
||||
Number.isFinite(override.minBytes) &&
|
||||
override.minBytes >= 0
|
||||
? override.minBytes
|
||||
: defaults.minBytes;
|
||||
const maxBytes =
|
||||
typeof override?.maxBytes === "number" &&
|
||||
Number.isFinite(override.maxBytes) &&
|
||||
override.maxBytes > 0
|
||||
? override.maxBytes
|
||||
: defaults.maxBytes;
|
||||
return { minBytes, maxBytes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the per-client CAR_BODY accident-age window (in days).
|
||||
*
|
||||
|
||||
@@ -2,6 +2,41 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
|
||||
export type ClientDocument = ClientModel & Document;
|
||||
|
||||
/**
|
||||
* Per-media (video/image/voice) byte bounds. Either bound is optional so a
|
||||
* tenant can configure only one side (e.g. raise `maxBytes` without setting
|
||||
* a floor). When a bound is missing the system-wide default is used (see
|
||||
* {@link ClientService.getMediaLimits} for the defaults table).
|
||||
*/
|
||||
export class ClientMediaLimits {
|
||||
@Prop({ type: Number, required: false })
|
||||
minBytes?: number;
|
||||
|
||||
@Prop({ type: Number, required: false })
|
||||
maxBytes?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bag of per-media-kind limits. Each kind is optional and any unset values
|
||||
* fall back to the system default (see `ClientService.getMediaLimits`).
|
||||
*
|
||||
* Note: the system also keeps an absolute multer ceiling per upload route
|
||||
* (e.g. 50MB for car-capture videos, 20MB for blame videos, 10MB for
|
||||
* voice/signature/image). A client-configured `maxBytes` cannot exceed the
|
||||
* route's multer ceiling because multer rejects oversize requests before
|
||||
* the request reaches the policy check.
|
||||
*/
|
||||
export class ClientMediaSettings {
|
||||
@Prop({ type: ClientMediaLimits, required: false })
|
||||
video?: ClientMediaLimits;
|
||||
|
||||
@Prop({ type: ClientMediaLimits, required: false })
|
||||
image?: ClientMediaLimits;
|
||||
|
||||
@Prop({ type: ClientMediaLimits, required: false })
|
||||
voice?: ClientMediaLimits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-tenant tunables. Add new policy fields here; consumers read them via
|
||||
* `ClientService` with documented defaults so older client documents keep
|
||||
@@ -15,6 +50,13 @@ export class ClientSettings {
|
||||
*/
|
||||
@Prop({ type: Number, required: false })
|
||||
carBodyAccidentMaxAgeDays?: number;
|
||||
|
||||
/**
|
||||
* Per-kind media upload bounds (V2 upload endpoints). Unset kinds /
|
||||
* bounds fall back to the defaults in `ClientService.getMediaLimits`.
|
||||
*/
|
||||
@Prop({ type: ClientMediaSettings, required: false })
|
||||
media?: ClientMediaSettings;
|
||||
}
|
||||
|
||||
@Schema({ collection: "clients", versionKey: false })
|
||||
|
||||
37
src/media-policy/media-policy.module.ts
Normal file
37
src/media-policy/media-policy.module.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { MongooseModule } from "@nestjs/mongoose";
|
||||
|
||||
import { ClientModule } from "src/client/client.module";
|
||||
import {
|
||||
ClaimCase,
|
||||
ClaimCaseSchema,
|
||||
} from "src/claim-request-management/entites/schema/claim-cases.schema";
|
||||
import {
|
||||
BlameRequest,
|
||||
BlameRequestSchema,
|
||||
} from "src/request-management/entities/schema/blame-cases.schema";
|
||||
|
||||
import { MediaPolicyService } from "./media-policy.service";
|
||||
|
||||
/**
|
||||
* Stand-alone module so we can inject `MediaPolicyService` into both the
|
||||
* blame and claim V2 controllers without dragging the entire
|
||||
* `RequestManagementModule` / `ClaimRequestManagementModule` graph along
|
||||
* (which would risk a circular dependency).
|
||||
*
|
||||
* The schemas are registered directly via `MongooseModule.forFeature` —
|
||||
* Mongoose deduplicates models behind the scenes so a second registration
|
||||
* does not create a separate model.
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
ClientModule,
|
||||
MongooseModule.forFeature([
|
||||
{ name: BlameRequest.name, schema: BlameRequestSchema },
|
||||
{ name: ClaimCase.name, schema: ClaimCaseSchema },
|
||||
]),
|
||||
],
|
||||
providers: [MediaPolicyService],
|
||||
exports: [MediaPolicyService],
|
||||
})
|
||||
export class MediaPolicyModule {}
|
||||
270
src/media-policy/media-policy.service.ts
Normal file
270
src/media-policy/media-policy.service.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger,
|
||||
} from "@nestjs/common";
|
||||
import { InjectModel } from "@nestjs/mongoose";
|
||||
import { promises as fs } from "node:fs";
|
||||
import { Model, Types } from "mongoose";
|
||||
|
||||
import {
|
||||
ClientService,
|
||||
MediaKind,
|
||||
MediaLimits,
|
||||
} from "src/client/client.service";
|
||||
import { ClaimCase } from "src/claim-request-management/entites/schema/claim-cases.schema";
|
||||
import { BlameRequest } from "src/request-management/entities/schema/blame-cases.schema";
|
||||
|
||||
/**
|
||||
* Shape we expect from a multer-uploaded file. We type loosely because two
|
||||
* different multer interceptors are in play (`FileInterceptor` and
|
||||
* `FileFieldsInterceptor`) and we don't want to drag `Express.Multer.File`
|
||||
* typings into a service file.
|
||||
*/
|
||||
type UploadedFileLike =
|
||||
| (Express.Multer.File & { path?: string })
|
||||
| undefined
|
||||
| null;
|
||||
|
||||
/**
|
||||
* Resolves the relevant client for a blame/claim file and enforces the
|
||||
* client's configured byte bounds against an uploaded file. On violation it
|
||||
* deletes the offending file from disk (best-effort) and throws a
|
||||
* `BadRequestException` carrying a structured payload the frontend can
|
||||
* branch on.
|
||||
*
|
||||
* Client resolution:
|
||||
* - Blame endpoints: any party on the `BlameRequest` that has
|
||||
* `person.clientId` set. We pick the first one we find.
|
||||
* - Claim endpoints: `ClaimCase.owner.clientId`.
|
||||
*
|
||||
* When no clientId can be resolved (early steps, malformed ids, missing
|
||||
* documents), the helper falls back to the system defaults declared in
|
||||
* `DEFAULT_MEDIA_LIMITS` so the gate is still enforced.
|
||||
*/
|
||||
@Injectable()
|
||||
export class MediaPolicyService {
|
||||
private readonly logger = new Logger(MediaPolicyService.name);
|
||||
|
||||
constructor(
|
||||
@InjectModel(BlameRequest.name)
|
||||
private readonly blameRequestModel: Model<BlameRequest>,
|
||||
@InjectModel(ClaimCase.name)
|
||||
private readonly claimCaseModel: Model<ClaimCase>,
|
||||
private readonly clientService: ClientService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Enforce limits for a file uploaded against a `BlameRequest` (V2).
|
||||
* Safe to call with a `null`/`undefined` file — it just no-ops, leaving
|
||||
* "file required" errors to be raised by the existing service handlers.
|
||||
*/
|
||||
async assertForBlame(
|
||||
file: UploadedFileLike,
|
||||
blameRequestId: string,
|
||||
kind: MediaKind,
|
||||
): Promise<void> {
|
||||
if (!file) return;
|
||||
const clientId = await this.resolveBlameClientId(blameRequestId);
|
||||
await this.assertWithClient(file, clientId, kind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce limits for a file uploaded against a `ClaimCase` (V2).
|
||||
* Safe to call with a `null`/`undefined` file.
|
||||
*/
|
||||
async assertForClaim(
|
||||
file: UploadedFileLike,
|
||||
claimRequestId: string,
|
||||
kind: MediaKind,
|
||||
): Promise<void> {
|
||||
if (!file) return;
|
||||
const clientId = await this.resolveClaimClientId(claimRequestId);
|
||||
await this.assertWithClient(file, clientId, kind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a bag of files (as produced by `FileFieldsInterceptor`) against
|
||||
* the blame file's client. Each field key is mapped to a `MediaKind` via
|
||||
* `fieldKindMap`; unmapped keys are ignored. On the first violation we
|
||||
* delete *every* provided file (the request is being rejected anyway) and
|
||||
* throw.
|
||||
*/
|
||||
async assertForBlameMany<TKeys extends string>(
|
||||
files: Partial<Record<TKeys, UploadedFileLike[] | undefined>>,
|
||||
blameRequestId: string,
|
||||
fieldKindMap: Record<TKeys, MediaKind | undefined>,
|
||||
): Promise<void> {
|
||||
const provided = this.flattenProvidedFiles<TKeys>(files);
|
||||
if (provided.length === 0) return;
|
||||
|
||||
const clientId = await this.resolveBlameClientId(blameRequestId);
|
||||
await this.assertManyWithClient(provided, clientId, fieldKindMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as `assertForBlameMany` but for claim files. Use this when (and
|
||||
* if) a multi-field claim upload endpoint is added; today's claim
|
||||
* controllers only upload one file at a time.
|
||||
*/
|
||||
async assertForClaimMany<TKeys extends string>(
|
||||
files: Partial<Record<TKeys, UploadedFileLike[] | undefined>>,
|
||||
claimRequestId: string,
|
||||
fieldKindMap: Record<TKeys, MediaKind | undefined>,
|
||||
): Promise<void> {
|
||||
const provided = this.flattenProvidedFiles<TKeys>(files);
|
||||
if (provided.length === 0) return;
|
||||
|
||||
const clientId = await this.resolveClaimClientId(claimRequestId);
|
||||
await this.assertManyWithClient(provided, clientId, fieldKindMap);
|
||||
}
|
||||
|
||||
// -- internal helpers ---------------------------------------------------
|
||||
|
||||
private async assertWithClient(
|
||||
file: NonNullable<UploadedFileLike>,
|
||||
clientId: Types.ObjectId | string | null,
|
||||
kind: MediaKind,
|
||||
): Promise<void> {
|
||||
const limits = await this.clientService.getMediaLimits(clientId, kind);
|
||||
const violation = this.checkLimits(file, limits, kind);
|
||||
if (!violation) return;
|
||||
|
||||
await this.tryDelete(file?.path);
|
||||
throw new BadRequestException(violation);
|
||||
}
|
||||
|
||||
private async assertManyWithClient<TKeys extends string>(
|
||||
provided: Array<{ field: TKeys; file: NonNullable<UploadedFileLike> }>,
|
||||
clientId: Types.ObjectId | string | null,
|
||||
fieldKindMap: Record<TKeys, MediaKind | undefined>,
|
||||
): Promise<void> {
|
||||
const limitsCache = new Map<MediaKind, MediaLimits>();
|
||||
const getLimits = async (kind: MediaKind) => {
|
||||
if (!limitsCache.has(kind)) {
|
||||
limitsCache.set(
|
||||
kind,
|
||||
await this.clientService.getMediaLimits(clientId, kind),
|
||||
);
|
||||
}
|
||||
return limitsCache.get(kind) as MediaLimits;
|
||||
};
|
||||
|
||||
for (const { field, file } of provided) {
|
||||
const kind = fieldKindMap[field];
|
||||
if (!kind) continue;
|
||||
const limits = await getLimits(kind);
|
||||
const violation = this.checkLimits(file, limits, kind, field);
|
||||
if (!violation) continue;
|
||||
|
||||
// The request is being rejected — clean up all uploaded files so we
|
||||
// don't leave orphans on disk.
|
||||
await Promise.all(provided.map(({ file: f }) => this.tryDelete(f?.path)));
|
||||
throw new BadRequestException(violation);
|
||||
}
|
||||
}
|
||||
|
||||
private checkLimits(
|
||||
file: NonNullable<UploadedFileLike>,
|
||||
{ minBytes, maxBytes }: MediaLimits,
|
||||
kind: MediaKind,
|
||||
field?: string,
|
||||
): Record<string, unknown> | null {
|
||||
const size = Number(file.size ?? 0);
|
||||
if (!Number.isFinite(size)) {
|
||||
return {
|
||||
code: "MEDIA_SIZE_UNKNOWN",
|
||||
kind,
|
||||
...(field ? { field } : {}),
|
||||
message: `Could not determine the size of the uploaded ${kind} file.`,
|
||||
};
|
||||
}
|
||||
if (size < minBytes) {
|
||||
return {
|
||||
code: "MEDIA_TOO_SMALL",
|
||||
kind,
|
||||
...(field ? { field } : {}),
|
||||
sizeBytes: size,
|
||||
minBytes,
|
||||
maxBytes,
|
||||
message:
|
||||
`Uploaded ${kind} is too small (${size} bytes). ` +
|
||||
`Minimum allowed for this client is ${minBytes} bytes.`,
|
||||
};
|
||||
}
|
||||
if (size > maxBytes) {
|
||||
return {
|
||||
code: "MEDIA_TOO_LARGE",
|
||||
kind,
|
||||
...(field ? { field } : {}),
|
||||
sizeBytes: size,
|
||||
minBytes,
|
||||
maxBytes,
|
||||
message:
|
||||
`Uploaded ${kind} is too large (${size} bytes). ` +
|
||||
`Maximum allowed for this client is ${maxBytes} bytes.`,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private flattenProvidedFiles<TKeys extends string>(
|
||||
files: Partial<Record<TKeys, UploadedFileLike[] | undefined>>,
|
||||
): Array<{ field: TKeys; file: NonNullable<UploadedFileLike> }> {
|
||||
const out: Array<{ field: TKeys; file: NonNullable<UploadedFileLike> }> = [];
|
||||
if (!files) return out;
|
||||
for (const key of Object.keys(files) as TKeys[]) {
|
||||
for (const f of files[key] || []) {
|
||||
if (f) out.push({ field: key, file: f });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private async resolveBlameClientId(
|
||||
blameRequestId: string,
|
||||
): Promise<Types.ObjectId | null> {
|
||||
if (!blameRequestId || !Types.ObjectId.isValid(blameRequestId)) return null;
|
||||
|
||||
const blame = (await this.blameRequestModel
|
||||
.findById(blameRequestId)
|
||||
.select("parties")
|
||||
.lean()
|
||||
.exec()) as { parties?: Array<{ person?: { clientId?: Types.ObjectId } }> } | null;
|
||||
if (!blame?.parties?.length) return null;
|
||||
|
||||
for (const party of blame.parties) {
|
||||
const clientId = party?.person?.clientId;
|
||||
if (clientId) return clientId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async resolveClaimClientId(
|
||||
claimRequestId: string,
|
||||
): Promise<Types.ObjectId | null> {
|
||||
if (!claimRequestId || !Types.ObjectId.isValid(claimRequestId)) return null;
|
||||
|
||||
const claim = (await this.claimCaseModel
|
||||
.findById(claimRequestId)
|
||||
.select("owner")
|
||||
.lean()
|
||||
.exec()) as { owner?: { clientId?: Types.ObjectId } } | null;
|
||||
return claim?.owner?.clientId ?? null;
|
||||
}
|
||||
|
||||
private async tryDelete(filePath?: string): Promise<void> {
|
||||
if (!filePath) return;
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (err) {
|
||||
// Best-effort cleanup — log and continue. The HTTP response will
|
||||
// already carry the violation details for the client.
|
||||
this.logger.warn(
|
||||
`MediaPolicyService: failed to delete file at ${filePath}: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||
import { CurrentUser } from "src/decorators/user.decorator";
|
||||
import { Roles } from "src/decorators/roles.decorator";
|
||||
import { MediaPolicyService } from "src/media-policy/media-policy.service";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { RequestManagementService } from "./request-management.service";
|
||||
import { CreateExpertInitiatedFileDto } from "./dto/expert-initiated.dto";
|
||||
@@ -53,6 +54,7 @@ export class ExpertInitiatedV2Controller {
|
||||
constructor(
|
||||
private readonly requestManagementService: RequestManagementService,
|
||||
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||
private readonly mediaPolicyService: MediaPolicyService,
|
||||
) {}
|
||||
|
||||
@Get("my-files")
|
||||
@@ -393,6 +395,7 @@ export class ExpertInitiatedV2Controller {
|
||||
@Param("requestId") requestId: string,
|
||||
@UploadedFile() file?: Express.Multer.File,
|
||||
) {
|
||||
await this.mediaPolicyService.assertForBlame(file, requestId, "video");
|
||||
return this.requestManagementService.expertUploadVideoForBlameV2(
|
||||
expert,
|
||||
requestId,
|
||||
@@ -432,6 +435,7 @@ export class ExpertInitiatedV2Controller {
|
||||
@Param("requestId") requestId: string,
|
||||
@UploadedFile() voice?: Express.Multer.File,
|
||||
) {
|
||||
await this.mediaPolicyService.assertForBlame(voice, requestId, "voice");
|
||||
return this.requestManagementService.expertUploadVoiceForBlameV2(
|
||||
expert,
|
||||
requestId,
|
||||
@@ -497,6 +501,8 @@ export class ExpertInitiatedV2Controller {
|
||||
@Body() body: { partyRole?: string; isAccept?: string | boolean },
|
||||
@UploadedFile() sign?: Express.Multer.File,
|
||||
) {
|
||||
await this.mediaPolicyService.assertForBlame(sign, requestId, "image");
|
||||
|
||||
const partyRole = (body.partyRole === "FIRST" || body.partyRole === "SECOND")
|
||||
? body.partyRole
|
||||
: ("FIRST" as const);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { HashModule } from "src/utils/hash/hash.module";
|
||||
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
|
||||
import { WorkflowStepManagementModule } from "src/workflow-step-management/workflow-step-management.module";
|
||||
import { AuthModule } from "src/auth/auth.module";
|
||||
import { MediaPolicyModule } from "src/media-policy/media-policy.module";
|
||||
import { BlameDocumentDbService } from "./entities/db-service/blame-document.db.service";
|
||||
import { BlameVoiceDbService } from "./entities/db-service/blame.voice.db.service";
|
||||
import { UserSignDbService } from "./entities/db-service/sign.db.service";
|
||||
@@ -64,6 +65,7 @@ import { RegistrarInitiatedController } from "./registrar-initiated.controller";
|
||||
forwardRef(() => ClaimRequestManagementModule),
|
||||
CronModule,
|
||||
AuthModule,
|
||||
MediaPolicyModule,
|
||||
],
|
||||
controllers: [
|
||||
RequestManagementController,
|
||||
|
||||
@@ -28,6 +28,7 @@ import { GlobalGuard } from "src/auth/guards/global.guard";
|
||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||
import { Roles } from "src/decorators/roles.decorator";
|
||||
import { CurrentUser } from "src/decorators/user.decorator";
|
||||
import { MediaPolicyService } from "src/media-policy/media-policy.service";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
|
||||
import {
|
||||
@@ -53,6 +54,7 @@ import { RequestManagementService } from "./request-management.service";
|
||||
export class RequestManagementV2Controller {
|
||||
constructor(
|
||||
private readonly requestManagementService: RequestManagementService,
|
||||
private readonly mediaPolicyService: MediaPolicyService,
|
||||
) { }
|
||||
|
||||
@Post()
|
||||
@@ -154,6 +156,7 @@ export class RequestManagementV2Controller {
|
||||
@CurrentUser() user,
|
||||
@UploadedFile() file?: Express.Multer.File,
|
||||
) {
|
||||
await this.mediaPolicyService.assertForBlame(file, requestId, "video");
|
||||
return this.requestManagementService.uploadFirstPartyVideoV2(
|
||||
requestId,
|
||||
file,
|
||||
@@ -222,6 +225,7 @@ export class RequestManagementV2Controller {
|
||||
@CurrentUser() user,
|
||||
@UploadedFile() voice?: Express.Multer.File,
|
||||
) {
|
||||
await this.mediaPolicyService.assertForBlame(voice, requestId, "voice");
|
||||
return this.requestManagementService.uploadVoiceV2(requestId, voice, user);
|
||||
}
|
||||
|
||||
@@ -338,6 +342,8 @@ export class RequestManagementV2Controller {
|
||||
@CurrentUser() user: any,
|
||||
@UploadedFile() sign: Express.Multer.File,
|
||||
) {
|
||||
await this.mediaPolicyService.assertForBlame(sign, requestId, "image");
|
||||
|
||||
// Convert string "true"/"false" to boolean
|
||||
const acceptDecision = typeof isAccept === "string"
|
||||
? isAccept === "true"
|
||||
@@ -449,6 +455,18 @@ export class RequestManagementV2Controller {
|
||||
@Body("description") description?: string,
|
||||
@Body("textDescription") textDescription?: string,
|
||||
) {
|
||||
// Each multipart field maps to a media kind so the policy gate can pick
|
||||
// the right per-client bounds. Document images = `image`, audio = `voice`,
|
||||
// video = `video`.
|
||||
await this.mediaPolicyService.assertForBlameMany(files, requestId, {
|
||||
drivingLicense: "image",
|
||||
carCertificate: "image",
|
||||
nationalCertificate: "image",
|
||||
carGreenCard: "image",
|
||||
voice: "voice",
|
||||
video: "video",
|
||||
});
|
||||
|
||||
const descCandidates = [description, textDescription]
|
||||
.map((x) => (x != null ? String(x).trim() : ""))
|
||||
.filter((s) => s.length > 0);
|
||||
|
||||
Reference in New Issue
Block a user