Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions server/server/audit/audit_logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const Logger = require("../logger");
const DB_CONFIG = require("../database/db_config");

const ACTION_TYPES = {
CREATE: "CREATED",
UPDATE: "UPDATED",
DEACTIVATE: "DEACTIVATED",
REACTIVATE: "REACTIVATED",
};

module.exports = (db) => {
function getActor(req) {
Comment thread
yyi5708 marked this conversation as resolved.
Outdated
const system_id = req.user ? req.user.system_id : null;
const mock_id = req.user && req.user.mock ? req.user.mock.system_id : null;
return { system_id, mock_id };
}

function actorLabel(req) {
if (!req.user) return "Unknown user";
const name = [req.user.fname, req.user.lname].filter(Boolean).join(" ");
return name ? `${name} (${req.user.system_id})` : req.user.system_id;
}

async function record(
req,
{ actionType, entityType, entityId, message, details },
) {
const { system_id, mock_id } = getActor(req);

const insertQuery = `
INSERT INTO ${DB_CONFIG.tableNames.audit_log}
(system_id, mock_id, action_type, entity_type, entity_id, message, details)
VALUES (?, ?, ?, ?, ?, ?, ?)
`;

const params = [
system_id,
mock_id,
actionType,
entityType,
entityId === undefined || entityId === null ? null : String(entityId),
message,
details === undefined ? null : JSON.stringify(details),
];

try {
await db.query(insertQuery, params);
} catch (err) {
Logger.error(`Failed to write audit log entry: ${err.message}`);
Comment thread
yyi5708 marked this conversation as resolved.
Outdated
}
}

function humanizeFieldName(field) {
Comment thread
yyi5708 marked this conversation as resolved.
Outdated
return field.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}

function displayValue(value) {
if (value === null || value === undefined || value === "") return "(empty)";
if (typeof value === "object") return JSON.stringify(value);
return String(value);
}

function summarizeChangedFields(changedFields, excludeFields = []) {
if (!changedFields || typeof changedFields !== "object") return "";

return Object.keys(changedFields)
.filter((field) => !excludeFields.includes(field))
.map((field) => {
const [before, after] = changedFields[field];
return `${humanizeFieldName(field)}: "${displayValue(before)}" → "${displayValue(after)}"`;
})
.join("; ");
}

return { record, actorLabel, summarizeChangedFields, ACTION_TYPES };
};

module.exports.ACTION_TYPES = ACTION_TYPES;
1 change: 1 addition & 0 deletions server/server/database/db_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module.exports = {
project_coaches: "project_coaches",
sponsors: "sponsors",
error_log: "error_log",
audit_log: "audit_log",
},
senior_project_proposal_keys: {
title: "Title",
Expand Down
12 changes: 12 additions & 0 deletions server/server/database/table_sql/audit_log.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE audit_log (
audit_log_id INTEGER PRIMARY KEY AUTOINCREMENT,
audit_datetime DATETIME DEFAULT CURRENT_TIMESTAMP,
system_id TEXT,
mock_id TEXT,
action_type TEXT NOT NULL,
entity_type TEXT NOT NULL,
entity_id TEXT,
message TEXT NOT NULL,
details TEXT,
Comment thread
yyi5708 marked this conversation as resolved.
Outdated
FOREIGN KEY (system_id) REFERENCES users(system_id)
);
3 changes: 2 additions & 1 deletion server/server/database/table_sql/create_all_tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
.read table_sql/users.sql
.read table_sql/sponsor_notes.sql
.read table_sql/page_html.sql
Comment thread
yyi5708 marked this conversation as resolved.
.read table_sql/error_log.sql
.read table_sql/error_log.sql
.read table_sql/audit_log.sql
80 changes: 79 additions & 1 deletion server/server/routing/db_routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const ACTION_TARGETS = {
};

// Routes
module.exports = (db) => {
module.exports = (db, AuditLog) => {
Comment thread
yyi5708 marked this conversation as resolved.
Outdated
/**
* /getAllUsersForLogin ENDPOINT SHOULD ONLY BE HIT IN DEVELOPMENT ONLY
*
Expand Down Expand Up @@ -1730,6 +1730,27 @@ module.exports = (db) => {
db.query(deleteCoachesSQL),
])
.then((values) => {
let changedFields = {};
try {
changedFields = body.changed_fields
? JSON.parse(body.changed_fields)
: {};
} catch (err) {
changedFields = {};
}

const changeSummary = AuditLog.summarizeChangedFields(changedFields);
const baseMessage = `${AuditLog.actorLabel(req)} updated project ${body.project_id} (${body.title})`;

AuditLog.record(req, {
actionType: AuditLog.ACTION_TYPES.UPDATE,
entityType: "project",
entityId: body.project_id,
message: changeSummary
? `${baseMessage} — ${changeSummary}`
: baseMessage,
details: changedFields,
});
return res.sendStatus(200);
})
.catch((err) => {
Expand Down Expand Up @@ -3404,6 +3425,47 @@ module.exports = (db) => {

db.query(updateQuery, params)
.then(() => {
let changedFields = {};
Comment thread
yyi5708 marked this conversation as resolved.
Outdated
try {
changedFields = body.changed_fields
? JSON.parse(body.changed_fields)
: {};
} catch (err) {
changedFields = {};
}

let auditActionType = AuditLog.ACTION_TYPES.UPDATE;
let auditVerb = "updated";

if ("date_deleted" in changedFields) {
const [beforeRaw, afterRaw] = changedFields.date_deleted;
const isEmpty = (v) => v === "" || v === undefined || v === null;
const wasActive = isEmpty(beforeRaw);
const isActiveNow = isEmpty(afterRaw);

if (wasActive && !isActiveNow) {
auditActionType = AuditLog.ACTION_TYPES.DEACTIVATE;
auditVerb = "deactivated";
} else if (!wasActive && isActiveNow) {
auditActionType = AuditLog.ACTION_TYPES.REACTIVATE;
auditVerb = "reactivated";
}

delete changedFields.date_deleted;
}

const changeSummary = AuditLog.summarizeChangedFields(changedFields);
const baseMessage = `${AuditLog.actorLabel(req)} ${auditVerb} action ${body.action_id} (${body.action_title})`;

AuditLog.record(req, {
actionType: auditActionType,
entityType: "action",
entityId: body.action_id,
message: changeSummary
? `${baseMessage} — ${changeSummary}`
: baseMessage,
details: changedFields,
});
return res.status(200).send();
})
.catch((err) => {
Expand Down Expand Up @@ -3966,6 +4028,22 @@ module.exports = (db) => {

db.query(updateQuery, params)
.then(() => {
return db.query(
`SELECT action_id FROM actions
WHERE semester = ? AND action_title = ? AND start_date = ? AND due_date = ?
ORDER BY action_id DESC LIMIT 1`,
[body.semester, body.action_title, body.start_date, body.due_date],
);
})
.then((rows) => {
const newActionId = rows && rows[0] ? rows[0].action_id : null;
AuditLog.record(req, {
actionType: AuditLog.ACTION_TYPES.CREATE,
entityType: "action",
entityId: newActionId,
message: `${AuditLog.actorLabel(req)} created action ${newActionId} (${body.action_title})`,
details: body,
});
return res.status(200).send();
})
.catch((err) => {
Expand Down
3 changes: 2 additions & 1 deletion server/server/routing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ require("../config/passport");
const router = require("express").Router();
const DBHandler = require("../database/db");
let db = new DBHandler();
const AuditLog = require("../audit/audit_logger")(db);
Comment thread
yyi5708 marked this conversation as resolved.
Outdated

const saml_router = require("./saml_routes")(router, db);
const db_router = require("./db_routes")(db);
const db_router = require("./db_routes")(db, AuditLog);
const ai_router = require("./ai_routes")(router);

// Database routes
Expand Down