Add SSL monitoring (Svc, model, cron, UI)

Introduce SSL certificate monitoring: add SslService for fetching/parsing certs and parsing monitor targets, SslCertificate model for storing snapshots and managing monitored targets, and cron/check_ssl.php for scheduled checks. Extend DomainController with many SSL endpoints and helpers (add/refresh/bulk refresh/delete/bulk delete, snapshot handling, formatting, stats, safety checks) and surface SSL data in domain views. Add NotificationService helpers to create/send SSL alerts, update Installer to include new migration, add migration 028 to create ssl_certificates table, bump app version default to 1.1.5, update changelog, and modify routes and templates to include SSL tab and related UI. Logs and basic validation/error handling are included to surface SSL issues and protect default root-target behavior.
This commit is contained in:
Hosteroid
2026-03-08 21:12:09 +02:00
parent 8559e903b9
commit 5916daa293
17 changed files with 2460 additions and 349 deletions

View File

@@ -713,6 +713,151 @@ class NotificationService
return $results;
}
/**
* Create an SSL monitoring notification (in-app / bell icon).
*/
public function notifySslStatusChange(
int $userId,
string $domainName,
string $hostname,
int $domainId,
string $newStatus,
?string $oldStatus = null
): void {
$notificationModel = new \App\Models\Notification();
$notificationModel->createNotification(
$userId,
'ssl_status_change',
$this->getSslNotificationTitle($newStatus),
$this->formatSslStatusSummary($domainName, $hostname, $newStatus, $oldStatus),
$domainId
);
}
/**
* Send SSL status alert to external channels.
*/
public function sendSslStatusAlert(
array $domain,
array $notificationChannels,
string $hostname,
string $newStatus,
?string $oldStatus = null,
?string $validTo = null,
?string $error = null
): array {
$message = $this->formatSslStatusMessage($domain, $hostname, $newStatus, $oldStatus, $validTo, $error);
$results = [];
foreach ($notificationChannels as $channel) {
$config = json_decode($channel['channel_config'], true);
$success = $this->send(
$channel['channel_type'],
$config,
$message,
[
'subject' => $this->getSslNotificationTitle($newStatus) . ': ' . $hostname,
'domain' => $domain['domain_name'],
'domain_id' => $domain['id'],
'hostname' => $hostname,
'new_status' => $newStatus,
'old_status' => $oldStatus,
'valid_to' => $validTo,
'error' => $error,
]
);
$results[] = [
'channel' => $channel['channel_type'],
'success' => $success,
];
}
return $results;
}
/**
* Get SSL status label for human-readable messages.
*/
public static function getSslStatusLabel(string $status): string
{
return match ($status) {
'valid' => 'Valid',
'expiring' => 'Expiring Soon',
'expired' => 'Expired',
'invalid' => 'Invalid',
default => ucfirst(str_replace('_', ' ', $status)),
};
}
private function getSslNotificationTitle(string $status): string
{
return match ($status) {
'valid' => 'SSL Certificate Recovered',
'expiring' => 'SSL Certificate Expiring Soon',
'expired' => 'SSL Certificate Expired',
'invalid' => 'SSL Certificate Check Failed',
default => 'SSL Status Changed',
};
}
private function formatSslStatusSummary(
string $domainName,
string $hostname,
string $newStatus,
?string $oldStatus = null
): string {
$hostText = $hostname === $domainName ? $domainName : "{$hostname} ({$domainName})";
$newLabel = self::getSslStatusLabel($newStatus);
if ($oldStatus !== null) {
$oldLabel = self::getSslStatusLabel($oldStatus);
return "{$hostText} - SSL status changed from {$oldLabel} to {$newLabel}";
}
return "{$hostText} - SSL status is {$newLabel}";
}
private function formatSslStatusMessage(
array $domain,
string $hostname,
string $newStatus,
?string $oldStatus = null,
?string $validTo = null,
?string $error = null
): string {
$domainName = $domain['domain_name'];
$validToText = $validTo ? date('F j, Y H:i', strtotime($validTo)) : 'Unknown';
$oldLabel = $oldStatus !== null ? self::getSslStatusLabel($oldStatus) : null;
return match ($newStatus) {
'valid' => "✅ SSL RECOVERED: {$hostname} is now using a valid certificate.\n\n" .
($oldLabel ? "Previous status: {$oldLabel}\n" : '') .
"Domain: {$domainName}\n" .
"Valid until: {$validToText}",
'expiring' => "⚠️ SSL EXPIRING SOON: {$hostname} is approaching certificate expiration.\n\n" .
($oldLabel ? "Previous status: {$oldLabel}\n" : '') .
"Domain: {$domainName}\n" .
"Valid until: {$validToText}",
'expired' => "🚨 SSL EXPIRED: {$hostname} has an expired certificate.\n\n" .
($oldLabel ? "Previous status: {$oldLabel}\n" : '') .
"Domain: {$domainName}\n" .
"Expired on: {$validToText}",
'invalid' => "❌ SSL INVALID: {$hostname} failed certificate validation.\n\n" .
($oldLabel ? "Previous status: {$oldLabel}\n" : '') .
"Domain: {$domainName}\n" .
($error ? "Error: {$error}\n" : ''),
default => " SSL STATUS CHANGE: {$hostname} changed SSL status.\n\n" .
($oldLabel ? "Previous status: {$oldLabel}\n" : '') .
"Current status: " . self::getSslStatusLabel($newStatus) . "\n" .
"Domain: {$domainName}"
};
}
/**
* Delete old read notifications (cleanup)
*/