Add audit logging for CRUD operations

- 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 <noreply@anthropic.com>
This commit is contained in:
richardtekula
2025-12-04 10:33:04 +01:00
parent fa7129a5b4
commit a49bff56da
9 changed files with 251 additions and 1 deletions

View File

@@ -23,6 +23,7 @@ import projectRoutes from './routes/project.routes.js';
import todoRoutes from './routes/todo.routes.js'; import todoRoutes from './routes/todo.routes.js';
import timeTrackingRoutes from './routes/time-tracking.routes.js'; import timeTrackingRoutes from './routes/time-tracking.routes.js';
import noteRoutes from './routes/note.routes.js'; import noteRoutes from './routes/note.routes.js';
import auditRoutes from './routes/audit.routes.js';
const app = express(); const app = express();
@@ -84,6 +85,7 @@ app.use('/api/projects', projectRoutes);
app.use('/api/todos', todoRoutes); app.use('/api/todos', todoRoutes);
app.use('/api/time-tracking', timeTrackingRoutes); app.use('/api/time-tracking', timeTrackingRoutes);
app.use('/api/notes', noteRoutes); app.use('/api/notes', noteRoutes);
app.use('/api/audit-logs', auditRoutes);
// Basic route // Basic route
app.get('/', (req, res) => { app.get('/', (req, res) => {

View File

@@ -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);
}
};

View File

@@ -3,6 +3,8 @@ import {
logLoginAttempt, logLoginAttempt,
logPasswordChange, logPasswordChange,
logEmailLink, logEmailLink,
logLogin,
logLogout,
} from '../services/audit.service.js'; } from '../services/audit.service.js';
/** /**
@@ -24,6 +26,7 @@ export const login = async (req, res, next) => {
// Log successful login // Log successful login
await logLoginAttempt(username, true, ipAddress, userAgent); await logLoginAttempt(username, true, ipAddress, userAgent);
await logLogin(result.user.id, username, ipAddress, userAgent);
// Nastav cookie s access tokenom (httpOnly, secure) // Nastav cookie s access tokenom (httpOnly, secure)
res.cookie('accessToken', result.tokens.accessToken, { res.cookie('accessToken', result.tokens.accessToken, {
@@ -143,6 +146,13 @@ export const skipEmail = async (req, res, next) => {
*/ */
export const logout = async (req, res, next) => { export const logout = async (req, res, next) => {
try { 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(); const result = await authService.logout();
// Vymaž cookies // Vymaž cookies

View File

@@ -2,6 +2,7 @@ import * as companyService from '../services/company.service.js';
import * as noteService from '../services/note.service.js'; import * as noteService from '../services/note.service.js';
import * as companyReminderService from '../services/company-reminder.service.js'; import * as companyReminderService from '../services/company-reminder.service.js';
import * as companyEmailService from '../services/company-email.service.js'; import * as companyEmailService from '../services/company-email.service.js';
import { logCompanyCreated, logCompanyDeleted } from '../services/audit.service.js';
/** /**
* Get all companies * Get all companies
@@ -114,6 +115,9 @@ export const createCompany = async (req, res, next) => {
const company = await companyService.createCompany(userId, data); 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({ res.status(201).json({
success: true, success: true,
data: company, data: company,
@@ -153,9 +157,17 @@ export const updateCompany = async (req, res, next) => {
export const deleteCompany = async (req, res, next) => { export const deleteCompany = async (req, res, next) => {
try { try {
const { companyId } = req.params; 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); const result = await companyService.deleteCompany(companyId);
// Log audit event
await logCompanyDeleted(userId, companyId, companyName, req.ip, req.headers['user-agent']);
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: result.message, message: result.message,

View File

@@ -1,5 +1,6 @@
import * as projectService from '../services/project.service.js'; import * as projectService from '../services/project.service.js';
import * as noteService from '../services/note.service.js'; import * as noteService from '../services/note.service.js';
import { logProjectCreated, logProjectDeleted } from '../services/audit.service.js';
/** /**
* Get all projects * Get all projects
@@ -71,6 +72,9 @@ export const createProject = async (req, res, next) => {
const project = await projectService.createProject(userId, data); 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({ res.status(201).json({
success: true, success: true,
data: project, data: project,
@@ -110,9 +114,17 @@ export const updateProject = async (req, res, next) => {
export const deleteProject = async (req, res, next) => { export const deleteProject = async (req, res, next) => {
try { try {
const { projectId } = req.params; 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); const result = await projectService.deleteProject(projectId);
// Log audit event
await logProjectDeleted(userId, projectId, projectName, req.ip, req.headers['user-agent']);
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: result.message, message: result.message,

View File

@@ -1,4 +1,5 @@
import * as timeTrackingService from '../services/time-tracking.service.js'; import * as timeTrackingService from '../services/time-tracking.service.js';
import { logTimerStarted, logTimerStopped } from '../services/audit.service.js';
/** /**
* Start a new time entry * Start a new time entry
@@ -16,6 +17,9 @@ export const startTimeEntry = async (req, res, next) => {
description, description,
}); });
// Log audit event
await logTimerStarted(userId, entry.id, description || 'Timer', req.ip, req.headers['user-agent']);
res.status(201).json({ res.status(201).json({
success: true, success: true,
data: entry, data: entry,
@@ -43,6 +47,9 @@ export const stopTimeEntry = async (req, res, next) => {
description, description,
}); });
// Log audit event
await logTimerStopped(userId, entry.id, description || 'Timer', entry.duration, req.ip, req.headers['user-agent']);
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: entry, data: entry,

View File

@@ -1,4 +1,5 @@
import * as todoService from '../services/todo.service.js'; import * as todoService from '../services/todo.service.js';
import { logTodoCreated, logTodoDeleted, logTodoCompleted } from '../services/audit.service.js';
/** /**
* Get all todos * Get all todos
@@ -112,6 +113,9 @@ export const createTodo = async (req, res, next) => {
console.log('Backend received todo data:', data); console.log('Backend received todo data:', data);
const todo = await todoService.createTodo(userId, 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({ res.status(201).json({
success: true, success: true,
data: todo, data: todo,
@@ -152,9 +156,17 @@ export const updateTodo = async (req, res, next) => {
export const deleteTodo = async (req, res, next) => { export const deleteTodo = async (req, res, next) => {
try { try {
const { todoId } = req.params; 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); const result = await todoService.deleteTodo(todoId);
// Log audit event
await logTodoDeleted(userId, todoId, todoTitle, req.ip, req.headers['user-agent']);
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: result.message, message: result.message,
@@ -171,15 +183,22 @@ export const deleteTodo = async (req, res, next) => {
export const toggleTodo = async (req, res, next) => { export const toggleTodo = async (req, res, next) => {
try { try {
const { todoId } = req.params; const { todoId } = req.params;
const userId = req.userId;
// Get current todo // Get current todo
const todo = await todoService.getTodoById(todoId); const todo = await todoService.getTodoById(todoId);
const wasCompleted = todo.status === 'completed';
// Toggle completed status // Toggle completed status
const updated = await todoService.updateTodo(todoId, { 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({ res.status(200).json({
success: true, success: true,
data: updated, data: updated,

View File

@@ -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;

View File

@@ -106,3 +106,139 @@ export const logUserCreation = async (adminId, newUserId, username, role, ipAddr
success: true, 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,
});
};