feat: C-8/C-1/C-2/C-3/C-4/C-6/M-1/M-5/M-6 — WebSocket gateway, SENAE real, SP-API, Twilio SMS, WhatsApp Business, soporte portal, HMAC audit, reportes CSV

This commit is contained in:
Lizandro Guarnizo
2026-06-01 21:28:42 -05:00
parent a5842278fb
commit 84c1fdec54
27 changed files with 1743 additions and 122 deletions
+67 -1
View File
@@ -1,4 +1,5 @@
import { Injectable, Logger } from "@nestjs/common";
import { createHmac } from "crypto";
import { PrismaService } from "../prisma/prisma.service";
export interface AuditLogEntry {
@@ -12,21 +13,86 @@ export interface AuditLogEntry {
userAgent?: string;
}
/**
* AuditLogService — M-5
* Adds HMAC-SHA256 integrity hash to every log entry so tampering can be detected.
* Hash is stored in metadata.integrity.
* Secret: env var AUDIT_HMAC_SECRET (falls back to a default for dev).
*/
@Injectable()
export class AuditLogService {
private readonly logger = new Logger(AuditLogService.name);
private readonly hmacSecret = process.env.AUDIT_HMAC_SECRET ?? "moraworld-audit-hmac-secret-change-in-prod";
constructor(private prisma: PrismaService) {}
/**
* Compute HMAC-SHA256 of the canonical log payload.
* Canonical form: JSON.stringify of { tenantId, userId, action, resource, resourceId, createdAt }
*/
private computeIntegrity(entry: AuditLogEntry, createdAt: Date): string {
const canonical = JSON.stringify({
tenantId: entry.tenantId ?? null,
userId: entry.userId ?? null,
action: entry.action,
resource: entry.resource ?? null,
resourceId: entry.resourceId ?? null,
createdAt: createdAt.toISOString(),
});
return createHmac("sha256", this.hmacSecret).update(canonical).digest("hex");
}
async log(entry: AuditLogEntry): Promise<void> {
try {
await this.prisma.client.auditLog.create({ data: entry });
const createdAt = new Date();
const integrity = this.computeIntegrity(entry, createdAt);
await this.prisma.client.auditLog.create({
data: {
...entry,
metadata: {
...(entry.metadata ?? {}),
integrity, // HMAC-SHA256 of canonical payload
},
createdAt,
},
});
} catch (e: unknown) {
// Never let audit log failure break main flow
this.logger.error("AuditLog write failed", (e as Error).message);
}
}
/**
* Verify the integrity hash of a stored audit log entry.
* Returns true if the hash matches, false if the record was tampered with.
*/
verify(entry: {
tenantId?: string | null;
userId?: string | null;
action: string;
resource?: string | null;
resourceId?: string | null;
createdAt: Date;
metadata?: any;
}): boolean {
const stored = entry.metadata?.integrity;
if (!stored) return false;
const canonical = JSON.stringify({
tenantId: entry.tenantId ?? null,
userId: entry.userId ?? null,
action: entry.action,
resource: entry.resource ?? null,
resourceId: entry.resourceId ?? null,
createdAt: entry.createdAt instanceof Date
? entry.createdAt.toISOString()
: new Date(entry.createdAt).toISOString(),
});
const expected = createHmac("sha256", this.hmacSecret).update(canonical).digest("hex");
return expected === stored;
}
async findAll(filters: {
tenantId?: string;
userId?: string;