audit logic
This commit is contained in:
213
water-api/src/controllers/audit.controller.ts
Normal file
213
water-api/src/controllers/audit.controller.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 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
|
||||
): Promise<void> {
|
||||
try {
|
||||
const {
|
||||
userId,
|
||||
action,
|
||||
tableName,
|
||||
recordId,
|
||||
startDate,
|
||||
endDate,
|
||||
success,
|
||||
page = '1',
|
||||
limit = '50',
|
||||
} = req.query;
|
||||
|
||||
const filters: auditService.AuditLogFilters = {
|
||||
userId: userId as string,
|
||||
action: action as auditService.AuditAction,
|
||||
tableName: tableName as string,
|
||||
recordId: recordId as string,
|
||||
startDate: startDate ? new Date(startDate as string) : undefined,
|
||||
endDate: endDate ? new Date(endDate as string) : undefined,
|
||||
success: success === 'true' ? true : success === 'false' ? false : undefined,
|
||||
page: parseInt(page as string, 10),
|
||||
limit: parseInt(limit as string, 10),
|
||||
};
|
||||
|
||||
const result = await auditService.getAuditLogs(filters);
|
||||
|
||||
const totalPages = Math.ceil(result.total / filters.limit!);
|
||||
const hasNextPage = filters.page! < totalPages;
|
||||
const hasPreviousPage = filters.page! > 1;
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Audit logs retrieved successfully',
|
||||
data: result.logs,
|
||||
pagination: {
|
||||
page: filters.page,
|
||||
limit: filters.limit,
|
||||
total: result.total,
|
||||
totalPages,
|
||||
hasNextPage,
|
||||
hasPreviousPage,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching audit logs:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || 'Failed to fetch audit logs',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /audit-logs/:id
|
||||
* Get a single audit log by ID (admin only)
|
||||
*/
|
||||
export async function getAuditLogById(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const log = await auditService.getAuditLogById(id);
|
||||
|
||||
if (!log) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Audit log not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Audit log retrieved successfully',
|
||||
data: log,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching audit log:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || 'Failed to fetch audit log',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /audit-logs/record/:tableName/:recordId
|
||||
* Get audit logs for a specific record (admin only)
|
||||
*/
|
||||
export async function getAuditLogsForRecord(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { tableName, recordId } = req.params;
|
||||
|
||||
const logs = await auditService.getAuditLogsForRecord(tableName, recordId);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Audit logs retrieved successfully',
|
||||
data: logs,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching audit logs for record:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || 'Failed to fetch audit logs for record',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /audit-logs/statistics
|
||||
* Get audit statistics (admin only)
|
||||
*/
|
||||
export async function getAuditStatistics(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { days = '30' } = req.query;
|
||||
const daysNum = parseInt(days as string, 10);
|
||||
|
||||
const stats = await auditService.getAuditStatistics(daysNum);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Audit statistics retrieved successfully',
|
||||
data: stats,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching audit statistics:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || 'Failed to fetch audit statistics',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /audit-logs/my-activity
|
||||
* Get current user's own audit logs
|
||||
*/
|
||||
export async function getMyActivity(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const user = req.user;
|
||||
if (!user) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: 'User not authenticated',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { page = '1', limit = '50' } = req.query;
|
||||
|
||||
const filters: auditService.AuditLogFilters = {
|
||||
userId: user.userId,
|
||||
page: parseInt(page as string, 10),
|
||||
limit: parseInt(limit as string, 10),
|
||||
};
|
||||
|
||||
const result = await auditService.getAuditLogs(filters);
|
||||
|
||||
const totalPages = Math.ceil(result.total / filters.limit!);
|
||||
const hasNextPage = filters.page! < totalPages;
|
||||
const hasPreviousPage = filters.page! > 1;
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Your activity logs retrieved successfully',
|
||||
data: result.logs,
|
||||
pagination: {
|
||||
page: filters.page,
|
||||
limit: filters.limit,
|
||||
total: result.total,
|
||||
totalPages,
|
||||
hasNextPage,
|
||||
hasPreviousPage,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching user activity:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || 'Failed to fetch activity logs',
|
||||
});
|
||||
}
|
||||
}
|
||||
69
water-api/src/routes/audit.routes.ts
Normal file
69
water-api/src/routes/audit.routes.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 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,
|
||||
requireRole('ADMIN'),
|
||||
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,
|
||||
requireRole('ADMIN'),
|
||||
auditController.getAuditStatistics
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /audit-logs/:id
|
||||
* Get a single audit log by ID (admin only)
|
||||
*/
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticateToken,
|
||||
requireRole('ADMIN'),
|
||||
auditController.getAuditLogById
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /audit-logs/record/:tableName/:recordId
|
||||
* Get audit logs for a specific record (admin only)
|
||||
*/
|
||||
router.get(
|
||||
'/record/:tableName/:recordId',
|
||||
authenticateToken,
|
||||
requireRole('ADMIN'),
|
||||
auditController.getAuditLogsForRecord
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -12,6 +12,7 @@ import roleRoutes from './role.routes';
|
||||
import ttsRoutes from './tts.routes';
|
||||
import readingRoutes from './reading.routes';
|
||||
import bulkUploadRoutes from './bulk-upload.routes';
|
||||
import auditRoutes from './audit.routes';
|
||||
|
||||
// Create main router
|
||||
const router = Router();
|
||||
@@ -130,4 +131,14 @@ router.use('/readings', readingRoutes);
|
||||
*/
|
||||
router.use('/bulk-upload', bulkUploadRoutes);
|
||||
|
||||
/**
|
||||
* Audit routes:
|
||||
* - GET /audit-logs - List all audit logs (admin only)
|
||||
* - GET /audit-logs/my-activity - Get current user's activity
|
||||
* - GET /audit-logs/statistics - Get audit statistics (admin only)
|
||||
* - GET /audit-logs/:id - Get audit log by ID (admin only)
|
||||
* - GET /audit-logs/record/:tableName/:recordId - Get logs for specific record (admin only)
|
||||
*/
|
||||
router.use('/audit-logs', auditRoutes);
|
||||
|
||||
export default router;
|
||||
|
||||
298
water-api/src/services/audit.service.ts
Normal file
298
water-api/src/services/audit.service.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* Audit Service
|
||||
* Handles logging of user actions and system changes
|
||||
*/
|
||||
|
||||
import { query } from '../config/database';
|
||||
import type { Request } from 'express';
|
||||
|
||||
export type AuditAction =
|
||||
| 'CREATE'
|
||||
| 'UPDATE'
|
||||
| 'DELETE'
|
||||
| 'LOGIN'
|
||||
| 'LOGOUT'
|
||||
| 'READ'
|
||||
| 'EXPORT'
|
||||
| 'BULK_UPLOAD'
|
||||
| 'STATUS_CHANGE'
|
||||
| 'PERMISSION_CHANGE';
|
||||
|
||||
export interface AuditLogData {
|
||||
userId: string;
|
||||
userEmail: string;
|
||||
userName: string;
|
||||
action: AuditAction;
|
||||
tableName: string;
|
||||
recordId?: string;
|
||||
oldValues?: Record<string, any>;
|
||||
newValues?: Record<string, any>;
|
||||
description?: string;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
success?: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface AuditLog {
|
||||
id: string;
|
||||
user_id: string | null;
|
||||
user_email: string;
|
||||
user_name: string;
|
||||
action: AuditAction;
|
||||
table_name: string;
|
||||
record_id: string | null;
|
||||
old_values: Record<string, any> | null;
|
||||
new_values: Record<string, any> | null;
|
||||
description: string | null;
|
||||
ip_address: string | null;
|
||||
user_agent: string | null;
|
||||
success: boolean;
|
||||
error_message: string | null;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export interface AuditLogFilters {
|
||||
userId?: string;
|
||||
action?: AuditAction;
|
||||
tableName?: string;
|
||||
recordId?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
success?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an audit log entry
|
||||
*/
|
||||
export async function createAuditLog(data: AuditLogData): Promise<string> {
|
||||
const sql = `
|
||||
SELECT log_audit(
|
||||
$1::UUID,
|
||||
$2,
|
||||
$3,
|
||||
$4::audit_action,
|
||||
$5,
|
||||
$6::UUID,
|
||||
$7::JSONB,
|
||||
$8::JSONB,
|
||||
$9,
|
||||
$10::INET,
|
||||
$11,
|
||||
$12,
|
||||
$13
|
||||
) as log_id
|
||||
`;
|
||||
|
||||
const result = await query(sql, [
|
||||
data.userId,
|
||||
data.userEmail,
|
||||
data.userName,
|
||||
data.action,
|
||||
data.tableName,
|
||||
data.recordId || null,
|
||||
data.oldValues ? JSON.stringify(data.oldValues) : null,
|
||||
data.newValues ? JSON.stringify(data.newValues) : null,
|
||||
data.description || null,
|
||||
data.ipAddress || null,
|
||||
data.userAgent || null,
|
||||
data.success !== undefined ? data.success : true,
|
||||
data.errorMessage || null,
|
||||
]);
|
||||
|
||||
return result.rows[0].log_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit logs with filters and pagination
|
||||
*/
|
||||
export async function getAuditLogs(
|
||||
filters: AuditLogFilters = {}
|
||||
): Promise<{ logs: AuditLog[]; total: number }> {
|
||||
const {
|
||||
userId,
|
||||
action,
|
||||
tableName,
|
||||
recordId,
|
||||
startDate,
|
||||
endDate,
|
||||
success,
|
||||
page = 1,
|
||||
limit = 50,
|
||||
} = filters;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (userId) {
|
||||
conditions.push(`user_id = $${paramIndex++}`);
|
||||
params.push(userId);
|
||||
}
|
||||
|
||||
if (action) {
|
||||
conditions.push(`action = $${paramIndex++}`);
|
||||
params.push(action);
|
||||
}
|
||||
|
||||
if (tableName) {
|
||||
conditions.push(`table_name = $${paramIndex++}`);
|
||||
params.push(tableName);
|
||||
}
|
||||
|
||||
if (recordId) {
|
||||
conditions.push(`record_id = $${paramIndex++}`);
|
||||
params.push(recordId);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
conditions.push(`created_at >= $${paramIndex++}`);
|
||||
params.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
conditions.push(`created_at <= $${paramIndex++}`);
|
||||
params.push(endDate);
|
||||
}
|
||||
|
||||
if (success !== undefined) {
|
||||
conditions.push(`success = $${paramIndex++}`);
|
||||
params.push(success);
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
|
||||
// Get total count
|
||||
const countSql = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM audit_logs
|
||||
${whereClause}
|
||||
`;
|
||||
|
||||
const countResult = await query(countSql, params);
|
||||
const total = parseInt(countResult.rows[0].total, 10);
|
||||
|
||||
// Get paginated results
|
||||
const dataSql = `
|
||||
SELECT *
|
||||
FROM audit_logs
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $${paramIndex++} OFFSET $${paramIndex}
|
||||
`;
|
||||
|
||||
const dataResult = await query(dataSql, [...params, limit, offset]);
|
||||
|
||||
return {
|
||||
logs: dataResult.rows as AuditLog[],
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit log by ID
|
||||
*/
|
||||
export async function getAuditLogById(id: string): Promise<AuditLog | null> {
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM audit_logs
|
||||
WHERE id = $1
|
||||
`;
|
||||
|
||||
const result = await query(sql, [id]);
|
||||
return (result.rows[0] as AuditLog) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit logs for a specific record
|
||||
*/
|
||||
export async function getAuditLogsForRecord(
|
||||
tableName: string,
|
||||
recordId: string
|
||||
): Promise<AuditLog[]> {
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM audit_logs
|
||||
WHERE table_name = $1 AND record_id = $2
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
|
||||
const result = await query(sql, [tableName, recordId]);
|
||||
return result.rows as AuditLog[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit statistics
|
||||
*/
|
||||
export async function getAuditStatistics(days: number = 30): Promise<any[]> {
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM audit_statistics
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '${days} days'
|
||||
ORDER BY date DESC, action_count DESC
|
||||
`;
|
||||
|
||||
const result = await query(sql);
|
||||
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() ||
|
||||
(req.headers['x-real-ip'] as string) ||
|
||||
req.socket.remoteAddress ||
|
||||
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
|
||||
action: AuditAction,
|
||||
tableName: string,
|
||||
options: {
|
||||
recordId?: string;
|
||||
oldValues?: Record<string, any>;
|
||||
newValues?: Record<string, any>;
|
||||
description?: string;
|
||||
success?: boolean;
|
||||
errorMessage?: string;
|
||||
} = {}
|
||||
): Promise<string> {
|
||||
const user = req.user;
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found in request for audit log');
|
||||
}
|
||||
|
||||
return createAuditLog({
|
||||
userId: user.userId,
|
||||
userEmail: user.email,
|
||||
userName: user.roleName || user.email,
|
||||
action,
|
||||
tableName,
|
||||
recordId: options.recordId,
|
||||
oldValues: options.oldValues,
|
||||
newValues: options.newValues,
|
||||
description: options.description,
|
||||
ipAddress: getIpAddress(req),
|
||||
userAgent: getUserAgent(req),
|
||||
success: options.success,
|
||||
errorMessage: options.errorMessage,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user