@kitstack/nest-powertools
Version:
A comprehensive collection of NestJS powertools, decorators, and utilities to supercharge your backend development
436 lines • 17 kB
JavaScript
;
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