UNPKG

@kitstack/nest-powertools

Version:

A comprehensive collection of NestJS powertools, decorators, and utilities to supercharge your backend development

436 lines 17 kB
"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); }; var MongoAuditStorage_1, InMemoryAuditStorage_1, FileAuditStorage_1, AuditInterceptor_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.AuditService = exports.AuditDelete = exports.AuditUpdate = exports.AuditRead = exports.AuditCreate = exports.Audit = exports.AuditInterceptor = exports.FileAuditStorage = exports.InMemoryAuditStorage = exports.MongoAuditStorage = void 0; exports.getAuditStorageFromConfig = getAuditStorageFromConfig; const common_1 = require("@nestjs/common"); const common_2 = require("@nestjs/common"); const operators_1 = require("rxjs/operators"); const rxjs_1 = require("rxjs"); const enums_1 = require("../types/enums"); const powertools_config_1 = require("../config/powertools.config"); const fs = require("fs/promises"); let MongoAuditStorage = MongoAuditStorage_1 = class MongoAuditStorage { constructor() { this.logger = new common_1.Logger(MongoAuditStorage_1.name); } async save(entry) { try { this.logger.log(`Audit log saved: ${entry.action} by user ${entry.userId}`, enums_1.LogContext.AUDIT); console.log('Audit Entry:', JSON.stringify(entry, null, 2)); } catch (error) { this.logger.error('Failed to save audit log', error, enums_1.LogContext.AUDIT); } } async find(filters, pagination) { return { data: [], pagination: { total: 0, page: pagination?.page || 1, limit: pagination?.limit || 10, totalPages: 0, hasNext: false, hasPrevious: false, }, status: enums_1.ResponseStatus.SUCCESS, timestamp: new Date().toISOString(), }; } async findById(id) { return null; } async count(filters) { return 0; } async delete(id) { return true; } async deleteMany(filters) { return 0; } }; exports.MongoAuditStorage = MongoAuditStorage; exports.MongoAuditStorage = MongoAuditStorage = MongoAuditStorage_1 = __decorate([ (0, common_1.Injectable)() ], MongoAuditStorage); let InMemoryAuditStorage = InMemoryAuditStorage_1 = class InMemoryAuditStorage { constructor() { this.logs = []; this.logger = new common_1.Logger(InMemoryAuditStorage_1.name); } async save(entry) { entry.id = `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; this.logs.push(entry); this.logger.log(`Audit log saved: ${entry.action} by user ${entry.userId}`, enums_1.LogContext.AUDIT); } async find(filters, pagination) { const filteredLogs = this.logs.filter((log) => { return Object.entries(filters).every(([key, value]) => { const logValue = log[key]; return logValue === value; }); }); const page = pagination?.page || 1; const limit = pagination?.limit || 10; const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; const paginatedData = filteredLogs.slice(startIndex, endIndex); return { data: paginatedData, pagination: { total: filteredLogs.length, page, limit, totalPages: Math.ceil(filteredLogs.length / limit), hasNext: endIndex < filteredLogs.length, hasPrevious: page > 1, }, status: enums_1.ResponseStatus.SUCCESS, timestamp: new Date().toISOString(), }; } async findById(id) { return this.logs.find((log) => log.id === id) || null; } async count(filters) { return this.logs.filter((log) => { return Object.entries(filters).every(([key, value]) => { const logValue = log[key]; return logValue === value; }); }).length; } async delete(id) { const index = this.logs.findIndex((log) => log.id === id); if (index > -1) { this.logs.splice(index, 1); return true; } return false; } async deleteMany(filters) { const initialLength = this.logs.length; this.logs = this.logs.filter((log) => { return !Object.entries(filters).every(([key, value]) => { const logValue = log[key]; return logValue === value; }); }); return initialLength - this.logs.length; } getAllLogs() { return [...this.logs]; } clearLogs() { this.logs = []; } }; exports.InMemoryAuditStorage = InMemoryAuditStorage; exports.InMemoryAuditStorage = InMemoryAuditStorage = InMemoryAuditStorage_1 = __decorate([ (0, common_1.Injectable)() ], InMemoryAuditStorage); let FileAuditStorage = FileAuditStorage_1 = class FileAuditStorage { constructor(filePath) { this.logger = new common_1.Logger(FileAuditStorage_1.name); this.filePath = filePath || './audit-logs.json'; } async save(entry) { try { let logs = []; try { const data = await fs.readFile(this.filePath, 'utf-8'); logs = JSON.parse(data); } catch { } logs.push(entry); await fs.writeFile(this.filePath, JSON.stringify(logs, null, 2)); this.logger.log(`Audit log saved to file: ${this.filePath}`); } catch (error) { this.logger.error('Failed to save audit log to file', error); } } async find(filters, pagination) { try { const data = await fs.readFile(this.filePath, 'utf-8'); let logs = JSON.parse(data); logs = logs.filter((log) => Object.entries(filters).every(([k, v]) => log[k] === v)); const page = pagination?.page || 1; const limit = pagination?.limit || 10; const total = logs.length; const totalPages = Math.ceil(total / limit); const paged = logs.slice((page - 1) * limit, page * limit); return { data: paged, pagination: { total, page, limit, totalPages, hasNext: page < totalPages, hasPrevious: page > 1, }, status: enums_1.ResponseStatus.SUCCESS, timestamp: new Date().toISOString(), }; } catch { return { data: [], pagination: { total: 0, page: 1, limit: 10, totalPages: 0, hasNext: false, hasPrevious: false, }, status: enums_1.ResponseStatus.SUCCESS, timestamp: new Date().toISOString(), }; } } async findById(id) { const result = await this.find({}); return result.data.find((log) => log.id === id) || null; } async count(filters) { const result = await this.find(filters); return result.data.length; } async delete(id) { try { const data = await fs.readFile(this.filePath, 'utf-8'); let logs = JSON.parse(data); const initialLength = logs.length; logs = logs.filter((log) => log.id !== id); await fs.writeFile(this.filePath, JSON.stringify(logs, null, 2)); return logs.length < initialLength; } catch { return false; } } async deleteMany(filters) { try { const data = await fs.readFile(this.filePath, 'utf-8'); let logs = JSON.parse(data); const initialLength = logs.length; logs = logs.filter((log) => !Object.entries(filters).every(([k, v]) => log[k] === v)); await fs.writeFile(this.filePath, JSON.stringify(logs, null, 2)); return initialLength - logs.length; } catch { return 0; } } }; exports.FileAuditStorage = FileAuditStorage; exports.FileAuditStorage = FileAuditStorage = FileAuditStorage_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [String]) ], FileAuditStorage); function getAuditStorageFromConfig() { const configService = powertools_config_1.PowertoolsConfigService.getInstance(); const auditConfig = configService.getFeatureConfig('audit'); const storage = auditConfig?.storage || { type: 'file', filePath: './audit-logs.json', }; if (storage.type === 'mongodb' && storage.mongoUrl) { return new MongoAuditStorage(); } return new FileAuditStorage(storage.filePath); } let AuditInterceptor = AuditInterceptor_1 = class AuditInterceptor { constructor(auditStorage) { this.auditStorage = auditStorage; this.logger = new common_1.Logger(AuditInterceptor_1.name); this.configService = powertools_config_1.PowertoolsConfigService.getInstance(); } intercept(context, next) { const auditConfig = this.getAuditConfig(context); if (!auditConfig || !this.configService.isFeatureEnabled('audit')) { return next.handle(); } const startTime = Date.now(); const request = context.switchToHttp().getRequest(); const user = request.user; if (auditConfig.condition && !auditConfig.condition(context, user)) { return next.handle(); } const auditEntry = { userId: user?.id || user?.sub, user, action: auditConfig.action, resource: auditConfig.resource || this.extractResourceFromContext(context), resourceId: this.extractResourceId(request), level: auditConfig.level || enums_1.AuditLevel.MEDIUM, ipAddress: request.ip || request.connection?.remoteAddress, userAgent: request.headers['user-agent'], timestamp: new Date(), endpoint: request.url, method: request.method, requestBody: auditConfig.includeRequestBody ? this.sanitizeData(request.body, auditConfig.excludeFields) : undefined, metadata: auditConfig.customMetadata ? auditConfig.customMetadata(context, user) : {}, }; return next.handle().pipe((0, operators_1.tap)((response) => { auditEntry.duration = Date.now() - startTime; auditEntry.responseStatus = context .switchToHttp() .getResponse().statusCode; auditEntry.success = true; if (auditConfig.includeResponseBody) { auditEntry.metadata = { ...auditEntry.metadata, responseBody: this.sanitizeData(response, auditConfig.excludeFields), }; } this.auditStorage.save(auditEntry).catch((error) => { this.logger.error('Failed to save audit log', error, enums_1.LogContext.AUDIT); }); }), (0, operators_1.catchError)((error) => { auditEntry.duration = Date.now() - startTime; auditEntry.responseStatus = error.status || 500; auditEntry.success = false; auditEntry.errorMessage = error.message; auditEntry.metadata = { ...auditEntry.metadata, error: error.message, stack: error.stack, }; this.auditStorage.save(auditEntry).catch((saveError) => { this.logger.error('Failed to save audit log for error case', saveError, enums_1.LogContext.AUDIT); }); return (0, rxjs_1.throwError)(() => error); })); } getAuditConfig(context) { const handler = context.getHandler(); const auditConfig = Reflect.getMetadata('audit-config', handler); return auditConfig || null; } extractResourceFromContext(context) { const className = context.getClass().name; const handlerName = context.getHandler().name; return `${className}.${handlerName}`; } extractResourceId(request) { return request.params?.id || request.params?.userId || request.body?.id; } sanitizeData(data, excludeFields = []) { if (!data || typeof data !== 'object') { return data; } const sanitized = { ...data }; const globalConfig = this.configService.getFeatureConfig('audit'); const defaultExcludeFields = globalConfig?.excludeFields || [ 'password', 'token', 'secret', 'key', ]; const fieldsToExclude = [...defaultExcludeFields, ...excludeFields]; fieldsToExclude.forEach((field) => { if (sanitized[field]) { sanitized[field] = '[REDACTED]'; } }); return sanitized; } }; exports.AuditInterceptor = AuditInterceptor; exports.AuditInterceptor = AuditInterceptor = AuditInterceptor_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [Object]) ], AuditInterceptor); const Audit = (action, options = {}) => (0, common_2.SetMetadata)('audit-config', { action, ...options }); exports.Audit = Audit; const AuditCreate = (resource, options) => (0, exports.Audit)(enums_1.AuditAction.CREATE, { resource, includeRequestBody: true, ...options, }); exports.AuditCreate = AuditCreate; const AuditRead = (resource, options) => (0, exports.Audit)(enums_1.AuditAction.READ, { resource, ...options }); exports.AuditRead = AuditRead; const AuditUpdate = (resource, options) => (0, exports.Audit)(enums_1.AuditAction.UPDATE, { resource, includeRequestBody: true, ...options, }); exports.AuditUpdate = AuditUpdate; const AuditDelete = (resource, options) => (0, exports.Audit)(enums_1.AuditAction.DELETE, { resource, ...options }); exports.AuditDelete = AuditDelete; let AuditService = class AuditService { constructor(auditStorage) { this.auditStorage = auditStorage; } async getAuditLogs(filters = {}, pagination) { return this.auditStorage.find(filters, pagination); } async getAuditLogById(id) { return this.auditStorage.findById(id); } async getUserAuditLogs(userId, pagination) { return this.auditStorage.find({ userId }, pagination); } async getResourceAuditLogs(resource, resourceId, pagination) { const filters = { resource }; if (resourceId) { filters.resourceId = resourceId; } return this.auditStorage.find(filters, pagination); } async getAuditStats() { const allLogs = await this.auditStorage.find({}); const logs = allLogs.data; const totalLogs = await this.auditStorage.count({}); const actionBreakdown = logs.reduce((acc, entry) => { acc[entry.action] = (acc[entry.action] || 0) + 1; return acc; }, {}); const userBreakdown = logs.reduce((acc, entry) => { if (entry.userId) { acc[entry.userId] = (acc[entry.userId] || 0) + 1; } return acc; }, {}); const recentActivity = logs .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) .slice(0, 10); return { totalLogs, actionBreakdown, userBreakdown, recentActivity, }; } async deleteAuditLog(id) { return this.auditStorage.delete(id); } async deleteAuditLogs(filters) { return this.auditStorage.deleteMany(filters); } }; exports.AuditService = AuditService; exports.AuditService = AuditService = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [Object]) ], AuditService); //# sourceMappingURL=audit-logging.hook.js.map