audit-log #190
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 LogDepth = 'normal' | 'verbose' | 'profiling';
|
||||
export type LogType = 'http' | 'app' | 'auth' | 'profiling';
|
||||
export type LogType = 'http' | 'app' | 'auth' | 'profiling' | 'audit';
|
||||
|
||||
export interface LogHeader {
|
||||
timestamp: string;
|
||||
|
||||
Reference in New Issue
Block a user