refactor: Move audit logging from controllers into services

Add auditContext parameter to service mutating functions. Services now
call audit log functions internally when auditContext is provided.
Controllers pass { userId, ipAddress, userAgent } and no longer import
audit service or fetch extra data for audit purposes.

Files modified:
- 10 service files: added audit imports and auditContext parameter
- 9 controller files: removed audit imports and calls

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
richardtekula
2026-01-28 07:39:41 +01:00
parent caab86079e
commit 3aba6c2955
19 changed files with 315 additions and 430 deletions

View File

@@ -1,6 +1,5 @@
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, logUserDeleted } from '../services/audit.service.js';
import { triggerEventNotifications } from '../cron/index.js'; import { triggerEventNotifications } from '../cron/index.js';
/** /**
@@ -21,17 +20,8 @@ export const createUser = async (req, res, next) => {
lastName, lastName,
role, role,
email, email,
emailPassword emailPassword,
); { userId: adminId, ipAddress, userAgent }
// Log user creation
await logUserCreation(
adminId,
result.user.id,
username,
result.user.role,
ipAddress,
userAgent
); );
res.status(201).json({ res.status(201).json({
@@ -110,10 +100,7 @@ export const changeUserRole = async (req, res, next) => {
const userAgent = req.headers['user-agent']; const userAgent = req.headers['user-agent'];
try { try {
const result = await adminService.changeUserRole(userId, role); const result = await adminService.changeUserRole(userId, role, { userId: adminId, ipAddress, userAgent });
// Log role change
await logRoleChange(adminId, userId, result.oldRole, result.newRole, ipAddress, userAgent);
res.status(200).json({ res.status(200).json({
success: true, success: true,
@@ -177,13 +164,7 @@ export const deleteUser = async (req, res, next) => {
const userAgent = req.headers['user-agent']; const userAgent = req.headers['user-agent'];
try { try {
// Get user info before deletion for audit const result = await adminService.deleteUser(userId, { userId: adminId, ipAddress, userAgent });
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({ res.status(200).json({
success: true, success: true,

View File

@@ -1,11 +1,4 @@
import * as authService from '../services/auth.service.js'; import * as authService from '../services/auth.service.js';
import {
logLoginAttempt,
logPasswordChange,
logEmailLink,
logLogin,
logLogout,
} from '../services/audit.service.js';
import { verifyRefreshToken, generateAccessToken } from '../utils/jwt.js'; import { verifyRefreshToken, generateAccessToken } from '../utils/jwt.js';
import { getUserById } from '../services/auth.service.js'; import { getUserById } from '../services/auth.service.js';
@@ -26,10 +19,6 @@ export const login = async (req, res, next) => {
userAgent userAgent
); );
// Log successful login
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)
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
res.cookie('accessToken', result.tokens.accessToken, { res.cookie('accessToken', result.tokens.accessToken, {
@@ -60,9 +49,6 @@ export const login = async (req, res, next) => {
message: 'Prihlásenie úspešné', message: 'Prihlásenie úspešné',
}); });
} catch (error) { } catch (error) {
// Log failed login
await logLoginAttempt(username, false, ipAddress, userAgent, error.message);
next(error); next(error);
} }
}; };
@@ -141,10 +127,7 @@ export const setPassword = async (req, res, next) => {
const userAgent = req.headers['user-agent']; const userAgent = req.headers['user-agent'];
try { try {
const result = await authService.setNewPassword(userId, newPassword); const result = await authService.setNewPassword(userId, newPassword, { userId, ipAddress, userAgent });
// Log password change
await logPasswordChange(userId, ipAddress, userAgent);
res.status(200).json({ res.status(200).json({
success: true, success: true,
@@ -168,10 +151,7 @@ export const linkEmail = async (req, res, next) => {
const userAgent = req.headers['user-agent']; const userAgent = req.headers['user-agent'];
try { try {
const result = await authService.linkEmail(userId, email, emailPassword); const result = await authService.linkEmail(userId, email, emailPassword, { userId, ipAddress, userAgent });
// Log email link
await logEmailLink(userId, email, ipAddress, userAgent);
res.status(200).json({ res.status(200).json({
success: true, success: true,
@@ -218,10 +198,7 @@ export const logout = async (req, res, next) => {
const ipAddress = req.ip || req.connection.remoteAddress; const ipAddress = req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent']; const userAgent = req.headers['user-agent'];
// Log logout event const result = await authService.logout({ userId, ipAddress, userAgent });
await logLogout(userId, ipAddress, userAgent);
const result = await authService.logout();
// Vymaž cookies // Vymaž cookies
res.clearCookie('accessToken'); res.clearCookie('accessToken');

View File

@@ -2,16 +2,6 @@ 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,
logCompanyUpdated,
logCompanyUserAssigned,
logCompanyUserRemoved,
logCompanyReminderCreated,
logCompanyReminderUpdated,
logCompanyReminderDeleted,
} from '../services/audit.service.js';
/** /**
* Get all companies * Get all companies
@@ -125,10 +115,7 @@ export const createCompany = async (req, res, next) => {
const userId = req.userId; const userId = req.userId;
const data = req.body; const data = req.body;
const company = await companyService.createCompany(userId, data); const company = await companyService.createCompany(userId, data, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
// 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,
@@ -151,20 +138,7 @@ export const updateCompany = async (req, res, next) => {
const { companyId } = req.params; const { companyId } = req.params;
const data = req.body; const data = req.body;
// Get old company for audit const company = await companyService.updateCompany(companyId, data, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
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({ res.status(200).json({
success: true, success: true,
@@ -185,14 +159,7 @@ export const deleteCompany = async (req, res, next) => {
const { companyId } = req.params; const { companyId } = req.params;
const userId = req.userId; const userId = req.userId;
// Get company info before deleting const result = await companyService.deleteCompany(companyId, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
const company = await companyService.getCompanyById(companyId);
const companyName = company?.name;
const result = await companyService.deleteCompany(companyId);
// Log audit event
await logCompanyDeleted(userId, companyId, companyName, req.ip, req.headers['user-agent']);
res.status(200).json({ res.status(200).json({
success: true, success: true,
@@ -318,10 +285,7 @@ export const createCompanyReminder = async (req, res, next) => {
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 }, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
// 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,
@@ -339,13 +303,7 @@ export const updateCompanyReminder = async (req, res, next) => {
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 reminder = await companyReminderService.updateReminder(companyId, reminderId, { description, dueDate, isChecked }, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
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({ res.status(200).json({
success: true, success: true,
@@ -362,13 +320,7 @@ export const deleteCompanyReminder = async (req, res, next) => {
const userId = req.userId; const userId = req.userId;
const { companyId, reminderId } = req.params; const { companyId, reminderId } = req.params;
// Get reminder for audit before deletion const result = await companyReminderService.deleteReminder(companyId, reminderId, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
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({ res.status(200).json({
success: true, success: true,
@@ -453,20 +405,7 @@ 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 assignment = await companyService.assignUserToCompany(companyId, userId, currentUserId, role, { userId: currentUserId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
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({ res.status(201).json({
success: true, success: true,
@@ -487,20 +426,7 @@ export const removeUserFromCompany = async (req, res, next) => {
const currentUserId = req.userId; const currentUserId = req.userId;
const { companyId, userId } = req.params; const { companyId, userId } = req.params;
// Get company name for audit const result = await companyService.removeUserFromCompany(companyId, userId, { userId: currentUserId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
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({ res.status(200).json({
success: true, success: true,

View File

@@ -1,7 +1,6 @@
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
@@ -248,17 +247,7 @@ export const linkCompanyToContact = async (req, res, next) => {
// Verify user has access to this email account // Verify user has access to this email account
await emailAccountService.getEmailAccountById(accountId, userId); await emailAccountService.getEmailAccountById(accountId, userId);
const updated = await contactService.linkCompanyToContact(contactId, accountId, companyId); const updated = await contactService.linkCompanyToContact(contactId, accountId, companyId, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
// 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,
@@ -330,17 +319,7 @@ export const createCompanyFromContact = async (req, res, next) => {
// Verify user has access to this email account // Verify user has access to this email account
await emailAccountService.getEmailAccountById(accountId, userId); await emailAccountService.getEmailAccountById(accountId, userId);
const result = await contactService.createCompanyFromContact(contactId, accountId, userId, companyData); const result = await contactService.createCompanyFromContact(contactId, accountId, userId, companyData, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
// 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,

View File

@@ -1,5 +1,4 @@
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
@@ -58,10 +57,7 @@ export const createNote = async (req, res, next) => {
const userId = req.userId; const userId = req.userId;
const data = req.body; const data = req.body;
const note = await noteService.createNote(userId, data); const note = await noteService.createNote(userId, data, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
// 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,
@@ -84,13 +80,7 @@ export const updateNote = async (req, res, next) => {
const { noteId } = req.params; const { noteId } = req.params;
const data = req.body; const data = req.body;
// Get old note for audit const note = await noteService.updateNote(noteId, data, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
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({ res.status(200).json({
success: true, success: true,
@@ -111,13 +101,7 @@ export const deleteNote = async (req, res, next) => {
const userId = req.userId; const userId = req.userId;
const { noteId } = req.params; const { noteId } = req.params;
// Get note for audit before deletion const result = await noteService.deleteNote(noteId, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
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({ res.status(200).json({
success: true, success: true,

View File

@@ -1,12 +1,5 @@
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,
logProjectUpdated,
logProjectUserAssigned,
logProjectUserRemoved,
} from '../services/audit.service.js';
/** /**
* Get all projects * Get all projects
@@ -79,10 +72,7 @@ export const createProject = async (req, res, next) => {
const userId = req.userId; const userId = req.userId;
const data = req.body; const data = req.body;
const project = await projectService.createProject(userId, data); const project = await projectService.createProject(userId, data, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
// 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,
@@ -105,20 +95,7 @@ export const updateProject = async (req, res, next) => {
const { projectId } = req.params; const { projectId } = req.params;
const data = req.body; const data = req.body;
// Get old project for audit const project = await projectService.updateProject(projectId, data, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
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({ res.status(200).json({
success: true, success: true,
@@ -139,14 +116,7 @@ export const deleteProject = async (req, res, next) => {
const { projectId } = req.params; const { projectId } = req.params;
const userId = req.userId; const userId = req.userId;
// Get project info before deleting const result = await projectService.deleteProject(projectId, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
const project = await projectService.getProjectById(projectId);
const projectName = project?.name;
const result = await projectService.deleteProject(projectId);
// Log audit event
await logProjectDeleted(userId, projectId, projectName, req.ip, req.headers['user-agent']);
res.status(200).json({ res.status(200).json({
success: true, success: true,
@@ -277,24 +247,12 @@ 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,
currentUserId, currentUserId,
role role,
); { userId: currentUserId, ipAddress: req.ip, userAgent: req.headers['user-agent'] }
// Log audit event
await logProjectUserAssigned(
currentUserId,
projectId,
userId,
project.name,
req.ip,
req.headers['user-agent']
); );
res.status(201).json({ res.status(201).json({
@@ -316,20 +274,7 @@ export const removeUserFromProject = async (req, res, next) => {
const currentUserId = req.userId; const currentUserId = req.userId;
const { projectId, userId } = req.params; const { projectId, userId } = req.params;
// Get project name for audit const result = await projectService.removeUserFromProject(projectId, userId, { userId: currentUserId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
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({ res.status(200).json({
success: true, success: true,

View File

@@ -1,12 +1,4 @@
import * as timeTrackingService from '../services/time-tracking.service.js'; import * as timeTrackingService from '../services/time-tracking.service.js';
import {
logTimerStarted,
logTimerStopped,
logTimerPaused,
logTimerResumed,
logTimeEntryUpdated,
logTimeEntryDeleted,
} from '../services/audit.service.js';
/** /**
* Start a new time entry * Start a new time entry
@@ -22,10 +14,7 @@ export const startTimeEntry = async (req, res, next) => {
todoId, todoId,
companyId, companyId,
description, description,
}); }, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
// 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,
@@ -52,10 +41,7 @@ export const stopTimeEntry = async (req, res, next) => {
todoId, todoId,
companyId, companyId,
description, description,
}); }, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
// 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,
@@ -76,9 +62,7 @@ export const pauseTimeEntry = async (req, res, next) => {
const userId = req.userId; const userId = req.userId;
const { entryId } = req.params; const { entryId } = req.params;
const entry = await timeTrackingService.pauseTimeEntry(entryId, userId); const entry = await timeTrackingService.pauseTimeEntry(entryId, userId, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
await logTimerPaused(userId, entry.id, req.ip, req.headers['user-agent']);
res.status(200).json({ res.status(200).json({
success: true, success: true,
@@ -99,9 +83,7 @@ export const resumeTimeEntry = async (req, res, next) => {
const userId = req.userId; const userId = req.userId;
const { entryId } = req.params; const { entryId } = req.params;
const entry = await timeTrackingService.resumeTimeEntry(entryId, userId); const entry = await timeTrackingService.resumeTimeEntry(entryId, userId, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
await logTimerResumed(userId, entry.id, req.ip, req.headers['user-agent']);
res.status(200).json({ res.status(200).json({
success: true, success: true,
@@ -280,9 +262,6 @@ export const updateTimeEntry = async (req, res, next) => {
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, userId,
role: req.user.role, role: req.user.role,
@@ -293,17 +272,7 @@ export const updateTimeEntry = async (req, res, next) => {
todoId, todoId,
companyId, companyId,
description, description,
}); }, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
// 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,
@@ -324,23 +293,10 @@ export const deleteTimeEntry = async (req, res, next) => {
const userId = req.userId; 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, userId,
role: req.user.role, role: req.user.role,
}); }, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
// 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) {

View File

@@ -1,6 +1,5 @@
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
@@ -26,11 +25,9 @@ export const uploadTimesheet = async (req, res, next) => {
year, year,
month, month,
file: req.file, file: req.file,
auditContext: { userId: req.userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] },
}); });
// 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 },
@@ -112,16 +109,10 @@ export const deleteTimesheet = async (req, res, next) => {
const userId = req.userId; 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, userId,
role: req.user.role, role: req.user.role,
}); }, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
// 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,

View File

@@ -1,11 +1,4 @@
import * as todoService from '../services/todo.service.js'; import * as todoService from '../services/todo.service.js';
import {
logTodoCreated,
logTodoDeleted,
logTodoCompleted,
logTodoUpdated,
logTodoUncompleted,
} from '../services/audit.service.js';
/** /**
* Get all todos * Get all todos
@@ -119,10 +112,7 @@ export const createTodo = async (req, res, next) => {
const userId = req.userId; const userId = req.userId;
const data = req.body; const data = req.body;
const todo = await todoService.createTodo(userId, data); const todo = await todoService.createTodo(userId, data, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
// 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,
@@ -145,20 +135,7 @@ export const updateTodo = async (req, res, next) => {
const { todoId } = req.params; const { todoId } = req.params;
const data = req.body; const data = req.body;
// Get old todo for audit const todo = await todoService.updateTodo(todoId, data, userId, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
const oldTodo = await todoService.getTodoById(todoId);
const todo = await todoService.updateTodo(todoId, data, userId);
// 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,
@@ -179,14 +156,7 @@ export const deleteTodo = async (req, res, next) => {
const { todoId } = req.params; const { todoId } = req.params;
const userId = req.userId; const userId = req.userId;
// Get todo info before deleting const result = await todoService.deleteTodo(todoId, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
const todo = await todoService.getTodoById(todoId);
const todoTitle = todo?.title;
const result = await todoService.deleteTodo(todoId);
// Log audit event
await logTodoDeleted(userId, todoId, todoTitle, req.ip, req.headers['user-agent']);
res.status(200).json({ res.status(200).json({
success: true, success: true,
@@ -213,14 +183,7 @@ export const toggleTodo = async (req, res, next) => {
// Toggle completed status // Toggle completed status
const updated = await todoService.updateTodo(todoId, { const updated = await todoService.updateTodo(todoId, {
status: wasCompleted ? 'pending' : 'completed', status: wasCompleted ? 'pending' : 'completed',
}); }, null, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
// 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({ res.status(200).json({
success: true, success: true,

View File

@@ -5,6 +5,7 @@ import { hashPassword, generateTempPassword } from '../utils/password.js';
import { ConflictError, NotFoundError } from '../utils/errors.js'; import { ConflictError, NotFoundError } from '../utils/errors.js';
import * as emailAccountService from './email-account.service.js'; import * as emailAccountService from './email-account.service.js';
import { logger } from '../utils/logger.js'; import { logger } from '../utils/logger.js';
import { logUserCreation, logRoleChange, logUserDeleted } from './audit.service.js';
/** /**
* Skontroluj či username už neexistuje * Skontroluj či username už neexistuje
@@ -22,7 +23,7 @@ export const checkUsernameExists = async (username) => {
/** /**
* Vytvorenie nového usera s automatic temporary password * Vytvorenie nového usera s automatic temporary password
*/ */
export const createUser = async (username, firstName, lastName, role, email, emailPassword) => { export const createUser = async (username, firstName, lastName, role, email, emailPassword, auditContext = null) => {
// Skontroluj či username už neexistuje // Skontroluj či username už neexistuje
if (await checkUsernameExists(username)) { if (await checkUsernameExists(username)) {
throw new ConflictError('Username už existuje'); throw new ConflictError('Username už existuje');
@@ -72,6 +73,10 @@ export const createUser = async (username, firstName, lastName, role, email, ema
} }
} }
if (auditContext) {
await logUserCreation(auditContext.userId, newUser.id, username, newUser.role, auditContext.ipAddress, auditContext.userAgent);
}
return { return {
user: newUser, user: newUser,
tempPassword, tempPassword,
@@ -136,7 +141,7 @@ export const getUserById = async (userId) => {
/** /**
* Zmena role usera * Zmena role usera
*/ */
export const changeUserRole = async (userId, newRole) => { export const changeUserRole = async (userId, newRole, auditContext = null) => {
// Získaj starú rolu // Získaj starú rolu
const [user] = await db const [user] = await db
.select() .select()
@@ -159,6 +164,10 @@ export const changeUserRole = async (userId, newRole) => {
}) })
.where(eq(users.id, userId)); .where(eq(users.id, userId));
if (auditContext) {
await logRoleChange(auditContext.userId, userId, oldRole, newRole, auditContext.ipAddress, auditContext.userAgent);
}
return { userId, oldRole, newRole }; return { userId, oldRole, newRole };
}; };
@@ -233,7 +242,7 @@ export const resetUserPassword = async (userId) => {
/** /**
* Zmazanie usera * Zmazanie usera
*/ */
export const deleteUser = async (userId) => { export const deleteUser = async (userId, auditContext = null) => {
const [user] = await db const [user] = await db
.select() .select()
.from(users) .from(users)
@@ -264,6 +273,9 @@ export const deleteUser = async (userId) => {
const emailAccountIds = userEmailAccountLinks.map(link => link.emailAccountId); const emailAccountIds = userEmailAccountLinks.map(link => link.emailAccountId);
// Save username before deletion for audit
const deletedUsername = user.username;
// Delete user (cascades userEmailAccounts links) // Delete user (cascades userEmailAccounts links)
await db.delete(users).where(eq(users.id, userId)); await db.delete(users).where(eq(users.id, userId));
@@ -281,5 +293,9 @@ export const deleteUser = async (userId) => {
} }
} }
if (auditContext) {
await logUserDeleted(auditContext.userId, userId, deletedUsername, auditContext.ipAddress, auditContext.userAgent);
}
return { deletedEmailAccounts }; return { deletedEmailAccounts };
}; };

View File

@@ -8,11 +8,13 @@ import {
AuthenticationError, AuthenticationError,
NotFoundError, NotFoundError,
} from '../utils/errors.js'; } from '../utils/errors.js';
import { logLoginAttempt, logLogin, logLogout, logPasswordChange, logEmailLink } from './audit.service.js';
/** /**
* KROK 1: Login s temporary password * KROK 1: Login s temporary password
*/ */
export const loginWithTempPassword = async (username, password, ipAddress, userAgent) => { export const loginWithTempPassword = async (username, password, ipAddress, userAgent) => {
try {
// Najdi usera // Najdi usera
const [user] = await db const [user] = await db
.select() .select()
@@ -56,6 +58,10 @@ export const loginWithTempPassword = async (username, password, ipAddress, userA
// Check if user has email accounts (many-to-many) // Check if user has email accounts (many-to-many)
const userEmailAccounts = await emailAccountService.getUserEmailAccounts(user.id); const userEmailAccounts = await emailAccountService.getUserEmailAccounts(user.id);
// Log successful login
await logLoginAttempt(username, true, ipAddress, userAgent);
await logLogin(user.id, username, ipAddress, userAgent);
return { return {
user: { user: {
id: user.id, id: user.id,
@@ -69,12 +75,17 @@ export const loginWithTempPassword = async (username, password, ipAddress, userA
needsPasswordChange: !user.changedPassword, needsPasswordChange: !user.changedPassword,
needsEmailSetup: userEmailAccounts.length === 0, needsEmailSetup: userEmailAccounts.length === 0,
}; };
} catch (error) {
// Log failed login attempt
await logLoginAttempt(username, false, ipAddress, userAgent, error.message);
throw error;
}
}; };
/** /**
* KROK 2: Nastavenie nového hesla * KROK 2: Nastavenie nového hesla
*/ */
export const setNewPassword = async (userId, newPassword) => { export const setNewPassword = async (userId, newPassword, auditContext = null) => {
const [user] = await db const [user] = await db
.select() .select()
.from(users) .from(users)
@@ -104,6 +115,10 @@ export const setNewPassword = async (userId, newPassword) => {
}) })
.where(eq(users.id, userId)); .where(eq(users.id, userId));
if (auditContext) {
await logPasswordChange(auditContext.userId, auditContext.ipAddress, auditContext.userAgent);
}
return { return {
success: true, success: true,
message: 'Heslo úspešne nastavené', message: 'Heslo úspešne nastavené',
@@ -114,7 +129,7 @@ export const setNewPassword = async (userId, newPassword) => {
* KROK 3: Pripojenie emailu s JMAP validáciou * KROK 3: Pripojenie emailu s JMAP validáciou
* Používa many-to-many vzťah cez userEmailAccounts * Používa many-to-many vzťah cez userEmailAccounts
*/ */
export const linkEmail = async (userId, email, emailPassword) => { export const linkEmail = async (userId, email, emailPassword, auditContext = null) => {
const [user] = await db const [user] = await db
.select() .select()
.from(users) .from(users)
@@ -132,6 +147,10 @@ export const linkEmail = async (userId, email, emailPassword) => {
emailPassword emailPassword
); );
if (auditContext) {
await logEmailLink(auditContext.userId, email, auditContext.ipAddress, auditContext.userAgent);
}
return { return {
success: true, success: true,
accountId: newEmailAccount.jmapAccountId, accountId: newEmailAccount.jmapAccountId,
@@ -167,7 +186,11 @@ export const skipEmailSetup = async (userId) => {
/** /**
* Logout - clear tokens (handled on client side) * Logout - clear tokens (handled on client side)
*/ */
export const logout = async () => { export const logout = async (auditContext = null) => {
if (auditContext) {
await logLogout(auditContext.userId, auditContext.ipAddress, auditContext.userAgent);
}
return { return {
success: true, success: true,
message: 'Úspešne odhlásený', message: 'Úspešne odhlásený',

View File

@@ -3,6 +3,7 @@ import { companies, companyReminders } from '../db/schema.js';
import { eq, desc, sql, and, lte, gte, isNull, or, inArray } from 'drizzle-orm'; import { eq, desc, sql, and, lte, gte, isNull, or, inArray } from 'drizzle-orm';
import { NotFoundError, BadRequestError } from '../utils/errors.js'; import { NotFoundError, BadRequestError } from '../utils/errors.js';
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js'; import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
import { logCompanyReminderCreated, logCompanyReminderUpdated, logCompanyReminderDeleted } from './audit.service.js';
const ensureCompanyExists = async (companyId) => { const ensureCompanyExists = async (companyId) => {
const [company] = await db const [company] = await db
@@ -44,7 +45,7 @@ export const getRemindersByCompanyId = async (companyId) => {
return reminders; return reminders;
}; };
export const createReminder = async (companyId, data) => { export const createReminder = async (companyId, data, auditContext = null) => {
await ensureCompanyExists(companyId); await ensureCompanyExists(companyId);
const description = data.description?.trim(); const description = data.description?.trim();
@@ -62,10 +63,14 @@ export const createReminder = async (companyId, data) => {
}) })
.returning(); .returning();
if (auditContext) {
await logCompanyReminderCreated(auditContext.userId, reminder.id, companyId, data.dueDate, auditContext.ipAddress, auditContext.userAgent);
}
return reminder; return reminder;
}; };
export const updateReminder = async (companyId, reminderId, data) => { export const updateReminder = async (companyId, reminderId, data, auditContext = null) => {
const reminder = await getReminderById(reminderId); const reminder = await getReminderById(reminderId);
if (reminder.companyId !== companyId) { if (reminder.companyId !== companyId) {
@@ -91,10 +96,14 @@ export const updateReminder = async (companyId, reminderId, data) => {
.where(eq(companyReminders.id, reminderId)) .where(eq(companyReminders.id, reminderId))
.returning(); .returning();
if (auditContext) {
await logCompanyReminderUpdated(auditContext.userId, reminderId, companyId, reminder.dueDate, data.dueDate, auditContext.ipAddress, auditContext.userAgent);
}
return updatedReminder; return updatedReminder;
}; };
export const deleteReminder = async (companyId, reminderId) => { export const deleteReminder = async (companyId, reminderId, auditContext = null) => {
const reminder = await getReminderById(reminderId); const reminder = await getReminderById(reminderId);
if (reminder.companyId !== companyId) { if (reminder.companyId !== companyId) {
@@ -103,6 +112,10 @@ export const deleteReminder = async (companyId, reminderId) => {
await db.delete(companyReminders).where(eq(companyReminders.id, reminderId)); await db.delete(companyReminders).where(eq(companyReminders.id, reminderId));
if (auditContext) {
await logCompanyReminderDeleted(auditContext.userId, reminderId, companyId, reminder.dueDate, auditContext.ipAddress, auditContext.userAgent);
}
return { success: true, message: 'Reminder bol odstránený' }; return { success: true, message: 'Reminder bol odstránený' };
}; };

View File

@@ -3,6 +3,10 @@ import { companies, projects, todos, notes, companyReminders, companyUsers, user
import { eq, desc, ilike, or, and, inArray } from 'drizzle-orm'; import { eq, desc, ilike, or, and, inArray } from 'drizzle-orm';
import { NotFoundError, ConflictError } from '../utils/errors.js'; import { NotFoundError, ConflictError } from '../utils/errors.js';
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js'; import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
import {
logCompanyCreated, logCompanyDeleted, logCompanyUpdated,
logCompanyUserAssigned, logCompanyUserRemoved,
} from './audit.service.js';
/** /**
* Get all companies * Get all companies
@@ -111,7 +115,7 @@ export const getCompanyById = async (companyId) => {
/** /**
* Create new company * Create new company
*/ */
export const createCompany = async (userId, data) => { export const createCompany = async (userId, data, auditContext = null) => {
const { name, description, address, city, postalCode, country, phone, email, website, status } = data; const { name, description, address, city, postalCode, country, phone, email, website, status } = data;
// Check if company with same name already exists // Check if company with same name already exists
@@ -150,13 +154,17 @@ export const createCompany = async (userId, data) => {
addedBy: userId, addedBy: userId,
}); });
if (auditContext) {
await logCompanyCreated(auditContext.userId, newCompany.id, newCompany.name, auditContext.ipAddress, auditContext.userAgent);
}
return newCompany; return newCompany;
}; };
/** /**
* Update company * Update company
*/ */
export const updateCompany = async (companyId, data) => { export const updateCompany = async (companyId, data, auditContext = null) => {
const company = await getCompanyById(companyId); const company = await getCompanyById(companyId);
const { name, description, address, city, postalCode, country, phone, email, website, status } = data; const { name, description, address, city, postalCode, country, phone, email, website, status } = data;
@@ -192,17 +200,25 @@ export const updateCompany = async (companyId, data) => {
.where(eq(companies.id, companyId)) .where(eq(companies.id, companyId))
.returning(); .returning();
if (auditContext) {
await logCompanyUpdated(auditContext.userId, companyId, { name: company.name }, { name: updated.name }, auditContext.ipAddress, auditContext.userAgent);
}
return updated; return updated;
}; };
/** /**
* Delete company * Delete company
*/ */
export const deleteCompany = async (companyId) => { export const deleteCompany = async (companyId, auditContext = null) => {
await getCompanyById(companyId); // Check if exists const company = await getCompanyById(companyId); // Check if exists
await db.delete(companies).where(eq(companies.id, companyId)); await db.delete(companies).where(eq(companies.id, companyId));
if (auditContext) {
await logCompanyDeleted(auditContext.userId, companyId, company.name, auditContext.ipAddress, auditContext.userAgent);
}
return { success: true, message: 'Firma bola odstránená' }; return { success: true, message: 'Firma bola odstránená' };
}; };
@@ -281,8 +297,8 @@ export const getCompanyUsers = async (companyId) => {
/** /**
* Assign user to company * Assign user to company
*/ */
export const assignUserToCompany = async (companyId, userId, addedByUserId, role = null) => { export const assignUserToCompany = async (companyId, userId, addedByUserId, role = null, auditContext = null) => {
await getCompanyById(companyId); // Verify company exists const company = await getCompanyById(companyId); // Verify company exists
// Verify user exists // Verify user exists
const [user] = await db const [user] = await db
@@ -325,6 +341,10 @@ export const assignUserToCompany = async (companyId, userId, addedByUserId, role
.where(eq(companyUsers.id, assignment.id)) .where(eq(companyUsers.id, assignment.id))
.limit(1); .limit(1);
if (auditContext) {
await logCompanyUserAssigned(auditContext.userId, companyId, userId, company.name, auditContext.ipAddress, auditContext.userAgent);
}
return { return {
id: row.company_users.id, id: row.company_users.id,
userId: row.company_users.userId, userId: row.company_users.userId,
@@ -343,8 +363,8 @@ export const assignUserToCompany = async (companyId, userId, addedByUserId, role
/** /**
* Remove user from company * Remove user from company
*/ */
export const removeUserFromCompany = async (companyId, userId) => { export const removeUserFromCompany = async (companyId, userId, auditContext = null) => {
await getCompanyById(companyId); // Verify company exists const company = await getCompanyById(companyId); // Verify company exists
// Check if user is assigned // Check if user is assigned
const [existing] = await db const [existing] = await db
@@ -362,6 +382,10 @@ export const removeUserFromCompany = async (companyId, userId) => {
.delete(companyUsers) .delete(companyUsers)
.where(and(eq(companyUsers.companyId, companyId), eq(companyUsers.userId, userId))); .where(and(eq(companyUsers.companyId, companyId), eq(companyUsers.userId, userId)));
if (auditContext) {
await logCompanyUserRemoved(auditContext.userId, companyId, userId, company.name, auditContext.ipAddress, auditContext.userAgent);
}
return { success: true, message: 'Používateľ bol odstránený z firmy' }; return { success: true, message: 'Používateľ bol odstránený z firmy' };
}; };

View File

@@ -3,6 +3,7 @@ import { contacts, emails, companies, emailAccounts } from '../db/schema.js';
import { eq, and, desc, or, ne } from 'drizzle-orm'; import { eq, and, desc, or, ne } from 'drizzle-orm';
import { NotFoundError, ConflictError } from '../utils/errors.js'; import { NotFoundError, ConflictError } from '../utils/errors.js';
import { syncEmailsFromSender } from './jmap/index.js'; import { syncEmailsFromSender } from './jmap/index.js';
import { logContactLinkedToCompany, logCompanyCreatedFromContact } from './audit.service.js';
/** /**
* Get contacts with related data (emailAccount, company) using joins * Get contacts with related data (emailAccount, company) using joins
@@ -185,7 +186,7 @@ export const updateContact = async (contactId, emailAccountId, { name, notes })
/** /**
* Link company to contact * Link company to contact
*/ */
export const linkCompanyToContact = async (contactId, emailAccountId, companyId) => { export const linkCompanyToContact = async (contactId, emailAccountId, companyId, auditContext = null) => {
const contact = await getContactById(contactId, emailAccountId); const contact = await getContactById(contactId, emailAccountId);
const [updated] = await db const [updated] = await db
@@ -206,6 +207,10 @@ export const linkCompanyToContact = async (contactId, emailAccountId, companyId)
}) })
.where(eq(emails.contactId, contactId)); .where(eq(emails.contactId, contactId));
if (auditContext) {
await logContactLinkedToCompany(auditContext.userId, contactId, companyId, updated.company?.name || companyId, auditContext.ipAddress, auditContext.userAgent);
}
return updated; return updated;
}; };
@@ -240,7 +245,7 @@ export const unlinkCompanyFromContact = async (contactId, emailAccountId) => {
* Create company from contact * Create company from contact
* Creates a new company using contact's information and links it * Creates a new company using contact's information and links it
*/ */
export const createCompanyFromContact = async (contactId, emailAccountId, userId, companyData = {}) => { export const createCompanyFromContact = async (contactId, emailAccountId, userId, companyData = {}, auditContext = null) => {
const contact = await getContactById(contactId, emailAccountId); const contact = await getContactById(contactId, emailAccountId);
// Check if company with same name already exists // Check if company with same name already exists
@@ -291,6 +296,10 @@ export const createCompanyFromContact = async (contactId, emailAccountId, userId
}) })
.where(eq(emails.contactId, contactId)); .where(eq(emails.contactId, contactId));
if (auditContext) {
await logCompanyCreatedFromContact(auditContext.userId, contactId, newCompany.id, newCompany.name, auditContext.ipAddress, auditContext.userAgent);
}
return { return {
company: newCompany, company: newCompany,
contact: updatedContact, contact: updatedContact,

View File

@@ -2,6 +2,7 @@ import { db } from '../config/database.js';
import { notes, companies, projects, todos, contacts, users } from '../db/schema.js'; import { notes, companies, projects, todos, contacts, users } from '../db/schema.js';
import { eq, desc, ilike, or, and } from 'drizzle-orm'; import { eq, desc, ilike, or, and } from 'drizzle-orm';
import { NotFoundError } from '../utils/errors.js'; import { NotFoundError } from '../utils/errors.js';
import { logNoteCreated, logNoteUpdated, logNoteDeleted } from './audit.service.js';
/** /**
* Get all notes * Get all notes
@@ -86,7 +87,7 @@ export const getNoteById = async (noteId) => {
/** /**
* Create new note * Create new note
*/ */
export const createNote = async (userId, data) => { export const createNote = async (userId, data, auditContext = null) => {
const { title, content, companyId, projectId, todoId, contactId, dueDate } = data; const { title, content, companyId, projectId, todoId, contactId, dueDate } = data;
// Verify company exists if provided // Verify company exists if provided
@@ -155,13 +156,17 @@ export const createNote = async (userId, data) => {
}) })
.returning(); .returning();
if (auditContext) {
await logNoteCreated(auditContext.userId, newNote.id, newNote.content, auditContext.ipAddress, auditContext.userAgent);
}
return newNote; return newNote;
}; };
/** /**
* Update note * Update note
*/ */
export const updateNote = async (noteId, data) => { export const updateNote = async (noteId, data, auditContext = null) => {
const note = await getNoteById(noteId); const note = await getNoteById(noteId);
const { title, content, companyId, projectId, todoId, contactId, dueDate } = data; const { title, content, companyId, projectId, todoId, contactId, dueDate } = data;
@@ -239,17 +244,25 @@ export const updateNote = async (noteId, data) => {
.where(eq(notes.id, noteId)) .where(eq(notes.id, noteId))
.returning(); .returning();
if (auditContext) {
await logNoteUpdated(auditContext.userId, noteId, note.content, updated.content, auditContext.ipAddress, auditContext.userAgent);
}
return updated; return updated;
}; };
/** /**
* Delete note * Delete note
*/ */
export const deleteNote = async (noteId) => { export const deleteNote = async (noteId, auditContext = null) => {
await getNoteById(noteId); // Check if exists const note = await getNoteById(noteId); // Check if exists
await db.delete(notes).where(eq(notes.id, noteId)); await db.delete(notes).where(eq(notes.id, noteId));
if (auditContext) {
await logNoteDeleted(auditContext.userId, noteId, note.content, auditContext.ipAddress, auditContext.userAgent);
}
return { success: true, message: 'Poznámka bola odstránená' }; return { success: true, message: 'Poznámka bola odstránená' };
}; };

View File

@@ -3,6 +3,10 @@ import { projects, todos, notes, timesheets, companies, projectUsers, users } fr
import { eq, desc, ilike, or, and, inArray } from 'drizzle-orm'; import { eq, desc, ilike, or, and, inArray } from 'drizzle-orm';
import { NotFoundError, ConflictError } from '../utils/errors.js'; import { NotFoundError, ConflictError } from '../utils/errors.js';
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js'; import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
import {
logProjectCreated, logProjectDeleted, logProjectUpdated,
logProjectUserAssigned, logProjectUserRemoved,
} from './audit.service.js';
/** /**
* Get all projects * Get all projects
@@ -109,7 +113,7 @@ export const getProjectById = async (projectId) => {
/** /**
* Create new project * Create new project
*/ */
export const createProject = async (userId, data) => { export const createProject = async (userId, data, auditContext = null) => {
const { name, description, companyId, status, startDate, endDate } = data; const { name, description, companyId, status, startDate, endDate } = data;
// If companyId is provided, verify company exists // If companyId is provided, verify company exists
@@ -146,13 +150,17 @@ export const createProject = async (userId, data) => {
addedBy: userId, addedBy: userId,
}); });
if (auditContext) {
await logProjectCreated(auditContext.userId, newProject.id, newProject.name, auditContext.ipAddress, auditContext.userAgent);
}
return newProject; return newProject;
}; };
/** /**
* Update project * Update project
*/ */
export const updateProject = async (projectId, data) => { export const updateProject = async (projectId, data, auditContext = null) => {
const project = await getProjectById(projectId); const project = await getProjectById(projectId);
const { name, description, companyId, status, startDate, endDate } = data; const { name, description, companyId, status, startDate, endDate } = data;
@@ -187,17 +195,25 @@ export const updateProject = async (projectId, data) => {
.where(eq(projects.id, projectId)) .where(eq(projects.id, projectId))
.returning(); .returning();
if (auditContext) {
await logProjectUpdated(auditContext.userId, projectId, { name: project.name }, { name: updated.name }, auditContext.ipAddress, auditContext.userAgent);
}
return updated; return updated;
}; };
/** /**
* Delete project * Delete project
*/ */
export const deleteProject = async (projectId) => { export const deleteProject = async (projectId, auditContext = null) => {
await getProjectById(projectId); // Check if exists const project = await getProjectById(projectId); // Check if exists
await db.delete(projects).where(eq(projects.id, projectId)); await db.delete(projects).where(eq(projects.id, projectId));
if (auditContext) {
await logProjectDeleted(auditContext.userId, projectId, project.name, auditContext.ipAddress, auditContext.userAgent);
}
return { success: true, message: 'Projekt bol odstránený' }; return { success: true, message: 'Projekt bol odstránený' };
}; };
@@ -314,8 +330,8 @@ export const getProjectUsers = async (projectId) => {
/** /**
* Assign user to project * Assign user to project
*/ */
export const assignUserToProject = async (projectId, userId, addedByUserId, role = null) => { export const assignUserToProject = async (projectId, userId, addedByUserId, role = null, auditContext = null) => {
await getProjectById(projectId); // Verify project exists const project = await getProjectById(projectId); // Verify project exists
// Verify user exists // Verify user exists
const [user] = await db const [user] = await db
@@ -358,6 +374,10 @@ export const assignUserToProject = async (projectId, userId, addedByUserId, role
.where(eq(projectUsers.id, assignment.id)) .where(eq(projectUsers.id, assignment.id))
.limit(1); .limit(1);
if (auditContext) {
await logProjectUserAssigned(auditContext.userId, projectId, userId, project.name, auditContext.ipAddress, auditContext.userAgent);
}
return { return {
id: row.project_users.id, id: row.project_users.id,
userId: row.project_users.userId, userId: row.project_users.userId,
@@ -376,8 +396,8 @@ export const assignUserToProject = async (projectId, userId, addedByUserId, role
/** /**
* Remove user from project * Remove user from project
*/ */
export const removeUserFromProject = async (projectId, userId) => { export const removeUserFromProject = async (projectId, userId, auditContext = null) => {
await getProjectById(projectId); // Verify project exists const project = await getProjectById(projectId); // Verify project exists
// Check if user is assigned // Check if user is assigned
const [existing] = await db const [existing] = await db
@@ -395,6 +415,10 @@ export const removeUserFromProject = async (projectId, userId) => {
.delete(projectUsers) .delete(projectUsers)
.where(and(eq(projectUsers.projectId, projectId), eq(projectUsers.userId, userId))); .where(and(eq(projectUsers.projectId, projectId), eq(projectUsers.userId, userId)));
if (auditContext) {
await logProjectUserRemoved(auditContext.userId, projectId, userId, project.name, auditContext.ipAddress, auditContext.userAgent);
}
return { success: true, message: 'Používateľ bol odstránený z projektu' }; return { success: true, message: 'Používateľ bol odstránený z projektu' };
}; };

View File

@@ -5,6 +5,10 @@ import { NotFoundError, BadRequestError, ForbiddenError } from '../utils/errors.
import ExcelJS from 'exceljs'; import ExcelJS from 'exceljs';
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import {
logTimerStarted, logTimerStopped, logTimerPaused, logTimerResumed,
logTimeEntryUpdated, logTimeEntryDeleted,
} from './audit.service.js';
// Helpers to normalize optional payload fields // Helpers to normalize optional payload fields
const normalizeOptionalId = (value) => { const normalizeOptionalId = (value) => {
@@ -216,7 +220,7 @@ const saveTimesheetFile = async (workbook, { userId, year, month, filename }) =>
/** /**
* Start a new time entry * Start a new time entry
*/ */
export const startTimeEntry = async (userId, data) => { export const startTimeEntry = async (userId, data, auditContext = null) => {
const projectId = normalizeOptionalId(data.projectId); const projectId = normalizeOptionalId(data.projectId);
const todoId = normalizeOptionalId(data.todoId); const todoId = normalizeOptionalId(data.todoId);
const companyId = normalizeOptionalId(data.companyId); const companyId = normalizeOptionalId(data.companyId);
@@ -268,13 +272,17 @@ export const startTimeEntry = async (userId, data) => {
}) })
.returning(); .returning();
if (auditContext) {
await logTimerStarted(auditContext.userId, newEntry.id, data.description || 'Timer', auditContext.ipAddress, auditContext.userAgent);
}
return newEntry; return newEntry;
}; };
/** /**
* Stop a running time entry * Stop a running time entry
*/ */
export const stopTimeEntry = async (entryId, userId, data = {}) => { export const stopTimeEntry = async (entryId, userId, data = {}, auditContext = null) => {
const projectId = normalizeOptionalId(data.projectId); const projectId = normalizeOptionalId(data.projectId);
const todoId = normalizeOptionalId(data.todoId); const todoId = normalizeOptionalId(data.todoId);
const companyId = normalizeOptionalId(data.companyId); const companyId = normalizeOptionalId(data.companyId);
@@ -315,13 +323,17 @@ export const stopTimeEntry = async (entryId, userId, data = {}) => {
.where(eq(timeEntries.id, entryId)) .where(eq(timeEntries.id, entryId))
.returning(); .returning();
if (auditContext) {
await logTimerStopped(auditContext.userId, updated.id, data.description || 'Timer', updated.duration, auditContext.ipAddress, auditContext.userAgent);
}
return updated; return updated;
}; };
/** /**
* Pause a running time entry * Pause a running time entry
*/ */
export const pauseTimeEntry = async (entryId, userId) => { export const pauseTimeEntry = async (entryId, userId, auditContext = null) => {
const entry = await getTimeEntryById(entryId); const entry = await getTimeEntryById(entryId);
if (entry.userId !== userId) { if (entry.userId !== userId) {
@@ -345,13 +357,17 @@ export const pauseTimeEntry = async (entryId, userId) => {
.where(eq(timeEntries.id, entryId)) .where(eq(timeEntries.id, entryId))
.returning(); .returning();
if (auditContext) {
await logTimerPaused(auditContext.userId, updated.id, auditContext.ipAddress, auditContext.userAgent);
}
return updated; return updated;
}; };
/** /**
* Resume a paused time entry * Resume a paused time entry
*/ */
export const resumeTimeEntry = async (entryId, userId) => { export const resumeTimeEntry = async (entryId, userId, auditContext = null) => {
const entry = await getTimeEntryById(entryId); const entry = await getTimeEntryById(entryId);
if (entry.userId !== userId) { if (entry.userId !== userId) {
@@ -380,6 +396,10 @@ export const resumeTimeEntry = async (entryId, userId) => {
.where(eq(timeEntries.id, entryId)) .where(eq(timeEntries.id, entryId))
.returning(); .returning();
if (auditContext) {
await logTimerResumed(auditContext.userId, updated.id, auditContext.ipAddress, auditContext.userAgent);
}
return updated; return updated;
}; };
@@ -603,7 +623,7 @@ export const generateMonthlyTimesheet = async (userId, year, month) => {
/** /**
* Update time entry * Update time entry
*/ */
export const updateTimeEntry = async (entryId, actor, data) => { export const updateTimeEntry = async (entryId, actor, data, auditContext = null) => {
const { userId, role } = actor; const { userId, role } = actor;
const entry = await getTimeEntryById(entryId); const entry = await getTimeEntryById(entryId);
@@ -652,13 +672,17 @@ export const updateTimeEntry = async (entryId, actor, data) => {
.where(eq(timeEntries.id, entryId)) .where(eq(timeEntries.id, entryId))
.returning(); .returning();
if (auditContext) {
await logTimeEntryUpdated(auditContext.userId, entryId, { description: entry.description, duration: entry.duration }, { description: updated.description, duration: updated.duration }, auditContext.ipAddress, auditContext.userAgent);
}
return updated; return updated;
}; };
/** /**
* Delete time entry * Delete time entry
*/ */
export const deleteTimeEntry = async (entryId, actor) => { export const deleteTimeEntry = async (entryId, actor, auditContext = null) => {
const { userId, role } = actor; const { userId, role } = actor;
const entry = await getTimeEntryById(entryId); const entry = await getTimeEntryById(entryId);
@@ -672,6 +696,10 @@ export const deleteTimeEntry = async (entryId, actor) => {
await db.delete(timeEntries).where(eq(timeEntries.id, entryId)); await db.delete(timeEntries).where(eq(timeEntries.id, entryId));
if (auditContext) {
await logTimeEntryDeleted(auditContext.userId, entryId, entry.description || 'Time entry', entry.duration, auditContext.ipAddress, auditContext.userAgent);
}
return { success: true, message: 'Záznam bol odstránený' }; return { success: true, message: 'Záznam bol odstránený' };
}; };

View File

@@ -5,6 +5,7 @@ import { timesheets, users } from '../db/schema.js';
import { and, desc, eq } from 'drizzle-orm'; import { and, desc, eq } from 'drizzle-orm';
import { BadRequestError, ForbiddenError, NotFoundError } from '../utils/errors.js'; import { BadRequestError, ForbiddenError, NotFoundError } from '../utils/errors.js';
import { logger } from '../utils/logger.js'; import { logger } from '../utils/logger.js';
import { logTimesheetUploaded, logTimesheetDeleted } from './audit.service.js';
const ALLOWED_MIME_TYPES = [ const ALLOWED_MIME_TYPES = [
'application/pdf', 'application/pdf',
@@ -91,7 +92,7 @@ const generateTimesheetFileName = (firstName, lastName, username, year, month, f
return `${namePrefix}-vykazprace-${year}-${monthStr}${fileExt}`; return `${namePrefix}-vykazprace-${year}-${monthStr}${fileExt}`;
}; };
export const uploadTimesheet = async ({ userId, year, month, file }) => { export const uploadTimesheet = async ({ userId, year, month, file, auditContext = null }) => {
if (!file) { if (!file) {
throw new BadRequestError('Súbor nebol nahraný'); throw new BadRequestError('Súbor nebol nahraný');
} }
@@ -138,7 +139,13 @@ export const uploadTimesheet = async ({ userId, year, month, file }) => {
}) })
.returning(); .returning();
return sanitizeTimesheet(newTimesheet); const sanitized = sanitizeTimesheet(newTimesheet);
if (auditContext) {
await logTimesheetUploaded(auditContext.userId, newTimesheet.id, parsedYear, parsedMonth, auditContext.ipAddress, auditContext.userAgent);
}
return sanitized;
} catch (error) { } catch (error) {
await safeUnlink(filePath); await safeUnlink(filePath);
throw error; throw error;
@@ -225,10 +232,14 @@ export const getDownloadInfo = async (timesheetId, { userId, role }) => {
}; };
}; };
export const deleteTimesheet = async (timesheetId, { userId, role }) => { export const deleteTimesheet = async (timesheetId, { userId, role }, auditContext = null) => {
const timesheet = await ensureTimesheetExists(timesheetId); const timesheet = await ensureTimesheetExists(timesheetId);
assertAccess(timesheet, { userId, role }); assertAccess(timesheet, { userId, role });
await safeUnlink(timesheet.filePath); await safeUnlink(timesheet.filePath);
await db.delete(timesheets).where(eq(timesheets.id, timesheetId)); await db.delete(timesheets).where(eq(timesheets.id, timesheetId));
if (auditContext) {
await logTimesheetDeleted(auditContext.userId, timesheetId, timesheet.year, timesheet.month, auditContext.ipAddress, auditContext.userAgent);
}
}; };

View File

@@ -5,6 +5,9 @@ import { NotFoundError } from '../utils/errors.js';
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js'; import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
import { sendPushNotificationToUsers } from './push.service.js'; import { sendPushNotificationToUsers } from './push.service.js';
import { logger } from '../utils/logger.js'; import { logger } from '../utils/logger.js';
import {
logTodoCreated, logTodoDeleted, logTodoCompleted, logTodoUpdated, logTodoUncompleted,
} from './audit.service.js';
/** /**
* Get all todos * Get all todos
@@ -142,7 +145,7 @@ export const getTodoById = async (todoId) => {
* @param {string} userId - ID of user creating the todo * @param {string} userId - ID of user creating the todo
* @param {object} data - Todo data including assignedUserIds array * @param {object} data - Todo data including assignedUserIds array
*/ */
export const createTodo = async (userId, data) => { export const createTodo = async (userId, data, auditContext = null) => {
const { title, description, projectId, companyId, assignedUserIds, status, priority, dueDate } = data; const { title, description, projectId, companyId, assignedUserIds, status, priority, dueDate } = data;
// Verify project exists if provided // Verify project exists if provided
@@ -228,6 +231,10 @@ export const createTodo = async (userId, data) => {
} }
} }
if (auditContext) {
await logTodoCreated(auditContext.userId, newTodo.id, newTodo.title, auditContext.ipAddress, auditContext.userAgent);
}
return newTodo; return newTodo;
}; };
@@ -237,7 +244,7 @@ export const createTodo = async (userId, data) => {
* @param {object} data - Updated data including assignedUserIds array * @param {object} data - Updated data including assignedUserIds array
* @param {string} updatedByUserId - ID of user making the update (for notifications) * @param {string} updatedByUserId - ID of user making the update (for notifications)
*/ */
export const updateTodo = async (todoId, data, updatedByUserId = null) => { export const updateTodo = async (todoId, data, updatedByUserId = null, auditContext = null) => {
const todo = await getTodoById(todoId); const todo = await getTodoById(todoId);
const { title, description, projectId, companyId, assignedUserIds, status, priority, dueDate } = data; const { title, description, projectId, companyId, assignedUserIds, status, priority, dueDate } = data;
@@ -352,17 +359,32 @@ export const updateTodo = async (todoId, data, updatedByUserId = null) => {
} }
} }
if (auditContext) {
// Detect status changes for specific audit events
if (status === 'completed' && todo.status !== 'completed') {
await logTodoCompleted(auditContext.userId, todoId, todo.title, auditContext.ipAddress, auditContext.userAgent);
} else if (status && status !== 'completed' && todo.status === 'completed') {
await logTodoUncompleted(auditContext.userId, todoId, todo.title, auditContext.ipAddress, auditContext.userAgent);
} else {
await logTodoUpdated(auditContext.userId, todoId, { title: todo.title }, { title: updated.title }, auditContext.ipAddress, auditContext.userAgent);
}
}
return updated; return updated;
}; };
/** /**
* Delete todo * Delete todo
*/ */
export const deleteTodo = async (todoId) => { export const deleteTodo = async (todoId, auditContext = null) => {
await getTodoById(todoId); // Check if exists const todo = await getTodoById(todoId); // Check if exists
await db.delete(todos).where(eq(todos.id, todoId)); await db.delete(todos).where(eq(todos.id, todoId));
if (auditContext) {
await logTodoDeleted(auditContext.userId, todoId, todo.title, auditContext.ipAddress, auditContext.userAgent);
}
return { success: true, message: 'Todo bolo odstránené' }; return { success: true, message: 'Todo bolo odstránené' };
}; };