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:
179
routes/activity.js
Normal file
179
routes/activity.js
Normal file
@ -0,0 +1,179 @@
|
||||
const express = require('express');
|
||||
const { ObjectId } = require('mongodb');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||
const { logActivity, ACTIONS, ACTIVITY_COLLECTION } = require('../services/activityLogger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* POST /log - Log frontend activity
|
||||
* Protected with requireAuth
|
||||
*/
|
||||
router.post('/log', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { action, metadata } = req.body;
|
||||
|
||||
// Validate action is in ACTIONS object
|
||||
if (!action || !Object.values(ACTIONS).includes(action)) {
|
||||
return res.status(400).json({ error: 'Invalid action type' });
|
||||
}
|
||||
|
||||
const db = req.app.locals.db;
|
||||
|
||||
// Log the activity with merged metadata
|
||||
await logActivity(
|
||||
db,
|
||||
req.user._id,
|
||||
action,
|
||||
{ ...metadata, page: metadata?.page },
|
||||
req
|
||||
);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error logging activity:', error);
|
||||
res.status(500).json({ error: 'Failed to log activity' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /user/:userId - Get activity for specific user
|
||||
* Protected with requireAuth and requireAdmin
|
||||
*/
|
||||
router.get('/user/:userId', requireAuth, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const limit = parseInt(req.query.limit) || 50;
|
||||
const skip = parseInt(req.query.skip) || 0;
|
||||
|
||||
const db = req.app.locals.db;
|
||||
|
||||
// Convert userId to ObjectId
|
||||
let userObjectId;
|
||||
try {
|
||||
userObjectId = new ObjectId(userId);
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: 'Invalid user ID format' });
|
||||
}
|
||||
|
||||
// Find activities for the user
|
||||
const activities = await db
|
||||
.collection(ACTIVITY_COLLECTION)
|
||||
.find({ userId: userObjectId })
|
||||
.sort({ timestamp: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
|
||||
// Get total count
|
||||
const total = await db
|
||||
.collection(ACTIVITY_COLLECTION)
|
||||
.countDocuments({ userId: userObjectId });
|
||||
|
||||
res.json({ activities, total });
|
||||
} catch (error) {
|
||||
console.error('Error fetching user activity:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch user activity' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /recent - Get recent activity across all users
|
||||
* Protected with requireAuth and requireAdmin
|
||||
*/
|
||||
router.get('/recent', requireAuth, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit) || 50;
|
||||
const skip = parseInt(req.query.skip) || 0;
|
||||
|
||||
const db = req.app.locals.db;
|
||||
|
||||
// Find all activities sorted by timestamp
|
||||
const activities = await db
|
||||
.collection(ACTIVITY_COLLECTION)
|
||||
.find({})
|
||||
.sort({ timestamp: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
|
||||
// Get total count
|
||||
const total = await db.collection(ACTIVITY_COLLECTION).countDocuments({});
|
||||
|
||||
res.json({ activities, total });
|
||||
} catch (error) {
|
||||
console.error('Error fetching recent activity:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch recent activity' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /stats - Get activity statistics
|
||||
* Protected with requireAuth and requireAdmin
|
||||
*/
|
||||
router.get('/stats', requireAuth, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const db = req.app.locals.db;
|
||||
|
||||
// Calculate date 7 days ago
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
|
||||
// Aggregate activity counts by action type in last 7 days
|
||||
const actionCountsResult = await db
|
||||
.collection(ACTIVITY_COLLECTION)
|
||||
.aggregate([
|
||||
{
|
||||
$match: {
|
||||
timestamp: { $gte: sevenDaysAgo }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$action',
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
}
|
||||
])
|
||||
.toArray();
|
||||
|
||||
// Convert array to object
|
||||
const actionCounts = {};
|
||||
actionCountsResult.forEach(item => {
|
||||
actionCounts[item._id] = item.count;
|
||||
});
|
||||
|
||||
// Count unique active users in last 7 days
|
||||
const activeUsersResult = await db
|
||||
.collection(ACTIVITY_COLLECTION)
|
||||
.aggregate([
|
||||
{
|
||||
$match: {
|
||||
timestamp: { $gte: sevenDaysAgo }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$userId'
|
||||
}
|
||||
},
|
||||
{
|
||||
$count: 'total'
|
||||
}
|
||||
])
|
||||
.toArray();
|
||||
|
||||
const activeUsers = activeUsersResult.length > 0 ? activeUsersResult[0].total : 0;
|
||||
|
||||
res.json({
|
||||
actionCounts,
|
||||
activeUsers,
|
||||
period: '7d'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching activity stats:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch activity statistics' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -3,6 +3,7 @@ const passport = require('passport');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authConfig = require('../config/auth');
|
||||
const { requireAuth, generateToken } = require('../middleware/auth');
|
||||
const { logActivity, ACTIONS } = require('../services/activityLogger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@ -49,6 +50,10 @@ router.get('/google/callback',
|
||||
// Set auth cookie
|
||||
res.cookie(authConfig.cookie.name, token, authConfig.cookie.options);
|
||||
|
||||
// Log login activity
|
||||
const db = req.app.locals.db;
|
||||
await logActivity(db, user._id, ACTIONS.LOGIN, { method: 'google' }, req);
|
||||
|
||||
// Redirect to frontend
|
||||
res.redirect(process.env.FRONTEND_URL);
|
||||
} catch (err) {
|
||||
@ -80,7 +85,11 @@ router.get('/me', requireAuth, (req, res) => {
|
||||
* Clears authentication cookie
|
||||
* Protected route - requires valid JWT
|
||||
*/
|
||||
router.post('/logout', requireAuth, (req, res) => {
|
||||
router.post('/logout', requireAuth, async (req, res) => {
|
||||
// Log logout activity before clearing cookie
|
||||
const db = req.app.locals.db;
|
||||
await logActivity(db, req.user._id, ACTIONS.LOGOUT, {}, req);
|
||||
|
||||
// Clear the auth cookie
|
||||
res.clearCookie(authConfig.cookie.name, {
|
||||
...authConfig.cookie.options,
|
||||
|
||||
Reference in New Issue
Block a user