feat: Add comprehensive audit logging system
- Add audit logging for contacts (link company, create company from contact) - Add audit logging for notes (create, update, delete) - Add audit logging for companies (update, user assign/remove, reminder CRUD) - Add audit logging for projects (update, user assign/remove) - Add audit logging for todos (update, uncomplete) - Add audit logging for time entries (update, delete) - Add audit logging for timesheets (upload, delete) - Add audit logging for user deletion - Add pagination and filters to audit logs API (userId, action, resource, dateFrom, dateTo) - Add endpoints for distinct actions and resources 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import * as adminService from '../services/admin.service.js';
|
||||
import * as statusService from '../services/status.service.js';
|
||||
import { logUserCreation, logRoleChange } from '../services/audit.service.js';
|
||||
import { logUserCreation, logRoleChange, logUserDeleted } from '../services/audit.service.js';
|
||||
import { triggerEventNotifications } from '../cron/index.js';
|
||||
|
||||
/**
|
||||
@@ -131,10 +131,19 @@ export const changeUserRole = async (req, res, next) => {
|
||||
*/
|
||||
export const deleteUser = async (req, res, next) => {
|
||||
const { userId } = req.params;
|
||||
const adminId = req.userId;
|
||||
const ipAddress = req.ip || req.connection.remoteAddress;
|
||||
const userAgent = req.headers['user-agent'];
|
||||
|
||||
try {
|
||||
// Get user info before deletion for audit
|
||||
const userToDelete = await adminService.getUserById(userId);
|
||||
|
||||
const result = await adminService.deleteUser(userId);
|
||||
|
||||
// Log user deletion
|
||||
await logUserDeleted(adminId, userId, userToDelete.username, ipAddress, userAgent);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Používateľ bol zmazaný',
|
||||
|
||||
@@ -1,12 +1,62 @@
|
||||
import { db } from '../config/database.js';
|
||||
import { auditLogs, users } from '../db/schema.js';
|
||||
import { desc, eq } from 'drizzle-orm';
|
||||
import { desc, eq, and, gte, lte, like, sql } from 'drizzle-orm';
|
||||
|
||||
export const getRecentAuditLogs = async (req, res, next) => {
|
||||
try {
|
||||
const { limit = 20, userId } = req.query;
|
||||
const {
|
||||
page = 1,
|
||||
limit = 50,
|
||||
userId,
|
||||
action,
|
||||
resource,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
} = req.query;
|
||||
|
||||
let query = db
|
||||
const pageNum = parseInt(page);
|
||||
const limitNum = parseInt(limit);
|
||||
const offset = (pageNum - 1) * limitNum;
|
||||
|
||||
// Build conditions array
|
||||
const conditions = [];
|
||||
|
||||
if (userId) {
|
||||
conditions.push(eq(auditLogs.userId, userId));
|
||||
}
|
||||
|
||||
if (action) {
|
||||
conditions.push(eq(auditLogs.action, action));
|
||||
}
|
||||
|
||||
if (resource) {
|
||||
conditions.push(eq(auditLogs.resource, resource));
|
||||
}
|
||||
|
||||
if (dateFrom) {
|
||||
conditions.push(gte(auditLogs.createdAt, new Date(dateFrom)));
|
||||
}
|
||||
|
||||
if (dateTo) {
|
||||
// Add one day to include the entire end date
|
||||
const endDate = new Date(dateTo);
|
||||
endDate.setDate(endDate.getDate() + 1);
|
||||
conditions.push(lte(auditLogs.createdAt, endDate));
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
// Get total count for pagination
|
||||
const [countResult] = await db
|
||||
.select({ count: sql`count(*)::int` })
|
||||
.from(auditLogs)
|
||||
.where(whereClause);
|
||||
|
||||
const totalCount = countResult?.count || 0;
|
||||
const totalPages = Math.ceil(totalCount / limitNum);
|
||||
|
||||
// Get paginated logs
|
||||
const logs = await db
|
||||
.select({
|
||||
id: auditLogs.id,
|
||||
userId: auditLogs.userId,
|
||||
@@ -15,6 +65,7 @@ export const getRecentAuditLogs = async (req, res, next) => {
|
||||
resourceId: auditLogs.resourceId,
|
||||
oldValue: auditLogs.oldValue,
|
||||
newValue: auditLogs.newValue,
|
||||
ipAddress: auditLogs.ipAddress,
|
||||
success: auditLogs.success,
|
||||
createdAt: auditLogs.createdAt,
|
||||
// User info
|
||||
@@ -24,18 +75,58 @@ export const getRecentAuditLogs = async (req, res, next) => {
|
||||
})
|
||||
.from(auditLogs)
|
||||
.leftJoin(users, eq(auditLogs.userId, users.id))
|
||||
.where(whereClause)
|
||||
.orderBy(desc(auditLogs.createdAt))
|
||||
.limit(parseInt(limit));
|
||||
|
||||
if (userId) {
|
||||
query = query.where(eq(auditLogs.userId, userId));
|
||||
}
|
||||
|
||||
const logs = await query;
|
||||
.limit(limitNum)
|
||||
.offset(offset);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: logs,
|
||||
data: {
|
||||
logs,
|
||||
pagination: {
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
totalCount,
|
||||
totalPages,
|
||||
hasNextPage: pageNum < totalPages,
|
||||
hasPrevPage: pageNum > 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Get distinct actions for filter dropdown
|
||||
export const getAuditActions = async (req, res, next) => {
|
||||
try {
|
||||
const actions = await db
|
||||
.selectDistinct({ action: auditLogs.action })
|
||||
.from(auditLogs)
|
||||
.orderBy(auditLogs.action);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: actions.map((a) => a.action),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Get distinct resources for filter dropdown
|
||||
export const getAuditResources = async (req, res, next) => {
|
||||
try {
|
||||
const resources = await db
|
||||
.selectDistinct({ resource: auditLogs.resource })
|
||||
.from(auditLogs)
|
||||
.orderBy(auditLogs.resource);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: resources.map((r) => r.resource),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -2,7 +2,16 @@ import * as companyService from '../services/company.service.js';
|
||||
import * as noteService from '../services/note.service.js';
|
||||
import * as companyReminderService from '../services/company-reminder.service.js';
|
||||
import * as companyEmailService from '../services/company-email.service.js';
|
||||
import { logCompanyCreated, logCompanyDeleted } from '../services/audit.service.js';
|
||||
import {
|
||||
logCompanyCreated,
|
||||
logCompanyDeleted,
|
||||
logCompanyUpdated,
|
||||
logCompanyUserAssigned,
|
||||
logCompanyUserRemoved,
|
||||
logCompanyReminderCreated,
|
||||
logCompanyReminderUpdated,
|
||||
logCompanyReminderDeleted,
|
||||
} from '../services/audit.service.js';
|
||||
|
||||
/**
|
||||
* Get all companies
|
||||
@@ -138,11 +147,25 @@ export const createCompany = async (req, res, next) => {
|
||||
*/
|
||||
export const updateCompany = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const { companyId } = req.params;
|
||||
const data = req.body;
|
||||
|
||||
// Get old company for audit
|
||||
const oldCompany = await companyService.getCompanyById(companyId);
|
||||
|
||||
const company = await companyService.updateCompany(companyId, data);
|
||||
|
||||
// Log audit event
|
||||
await logCompanyUpdated(
|
||||
userId,
|
||||
companyId,
|
||||
{ name: oldCompany.name },
|
||||
{ name: company.name },
|
||||
req.ip,
|
||||
req.headers['user-agent']
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: company,
|
||||
@@ -291,11 +314,15 @@ export const getCompanyReminders = async (req, res, next) => {
|
||||
|
||||
export const createCompanyReminder = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const { companyId } = req.params;
|
||||
const { description, dueDate, isChecked } = req.body;
|
||||
|
||||
const reminder = await companyReminderService.createReminder(companyId, { description, dueDate, isChecked });
|
||||
|
||||
// Log audit event
|
||||
await logCompanyReminderCreated(userId, reminder.id, companyId, dueDate, req.ip, req.headers['user-agent']);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: reminder,
|
||||
@@ -308,11 +335,18 @@ export const createCompanyReminder = async (req, res, next) => {
|
||||
|
||||
export const updateCompanyReminder = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const { companyId, reminderId } = req.params;
|
||||
const { description, dueDate, isChecked } = req.body;
|
||||
|
||||
// Get old reminder for audit
|
||||
const oldReminder = await companyReminderService.getReminderById(reminderId);
|
||||
|
||||
const reminder = await companyReminderService.updateReminder(companyId, reminderId, { description, dueDate, isChecked });
|
||||
|
||||
// Log audit event
|
||||
await logCompanyReminderUpdated(userId, reminderId, companyId, oldReminder?.dueDate, dueDate, req.ip, req.headers['user-agent']);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: reminder,
|
||||
@@ -325,10 +359,17 @@ export const updateCompanyReminder = async (req, res, next) => {
|
||||
|
||||
export const deleteCompanyReminder = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const { companyId, reminderId } = req.params;
|
||||
|
||||
// Get reminder for audit before deletion
|
||||
const reminder = await companyReminderService.getReminderById(reminderId);
|
||||
|
||||
const result = await companyReminderService.deleteReminder(companyId, reminderId);
|
||||
|
||||
// Log audit event
|
||||
await logCompanyReminderDeleted(userId, reminderId, companyId, reminder?.dueDate, req.ip, req.headers['user-agent']);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: result.message,
|
||||
@@ -412,8 +453,21 @@ export const assignUserToCompany = async (req, res, next) => {
|
||||
const { companyId } = req.params;
|
||||
const { userId, role } = req.body;
|
||||
|
||||
// Get company name for audit
|
||||
const company = await companyService.getCompanyById(companyId);
|
||||
|
||||
const assignment = await companyService.assignUserToCompany(companyId, userId, currentUserId, role);
|
||||
|
||||
// Log audit event
|
||||
await logCompanyUserAssigned(
|
||||
currentUserId,
|
||||
companyId,
|
||||
userId,
|
||||
company.name,
|
||||
req.ip,
|
||||
req.headers['user-agent']
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: assignment,
|
||||
@@ -430,10 +484,24 @@ export const assignUserToCompany = async (req, res, next) => {
|
||||
*/
|
||||
export const removeUserFromCompany = async (req, res, next) => {
|
||||
try {
|
||||
const currentUserId = req.userId;
|
||||
const { companyId, userId } = req.params;
|
||||
|
||||
// Get company name for audit
|
||||
const company = await companyService.getCompanyById(companyId);
|
||||
|
||||
const result = await companyService.removeUserFromCompany(companyId, userId);
|
||||
|
||||
// Log audit event
|
||||
await logCompanyUserRemoved(
|
||||
currentUserId,
|
||||
companyId,
|
||||
userId,
|
||||
company.name,
|
||||
req.ip,
|
||||
req.headers['user-agent']
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: result.message,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as contactService from '../services/contact.service.js';
|
||||
import { discoverContactsFromJMAP, getJmapConfigFromAccount } from '../services/jmap/index.js';
|
||||
import * as emailAccountService from '../services/email-account.service.js';
|
||||
import { logContactLinkedToCompany, logCompanyCreatedFromContact } from '../services/audit.service.js';
|
||||
|
||||
/**
|
||||
* Get all contacts for an email account
|
||||
@@ -249,6 +250,16 @@ export const linkCompanyToContact = async (req, res, next) => {
|
||||
|
||||
const updated = await contactService.linkCompanyToContact(contactId, accountId, companyId);
|
||||
|
||||
// Log audit event
|
||||
await logContactLinkedToCompany(
|
||||
userId,
|
||||
contactId,
|
||||
companyId,
|
||||
updated.company?.name || companyId,
|
||||
req.ip,
|
||||
req.headers['user-agent']
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: updated,
|
||||
@@ -321,6 +332,16 @@ export const createCompanyFromContact = async (req, res, next) => {
|
||||
|
||||
const result = await contactService.createCompanyFromContact(contactId, accountId, userId, companyData);
|
||||
|
||||
// Log audit event
|
||||
await logCompanyCreatedFromContact(
|
||||
userId,
|
||||
contactId,
|
||||
result.company?.id,
|
||||
result.company?.name,
|
||||
req.ip,
|
||||
req.headers['user-agent']
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: result,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as noteService from '../services/note.service.js';
|
||||
import { logNoteCreated, logNoteUpdated, logNoteDeleted } from '../services/audit.service.js';
|
||||
|
||||
/**
|
||||
* Get all notes
|
||||
@@ -59,6 +60,9 @@ export const createNote = async (req, res, next) => {
|
||||
|
||||
const note = await noteService.createNote(userId, data);
|
||||
|
||||
// Log audit event
|
||||
await logNoteCreated(userId, note.id, note.content, req.ip, req.headers['user-agent']);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: note,
|
||||
@@ -76,11 +80,18 @@ export const createNote = async (req, res, next) => {
|
||||
*/
|
||||
export const updateNote = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const { noteId } = req.params;
|
||||
const data = req.body;
|
||||
|
||||
// Get old note for audit
|
||||
const oldNote = await noteService.getNoteById(noteId);
|
||||
|
||||
const note = await noteService.updateNote(noteId, data);
|
||||
|
||||
// Log audit event
|
||||
await logNoteUpdated(userId, noteId, oldNote.content, note.content, req.ip, req.headers['user-agent']);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: note,
|
||||
@@ -97,10 +108,17 @@ export const updateNote = async (req, res, next) => {
|
||||
*/
|
||||
export const deleteNote = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const { noteId } = req.params;
|
||||
|
||||
// Get note for audit before deletion
|
||||
const note = await noteService.getNoteById(noteId);
|
||||
|
||||
const result = await noteService.deleteNote(noteId);
|
||||
|
||||
// Log audit event
|
||||
await logNoteDeleted(userId, noteId, note.content, req.ip, req.headers['user-agent']);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: result.message,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import * as projectService from '../services/project.service.js';
|
||||
import * as noteService from '../services/note.service.js';
|
||||
import { logProjectCreated, logProjectDeleted } from '../services/audit.service.js';
|
||||
import {
|
||||
logProjectCreated,
|
||||
logProjectDeleted,
|
||||
logProjectUpdated,
|
||||
logProjectUserAssigned,
|
||||
logProjectUserRemoved,
|
||||
} from '../services/audit.service.js';
|
||||
|
||||
/**
|
||||
* Get all projects
|
||||
@@ -95,11 +101,25 @@ export const createProject = async (req, res, next) => {
|
||||
*/
|
||||
export const updateProject = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const { projectId } = req.params;
|
||||
const data = req.body;
|
||||
|
||||
// Get old project for audit
|
||||
const oldProject = await projectService.getProjectById(projectId);
|
||||
|
||||
const project = await projectService.updateProject(projectId, data);
|
||||
|
||||
// Log audit event
|
||||
await logProjectUpdated(
|
||||
userId,
|
||||
projectId,
|
||||
{ name: oldProject.name },
|
||||
{ name: project.name },
|
||||
req.ip,
|
||||
req.headers['user-agent']
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: project,
|
||||
@@ -257,6 +277,9 @@ export const assignUserToProject = async (req, res, next) => {
|
||||
const { projectId } = req.params;
|
||||
const { userId, role } = req.body;
|
||||
|
||||
// Get project name for audit
|
||||
const project = await projectService.getProjectById(projectId);
|
||||
|
||||
const assignment = await projectService.assignUserToProject(
|
||||
projectId,
|
||||
userId,
|
||||
@@ -264,6 +287,16 @@ export const assignUserToProject = async (req, res, next) => {
|
||||
role
|
||||
);
|
||||
|
||||
// Log audit event
|
||||
await logProjectUserAssigned(
|
||||
currentUserId,
|
||||
projectId,
|
||||
userId,
|
||||
project.name,
|
||||
req.ip,
|
||||
req.headers['user-agent']
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: assignment,
|
||||
@@ -280,10 +313,24 @@ export const assignUserToProject = async (req, res, next) => {
|
||||
*/
|
||||
export const removeUserFromProject = async (req, res, next) => {
|
||||
try {
|
||||
const currentUserId = req.userId;
|
||||
const { projectId, userId } = req.params;
|
||||
|
||||
// Get project name for audit
|
||||
const project = await projectService.getProjectById(projectId);
|
||||
|
||||
const result = await projectService.removeUserFromProject(projectId, userId);
|
||||
|
||||
// Log audit event
|
||||
await logProjectUserRemoved(
|
||||
currentUserId,
|
||||
projectId,
|
||||
userId,
|
||||
project.name,
|
||||
req.ip,
|
||||
req.headers['user-agent']
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: result.message,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import * as timeTrackingService from '../services/time-tracking.service.js';
|
||||
import { logTimerStarted, logTimerStopped } from '../services/audit.service.js';
|
||||
import {
|
||||
logTimerStarted,
|
||||
logTimerStopped,
|
||||
logTimeEntryUpdated,
|
||||
logTimeEntryDeleted,
|
||||
} from '../services/audit.service.js';
|
||||
|
||||
/**
|
||||
* Start a new time entry
|
||||
@@ -223,11 +228,15 @@ export const getTimeEntryWithRelations = async (req, res, next) => {
|
||||
*/
|
||||
export const updateTimeEntry = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const { entryId } = req.params;
|
||||
const { startTime, endTime, projectId, todoId, companyId, description } = req.body;
|
||||
|
||||
// Get old entry for audit
|
||||
const oldEntry = await timeTrackingService.getTimeEntryById(entryId);
|
||||
|
||||
const entry = await timeTrackingService.updateTimeEntry(entryId, {
|
||||
userId: req.userId,
|
||||
userId,
|
||||
role: req.user.role,
|
||||
}, {
|
||||
startTime,
|
||||
@@ -238,6 +247,16 @@ export const updateTimeEntry = async (req, res, next) => {
|
||||
description,
|
||||
});
|
||||
|
||||
// Log audit event
|
||||
await logTimeEntryUpdated(
|
||||
userId,
|
||||
entryId,
|
||||
{ description: oldEntry.description, duration: oldEntry.duration },
|
||||
{ description: entry.description, duration: entry.duration },
|
||||
req.ip,
|
||||
req.headers['user-agent']
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: entry,
|
||||
@@ -254,13 +273,27 @@ export const updateTimeEntry = async (req, res, next) => {
|
||||
*/
|
||||
export const deleteTimeEntry = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const { entryId } = req.params;
|
||||
|
||||
// Get entry for audit before deletion
|
||||
const entry = await timeTrackingService.getTimeEntryById(entryId);
|
||||
|
||||
const result = await timeTrackingService.deleteTimeEntry(entryId, {
|
||||
userId: req.userId,
|
||||
userId,
|
||||
role: req.user.role,
|
||||
});
|
||||
|
||||
// Log audit event
|
||||
await logTimeEntryDeleted(
|
||||
userId,
|
||||
entryId,
|
||||
entry.description || 'Time entry',
|
||||
entry.duration,
|
||||
req.ip,
|
||||
req.headers['user-agent']
|
||||
);
|
||||
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as timesheetService from '../services/timesheet.service.js';
|
||||
import { ForbiddenError } from '../utils/errors.js';
|
||||
import { logTimesheetUploaded, logTimesheetDeleted } from '../services/audit.service.js';
|
||||
|
||||
/**
|
||||
* Upload timesheet
|
||||
@@ -27,6 +28,9 @@ export const uploadTimesheet = async (req, res, next) => {
|
||||
file: req.file,
|
||||
});
|
||||
|
||||
// Log audit event
|
||||
await logTimesheetUploaded(req.userId, timesheet.id, year, month, req.ip, req.headers['user-agent']);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { timesheet },
|
||||
@@ -105,13 +109,20 @@ export const downloadTimesheet = async (req, res, next) => {
|
||||
*/
|
||||
export const deleteTimesheet = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const { timesheetId } = req.params;
|
||||
|
||||
// Get timesheet for audit before deletion
|
||||
const timesheet = await timesheetService.getTimesheetById(timesheetId);
|
||||
|
||||
await timesheetService.deleteTimesheet(timesheetId, {
|
||||
userId: req.userId,
|
||||
userId,
|
||||
role: req.user.role,
|
||||
});
|
||||
|
||||
// Log audit event
|
||||
await logTimesheetDeleted(userId, timesheetId, timesheet?.year, timesheet?.month, req.ip, req.headers['user-agent']);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Timesheet bol zmazaný',
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import * as todoService from '../services/todo.service.js';
|
||||
import { logTodoCreated, logTodoDeleted, logTodoCompleted } from '../services/audit.service.js';
|
||||
import {
|
||||
logTodoCreated,
|
||||
logTodoDeleted,
|
||||
logTodoCompleted,
|
||||
logTodoUpdated,
|
||||
logTodoUncompleted,
|
||||
} from '../services/audit.service.js';
|
||||
|
||||
/**
|
||||
* Get all todos
|
||||
@@ -135,11 +141,25 @@ export const createTodo = async (req, res, next) => {
|
||||
*/
|
||||
export const updateTodo = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const { todoId } = req.params;
|
||||
const data = req.body;
|
||||
|
||||
// Get old todo for audit
|
||||
const oldTodo = await todoService.getTodoById(todoId);
|
||||
|
||||
const todo = await todoService.updateTodo(todoId, data);
|
||||
|
||||
// Log audit event
|
||||
await logTodoUpdated(
|
||||
userId,
|
||||
todoId,
|
||||
{ title: oldTodo.title },
|
||||
{ title: todo.title },
|
||||
req.ip,
|
||||
req.headers['user-agent']
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: todo,
|
||||
@@ -195,9 +215,11 @@ export const toggleTodo = async (req, res, next) => {
|
||||
status: wasCompleted ? 'pending' : 'completed',
|
||||
});
|
||||
|
||||
// Log audit event if todo was completed
|
||||
// Log audit event
|
||||
if (!wasCompleted) {
|
||||
await logTodoCompleted(userId, todoId, todo.title, req.ip, req.headers['user-agent']);
|
||||
} else {
|
||||
await logTodoUncompleted(userId, todoId, todo.title, req.ip, req.headers['user-agent']);
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router } from 'express';
|
||||
import { getRecentAuditLogs } from '../controllers/audit.controller.js';
|
||||
import { getRecentAuditLogs, getAuditActions, getAuditResources } from '../controllers/audit.controller.js';
|
||||
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
||||
import { requireAdmin } from '../middlewares/auth/roleMiddleware.js';
|
||||
|
||||
@@ -7,5 +7,7 @@ const router = Router();
|
||||
|
||||
// Audit logs are admin only
|
||||
router.get('/', authenticate, requireAdmin, getRecentAuditLogs);
|
||||
router.get('/actions', authenticate, requireAdmin, getAuditActions);
|
||||
router.get('/resources', authenticate, requireAdmin, getAuditResources);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -242,3 +242,270 @@ export const logLogout = async (userId, ipAddress, userAgent) => {
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
// User deletion
|
||||
export const logUserDeleted = async (adminId, deletedUserId, username, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId: adminId,
|
||||
action: 'user_deleted',
|
||||
resource: 'user',
|
||||
resourceId: deletedUserId,
|
||||
oldValue: { username },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
// Contacts
|
||||
export const logContactLinkedToCompany = async (userId, contactId, companyId, companyName, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'contact_linked_to_company',
|
||||
resource: 'contact',
|
||||
resourceId: contactId,
|
||||
newValue: { companyId, companyName },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
export const logCompanyCreatedFromContact = async (userId, contactId, companyId, companyName, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'company_created_from_contact',
|
||||
resource: 'contact',
|
||||
resourceId: contactId,
|
||||
newValue: { companyId, companyName },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
// Notes
|
||||
export const logNoteCreated = async (userId, noteId, noteContent, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'note_created',
|
||||
resource: 'note',
|
||||
resourceId: noteId,
|
||||
newValue: { content: noteContent?.substring(0, 100) },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
export const logNoteUpdated = async (userId, noteId, oldContent, newContent, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'note_updated',
|
||||
resource: 'note',
|
||||
resourceId: noteId,
|
||||
oldValue: { content: oldContent?.substring(0, 100) },
|
||||
newValue: { content: newContent?.substring(0, 100) },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
export const logNoteDeleted = async (userId, noteId, noteContent, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'note_deleted',
|
||||
resource: 'note',
|
||||
resourceId: noteId,
|
||||
oldValue: { content: noteContent?.substring(0, 100) },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
// Company - updates and user management
|
||||
export const logCompanyUpdated = async (userId, companyId, oldValues, newValues, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'company_updated',
|
||||
resource: 'company',
|
||||
resourceId: companyId,
|
||||
oldValue: oldValues,
|
||||
newValue: newValues,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
export const logCompanyUserAssigned = async (userId, companyId, assignedUserId, companyName, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'company_user_assigned',
|
||||
resource: 'company',
|
||||
resourceId: companyId,
|
||||
newValue: { assignedUserId, companyName },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
export const logCompanyUserRemoved = async (userId, companyId, removedUserId, companyName, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'company_user_removed',
|
||||
resource: 'company',
|
||||
resourceId: companyId,
|
||||
oldValue: { removedUserId, companyName },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
// Company reminders
|
||||
export const logCompanyReminderCreated = async (userId, reminderId, companyId, reminderDate, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'company_reminder_created',
|
||||
resource: 'company_reminder',
|
||||
resourceId: reminderId,
|
||||
newValue: { companyId, reminderDate },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
export const logCompanyReminderUpdated = async (userId, reminderId, companyId, oldDate, newDate, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'company_reminder_updated',
|
||||
resource: 'company_reminder',
|
||||
resourceId: reminderId,
|
||||
oldValue: { companyId, reminderDate: oldDate },
|
||||
newValue: { companyId, reminderDate: newDate },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
export const logCompanyReminderDeleted = async (userId, reminderId, companyId, reminderDate, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'company_reminder_deleted',
|
||||
resource: 'company_reminder',
|
||||
resourceId: reminderId,
|
||||
oldValue: { companyId, reminderDate },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
// Project - updates and user management
|
||||
export const logProjectUpdated = async (userId, projectId, oldValues, newValues, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'project_updated',
|
||||
resource: 'project',
|
||||
resourceId: projectId,
|
||||
oldValue: oldValues,
|
||||
newValue: newValues,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
export const logProjectUserAssigned = async (userId, projectId, assignedUserId, projectName, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'project_user_assigned',
|
||||
resource: 'project',
|
||||
resourceId: projectId,
|
||||
newValue: { assignedUserId, projectName },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
export const logProjectUserRemoved = async (userId, projectId, removedUserId, projectName, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'project_user_removed',
|
||||
resource: 'project',
|
||||
resourceId: projectId,
|
||||
oldValue: { removedUserId, projectName },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
// Todos - update and uncomplete
|
||||
export const logTodoUpdated = async (userId, todoId, oldValues, newValues, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'todo_updated',
|
||||
resource: 'todo',
|
||||
resourceId: todoId,
|
||||
oldValue: oldValues,
|
||||
newValue: newValues,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
export const logTodoUncompleted = async (userId, todoId, todoTitle, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'todo_uncompleted',
|
||||
resource: 'todo',
|
||||
resourceId: todoId,
|
||||
newValue: { title: todoTitle },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
// Time Entry - update and delete
|
||||
export const logTimeEntryUpdated = async (userId, entryId, oldValues, newValues, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'time_entry_updated',
|
||||
resource: 'time_entry',
|
||||
resourceId: entryId,
|
||||
oldValue: oldValues,
|
||||
newValue: newValues,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
export const logTimeEntryDeleted = async (userId, entryId, projectName, duration, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'time_entry_deleted',
|
||||
resource: 'time_entry',
|
||||
resourceId: entryId,
|
||||
oldValue: { project: projectName, duration },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
// Timesheets
|
||||
export const logTimesheetUploaded = async (userId, timesheetId, year, month, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'timesheet_uploaded',
|
||||
resource: 'timesheet',
|
||||
resourceId: timesheetId,
|
||||
newValue: { year, month },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
export const logTimesheetDeleted = async (userId, timesheetId, year, month, ipAddress, userAgent) => {
|
||||
await logAuditEvent({
|
||||
userId,
|
||||
action: 'timesheet_deleted',
|
||||
resource: 'timesheet',
|
||||
resourceId: timesheetId,
|
||||
oldValue: { year, month },
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ const ensureCompanyExists = async (companyId) => {
|
||||
return company;
|
||||
};
|
||||
|
||||
const getReminderById = async (reminderId) => {
|
||||
export const getReminderById = async (reminderId) => {
|
||||
const [reminder] = await db
|
||||
.select()
|
||||
.from(companyReminders)
|
||||
|
||||
@@ -56,6 +56,10 @@ const ensureTimesheetExists = async (timesheetId) => {
|
||||
return timesheet;
|
||||
};
|
||||
|
||||
export const getTimesheetById = async (timesheetId) => {
|
||||
return ensureTimesheetExists(timesheetId);
|
||||
};
|
||||
|
||||
const assertAccess = (timesheet, { userId, role }) => {
|
||||
if (role !== 'admin' && timesheet.userId !== userId) {
|
||||
throw new ForbiddenError('Nemáte oprávnenie k tomuto timesheetu');
|
||||
|
||||
Reference in New Issue
Block a user