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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user