chore: import DH V2 D5.6.4 production baseline

This commit is contained in:
DH V2
2026-09-05 10:12:35 -03:00
commit 82213e72f5
757 changed files with 84218 additions and 0 deletions
@@ -0,0 +1,22 @@
import { createHash } from 'node:crypto';
function canonicalValue(value: unknown): unknown {
if (value instanceof Date) return value.toISOString();
if (Array.isArray(value)) return value.map(canonicalValue);
if (value !== null && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value as Record<string, unknown>)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, item]) => [key, canonicalValue(item)]),
);
}
return value;
}
export function canonicalJson(value: unknown): string {
return JSON.stringify(canonicalValue(value));
}
export function sha256CanonicalJson(value: unknown): string {
return createHash('sha256').update(canonicalJson(value)).digest('hex');
}
@@ -0,0 +1,10 @@
import { IsEnum, IsISO8601 } from 'class-validator';
import { InspectionActUploadMode } from '../../database/entities';
export class CloseInspectionActDto {
@IsISO8601({ strict: true })
clientClosedAt!: string;
@IsEnum(InspectionActUploadMode)
uploadMode!: InspectionActUploadMode;
}
@@ -0,0 +1,17 @@
import { Transform } from 'class-transformer';
import { IsIn, IsString, MaxLength, MinLength } from 'class-validator';
import { InspectionActSignatureStatus } from '../../database/entities';
export class CreateCompanyOutcomeDto {
@IsIn([
InspectionActSignatureStatus.REFUSED,
InspectionActSignatureStatus.ABSENT,
])
status!: InspectionActSignatureStatus.REFUSED | InspectionActSignatureStatus.ABSENT;
@Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
@IsString()
@MinLength(10)
@MaxLength(4000)
reason!: string;
}
@@ -0,0 +1,49 @@
import { Transform, Type } from 'class-transformer';
import {
Equals,
IsBoolean,
IsISO8601,
IsNumber,
IsOptional,
IsString,
Max,
MaxLength,
Min,
} from 'class-validator';
export class CreateInspectionSignatureDto {
@Transform(({ value }) => value === true || value === 'true')
@IsBoolean()
@Equals(true)
consentAccepted!: boolean;
@IsOptional()
@IsISO8601({ strict: true })
clientSignedAt?: string;
@IsOptional()
@Type(() => Number)
@IsNumber({ maxDecimalPlaces: 6 })
@Min(-90)
@Max(90)
latitude?: number;
@IsOptional()
@Type(() => Number)
@IsNumber({ maxDecimalPlaces: 6 })
@Min(-180)
@Max(180)
longitude?: number;
@IsOptional()
@Type(() => Number)
@IsNumber({ maxDecimalPlaces: 3 })
@Min(0)
@Max(100000)
accuracyM?: number;
@IsOptional()
@IsString()
@MaxLength(200)
deviceLabel?: string;
}
@@ -0,0 +1,77 @@
import { Transform } from 'class-transformer';
import {
IsEmail,
IsEnum,
IsOptional,
IsString,
MaxLength,
MinLength,
ValidateIf,
} from 'class-validator';
import {
InspectionResponsibleAttendanceStatus,
InspectionResponsibleDocumentType,
} from '../../database/entities';
const trimOrUndefined = ({ value }: { value: unknown }) => (
typeof value === 'string' && value.trim() ? value.trim() : undefined
);
export class UpsertInspectionResponsibleDto {
@IsEnum(InspectionResponsibleAttendanceStatus)
attendanceStatus!: InspectionResponsibleAttendanceStatus;
@ValidateIf((dto: UpsertInspectionResponsibleDto) => (
dto.attendanceStatus === InspectionResponsibleAttendanceStatus.PRESENT
))
@Transform(trimOrUndefined)
@IsString()
@MinLength(1)
@MaxLength(200)
fullName?: string;
@ValidateIf((dto: UpsertInspectionResponsibleDto) => (
dto.attendanceStatus === InspectionResponsibleAttendanceStatus.PRESENT
))
@IsEnum(InspectionResponsibleDocumentType)
documentType?: InspectionResponsibleDocumentType;
@ValidateIf((dto: UpsertInspectionResponsibleDto) => (
dto.attendanceStatus === InspectionResponsibleAttendanceStatus.PRESENT
))
@Transform(trimOrUndefined)
@IsString()
@MinLength(1)
@MaxLength(40)
documentNumber?: string;
@ValidateIf((dto: UpsertInspectionResponsibleDto) => (
dto.attendanceStatus === InspectionResponsibleAttendanceStatus.PRESENT
))
@Transform(trimOrUndefined)
@IsString()
@MinLength(1)
@MaxLength(200)
position?: string;
@IsOptional()
@Transform(trimOrUndefined)
@IsEmail()
@MaxLength(320)
email?: string;
@IsOptional()
@Transform(trimOrUndefined)
@IsString()
@MaxLength(50)
phone?: string;
@ValidateIf((dto: UpsertInspectionResponsibleDto) => (
dto.attendanceStatus === InspectionResponsibleAttendanceStatus.ABSENT
))
@Transform(trimOrUndefined)
@IsString()
@MinLength(10)
@MaxLength(4000)
absenceReason?: string;
}
@@ -0,0 +1,147 @@
import {
Body,
Controller,
Get,
Param,
ParseUUIDPipe,
Post,
Put,
Req,
Res,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import type { Response } from 'express';
import { CurrentAuth } from '../auth/decorators/current-auth.decorator';
import { RequirePermissions } from '../authorization/decorators/require-permissions.decorator';
import type { AuthPrincipal, RequestWithContext } from '../common/http/request-context';
import { CloseInspectionActDto } from './dto/close-inspection-act.dto';
import { CreateCompanyOutcomeDto } from './dto/create-company-outcome.dto';
import { CreateInspectionSignatureDto } from './dto/create-inspection-signature.dto';
import { UpsertInspectionResponsibleDto } from './dto/upsert-inspection-responsible.dto';
import { InspectionClosingService } from './inspection-closing.service';
import {
MAX_INSPECTION_SIGNATURE_BYTES,
type UploadedInspectionSignatureFile,
} from './inspection-signature-file';
@Controller('inspection-acts/:actId')
export class InspectionClosingController {
constructor(private readonly closing: InspectionClosingService) {}
@Get('closure')
@RequirePermissions('inspection_closure.read')
get(@Param('actId', new ParseUUIDPipe({ version: '4' })) actId: string) {
return this.closing.get(actId);
}
@Put('responsible')
@RequirePermissions('inspection_closure.prepare')
responsible(
@Param('actId', new ParseUUIDPipe({ version: '4' })) actId: string,
@Body() dto: UpsertInspectionResponsibleDto,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.closing.upsertResponsible(actId, dto, principal, request);
}
@Post('ready')
@RequirePermissions('inspection_closure.prepare')
ready(
@Param('actId', new ParseUUIDPipe({ version: '4' })) actId: string,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.closing.prepare(actId, principal, request);
}
@Post('reopen')
@RequirePermissions('inspection_closure.prepare')
reopen(
@Param('actId', new ParseUUIDPipe({ version: '4' })) actId: string,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.closing.reopen(actId, principal, request);
}
@Post('signatures/inspector')
@RequirePermissions('inspection_closure.sign')
@UseInterceptors(FileInterceptor('file', {
limits: { fileSize: MAX_INSPECTION_SIGNATURE_BYTES, files: 1 },
}))
inspectorSignature(
@Param('actId', new ParseUUIDPipe({ version: '4' })) actId: string,
@Body() dto: CreateInspectionSignatureDto,
@UploadedFile() file: UploadedInspectionSignatureFile | undefined,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.closing.signInspector(actId, dto, file, principal, request);
}
@Post('signatures/company')
@RequirePermissions('inspection_closure.sign')
@UseInterceptors(FileInterceptor('file', {
limits: { fileSize: MAX_INSPECTION_SIGNATURE_BYTES, files: 1 },
}))
companySignature(
@Param('actId', new ParseUUIDPipe({ version: '4' })) actId: string,
@Body() dto: CreateInspectionSignatureDto,
@UploadedFile() file: UploadedInspectionSignatureFile | undefined,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.closing.signCompany(actId, dto, file, principal, request);
}
@Post('company-outcome')
@RequirePermissions('inspection_closure.sign')
companyOutcome(
@Param('actId', new ParseUUIDPipe({ version: '4' })) actId: string,
@Body() dto: CreateCompanyOutcomeDto,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.closing.recordCompanyOutcome(actId, dto, principal, request);
}
@Post('close')
@RequirePermissions('inspection_closure.close')
close(
@Param('actId', new ParseUUIDPipe({ version: '4' })) actId: string,
@Body() dto: CloseInspectionActDto,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.closing.close(actId, dto, principal, request);
}
}
@Controller('inspection-act-signatures')
export class InspectionSignatureContentController {
constructor(private readonly closing: InspectionClosingService) {}
@Get(':signatureId/content')
@RequirePermissions('inspection_closure.read')
async content(
@Param('signatureId', new ParseUUIDPipe({ version: '4' })) signatureId: string,
@Res() response: Response,
): Promise<void> {
const { filePath, signature } = await this.closing.signatureContent(signatureId);
response.setHeader('Content-Type', 'image/png');
response.setHeader('Content-Length', String(signature.sizeBytes));
response.setHeader('Content-Disposition', 'inline; filename="firma.png"');
response.setHeader('Cache-Control', 'private, no-store');
response.setHeader('X-Content-Type-Options', 'nosniff');
response.setHeader('Content-Security-Policy', "sandbox; default-src 'none'");
await new Promise<void>((resolveSend, rejectSend) => {
response.sendFile(filePath, (error) => {
if (error) rejectSend(error);
else resolveSend();
});
});
}
}
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { AuditModule } from '../audit/audit.module';
import { InspectionReportsModule } from '../inspection-reports/inspection-reports.module';
import {
InspectionClosingController,
InspectionSignatureContentController,
} from './inspection-closing.controller';
import { InspectionClosingService } from './inspection-closing.service';
@Module({
imports: [AuditModule, InspectionReportsModule],
controllers: [InspectionClosingController, InspectionSignatureContentController],
providers: [InspectionClosingService],
})
export class InspectionClosingModule {}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
import { BadRequestException } from '@nestjs/common';
export const MAX_INSPECTION_SIGNATURE_BYTES = 1024 * 1024;
export interface UploadedInspectionSignatureFile {
originalname: string;
mimetype: string;
size: number;
buffer: Buffer;
}
export interface InspectedInspectionSignatureFile {
originalName: string;
mimeType: 'image/png';
extension: '.png';
}
export function inspectInspectionSignatureFile(
file: UploadedInspectionSignatureFile | undefined,
): InspectedInspectionSignatureFile {
if (!file?.buffer?.length) {
throw new BadRequestException({
code: 'INSPECTION_SIGNATURE_FILE_REQUIRED',
message: 'La firma manuscrita en formato PNG es obligatoria',
});
}
if (file.buffer.length > MAX_INSPECTION_SIGNATURE_BYTES) {
throw new BadRequestException({
code: 'INSPECTION_SIGNATURE_FILE_TOO_LARGE',
message: 'La firma no puede superar 1 MB',
});
}
const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
if (
file.mimetype !== 'image/png'
|| file.buffer.length < pngSignature.length
|| !file.buffer.subarray(0, pngSignature.length).equals(pngSignature)
) {
throw new BadRequestException({
code: 'INSPECTION_SIGNATURE_PNG_REQUIRED',
message: 'La firma debe ser una imagen PNG válida',
});
}
return {
originalName: (file.originalname || 'firma.png').slice(0, 255),
mimeType: 'image/png',
extension: '.png',
};
}