const express = require('express'); const { validateURL, validateToken } = require('../input-validator'); const validatorLib = require('validator'); const { paginate, parsePaginationParams } = require('../pagination'); const { ValidationError } = require('../errors'); const { ok, successMessage } = require('../src/utils/responses'); /** * Notifications route factory * @param {Object} deps - Explicit dependencies * @param {Object} deps.notification - Notification manager * @param {Function} deps.asyncHandler - Async route handler wrapper * @returns {express.Router} */ module.exports = function({ notification, asyncHandler }) { const router = express.Router(); // GET /config — Get notification configuration (sensitive data redacted) router.get('/config', asyncHandler(async (req, res) => { const notificationConfig = notification.getConfig(); // Return config without sensitive data const safeConfig = { enabled: notificationConfig.enabled, providers: { discord: { enabled: notificationConfig.providers.discord?.enabled || false, configured: !!notificationConfig.providers.discord?.webhookUrl }, telegram: { enabled: notificationConfig.providers.telegram?.enabled || false, configured: !!(notificationConfig.providers.telegram?.botToken && notificationConfig.providers.telegram?.chatId) }, ntfy: { enabled: notificationConfig.providers.ntfy?.enabled || false, configured: !!notificationConfig.providers.ntfy?.topic, serverUrl: notificationConfig.providers.ntfy?.serverUrl || 'https://ntfy.sh' }, email: { enabled: notificationConfig.providers.email?.enabled || false, configured: !!(notificationConfig.providers.email?.host && notificationConfig.providers.email?.to), host: notificationConfig.providers.email?.host || '', from: notificationConfig.providers.email?.from || '' } }, events: notificationConfig.events, healthCheck: notificationConfig.healthCheck }; ok(res, { config: safeConfig }); }, 'notifications-config-get')); // POST /config — Update notification configuration router.post('/config', asyncHandler(async (req, res) => { const { enabled, providers, events, healthCheck } = req.body; const notificationConfig = notification.getConfig(); // Validate provider webhook URLs and tokens if (providers) { if (providers.discord?.webhookUrl) { try { validateURL(providers.discord.webhookUrl); } catch (validationErr) { throw new ValidationError('Invalid Discord webhook URL'); } } if (providers.telegram?.botToken) { try { validateToken(providers.telegram.botToken); } catch (validationErr) { throw new ValidationError('Invalid Telegram bot token format'); } } if (providers.ntfy?.serverUrl) { try { validateURL(providers.ntfy.serverUrl); } catch (validationErr) { throw new ValidationError('Invalid ntfy server URL'); } } if (providers.ntfy?.topic) { const topicRegex = /^[a-zA-Z0-9_-]{1,64}$/; if (!topicRegex.test(providers.ntfy.topic)) { throw new ValidationError('Invalid ntfy topic (alphanumeric, hyphens, underscores only, max 64 chars)'); } } if (providers.email?.to) { const emails = providers.email.to.split(',').map(e => e.trim()); for (const email of emails) { if (!validatorLib.isEmail(email)) { throw new ValidationError(`Invalid email address: ${email}`); } } } if (providers.email?.host && typeof providers.email.host === 'string') { if (!validatorLib.isFQDN(providers.email.host) && !validatorLib.isIP(providers.email.host)) { throw new ValidationError('Invalid SMTP host'); } } } // Update enabled state if (typeof enabled === 'boolean') { notificationConfig.enabled = enabled; } // Update providers (only update provided fields) if (providers) { if (providers.discord) { notificationConfig.providers.discord = { ...notificationConfig.providers.discord, ...providers.discord }; } if (providers.telegram) { notificationConfig.providers.telegram = { ...notificationConfig.providers.telegram, ...providers.telegram }; } if (providers.ntfy) { notificationConfig.providers.ntfy = { ...notificationConfig.providers.ntfy, ...providers.ntfy }; } if (providers.email) { notificationConfig.providers.email = { ...notificationConfig.providers.email, ...providers.email }; } } // Update events if (events) { notificationConfig.events = { ...notificationConfig.events, ...events }; } // Update health check settings if (healthCheck) { const wasEnabled = notificationConfig.healthCheck?.enabled; notificationConfig.healthCheck = { ...notificationConfig.healthCheck, ...healthCheck }; // Restart daemon if settings changed if (healthCheck.enabled !== wasEnabled || healthCheck.intervalMinutes) { if (notificationConfig.healthCheck.enabled) { notification.startHealthDaemon(); } else { notification.stopHealthDaemon(); } } } await notification.saveConfig(); successMessage(res, 'Notification config updated'); }, 'notifications-config-update')); // POST /test — Test notification delivery router.post('/test', asyncHandler(async (req, res) => { const { provider } = req.body; if (provider) { // Test specific provider let result; switch (provider) { case 'discord': result = await notification.sendDiscord('Test Notification', 'This is a test notification from DashCaddy.', 'info'); break; case 'telegram': result = await notification.sendTelegram('Test Notification', 'This is a test notification from DashCaddy.', 'info'); break; case 'ntfy': result = await notification.sendNtfy('Test Notification', 'This is a test notification from DashCaddy.', 'info'); break; case 'email': result = await notification.sendEmail('Test Notification', 'This is a test notification from DashCaddy.', 'info'); break; default: throw new ValidationError('Unknown provider'); } ok(res, { success: result.success, provider, error: result.error }); } else { // Test all enabled providers const result = await notification.send('test', 'Test Notification', 'This is a test notification from DashCaddy.', 'info'); ok(res, { success: true, ...result }); } }, 'notifications-test')); // GET /history — Get notification history router.get('/history', asyncHandler(async (req, res) => { const notificationHistory = notification.getHistory(); const paginationParams = parsePaginationParams(req.query); if (paginationParams) { const result = paginate(notificationHistory, paginationParams); ok(res, { history: result.data, total: notificationHistory.length, pagination: result.pagination }); } else { const limit = parseInt(req.query.limit) || 50; ok(res, { history: notificationHistory.slice(0, limit), total: notificationHistory.length }); } }, 'notifications-history')); // DELETE /history — Clear notification history router.delete('/history', asyncHandler(async (req, res) => { notification.clearHistory(); successMessage(res, 'Notification history cleared'); }, 'notifications-history-clear')); // POST /health-check — Manually trigger health check router.post('/health-check', asyncHandler(async (req, res) => { await notification.checkHealth(); const notificationConfig = notification.getConfig(); ok(res, { lastCheck: notificationConfig.healthCheck.lastCheck, containersMonitored: Object.keys(notification.getHealthState()).length }); }, 'notifications-health-check')); // GET /status — Get notification system status router.get('/status', asyncHandler(async (req, res) => { const notificationConfig = notification.getConfig(); const providers = notificationConfig.providers || {}; ok(res, { enabled: notificationConfig.enabled, providers: { discord: providers.discord?.enabled && !!providers.discord?.webhookUrl, telegram: providers.telegram?.enabled && !!providers.telegram?.botToken && !!providers.telegram?.chatId, ntfy: providers.ntfy?.enabled && !!providers.ntfy?.topic, email: providers.email?.enabled && !!providers.email?.host && !!providers.email?.to }, lastSent: notification.lastSent, healthCheck: notificationConfig.healthCheck?.enabled ? { enabled: true, lastCheck: notificationConfig.healthCheck.lastCheck, intervalMinutes: notificationConfig.healthCheck.intervalMinutes } : { enabled: false } }); }, 'notifications-status')); // POST /send — Manual test send (used by frontend "Send Test" button) router.post('/send', asyncHandler(async (req, res) => { const { event, data, type } = req.body; if (!event) { throw new ValidationError('Event type is required'); } // Use 'test' as the event for manual sends const result = await notification.send(event, data || {}, type || 'info'); ok(res, { success: result.success, event, results: result.results }); }, 'notifications-send')); return router; };