emailEnabled = (bool) setting('alert_email_enabled', false); $this->discordEnabled = (bool) setting('alert_discord_enabled', false); $this->discordWebhookUrl = setting('alert_discord_webhook_url', ''); $this->alertEmail = setting('alert_email_address', ''); $this->siteName = setting('hotel_name', config('app.name', 'Atom CMS')); } public function send( AlertType $type, string $message, array $context = [], AlertChannel $channel = AlertChannel::BOTH, ): void { $this->logAlert($type, $message, $context); $channels = $this->resolveChannels($channel); foreach ($channels as $ch) { match ($ch) { AlertChannel::EMAIL => $this->sendEmail($type, $message, $context), AlertChannel::DISCORD => $this->sendDiscord($type, $message, $context), default => null, }; } } public function sendCritical(AlertType $type, string $message, array $context = []): void { $this->send($type, $message, $context, AlertChannel::BOTH); } public function sendEmulatorOffline(string $error = ''): void { $this->send( AlertType::EMULATOR_OFFLINE, 'De emulator is offline of niet bereikbaar via RCON.', ['error' => $error], ); } public function sendEmulatorOnline(): void { $this->send( AlertType::EMULATOR_ONLINE, 'De emulator is weer online en verbonden via RCON.', [], AlertChannel::BOTH, ); } public function sendDDoSDetected(array $details): void { $message = sprintf( 'Mogelijke DDoS aanval gedetecteerd! %d requests in %d seconden van %d unieke IPs.', $details['total_requests'] ?? 0, $details['time_window'] ?? 0, $details['unique_ips'] ?? 0, ); $this->send(AlertType::DDOS_DETECTED, $message, $details); } public function sendCriticalError(string $error, ?Throwable $exception = null): void { $context = [ 'error' => $error, 'exception_class' => $exception instanceof Throwable ? $exception::class : null, 'file' => $exception?->getFile(), 'line' => $exception?->getLine(), 'trace' => $exception instanceof Throwable ? $exception->getTraceAsString() : null, ]; $this->send(AlertType::CRITICAL_ERROR, $error, $context); } public function sendQueueFailed(string $job, array $details = []): void { $this->send( AlertType::QUEUE_FAILED, "Queue job gefaald: {$job}", array_merge(['job' => $job], $details), ); } public function sendHighErrorRate(int $errorCount, int $threshold, int $timeWindow): void { $this->send( AlertType::HIGH_ERROR_RATE, "Hoge error rate gedetecteerd: {$errorCount} errors in {$timeWindow} minuten (drempel: {$threshold})", ['error_count' => $errorCount, 'threshold' => $threshold, 'time_window' => $timeWindow], ); } public function sendEmulatorUpdate(string $version, string $details = ''): void { $this->send( AlertType::EMULATOR_UPDATE, "Emulator succesvol geüpdatet naar versie {$version}", ['version' => $version, 'details' => $details], AlertChannel::DISCORD, ); } public function sendSqlUpdate(int $count, string $details = ''): void { $this->send( AlertType::SQL_UPDATE, "{$count} SQL update(s) succesvol toegepast", ['count' => $count, 'details' => $details], AlertChannel::DISCORD, ); } private function resolveChannels(AlertChannel $preferred): array { $channels = []; if (($preferred === AlertChannel::BOTH || $preferred === AlertChannel::EMAIL) && ($this->emailEnabled && ($this->alertEmail !== '' && $this->alertEmail !== '0'))) { $channels[] = AlertChannel::EMAIL; } if (($preferred === AlertChannel::BOTH || $preferred === AlertChannel::DISCORD) && ($this->discordEnabled && ($this->discordWebhookUrl !== '' && $this->discordWebhookUrl !== '0'))) { $channels[] = AlertChannel::DISCORD; } return $channels; } private function sendEmail(AlertType $type, string $message, array $context): void { if (! $this->emailEnabled || ($this->alertEmail === '' || $this->alertEmail === '0')) { Log::warning('Email alert skipped: not enabled or no email configured', [ 'emailEnabled' => $this->emailEnabled, 'alertEmail' => $this->alertEmail, ]); return; } try { $severity = $type->getSeverity(); Mail::raw($this->buildEmailBody($type, $message, $context), function ($mail) use ($type, $severity) { $mail->from(config('mail.from.address', 'noreply@' . request()->getHost()), $this->siteName) ->to($this->alertEmail) ->subject(sprintf( '[%s] %s - %s', config('app.env', 'Production'), strtoupper($severity->value), $type->getLabel(), )); }); Log::info('Alert email sent successfully', [ 'to' => $this->alertEmail, 'type' => $type->value, ]); } catch (\Exception $e) { Log::error('Failed to send alert email: ' . $e->getMessage(), [ 'to' => $this->alertEmail, 'exception' => $e, ]); } } private function buildEmailBody(AlertType $type, string $message, array $context): string { $severity = $type->getSeverity(); $timestamp = now()->format('Y-m-d H:i:s T'); $body = "{$severity->getEmoji()} ALERT: {$type->getLabel()}\n"; $body .= "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"; $body .= "Bericht: {$message}\n"; $body .= "Tijd: {$timestamp}\n"; $body .= "Server: {$this->siteName}\n"; $body .= 'Environment: ' . config('app.env', 'Production') . "\n"; $body .= 'Severity: ' . ucfirst($severity->value) . "\n\n"; if ($context !== []) { $body .= "Details:\n"; foreach ($context as $key => $value) { if ($key === 'trace' && is_string($value)) { $body .= " - {$key}: [Zie logs voor volledige trace]\n"; } else { $body .= " - {$key}: " . (is_array($value) ? json_encode($value) : (string) $value) . "\n"; } } } $body .= "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; return $body . "Dit is een automatisch gegenereerd bericht van {$this->siteName}.\n"; } private function sendDiscord(AlertType $type, string $message, array $context): void { if (! $this->discordEnabled || ($this->discordWebhookUrl === '' || $this->discordWebhookUrl === '0')) { return; } try { $severity = $type->getSeverity(); $color = $this->getDiscordColor($severity); $embed = [ 'title' => "{$severity->getEmoji()} {$type->getLabel()}", 'description' => $message, 'color' => $color, 'fields' => [ [ 'name' => 'Server', 'value' => $this->siteName, 'inline' => true, ], [ 'name' => 'Environment', 'value' => config('app.env', 'Production'), 'inline' => true, ], [ 'name' => 'Tijd', 'value' => now()->format('Y-m-d H:i:s T'), 'inline' => true, ], ], 'footer' => [ 'text' => 'Atom CMS Alert System', ], 'timestamp' => now()->toIso8601String(), ]; if ($context !== []) { $details = []; foreach (array_slice($context, 0, 5, true) as $key => $value) { if (in_array($key, ['trace', 'stack_trace'])) { continue; } $details[] = [ 'name' => ucfirst(str_replace('_', ' ', $key)), 'value' => mb_substr((string) (is_array($value) ? json_encode($value) : $value), 0, 1024), 'inline' => false, ]; } $embed['fields'] = array_merge($embed['fields'], $details); } Http::post($this->discordWebhookUrl, [ 'username' => 'Atom CMS Alerts', 'embeds' => [$embed], ]); } catch (\Exception $e) { Log::error('Failed to send Discord alert: ' . $e->getMessage()); } } private function getDiscordColor(AlertSeverity $severity): int { return match ($severity) { AlertSeverity::INFO => 3447003, AlertSeverity::WARNING => 16776960, AlertSeverity::ERROR => 15158332, AlertSeverity::CRITICAL => 10038562, }; } private function logAlert(AlertType $type, string $message, array $context = []): void { try { AlertLog::create([ 'type' => $type->value, 'severity' => $type->getSeverity()->value, 'message' => $message, 'context' => $context, 'sent_via_email' => $this->emailEnabled && ($this->alertEmail !== '' && $this->alertEmail !== '0'), 'sent_via_discord' => $this->discordEnabled && ($this->discordWebhookUrl !== '' && $this->discordWebhookUrl !== '0'), ]); } catch (\Exception $e) { Log::error('Failed to log alert: ' . $e->getMessage()); } } public function testAlert(): array { Cache::forget('website_settings'); $this->refreshSettings(); $results = []; if ($this->emailEnabled && ($this->alertEmail !== '' && $this->alertEmail !== '0')) { try { $this->send( AlertType::EMULATOR_ONLINE, 'Dit is een testmelding van het Atom CMS Alert Systeem.', ['test' => true], AlertChannel::EMAIL, ); $results['email'] = 'success'; } catch (\Exception $e) { $results['email'] = 'failed: ' . $e->getMessage(); } } if ($this->discordEnabled && ($this->discordWebhookUrl !== '' && $this->discordWebhookUrl !== '0')) { try { $this->send( AlertType::EMULATOR_ONLINE, 'Dit is een testmelding van het Atom CMS Alert Systeem.', ['test' => true], AlertChannel::DISCORD, ); $results['discord'] = 'success'; } catch (\Exception $e) { $results['discord'] = 'failed: ' . $e->getMessage(); } } return $results; } private function refreshSettings(): void { $this->emailEnabled = (bool) setting('alert_email_enabled', false); $this->discordEnabled = (bool) setting('alert_discord_enabled', false); $this->discordWebhookUrl = setting('alert_discord_webhook_url', ''); $this->alertEmail = setting('alert_email_address', ''); } public static function clearCache(): void { Cache::forget('alert_settings'); } }