feat: Add comprehensive audit logging system

- Add audit logging for contacts (link company, create company from contact)
- Add audit logging for notes (create, update, delete)
- Add audit logging for companies (update, user assign/remove, reminder CRUD)
- Add audit logging for projects (update, user assign/remove)
- Add audit logging for todos (update, uncomplete)
- Add audit logging for time entries (update, delete)
- Add audit logging for timesheets (upload, delete)
- Add audit logging for user deletion
- Add pagination and filters to audit logs API (userId, action, resource, dateFrom, dateTo)
- Add endpoints for distinct actions and resources

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
richardtekula
2025-12-17 07:19:40 +01:00
parent 548a8effdb
commit 0585e51b25
13 changed files with 615 additions and 22 deletions

View File

@@ -1,6 +1,6 @@
import * as adminService from '../services/admin.service.js';
import * as statusService from '../services/status.service.js';
import { logUserCreation, logRoleChange } from '../services/audit.service.js';
import { logUserCreation, logRoleChange, logUserDeleted } from '../services/audit.service.js';
import { triggerEventNotifications } from '../cron/index.js';
/**
@@ -131,10 +131,19 @@ export const changeUserRole = async (req, res, next) => {
*/
export const deleteUser = async (req, res, next) => {
const { userId } = req.params;
const adminId = req.userId;
const ipAddress = req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'];
try {
// Get user info before deletion for audit
const userToDelete = await adminService.getUserById(userId);
const result = await adminService.deleteUser(userId);
// Log user deletion
await logUserDeleted(adminId, userId, userToDelete.username, ipAddress, userAgent);
res.status(200).json({
success: true,
message: 'Používateľ bol zmazaný',

View File

@@ -1,12 +1,62 @@
import { db } from '../config/database.js';
import { auditLogs, users } from '../db/schema.js';
import { desc, eq } from 'drizzle-orm';
import { desc, eq, and, gte, lte, like, sql } from 'drizzle-orm';
export const getRecentAuditLogs = async (req, res, next) => {
try {
const { limit = 20, userId } = req.query;
const {
page = 1,
limit = 50,
userId,
action,
resource,
dateFrom,
dateTo,
} = req.query;
let query = db
const pageNum = parseInt(page);
const limitNum = parseInt(limit);
const offset = (pageNum - 1) * limitNum;
// Build conditions array
const conditions = [];
if (userId) {
conditions.push(eq(auditLogs.userId, userId));
}
if (action) {
conditions.push(eq(auditLogs.action, action));
}
if (resource) {
conditions.push(eq(auditLogs.resource, resource));
}
if (dateFrom) {
conditions.push(gte(auditLogs.createdAt, new Date(dateFrom)));
}
if (dateTo) {
// Add one day to include the entire end date
const endDate = new Date(dateTo);
endDate.setDate(endDate.getDate() + 1);
conditions.push(lte(auditLogs.createdAt, endDate));
}
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
// Get total count for pagination
const [countResult] = await db
.select({ count: sql`count(*)::int` })
.from(auditLogs)
.where(whereClause);
const totalCount = countResult?.count || 0;
const totalPages = Math.ceil(totalCount / limitNum);
// Get paginated logs
const logs = await db
.select({
id: auditLogs.id,
userId: auditLogs.userId,
@@ -15,6 +65,7 @@ export const getRecentAuditLogs = async (req, res, next) => {
resourceId: auditLogs.resourceId,
oldValue: auditLogs.oldValue,
newValue: auditLogs.newValue,
ipAddress: auditLogs.ipAddress,
success: auditLogs.success,
createdAt: auditLogs.createdAt,
// User info
@@ -24,18 +75,58 @@ export const getRecentAuditLogs = async (req, res, next) => {
})
.from(auditLogs)
.leftJoin(users, eq(auditLogs.userId, users.id))
.where(whereClause)
.orderBy(desc(auditLogs.createdAt))
.limit(parseInt(limit));
if (userId) {
query = query.where(eq(auditLogs.userId, userId));
}
const logs = await query;
.limit(limitNum)
.offset(offset);
res.json({
success: true,
data: logs,
data: {
logs,
pagination: {
page: pageNum,
limit: limitNum,
totalCount,
totalPages,
hasNextPage: pageNum < totalPages,
hasPrevPage: pageNum > 1,
},
},
});
} catch (error) {
next(error);
}
};
// Get distinct actions for filter dropdown
export const getAuditActions = async (req, res, next) => {
try {
const actions = await db
.selectDistinct({ action: auditLogs.action })
.from(auditLogs)
.orderBy(auditLogs.action);
res.json({
success: true,
data: actions.map((a) => a.action),
});
} catch (error) {
next(error);
}
};
// Get distinct resources for filter dropdown
export const getAuditResources = async (req, res, next) => {
try {
const resources = await db
.selectDistinct({ resource: auditLogs.resource })
.from(auditLogs)
.orderBy(auditLogs.resource);
res.json({
success: true,
data: resources.map((r) => r.resource),
});
} catch (error) {
next(error);

View File

@@ -2,7 +2,16 @@ 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';
import {
logCompanyCreated,
logCompanyDeleted,
logCompanyUpdated,
logCompanyUserAssigned,
logCompanyUserRemoved,
logCompanyReminderCreated,
logCompanyReminderUpdated,
logCompanyReminderDeleted,
} from '../services/audit.service.js';
/**
* Get all companies
@@ -138,11 +147,25 @@ export const createCompany = async (req, res, next) => {
*/
export const updateCompany = async (req, res, next) => {
try {
const userId = req.userId;
const { companyId } = req.params;
const data = req.body;
// Get old company for audit
const oldCompany = await companyService.getCompanyById(companyId);
const company = await companyService.updateCompany(companyId, data);
// Log audit event
await logCompanyUpdated(
userId,
companyId,
{ name: oldCompany.name },
{ name: company.name },
req.ip,
req.headers['user-agent']
);
res.status(200).json({
success: true,
data: company,
@@ -291,11 +314,15 @@ export const getCompanyReminders = async (req, res, next) => {
export const createCompanyReminder = async (req, res, next) => {
try {
const userId = req.userId;
const { companyId } = req.params;
const { description, dueDate, isChecked } = req.body;
const reminder = await companyReminderService.createReminder(companyId, { description, dueDate, isChecked });
// Log audit event
await logCompanyReminderCreated(userId, reminder.id, companyId, dueDate, req.ip, req.headers['user-agent']);
res.status(201).json({
success: true,
data: reminder,
@@ -308,11 +335,18 @@ export const createCompanyReminder = async (req, res, next) => {
export const updateCompanyReminder = async (req, res, next) => {
try {
const userId = req.userId;
const { companyId, reminderId } = req.params;
const { description, dueDate, isChecked } = req.body;
// Get old reminder for audit
const oldReminder = await companyReminderService.getReminderById(reminderId);
const reminder = await companyReminderService.updateReminder(companyId, reminderId, { description, dueDate, isChecked });
// Log audit event
await logCompanyReminderUpdated(userId, reminderId, companyId, oldReminder?.dueDate, dueDate, req.ip, req.headers['user-agent']);
res.status(200).json({
success: true,
data: reminder,
@@ -325,10 +359,17 @@ export const updateCompanyReminder = async (req, res, next) => {
export const deleteCompanyReminder = async (req, res, next) => {
try {
const userId = req.userId;
const { companyId, reminderId } = req.params;
// Get reminder for audit before deletion
const reminder = await companyReminderService.getReminderById(reminderId);
const result = await companyReminderService.deleteReminder(companyId, reminderId);
// Log audit event
await logCompanyReminderDeleted(userId, reminderId, companyId, reminder?.dueDate, req.ip, req.headers['user-agent']);
res.status(200).json({
success: true,
message: result.message,
@@ -412,8 +453,21 @@ export const assignUserToCompany = async (req, res, next) => {
const { companyId } = req.params;
const { userId, role } = req.body;
// Get company name for audit
const company = await companyService.getCompanyById(companyId);
const assignment = await companyService.assignUserToCompany(companyId, userId, currentUserId, role);
// Log audit event
await logCompanyUserAssigned(
currentUserId,
companyId,
userId,
company.name,
req.ip,
req.headers['user-agent']
);
res.status(201).json({
success: true,
data: assignment,
@@ -430,10 +484,24 @@ export const assignUserToCompany = async (req, res, next) => {
*/
export const removeUserFromCompany = async (req, res, next) => {
try {
const currentUserId = req.userId;
const { companyId, userId } = req.params;
// Get company name for audit
const company = await companyService.getCompanyById(companyId);
const result = await companyService.removeUserFromCompany(companyId, userId);
// Log audit event
await logCompanyUserRemoved(
currentUserId,
companyId,
userId,
company.name,
req.ip,
req.headers['user-agent']
);
res.status(200).json({
success: true,
message: result.message,

View File

@@ -1,6 +1,7 @@
import * as contactService from '../services/contact.service.js';
import { discoverContactsFromJMAP, getJmapConfigFromAccount } from '../services/jmap/index.js';
import * as emailAccountService from '../services/email-account.service.js';
import { logContactLinkedToCompany, logCompanyCreatedFromContact } from '../services/audit.service.js';
/**
* Get all contacts for an email account
@@ -249,6 +250,16 @@ export const linkCompanyToContact = async (req, res, next) => {
const updated = await contactService.linkCompanyToContact(contactId, accountId, companyId);
// Log audit event
await logContactLinkedToCompany(
userId,
contactId,
companyId,
updated.company?.name || companyId,
req.ip,
req.headers['user-agent']
);
res.status(200).json({
success: true,
data: updated,
@@ -321,6 +332,16 @@ export const createCompanyFromContact = async (req, res, next) => {
const result = await contactService.createCompanyFromContact(contactId, accountId, userId, companyData);
// Log audit event
await logCompanyCreatedFromContact(
userId,
contactId,
result.company?.id,
result.company?.name,
req.ip,
req.headers['user-agent']
);
res.status(201).json({
success: true,
data: result,

View File

@@ -1,4 +1,5 @@
import * as noteService from '../services/note.service.js';
import { logNoteCreated, logNoteUpdated, logNoteDeleted } from '../services/audit.service.js';
/**
* Get all notes
@@ -59,6 +60,9 @@ export const createNote = async (req, res, next) => {
const note = await noteService.createNote(userId, data);
// Log audit event
await logNoteCreated(userId, note.id, note.content, req.ip, req.headers['user-agent']);
res.status(201).json({
success: true,
data: note,
@@ -76,11 +80,18 @@ export const createNote = async (req, res, next) => {
*/
export const updateNote = async (req, res, next) => {
try {
const userId = req.userId;
const { noteId } = req.params;
const data = req.body;
// Get old note for audit
const oldNote = await noteService.getNoteById(noteId);
const note = await noteService.updateNote(noteId, data);
// Log audit event
await logNoteUpdated(userId, noteId, oldNote.content, note.content, req.ip, req.headers['user-agent']);
res.status(200).json({
success: true,
data: note,
@@ -97,10 +108,17 @@ export const updateNote = async (req, res, next) => {
*/
export const deleteNote = async (req, res, next) => {
try {
const userId = req.userId;
const { noteId } = req.params;
// Get note for audit before deletion
const note = await noteService.getNoteById(noteId);
const result = await noteService.deleteNote(noteId);
// Log audit event
await logNoteDeleted(userId, noteId, note.content, req.ip, req.headers['user-agent']);
res.status(200).json({
success: true,
message: result.message,

View File

@@ -1,6 +1,12 @@
import * as projectService from '../services/project.service.js';
import * as noteService from '../services/note.service.js';
import { logProjectCreated, logProjectDeleted } from '../services/audit.service.js';
import {
logProjectCreated,
logProjectDeleted,
logProjectUpdated,
logProjectUserAssigned,
logProjectUserRemoved,
} from '../services/audit.service.js';
/**
* Get all projects
@@ -95,11 +101,25 @@ export const createProject = async (req, res, next) => {
*/
export const updateProject = async (req, res, next) => {
try {
const userId = req.userId;
const { projectId } = req.params;
const data = req.body;
// Get old project for audit
const oldProject = await projectService.getProjectById(projectId);
const project = await projectService.updateProject(projectId, data);
// Log audit event
await logProjectUpdated(
userId,
projectId,
{ name: oldProject.name },
{ name: project.name },
req.ip,
req.headers['user-agent']
);
res.status(200).json({
success: true,
data: project,
@@ -257,6 +277,9 @@ export const assignUserToProject = async (req, res, next) => {
const { projectId } = req.params;
const { userId, role } = req.body;
// Get project name for audit
const project = await projectService.getProjectById(projectId);
const assignment = await projectService.assignUserToProject(
projectId,
userId,
@@ -264,6 +287,16 @@ export const assignUserToProject = async (req, res, next) => {
role
);
// Log audit event
await logProjectUserAssigned(
currentUserId,
projectId,
userId,
project.name,
req.ip,
req.headers['user-agent']
);
res.status(201).json({
success: true,
data: assignment,
@@ -280,10 +313,24 @@ export const assignUserToProject = async (req, res, next) => {
*/
export const removeUserFromProject = async (req, res, next) => {
try {
const currentUserId = req.userId;
const { projectId, userId } = req.params;
// Get project name for audit
const project = await projectService.getProjectById(projectId);
const result = await projectService.removeUserFromProject(projectId, userId);
// Log audit event
await logProjectUserRemoved(
currentUserId,
projectId,
userId,
project.name,
req.ip,
req.headers['user-agent']
);
res.status(200).json({
success: true,
message: result.message,

View File

@@ -1,5 +1,10 @@
import * as timeTrackingService from '../services/time-tracking.service.js';
import { logTimerStarted, logTimerStopped } from '../services/audit.service.js';
import {
logTimerStarted,
logTimerStopped,
logTimeEntryUpdated,
logTimeEntryDeleted,
} from '../services/audit.service.js';
/**
* Start a new time entry
@@ -223,11 +228,15 @@ export const getTimeEntryWithRelations = async (req, res, next) => {
*/
export const updateTimeEntry = async (req, res, next) => {
try {
const userId = req.userId;
const { entryId } = req.params;
const { startTime, endTime, projectId, todoId, companyId, description } = req.body;
// Get old entry for audit
const oldEntry = await timeTrackingService.getTimeEntryById(entryId);
const entry = await timeTrackingService.updateTimeEntry(entryId, {
userId: req.userId,
userId,
role: req.user.role,
}, {
startTime,
@@ -238,6 +247,16 @@ export const updateTimeEntry = async (req, res, next) => {
description,
});
// Log audit event
await logTimeEntryUpdated(
userId,
entryId,
{ description: oldEntry.description, duration: oldEntry.duration },
{ description: entry.description, duration: entry.duration },
req.ip,
req.headers['user-agent']
);
res.status(200).json({
success: true,
data: entry,
@@ -254,13 +273,27 @@ export const updateTimeEntry = async (req, res, next) => {
*/
export const deleteTimeEntry = async (req, res, next) => {
try {
const userId = req.userId;
const { entryId } = req.params;
// Get entry for audit before deletion
const entry = await timeTrackingService.getTimeEntryById(entryId);
const result = await timeTrackingService.deleteTimeEntry(entryId, {
userId: req.userId,
userId,
role: req.user.role,
});
// Log audit event
await logTimeEntryDeleted(
userId,
entryId,
entry.description || 'Time entry',
entry.duration,
req.ip,
req.headers['user-agent']
);
res.status(200).json(result);
} catch (error) {
next(error);

View File

@@ -1,5 +1,6 @@
import * as timesheetService from '../services/timesheet.service.js';
import { ForbiddenError } from '../utils/errors.js';
import { logTimesheetUploaded, logTimesheetDeleted } from '../services/audit.service.js';
/**
* Upload timesheet
@@ -27,6 +28,9 @@ export const uploadTimesheet = async (req, res, next) => {
file: req.file,
});
// Log audit event
await logTimesheetUploaded(req.userId, timesheet.id, year, month, req.ip, req.headers['user-agent']);
res.status(201).json({
success: true,
data: { timesheet },
@@ -105,13 +109,20 @@ export const downloadTimesheet = async (req, res, next) => {
*/
export const deleteTimesheet = async (req, res, next) => {
try {
const userId = req.userId;
const { timesheetId } = req.params;
// Get timesheet for audit before deletion
const timesheet = await timesheetService.getTimesheetById(timesheetId);
await timesheetService.deleteTimesheet(timesheetId, {
userId: req.userId,
userId,
role: req.user.role,
});
// Log audit event
await logTimesheetDeleted(userId, timesheetId, timesheet?.year, timesheet?.month, req.ip, req.headers['user-agent']);
res.status(200).json({
success: true,
message: 'Timesheet bol zmazaný',

View File

@@ -1,5 +1,11 @@
import * as todoService from '../services/todo.service.js';
import { logTodoCreated, logTodoDeleted, logTodoCompleted } from '../services/audit.service.js';
import {
logTodoCreated,
logTodoDeleted,
logTodoCompleted,
logTodoUpdated,
logTodoUncompleted,
} from '../services/audit.service.js';
/**
* Get all todos
@@ -135,11 +141,25 @@ export const createTodo = async (req, res, next) => {
*/
export const updateTodo = async (req, res, next) => {
try {
const userId = req.userId;
const { todoId } = req.params;
const data = req.body;
// Get old todo for audit
const oldTodo = await todoService.getTodoById(todoId);
const todo = await todoService.updateTodo(todoId, data);
// Log audit event
await logTodoUpdated(
userId,
todoId,
{ title: oldTodo.title },
{ title: todo.title },
req.ip,
req.headers['user-agent']
);
res.status(200).json({
success: true,
data: todo,
@@ -195,9 +215,11 @@ export const toggleTodo = async (req, res, next) => {
status: wasCompleted ? 'pending' : 'completed',
});
// Log audit event if todo was completed
// Log audit event
if (!wasCompleted) {
await logTodoCompleted(userId, todoId, todo.title, req.ip, req.headers['user-agent']);
} else {
await logTodoUncompleted(userId, todoId, todo.title, req.ip, req.headers['user-agent']);
}
res.status(200).json({