Implemented audit log system
This commit is contained in:
53
api/migrations/20260212165353-audit-log.js
Normal file
53
api/migrations/20260212165353-audit-log.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var dbm;
|
||||||
|
var type;
|
||||||
|
var seed;
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
var Promise;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We receive the dbmigrate dependency from dbmigrate initially.
|
||||||
|
* This enables us to not have to rely on NODE_PATH.
|
||||||
|
*/
|
||||||
|
exports.setup = function(options, seedLink) {
|
||||||
|
dbm = options.dbmigrate;
|
||||||
|
type = dbm.dataType;
|
||||||
|
seed = seedLink;
|
||||||
|
Promise = options.Promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.up = function(db) {
|
||||||
|
var filePath = path.join(__dirname, 'sqls', '20260212165353-audit-log-up.sql');
|
||||||
|
return new Promise( function( resolve, reject ) {
|
||||||
|
fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){
|
||||||
|
if (err) return reject(err);
|
||||||
|
console.log('received data: ' + data);
|
||||||
|
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
return db.runSql(data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db) {
|
||||||
|
var filePath = path.join(__dirname, 'sqls', '20260212165353-audit-log-down.sql');
|
||||||
|
return new Promise( function( resolve, reject ) {
|
||||||
|
fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){
|
||||||
|
if (err) return reject(err);
|
||||||
|
console.log('received data: ' + data);
|
||||||
|
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
return db.runSql(data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports._meta = {
|
||||||
|
"version": 1
|
||||||
|
};
|
||||||
1
api/migrations/sqls/20260212165353-audit-log-down.sql
Normal file
1
api/migrations/sqls/20260212165353-audit-log-down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/* Replace with your SQL commands */
|
||||||
17
api/migrations/sqls/20260212165353-audit-log-up.sql
Normal file
17
api/migrations/sqls/20260212165353-audit-log-up.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
CREATE TABLE audit_log (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
-- "area.action" (e.g., 'calendarEvent.create', 'member.update_rank')
|
||||||
|
action_type VARCHAR(100) NOT NULL,
|
||||||
|
-- The JSON blob containing detailed information
|
||||||
|
payload JSON DEFAULT NULL,
|
||||||
|
-- Identifying the actor
|
||||||
|
created_by INT,
|
||||||
|
-- The ID of the resource being acted upon
|
||||||
|
target_id INT DEFAULT NULL,
|
||||||
|
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT fk_created_by FOREIGN KEY (created_by) REFERENCES members(id) ON DELETE
|
||||||
|
SET NULL,
|
||||||
|
INDEX idx_action (action_type),
|
||||||
|
INDEX idx_target (target_id)
|
||||||
|
);
|
||||||
50
api/src/services/logging/auditLog.ts
Normal file
50
api/src/services/logging/auditLog.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import pool from "../../db";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
|
||||||
|
export type AuditArea = 'member' | 'calendar' | 'unit' | 'auth' | 'admin' | 'application';
|
||||||
|
|
||||||
|
export interface AuditContext {
|
||||||
|
actorId: number; // The person doing the action (created_by)
|
||||||
|
targetId?: number; // The ID of the thing being changed (target_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuditLogger {
|
||||||
|
async record(
|
||||||
|
area: AuditArea,
|
||||||
|
action: string,
|
||||||
|
context: AuditContext,
|
||||||
|
data: Record<string, any> = {} // Already optional with default {}
|
||||||
|
) {
|
||||||
|
const actionType = `${area}.${action}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO audit_log (action_type, payload, target_id, created_by)
|
||||||
|
VALUES (?, ?, ?, ?)`, // Fixed: removed extra comma/placeholder
|
||||||
|
[
|
||||||
|
actionType,
|
||||||
|
JSON.stringify(data),
|
||||||
|
context.targetId || null,
|
||||||
|
context.actorId,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('audit', `AUDIT_FAILURE: Failed to log ${actionType}`, { error: err });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Making data optional using '?' and default parameter
|
||||||
|
member(action: 'update_rank' | 'status_change' | 'create', context: AuditContext, data: any = {}) {
|
||||||
|
return this.record('member', action, context, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
calendar(action: 'event_signup' | 'event_create' | 'attendance', context: AuditContext, data: any = {}) {
|
||||||
|
return this.record('calendar', action, context, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
application(action: 'created' | 'approved' | 'denied' | 'restarted', context: AuditContext, data: any = {}) {
|
||||||
|
return this.record('application', action, context, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const audit = new AuditLogger();
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||||
export type LogDepth = 'normal' | 'verbose' | 'profiling';
|
export type LogDepth = 'normal' | 'verbose' | 'profiling';
|
||||||
export type LogType = 'http' | 'app' | 'auth' | 'profiling';
|
export type LogType = 'http' | 'app' | 'auth' | 'profiling' | 'audit';
|
||||||
|
|
||||||
export interface LogHeader {
|
export interface LogHeader {
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user