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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user