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 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) => {
|
||||||
|
|||||||
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,
|
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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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,
|
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