diff --git a/backend/src/controllers/auth.js b/backend/src/controllers/auth.js index a32ffea..6d2076e 100644 --- a/backend/src/controllers/auth.js +++ b/backend/src/controllers/auth.js @@ -10,6 +10,8 @@ const { const { sendVerificationEmail, sendWelcomeEmail, sendPasswordResetEmail } = require('../emails'); const { sanitizeForEmail, timingSafeEqual } = require('../utils/sanitize'); 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) async function register(req, res, next) { @@ -92,6 +94,20 @@ async function register(req, res, next) { // Generate JWT token 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({ success: true, 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 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({ success: true, message: 'Login successful', @@ -276,6 +306,19 @@ async function verifyEmailByToken(req, res, next) { // Remove sensitive data 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({ success: true, message: 'Email verified successfully!', @@ -356,6 +399,19 @@ async function verifyEmailByCode(req, res, next) { // Remove sensitive data 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({ success: true, 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({ success: true, message: 'Password reset successfully', diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index 45f3395..3de8d2a 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -1,13 +1,16 @@ const express = require('express'); const { prisma } = require('../utils/db'); const { authenticate } = require('../middleware/auth'); +const { requireAdmin } = require('../middleware/admin'); const matchingService = require('../services/matching'); const { SUGGESTION_STATUS } = require('../constants'); +const { ACTIONS, log: activityLog } = require('../services/activityLog'); +const { getClientIP } = require('../utils/request'); const router = express.Router(); // 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 { 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({ success: true, 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 -router.get('/events/:slug/matching-runs', authenticate, async (req, res, next) => { +router.get('/events/:slug/matching-runs', authenticate, requireAdmin, async (req, res, next) => { try { const { slug } = req.params; 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; // 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 { const { slug, runId } = req.params; const onlyAssigned = String(req.query.onlyAssigned || 'true') === 'true'; diff --git a/backend/src/routes/events.js b/backend/src/routes/events.js index aa72228..329a305 100644 --- a/backend/src/routes/events.js +++ b/backend/src/routes/events.js @@ -4,6 +4,8 @@ const { authenticate } = require('../middleware/auth'); const { getIO } = require('../socket'); const matchingService = require('../services/matching'); const { SUGGESTION_STATUS, MATCH_STATUS } = require('../constants'); +const { ACTIONS, log: activityLog } = require('../services/activityLog'); +const { getClientIP } = require('../utils/request'); 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({ success: true, 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({ success: true, message: 'Successfully left the event', diff --git a/backend/src/routes/matches.js b/backend/src/routes/matches.js index c044918..f27aae1 100644 --- a/backend/src/routes/matches.js +++ b/backend/src/routes/matches.js @@ -5,6 +5,8 @@ const { authenticate } = require('../middleware/auth'); const { getIO } = require('../socket'); const { MATCH_STATUS } = require('../constants'); const matchingService = require('../services/matching'); +const { ACTIONS, log: activityLog } = require('../services/activityLog'); +const { getClientIP } = require('../utils/request'); 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); } + // 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({ success: true, data: match, @@ -778,6 +796,22 @@ router.put('/:slug/accept', authenticate, async (req, res, next) => { const isUser1 = updatedMatch.user1Id === userId; 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({ success: true, data: { @@ -867,6 +901,22 @@ router.delete('/:slug', authenticate, async (req, res, next) => { 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({ success: true, message: 'Match deleted successfully', diff --git a/backend/src/socket/index.js b/backend/src/socket/index.js index d653cec..fce5242 100644 --- a/backend/src/socket/index.js +++ b/backend/src/socket/index.js @@ -1,6 +1,7 @@ const { Server } = require('socket.io'); const { verifyToken } = require('../utils/auth'); const { prisma } = require('../utils/db'); +const { ACTIONS, log: activityLog } = require('../services/activityLog'); // Track active users in each event room const activeUsers = new Map(); // eventId -> Set of { socketId, userId, username, avatar } @@ -202,6 +203,20 @@ function initializeSocket(httpServer) { username: socket.user.username, 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) { console.error('Join event room error:', error); 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}`); + // 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 socket.currentEventId = null; socket.currentEventRoom = null; @@ -349,6 +378,20 @@ function initializeSocket(httpServer) { where: { id: parseInt(matchId) }, 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) { console.error('Join match room error:', error);