From a49bff56daa80dff5c073b93b0a63b56fd677571 Mon Sep 17 00:00:00 2001 From: richardtekula Date: Thu, 4 Dec 2025 10:33:04 +0100 Subject: [PATCH] Add audit logging for CRUD operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extend audit.service.js with logging functions for projects, todos, companies, time tracking, and auth - Create audit.controller.js for fetching recent audit logs with user info - Create audit.routes.js with GET /api/audit-logs endpoint - Add audit logging to project, todo, company, time-tracking, and auth controllers - Log create/delete operations for projects, todos, companies - Log timer start/stop for time tracking - Log login/logout events 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/app.js | 2 + src/controllers/audit.controller.js | 43 +++++++ src/controllers/auth.controller.js | 10 ++ src/controllers/company.controller.js | 12 ++ src/controllers/project.controller.js | 12 ++ src/controllers/time-tracking.controller.js | 7 + src/controllers/todo.controller.js | 21 ++- src/routes/audit.routes.js | 9 ++ src/services/audit.service.js | 136 ++++++++++++++++++++ 9 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 src/controllers/audit.controller.js create mode 100644 src/routes/audit.routes.js diff --git a/src/app.js b/src/app.js index 0eac68d..0aa3e2e 100644 --- a/src/app.js +++ b/src/app.js @@ -23,6 +23,7 @@ import projectRoutes from './routes/project.routes.js'; import todoRoutes from './routes/todo.routes.js'; import timeTrackingRoutes from './routes/time-tracking.routes.js'; import noteRoutes from './routes/note.routes.js'; +import auditRoutes from './routes/audit.routes.js'; const app = express(); @@ -84,6 +85,7 @@ app.use('/api/projects', projectRoutes); app.use('/api/todos', todoRoutes); app.use('/api/time-tracking', timeTrackingRoutes); app.use('/api/notes', noteRoutes); +app.use('/api/audit-logs', auditRoutes); // Basic route app.get('/', (req, res) => { diff --git a/src/controllers/audit.controller.js b/src/controllers/audit.controller.js new file mode 100644 index 0000000..af9daf4 --- /dev/null +++ b/src/controllers/audit.controller.js @@ -0,0 +1,43 @@ +import { db } from '../config/database.js'; +import { auditLogs, users } from '../db/schema.js'; +import { desc, eq } from 'drizzle-orm'; + +export const getRecentAuditLogs = async (req, res, next) => { + try { + const { limit = 20, userId } = req.query; + + let query = db + .select({ + id: auditLogs.id, + userId: auditLogs.userId, + action: auditLogs.action, + resource: auditLogs.resource, + resourceId: auditLogs.resourceId, + oldValue: auditLogs.oldValue, + newValue: auditLogs.newValue, + success: auditLogs.success, + createdAt: auditLogs.createdAt, + // User info + userFirstName: users.firstName, + userLastName: users.lastName, + username: users.username, + }) + .from(auditLogs) + .leftJoin(users, eq(auditLogs.userId, users.id)) + .orderBy(desc(auditLogs.createdAt)) + .limit(parseInt(limit)); + + if (userId) { + query = query.where(eq(auditLogs.userId, userId)); + } + + const logs = await query; + + res.json({ + success: true, + data: logs, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index e12d50e..b47f61c 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -3,6 +3,8 @@ import { logLoginAttempt, logPasswordChange, logEmailLink, + logLogin, + logLogout, } from '../services/audit.service.js'; /** @@ -24,6 +26,7 @@ export const login = async (req, res, next) => { // Log successful login await logLoginAttempt(username, true, ipAddress, userAgent); + await logLogin(result.user.id, username, ipAddress, userAgent); // Nastav cookie s access tokenom (httpOnly, secure) res.cookie('accessToken', result.tokens.accessToken, { @@ -143,6 +146,13 @@ export const skipEmail = async (req, res, next) => { */ export const logout = async (req, res, next) => { try { + const userId = req.userId; + const ipAddress = req.ip || req.connection.remoteAddress; + const userAgent = req.headers['user-agent']; + + // Log logout event + await logLogout(userId, ipAddress, userAgent); + const result = await authService.logout(); // Vymaž cookies diff --git a/src/controllers/company.controller.js b/src/controllers/company.controller.js index 8b31469..3ce84b4 100644 --- a/src/controllers/company.controller.js +++ b/src/controllers/company.controller.js @@ -2,6 +2,7 @@ import * as companyService from '../services/company.service.js'; import * as noteService from '../services/note.service.js'; import * as companyReminderService from '../services/company-reminder.service.js'; import * as companyEmailService from '../services/company-email.service.js'; +import { logCompanyCreated, logCompanyDeleted } from '../services/audit.service.js'; /** * Get all companies @@ -114,6 +115,9 @@ export const createCompany = async (req, res, next) => { const company = await companyService.createCompany(userId, data); + // Log audit event + await logCompanyCreated(userId, company.id, company.name, req.ip, req.headers['user-agent']); + res.status(201).json({ success: true, data: company, @@ -153,9 +157,17 @@ export const updateCompany = async (req, res, next) => { export const deleteCompany = async (req, res, next) => { try { const { companyId } = req.params; + const userId = req.userId; + + // Get company info before deleting + const company = await companyService.getCompanyById(companyId); + const companyName = company?.name; const result = await companyService.deleteCompany(companyId); + // Log audit event + await logCompanyDeleted(userId, companyId, companyName, req.ip, req.headers['user-agent']); + res.status(200).json({ success: true, message: result.message, diff --git a/src/controllers/project.controller.js b/src/controllers/project.controller.js index bf4bcec..9585e66 100644 --- a/src/controllers/project.controller.js +++ b/src/controllers/project.controller.js @@ -1,5 +1,6 @@ import * as projectService from '../services/project.service.js'; import * as noteService from '../services/note.service.js'; +import { logProjectCreated, logProjectDeleted } from '../services/audit.service.js'; /** * Get all projects @@ -71,6 +72,9 @@ export const createProject = async (req, res, next) => { const project = await projectService.createProject(userId, data); + // Log audit event + await logProjectCreated(userId, project.id, project.name, req.ip, req.headers['user-agent']); + res.status(201).json({ success: true, data: project, @@ -110,9 +114,17 @@ export const updateProject = async (req, res, next) => { export const deleteProject = async (req, res, next) => { try { const { projectId } = req.params; + const userId = req.userId; + + // Get project info before deleting + const project = await projectService.getProjectById(projectId); + const projectName = project?.name; const result = await projectService.deleteProject(projectId); + // Log audit event + await logProjectDeleted(userId, projectId, projectName, req.ip, req.headers['user-agent']); + res.status(200).json({ success: true, message: result.message, diff --git a/src/controllers/time-tracking.controller.js b/src/controllers/time-tracking.controller.js index 004a20a..c83d3a8 100644 --- a/src/controllers/time-tracking.controller.js +++ b/src/controllers/time-tracking.controller.js @@ -1,4 +1,5 @@ import * as timeTrackingService from '../services/time-tracking.service.js'; +import { logTimerStarted, logTimerStopped } from '../services/audit.service.js'; /** * Start a new time entry @@ -16,6 +17,9 @@ export const startTimeEntry = async (req, res, next) => { description, }); + // Log audit event + await logTimerStarted(userId, entry.id, description || 'Timer', req.ip, req.headers['user-agent']); + res.status(201).json({ success: true, data: entry, @@ -43,6 +47,9 @@ export const stopTimeEntry = async (req, res, next) => { description, }); + // Log audit event + await logTimerStopped(userId, entry.id, description || 'Timer', entry.duration, req.ip, req.headers['user-agent']); + res.status(200).json({ success: true, data: entry, diff --git a/src/controllers/todo.controller.js b/src/controllers/todo.controller.js index c773300..c76e867 100644 --- a/src/controllers/todo.controller.js +++ b/src/controllers/todo.controller.js @@ -1,4 +1,5 @@ import * as todoService from '../services/todo.service.js'; +import { logTodoCreated, logTodoDeleted, logTodoCompleted } from '../services/audit.service.js'; /** * Get all todos @@ -112,6 +113,9 @@ export const createTodo = async (req, res, next) => { console.log('Backend received todo data:', data); const todo = await todoService.createTodo(userId, data); + // Log audit event + await logTodoCreated(userId, todo.id, todo.title, req.ip, req.headers['user-agent']); + res.status(201).json({ success: true, data: todo, @@ -152,9 +156,17 @@ export const updateTodo = async (req, res, next) => { export const deleteTodo = async (req, res, next) => { try { const { todoId } = req.params; + const userId = req.userId; + + // Get todo info before deleting + const todo = await todoService.getTodoById(todoId); + const todoTitle = todo?.title; const result = await todoService.deleteTodo(todoId); + // Log audit event + await logTodoDeleted(userId, todoId, todoTitle, req.ip, req.headers['user-agent']); + res.status(200).json({ success: true, message: result.message, @@ -171,15 +183,22 @@ export const deleteTodo = async (req, res, next) => { export const toggleTodo = async (req, res, next) => { try { const { todoId } = req.params; + const userId = req.userId; // Get current todo const todo = await todoService.getTodoById(todoId); + const wasCompleted = todo.status === 'completed'; // Toggle completed status const updated = await todoService.updateTodo(todoId, { - status: todo.status === 'completed' ? 'pending' : 'completed', + status: wasCompleted ? 'pending' : 'completed', }); + // Log audit event if todo was completed + if (!wasCompleted) { + await logTodoCompleted(userId, todoId, todo.title, req.ip, req.headers['user-agent']); + } + res.status(200).json({ success: true, data: updated, diff --git a/src/routes/audit.routes.js b/src/routes/audit.routes.js new file mode 100644 index 0000000..eeeb204 --- /dev/null +++ b/src/routes/audit.routes.js @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { getRecentAuditLogs } from '../controllers/audit.controller.js'; +import { authenticate } from '../middlewares/auth/authMiddleware.js'; + +const router = Router(); + +router.get('/', authenticate, getRecentAuditLogs); + +export default router; diff --git a/src/services/audit.service.js b/src/services/audit.service.js index e6c6fa4..6c7fbf3 100644 --- a/src/services/audit.service.js +++ b/src/services/audit.service.js @@ -106,3 +106,139 @@ export const logUserCreation = async (adminId, newUserId, username, role, ipAddr success: true, }); }; + +// Projects +export const logProjectCreated = async (userId, projectId, projectName, ipAddress, userAgent) => { + await logAuditEvent({ + userId, + action: 'project_created', + resource: 'project', + resourceId: projectId, + newValue: { name: projectName }, + ipAddress, + userAgent, + }); +}; + +export const logProjectDeleted = async (userId, projectId, projectName, ipAddress, userAgent) => { + await logAuditEvent({ + userId, + action: 'project_deleted', + resource: 'project', + resourceId: projectId, + oldValue: { name: projectName }, + ipAddress, + userAgent, + }); +}; + +// Todos +export const logTodoCreated = async (userId, todoId, todoTitle, ipAddress, userAgent) => { + await logAuditEvent({ + userId, + action: 'todo_created', + resource: 'todo', + resourceId: todoId, + newValue: { title: todoTitle }, + ipAddress, + userAgent, + }); +}; + +export const logTodoCompleted = async (userId, todoId, todoTitle, ipAddress, userAgent) => { + await logAuditEvent({ + userId, + action: 'todo_completed', + resource: 'todo', + resourceId: todoId, + newValue: { title: todoTitle }, + ipAddress, + userAgent, + }); +}; + +export const logTodoDeleted = async (userId, todoId, todoTitle, ipAddress, userAgent) => { + await logAuditEvent({ + userId, + action: 'todo_deleted', + resource: 'todo', + resourceId: todoId, + oldValue: { title: todoTitle }, + ipAddress, + userAgent, + }); +}; + +// Time Tracking +export const logTimerStarted = async (userId, entryId, projectName, ipAddress, userAgent) => { + await logAuditEvent({ + userId, + action: 'timer_started', + resource: 'time_entry', + resourceId: entryId, + newValue: { project: projectName }, + ipAddress, + userAgent, + }); +}; + +export const logTimerStopped = async (userId, entryId, projectName, duration, ipAddress, userAgent) => { + await logAuditEvent({ + userId, + action: 'timer_stopped', + resource: 'time_entry', + resourceId: entryId, + newValue: { project: projectName, duration }, + ipAddress, + userAgent, + }); +}; + +// Companies +export const logCompanyCreated = async (userId, companyId, companyName, ipAddress, userAgent) => { + await logAuditEvent({ + userId, + action: 'company_created', + resource: 'company', + resourceId: companyId, + newValue: { name: companyName }, + ipAddress, + userAgent, + }); +}; + +export const logCompanyDeleted = async (userId, companyId, companyName, ipAddress, userAgent) => { + await logAuditEvent({ + userId, + action: 'company_deleted', + resource: 'company', + resourceId: companyId, + oldValue: { name: companyName }, + ipAddress, + userAgent, + }); +}; + +// Auth +export const logLogin = async (userId, username, ipAddress, userAgent) => { + await logAuditEvent({ + userId, + action: 'login', + resource: 'auth', + resourceId: userId, + newValue: { username }, + ipAddress, + userAgent, + }); +}; + +export const logLogout = async (userId, ipAddress, userAgent) => { + await logAuditEvent({ + userId, + action: 'logout', + resource: 'auth', + resourceId: userId, + ipAddress, + userAgent, + }); +};