You've already forked Atomcms-edit
363 lines
13 KiB
PHP
Executable File
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');
|
|
}
|
|
}
|