diff --git a/backend/src/routes/timesheets.js b/backend/src/routes/timesheets.js index 4483384..0f47e6b 100644 --- a/backend/src/routes/timesheets.js +++ b/backend/src/routes/timesheets.js @@ -272,49 +272,62 @@ router.get('/:id/pdf', async (req, res) => { } }); +// ─────────────── GET /api/timesheets/:id/email-status ─────────────── +router.get('/:id/email-status', async (req, res) => { + const { id } = req.params; + const timesheet = await req.prisma.timesheet.findUnique({ where: { id }, select: { userId: true } }); + if (!timesheet) return res.status(404).json({ error: 'Not found' }); + const isAdmin = req.user.role === 'admin' || req.user.role === 'super_admin'; + if (timesheet.userId !== req.user.id && !isAdmin) return res.status(403).json({ error: 'Access denied' }); + const limit = checkEmailLimit(id); + res.json({ count: limit.count, max: MAX_EMAILS, cooldownMs: limit.cooldownMs, allowed: limit.allowed }); +}); + // ─────────────── POST /api/timesheets/:id/email ─────────────── -router.post('/:id/email', validateBody(emailTimesheetSchema), async (req, res) => { +router.post('/:id/email', async (req, res) => { try { const { id } = req.params; - const { to, subject, message } = req.validated; const timesheet = await req.prisma.timesheet.findUnique({ where: { id }, include: { user: { select: { id: true, name: true, email: true } } }, }); - if (!timesheet) { - return res.status(404).json({ error: 'Timesheet not found' }); - } + if (!timesheet) return res.status(404).json({ error: 'Timesheet not found' }); - // Only owner or admin const isAdmin = req.user.role === 'admin' || req.user.role === 'super_admin'; if (timesheet.userId !== req.user.id && !isAdmin) { return res.status(403).json({ error: 'Access denied' }); } + // Rate limit check + const limit = checkEmailLimit(id); + if (!limit.allowed) { + const minutesLeft = Math.ceil(limit.cooldownMs / 60000); + if (limit.count >= MAX_EMAILS) { + return res.status(429).json({ error: `Maximum ${MAX_EMAILS} emails per timesheet reached.`, count: limit.count, max: MAX_EMAILS }); + } + return res.status(429).json({ error: `Please wait ${minutesLeft} minute${minutesLeft !== 1 ? 's' : ''} before sending again.`, cooldownMs: limit.cooldownMs, count: limit.count }); + } + const weekStart = timesheet.weekStart.toISOString().split('T')[0]; const data = await loadTimesheetData(req.prisma, timesheet.userId, weekStart); - - const pdfBuffer = await generateTimesheetPDF({ - userName: data.user.name, - weekStart, - entries: data.entries, - status: timesheet.status, - }); - + const pdfBuffer = await generateTimesheetPDF({ userName: data.user.name, weekStart, entries: data.entries, status: timesheet.status }); const pdfFilename = buildPdfFilename(data.user.name, weekStart); + const adminEmail = process.env.ADMIN_EMAIL || 'Office@CoastalContractingFL.com'; await sendTimesheetEmail({ - to, - subject: subject || `Timesheet – ${data.user.name} – Week of ${weekStart}`, - message, + to: adminEmail, + subject: `Timesheet – ${data.user.name} – Week of ${weekStart}`, + message: `${data.user.name} has submitted their timesheet for the week of ${weekStart}.`, pdfBuffer, pdfFilename, fromName: data.user.name, }); - res.json({ message: `Timesheet emailed to ${to}` }); + recordEmailSent(id); + const newCount = limit.count + 1; + res.json({ message: `Timesheet emailed to ${adminEmail}`, count: newCount, max: MAX_EMAILS, remaining: MAX_EMAILS - newCount }); } catch (err) { console.error('Email timesheet error:', err); if (err.message && err.message.includes('SMTP')) {