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:
@@ -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) => {
|
||||
|
||||
43
src/controllers/audit.controller.js
Normal file
43
src/controllers/audit.controller.js
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
9
src/routes/audit.routes.js
Normal file
9
src/routes/audit.routes.js
Normal 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;
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user