Audit changes

This commit is contained in:
2026-01-27 21:00:39 -06:00
parent 6b9f6810ab
commit 936471542a
6 changed files with 241 additions and 85 deletions

View File

@@ -1,16 +1,7 @@
/**
* Audit Controller
* Handles HTTP requests for audit log operations
*/
import { Response } from 'express';
import { AuthenticatedRequest } from '../types';
import * as auditService from '../services/audit.service';
/**
* GET /audit-logs
* Get audit logs with filters and pagination (admin only)
*/
export async function getAuditLogs(
req: AuthenticatedRequest,
res: Response
@@ -68,10 +59,6 @@ export async function getAuditLogs(
}
}
/**
* GET /audit-logs/:id
* Get a single audit log by ID (admin only)
*/
export async function getAuditLogById(
req: AuthenticatedRequest,
res: Response
@@ -103,10 +90,6 @@ export async function getAuditLogById(
}
}
/**
* GET /audit-logs/record/:tableName/:recordId
* Get audit logs for a specific record (admin only)
*/
export async function getAuditLogsForRecord(
req: AuthenticatedRequest,
res: Response
@@ -130,10 +113,6 @@ export async function getAuditLogsForRecord(
}
}
/**
* GET /audit-logs/statistics
* Get audit statistics (admin only)
*/
export async function getAuditStatistics(
req: AuthenticatedRequest,
res: Response
@@ -158,10 +137,6 @@ export async function getAuditStatistics(
}
}
/**
* GET /audit-logs/my-activity
* Get current user's own audit logs
*/
export async function getMyActivity(
req: AuthenticatedRequest,
res: Response

View File

@@ -2,6 +2,7 @@ import { Request, Response } from 'express';
import { AuthenticatedRequest } from '../middleware/auth.middleware';
import * as authService from '../services/auth.service';
import { LoginInput, RefreshInput } from '../validators/auth.validator';
import { createAuditLog, getIpAddress, getUserAgent } from '../services/audit.service';
/**
* POST /auth/login
@@ -14,6 +15,19 @@ export async function login(req: Request, res: Response): Promise<void> {
const result = await authService.login(email, password);
createAuditLog({
userId: result.user.id,
userEmail: result.user.email,
userName: result.user.name,
action: 'LOGIN',
tableName: 'users',
recordId: result.user.id,
description: `User logged in successfully`,
ipAddress: getIpAddress(req),
userAgent: getUserAgent(req),
success: true,
}).catch(err => console.error('Failed to log login:', err));
res.status(200).json({
success: true,
data: {
@@ -24,6 +38,22 @@ export async function login(req: Request, res: Response): Promise<void> {
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Login failed';
const { email } = req.body as LoginInput;
if (email) {
createAuditLog({
userId: email,
userEmail: email,
userName: email,
action: 'LOGIN',
tableName: 'users',
description: `Failed login attempt`,
ipAddress: getIpAddress(req),
userAgent: getUserAgent(req),
success: false,
errorMessage: message,
}).catch(err => console.error('Failed to log failed login:', err));
}
// Use 401 for authentication failures
if (message === 'Invalid email or password') {
@@ -89,6 +119,21 @@ export async function logout(req: AuthenticatedRequest, res: Response): Promise<
await authService.logout(userId, refreshToken);
}
if (req.user) {
createAuditLog({
userId: req.user.id,
userEmail: req.user.email,
userName: req.user.role || req.user.email,
action: 'LOGOUT',
tableName: 'users',
recordId: req.user.id,
description: `User logged out`,
ipAddress: getIpAddress(req),
userAgent: getUserAgent(req),
success: true,
}).catch(err => console.error('Failed to log logout:', err));
}
res.status(200).json({
success: true,
message: 'Logout successful',

View File

@@ -7,6 +7,7 @@ import helmet from 'helmet';
import routes from './routes';
import logger from './utils/logger';
import { testConnection } from './config/database';
import { auditMiddleware } from './middleware/audit.middleware';
const app: Application = express();
@@ -42,6 +43,9 @@ app.use(cors({
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Audit logging middleware (before routes)
app.use(auditMiddleware);
// Health check endpoint
app.get('/health', (_req: Request, res: Response) => {
res.status(200).json({

View File

@@ -0,0 +1,191 @@
import { Request, Response, NextFunction } from 'express';
import { createAuditLog, getIpAddress, getUserAgent, type AuditAction } from '../services/audit.service';
import { AuthenticatedRequest } from '../types';
function methodToAction(method: string): AuditAction {
switch (method.toUpperCase()) {
case 'POST':
return 'CREATE';
case 'PUT':
case 'PATCH':
return 'UPDATE';
case 'DELETE':
return 'DELETE';
case 'GET':
return 'READ';
default:
return 'READ';
}
}
function extractTableName(path: string): string {
const segments = path.replace(/^\/api\//, '').split('/');
let tableName = segments[0] || 'unknown';
const tableMapping: Record<string, string> = {
'auth': 'users',
'webhooks': 'webhooks',
'bulk-upload': 'bulk_operations',
'audit-logs': 'audit_logs',
};
return tableMapping[tableName] || tableName;
}
function extractRecordId(req: Request): string | undefined {
if (req.params.id) {
return req.params.id;
}
const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
const match = req.path.match(uuidRegex);
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));
}
export function auditMiddleware(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void {
console.log(`[AUDIT-MIDDLEWARE] Request received: ${req.method} ${req.path}`);
if (shouldExclude(req.path)) {
console.log(`[AUDIT-MIDDLEWARE] Path excluded: ${req.path}`);
next();
return;
}
if (!req.path.startsWith('/api')) {
console.log(`[AUDIT-MIDDLEWARE] Not an API path: ${req.path}`);
next();
return;
}
console.log(`[AUDIT-MIDDLEWARE] Setting up audit tracking for: ${req.method} ${req.path}`);
const originalSend = res.send;
const originalJson = res.json;
let responseBody: any;
res.send = function (body: any): Response {
responseBody = body;
return originalSend.call(this, body);
};
res.json = function (body: any): Response {
responseBody = body;
return originalJson.call(this, body);
};
res.on('finish', async () => {
try {
console.log('🔍 [Audit] Response finished:', {
path: req.path,
method: req.method,
user: req.user ? req.user.email : 'NO USER',
statusCode: res.statusCode
});
if (!req.user) {
console.log('⚠️ [Audit] Skipping - no authenticated user');
return;
}
const action = methodToAction(req.method);
const tableName = extractTableName(req.path);
const recordId = extractRecordId(req);
const success = res.statusCode >= 200 && res.statusCode < 400;
let description = `${req.method} ${req.path}`;
if (req.path.includes('/login')) {
description = 'User logged in';
} else if (req.path.includes('/logout')) {
description = 'User logged out';
} else if (req.path.includes('/password')) {
description = 'Password changed';
}
const auditData = {
userId: req.user.userId,
userEmail: req.user.email,
userName: req.user.roleName || req.user.email,
action,
tableName,
recordId,
description,
ipAddress: getIpAddress(req),
userAgent: getUserAgent(req),
success,
newValues: ['POST', 'PUT', 'PATCH'].includes(req.method)
? sanitizeData(req.body)
: undefined,
errorMessage: !success && responseBody?.error
? responseBody.error
: undefined,
};
console.log('✅ [Audit] Creating audit log...', {
action,
tableName,
recordId,
userEmail: req.user.email
});
createAuditLog(auditData)
.then(logId => {
console.log('✅ [Audit] Log created successfully:', logId);
})
.catch(err => {
console.error('❌ [Audit] Failed to create audit log:', err);
console.error(' Audit data was:', JSON.stringify(auditData, null, 2));
});
} catch (error) {
console.error('❌ [Audit] Error in audit middleware:', error);
}
});
next();
}
function sanitizeData(data: any): any {
if (!data || typeof data !== 'object') {
return data;
}
const sanitized = { ...data };
const sensitiveFields = [
'password',
'password_hash',
'currentPassword',
'newPassword',
'token',
'accessToken',
'refreshToken',
'secret',
'api_key',
'apiKey',
];
sensitiveFields.forEach(field => {
if (field in sanitized) {
sanitized[field] = '[REDACTED]';
}
});
return sanitized;
}

View File

@@ -1,19 +1,9 @@
/**
* Audit Routes
* Defines API endpoints for audit log operations
*/
import { Router } from 'express';
import * as auditController from '../controllers/audit.controller';
import { authenticateToken, requireRole } from '../middleware/auth.middleware';
const router = Router();
/**
* GET /audit-logs
* Get all audit logs with filters (admin only)
* Query params: userId, action, tableName, recordId, startDate, endDate, success, page, limit
*/
router.get(
'/',
authenticateToken,
@@ -21,22 +11,12 @@ router.get(
auditController.getAuditLogs
);
/**
* GET /audit-logs/my-activity
* Get current user's own audit logs
* Query params: page, limit
*/
router.get(
'/my-activity',
authenticateToken,
auditController.getMyActivity
);
/**
* GET /audit-logs/statistics
* Get audit statistics (admin only)
* Query params: days
*/
router.get(
'/statistics',
authenticateToken,
@@ -44,10 +24,6 @@ router.get(
auditController.getAuditStatistics
);
/**
* GET /audit-logs/:id
* Get a single audit log by ID (admin only)
*/
router.get(
'/:id',
authenticateToken,
@@ -55,10 +31,6 @@ router.get(
auditController.getAuditLogById
);
/**
* GET /audit-logs/record/:tableName/:recordId
* Get audit logs for a specific record (admin only)
*/
router.get(
'/record/:tableName/:recordId',
authenticateToken,

View File

@@ -1,8 +1,3 @@
/**
* Audit Service
* Handles logging of user actions and system changes
*/
import { query } from '../config/database';
import type { Request } from 'express';
@@ -64,9 +59,6 @@ export interface AuditLogFilters {
limit?: number;
}
/**
* Create an audit log entry
*/
export async function createAuditLog(data: AuditLogData): Promise<string> {
const sql = `
SELECT log_audit(
@@ -105,9 +97,6 @@ export async function createAuditLog(data: AuditLogData): Promise<string> {
return result.rows[0].log_id;
}
/**
* Get audit logs with filters and pagination
*/
export async function getAuditLogs(
filters: AuditLogFilters = {}
): Promise<{ logs: AuditLog[]; total: number }> {
@@ -165,7 +154,6 @@ export async function getAuditLogs(
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
// Get total count
const countSql = `
SELECT COUNT(*) as total
FROM audit_logs
@@ -175,7 +163,6 @@ export async function getAuditLogs(
const countResult = await query(countSql, params);
const total = parseInt(countResult.rows[0].total, 10);
// Get paginated results
const dataSql = `
SELECT *
FROM audit_logs
@@ -192,9 +179,6 @@ export async function getAuditLogs(
};
}
/**
* Get audit log by ID
*/
export async function getAuditLogById(id: string): Promise<AuditLog | null> {
const sql = `
SELECT *
@@ -206,9 +190,6 @@ export async function getAuditLogById(id: string): Promise<AuditLog | null> {
return (result.rows[0] as AuditLog) || null;
}
/**
* Get audit logs for a specific record
*/
export async function getAuditLogsForRecord(
tableName: string,
recordId: string
@@ -224,9 +205,6 @@ export async function getAuditLogsForRecord(
return result.rows as AuditLog[];
}
/**
* Get audit statistics
*/
export async function getAuditStatistics(days: number = 30): Promise<any[]> {
const sql = `
SELECT *
@@ -239,9 +217,6 @@ export async function getAuditStatistics(days: number = 30): Promise<any[]> {
return result.rows;
}
/**
* Helper: Extract IP address from request
*/
export function getIpAddress(req: Request): string | undefined {
return (
(req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ||
@@ -251,18 +226,12 @@ export function getIpAddress(req: Request): string | undefined {
);
}
/**
* Helper: Extract user agent from request
*/
export function getUserAgent(req: Request): string | undefined {
return req.headers['user-agent'];
}
/**
* Helper: Create audit log from authenticated request
*/
export async function logAction(
req: any, // AuthenticatedRequest
req: any,
action: AuditAction,
tableName: string,
options: {