Add Timesheets API with file upload and role-based access

Backend Features:
- Timesheets database table (id, userId, fileName, filePath, fileType, fileSize, year, month, timestamps)
- File upload with multer (memory storage, 10MB limit, PDF/Excel validation)
- Structured file storage: uploads/timesheets/{userId}/{year}/{month}/
- RESTful API endpoints:
  * POST /api/timesheets/upload - Upload timesheet
  * GET /api/timesheets/my - Get user's timesheets (with filters)
  * GET /api/timesheets/all - Get all timesheets (admin only)
  * GET /api/timesheets/:id/download - Download file
  * DELETE /api/timesheets/:id - Delete timesheet
- Role-based permissions: users access own files, admins access all
- Proper error handling and file cleanup on errors
- Database migration for timesheets table

Technical:
- Uses req.user.role for permission checks
- Automatic directory creation for user/year/month structure
- Blob URL cleanup and proper file handling
- Integration with existing auth middleware

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
richardtekula
2025-11-21 08:35:30 +01:00
parent 05be898259
commit bb851639b8
27 changed files with 2847 additions and 532 deletions

View File

@@ -1,10 +1,10 @@
import { db } from '../config/database.js';
import { users } from '../db/schema.js';
import { eq } from 'drizzle-orm';
import { hashPassword, generateTempPassword, encryptPassword } from '../utils/password.js';
import { hashPassword, generateTempPassword } from '../utils/password.js';
import { logUserCreation, logRoleChange } from '../services/audit.service.js';
import { formatErrorResponse, ConflictError, NotFoundError } from '../utils/errors.js';
import { validateJmapCredentials } from '../services/email.service.js';
import * as emailAccountService from '../services/email-account.service.js';
/**
* Vytvorenie nového usera s automatic temporary password (admin only)
@@ -33,28 +33,11 @@ export const createUser = async (req, res) => {
const tempPassword = generateTempPassword(12);
const hashedTempPassword = await hashPassword(tempPassword);
// Ak sú poskytnuté email credentials, validuj ich a získaj JMAP account ID
let jmapAccountId = null;
let encryptedEmailPassword = null;
if (email && emailPassword) {
try {
const { accountId } = await validateJmapCredentials(email, emailPassword);
jmapAccountId = accountId;
encryptedEmailPassword = encryptPassword(emailPassword);
} catch (emailError) {
throw new ConflictError(`Nepodarilo sa overiť emailový účet: ${emailError.message}`);
}
}
// Vytvor usera
const [newUser] = await db
.insert(users)
.values({
username,
email: email || null,
emailPassword: encryptedEmailPassword,
jmapAccountId,
tempPassword: hashedTempPassword,
role: 'member', // Vždy member, nie admin
firstName: firstName || null,
@@ -63,6 +46,33 @@ export const createUser = async (req, res) => {
})
.returning();
// Ak sú poskytnuté email credentials, vytvor email account (many-to-many)
let emailAccountCreated = false;
let emailAccountData = null;
if (email && emailPassword) {
try {
// Použij emailAccountService ktorý automaticky vytvorí many-to-many link
const newEmailAccount = await emailAccountService.createEmailAccount(
newUser.id,
email,
emailPassword
);
emailAccountCreated = true;
emailAccountData = {
id: newEmailAccount.id,
email: newEmailAccount.email,
jmapAccountId: newEmailAccount.jmapAccountId,
shared: newEmailAccount.shared,
};
} catch (emailError) {
// Email account sa nepodarilo vytvoriť, ale user bol vytvorený
// Admin môže pridať email account neskôr
console.error('Failed to create email account:', emailError);
}
}
// Log user creation
await logUserCreation(adminId, newUser.id, username, 'member', ipAddress, userAgent);
@@ -72,17 +82,18 @@ export const createUser = async (req, res) => {
user: {
id: newUser.id,
username: newUser.username,
email: newUser.email,
firstName: newUser.firstName,
lastName: newUser.lastName,
role: newUser.role,
jmapAccountId: newUser.jmapAccountId,
emailSetup: !!newUser.jmapAccountId,
emailSetup: emailAccountCreated,
emailAccount: emailAccountData,
tempPassword: tempPassword, // Vráti plain text password pre admina aby ho mohol poslať userovi
},
},
message: newUser.jmapAccountId
? 'Používateľ úspešne vytvorený s emailovým účtom.'
message: emailAccountCreated
? emailAccountData.shared
? 'Používateľ vytvorený a pripojený k existujúcemu zdieľanému email účtu.'
: 'Používateľ úspešne vytvorený s novým emailovým účtom.'
: 'Používateľ úspešne vytvorený. Email môže byť nastavený neskôr.',
});
} catch (error) {
@@ -101,7 +112,6 @@ export const getAllUsers = async (req, res) => {
.select({
id: users.id,
username: users.username,
email: users.email,
firstName: users.firstName,
lastName: users.lastName,
role: users.role,
@@ -136,7 +146,6 @@ export const getUser = async (req, res) => {
.select({
id: users.id,
username: users.username,
email: users.email,
firstName: users.firstName,
lastName: users.lastName,
role: users.role,
@@ -153,9 +162,17 @@ export const getUser = async (req, res) => {
throw new NotFoundError('Používateľ nenájdený');
}
// Get user's email accounts (cez many-to-many)
const userEmailAccounts = await emailAccountService.getUserEmailAccounts(userId);
res.status(200).json({
success: true,
data: { user },
data: {
user: {
...user,
emailAccounts: userEmailAccounts,
},
},
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');

View File

@@ -5,15 +5,29 @@ import * as emailAccountService from '../services/email-account.service.js';
import { logger } from '../utils/logger.js';
/**
* Get all contacts for authenticated user
* GET /api/contacts?accountId=xxx (optional)
* Get all contacts for an email account
* GET /api/contacts?accountId=xxx (required)
*/
export const getContacts = async (req, res) => {
try {
const userId = req.userId;
const { accountId } = req.query;
const contacts = await contactService.getUserContacts(userId, accountId || null);
if (!accountId) {
return res.status(400).json({
success: false,
error: {
message: 'accountId je povinný parameter',
statusCode: 400,
},
});
}
// Verify user has access to this email account
await emailAccountService.getEmailAccountById(accountId, userId);
// Get contacts for email account
const contacts = await contactService.getContactsForEmailAccount(accountId);
res.status(200).json({
success: true,
@@ -70,7 +84,7 @@ export const discoverContacts = async (req, res) => {
const potentialContacts = await discoverContactsFromJMAP(
jmapConfig,
userId,
emailAccount.id, // emailAccountId
search,
parseInt(limit)
);
@@ -134,12 +148,12 @@ export const addContact = async (req, res) => {
const jmapConfig = getJmapConfigFromAccount(emailAccount);
const contact = await contactService.addContact(
userId,
emailAccount.id,
jmapConfig,
email,
name,
notes
notes,
userId // addedByUserId
);
res.status(201).json({
@@ -155,14 +169,28 @@ export const addContact = async (req, res) => {
/**
* Remove a contact
* DELETE /api/contacts/:contactId
* DELETE /api/contacts/:contactId?accountId=xxx
*/
export const removeContact = async (req, res) => {
try {
const userId = req.userId;
const { contactId } = req.params;
const { accountId } = req.query;
const result = await contactService.removeContact(userId, contactId);
if (!accountId) {
return res.status(400).json({
success: false,
error: {
message: 'accountId je povinný parameter',
statusCode: 400,
},
});
}
// Verify user has access to this email account
await emailAccountService.getEmailAccountById(accountId, userId);
const result = await contactService.removeContact(contactId, accountId);
res.status(200).json({
success: true,
@@ -176,15 +204,29 @@ export const removeContact = async (req, res) => {
/**
* Update a contact
* PATCH /api/contacts/:contactId
* PATCH /api/contacts/:contactId?accountId=xxx
*/
export const updateContact = async (req, res) => {
try {
const userId = req.userId;
const { contactId } = req.params;
const { accountId } = req.query;
const { name, notes } = req.body;
const updated = await contactService.updateContact(userId, contactId, { name, notes });
if (!accountId) {
return res.status(400).json({
success: false,
error: {
message: 'accountId je povinný parameter',
statusCode: 400,
},
});
}
// Verify user has access to this email account
await emailAccountService.getEmailAccountById(accountId, userId);
const updated = await contactService.updateContact(contactId, accountId, { name, notes });
res.status(200).json({
success: true,

View File

@@ -8,14 +8,27 @@ import { logger } from '../utils/logger.js';
/**
* Get all emails for authenticated user
* GET /api/emails?accountId=xxx (optional)
* GET /api/emails?accountId=xxx (REQUIRED)
*/
export const getEmails = async (req, res) => {
try {
const userId = req.userId;
const { accountId } = req.query;
const emails = await crmEmailService.getUserEmails(userId, accountId || null);
if (!accountId) {
return res.status(400).json({
success: false,
error: {
message: 'accountId je povinný parameter',
statusCode: 400,
},
});
}
// Verify user has access to this email account
await emailAccountService.getEmailAccountById(accountId, userId);
const emails = await crmEmailService.getEmailsForAccount(accountId);
res.status(200).json({
success: true,
@@ -30,14 +43,28 @@ export const getEmails = async (req, res) => {
/**
* Get emails by thread (conversation)
* GET /api/emails/thread/:threadId
* GET /api/emails/thread/:threadId?accountId=xxx (accountId required)
*/
export const getThread = async (req, res) => {
try {
const userId = req.userId;
const { threadId } = req.params;
const { accountId } = req.query;
const thread = await crmEmailService.getEmailThread(userId, threadId);
if (!accountId) {
return res.status(400).json({
success: false,
error: {
message: 'accountId je povinný parameter',
statusCode: 400,
},
});
}
// Verify user has access to this email account
await emailAccountService.getEmailAccountById(accountId, userId);
const thread = await crmEmailService.getEmailThread(accountId, threadId);
res.status(200).json({
success: true,
@@ -52,14 +79,27 @@ export const getThread = async (req, res) => {
/**
* Search emails
* GET /api/emails/search?q=query&accountId=xxx (accountId optional)
* GET /api/emails/search?q=query&accountId=xxx (accountId required)
*/
export const searchEmails = async (req, res) => {
try {
const userId = req.userId;
const { q, accountId } = req.query;
const results = await crmEmailService.searchEmails(userId, q, accountId || null);
if (!accountId) {
return res.status(400).json({
success: false,
error: {
message: 'accountId je povinný parameter',
statusCode: 400,
},
});
}
// Verify user has access to this email account
await emailAccountService.getEmailAccountById(accountId, userId);
const results = await crmEmailService.searchEmails(accountId, q);
res.status(200).json({
success: true,
@@ -80,14 +120,20 @@ export const searchEmails = async (req, res) => {
export const getUnreadCount = async (req, res) => {
try {
const userId = req.userId;
const unreadData = await crmEmailService.getUnreadCount(userId);
// Get all user's email accounts
const userAccounts = await emailAccountService.getUserEmailAccounts(userId);
const emailAccountIds = userAccounts.map((account) => account.id);
// Get unread count summary for all accounts
const unreadData = await crmEmailService.getUnreadCountSummary(emailAccountIds);
res.status(200).json({
success: true,
data: {
count: unreadData.totalUnread,
totalUnread: unreadData.totalUnread,
accounts: unreadData.accounts,
accounts: unreadData.byAccount,
lastUpdatedAt: new Date().toISOString(),
},
});
@@ -127,7 +173,7 @@ export const syncEmails = async (req, res) => {
}
// Get contacts for this email account
const contacts = await contactService.getUserContacts(userId, emailAccount.id);
const contacts = await contactService.getContactsForEmailAccount(emailAccount.id);
if (!contacts.length) {
return res.status(200).json({
@@ -145,7 +191,6 @@ export const syncEmails = async (req, res) => {
try {
const { total, saved } = await syncEmailsFromSender(
jmapConfig,
userId,
emailAccount.id,
contact.id,
contact.email,
@@ -175,17 +220,27 @@ export const syncEmails = async (req, res) => {
/**
* Mark email as read/unread
* PATCH /api/emails/:jmapId/read
* PATCH /api/emails/:jmapId/read?accountId=xxx
*/
export const markAsRead = async (req, res) => {
try {
const userId = req.userId;
const { jmapId } = req.params;
const { isRead } = req.body;
const { isRead, accountId } = req.body;
// Get user to access JMAP config
const user = await getUserById(userId);
const jmapConfig = getJmapConfig(user);
if (!accountId) {
return res.status(400).json({
success: false,
error: {
message: 'accountId je povinný parameter',
statusCode: 400,
},
});
}
// Verify user has access to this email account
const emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
const jmapConfig = getJmapConfigFromAccount(emailAccount);
await markEmailAsRead(jmapConfig, userId, jmapId, isRead);
@@ -201,35 +256,40 @@ export const markAsRead = async (req, res) => {
/**
* Mark all emails from contact as read
* POST /api/emails/contact/:contactId/read
* POST /api/emails/contact/:contactId/read?accountId=xxx
*/
export const markContactEmailsRead = async (req, res) => {
try {
const userId = req.userId;
const { contactId } = req.params;
const { accountId } = req.query;
// Get contact to find which email account it belongs to
const contact = await contactService.getContactById(contactId, userId);
if (!contact) {
return res.status(404).json({
if (!accountId) {
return res.status(400).json({
success: false,
error: { message: 'Kontakt nenájdený', statusCode: 404 },
error: {
message: 'accountId je povinný parameter',
statusCode: 400,
},
});
}
// Get email account with credentials
const emailAccount = await emailAccountService.getEmailAccountWithCredentials(contact.emailAccountId, userId);
// Verify user has access to this email account
const emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
const jmapConfig = getJmapConfigFromAccount(emailAccount);
// Mark emails as read in database and get the updated emails
const result = await crmEmailService.markContactEmailsAsRead(userId, contactId);
// Mark emails as read in database
const count = await crmEmailService.markContactEmailsAsRead(contactId, accountId);
// Get the emails that were marked as read to sync with JMAP
const contactEmails = await crmEmailService.getContactEmailsWithUnread(accountId, contactId);
// Also mark emails as read on JMAP server
if (result.emails && result.emails.length > 0) {
logger.info(`Marking ${result.emails.length} emails as read on JMAP server`);
if (contactEmails && contactEmails.length > 0) {
logger.info(`Marking ${contactEmails.length} emails as read on JMAP server`);
for (const email of result.emails) {
if (!email.jmapId) {
for (const email of contactEmails) {
if (!email.jmapId || email.isRead) {
continue;
}
try {
@@ -243,8 +303,8 @@ export const markContactEmailsRead = async (req, res) => {
res.status(200).json({
success: true,
message: `Označených ${result.count} emailov ako prečítaných`,
data: result,
message: `Označených ${count} emailov ako prečítaných`,
data: { count },
});
} catch (error) {
logger.error('ERROR in markContactEmailsRead', { error: error.message });
@@ -255,41 +315,50 @@ export const markContactEmailsRead = async (req, res) => {
/**
* Mark entire thread as read
* POST /api/emails/thread/:threadId/read
* POST /api/emails/thread/:threadId/read?accountId=xxx
*/
export const markThreadRead = async (req, res) => {
try {
const userId = req.userId;
const { threadId } = req.params;
const { accountId } = req.query;
const user = await getUserById(userId);
const threadEmails = await crmEmailService.getEmailThread(userId, threadId);
const unreadEmails = threadEmails.filter((email) => !email.isRead);
let jmapConfig = null;
if (user?.email && user?.emailPassword && user?.jmapAccountId) {
jmapConfig = getJmapConfig(user);
if (!accountId) {
return res.status(400).json({
success: false,
error: {
message: 'accountId je povinný parameter',
statusCode: 400,
},
});
}
if (jmapConfig) {
for (const email of unreadEmails) {
if (!email.jmapId) {
continue;
}
try {
await markEmailAsRead(jmapConfig, userId, email.jmapId, true);
} catch (jmapError) {
logger.error('Failed to mark JMAP email as read', { jmapId: email.jmapId, error: jmapError.message });
}
// Verify user has access to this email account
const emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
const jmapConfig = getJmapConfigFromAccount(emailAccount);
const threadEmails = await crmEmailService.getEmailThread(accountId, threadId);
const unreadEmails = threadEmails.filter((email) => !email.isRead);
// Mark emails as read on JMAP server
for (const email of unreadEmails) {
if (!email.jmapId) {
continue;
}
try {
await markEmailAsRead(jmapConfig, userId, email.jmapId, true);
} catch (jmapError) {
logger.error('Failed to mark JMAP email as read', { jmapId: email.jmapId, error: jmapError.message });
}
}
await crmEmailService.markThreadAsRead(userId, threadId);
// Mark thread as read in database
const count = await crmEmailService.markThreadAsRead(accountId, threadId);
res.status(200).json({
success: true,
message: 'Konverzácia označená ako prečítaná',
count: unreadEmails.length,
count,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
@@ -352,14 +421,28 @@ export const replyToEmail = async (req, res) => {
/**
* Get emails for a specific contact
* GET /api/emails/contact/:contactId
* GET /api/emails/contact/:contactId?accountId=xxx
*/
export const getContactEmails = async (req, res) => {
try {
const userId = req.userId;
const { contactId } = req.params;
const { accountId } = req.query;
const emails = await crmEmailService.getContactEmails(userId, contactId);
if (!accountId) {
return res.status(400).json({
success: false,
error: {
message: 'accountId je povinný parameter',
statusCode: 400,
},
});
}
// Verify user has access to this email account
await emailAccountService.getEmailAccountById(accountId, userId);
const emails = await crmEmailService.getContactEmailsWithUnread(accountId, contactId);
res.status(200).json({
success: true,
@@ -409,7 +492,7 @@ export const searchEmailsJMAP = async (req, res) => {
const results = await searchEmailsJMAPService(
jmapConfig,
userId,
emailAccount.id,
query,
parseInt(limit),
parseInt(offset)

View File

@@ -0,0 +1,282 @@
import { db } from '../config/database.js';
import { timesheets, users } from '../db/schema.js';
import { eq, and, desc } from 'drizzle-orm';
import { formatErrorResponse, NotFoundError, BadRequestError, ForbiddenError } from '../utils/errors.js';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Upload timesheet
* POST /api/timesheets/upload
*/
export const uploadTimesheet = async (req, res) => {
const { year, month } = req.body;
const userId = req.userId;
const file = req.file;
let savedFilePath = null;
try {
if (!file) {
throw new BadRequestError('Súbor nebol nahraný');
}
// Validate file type
const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel'];
if (!allowedTypes.includes(file.mimetype)) {
throw new BadRequestError('Neplatný typ súboru. Povolené sú iba PDF a Excel súbory.');
}
// Determine file type
let fileType = 'pdf';
if (file.mimetype.includes('sheet') || file.mimetype.includes('excel')) {
fileType = 'xlsx';
}
// Create directory structure: uploads/timesheets/{userId}/{year}/{month}
const uploadsDir = path.join(process.cwd(), 'uploads', 'timesheets');
const userDir = path.join(uploadsDir, userId, year.toString(), month.toString());
await fs.mkdir(userDir, { recursive: true });
// Generate unique filename
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
const name = path.basename(file.originalname, ext);
const filename = `${name}-${uniqueSuffix}${ext}`;
savedFilePath = path.join(userDir, filename);
// Save file from memory buffer to disk
await fs.writeFile(savedFilePath, file.buffer);
// Create timesheet record
const [newTimesheet] = await db
.insert(timesheets)
.values({
userId,
fileName: file.originalname,
filePath: savedFilePath,
fileType,
fileSize: file.size,
year: parseInt(year),
month: parseInt(month),
})
.returning();
res.status(201).json({
success: true,
data: {
timesheet: {
id: newTimesheet.id,
fileName: newTimesheet.fileName,
fileType: newTimesheet.fileType,
fileSize: newTimesheet.fileSize,
year: newTimesheet.year,
month: newTimesheet.month,
uploadedAt: newTimesheet.uploadedAt,
},
},
message: 'Timesheet bol úspešne nahraný',
});
} catch (error) {
// If error occurs and file was saved, delete it
if (savedFilePath) {
try {
await fs.unlink(savedFilePath);
} catch (unlinkError) {
console.error('Failed to delete file:', unlinkError);
}
}
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Get user's timesheets (with optional filters)
* GET /api/timesheets/my
*/
export const getMyTimesheets = async (req, res) => {
const userId = req.userId;
const { year, month } = req.query;
try {
let conditions = [eq(timesheets.userId, userId)];
if (year) {
conditions.push(eq(timesheets.year, parseInt(year)));
}
if (month) {
conditions.push(eq(timesheets.month, parseInt(month)));
}
const userTimesheets = await db
.select({
id: timesheets.id,
fileName: timesheets.fileName,
fileType: timesheets.fileType,
fileSize: timesheets.fileSize,
year: timesheets.year,
month: timesheets.month,
uploadedAt: timesheets.uploadedAt,
})
.from(timesheets)
.where(and(...conditions))
.orderBy(desc(timesheets.uploadedAt));
res.status(200).json({
success: true,
data: {
timesheets: userTimesheets,
count: userTimesheets.length,
},
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Get all users' timesheets (admin only) - grouped by user
* GET /api/timesheets/all
*/
export const getAllTimesheets = async (req, res) => {
const { userId: filterUserId, year, month } = req.query;
try {
let conditions = [];
if (filterUserId) {
conditions.push(eq(timesheets.userId, filterUserId));
}
if (year) {
conditions.push(eq(timesheets.year, parseInt(year)));
}
if (month) {
conditions.push(eq(timesheets.month, parseInt(month)));
}
const allTimesheets = await db
.select({
id: timesheets.id,
fileName: timesheets.fileName,
fileType: timesheets.fileType,
fileSize: timesheets.fileSize,
year: timesheets.year,
month: timesheets.month,
uploadedAt: timesheets.uploadedAt,
userId: timesheets.userId,
username: users.username,
firstName: users.firstName,
lastName: users.lastName,
})
.from(timesheets)
.leftJoin(users, eq(timesheets.userId, users.id))
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(desc(timesheets.uploadedAt));
res.status(200).json({
success: true,
data: {
timesheets: allTimesheets,
count: allTimesheets.length,
},
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Download timesheet file
* GET /api/timesheets/:timesheetId/download
*/
export const downloadTimesheet = async (req, res) => {
const { timesheetId } = req.params;
const userId = req.userId;
const userRole = req.user.role; // Fix: use req.user.role instead of req.userRole
try {
const [timesheet] = await db
.select()
.from(timesheets)
.where(eq(timesheets.id, timesheetId))
.limit(1);
if (!timesheet) {
throw new NotFoundError('Timesheet nenájdený');
}
// Check permissions: user can only download their own timesheets, admin can download all
if (userRole !== 'admin' && timesheet.userId !== userId) {
throw new ForbiddenError('Nemáte oprávnenie stiahnuť tento timesheet');
}
// Check if file exists
try {
await fs.access(timesheet.filePath);
} catch {
throw new NotFoundError('Súbor nebol nájdený na serveri');
}
// Send file
res.download(timesheet.filePath, timesheet.fileName);
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Delete timesheet
* DELETE /api/timesheets/:timesheetId
*/
export const deleteTimesheet = async (req, res) => {
const { timesheetId } = req.params;
const userId = req.userId;
const userRole = req.user.role; // Fix: use req.user.role instead of req.userRole
try {
const [timesheet] = await db
.select()
.from(timesheets)
.where(eq(timesheets.id, timesheetId))
.limit(1);
if (!timesheet) {
throw new NotFoundError('Timesheet nenájdený');
}
// Check permissions: user can only delete their own timesheets, admin can delete all
if (userRole !== 'admin' && timesheet.userId !== userId) {
throw new ForbiddenError('Nemáte oprávnenie zmazať tento timesheet');
}
// Delete file from filesystem
try {
await fs.unlink(timesheet.filePath);
} catch (unlinkError) {
console.error('Failed to delete file from filesystem:', unlinkError);
// Continue with database deletion even if file deletion fails
}
// Delete from database
await db.delete(timesheets).where(eq(timesheets.id, timesheetId));
res.status(200).json({
success: true,
message: 'Timesheet bol zmazaný',
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};