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

View File

@@ -1,12 +1,62 @@
import { db } from '../config/database.js'; import { db } from '../config/database.js';
import { auditLogs, users } from '../db/schema.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) => { export const getRecentAuditLogs = async (req, res, next) => {
try { 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({ .select({
id: auditLogs.id, id: auditLogs.id,
userId: auditLogs.userId, userId: auditLogs.userId,
@@ -15,6 +65,7 @@ export const getRecentAuditLogs = async (req, res, next) => {
resourceId: auditLogs.resourceId, resourceId: auditLogs.resourceId,
oldValue: auditLogs.oldValue, oldValue: auditLogs.oldValue,
newValue: auditLogs.newValue, newValue: auditLogs.newValue,
ipAddress: auditLogs.ipAddress,
success: auditLogs.success, success: auditLogs.success,
createdAt: auditLogs.createdAt, createdAt: auditLogs.createdAt,
// User info // User info
@@ -24,18 +75,58 @@ export const getRecentAuditLogs = async (req, res, next) => {
}) })
.from(auditLogs) .from(auditLogs)
.leftJoin(users, eq(auditLogs.userId, users.id)) .leftJoin(users, eq(auditLogs.userId, users.id))
.where(whereClause)
.orderBy(desc(auditLogs.createdAt)) .orderBy(desc(auditLogs.createdAt))
.limit(parseInt(limit)); .limit(limitNum)
.offset(offset);
if (userId) {
query = query.where(eq(auditLogs.userId, userId));
}
const logs = await query;
res.json({ res.json({
success: true, 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) { } catch (error) {
next(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 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'; import {
logCompanyCreated,
logCompanyDeleted,
logCompanyUpdated,
logCompanyUserAssigned,
logCompanyUserRemoved,
logCompanyReminderCreated,
logCompanyReminderUpdated,
logCompanyReminderDeleted,
} from '../services/audit.service.js';
/** /**
* Get all companies * Get all companies
@@ -138,11 +147,25 @@ export const createCompany = async (req, res, next) => {
*/ */
export const updateCompany = async (req, res, next) => { export const updateCompany = async (req, res, next) => {
try { try {
const userId = req.userId;
const { companyId } = req.params; const { companyId } = req.params;
const data = req.body; const data = req.body;
// Get old company for audit
const oldCompany = await companyService.getCompanyById(companyId);
const company = await companyService.updateCompany(companyId, data); 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({ res.status(200).json({
success: true, success: true,
data: company, data: company,
@@ -291,11 +314,15 @@ export const getCompanyReminders = async (req, res, next) => {
export const createCompanyReminder = async (req, res, next) => { export const createCompanyReminder = async (req, res, next) => {
try { try {
const userId = req.userId;
const { companyId } = req.params; const { companyId } = req.params;
const { description, dueDate, isChecked } = req.body; const { description, dueDate, isChecked } = req.body;
const reminder = await companyReminderService.createReminder(companyId, { description, dueDate, isChecked }); 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({ res.status(201).json({
success: true, success: true,
data: reminder, data: reminder,
@@ -308,11 +335,18 @@ export const createCompanyReminder = async (req, res, next) => {
export const updateCompanyReminder = async (req, res, next) => { export const updateCompanyReminder = async (req, res, next) => {
try { try {
const userId = req.userId;
const { companyId, reminderId } = req.params; const { companyId, reminderId } = req.params;
const { description, dueDate, isChecked } = req.body; 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 }); 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({ res.status(200).json({
success: true, success: true,
data: reminder, data: reminder,
@@ -325,10 +359,17 @@ export const updateCompanyReminder = async (req, res, next) => {
export const deleteCompanyReminder = async (req, res, next) => { export const deleteCompanyReminder = async (req, res, next) => {
try { try {
const userId = req.userId;
const { companyId, reminderId } = req.params; const { companyId, reminderId } = req.params;
// Get reminder for audit before deletion
const reminder = await companyReminderService.getReminderById(reminderId);
const result = await companyReminderService.deleteReminder(companyId, 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({ res.status(200).json({
success: true, success: true,
message: result.message, message: result.message,
@@ -412,8 +453,21 @@ export const assignUserToCompany = async (req, res, next) => {
const { companyId } = req.params; const { companyId } = req.params;
const { userId, role } = req.body; 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); 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({ res.status(201).json({
success: true, success: true,
data: assignment, data: assignment,
@@ -430,10 +484,24 @@ export const assignUserToCompany = async (req, res, next) => {
*/ */
export const removeUserFromCompany = async (req, res, next) => { export const removeUserFromCompany = async (req, res, next) => {
try { try {
const currentUserId = req.userId;
const { companyId, userId } = req.params; const { companyId, userId } = req.params;
// Get company name for audit
const company = await companyService.getCompanyById(companyId);
const result = await companyService.removeUserFromCompany(companyId, userId); 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({ res.status(200).json({
success: true, success: true,
message: result.message, message: result.message,

View File

@@ -1,6 +1,7 @@
import * as contactService from '../services/contact.service.js'; import * as contactService from '../services/contact.service.js';
import { discoverContactsFromJMAP, getJmapConfigFromAccount } from '../services/jmap/index.js'; import { discoverContactsFromJMAP, getJmapConfigFromAccount } from '../services/jmap/index.js';
import * as emailAccountService from '../services/email-account.service.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 * 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); 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({ res.status(200).json({
success: true, success: true,
data: updated, data: updated,
@@ -321,6 +332,16 @@ export const createCompanyFromContact = async (req, res, next) => {
const result = await contactService.createCompanyFromContact(contactId, accountId, userId, companyData); 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({ res.status(201).json({
success: true, success: true,
data: result, data: result,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { Router } from 'express'; import { Router } from 'express';
import { getRecentAuditLogs } from '../controllers/audit.controller.js'; import { getRecentAuditLogs, getAuditActions, getAuditResources } from '../controllers/audit.controller.js';
import { authenticate } from '../middlewares/auth/authMiddleware.js'; import { authenticate } from '../middlewares/auth/authMiddleware.js';
import { requireAdmin } from '../middlewares/auth/roleMiddleware.js'; import { requireAdmin } from '../middlewares/auth/roleMiddleware.js';
@@ -7,5 +7,7 @@ const router = Router();
// Audit logs are admin only // Audit logs are admin only
router.get('/', authenticate, requireAdmin, getRecentAuditLogs); router.get('/', authenticate, requireAdmin, getRecentAuditLogs);
router.get('/actions', authenticate, requireAdmin, getAuditActions);
router.get('/resources', authenticate, requireAdmin, getAuditResources);
export default router; export default router;

View File

@@ -242,3 +242,270 @@ export const logLogout = async (userId, ipAddress, userAgent) => {
userAgent, userAgent,
}); });
}; };
// User deletion
export const logUserDeleted = async (adminId, deletedUserId, username, ipAddress, userAgent) => {
await logAuditEvent({
userId: adminId,
action: 'user_deleted',
resource: 'user',
resourceId: deletedUserId,
oldValue: { username },
ipAddress,
userAgent,
});
};
// Contacts
export const logContactLinkedToCompany = async (userId, contactId, companyId, companyName, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'contact_linked_to_company',
resource: 'contact',
resourceId: contactId,
newValue: { companyId, companyName },
ipAddress,
userAgent,
});
};
export const logCompanyCreatedFromContact = async (userId, contactId, companyId, companyName, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'company_created_from_contact',
resource: 'contact',
resourceId: contactId,
newValue: { companyId, companyName },
ipAddress,
userAgent,
});
};
// Notes
export const logNoteCreated = async (userId, noteId, noteContent, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'note_created',
resource: 'note',
resourceId: noteId,
newValue: { content: noteContent?.substring(0, 100) },
ipAddress,
userAgent,
});
};
export const logNoteUpdated = async (userId, noteId, oldContent, newContent, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'note_updated',
resource: 'note',
resourceId: noteId,
oldValue: { content: oldContent?.substring(0, 100) },
newValue: { content: newContent?.substring(0, 100) },
ipAddress,
userAgent,
});
};
export const logNoteDeleted = async (userId, noteId, noteContent, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'note_deleted',
resource: 'note',
resourceId: noteId,
oldValue: { content: noteContent?.substring(0, 100) },
ipAddress,
userAgent,
});
};
// Company - updates and user management
export const logCompanyUpdated = async (userId, companyId, oldValues, newValues, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'company_updated',
resource: 'company',
resourceId: companyId,
oldValue: oldValues,
newValue: newValues,
ipAddress,
userAgent,
});
};
export const logCompanyUserAssigned = async (userId, companyId, assignedUserId, companyName, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'company_user_assigned',
resource: 'company',
resourceId: companyId,
newValue: { assignedUserId, companyName },
ipAddress,
userAgent,
});
};
export const logCompanyUserRemoved = async (userId, companyId, removedUserId, companyName, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'company_user_removed',
resource: 'company',
resourceId: companyId,
oldValue: { removedUserId, companyName },
ipAddress,
userAgent,
});
};
// Company reminders
export const logCompanyReminderCreated = async (userId, reminderId, companyId, reminderDate, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'company_reminder_created',
resource: 'company_reminder',
resourceId: reminderId,
newValue: { companyId, reminderDate },
ipAddress,
userAgent,
});
};
export const logCompanyReminderUpdated = async (userId, reminderId, companyId, oldDate, newDate, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'company_reminder_updated',
resource: 'company_reminder',
resourceId: reminderId,
oldValue: { companyId, reminderDate: oldDate },
newValue: { companyId, reminderDate: newDate },
ipAddress,
userAgent,
});
};
export const logCompanyReminderDeleted = async (userId, reminderId, companyId, reminderDate, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'company_reminder_deleted',
resource: 'company_reminder',
resourceId: reminderId,
oldValue: { companyId, reminderDate },
ipAddress,
userAgent,
});
};
// Project - updates and user management
export const logProjectUpdated = async (userId, projectId, oldValues, newValues, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'project_updated',
resource: 'project',
resourceId: projectId,
oldValue: oldValues,
newValue: newValues,
ipAddress,
userAgent,
});
};
export const logProjectUserAssigned = async (userId, projectId, assignedUserId, projectName, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'project_user_assigned',
resource: 'project',
resourceId: projectId,
newValue: { assignedUserId, projectName },
ipAddress,
userAgent,
});
};
export const logProjectUserRemoved = async (userId, projectId, removedUserId, projectName, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'project_user_removed',
resource: 'project',
resourceId: projectId,
oldValue: { removedUserId, projectName },
ipAddress,
userAgent,
});
};
// Todos - update and uncomplete
export const logTodoUpdated = async (userId, todoId, oldValues, newValues, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'todo_updated',
resource: 'todo',
resourceId: todoId,
oldValue: oldValues,
newValue: newValues,
ipAddress,
userAgent,
});
};
export const logTodoUncompleted = async (userId, todoId, todoTitle, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'todo_uncompleted',
resource: 'todo',
resourceId: todoId,
newValue: { title: todoTitle },
ipAddress,
userAgent,
});
};
// Time Entry - update and delete
export const logTimeEntryUpdated = async (userId, entryId, oldValues, newValues, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'time_entry_updated',
resource: 'time_entry',
resourceId: entryId,
oldValue: oldValues,
newValue: newValues,
ipAddress,
userAgent,
});
};
export const logTimeEntryDeleted = async (userId, entryId, projectName, duration, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'time_entry_deleted',
resource: 'time_entry',
resourceId: entryId,
oldValue: { project: projectName, duration },
ipAddress,
userAgent,
});
};
// Timesheets
export const logTimesheetUploaded = async (userId, timesheetId, year, month, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'timesheet_uploaded',
resource: 'timesheet',
resourceId: timesheetId,
newValue: { year, month },
ipAddress,
userAgent,
});
};
export const logTimesheetDeleted = async (userId, timesheetId, year, month, ipAddress, userAgent) => {
await logAuditEvent({
userId,
action: 'timesheet_deleted',
resource: 'timesheet',
resourceId: timesheetId,
oldValue: { year, month },
ipAddress,
userAgent,
});
};

View File

@@ -18,7 +18,7 @@ const ensureCompanyExists = async (companyId) => {
return company; return company;
}; };
const getReminderById = async (reminderId) => { export const getReminderById = async (reminderId) => {
const [reminder] = await db const [reminder] = await db
.select() .select()
.from(companyReminders) .from(companyReminders)

View File

@@ -56,6 +56,10 @@ const ensureTimesheetExists = async (timesheetId) => {
return timesheet; return timesheet;
}; };
export const getTimesheetById = async (timesheetId) => {
return ensureTimesheetExists(timesheetId);
};
const assertAccess = (timesheet, { userId, role }) => { const assertAccess = (timesheet, { userId, role }) => {
if (role !== 'admin' && timesheet.userId !== userId) { if (role !== 'admin' && timesheet.userId !== userId) {
throw new ForbiddenError('Nemáte oprávnenie k tomuto timesheetu'); throw new ForbiddenError('Nemáte oprávnenie k tomuto timesheetu');