This commit is contained in:
2026-02-01 18:30:28 -06:00
parent b5ea12dd27
commit 6c02bd5448
5 changed files with 110 additions and 272 deletions

View File

@@ -18,31 +18,6 @@ function methodToAction(method: string): AuditAction {
}
}
function extractSection(path: string): string {
const cleanPath = path.replace(/^\/api\//, '').replace(/^\//, '');
const segments = cleanPath.split('/').filter(s => s.length > 0);
if (segments.length === 0) return 'general';
const sectionMapping: Record<string, string> = {
'auth': 'authentication',
'me': 'profile',
'users': 'user-management',
'roles': 'role-management',
'meters': 'meters',
'concentrators': 'concentrators',
'gateways': 'gateways',
'devices': 'devices',
'projects': 'projects',
'readings': 'readings',
'bulk-upload': 'bulk-operations',
'audit-logs': 'audit',
'webhooks': 'webhooks',
};
return sectionMapping[segments[0]] || segments[0];
}
function extractTableName(path: string): string {
const cleanPath = path.replace(/^\/api\//, '').replace(/^\//, '');
const segments = cleanPath.split('/').filter(s => s.length > 0);
@@ -52,48 +27,22 @@ function extractTableName(path: string): string {
}
const baseTableMapping: Record<string, string> = {
'auth': 'users',
'me': 'users',
'users': 'users',
'roles': 'roles',
'meters': 'meters',
'concentrators': 'concentrators',
'gateways': 'gateways',
'devices': 'devices',
'projects': 'projects',
'readings': 'readings',
'webhooks': 'webhooks',
'bulk-upload': 'bulk_operations',
'audit-logs': 'audit_logs',
};
const operations = new Set([
'password',
'stats',
'health',
'template',
'summary',
'statistics',
'my-activity',
'record',
'refresh',
'logout',
'uplink',
'join',
'ack',
'dev-eui',
]);
const nestedResourceMapping: Record<string, string> = {
'meters/readings': 'readings',
'concentrators/meters': 'meters',
'gateways/devices': 'devices',
'projects/meters': 'meters',
'projects/concentrators': 'concentrators',
};
const firstSegment = segments[0];
const baseTable = baseTableMapping[firstSegment] || firstSegment;
const baseTable = baseTableMapping[firstSegment];
if (!baseTable) {
return 'unknown';
}
if (segments.length === 1) {
return baseTable;
@@ -109,36 +58,9 @@ function extractTableName(path: string): string {
return nestedResourceMapping[nestedKey];
}
if (operations.has(thirdSegment)) {
return baseTable;
}
if (baseTableMapping[thirdSegment] || isPluralResourceName(thirdSegment)) {
return baseTableMapping[thirdSegment] || thirdSegment;
}
return baseTable;
}
if (firstSegment === 'bulk-upload' && secondSegment) {
return baseTableMapping[secondSegment] || secondSegment;
}
if (firstSegment === 'audit-logs') {
if (secondSegment === 'record' && segments.length >= 4) {
return segments[3];
}
return 'audit_logs';
}
if (firstSegment === 'auth') {
return 'users';
}
if (firstSegment === 'devices' && secondSegment === 'dev-eui') {
return 'devices';
}
if (segments.length === 2 && isUUID(secondSegment)) {
return baseTable;
}
@@ -151,10 +73,6 @@ function isUUID(str: string): boolean {
return uuidRegex.test(str) || /^\d+$/.test(str);
}
function isPluralResourceName(str: string): boolean {
return str.endsWith('s') || str.endsWith('ies') || str.includes('-');
}
function extractRecordId(req: Request): string | undefined {
if (req.params.id) {
return req.params.id;
@@ -166,23 +84,10 @@ function extractRecordId(req: Request): string | undefined {
return match ? match[0] : undefined;
}
const EXCLUDED_PATHS = [
'/api/auth/refresh',
'/api/audit-logs',
'/webhooks',
'/health',
];
function shouldExclude(path: string): boolean {
return EXCLUDED_PATHS.some(excluded => path.startsWith(excluded));
}
function generateDescription(
fullPath: string,
action: AuditAction,
tableName: string,
recordId?: string,
section?: string
recordId?: string
): string {
const actionDescriptions: Record<AuditAction, string> = {
'CREATE': 'Created',
@@ -198,55 +103,17 @@ function generateDescription(
};
const tableLabels: Record<string, string> = {
'users': 'user',
'roles': 'role',
'meters': 'meter',
'concentrators': 'concentrator',
'gateways': 'gateway',
'devices': 'device',
'projects': 'project',
'readings': 'reading',
'bulk_operations': 'bulk operation',
};
const actionLabel = actionDescriptions[action] || action;
const tableLabel = tableLabels[tableName] || tableName;
if (fullPath === '/api/me' || fullPath === '/me' || fullPath.endsWith('/auth/me')) {
return 'Viewed own profile';
}
if (fullPath.includes('/users') && action === 'READ' && !recordId) {
return 'Viewed users list';
}
if (fullPath.includes('/meters') && action === 'READ' && !recordId) {
return 'Viewed meters list';
}
if (fullPath.includes('/concentrators') && action === 'READ' && !recordId) {
return 'Viewed concentrators list';
}
if (fullPath.includes('/projects') && action === 'READ' && !recordId) {
return 'Viewed projects list';
}
if (fullPath.includes('/readings') && action === 'READ' && !recordId) {
return 'Viewed readings list';
}
if (recordId) {
if (tableName === 'unknown' && section) {
return `${actionLabel} record in ${section} (ID: ${recordId.substring(0, 8)}...)`;
}
return `${actionLabel} ${tableLabel} (ID: ${recordId.substring(0, 8)}...)`;
}
if (tableName === 'unknown' && section) {
return `${actionLabel} in ${section} section`;
}
return `${actionLabel} ${tableLabel}`;
}
@@ -256,11 +123,6 @@ export function auditMiddleware(
next: NextFunction
): void {
if (shouldExclude(req.path)) {
next();
return;
}
if (!req.path.startsWith('/api')) {
next();
return;
@@ -286,24 +148,18 @@ export function auditMiddleware(
return;
}
const action = methodToAction(req.method);
const fullPath = (req.originalUrl || req.url).split('?')[0];
const section = extractSection(fullPath);
const tableName = extractTableName(fullPath);
const allowedTables = ['concentrators', 'meters'];
if (!allowedTables.includes(tableName)) {
return;
}
const action = methodToAction(req.method);
const recordId = extractRecordId(req);
const success = res.statusCode >= 200 && res.statusCode < 400;
let description = generateDescription(fullPath, action, tableName, recordId, section);
if (fullPath.includes('/login')) {
description = 'User logged in successfully';
} else if (fullPath.includes('/logout')) {
description = 'User logged out';
} else if (fullPath.includes('/password')) {
description = 'Password changed';
} else if (fullPath.includes('/bulk-upload')) {
description = `Bulk upload for ${tableName}`;
}
const description = generateDescription(action, tableName, recordId);
let newValues: unknown = undefined;
let oldValues: unknown = undefined;