/** * Alert Service * * Zentraler Service für System-Benachrichtigungen. * Unterstützt verschiedene Kanäle: E-Mail, Slack, Discord, etc. */ import type { Payload } from 'payload' export type AlertChannel = 'email' | 'slack' | 'discord' | 'console' export type AlertLevel = 'info' | 'warning' | 'error' | 'critical' export interface AlertConfig { channels: AlertChannel[] recipients?: string[] // E-Mail-Adressen slackWebhook?: string discordWebhook?: string minLevel: AlertLevel } interface AlertInput { level: AlertLevel title: string message: string details?: Record tenantId?: number tenantName?: string } // Alert-Konfiguration aus Environment const alertConfig: AlertConfig = { channels: (process.env.ALERT_CHANNELS?.split(',') as AlertChannel[]) || ['console'], recipients: process.env.ALERT_EMAIL_RECIPIENTS?.split(','), slackWebhook: process.env.ALERT_SLACK_WEBHOOK, discordWebhook: process.env.ALERT_DISCORD_WEBHOOK, minLevel: (process.env.ALERT_MIN_LEVEL as AlertLevel) || 'warning', } // Level-Hierarchie für Filterung const levelPriority: Record = { info: 0, warning: 1, error: 2, critical: 3, } /** * Prüft ob ein Alert basierend auf dem Level gesendet werden soll */ function shouldSendAlert(level: AlertLevel): boolean { return levelPriority[level] >= levelPriority[alertConfig.minLevel] } /** * Formatiert die Alert-Nachricht für verschiedene Kanäle */ function formatAlertMessage(alert: AlertInput, format: 'text' | 'html' | 'markdown'): string { const timestamp = new Date().toLocaleString('de-DE') const levelEmoji: Record = { info: 'ℹ️', warning: '⚠️', error: '❌', critical: '🚨', } if (format === 'html') { return `

${levelEmoji[alert.level]} ${alert.title}

${timestamp}

${alert.tenantName ? `

Tenant: ${alert.tenantName}

` : ''}

${alert.message}

${ alert.details ? `
Details
${JSON.stringify(alert.details, null, 2)}
` : '' }
` } if (format === 'markdown') { let md = `${levelEmoji[alert.level]} **${alert.title}**\n\n` md += `*${timestamp}*\n\n` if (alert.tenantName) md += `**Tenant:** ${alert.tenantName}\n\n` md += `${alert.message}\n` if (alert.details) { md += `\n\`\`\`json\n${JSON.stringify(alert.details, null, 2)}\n\`\`\`` } return md } // Plain text let text = `${levelEmoji[alert.level]} ${alert.title}\n` text += `${timestamp}\n\n` if (alert.tenantName) text += `Tenant: ${alert.tenantName}\n` text += `${alert.message}\n` if (alert.details) { text += `\nDetails: ${JSON.stringify(alert.details)}` } return text } /** * Sendet Alert via E-Mail */ async function sendEmailAlert(payload: Payload, alert: AlertInput): Promise { if (!alertConfig.recipients?.length) { console.warn('[AlertService] No email recipients configured') return } const levelLabels: Record = { info: 'Info', warning: 'Warnung', error: 'Fehler', critical: 'KRITISCH', } try { await payload.sendEmail({ to: alertConfig.recipients.join(','), subject: `[${levelLabels[alert.level]}] ${alert.title}`, html: formatAlertMessage(alert, 'html'), text: formatAlertMessage(alert, 'text'), }) console.log(`[AlertService] Email alert sent to ${alertConfig.recipients.join(', ')}`) } catch (error) { console.error('[AlertService] Failed to send email alert:', error) } } /** * Sendet Alert via Slack Webhook */ async function sendSlackAlert(alert: AlertInput): Promise { if (!alertConfig.slackWebhook) { console.warn('[AlertService] No Slack webhook configured') return } const colors: Record = { info: '#3b82f6', warning: '#eab308', error: '#f97316', critical: '#dc2626', } try { const response = await fetch(alertConfig.slackWebhook, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ attachments: [ { color: colors[alert.level], title: alert.title, text: alert.message, fields: [ ...(alert.tenantName ? [ { title: 'Tenant', value: alert.tenantName, short: true, }, ] : []), { title: 'Level', value: alert.level.toUpperCase(), short: true, }, ], footer: 'Payload CMS Alert', ts: Math.floor(Date.now() / 1000), }, ], }), }) if (!response.ok) { throw new Error(`Slack responded with ${response.status}`) } console.log('[AlertService] Slack alert sent') } catch (error) { console.error('[AlertService] Failed to send Slack alert:', error) } } /** * Sendet Alert via Discord Webhook */ async function sendDiscordAlert(alert: AlertInput): Promise { if (!alertConfig.discordWebhook) { console.warn('[AlertService] No Discord webhook configured') return } const colors: Record = { info: 0x3b82f6, warning: 0xeab308, error: 0xf97316, critical: 0xdc2626, } try { const response = await fetch(alertConfig.discordWebhook, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ embeds: [ { title: alert.title, description: alert.message, color: colors[alert.level], fields: [ ...(alert.tenantName ? [ { name: 'Tenant', value: alert.tenantName, inline: true, }, ] : []), { name: 'Level', value: alert.level.toUpperCase(), inline: true, }, ], footer: { text: 'Payload CMS Alert', }, timestamp: new Date().toISOString(), }, ], }), }) if (!response.ok) { throw new Error(`Discord responded with ${response.status}`) } console.log('[AlertService] Discord alert sent') } catch (error) { console.error('[AlertService] Failed to send Discord alert:', error) } } /** * Sendet Alert an alle konfigurierten Kanäle */ export async function sendAlert(payload: Payload, alert: AlertInput): Promise { if (!shouldSendAlert(alert.level)) { return } const promises: Promise[] = [] for (const channel of alertConfig.channels) { switch (channel) { case 'email': promises.push(sendEmailAlert(payload, alert)) break case 'slack': promises.push(sendSlackAlert(alert)) break case 'discord': promises.push(sendDiscordAlert(alert)) break case 'console': console.log(`[Alert:${alert.level.toUpperCase()}]`, formatAlertMessage(alert, 'text')) break } } await Promise.allSettled(promises) } /** * Email-Fehler Alert */ export async function alertEmailFailed( payload: Payload, tenantId: number, tenantName: string, to: string, subject: string, error: string, failedCount: number, ): Promise { await sendAlert(payload, { level: failedCount >= 5 ? 'critical' : 'error', title: `E-Mail-Versand fehlgeschlagen${failedCount >= 5 ? ' (wiederholt)' : ''}`, message: `E-Mail an "${to}" mit Betreff "${subject}" konnte nicht gesendet werden.`, tenantId, tenantName, details: { to, subject, error: error.substring(0, 200), // Gekürzt um Secrets zu vermeiden failedCount, recommendation: failedCount >= 5 ? 'SMTP-Konfiguration prüfen! Mehrere Fehler in Folge.' : 'Einzelner Fehler. Weiter beobachten.', }, }) } /** * Rate-Limit Alert */ export async function alertRateLimitReached( payload: Payload, endpoint: string, userId?: number, userEmail?: string, ): Promise { await sendAlert(payload, { level: 'warning', title: 'Rate-Limit erreicht', message: `Rate-Limit für Endpoint "${endpoint}" wurde erreicht.`, details: { endpoint, userId, userEmail, }, }) }