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

View File

@@ -1,11 +1,4 @@
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 { getUserById } from '../services/auth.service.js';
@@ -26,10 +19,6 @@ export const login = async (req, res, next) => {
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)
const isProduction = process.env.NODE_ENV === 'production';
res.cookie('accessToken', result.tokens.accessToken, {
@@ -60,9 +49,6 @@ export const login = async (req, res, next) => {
message: 'Prihlásenie úspešné',
});
} catch (error) {
// Log failed login
await logLoginAttempt(username, false, ipAddress, userAgent, error.message);
next(error);
}
};
@@ -141,10 +127,7 @@ export const setPassword = async (req, res, next) => {
const userAgent = req.headers['user-agent'];
try {
const result = await authService.setNewPassword(userId, newPassword);
// Log password change
await logPasswordChange(userId, ipAddress, userAgent);
const result = await authService.setNewPassword(userId, newPassword, { userId, ipAddress, userAgent });
res.status(200).json({
success: true,
@@ -168,10 +151,7 @@ export const linkEmail = async (req, res, next) => {
const userAgent = req.headers['user-agent'];
try {
const result = await authService.linkEmail(userId, email, emailPassword);
// Log email link
await logEmailLink(userId, email, ipAddress, userAgent);
const result = await authService.linkEmail(userId, email, emailPassword, { userId, ipAddress, userAgent });
res.status(200).json({
success: true,
@@ -218,10 +198,7 @@ export const logout = async (req, res, next) => {
const ipAddress = req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'];
// Log logout event
await logLogout(userId, ipAddress, userAgent);
const result = await authService.logout();
const result = await authService.logout({ userId, ipAddress, userAgent });
// Vymaž cookies
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 companyReminderService from '../services/company-reminder.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
@@ -125,10 +115,7 @@ export const createCompany = async (req, res, next) => {
const userId = req.userId;
const data = req.body;
const company = await companyService.createCompany(userId, data);
// Log audit event
await logCompanyCreated(userId, company.id, company.name, req.ip, req.headers['user-agent']);
const company = await companyService.createCompany(userId, data, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(201).json({
success: true,
@@ -151,20 +138,7 @@ export const updateCompany = async (req, res, next) => {
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']
);
const company = await companyService.updateCompany(companyId, data, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(200).json({
success: true,
@@ -185,14 +159,7 @@ export const deleteCompany = async (req, res, next) => {
const { companyId } = req.params;
const userId = req.userId;
// Get company info before deleting
const company = await companyService.getCompanyById(companyId);
const companyName = company?.name;
const result = await companyService.deleteCompany(companyId);
// Log audit event
await logCompanyDeleted(userId, companyId, companyName, req.ip, req.headers['user-agent']);
const result = await companyService.deleteCompany(companyId, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(200).json({
success: true,
@@ -318,10 +285,7 @@ export const createCompanyReminder = async (req, res, next) => {
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']);
const reminder = await companyReminderService.createReminder(companyId, { description, dueDate, isChecked }, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(201).json({
success: true,
@@ -339,13 +303,7 @@ export const updateCompanyReminder = async (req, res, next) => {
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']);
const reminder = await companyReminderService.updateReminder(companyId, reminderId, { description, dueDate, isChecked }, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(200).json({
success: true,
@@ -362,13 +320,7 @@ export const deleteCompanyReminder = async (req, res, next) => {
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']);
const result = await companyReminderService.deleteReminder(companyId, reminderId, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(200).json({
success: true,
@@ -453,20 +405,7 @@ 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']
);
const assignment = await companyService.assignUserToCompany(companyId, userId, currentUserId, role, { userId: currentUserId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(201).json({
success: true,
@@ -487,20 +426,7 @@ export const removeUserFromCompany = async (req, res, next) => {
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']
);
const result = await companyService.removeUserFromCompany(companyId, userId, { userId: currentUserId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(200).json({
success: true,

View File

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

View File

@@ -1,5 +1,4 @@
import * as noteService from '../services/note.service.js';
import { logNoteCreated, logNoteUpdated, logNoteDeleted } from '../services/audit.service.js';
/**
* Get all notes
@@ -58,10 +57,7 @@ export const createNote = async (req, res, next) => {
const userId = req.userId;
const data = req.body;
const note = await noteService.createNote(userId, data);
// Log audit event
await logNoteCreated(userId, note.id, note.content, req.ip, req.headers['user-agent']);
const note = await noteService.createNote(userId, data, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(201).json({
success: true,
@@ -84,13 +80,7 @@ export const updateNote = async (req, res, next) => {
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']);
const note = await noteService.updateNote(noteId, data, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(200).json({
success: true,
@@ -111,13 +101,7 @@ export const deleteNote = async (req, res, next) => {
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']);
const result = await noteService.deleteNote(noteId, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(200).json({
success: true,

View File

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

View File

@@ -1,12 +1,4 @@
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
@@ -22,10 +14,7 @@ export const startTimeEntry = async (req, res, next) => {
todoId,
companyId,
description,
});
// Log audit event
await logTimerStarted(userId, entry.id, description || 'Timer', req.ip, req.headers['user-agent']);
}, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(201).json({
success: true,
@@ -52,10 +41,7 @@ export const stopTimeEntry = async (req, res, next) => {
todoId,
companyId,
description,
});
// Log audit event
await logTimerStopped(userId, entry.id, description || 'Timer', entry.duration, req.ip, req.headers['user-agent']);
}, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(200).json({
success: true,
@@ -76,9 +62,7 @@ export const pauseTimeEntry = async (req, res, next) => {
const userId = req.userId;
const { entryId } = req.params;
const entry = await timeTrackingService.pauseTimeEntry(entryId, userId);
await logTimerPaused(userId, entry.id, req.ip, req.headers['user-agent']);
const entry = await timeTrackingService.pauseTimeEntry(entryId, userId, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(200).json({
success: true,
@@ -99,9 +83,7 @@ export const resumeTimeEntry = async (req, res, next) => {
const userId = req.userId;
const { entryId } = req.params;
const entry = await timeTrackingService.resumeTimeEntry(entryId, userId);
await logTimerResumed(userId, entry.id, req.ip, req.headers['user-agent']);
const entry = await timeTrackingService.resumeTimeEntry(entryId, userId, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(200).json({
success: true,
@@ -280,9 +262,6 @@ export const updateTimeEntry = async (req, res, next) => {
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,
role: req.user.role,
@@ -293,17 +272,7 @@ export const updateTimeEntry = async (req, res, next) => {
todoId,
companyId,
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']
);
}, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(200).json({
success: true,
@@ -324,23 +293,10 @@ export const deleteTimeEntry = async (req, res, next) => {
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,
role: req.user.role,
});
// Log audit event
await logTimeEntryDeleted(
userId,
entryId,
entry.description || 'Time entry',
entry.duration,
req.ip,
req.headers['user-agent']
);
}, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(200).json(result);
} catch (error) {

View File

@@ -1,6 +1,5 @@
import * as timesheetService from '../services/timesheet.service.js';
import { ForbiddenError } from '../utils/errors.js';
import { logTimesheetUploaded, logTimesheetDeleted } from '../services/audit.service.js';
/**
* Upload timesheet
@@ -26,11 +25,9 @@ export const uploadTimesheet = async (req, res, next) => {
year,
month,
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({
success: true,
data: { timesheet },
@@ -112,16 +109,10 @@ export const deleteTimesheet = async (req, res, next) => {
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,
role: req.user.role,
});
// Log audit event
await logTimesheetDeleted(userId, timesheetId, timesheet?.year, timesheet?.month, req.ip, req.headers['user-agent']);
}, { userId, ipAddress: req.ip, userAgent: req.headers['user-agent'] });
res.status(200).json({
success: true,

View File

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

View File

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

View File

@@ -8,11 +8,13 @@ import {
AuthenticationError,
NotFoundError,
} from '../utils/errors.js';
import { logLoginAttempt, logLogin, logLogout, logPasswordChange, logEmailLink } from './audit.service.js';
/**
* KROK 1: Login s temporary password
*/
export const loginWithTempPassword = async (username, password, ipAddress, userAgent) => {
try {
// Najdi usera
const [user] = await db
.select()
@@ -56,6 +58,10 @@ export const loginWithTempPassword = async (username, password, ipAddress, userA
// Check if user has email accounts (many-to-many)
const userEmailAccounts = await emailAccountService.getUserEmailAccounts(user.id);
// Log successful login
await logLoginAttempt(username, true, ipAddress, userAgent);
await logLogin(user.id, username, ipAddress, userAgent);
return {
user: {
id: user.id,
@@ -69,12 +75,17 @@ export const loginWithTempPassword = async (username, password, ipAddress, userA
needsPasswordChange: !user.changedPassword,
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
*/
export const setNewPassword = async (userId, newPassword) => {
export const setNewPassword = async (userId, newPassword, auditContext = null) => {
const [user] = await db
.select()
.from(users)
@@ -104,6 +115,10 @@ export const setNewPassword = async (userId, newPassword) => {
})
.where(eq(users.id, userId));
if (auditContext) {
await logPasswordChange(auditContext.userId, auditContext.ipAddress, auditContext.userAgent);
}
return {
success: true,
message: 'Heslo úspešne nastavené',
@@ -114,7 +129,7 @@ export const setNewPassword = async (userId, newPassword) => {
* KROK 3: Pripojenie emailu s JMAP validáciou
* 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
.select()
.from(users)
@@ -132,6 +147,10 @@ export const linkEmail = async (userId, email, emailPassword) => {
emailPassword
);
if (auditContext) {
await logEmailLink(auditContext.userId, email, auditContext.ipAddress, auditContext.userAgent);
}
return {
success: true,
accountId: newEmailAccount.jmapAccountId,
@@ -167,7 +186,11 @@ export const skipEmailSetup = async (userId) => {
/**
* 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 {
success: true,
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 { NotFoundError, BadRequestError } from '../utils/errors.js';
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
import { logCompanyReminderCreated, logCompanyReminderUpdated, logCompanyReminderDeleted } from './audit.service.js';
const ensureCompanyExists = async (companyId) => {
const [company] = await db
@@ -44,7 +45,7 @@ export const getRemindersByCompanyId = async (companyId) => {
return reminders;
};
export const createReminder = async (companyId, data) => {
export const createReminder = async (companyId, data, auditContext = null) => {
await ensureCompanyExists(companyId);
const description = data.description?.trim();
@@ -62,10 +63,14 @@ export const createReminder = async (companyId, data) => {
})
.returning();
if (auditContext) {
await logCompanyReminderCreated(auditContext.userId, reminder.id, companyId, data.dueDate, auditContext.ipAddress, auditContext.userAgent);
}
return reminder;
};
export const updateReminder = async (companyId, reminderId, data) => {
export const updateReminder = async (companyId, reminderId, data, auditContext = null) => {
const reminder = await getReminderById(reminderId);
if (reminder.companyId !== companyId) {
@@ -91,10 +96,14 @@ export const updateReminder = async (companyId, reminderId, data) => {
.where(eq(companyReminders.id, reminderId))
.returning();
if (auditContext) {
await logCompanyReminderUpdated(auditContext.userId, reminderId, companyId, reminder.dueDate, data.dueDate, auditContext.ipAddress, auditContext.userAgent);
}
return updatedReminder;
};
export const deleteReminder = async (companyId, reminderId) => {
export const deleteReminder = async (companyId, reminderId, auditContext = null) => {
const reminder = await getReminderById(reminderId);
if (reminder.companyId !== companyId) {
@@ -103,6 +112,10 @@ export const deleteReminder = async (companyId, 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ý' };
};

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 { NotFoundError, ConflictError } from '../utils/errors.js';
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
import {
logCompanyCreated, logCompanyDeleted, logCompanyUpdated,
logCompanyUserAssigned, logCompanyUserRemoved,
} from './audit.service.js';
/**
* Get all companies
@@ -111,7 +115,7 @@ export const getCompanyById = async (companyId) => {
/**
* 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;
// Check if company with same name already exists
@@ -150,13 +154,17 @@ export const createCompany = async (userId, data) => {
addedBy: userId,
});
if (auditContext) {
await logCompanyCreated(auditContext.userId, newCompany.id, newCompany.name, auditContext.ipAddress, auditContext.userAgent);
}
return newCompany;
};
/**
* Update company
*/
export const updateCompany = async (companyId, data) => {
export const updateCompany = async (companyId, data, auditContext = null) => {
const company = await getCompanyById(companyId);
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))
.returning();
if (auditContext) {
await logCompanyUpdated(auditContext.userId, companyId, { name: company.name }, { name: updated.name }, auditContext.ipAddress, auditContext.userAgent);
}
return updated;
};
/**
* Delete company
*/
export const deleteCompany = async (companyId) => {
await getCompanyById(companyId); // Check if exists
export const deleteCompany = async (companyId, auditContext = null) => {
const company = await getCompanyById(companyId); // Check if exists
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á' };
};
@@ -281,8 +297,8 @@ export const getCompanyUsers = async (companyId) => {
/**
* Assign user to company
*/
export const assignUserToCompany = async (companyId, userId, addedByUserId, role = null) => {
await getCompanyById(companyId); // Verify company exists
export const assignUserToCompany = async (companyId, userId, addedByUserId, role = null, auditContext = null) => {
const company = await getCompanyById(companyId); // Verify company exists
// Verify user exists
const [user] = await db
@@ -325,6 +341,10 @@ export const assignUserToCompany = async (companyId, userId, addedByUserId, role
.where(eq(companyUsers.id, assignment.id))
.limit(1);
if (auditContext) {
await logCompanyUserAssigned(auditContext.userId, companyId, userId, company.name, auditContext.ipAddress, auditContext.userAgent);
}
return {
id: row.company_users.id,
userId: row.company_users.userId,
@@ -343,8 +363,8 @@ export const assignUserToCompany = async (companyId, userId, addedByUserId, role
/**
* Remove user from company
*/
export const removeUserFromCompany = async (companyId, userId) => {
await getCompanyById(companyId); // Verify company exists
export const removeUserFromCompany = async (companyId, userId, auditContext = null) => {
const company = await getCompanyById(companyId); // Verify company exists
// Check if user is assigned
const [existing] = await db
@@ -362,6 +382,10 @@ export const removeUserFromCompany = async (companyId, userId) => {
.delete(companyUsers)
.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' };
};

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 { NotFoundError, ConflictError } from '../utils/errors.js';
import { syncEmailsFromSender } from './jmap/index.js';
import { logContactLinkedToCompany, logCompanyCreatedFromContact } from './audit.service.js';
/**
* 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
*/
export const linkCompanyToContact = async (contactId, emailAccountId, companyId) => {
export const linkCompanyToContact = async (contactId, emailAccountId, companyId, auditContext = null) => {
const contact = await getContactById(contactId, emailAccountId);
const [updated] = await db
@@ -206,6 +207,10 @@ export const linkCompanyToContact = async (contactId, emailAccountId, companyId)
})
.where(eq(emails.contactId, contactId));
if (auditContext) {
await logContactLinkedToCompany(auditContext.userId, contactId, companyId, updated.company?.name || companyId, auditContext.ipAddress, auditContext.userAgent);
}
return updated;
};
@@ -240,7 +245,7 @@ export const unlinkCompanyFromContact = async (contactId, emailAccountId) => {
* Create company from contact
* 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);
// Check if company with same name already exists
@@ -291,6 +296,10 @@ export const createCompanyFromContact = async (contactId, emailAccountId, userId
})
.where(eq(emails.contactId, contactId));
if (auditContext) {
await logCompanyCreatedFromContact(auditContext.userId, contactId, newCompany.id, newCompany.name, auditContext.ipAddress, auditContext.userAgent);
}
return {
company: newCompany,
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 { eq, desc, ilike, or, and } from 'drizzle-orm';
import { NotFoundError } from '../utils/errors.js';
import { logNoteCreated, logNoteUpdated, logNoteDeleted } from './audit.service.js';
/**
* Get all notes
@@ -86,7 +87,7 @@ export const getNoteById = async (noteId) => {
/**
* 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;
// Verify company exists if provided
@@ -155,13 +156,17 @@ export const createNote = async (userId, data) => {
})
.returning();
if (auditContext) {
await logNoteCreated(auditContext.userId, newNote.id, newNote.content, auditContext.ipAddress, auditContext.userAgent);
}
return newNote;
};
/**
* Update note
*/
export const updateNote = async (noteId, data) => {
export const updateNote = async (noteId, data, auditContext = null) => {
const note = await getNoteById(noteId);
const { title, content, companyId, projectId, todoId, contactId, dueDate } = data;
@@ -239,17 +244,25 @@ export const updateNote = async (noteId, data) => {
.where(eq(notes.id, noteId))
.returning();
if (auditContext) {
await logNoteUpdated(auditContext.userId, noteId, note.content, updated.content, auditContext.ipAddress, auditContext.userAgent);
}
return updated;
};
/**
* Delete note
*/
export const deleteNote = async (noteId) => {
await getNoteById(noteId); // Check if exists
export const deleteNote = async (noteId, auditContext = null) => {
const note = await getNoteById(noteId); // Check if exists
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á' };
};

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 { NotFoundError, ConflictError } from '../utils/errors.js';
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
import {
logProjectCreated, logProjectDeleted, logProjectUpdated,
logProjectUserAssigned, logProjectUserRemoved,
} from './audit.service.js';
/**
* Get all projects
@@ -109,7 +113,7 @@ export const getProjectById = async (projectId) => {
/**
* 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;
// If companyId is provided, verify company exists
@@ -146,13 +150,17 @@ export const createProject = async (userId, data) => {
addedBy: userId,
});
if (auditContext) {
await logProjectCreated(auditContext.userId, newProject.id, newProject.name, auditContext.ipAddress, auditContext.userAgent);
}
return newProject;
};
/**
* Update project
*/
export const updateProject = async (projectId, data) => {
export const updateProject = async (projectId, data, auditContext = null) => {
const project = await getProjectById(projectId);
const { name, description, companyId, status, startDate, endDate } = data;
@@ -187,17 +195,25 @@ export const updateProject = async (projectId, data) => {
.where(eq(projects.id, projectId))
.returning();
if (auditContext) {
await logProjectUpdated(auditContext.userId, projectId, { name: project.name }, { name: updated.name }, auditContext.ipAddress, auditContext.userAgent);
}
return updated;
};
/**
* Delete project
*/
export const deleteProject = async (projectId) => {
await getProjectById(projectId); // Check if exists
export const deleteProject = async (projectId, auditContext = null) => {
const project = await getProjectById(projectId); // Check if exists
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ý' };
};
@@ -314,8 +330,8 @@ export const getProjectUsers = async (projectId) => {
/**
* Assign user to project
*/
export const assignUserToProject = async (projectId, userId, addedByUserId, role = null) => {
await getProjectById(projectId); // Verify project exists
export const assignUserToProject = async (projectId, userId, addedByUserId, role = null, auditContext = null) => {
const project = await getProjectById(projectId); // Verify project exists
// Verify user exists
const [user] = await db
@@ -358,6 +374,10 @@ export const assignUserToProject = async (projectId, userId, addedByUserId, role
.where(eq(projectUsers.id, assignment.id))
.limit(1);
if (auditContext) {
await logProjectUserAssigned(auditContext.userId, projectId, userId, project.name, auditContext.ipAddress, auditContext.userAgent);
}
return {
id: row.project_users.id,
userId: row.project_users.userId,
@@ -376,8 +396,8 @@ export const assignUserToProject = async (projectId, userId, addedByUserId, role
/**
* Remove user from project
*/
export const removeUserFromProject = async (projectId, userId) => {
await getProjectById(projectId); // Verify project exists
export const removeUserFromProject = async (projectId, userId, auditContext = null) => {
const project = await getProjectById(projectId); // Verify project exists
// Check if user is assigned
const [existing] = await db
@@ -395,6 +415,10 @@ export const removeUserFromProject = async (projectId, userId) => {
.delete(projectUsers)
.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' };
};

View File

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

View File

@@ -5,6 +5,7 @@ import { timesheets, users } from '../db/schema.js';
import { and, desc, eq } from 'drizzle-orm';
import { BadRequestError, ForbiddenError, NotFoundError } from '../utils/errors.js';
import { logger } from '../utils/logger.js';
import { logTimesheetUploaded, logTimesheetDeleted } from './audit.service.js';
const ALLOWED_MIME_TYPES = [
'application/pdf',
@@ -91,7 +92,7 @@ const generateTimesheetFileName = (firstName, lastName, username, year, month, f
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) {
throw new BadRequestError('Súbor nebol nahraný');
}
@@ -138,7 +139,13 @@ export const uploadTimesheet = async ({ userId, year, month, file }) => {
})
.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) {
await safeUnlink(filePath);
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);
assertAccess(timesheet, { userId, role });
await safeUnlink(timesheet.filePath);
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 { sendPushNotificationToUsers } from './push.service.js';
import { logger } from '../utils/logger.js';
import {
logTodoCreated, logTodoDeleted, logTodoCompleted, logTodoUpdated, logTodoUncompleted,
} from './audit.service.js';
/**
* Get all todos
@@ -142,7 +145,7 @@ export const getTodoById = async (todoId) => {
* @param {string} userId - ID of user creating the todo
* @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;
// 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;
};
@@ -237,7 +244,7 @@ export const createTodo = async (userId, data) => {
* @param {object} data - Updated data including assignedUserIds array
* @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 { 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;
};
/**
* Delete todo
*/
export const deleteTodo = async (todoId) => {
await getTodoById(todoId); // Check if exists
export const deleteTodo = async (todoId, auditContext = null) => {
const todo = await getTodoById(todoId); // Check if exists
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é' };
};