Template base para proyectos SaaS multi-tenant. Estructura inicial: - apps/backend (NestJS API) - apps/frontend (React/Vite) - apps/database (PostgreSQL DDL) - docs/ (Documentación) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
153 lines
6.3 KiB
JavaScript
153 lines
6.3 KiB
JavaScript
"use strict";
|
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
};
|
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.AuditInterceptor = exports.SKIP_AUDIT_KEY = exports.AUDIT_ENTITY_KEY = exports.AUDIT_ACTION_KEY = void 0;
|
|
exports.AuditActionDecorator = AuditActionDecorator;
|
|
exports.AuditEntity = AuditEntity;
|
|
exports.SkipAudit = SkipAudit;
|
|
const common_1 = require("@nestjs/common");
|
|
const rxjs_1 = require("rxjs");
|
|
const core_1 = require("@nestjs/core");
|
|
const audit_service_1 = require("../services/audit.service");
|
|
const audit_log_entity_1 = require("../entities/audit-log.entity");
|
|
exports.AUDIT_ACTION_KEY = 'audit_action';
|
|
exports.AUDIT_ENTITY_KEY = 'audit_entity';
|
|
exports.SKIP_AUDIT_KEY = 'skip_audit';
|
|
function AuditActionDecorator(action) {
|
|
return (target, propertyKey, descriptor) => {
|
|
Reflect.defineMetadata(exports.AUDIT_ACTION_KEY, action, descriptor.value);
|
|
return descriptor;
|
|
};
|
|
}
|
|
function AuditEntity(entityType) {
|
|
return (target, propertyKey, descriptor) => {
|
|
Reflect.defineMetadata(exports.AUDIT_ENTITY_KEY, entityType, descriptor.value);
|
|
return descriptor;
|
|
};
|
|
}
|
|
function SkipAudit() {
|
|
return (target, propertyKey, descriptor) => {
|
|
Reflect.defineMetadata(exports.SKIP_AUDIT_KEY, true, descriptor.value);
|
|
return descriptor;
|
|
};
|
|
}
|
|
let AuditInterceptor = class AuditInterceptor {
|
|
constructor(auditService, reflector) {
|
|
this.auditService = auditService;
|
|
this.reflector = reflector;
|
|
}
|
|
intercept(context, next) {
|
|
const request = context.switchToHttp().getRequest();
|
|
const handler = context.getHandler();
|
|
const startTime = Date.now();
|
|
const skipAudit = this.reflector.get(exports.SKIP_AUDIT_KEY, handler);
|
|
if (skipAudit) {
|
|
return next.handle();
|
|
}
|
|
const auditAction = this.reflector.get(exports.AUDIT_ACTION_KEY, handler);
|
|
const entityType = this.reflector.get(exports.AUDIT_ENTITY_KEY, handler);
|
|
const action = auditAction || this.inferActionFromMethod(request.method);
|
|
if (!action) {
|
|
return next.handle();
|
|
}
|
|
return next.handle().pipe((0, rxjs_1.tap)({
|
|
next: async (response) => {
|
|
const duration = Date.now() - startTime;
|
|
await this.logAudit(request, action, entityType, response, 200, duration);
|
|
},
|
|
error: async (error) => {
|
|
const duration = Date.now() - startTime;
|
|
const statusCode = error.status || 500;
|
|
await this.logAudit(request, action, entityType, null, statusCode, duration);
|
|
},
|
|
}));
|
|
}
|
|
inferActionFromMethod(method) {
|
|
switch (method.toUpperCase()) {
|
|
case 'POST':
|
|
return audit_log_entity_1.AuditAction.CREATE;
|
|
case 'PUT':
|
|
case 'PATCH':
|
|
return audit_log_entity_1.AuditAction.UPDATE;
|
|
case 'DELETE':
|
|
return audit_log_entity_1.AuditAction.DELETE;
|
|
case 'GET':
|
|
return null;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
async logAudit(request, action, entityType, response, statusCode, duration) {
|
|
try {
|
|
const tenantId = request.user?.tenantId || request.headers['x-tenant-id'];
|
|
if (!tenantId) {
|
|
return;
|
|
}
|
|
const params = {
|
|
tenant_id: tenantId,
|
|
user_id: request.user?.sub || request.user?.id,
|
|
action,
|
|
entity_type: entityType || this.inferEntityFromPath(request.path),
|
|
entity_id: request.params?.id,
|
|
old_values: request.body?._oldValues,
|
|
new_values: this.sanitizeBody(request.body),
|
|
ip_address: this.getClientIp(request),
|
|
user_agent: request.headers['user-agent'],
|
|
endpoint: request.path,
|
|
http_method: request.method,
|
|
response_status: statusCode,
|
|
duration_ms: duration,
|
|
metadata: {
|
|
query: request.query,
|
|
response_id: response?.id,
|
|
},
|
|
};
|
|
await this.auditService.createAuditLog(params);
|
|
}
|
|
catch (error) {
|
|
console.error('Failed to create audit log:', error);
|
|
}
|
|
}
|
|
inferEntityFromPath(path) {
|
|
const segments = path.split('/').filter(Boolean);
|
|
const apiIndex = segments.findIndex((s) => s === 'api');
|
|
if (apiIndex !== -1 && segments.length > apiIndex + 2) {
|
|
return segments[apiIndex + 2];
|
|
}
|
|
return segments[segments.length - 1] || 'unknown';
|
|
}
|
|
sanitizeBody(body) {
|
|
if (!body)
|
|
return undefined;
|
|
const sanitized = { ...body };
|
|
const sensitiveFields = ['password', 'token', 'secret', 'creditCard', 'cvv', '_oldValues'];
|
|
for (const field of sensitiveFields) {
|
|
if (field in sanitized) {
|
|
sanitized[field] = '[REDACTED]';
|
|
}
|
|
}
|
|
return sanitized;
|
|
}
|
|
getClientIp(request) {
|
|
return (request.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
|
request.headers['x-real-ip'] ||
|
|
request.connection?.remoteAddress ||
|
|
request.ip ||
|
|
'unknown');
|
|
}
|
|
};
|
|
exports.AuditInterceptor = AuditInterceptor;
|
|
exports.AuditInterceptor = AuditInterceptor = __decorate([
|
|
(0, common_1.Injectable)(),
|
|
__metadata("design:paramtypes", [audit_service_1.AuditService,
|
|
core_1.Reflector])
|
|
], AuditInterceptor);
|
|
//# sourceMappingURL=audit.interceptor.js.map
|