diff --git a/application/src/services/ssl/notification/index.ts b/application/src/services/ssl/notification/index.ts new file mode 100644 index 0000000..14deb11 --- /dev/null +++ b/application/src/services/ssl/notification/index.ts @@ -0,0 +1,10 @@ + +// Export all SSL notification related functionality +export { + checkAllCertificatesAndNotify, + checkCertificateAndNotify, + testCertificateNotification + } from './sslCheckNotifier'; + + export { sendSSLNotification } from './sslNotificationSender'; + export { shouldRunDailyCheck } from './sslScheduleUtils'; \ No newline at end of file diff --git a/application/src/services/ssl/notification/sslCheckNotifier.ts b/application/src/services/ssl/notification/sslCheckNotifier.ts new file mode 100644 index 0000000..1b5cab9 --- /dev/null +++ b/application/src/services/ssl/notification/sslCheckNotifier.ts @@ -0,0 +1,168 @@ + +import { pb } from "@/lib/pocketbase"; +import { SSLCertificate } from "../types"; +import { checkSSLCertificate } from "../sslCheckerService"; +import { determineSSLStatus } from "../sslStatusUtils"; +import { sendSSLNotification } from "./sslNotificationSender"; +import { toast } from "sonner"; + +/** + * Checks all SSL certificates and sends notifications for expiring ones + * This should be called once per day + */ +export async function checkAllCertificatesAndNotify(): Promise { + console.log("Starting daily SSL certificates check..."); + + try { + // Fetch all SSL certificates from database + const response = await pb.collection('ssl_certificates').getList(1, 100, {}); + // Properly cast the items as SSLCertificate + const certificates = response.items as unknown as SSLCertificate[]; + + console.log(`Found ${certificates.length} certificates to check`); + + // Check each certificate + for (const cert of certificates) { + await checkCertificateAndNotify(cert); + } + + console.log("Daily SSL certificates check completed"); + } catch (error) { + console.error("Error during SSL certificates daily check:", error); + } +} + +/** + * Checks a specific SSL certificate and sends notification if needed + * This respects the Warning and Expiry Thresholds set on the certificate + */ +export async function checkCertificateAndNotify(certificate: SSLCertificate): Promise { + console.log(`Checking certificate for ${certificate.domain}...`); + + try { + // Get fresh SSL data + const sslData = await checkSSLCertificate(certificate.domain); + if (!sslData || !sslData.result) { + console.error(`Failed to check SSL for ${certificate.domain}`); + return false; + } + + // Extract days left from the check result + const daysLeft = sslData.result.days_left || 0; + + // Get threshold values (ensure they are numbers) + const warningThreshold = Number(certificate.warning_threshold) || 30; + const expiryThreshold = Number(certificate.expiry_threshold) || 7; + + console.log(`Certificate ${certificate.domain} thresholds: warning=${warningThreshold}, expiry=${expiryThreshold}, days left=${daysLeft}`); + + // Update status based on thresholds + const status = determineSSLStatus(daysLeft, warningThreshold, expiryThreshold); + + // Check if we should send a notification based on thresholds + let shouldNotify = false; + let isCritical = false; + + // Critical notifications - when days left is less than or equal to expiry threshold + if (daysLeft <= expiryThreshold) { + shouldNotify = true; + isCritical = true; + } + // Warning notifications - when days left is less than or equal to warning threshold but greater than expiry threshold + else if (daysLeft <= warningThreshold) { + shouldNotify = true; + isCritical = false; + } + + console.log(`${certificate.domain}: ${daysLeft} days left, status: ${status}, should notify: ${shouldNotify}, critical: ${isCritical}`); + + // Update certificate data in database + const updateData: Partial = { + days_left: daysLeft, + status: status, + // Other fields from the SSL check that should be updated + issuer_o: sslData.result.issuer_o || certificate.issuer_o, + valid_from: sslData.result.valid_from || certificate.valid_from, + valid_till: sslData.result.valid_till || certificate.valid_till, + validity_days: sslData.result.validity_days || certificate.validity_days, + cert_sans: sslData.result.cert_sans, + cert_alg: sslData.result.cert_alg, + serial_number: sslData.result.cert_sn + }; + + await pb.collection('ssl_certificates').update(certificate.id, updateData); + + // Send notification if needed + if (shouldNotify && certificate.notification_channel) { + console.log(`Sending notification for ${certificate.domain}`); + + // Different message based on expiry threshold + const message = isCritical + ? `🚨 CRITICAL: SSL Certificate for ${certificate.domain} will expire in ${daysLeft} days!` + : `⚠️ WARNING: SSL Certificate for ${certificate.domain} will expire in ${daysLeft} days.`; + + // Send the notification using our specialized SSL notification sender + const notificationSent = await sendSSLNotification(certificate, message, isCritical); + + if (notificationSent) { + // Update last_notified timestamp + await pb.collection('ssl_certificates').update(certificate.id, { + last_notified: new Date().toISOString() + }); + + console.log(`Notification sent for ${certificate.domain}`); + // Show toast for manual checks + toast.success(`Notification sent for ${certificate.domain}`); + return true; + } else { + console.error(`Failed to send notification for ${certificate.domain}`); + // Show error toast for manual checks + toast.error(`Failed to send notification for ${certificate.domain}`); + return false; + } + } else if (shouldNotify && !certificate.notification_channel) { + console.log(`No notification channel set for ${certificate.domain}, skipping notification`); + toast.info(`No notification channel set for ${certificate.domain}, skipping notification`); + } else { + console.log(`No notification needed for ${certificate.domain} (${daysLeft} days left)`); + // For manual checks, inform the user that thresholds weren't met + toast.info(`Certificate for ${certificate.domain} is valid (${daysLeft} days left)`); + } + + return true; + } catch (error) { + console.error(`Error checking certificate for ${certificate.domain}:`, error); + toast.error(`Error checking certificate: ${error instanceof Error ? error.message : "Unknown error"}`); + return false; + } +} + +/** + * Manual trigger for certificate notification test + * Useful for testing notification channels + */ +export async function testCertificateNotification(certificate: SSLCertificate): Promise { + try { + console.log(`Testing notification for ${certificate.domain}...`); + + // Create test message + const message = `🧪 TEST: SSL Certificate notification for ${certificate.domain}.`; + + // We set isCritical to false for test notifications + const notificationSent = await sendSSLNotification(certificate, message, false); + + if (notificationSent) { + console.log(`Test notification sent for ${certificate.domain}`); + toast.success(`Test notification sent for ${certificate.domain}`); + return true; + } else { + console.error(`Failed to send test notification for ${certificate.domain}`); + toast.error(`Failed to send test notification for ${certificate.domain}`); + return false; + } + } catch (error) { + console.error(`Error sending test notification for ${certificate.domain}:`, error); + toast.error(`Error sending test notification: ${error instanceof Error ? error.message : "Unknown error"}`); + return false; + } +} \ No newline at end of file diff --git a/application/src/services/ssl/notification/sslNotificationSender.ts b/application/src/services/ssl/notification/sslNotificationSender.ts new file mode 100644 index 0000000..32c2f16 --- /dev/null +++ b/application/src/services/ssl/notification/sslNotificationSender.ts @@ -0,0 +1,141 @@ + +import { pb } from "@/lib/pocketbase"; +import { SSLCertificate, SSLNotification } from "../types"; +import { AlertConfiguration } from "@/services/alertConfigService"; + +/** + * Dedicated function to send SSL certificate notifications + */ +export async function sendSSLNotification( + certificate: SSLCertificate, + message: string, + isCritical: boolean +): Promise { + try { + // Check if notification channel is set + if (!certificate.notification_channel) { + console.log(`No notification channel set for certificate: ${certificate.domain}`); + return false; + } + + // Fetch the notification configuration + const alertConfigRecord = await pb.collection('alert_configurations').getOne(certificate.notification_channel); + + if (!alertConfigRecord) { + console.error(`Alert configuration not found for ID: ${certificate.notification_channel}`); + return false; + } + + if (!alertConfigRecord.enabled) { + console.log(`Alert configuration is disabled for certificate: ${certificate.domain}`); + return false; + } + + // Convert PocketBase record to AlertConfiguration + const alertConfig: AlertConfiguration = { + id: alertConfigRecord.id, + collectionId: alertConfigRecord.collectionId, + collectionName: alertConfigRecord.collectionName, + service_id: alertConfigRecord.service_id || "", + notification_type: alertConfigRecord.notification_type, + telegram_chat_id: alertConfigRecord.telegram_chat_id, + discord_webhook_url: alertConfigRecord.discord_webhook_url, + signal_number: alertConfigRecord.signal_number, + notify_name: alertConfigRecord.notify_name, + bot_token: alertConfigRecord.bot_token, + template_id: alertConfigRecord.template_id, + slack_webhook_url: alertConfigRecord.slack_webhook_url, + enabled: alertConfigRecord.enabled, + created: alertConfigRecord.created, + updated: alertConfigRecord.updated + }; + + // Create an SSL notification object for tracking + const sslNotification: SSLNotification = { + certificateId: certificate.id, + domain: certificate.domain, + message: message, + isCritical: isCritical, + timestamp: new Date().toISOString() + }; + + // Send the notification based on the notification type + return await sendNotificationByType(alertConfig, certificate, message, isCritical, sslNotification); + + } catch (error) { + console.error("Error sending SSL notification:", error); + return false; + } +} + +/** + * Send notification based on notification channel type + */ +async function sendNotificationByType( + alertConfig: AlertConfiguration, + certificate: SSLCertificate, + message: string, + isCritical: boolean, + notification: SSLNotification +): Promise { + switch (alertConfig.notification_type) { + case 'telegram': + return await sendTelegramNotification(alertConfig, certificate, message, isCritical); + // Add other notification types as they are implemented + // case 'discord': + // return await sendDiscordNotification(alertConfig, certificate, message, isCritical); + // case 'slack': + // return await sendSlackNotification(alertConfig, certificate, message, isCritical); + default: + console.log(`Notification type ${alertConfig.notification_type} not implemented yet for SSL certificates`); + return false; + } +} + +/** + * Send notification via Telegram + */ +async function sendTelegramNotification( + alertConfig: AlertConfiguration, + certificate: SSLCertificate, + message: string, + isCritical: boolean +): Promise { + if (!alertConfig.bot_token || !alertConfig.telegram_chat_id) { + console.error("Missing Telegram bot token or chat ID"); + return false; + } + + const telegramUrl = `https://api.telegram.org/bot${alertConfig.bot_token}/sendMessage`; + const severity = isCritical ? "🚨 CRITICAL" : "⚠️ WARNING"; + + // Enhanced message with more SSL certificate details + const enhancedMessage = `${severity}: SSL Certificate Alert + +Domain: ${certificate.domain} +Days Remaining: ${certificate.days_left} +Expiration Date: ${certificate.valid_till} +Status: ${certificate.status.toUpperCase()} + +${message} + +This is an automated notification from your SSL certificate monitoring system.`; + + try { + const response = await fetch(telegramUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + chat_id: alertConfig.telegram_chat_id, + text: enhancedMessage, + parse_mode: 'Markdown' + }) + }); + + const result = await response.json(); + return result.ok === true; + } catch (error) { + console.error("Error sending Telegram notification:", error); + return false; + } +} \ No newline at end of file diff --git a/application/src/services/ssl/notification/sslScheduleUtils.ts b/application/src/services/ssl/notification/sslScheduleUtils.ts new file mode 100644 index 0000000..5ae250e --- /dev/null +++ b/application/src/services/ssl/notification/sslScheduleUtils.ts @@ -0,0 +1,27 @@ + +/** + * Check if SSL certificate daily check should run + * This helps implement the once-per-day check logic + */ +export function shouldRunDailyCheck(): boolean { + const lastRunKey = 'ssl_daily_check_last_run'; + const lastRun = localStorage.getItem(lastRunKey); + + if (!lastRun) { + // First time running, save timestamp and run + localStorage.setItem(lastRunKey, new Date().toISOString()); + return true; + } + + const lastRunDate = new Date(lastRun).setHours(0, 0, 0, 0); + const today = new Date().setHours(0, 0, 0, 0); + + // If last run was not today, run again + if (lastRunDate < today) { + localStorage.setItem(lastRunKey, new Date().toISOString()); + return true; + } + + // Already ran today + return false; + } \ No newline at end of file