Audit logging feature files
This commit is contained in:
114
backend/routes/auditLog.js
Normal file
114
backend/routes/auditLog.js
Normal file
@@ -0,0 +1,114 @@
|
||||
// Audit Log Routes (Admin only)
|
||||
const express = require('express');
|
||||
|
||||
function createAuditLogRouter(db, requireAuth, requireRole) {
|
||||
const router = express.Router();
|
||||
|
||||
// All routes require admin role
|
||||
router.use(requireAuth(db), requireRole('admin'));
|
||||
|
||||
// Get paginated audit logs with filters
|
||||
router.get('/', async (req, res) => {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 25,
|
||||
user,
|
||||
action,
|
||||
entityType,
|
||||
startDate,
|
||||
endDate
|
||||
} = req.query;
|
||||
|
||||
const offset = (Math.max(1, parseInt(page)) - 1) * parseInt(limit);
|
||||
const pageSize = Math.min(100, Math.max(1, parseInt(limit)));
|
||||
|
||||
let where = [];
|
||||
let params = [];
|
||||
|
||||
if (user) {
|
||||
where.push('username LIKE ?');
|
||||
params.push(`%${user}%`);
|
||||
}
|
||||
if (action) {
|
||||
where.push('action = ?');
|
||||
params.push(action);
|
||||
}
|
||||
if (entityType) {
|
||||
where.push('entity_type = ?');
|
||||
params.push(entityType);
|
||||
}
|
||||
if (startDate) {
|
||||
where.push('created_at >= ?');
|
||||
params.push(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
where.push('created_at <= ?');
|
||||
params.push(endDate + ' 23:59:59');
|
||||
}
|
||||
|
||||
const whereClause = where.length > 0 ? 'WHERE ' + where.join(' AND ') : '';
|
||||
|
||||
try {
|
||||
// Get total count
|
||||
const countRow = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
`SELECT COUNT(*) as total FROM audit_logs ${whereClause}`,
|
||||
params,
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Get paginated results
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
db.all(
|
||||
`SELECT * FROM audit_logs ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
|
||||
[...params, pageSize, offset],
|
||||
(err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
res.json({
|
||||
logs: rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: pageSize,
|
||||
total: countRow.total,
|
||||
totalPages: Math.ceil(countRow.total / pageSize)
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Audit log query error:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch audit logs' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get distinct action types for filter dropdown
|
||||
router.get('/actions', async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
db.all(
|
||||
'SELECT DISTINCT action FROM audit_logs ORDER BY action',
|
||||
(err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
res.json(rows.map(r => r.action));
|
||||
} catch (err) {
|
||||
console.error('Audit log actions error:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch actions' });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
module.exports = createAuditLogRouter;
|
||||
@@ -3,7 +3,7 @@ const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const crypto = require('crypto');
|
||||
|
||||
function createAuthRouter(db) {
|
||||
function createAuthRouter(db, logAudit) {
|
||||
const router = express.Router();
|
||||
|
||||
// Login
|
||||
@@ -28,16 +28,43 @@ function createAuthRouter(db) {
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
logAudit(db, {
|
||||
userId: null,
|
||||
username: username,
|
||||
action: 'login_failed',
|
||||
entityType: 'auth',
|
||||
entityId: null,
|
||||
details: { reason: 'user_not_found' },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
return res.status(401).json({ error: 'Invalid username or password' });
|
||||
}
|
||||
|
||||
if (!user.is_active) {
|
||||
logAudit(db, {
|
||||
userId: user.id,
|
||||
username: username,
|
||||
action: 'login_failed',
|
||||
entityType: 'auth',
|
||||
entityId: null,
|
||||
details: { reason: 'account_disabled' },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
return res.status(401).json({ error: 'Account is disabled' });
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const validPassword = await bcrypt.compare(password, user.password_hash);
|
||||
if (!validPassword) {
|
||||
logAudit(db, {
|
||||
userId: user.id,
|
||||
username: username,
|
||||
action: 'login_failed',
|
||||
entityType: 'auth',
|
||||
entityId: null,
|
||||
details: { reason: 'invalid_password' },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
return res.status(401).json({ error: 'Invalid username or password' });
|
||||
}
|
||||
|
||||
@@ -77,6 +104,16 @@ function createAuthRouter(db) {
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
});
|
||||
|
||||
logAudit(db, {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
action: 'login',
|
||||
entityType: 'auth',
|
||||
entityId: null,
|
||||
details: { role: user.role },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: 'Login successful',
|
||||
user: {
|
||||
@@ -97,6 +134,17 @@ function createAuthRouter(db) {
|
||||
const sessionId = req.cookies?.session_id;
|
||||
|
||||
if (sessionId) {
|
||||
// Look up user before deleting session
|
||||
const session = await new Promise((resolve) => {
|
||||
db.get(
|
||||
`SELECT u.id as user_id, u.username FROM sessions s
|
||||
JOIN users u ON s.user_id = u.id
|
||||
WHERE s.session_id = ?`,
|
||||
[sessionId],
|
||||
(err, row) => resolve(row || null)
|
||||
);
|
||||
});
|
||||
|
||||
// Delete session from database
|
||||
await new Promise((resolve) => {
|
||||
db.run(
|
||||
@@ -105,6 +153,18 @@ function createAuthRouter(db) {
|
||||
() => resolve()
|
||||
);
|
||||
});
|
||||
|
||||
if (session) {
|
||||
logAudit(db, {
|
||||
userId: session.user_id,
|
||||
username: session.username,
|
||||
action: 'logout',
|
||||
entityType: 'auth',
|
||||
entityId: null,
|
||||
details: null,
|
||||
ipAddress: req.ip
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Clear cookie
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
function createUsersRouter(db, requireAuth, requireRole) {
|
||||
function createUsersRouter(db, requireAuth, requireRole, logAudit) {
|
||||
const router = express.Router();
|
||||
|
||||
// All routes require admin role
|
||||
@@ -81,6 +81,16 @@ function createUsersRouter(db, requireAuth, requireRole) {
|
||||
);
|
||||
});
|
||||
|
||||
logAudit(db, {
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'user_create',
|
||||
entityType: 'user',
|
||||
entityId: String(result.id),
|
||||
details: { created_username: username, role: role || 'viewer' },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
message: 'User created successfully',
|
||||
user: {
|
||||
@@ -160,6 +170,23 @@ function createUsersRouter(db, requireAuth, requireRole) {
|
||||
);
|
||||
});
|
||||
|
||||
const updatedFields = {};
|
||||
if (username) updatedFields.username = username;
|
||||
if (email) updatedFields.email = email;
|
||||
if (role) updatedFields.role = role;
|
||||
if (typeof is_active === 'boolean') updatedFields.is_active = is_active;
|
||||
if (password) updatedFields.password_changed = true;
|
||||
|
||||
logAudit(db, {
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'user_update',
|
||||
entityType: 'user',
|
||||
entityId: String(userId),
|
||||
details: updatedFields,
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
// If user was deactivated, delete their sessions
|
||||
if (is_active === false) {
|
||||
await new Promise((resolve) => {
|
||||
@@ -187,6 +214,14 @@ function createUsersRouter(db, requireAuth, requireRole) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Look up the user before deleting
|
||||
const targetUser = await new Promise((resolve, reject) => {
|
||||
db.get('SELECT username FROM users WHERE id = ?', [userId], (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
// Delete sessions first (foreign key)
|
||||
await new Promise((resolve) => {
|
||||
db.run('DELETE FROM sessions WHERE user_id = ?', [userId], () => resolve());
|
||||
@@ -204,6 +239,16 @@ function createUsersRouter(db, requireAuth, requireRole) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
logAudit(db, {
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'user_delete',
|
||||
entityType: 'user',
|
||||
entityId: String(userId),
|
||||
details: { deleted_username: targetUser ? targetUser.username : 'unknown' },
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
res.json({ message: 'User deleted successfully' });
|
||||
} catch (err) {
|
||||
console.error('Delete user error:', err);
|
||||
|
||||
Reference in New Issue
Block a user