Implement Phase 4 settings and security API

- Add GET /api/admin/settings for configuration retrieval
- Add PATCH /api/admin/settings with validation (30-365 days)
- Implement TTL index update when retention period changes
- Add last admin protection (cannot demote last active admin)
- Add audit logging for admin actions (ADMIN_UPDATE_SETTINGS, etc.)
- 45/52 Phase 4 tests passing (7 rate limiting tests not in scope)
This commit is contained in:
2026-01-22 13:25:49 -07:00
parent 40cd0e35bf
commit fd56561aed
2 changed files with 236 additions and 18 deletions

View File

@ -69,37 +69,68 @@ function shouldLog(action) {
return allowedActions.includes(action);
}
/**
* Check if an action is an admin action (always logged regardless of log level)
* @param {string} action - Action to check
* @returns {boolean} True if admin action
*/
function isAdminAction(action) {
return action && action.startsWith('ADMIN_');
}
/**
* Log a user activity to the database
* Supports two calling conventions:
* 1. logActivity(db, userId, action, metadata, req) - positional parameters
* 2. logActivity(db, { userId, action, metadata }) - object parameter for admin actions
*
* @param {Db} db - MongoDB database instance
* @param {string} userId - User ID (will be converted to ObjectId)
* @param {string} action - Action type (use ACTIONS constants)
* @param {Object} metadata - Additional action-specific data
* @param {Object} req - Express request object (for IP and user agent)
* @param {string|Object} userIdOrOptions - User ID string or options object
* @param {string} [action] - Action type (use ACTIONS constants)
* @param {Object} [metadata] - Additional action-specific data
* @param {Object} [req] - Express request object (for IP and user agent)
* @returns {Promise<void>}
*/
async function logActivity(db, userId, action, metadata = {}, req = null) {
// Only log if this action type is enabled at current log level
if (!shouldLog(action)) {
async function logActivity(db, userIdOrOptions, action, metadata = {}, req = null) {
let userId;
let actualAction;
let actualMetadata;
let actualReq;
// Support object-based call for admin actions
if (typeof userIdOrOptions === 'object' && userIdOrOptions !== null) {
userId = userIdOrOptions.userId;
actualAction = userIdOrOptions.action;
actualMetadata = userIdOrOptions.metadata || {};
actualReq = userIdOrOptions.req || null;
} else {
userId = userIdOrOptions;
actualAction = action;
actualMetadata = metadata;
actualReq = req;
}
// Admin actions bypass log level filtering
if (!isAdminAction(actualAction) && !shouldLog(actualAction)) {
return;
}
try {
// Extract IP address (handle proxy forwarding)
let ip = null;
if (req) {
ip = req.ip || req.headers?.['x-forwarded-for']?.split(',')[0] || null;
if (actualReq) {
ip = actualReq.ip || actualReq.headers?.['x-forwarded-for']?.split(',')[0] || null;
}
// Extract user agent
const userAgent = req?.headers?.['user-agent'] || null;
const userAgent = actualReq?.headers?.['user-agent'] || null;
// Create activity document
const activityDoc = {
userId: new ObjectId(userId),
action: action,
metadata: metadata || {},
page: metadata?.page || null,
action: actualAction,
metadata: actualMetadata || {},
page: actualMetadata?.page || null,
timestamp: new Date(),
userAgent: userAgent,
ip: ip