Audit changes
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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({
|
||||
|
||||
191
water-api/src/middleware/audit.middleware.ts
Normal file
191
water-api/src/middleware/audit.middleware.ts
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user