Files
dashcaddy/dashcaddy-api/routes/notifications.js
Krystie b172a21b63 Migrate 25 route files to throw-based error handling
Converted routes:
- All auth routes (totp.js, keys.js, sso-gate.js)
- Recipe deployment routes (deploy.js, manage.js, index.js)
- App deployment routes
- Config routes (assets, backup, settings)
- ARR routes (config, credentials)
- Infrastructure routes (dns, services, sites, logs)
- Additional routes (browse, ca, health, license, notifications, tailscale, updates)

Changes:
- Replaced ctx.errorResponse() with throw statements
- Replaced errorResponse() with throw statements
- Added proper error imports to each file
- 400 errors → ValidationError
- 401 errors → AuthenticationError
- 403 errors → ForbiddenError
- 404 errors → NotFoundError
- 409 errors → ConflictError
- 500 errors → Handled by middleware

Result: 25 files migrated, ~150 error responses standardized
2026-03-29 18:53:03 -07:00

187 lines
7.0 KiB
JavaScript

const express = require('express');
const { validateURL, validateToken } = require('../input-validator');
const { paginate, parsePaginationParams } = require('../pagination');
const { ValidationError } = require('../errors');
module.exports = function(ctx) {
const router = express.Router();
// GET /config — Get notification configuration (sensitive data redacted)
router.get('/config', ctx.asyncHandler(async (req, res) => {
const notificationConfig = ctx.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', ctx.asyncHandler(async (req, res) => {
const { enabled, providers, events, healthCheck } = req.body;
const notificationConfig = ctx.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) {
ctx.notification.startHealthDaemon();
} else {
ctx.notification.stopHealthDaemon();
}
}
}
await ctx.notification.saveConfig();
res.json({ success: true, message: 'Notification config updated' });
}, 'notifications-config-update'));
// POST /test — Test notification delivery
router.post('/test', ctx.asyncHandler(async (req, res) => {
const { provider } = req.body;
if (provider) {
// Test specific provider
let result;
switch (provider) {
case 'discord':
result = await ctx.notification.sendDiscord('Test Notification', 'This is a test notification from DashCaddy.', 'info');
break;
case 'telegram':
result = await ctx.notification.sendTelegram('Test Notification', 'This is a test notification from DashCaddy.', 'info');
break;
case 'ntfy':
result = await ctx.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 ctx.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', ctx.asyncHandler(async (req, res) => {
const notificationHistory = ctx.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', ctx.asyncHandler(async (req, res) => {
ctx.notification.clearHistory();
res.json({ success: true, message: 'Notification history cleared' });
}, 'notifications-history-clear'));
// POST /health-check — Manually trigger health check
router.post('/health-check', ctx.asyncHandler(async (req, res) => {
await ctx.notification.checkHealth();
const notificationConfig = ctx.notification.getConfig();
res.json({
success: true,
lastCheck: notificationConfig.healthCheck.lastCheck,
containersMonitored: Object.keys(ctx.notification.getHealthState()).length
});
}, 'notifications-health-check'));
return router;
};