From 34ce7d1e14bd5350f014963cf3813fa15e39d207 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Thu, 12 Feb 2026 14:48:19 -0500 Subject: [PATCH 1/3] Implemented audit log system --- api/migrations/20260212165353-audit-log.js | 53 +++++++++++++++++++ .../sqls/20260212165353-audit-log-down.sql | 1 + .../sqls/20260212165353-audit-log-up.sql | 17 ++++++ api/src/services/logging/auditLog.ts | 50 +++++++++++++++++ api/src/services/logging/logger.ts | 2 +- 5 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 api/migrations/20260212165353-audit-log.js create mode 100644 api/migrations/sqls/20260212165353-audit-log-down.sql create mode 100644 api/migrations/sqls/20260212165353-audit-log-up.sql create mode 100644 api/src/services/logging/auditLog.ts diff --git a/api/migrations/20260212165353-audit-log.js b/api/migrations/20260212165353-audit-log.js new file mode 100644 index 0000000..951ad5a --- /dev/null +++ b/api/migrations/20260212165353-audit-log.js @@ -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 +}; diff --git a/api/migrations/sqls/20260212165353-audit-log-down.sql b/api/migrations/sqls/20260212165353-audit-log-down.sql new file mode 100644 index 0000000..44f074e --- /dev/null +++ b/api/migrations/sqls/20260212165353-audit-log-down.sql @@ -0,0 +1 @@ +/* Replace with your SQL commands */ \ No newline at end of file diff --git a/api/migrations/sqls/20260212165353-audit-log-up.sql b/api/migrations/sqls/20260212165353-audit-log-up.sql new file mode 100644 index 0000000..d375024 --- /dev/null +++ b/api/migrations/sqls/20260212165353-audit-log-up.sql @@ -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) +); \ No newline at end of file diff --git a/api/src/services/logging/auditLog.ts b/api/src/services/logging/auditLog.ts new file mode 100644 index 0000000..f6e5adc --- /dev/null +++ b/api/src/services/logging/auditLog.ts @@ -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 = {} // 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(); \ No newline at end of file diff --git a/api/src/services/logging/logger.ts b/api/src/services/logging/logger.ts index cc4c39e..73adba2 100644 --- a/api/src/services/logging/logger.ts +++ b/api/src/services/logging/logger.ts @@ -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; From 5106b72e242a261d97442881f6a18c982f84fdce Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Thu, 12 Feb 2026 14:48:27 -0500 Subject: [PATCH 2/3] Integrated audit log into applications --- api/src/routes/applications.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/api/src/routes/applications.ts b/api/src/routes/applications.ts index 5f3f90c..f76570f 100644 --- a/api/src/routes/applications.ts +++ b/api/src/routes/applications.ts @@ -12,6 +12,7 @@ import { Request, response, Response } from 'express'; import { getUserRoles } from '../services/db/rolesService'; import { requireLogin, requireRole } from '../middleware/auth'; import { logger } from '../services/logging/logger'; +import { audit, AuditContext } from '../services/logging/auditLog'; import { bus } from '../services/events/eventBus'; //get CoC @@ -58,6 +59,8 @@ router.post('/', [requireLogin], async (req: Request, res: Response) => { res.sendStatus(201); + audit.application('created', { actorId: memberID, targetId: appID }); + bus.emit("application.create", { application: appID, member_name: req.user.name, member_discord_id: req.user.discord_id || null }) logger.info('app', 'Application Posted', { @@ -228,31 +231,26 @@ router.post('/approve/:id', [requireLogin, requireRole("Recruiter")], async (req const app = await getApplicationByID(appID); try { - console.log("HELLO MFS") var con = await pool.getConnection(); - console.log("START") con.beginTransaction(); - console.log("APPROVE") await approveApplication(appID, approved_by, con); - console.log("STATE") //update user profile await setUserState(app.member_id, MemberState.Member, "Application Accepted", approved_by, con); - console.log("SP") - await con.query('CALL sp_accept_new_recruit_validation(?, ?, ?, ?)', [Number(process.env.CONFIG_ID), app.member_id, approved_by, approved_by]) - console.log("COMMIT") - con.commit(); logger.info('app', "Member application approved", { application: app.id, applicant: app.member_id, approver: approved_by }) + + audit.application('approved', { actorId: approved_by, targetId: appID }, { applicantId: app.member_id }); + res.sendStatus(200); } catch (error) { @@ -288,6 +286,7 @@ router.post('/deny/:id', [requireLogin, requireRole("Recruiter")], async (req: R applicant: app.member_id, approver: approver }) + audit.application('denied', { actorId: approver, targetId: appID }, { applicantId: app.member_id }); res.sendStatus(200); } catch (error) { logger.error( @@ -305,7 +304,7 @@ router.post('/deny/:id', [requireLogin, requireRole("Recruiter")], async (req: R // POST /application/:id/comment router.post('/:id/comment', [requireLogin], async (req: Request, res: Response) => { - const appID = req.params.id; + const appID = Number(req.params.id); const data = req.body.message; const user = req.user; @@ -337,10 +336,12 @@ VALUES(?, ?, ?);` WHERE app.id = ?; `; const comment = await conn.query(getSQL, [result.insertId]) + audit.record('application', 'comment_added', { actorId: user.id, targetId: appID }, { commentId: Number(result.insertId) }); + logger.info('app', "Application comment posted", { application: appID, poster: user.id, - comment: result.insertId, + comment: Number(result.insertId), }) res.status(201).json(comment[0]); @@ -363,7 +364,7 @@ VALUES(?, ?, ?);` // POST /application/:id/comment router.post('/:id/adminComment', [requireLogin, requireRole("Recruiter")], async (req: Request, res: Response) => { - const appID = req.params.id; + const appID = Number(req.params.id); const data = req.body.message; const user = req.user; @@ -395,7 +396,7 @@ VALUES(?, ?, ?, 1);` INNER JOIN members AS member ON member.id = app.poster_id WHERE app.id = ?; `; const comment = await conn.query(getSQL, [result.insertId]) - + audit.record('application', 'comment_added', { actorId: user.id, targetId: appID }, { commentId: result.insertId }); logger.info('app', "Admin application comment posted", { application: appID, poster: user.id, @@ -424,6 +425,7 @@ router.post('/restart', async (req: Request, res: Response) => { try { await setUserState(user, MemberState.Guest, "Restarted Application", user); + audit.application('restarted', { actorId: user, targetId: user }); logger.info('app', "Member restarted application", { user: user }) From c7d79ae586c8e151c09faac33be08aff3ac21c80 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Thu, 12 Feb 2026 22:04:14 -0500 Subject: [PATCH 3/3] integrated audit log into pretty everything hopefully --- api/src/routes/calendar.ts | 16 ++++++++++++---- api/src/routes/course.ts | 2 ++ api/src/routes/loa.ts | 12 ++++++++++-- api/src/routes/members.ts | 9 +++++++++ api/src/routes/ranks.ts | 3 +++ api/src/routes/roles.ts | 12 ++++++++++-- api/src/services/db/calendarService.ts | 3 ++- api/src/services/db/loaService.ts | 7 ++++--- api/src/services/logging/auditLog.ts | 19 +++++++++++++++---- 9 files changed, 67 insertions(+), 16 deletions(-) diff --git a/api/src/routes/calendar.ts b/api/src/routes/calendar.ts index f5279f7..6d01c2a 100644 --- a/api/src/routes/calendar.ts +++ b/api/src/routes/calendar.ts @@ -4,6 +4,7 @@ import { CalendarAttendance, CalendarEvent } from "@app/shared/types/calendar"; import { requireLogin, requireMemberState, requireRole } from "../middleware/auth"; import { MemberState } from "@app/shared/types/member"; import { logger } from "../services/logging/logger"; +import { audit } from "../services/logging/auditLog"; const express = require('express'); const r = express.Router(); @@ -46,10 +47,12 @@ r.get('/upcoming', async (req, res) => { }) r.post('/:id/cancel', [requireLogin, requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { + let member = req.user.id; try { const eventID = Number(req.params.id); - setEventCancelled(eventID, true); + await setEventCancelled(eventID, true); + audit.calendar('cancelled', { actorId: member, targetId: eventID }); logger.info('app', 'Calendar event cancelled', { event: eventID, user: req.user.id @@ -68,10 +71,12 @@ r.post('/:id/cancel', [requireLogin, requireMemberState(MemberState.Member)], as } }) r.post('/:id/uncancel', [requireLogin, requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { + let member = req.user.id; try { const eventID = Number(req.params.id); setEventCancelled(eventID, false); + audit.calendar('un-cancelled', { actorId: member, targetId: eventID }); logger.info('app', 'Calendar event un-cancelled', { event: eventID, user: req.user.id @@ -96,8 +101,9 @@ r.post('/:id/attendance', [requireLogin, requireMemberState(MemberState.Member)] let member = req.user.id; let event = Number(req.params.id); let state = req.query.state as CalendarAttendance; - setAttendanceStatus(member, event, state); + await setAttendanceStatus(member, event, state); + audit.calendar('attendance_set', { actorId: member, targetId: event }, { attendanceState: state }); logger.info('app', 'Member set calendar event attendance', { event: event, user: req.user.id, @@ -148,8 +154,8 @@ r.post('/', [requireLogin, requireMemberState(MemberState.Member)], async (req: event.creator_id = member; event.start = new Date(event.start); event.end = new Date(event.end); - createEvent(event); - + let eventID = await createEvent(event); + audit.calendar('event_created', { actorId: member, targetId: eventID }); logger.info('app', 'Calendar event posted', { event: event.id, user: req.user.id @@ -170,12 +176,14 @@ r.post('/', [requireLogin, requireMemberState(MemberState.Member)], async (req: }) r.put('/', [requireLogin, requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { + let member = req.user.id; try { let event: CalendarEvent = req.body; event.start = new Date(event.start); event.end = new Date(event.end); updateEvent(event); + audit.calendar('event_updated', { actorId: member, targetId: event.id }); logger.info('app', 'Calendar event updated', { event: event.id, user: req.user.id diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts index 007d593..65e4d72 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -4,6 +4,7 @@ import { Request, Response, Router } from "express"; import { requireLogin, requireMemberState } from "../middleware/auth"; import { MemberState } from "@app/shared/types/member"; import { logger } from "../services/logging/logger"; +import { audit } from "../services/logging/auditLog"; const cr = Router(); const er = Router(); @@ -125,6 +126,7 @@ er.post('/', async (req: Request, res: Response) => { data.event_date = new Date(data.event_date); const id = await insertCourseEvent(data); + audit.course('report_created', { actorId: posterID, targetId: id }); logger.info('app', 'Training report posted', { user: posterID, report: id }) res.status(201).json(id); } catch (error) { diff --git a/api/src/routes/loa.ts b/api/src/routes/loa.ts index 8555fb7..a3e2206 100644 --- a/api/src/routes/loa.ts +++ b/api/src/routes/loa.ts @@ -7,6 +7,7 @@ import { closeLOA, createNewLOA, getAllLOA, getLOAbyID, getLoaTypes, getUserLOA, import { LOARequest } from '@app/shared/types/loa'; import { requireLogin, requireRole } from '../middleware/auth'; import { logger } from '../services/logging/logger'; +import { audit } from '../services/logging/auditLog'; router.use(requireLogin); @@ -18,7 +19,9 @@ router.post("/", async (req: Request, res: Response) => { LOARequest.filed_date = new Date(); try { - await createNewLOA(LOARequest); + let loaID = await createNewLOA(LOARequest); + + audit.leaveOfAbsence('created', { actorId: req.user.id, targetId: loaID }) logger.info('app', 'LOA Posted', { poster: req.user.id, user: LOARequest.member_id }) res.sendStatus(201); } catch (error) { @@ -40,7 +43,8 @@ router.post("/admin", [requireRole(['17th Administrator', '17th HQ', '17th Comma LOARequest.created_by = req.user.id; LOARequest.filed_date = new Date(); try { - await createNewLOA(LOARequest); + let loaID = await createNewLOA(LOARequest); + audit.leaveOfAbsence('admin_created', { actorId: req.user.id, targetId: loaID }, { for: LOARequest.member_id }) logger.info('app', 'LOA Posted', { poster: req.user.id, user: LOARequest.member_id }) res.sendStatus(201); } catch (error) { @@ -144,6 +148,7 @@ router.post('/cancel/:id', async (req: Request, res: Response) => { await closeLOA(Number(req.params.id), closer); + audit.leaveOfAbsence('ended', { actorId: req.user.id, targetId: id }); logger.info('app', 'LOA Closed', { closed_by: closer, LOA: id }) res.sendStatus(200); @@ -166,6 +171,7 @@ router.post('/adminCancel/:id', [requireRole(['17th Administrator', '17th HQ', ' try { await closeLOA(Number(req.params.id), closer); + audit.leaveOfAbsence('admin_ended', { actorId: req.user.id, targetId: Number(req.params.id) }); logger.info('app', 'LOA Closed', { closed_by: closer, LOA: req.params.id }) res.sendStatus(200); @@ -192,6 +198,8 @@ router.post('/extend/:id', [requireRole(['17th Administrator', '17th HQ', '17th try { await setLOAExtension(Number(req.params.id), to); + + audit.leaveOfAbsence('extended', { actorId: req.user.id, targetId: Number(req.params.id) }); logger.info('app', 'LOA Extended', { extended_by: req.user.id, LOA: req.params.id }) res.sendStatus(200); diff --git a/api/src/routes/members.ts b/api/src/routes/members.ts index b7eb20f..be83f25 100644 --- a/api/src/routes/members.ts +++ b/api/src/routes/members.ts @@ -15,6 +15,7 @@ import { logger } from '../services/logging/logger'; import { memberCache } from './auth'; import { cancelLatestRank } from '../services/db/rankService'; import { cancelLatestUnit } from '../services/db/unitService'; +import { audit } from '../services/logging/auditLog'; //get all users router.get('/', [requireLogin, requireMemberState(MemberState.Member)], async (req, res) => { @@ -251,6 +252,9 @@ router.post('/discharge', [requireLogin, requireMemberState(MemberState.Member), con.commit(); memberCache.Invalidate(data.userID); + + audit.member('discharged', { actorId: req.user.id, targetId: data.userID }, { reason: data.reason }); + res.sendStatus(200); } catch (error) { logger.error('app', 'Failed to discharge user', { @@ -272,6 +276,9 @@ router.post('/suspend', [requireLogin, requireMemberState(MemberState.Member), r let target = Number(req.query.target); try { await setUserState(target, MemberState.Suspended, "Member Suspended", author, null); + + audit.member('suspension_added', { actorId: author, targetId: target }); + res.sendStatus(200); } catch (error) { logger.error('app', 'Failed to suspend user', { @@ -291,6 +298,8 @@ router.post('/unsuspend', [requireLogin, requireMemberState(MemberState.Member), try { let prevState = await getLastNonSuspendedState(target); await setUserState(target, prevState, "Member Suspension Removed", author, null); + audit.member('suspension_removed', { actorId: author, targetId: target }); + res.sendStatus(200); } catch (error) { logger.error('app', 'Failed to suspend user', { diff --git a/api/src/routes/ranks.ts b/api/src/routes/ranks.ts index e1cce00..44a74e1 100644 --- a/api/src/routes/ranks.ts +++ b/api/src/routes/ranks.ts @@ -5,6 +5,7 @@ import { BatchPromotion, BatchPromotionMember } from '@app/shared/schemas/promot import express = require('express'); import { logger } from "../services/logging/logger"; +import { audit } from "../services/logging/auditLog"; const r = express.Router(); const ur = express.Router(); @@ -21,6 +22,8 @@ ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]), re if (!change) res.sendStatus(400); await batchInsertMemberRank(change, author, approver); + + audit.member('update_rank', { actorId: author, targetId: null }, { changes: change.length }); logger.info('app', 'Promotion batch submitted', { author: author }) res.sendStatus(201); } catch (error) { diff --git a/api/src/routes/roles.ts b/api/src/routes/roles.ts index c6a088e..e983476 100644 --- a/api/src/routes/roles.ts +++ b/api/src/routes/roles.ts @@ -8,6 +8,7 @@ import { requireLogin, requireMemberState, requireRole } from '../middleware/aut import { assignUserGroup, createGroup, getAllRoles, getRole, getUsersWithRole } from '../services/db/rolesService'; import { Request, Response } from 'express'; import { logger } from '../services/logging/logger'; +import { audit } from '../services/logging/auditLog'; r.use(requireLogin) ur.use(requireLogin) @@ -22,6 +23,8 @@ ur.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administ logger.info('app', 'User assigned role', { user: body.member_id, role: body.role_id, assigner: req.user.id }) res.sendStatus(201); + audit.roles('add_member', { actorId: req.user.id, targetId: body.role_id }, { member: body.member_id, role: body.role_id }); + } catch (error) { if (error?.code === 'ER_DUP_ENTRY') { return res.status(400).json({ @@ -54,6 +57,7 @@ ur.delete('/', [requireMemberState(MemberState.Member), requireRole("17th Admini await pool.query(sql, [body.member_id, body.role_id]) logger.info('app', 'User removed role', { user: body.member_id, role: body.role_id, assigner: req.user.id }) + audit.roles('remove_member', { actorId: req.user.id, targetId: body.role_id }, { member: body.member_id, role: body.role_id }); res.sendStatus(200); } @@ -77,7 +81,7 @@ ur.delete('/', [requireMemberState(MemberState.Member), requireRole("17th Admini r.get('/', [requireMemberState(MemberState.Member)], async (req, res) => { try { const roles = await getAllRoles(); - + res.status(200).json(roles); } catch (error) { logger.error( @@ -144,7 +148,8 @@ r.post('/', [requireMemberState(MemberState.Member), requireRole("dev")], async return res.status(400).json({ error: 'Color must be a valid hex color (#ffffff)' }); } - await createGroup(name, color, description); + let out = await createGroup(name, color, description); + audit.roles('create', { actorId: req.user.id, targetId: out.id }); res.sendStatus(201); } catch (err) { @@ -159,6 +164,9 @@ r.delete('/:id', [requireMemberState(MemberState.Member), requireRole("dev")], a const sql = 'DELETE FROM roles WHERE id = ?'; const res = await pool.query(sql, [id]); + + audit.roles('delete', { actorId: req.user.id, targetId: id }); + res.sendStatus(200); } catch (error) { console.error(error); diff --git a/api/src/services/db/calendarService.ts b/api/src/services/db/calendarService.ts index 3c677b5..9a53ac6 100644 --- a/api/src/services/db/calendarService.ts +++ b/api/src/services/db/calendarService.ts @@ -19,7 +19,8 @@ export async function createEvent(eventObject: Omit { return LOAData; } -export async function createNewLOA(data: LOARequest) { +export async function createNewLOA(data: LOARequest): Promise { const sql = `INSERT INTO leave_of_absences (member_id, filed_date, start_date, end_date, type_id, reason) VALUES (?, ?, ?, ?, ?, ?)`; - await pool.query(sql, [data.member_id, toDateTime(data.filed_date), toDateTime(data.start_date), toDateTime(data.end_date), data.type_id, data.reason]) - return; + let out = await pool.query(sql, [data.member_id, toDateTime(data.filed_date), toDateTime(data.start_date), toDateTime(data.end_date), data.type_id, data.reason]) + + return Number(out.insertId); } export async function closeLOA(id: number, closer: number) { diff --git a/api/src/services/logging/auditLog.ts b/api/src/services/logging/auditLog.ts index f6e5adc..fded7ca 100644 --- a/api/src/services/logging/auditLog.ts +++ b/api/src/services/logging/auditLog.ts @@ -1,7 +1,7 @@ import pool from "../../db"; import { logger } from "./logger"; -export type AuditArea = 'member' | 'calendar' | 'unit' | 'auth' | 'admin' | 'application'; +export type AuditArea = 'member' | 'calendar' | 'roles' | 'auth' | 'leave_of_absence' | 'application' | 'course'; export interface AuditContext { actorId: number; // The person doing the action (created_by) @@ -33,18 +33,29 @@ class AuditLogger { } } - // Making data optional using '?' and default parameter - member(action: 'update_rank' | 'status_change' | 'create', context: AuditContext, data: any = {}) { + member(action: 'update_rank' | 'suspension_added' | 'suspension_removed' | 'discharged', context: AuditContext, data: any = {}) { return this.record('member', action, context, data); } - calendar(action: 'event_signup' | 'event_create' | 'attendance', context: AuditContext, data: any = {}) { + roles(action: 'add_member' | 'remove_member' | 'create' | 'delete', context: AuditContext, data: any = {}) { + return this.record('roles', action, context, data); + } + + leaveOfAbsence(action: 'created' | 'admin_created' | 'ended' | 'admin_ended' | 'extended', context: AuditContext, data: any = {}) { + return this.record('leave_of_absence', action, context, data); + } + + calendar(action: 'event_created' | 'event_updated' | 'attendance_set' | 'cancelled' | 'un-cancelled', 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); } + + course(action: 'report_created' | 'report_edited', context: AuditContext, data: any = {}) { + return this.record('course', action, context, data); + } } export const audit = new AuditLogger(); \ No newline at end of file