const express = require('express'); const { validateURL, validateToken } = require('../input-validator'); const { paginate, parsePaginationParams } = require('../pagination'); const { ValidationError } = require('../errors'); /** * 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' } }, events: notificationConfig.events, healthCheck: notificationConfig.healthCheck }; res.json({ success: true, 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)'); } } } // 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 }; } } // 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(); res.json({ success: true, message: '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; default: throw new ValidationError('Unknown provider'); } res.json({ 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'); res.json({ 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); res.json({ success: true, history: result.data, total: notificationHistory.length, pagination: result.pagination }); } else { const limit = parseInt(req.query.limit) || 50; res.json({ success: true, history: notificationHistory.slice(0, limit), total: notificationHistory.length }); } }, 'notifications-history')); // DELETE /history — Clear notification history router.delete('/history', asyncHandler(async (req, res) => { notification.clearHistory(); res.json({ success: true, message: '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(); res.json({ success: true, lastCheck: notificationConfig.healthCheck.lastCheck, containersMonitored: Object.keys(notification.getHealthState()).length }); }, 'notifications-health-check')); return router; };