Add user activity logging system
- Create services/activityLogger.js with configurable log levels (all, navigation, none) - Create routes/activity.js with endpoints for logging and admin statistics - Integrate login/logout activity logging into auth routes - Add TTL index for automatic 90-day cleanup of activity records - Mount activity routes at /activity
This commit is contained in:
161
services/activityLogger.js
Normal file
161
services/activityLogger.js
Normal file
@ -0,0 +1,161 @@
|
||||
const { ObjectId } = require('mongodb');
|
||||
|
||||
// Collection name
|
||||
const ACTIVITY_COLLECTION = 'user_activity';
|
||||
|
||||
// Activity action constants
|
||||
const ACTIONS = {
|
||||
LOGIN: 'login',
|
||||
LOGOUT: 'logout',
|
||||
PAGE_VIEW: 'page_view',
|
||||
NAVIGATION: 'navigation',
|
||||
UNIT_VIEW: 'unit_view',
|
||||
UNIT_WATCH: 'unit_watch',
|
||||
FILTER_CHANGE: 'filter_change',
|
||||
SORT_CHANGE: 'sort_change',
|
||||
SEARCH: 'search',
|
||||
EXPORT: 'export',
|
||||
VIEW_TOGGLE: 'view_toggle'
|
||||
};
|
||||
|
||||
// Log levels define which actions should be logged
|
||||
const LOG_LEVELS = {
|
||||
all: [
|
||||
'login',
|
||||
'logout',
|
||||
'page_view',
|
||||
'navigation',
|
||||
'filter_change',
|
||||
'sort_change',
|
||||
'search',
|
||||
'unit_view',
|
||||
'unit_watch',
|
||||
'export',
|
||||
'view_toggle'
|
||||
],
|
||||
navigation: [
|
||||
'login',
|
||||
'logout',
|
||||
'page_view',
|
||||
'navigation'
|
||||
],
|
||||
none: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current activity log level from environment variable
|
||||
* @returns {string} Current log level ('all', 'navigation', or 'none')
|
||||
*/
|
||||
function getActivityLevel() {
|
||||
const level = process.env.ACTIVITY_LOG_LEVEL || 'all';
|
||||
|
||||
// Validate level exists, default to 'all' if invalid
|
||||
if (!LOG_LEVELS[level]) {
|
||||
console.warn(`Invalid ACTIVITY_LOG_LEVEL: ${level}, defaulting to 'all'`);
|
||||
return 'all';
|
||||
}
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an action should be logged based on current log level
|
||||
* @param {string} action - Action to check
|
||||
* @returns {boolean} True if action should be logged
|
||||
*/
|
||||
function shouldLog(action) {
|
||||
const level = getActivityLevel();
|
||||
const allowedActions = LOG_LEVELS[level];
|
||||
return allowedActions.includes(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a user activity to the database
|
||||
* @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)
|
||||
* @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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Extract IP address (handle proxy forwarding)
|
||||
let ip = null;
|
||||
if (req) {
|
||||
ip = req.ip || req.headers?.['x-forwarded-for']?.split(',')[0] || null;
|
||||
}
|
||||
|
||||
// Extract user agent
|
||||
const userAgent = req?.headers?.['user-agent'] || null;
|
||||
|
||||
// Create activity document
|
||||
const activityDoc = {
|
||||
userId: new ObjectId(userId),
|
||||
action: action,
|
||||
metadata: metadata || {},
|
||||
page: metadata?.page || null,
|
||||
timestamp: new Date(),
|
||||
userAgent: userAgent,
|
||||
ip: ip
|
||||
};
|
||||
|
||||
// Insert into user_activity collection
|
||||
await db.collection(ACTIVITY_COLLECTION).insertOne(activityDoc);
|
||||
} catch (error) {
|
||||
// Log error but don't throw - activity logging should not break the main flow
|
||||
console.error('Error logging activity:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create required indexes for the user_activity collection
|
||||
* @param {Db} db - MongoDB database instance
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function createActivityIndexes(db) {
|
||||
try {
|
||||
const collection = db.collection(ACTIVITY_COLLECTION);
|
||||
|
||||
// Index for user activity queries (get all activities for a user, sorted by time)
|
||||
await collection.createIndex(
|
||||
{ userId: 1, timestamp: -1 },
|
||||
{ name: 'userId_timestamp' }
|
||||
);
|
||||
|
||||
// Index for action type queries (get all activities of a certain type)
|
||||
await collection.createIndex(
|
||||
{ action: 1, timestamp: -1 },
|
||||
{ name: 'action_timestamp' }
|
||||
);
|
||||
|
||||
// TTL index to automatically delete activities older than 90 days
|
||||
await collection.createIndex(
|
||||
{ timestamp: 1 },
|
||||
{
|
||||
name: 'timestamp_ttl',
|
||||
expireAfterSeconds: 7776000 // 90 days = 90 * 24 * 60 * 60
|
||||
}
|
||||
);
|
||||
|
||||
console.log('Activity collection indexes created successfully');
|
||||
} catch (error) {
|
||||
console.error('Error creating activity indexes:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ACTIVITY_COLLECTION,
|
||||
ACTIONS,
|
||||
LOG_LEVELS,
|
||||
getActivityLevel,
|
||||
shouldLog,
|
||||
logActivity,
|
||||
createActivityIndexes
|
||||
};
|
||||
Reference in New Issue
Block a user