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

@@ -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)