From 0585e51b252bcce4a87ea328f086b4c23ef6374e Mon Sep 17 00:00:00 2001 From: richardtekula Date: Wed, 17 Dec 2025 07:19:40 +0100 Subject: [PATCH] feat: Add comprehensive audit logging system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/controllers/admin.controller.js | 11 +- src/controllers/audit.controller.js | 113 ++++++++- src/controllers/company.controller.js | 70 ++++- src/controllers/contact.controller.js | 21 ++ src/controllers/note.controller.js | 18 ++ src/controllers/project.controller.js | 49 +++- src/controllers/time-tracking.controller.js | 39 ++- src/controllers/timesheet.controller.js | 13 +- src/controllers/todo.controller.js | 26 +- src/routes/audit.routes.js | 4 +- src/services/audit.service.js | 267 ++++++++++++++++++++ src/services/company-reminder.service.js | 2 +- src/services/timesheet.service.js | 4 + 13 files changed, 615 insertions(+), 22 deletions(-) diff --git a/src/controllers/admin.controller.js b/src/controllers/admin.controller.js index 311bfdd..a5d2167 100644 --- a/src/controllers/admin.controller.js +++ b/src/controllers/admin.controller.js @@ -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ý', diff --git a/src/controllers/audit.controller.js b/src/controllers/audit.controller.js index af9daf4..b63f822 100644 --- a/src/controllers/audit.controller.js +++ b/src/controllers/audit.controller.js @@ -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); diff --git a/src/controllers/company.controller.js b/src/controllers/company.controller.js index f0accda..d99bc35 100644 --- a/src/controllers/company.controller.js +++ b/src/controllers/company.controller.js @@ -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, diff --git a/src/controllers/contact.controller.js b/src/controllers/contact.controller.js index 22a91e9..39ecdbe 100644 --- a/src/controllers/contact.controller.js +++ b/src/controllers/contact.controller.js @@ -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, diff --git a/src/controllers/note.controller.js b/src/controllers/note.controller.js index 6e15ccd..c6d9dc0 100644 --- a/src/controllers/note.controller.js +++ b/src/controllers/note.controller.js @@ -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, diff --git a/src/controllers/project.controller.js b/src/controllers/project.controller.js index 8414268..bf53a7e 100644 --- a/src/controllers/project.controller.js +++ b/src/controllers/project.controller.js @@ -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, diff --git a/src/controllers/time-tracking.controller.js b/src/controllers/time-tracking.controller.js index 66bd7dc..61e80cf 100644 --- a/src/controllers/time-tracking.controller.js +++ b/src/controllers/time-tracking.controller.js @@ -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); diff --git a/src/controllers/timesheet.controller.js b/src/controllers/timesheet.controller.js index c62dc3c..1ad7bdd 100644 --- a/src/controllers/timesheet.controller.js +++ b/src/controllers/timesheet.controller.js @@ -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ý', diff --git a/src/controllers/todo.controller.js b/src/controllers/todo.controller.js index 5956e5e..d38420a 100644 --- a/src/controllers/todo.controller.js +++ b/src/controllers/todo.controller.js @@ -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({ diff --git a/src/routes/audit.routes.js b/src/routes/audit.routes.js index b5c2298..3beb375 100644 --- a/src/routes/audit.routes.js +++ b/src/routes/audit.routes.js @@ -1,5 +1,5 @@ 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 { requireAdmin } from '../middlewares/auth/roleMiddleware.js'; @@ -7,5 +7,7 @@ const router = Router(); // Audit logs are admin only router.get('/', authenticate, requireAdmin, getRecentAuditLogs); +router.get('/actions', authenticate, requireAdmin, getAuditActions); +router.get('/resources', authenticate, requireAdmin, getAuditResources); export default router; diff --git a/src/services/audit.service.js b/src/services/audit.service.js index 8761ee6..cc0eeae 100644 --- a/src/services/audit.service.js +++ b/src/services/audit.service.js @@ -242,3 +242,270 @@ export const logLogout = async (userId, ipAddress, 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, + }); +}; diff --git a/src/services/company-reminder.service.js b/src/services/company-reminder.service.js index 7651d70..063e200 100644 --- a/src/services/company-reminder.service.js +++ b/src/services/company-reminder.service.js @@ -18,7 +18,7 @@ const ensureCompanyExists = async (companyId) => { return company; }; -const getReminderById = async (reminderId) => { +export const getReminderById = async (reminderId) => { const [reminder] = await db .select() .from(companyReminders) diff --git a/src/services/timesheet.service.js b/src/services/timesheet.service.js index aa1f393..8908a2d 100644 --- a/src/services/timesheet.service.js +++ b/src/services/timesheet.service.js @@ -56,6 +56,10 @@ const ensureTimesheetExists = async (timesheetId) => { return timesheet; }; +export const getTimesheetById = async (timesheetId) => { + return ensureTimesheetExists(timesheetId); +}; + const assertAccess = (timesheet, { userId, role }) => { if (role !== 'admin' && timesheet.userId !== userId) { throw new ForbiddenError('Nemáte oprávnenie k tomuto timesheetu');