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