Files
Atomcms-edit/app/Services/AlertService.php
T
2026-05-09 17:32:17 +02:00

363 lines
13 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace App\Services;
use App\Enums\AlertChannel;
use App\Enums\AlertSeverity;
use App\Enums\AlertType;
use App\Models\Miscellaneous\AlertLog;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Throwable;
class AlertService
{
private bool $emailEnabled;
private bool $discordEnabled;
private string $discordWebhookUrl;
private string $alertEmail;
private readonly string $siteName;
public function __construct()
{
$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', '');
$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');
}
}