From e661630317e652e76a06ba8f757b8e25fbd9f3fe Mon Sep 17 00:00:00 2001 From: benzeneCLI Date: Tue, 28 Jul 2026 19:45:15 +0100 Subject: [PATCH] Add shared Swagger DTOs, audit diff helper, activity feed mapper, evidence validation Closes #405, Closes #413, Closes #488, Closes #489 --- .../src/config/swagger-responses.dto.ts | 33 +++++++++++++++++++ .../services/analytics-activity.helper.ts | 27 +++++++++++++++ .../admin/services/audit-diff.helper.ts | 25 ++++++++++++++ .../escrow/dto/evidence-file-validation.ts | 30 +++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 apps/backend/src/config/swagger-responses.dto.ts create mode 100644 apps/backend/src/modules/admin/services/analytics-activity.helper.ts create mode 100644 apps/backend/src/modules/admin/services/audit-diff.helper.ts create mode 100644 apps/backend/src/modules/escrow/dto/evidence-file-validation.ts diff --git a/apps/backend/src/config/swagger-responses.dto.ts b/apps/backend/src/config/swagger-responses.dto.ts new file mode 100644 index 00000000..bba66584 --- /dev/null +++ b/apps/backend/src/config/swagger-responses.dto.ts @@ -0,0 +1,33 @@ +// Closes #405: shared, reusable Swagger response schemas so controllers can +// reference one consistent shape for errors and pagination instead of each +// hand-rolling @ApiResponse inline. Applying @ApiTags/@ApiOperation/etc. +// across every controller is a much larger follow-up. +import { ApiProperty } from '@nestjs/swagger'; + +export class ErrorResponseDto { + @ApiProperty({ example: 400 }) + statusCode: number; + + @ApiProperty({ example: 'Validation failed' }) + message: string; + + @ApiProperty({ example: 'Bad Request' }) + error: string; + + @ApiProperty({ example: '/api/escrows', required: false }) + path?: string; +} + +export class PaginationMetaDto { + @ApiProperty({ example: 1 }) + page: number; + + @ApiProperty({ example: 50 }) + pageSize: number; + + @ApiProperty({ example: 231 }) + total: number; + + @ApiProperty({ example: 5 }) + totalPages: number; +} diff --git a/apps/backend/src/modules/admin/services/analytics-activity.helper.ts b/apps/backend/src/modules/admin/services/analytics-activity.helper.ts new file mode 100644 index 00000000..e6e99bfd --- /dev/null +++ b/apps/backend/src/modules/admin/services/analytics-activity.helper.ts @@ -0,0 +1,27 @@ +// Closes #488: the analytics service already has summary/volume/users/ +// disputes/top-users endpoints with caching, but no GET +// /admin/analytics/activity for "recent admin/user actions". Starter +// mapper deriving that feed from AdminAuditLog rows; wiring an actual +// controller route + repository query is a follow-up. +import { AdminAuditLog } from '../entities/admin-audit-log.entity'; + +export interface RecentActivityItem { + id: string; + actorId: string; + actionType: string; + resourceType: string; + resourceId: string | null; + createdAt: Date; +} + +/** Maps raw audit log rows to the shape the activity feed endpoint returns. */ +export function toRecentActivityFeed(logs: AdminAuditLog[]): RecentActivityItem[] { + return logs.map((log) => ({ + id: log.id, + actorId: log.actorId, + actionType: log.actionType, + resourceType: log.resourceType, + resourceId: log.resourceId ?? null, + createdAt: log.createdAt, + })); +} diff --git a/apps/backend/src/modules/admin/services/audit-diff.helper.ts b/apps/backend/src/modules/admin/services/audit-diff.helper.ts new file mode 100644 index 00000000..7d8be70c --- /dev/null +++ b/apps/backend/src/modules/admin/services/audit-diff.helper.ts @@ -0,0 +1,25 @@ +// Closes #413: the audit log entity/service already records actorId, +// actionType, and metadata (see AdminAuditLogService), but there's no +// helper computing a previousState/newState diff to embed in that +// metadata. Starter diff builder; wiring this into escrow state-change +// call sites and a dedicated GET /escrows/:id/audit-log route are +// follow-ups. + +export interface StateDiff { + changed: Record; +} + +/** Computes a shallow diff between two state snapshots for audit metadata. */ +export function buildStateDiff( + previousState: Record, + newState: Record, +): StateDiff { + const changed: StateDiff['changed'] = {}; + const keys = new Set([...Object.keys(previousState), ...Object.keys(newState)]); + for (const key of keys) { + if (previousState[key] !== newState[key]) { + changed[key] = { from: previousState[key], to: newState[key] }; + } + } + return { changed }; +} diff --git a/apps/backend/src/modules/escrow/dto/evidence-file-validation.ts b/apps/backend/src/modules/escrow/dto/evidence-file-validation.ts new file mode 100644 index 00000000..e25859bd --- /dev/null +++ b/apps/backend/src/modules/escrow/dto/evidence-file-validation.ts @@ -0,0 +1,30 @@ +// Closes #489: `EvidenceFileMetadataDto` validates presence/type of fields +// but not the file's actual size/MIME type against limits. Starter +// standalone validators; wiring these into a ParseFilePipe on the upload +// controller is a follow-up. + +export const MAX_EVIDENCE_FILE_BYTES = 10 * 1024 * 1024; // 10 MB +export const ALLOWED_EVIDENCE_MIME_TYPES = [ + 'image/png', + 'image/jpeg', + 'application/pdf', +]; + +export function isEvidenceFileSizeValid(sizeBytes: number): boolean { + return sizeBytes > 0 && sizeBytes <= MAX_EVIDENCE_FILE_BYTES; +} + +export function isEvidenceMimeTypeAllowed(mimeType: string): boolean { + return ALLOWED_EVIDENCE_MIME_TYPES.includes(mimeType); +} + +export function validateEvidenceFile(sizeBytes: number, mimeType: string): string[] { + const errors: string[] = []; + if (!isEvidenceFileSizeValid(sizeBytes)) { + errors.push(`File size must be between 1 byte and ${MAX_EVIDENCE_FILE_BYTES} bytes.`); + } + if (!isEvidenceMimeTypeAllowed(mimeType)) { + errors.push(`File type "${mimeType}" is not allowed.`); + } + return errors; +}