feat(activity-log): integrate logging across all endpoints (Phase 3)
Added comprehensive activity logging to 14 integration points: Auth Controller (4 actions): - AUTH_REGISTER: User registration - AUTH_LOGIN: User login - AUTH_VERIFY_EMAIL: Email verification (token & code) - AUTH_PASSWORD_RESET: Password reset Events Routes (2 actions): - EVENT_CHECKIN: User checks into event - EVENT_LEAVE: User leaves event Socket Handlers (3 actions): - EVENT_JOIN_CHAT: User joins event chat room - EVENT_LEAVE_CHAT: User leaves event chat room - CHAT_JOIN_ROOM: User joins private match chat Matches Routes (3 actions): - MATCH_CREATE: Match request created - MATCH_ACCEPT: Match request accepted - MATCH_REJECT: Match request rejected/cancelled Admin Routes (1 action + security): - ADMIN_MATCHING_RUN: Admin runs matching algorithm - Added requireAdmin middleware to all admin routes All logs include: - User ID and username (denormalized) - IP address (X-Forwarded-For aware) - Action type and resource ID - HTTP method and path (or SOCKET for WebSocket) - Contextual metadata (event slugs, match IDs, etc.) Fire-and-forget pattern ensures logging never blocks requests.
This commit is contained in:
@@ -10,6 +10,8 @@ const {
|
|||||||
const { sendVerificationEmail, sendWelcomeEmail, sendPasswordResetEmail } = require('../emails');
|
const { sendVerificationEmail, sendWelcomeEmail, sendPasswordResetEmail } = require('../emails');
|
||||||
const { sanitizeForEmail, timingSafeEqual } = require('../utils/sanitize');
|
const { sanitizeForEmail, timingSafeEqual } = require('../utils/sanitize');
|
||||||
const securityConfig = require('../config/security');
|
const securityConfig = require('../config/security');
|
||||||
|
const { ACTIONS, log: activityLog } = require('../services/activityLog');
|
||||||
|
const { getClientIP } = require('../utils/request');
|
||||||
|
|
||||||
// Register new user (Phase 1.5 - with WSDC support and email verification)
|
// Register new user (Phase 1.5 - with WSDC support and email verification)
|
||||||
async function register(req, res, next) {
|
async function register(req, res, next) {
|
||||||
@@ -92,6 +94,20 @@ async function register(req, res, next) {
|
|||||||
// Generate JWT token
|
// Generate JWT token
|
||||||
const token = generateToken({ userId: user.id });
|
const token = generateToken({ userId: user.id });
|
||||||
|
|
||||||
|
// Log registration activity
|
||||||
|
activityLog({
|
||||||
|
userId: user.id,
|
||||||
|
username: user.username,
|
||||||
|
ipAddress: getClientIP(req),
|
||||||
|
action: ACTIONS.AUTH_REGISTER,
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
metadata: {
|
||||||
|
email: user.email,
|
||||||
|
hasWsdcId: !!user.wsdcId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'User registered successfully. Please check your email to verify your account.',
|
message: 'User registered successfully. Please check your email to verify your account.',
|
||||||
@@ -199,6 +215,20 @@ async function login(req, res, next) {
|
|||||||
// Return user without password
|
// Return user without password
|
||||||
const { passwordHash, ...userWithoutPassword } = user;
|
const { passwordHash, ...userWithoutPassword } = user;
|
||||||
|
|
||||||
|
// Log login activity
|
||||||
|
activityLog({
|
||||||
|
userId: user.id,
|
||||||
|
username: user.username,
|
||||||
|
ipAddress: getClientIP(req),
|
||||||
|
action: ACTIONS.AUTH_LOGIN,
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
metadata: {
|
||||||
|
email: user.email,
|
||||||
|
emailVerified: user.emailVerified,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Login successful',
|
message: 'Login successful',
|
||||||
@@ -276,6 +306,19 @@ async function verifyEmailByToken(req, res, next) {
|
|||||||
// Remove sensitive data
|
// Remove sensitive data
|
||||||
const { passwordHash, verificationToken, verificationCode, verificationTokenExpiry, resetToken, resetTokenExpiry, ...userWithoutPassword } = updatedUser;
|
const { passwordHash, verificationToken, verificationCode, verificationTokenExpiry, resetToken, resetTokenExpiry, ...userWithoutPassword } = updatedUser;
|
||||||
|
|
||||||
|
// Log email verification activity
|
||||||
|
activityLog({
|
||||||
|
userId: updatedUser.id,
|
||||||
|
username: updatedUser.username,
|
||||||
|
ipAddress: getClientIP(req),
|
||||||
|
action: ACTIONS.AUTH_VERIFY_EMAIL,
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
metadata: {
|
||||||
|
verificationMethod: 'token',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Email verified successfully!',
|
message: 'Email verified successfully!',
|
||||||
@@ -356,6 +399,19 @@ async function verifyEmailByCode(req, res, next) {
|
|||||||
// Remove sensitive data
|
// Remove sensitive data
|
||||||
const { passwordHash, verificationToken, verificationCode, verificationTokenExpiry, resetToken, resetTokenExpiry, ...userWithoutPassword } = updatedUser;
|
const { passwordHash, verificationToken, verificationCode, verificationTokenExpiry, resetToken, resetTokenExpiry, ...userWithoutPassword } = updatedUser;
|
||||||
|
|
||||||
|
// Log email verification activity
|
||||||
|
activityLog({
|
||||||
|
userId: updatedUser.id,
|
||||||
|
username: updatedUser.username,
|
||||||
|
ipAddress: getClientIP(req),
|
||||||
|
action: ACTIONS.AUTH_VERIFY_EMAIL,
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
metadata: {
|
||||||
|
verificationMethod: 'code',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Email verified successfully!',
|
message: 'Email verified successfully!',
|
||||||
@@ -543,6 +599,19 @@ async function resetPassword(req, res, next) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Log password reset activity
|
||||||
|
activityLog({
|
||||||
|
userId: user.id,
|
||||||
|
username: user.username,
|
||||||
|
ipAddress: getClientIP(req),
|
||||||
|
action: ACTIONS.AUTH_PASSWORD_RESET,
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
metadata: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Password reset successfully',
|
message: 'Password reset successfully',
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { prisma } = require('../utils/db');
|
const { prisma } = require('../utils/db');
|
||||||
const { authenticate } = require('../middleware/auth');
|
const { authenticate } = require('../middleware/auth');
|
||||||
|
const { requireAdmin } = require('../middleware/admin');
|
||||||
const matchingService = require('../services/matching');
|
const matchingService = require('../services/matching');
|
||||||
const { SUGGESTION_STATUS } = require('../constants');
|
const { SUGGESTION_STATUS } = require('../constants');
|
||||||
|
const { ACTIONS, log: activityLog } = require('../services/activityLog');
|
||||||
|
const { getClientIP } = require('../utils/request');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// POST /api/admin/events/:slug/run-now - Trigger matching immediately for an event
|
// POST /api/admin/events/:slug/run-now - Trigger matching immediately for an event
|
||||||
router.post('/events/:slug/run-now', authenticate, async (req, res, next) => {
|
router.post('/events/:slug/run-now', authenticate, requireAdmin, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { slug } = req.params;
|
const { slug } = req.params;
|
||||||
|
|
||||||
@@ -47,6 +50,23 @@ router.post('/events/:slug/run-now', authenticate, async (req, res, next) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Log admin matching run activity
|
||||||
|
activityLog({
|
||||||
|
userId: req.user.id,
|
||||||
|
username: req.user.username,
|
||||||
|
ipAddress: getClientIP(req),
|
||||||
|
action: ACTIONS.ADMIN_MATCHING_RUN,
|
||||||
|
resource: `event:${event.id}`,
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
metadata: {
|
||||||
|
eventSlug: event.slug,
|
||||||
|
runId: runRow.id,
|
||||||
|
matchedCount,
|
||||||
|
notFoundCount,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
@@ -74,7 +94,7 @@ router.post('/events/:slug/run-now', authenticate, async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/admin/events/:slug/matching-runs?limit=20 - List recent runs
|
// GET /api/admin/events/:slug/matching-runs?limit=20 - List recent runs
|
||||||
router.get('/events/:slug/matching-runs', authenticate, async (req, res, next) => {
|
router.get('/events/:slug/matching-runs', authenticate, requireAdmin, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { slug } = req.params;
|
const { slug } = req.params;
|
||||||
const limit = Math.min(parseInt(req.query.limit || '20', 10), 100);
|
const limit = Math.min(parseInt(req.query.limit || '20', 10), 100);
|
||||||
@@ -136,7 +156,7 @@ router.get('/events/:slug/matching-runs', authenticate, async (req, res, next) =
|
|||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
// GET /api/admin/events/:slug/matching-runs/:runId/suggestions - List suggestions created in this run
|
// GET /api/admin/events/:slug/matching-runs/:runId/suggestions - List suggestions created in this run
|
||||||
router.get('/events/:slug/matching-runs/:runId/suggestions', authenticate, async (req, res, next) => {
|
router.get('/events/:slug/matching-runs/:runId/suggestions', authenticate, requireAdmin, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { slug, runId } = req.params;
|
const { slug, runId } = req.params;
|
||||||
const onlyAssigned = String(req.query.onlyAssigned || 'true') === 'true';
|
const onlyAssigned = String(req.query.onlyAssigned || 'true') === 'true';
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ const { authenticate } = require('../middleware/auth');
|
|||||||
const { getIO } = require('../socket');
|
const { getIO } = require('../socket');
|
||||||
const matchingService = require('../services/matching');
|
const matchingService = require('../services/matching');
|
||||||
const { SUGGESTION_STATUS, MATCH_STATUS } = require('../constants');
|
const { SUGGESTION_STATUS, MATCH_STATUS } = require('../constants');
|
||||||
|
const { ACTIONS, log: activityLog } = require('../services/activityLog');
|
||||||
|
const { getClientIP } = require('../utils/request');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -319,6 +321,21 @@ router.post('/checkin/:token', authenticate, async (req, res, next) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Log checkin activity
|
||||||
|
activityLog({
|
||||||
|
userId: req.user.id,
|
||||||
|
username: req.user.username,
|
||||||
|
ipAddress: getClientIP(req),
|
||||||
|
action: ACTIONS.EVENT_CHECKIN,
|
||||||
|
resource: `event:${event.id}`,
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
metadata: {
|
||||||
|
eventSlug: event.slug,
|
||||||
|
eventName: event.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
alreadyCheckedIn: false,
|
alreadyCheckedIn: false,
|
||||||
@@ -491,6 +508,21 @@ router.delete('/:slug/leave', authenticate, async (req, res, next) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Log leave event activity
|
||||||
|
activityLog({
|
||||||
|
userId: req.user.id,
|
||||||
|
username: req.user.username,
|
||||||
|
ipAddress: getClientIP(req),
|
||||||
|
action: ACTIONS.EVENT_LEAVE,
|
||||||
|
resource: `event:${event.id}`,
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
metadata: {
|
||||||
|
eventSlug: event.slug,
|
||||||
|
eventName: event.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Successfully left the event',
|
message: 'Successfully left the event',
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ const { authenticate } = require('../middleware/auth');
|
|||||||
const { getIO } = require('../socket');
|
const { getIO } = require('../socket');
|
||||||
const { MATCH_STATUS } = require('../constants');
|
const { MATCH_STATUS } = require('../constants');
|
||||||
const matchingService = require('../services/matching');
|
const matchingService = require('../services/matching');
|
||||||
|
const { ACTIONS, log: activityLog } = require('../services/activityLog');
|
||||||
|
const { getClientIP } = require('../utils/request');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -184,6 +186,22 @@ router.post('/', authenticate, matchRequestLimiter, async (req, res, next) => {
|
|||||||
console.error('Failed to emit match request notification:', socketError);
|
console.error('Failed to emit match request notification:', socketError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log match creation activity
|
||||||
|
activityLog({
|
||||||
|
userId: req.user.id,
|
||||||
|
username: req.user.username,
|
||||||
|
ipAddress: getClientIP(req),
|
||||||
|
action: ACTIONS.MATCH_CREATE,
|
||||||
|
resource: `match:${match.id}`,
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
metadata: {
|
||||||
|
matchSlug: match.slug,
|
||||||
|
targetUserId: targetUserId,
|
||||||
|
eventSlug: eventSlug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: match,
|
data: match,
|
||||||
@@ -778,6 +796,22 @@ router.put('/:slug/accept', authenticate, async (req, res, next) => {
|
|||||||
const isUser1 = updatedMatch.user1Id === userId;
|
const isUser1 = updatedMatch.user1Id === userId;
|
||||||
const partner = isUser1 ? updatedMatch.user2 : updatedMatch.user1;
|
const partner = isUser1 ? updatedMatch.user2 : updatedMatch.user1;
|
||||||
|
|
||||||
|
// Log match acceptance activity
|
||||||
|
activityLog({
|
||||||
|
userId: req.user.id,
|
||||||
|
username: req.user.username,
|
||||||
|
ipAddress: getClientIP(req),
|
||||||
|
action: ACTIONS.MATCH_ACCEPT,
|
||||||
|
resource: `match:${updatedMatch.id}`,
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
metadata: {
|
||||||
|
matchSlug: updatedMatch.slug,
|
||||||
|
partnerId: partner.id,
|
||||||
|
eventSlug: updatedMatch.event.slug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
@@ -867,6 +901,22 @@ router.delete('/:slug', authenticate, async (req, res, next) => {
|
|||||||
console.error('Failed to emit match cancelled notification:', socketError);
|
console.error('Failed to emit match cancelled notification:', socketError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log match rejection activity
|
||||||
|
activityLog({
|
||||||
|
userId: req.user.id,
|
||||||
|
username: req.user.username,
|
||||||
|
ipAddress: getClientIP(req),
|
||||||
|
action: ACTIONS.MATCH_REJECT,
|
||||||
|
resource: `match:${match.id}`,
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
metadata: {
|
||||||
|
matchSlug: match.slug,
|
||||||
|
eventSlug: match.event.slug,
|
||||||
|
matchStatus: match.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Match deleted successfully',
|
message: 'Match deleted successfully',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const { Server } = require('socket.io');
|
const { Server } = require('socket.io');
|
||||||
const { verifyToken } = require('../utils/auth');
|
const { verifyToken } = require('../utils/auth');
|
||||||
const { prisma } = require('../utils/db');
|
const { prisma } = require('../utils/db');
|
||||||
|
const { ACTIONS, log: activityLog } = require('../services/activityLog');
|
||||||
|
|
||||||
// Track active users in each event room
|
// Track active users in each event room
|
||||||
const activeUsers = new Map(); // eventId -> Set of { socketId, userId, username, avatar }
|
const activeUsers = new Map(); // eventId -> Set of { socketId, userId, username, avatar }
|
||||||
@@ -202,6 +203,20 @@ function initializeSocket(httpServer) {
|
|||||||
username: socket.user.username,
|
username: socket.user.username,
|
||||||
avatar: socket.user.avatar,
|
avatar: socket.user.avatar,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Log join event chat activity
|
||||||
|
activityLog({
|
||||||
|
userId: socket.user.id,
|
||||||
|
username: socket.user.username,
|
||||||
|
ipAddress: socket.handshake.address,
|
||||||
|
action: ACTIONS.EVENT_JOIN_CHAT,
|
||||||
|
resource: `event:${eventId}`,
|
||||||
|
method: 'SOCKET',
|
||||||
|
path: 'join_event_room',
|
||||||
|
metadata: {
|
||||||
|
eventSlug: slug,
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Join event room error:', error);
|
console.error('Join event room error:', error);
|
||||||
socket.emit('error', { message: 'Failed to join room' });
|
socket.emit('error', { message: 'Failed to join room' });
|
||||||
@@ -236,6 +251,20 @@ function initializeSocket(httpServer) {
|
|||||||
|
|
||||||
console.log(`👤 ${socket.user.username} left event room ${socket.currentEventSlug}`);
|
console.log(`👤 ${socket.user.username} left event room ${socket.currentEventSlug}`);
|
||||||
|
|
||||||
|
// Log leave event chat activity
|
||||||
|
activityLog({
|
||||||
|
userId: socket.user.id,
|
||||||
|
username: socket.user.username,
|
||||||
|
ipAddress: socket.handshake.address,
|
||||||
|
action: ACTIONS.EVENT_LEAVE_CHAT,
|
||||||
|
resource: `event:${eventId}`,
|
||||||
|
method: 'SOCKET',
|
||||||
|
path: 'leave_event_room',
|
||||||
|
metadata: {
|
||||||
|
eventSlug: socket.currentEventSlug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Clear current event data
|
// Clear current event data
|
||||||
socket.currentEventId = null;
|
socket.currentEventId = null;
|
||||||
socket.currentEventRoom = null;
|
socket.currentEventRoom = null;
|
||||||
@@ -349,6 +378,20 @@ function initializeSocket(httpServer) {
|
|||||||
where: { id: parseInt(matchId) },
|
where: { id: parseInt(matchId) },
|
||||||
data: updateData,
|
data: updateData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Log join match room activity
|
||||||
|
activityLog({
|
||||||
|
userId: socket.user.id,
|
||||||
|
username: socket.user.username,
|
||||||
|
ipAddress: socket.handshake.address,
|
||||||
|
action: ACTIONS.CHAT_JOIN_ROOM,
|
||||||
|
resource: `match:${matchId}`,
|
||||||
|
method: 'SOCKET',
|
||||||
|
path: 'join_match_room',
|
||||||
|
metadata: {
|
||||||
|
matchId: parseInt(matchId),
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Join match room error:', error);
|
console.error('Join match room error:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user