Notification (Telegram) for SSL notification thresholds
Ensure Telegram notifications respect warning/expiry thresholds, including manual checks.
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
|
||||
// Export all SSL notification related functionality
|
||||
export {
|
||||
checkAllCertificatesAndNotify,
|
||||
checkCertificateAndNotify,
|
||||
testCertificateNotification
|
||||
} from './sslCheckNotifier';
|
||||
|
||||
export { sendSSLNotification } from './sslNotificationSender';
|
||||
export { shouldRunDailyCheck } from './sslScheduleUtils';
|
||||
@@ -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<void> {
|
||||
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<boolean> {
|
||||
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<SSLCertificate> = {
|
||||
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<boolean> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user