Feat: Implement SSL notification channel selection
- Added notification channel selection to the Add SSL Certificate form. - Display available Telegram notification channels. - Integrate SSL expiry and warning thresholds for notifications. - Ensure daily auto-checks and manual check button functionality.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
|
||||
// Re-export all SSL-related functionality for domain SSL checking
|
||||
// Use explicit re-exports to avoid naming conflicts
|
||||
|
||||
// SSL Checker service
|
||||
export { checkSSLCertificate, checkSSLApi } from './sslCheckerService';
|
||||
|
||||
// SSL Status utilities
|
||||
export { determineSSLStatus } from './sslStatusUtils';
|
||||
|
||||
// Primary export for fetchSSLCertificates
|
||||
export { fetchSSLCertificates } from './sslFetchService';
|
||||
|
||||
// Certificate operations
|
||||
export {
|
||||
addSSLCertificate,
|
||||
checkAndUpdateCertificate,
|
||||
deleteSSLCertificate,
|
||||
refreshAllCertificates
|
||||
} from './sslCertificateOperations';
|
||||
|
||||
// SSL-specific notification service
|
||||
export {
|
||||
checkAllCertificatesAndNotify,
|
||||
checkCertificateAndNotify,
|
||||
shouldRunDailyCheck,
|
||||
sendSSLNotification
|
||||
} from './notification';
|
||||
|
||||
// Export types
|
||||
export type { SSLCheckerResponse, SSLCertificate, AddSSLCertificateDto, SSLNotification } from './types';
|
||||
|
||||
// Export utility functions
|
||||
export { normalizeDomain, createErrorResponse } from './sslCheckerUtils';
|
||||
|
||||
// Export checking mechanisms
|
||||
export { checkWithFetch } from './sslPrimaryChecker';
|
||||
@@ -0,0 +1,156 @@
|
||||
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import type { AddSSLCertificateDto, SSLCertificate } from "./types";
|
||||
import { checkSSLCertificate } from "./sslCheckerService";
|
||||
import { determineSSLStatus } from "./sslStatusUtils";
|
||||
import { checkCertificateAndNotify } from "./notification"; // Import notification service
|
||||
import { toast } from "sonner";
|
||||
|
||||
/**
|
||||
* Add a new SSL certificate to monitor
|
||||
*/
|
||||
export const addSSLCertificate = async (
|
||||
certificateData: AddSSLCertificateDto
|
||||
): Promise<SSLCertificate> => {
|
||||
try {
|
||||
// First check if the SSL certificate is valid and can be fetched
|
||||
const sslData = await checkSSLCertificate(certificateData.domain);
|
||||
|
||||
if (!sslData || !sslData.result) {
|
||||
throw new Error(`Could not fetch SSL certificate for ${certificateData.domain}`);
|
||||
}
|
||||
|
||||
// Prepare the data for saving to database
|
||||
const data = {
|
||||
domain: certificateData.domain,
|
||||
issued_to: sslData.result.issued_to || certificateData.domain,
|
||||
issuer_o: sslData.result.issuer_o || "",
|
||||
status: determineSSLStatus(
|
||||
sslData.result.days_left || 0,
|
||||
certificateData.warning_threshold,
|
||||
certificateData.expiry_threshold
|
||||
),
|
||||
cert_sans: sslData.result.cert_sans || "",
|
||||
cert_alg: sslData.result.cert_alg || "",
|
||||
serial_number: sslData.result.cert_sn || "",
|
||||
valid_from: sslData.result.valid_from || new Date().toISOString(),
|
||||
valid_till: sslData.result.valid_till || new Date().toISOString(),
|
||||
validity_days: sslData.result.validity_days || 0,
|
||||
days_left: sslData.result.days_left || 0,
|
||||
warning_threshold: Number(certificateData.warning_threshold) || 30,
|
||||
expiry_threshold: Number(certificateData.expiry_threshold) || 7,
|
||||
notification_channel: certificateData.notification_channel || "",
|
||||
};
|
||||
|
||||
// Save to database
|
||||
const record = await pb.collection("ssl_certificates").create(data);
|
||||
|
||||
return record as unknown as SSLCertificate;
|
||||
} catch (error) {
|
||||
console.error("Error adding SSL certificate:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check and update a specific SSL certificate
|
||||
*/
|
||||
export const checkAndUpdateCertificate = async (
|
||||
certificateId: string
|
||||
): Promise<SSLCertificate> => {
|
||||
try {
|
||||
// Get the certificate from database
|
||||
const certificate = await pb.collection("ssl_certificates").getOne(certificateId);
|
||||
|
||||
if (!certificate) {
|
||||
throw new Error(`Certificate with ID ${certificateId} not found`);
|
||||
}
|
||||
|
||||
const typedCertificate = certificate as unknown as SSLCertificate;
|
||||
const domain = typedCertificate.domain;
|
||||
|
||||
// Check SSL certificate
|
||||
const sslData = await checkSSLCertificate(domain);
|
||||
|
||||
if (!sslData || !sslData.result) {
|
||||
throw new Error(`Could not fetch SSL certificate for ${domain}`);
|
||||
}
|
||||
|
||||
// Update certificate data
|
||||
const updateData = {
|
||||
issued_to: sslData.result.issued_to || domain,
|
||||
issuer_o: sslData.result.issuer_o || typedCertificate.issuer_o,
|
||||
status: determineSSLStatus(
|
||||
sslData.result.days_left || 0,
|
||||
typedCertificate.warning_threshold,
|
||||
typedCertificate.expiry_threshold
|
||||
),
|
||||
cert_sans: sslData.result.cert_sans || typedCertificate.cert_sans,
|
||||
cert_alg: sslData.result.cert_alg || typedCertificate.cert_alg,
|
||||
serial_number: sslData.result.cert_sn || typedCertificate.serial_number,
|
||||
valid_from: sslData.result.valid_from || typedCertificate.valid_from,
|
||||
valid_till: sslData.result.valid_till || typedCertificate.valid_till,
|
||||
validity_days: sslData.result.validity_days || typedCertificate.validity_days,
|
||||
days_left: sslData.result.days_left || 0,
|
||||
};
|
||||
|
||||
// Update in database
|
||||
const updatedCert = await pb
|
||||
.collection("ssl_certificates")
|
||||
.update(certificateId, updateData);
|
||||
|
||||
const updatedCertificate = updatedCert as unknown as SSLCertificate;
|
||||
|
||||
// After updating, check if notification should be sent
|
||||
// This will respect the Warning and Expiry Thresholds
|
||||
await checkCertificateAndNotify(updatedCertificate);
|
||||
|
||||
return updatedCertificate;
|
||||
} catch (error) {
|
||||
console.error("Error updating SSL certificate:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete an SSL certificate from monitoring
|
||||
*/
|
||||
export const deleteSSLCertificate = async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
await pb.collection("ssl_certificates").delete(id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error deleting SSL certificate:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh all SSL certificates
|
||||
*/
|
||||
export const refreshAllCertificates = async (): Promise<{ success: number; failed: number }> => {
|
||||
try {
|
||||
const response = await pb.collection("ssl_certificates").getList(1, 100);
|
||||
const certificates = response.items as unknown as SSLCertificate[];
|
||||
|
||||
console.log(`Refreshing ${certificates.length} certificates...`);
|
||||
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const cert of certificates) {
|
||||
try {
|
||||
await checkAndUpdateCertificate(cert.id);
|
||||
success++;
|
||||
} catch (error) {
|
||||
console.error(`Failed to refresh certificate ${cert.domain}:`, error);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
return { success, failed };
|
||||
} catch (error) {
|
||||
console.error("Error refreshing certificates:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
import type { SSLCheckerResponse } from "./types";
|
||||
import { toast } from "sonner";
|
||||
import { checkWithFetch } from "./sslPrimaryChecker";
|
||||
import { normalizeDomain, createErrorResponse } from "./sslCheckerUtils";
|
||||
|
||||
/**
|
||||
* Check SSL certificate for a domain
|
||||
* Uses the reliable CORS proxy approach
|
||||
*/
|
||||
export const checkSSLCertificate = async (domain: string): Promise<SSLCheckerResponse> => {
|
||||
try {
|
||||
console.log(`Checking SSL certificate for domain: ${domain}`);
|
||||
|
||||
// Normalize domain (remove protocol if present)
|
||||
const normalizedDomain = normalizeDomain(domain);
|
||||
|
||||
if (!normalizedDomain) {
|
||||
throw new Error("Invalid domain provided");
|
||||
}
|
||||
|
||||
// Use the working CORS proxy approach
|
||||
const result = await checkWithFetch(normalizedDomain);
|
||||
console.log("SSL check result:", result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("SSL check failed completely:", error);
|
||||
toast.error(`SSL check failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
return createErrorResponse(domain, error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check SSL certificate using the CORS proxy
|
||||
* This is kept for backward compatibility
|
||||
*/
|
||||
export const checkSSLApi = async (domain: string): Promise<SSLCheckerResponse> => {
|
||||
return checkSSLCertificate(domain);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
/**
|
||||
* Normalize domain by removing protocol if present
|
||||
*/
|
||||
export const normalizeDomain = (domain: string): string => {
|
||||
if (!domain) return '';
|
||||
|
||||
// Remove any protocol (http://, https://)
|
||||
return domain.replace(/^(https?:\/\/)/, '').trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* Create error response for SSL check failures
|
||||
*/
|
||||
export const createErrorResponse = (domain: string, error: unknown): any => {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown SSL check error';
|
||||
|
||||
const errorResponse = {
|
||||
version: "1.0",
|
||||
app: "ssl-checker",
|
||||
host: domain,
|
||||
response_time_sec: "0",
|
||||
status: "error",
|
||||
message: errorMessage,
|
||||
error: errorMessage,
|
||||
result: {
|
||||
host: domain,
|
||||
resolved_ip: domain,
|
||||
issued_to: domain,
|
||||
issued_o: null,
|
||||
issuer_c: "Unknown",
|
||||
issuer_o: "Unknown",
|
||||
issuer_ou: null,
|
||||
issuer_cn: "Unknown",
|
||||
cert_sn: "Unknown",
|
||||
cert_sha1: "",
|
||||
cert_alg: "Unknown",
|
||||
cert_ver: 0,
|
||||
cert_sans: domain,
|
||||
cert_exp: true,
|
||||
cert_valid: false,
|
||||
valid_from: "Unknown",
|
||||
valid_till: "Unknown",
|
||||
validity_days: 0,
|
||||
days_left: 0,
|
||||
valid_days_to_expire: 0,
|
||||
hsts_header_enabled: false
|
||||
}
|
||||
};
|
||||
|
||||
return errorResponse;
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { SSLCertificate } from "@/types/ssl.types";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/**
|
||||
* Fetch SSL certificates from PocketBase
|
||||
*/
|
||||
export const fetchSSLCertificates = async (): Promise<SSLCertificate[]> => {
|
||||
try {
|
||||
console.log("Fetching SSL certificates from PocketBase...");
|
||||
|
||||
// Using the direct API path to fetch SSL certificates
|
||||
const endpoint = "/api/collections/ssl_certificates/records";
|
||||
const params = {
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
sort: "-created",
|
||||
cache: Date.now() // Prevent caching by adding a timestamp
|
||||
};
|
||||
|
||||
// Convert params to query string
|
||||
const queryString = new URLSearchParams(params as any).toString();
|
||||
const fullEndpoint = `${endpoint}?${queryString}`;
|
||||
|
||||
console.log("Fetching SSL certificates from:", fullEndpoint);
|
||||
const response = await pb.send(fullEndpoint, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
// Check if response has expected structure
|
||||
if (!response || !response.items) {
|
||||
console.error("Invalid response format:", response);
|
||||
toast.error("Failed to fetch SSL certificates: Invalid response format");
|
||||
throw new Error("Invalid response format from PocketBase API");
|
||||
}
|
||||
|
||||
console.log("Received SSL certificates:", response.items.length);
|
||||
|
||||
// Map items to SSLCertificate[] type with validation
|
||||
return response.items.map(item => {
|
||||
const cert = item as SSLCertificate;
|
||||
|
||||
// Log certificate details for debugging
|
||||
console.log(`Certificate for ${cert.domain}: Issued by ${cert.issuer_o}, Days left: ${cert.days_left}`);
|
||||
|
||||
// Ensure dates are valid
|
||||
try {
|
||||
if (cert.valid_from) new Date(cert.valid_from).toISOString();
|
||||
if (cert.valid_till) new Date(cert.valid_till).toISOString();
|
||||
if (cert.last_notified) new Date(cert.last_notified).toISOString();
|
||||
} catch (e) {
|
||||
console.warn("Invalid date found in certificate", cert.id, e);
|
||||
// Fix invalid dates if needed
|
||||
if (cert.valid_from && isNaN(new Date(cert.valid_from).getTime())) {
|
||||
cert.valid_from = new Date().toISOString();
|
||||
}
|
||||
if (cert.valid_till && isNaN(new Date(cert.valid_till).getTime())) {
|
||||
const futureDate = new Date();
|
||||
futureDate.setFullYear(futureDate.getFullYear() + 1);
|
||||
cert.valid_till = futureDate.toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate days left if not provided
|
||||
if (typeof cert.days_left !== 'number' && cert.valid_till) {
|
||||
try {
|
||||
const expirationDate = new Date(cert.valid_till);
|
||||
const currentDate = new Date();
|
||||
const diffTime = expirationDate.getTime() - currentDate.getTime();
|
||||
cert.days_left = Math.ceil(diffTime / (1000 * 3600 * 24));
|
||||
} catch (e) {
|
||||
console.warn("Error calculating days_left for certificate", cert.id, e);
|
||||
cert.days_left = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return cert;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching SSL certificates:", error);
|
||||
toast.error("Failed to fetch SSL certificates. Please try again later.");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
import { createErrorResponse } from "./sslCheckerUtils";
|
||||
import type { SSLCheckerResponse } from "./types";
|
||||
|
||||
/**
|
||||
* Check SSL certificate using fetch with CORS proxy
|
||||
* This is our primary method that's proven to work
|
||||
*/
|
||||
export const checkWithFetch = async (domain: string): Promise<SSLCheckerResponse> => {
|
||||
console.log(`Checking SSL via CORS proxy for: ${domain}`);
|
||||
|
||||
try {
|
||||
// Use the working CORS proxy
|
||||
const corsProxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(`https://ssl-checker.io/api/v1/check/${domain}`)}`;
|
||||
console.log("Using CORS proxy for SSL check:", corsProxyUrl);
|
||||
|
||||
const proxyResponse = await fetch(corsProxyUrl);
|
||||
if (!proxyResponse.ok) {
|
||||
throw new Error(`CORS proxy request failed with status: ${proxyResponse.status}`);
|
||||
}
|
||||
|
||||
const proxyData = await proxyResponse.json();
|
||||
console.log("CORS proxy returned SSL data:", proxyData);
|
||||
return proxyData;
|
||||
} catch (error) {
|
||||
console.error("SSL check failed:", error);
|
||||
return createErrorResponse(domain, error);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
/**
|
||||
* Determine SSL certificate status based on days left
|
||||
*/
|
||||
export const determineSSLStatus = (daysLeft: number, warningThreshold: number, expiryThreshold: number): string => {
|
||||
if (daysLeft <= 0) {
|
||||
return "expired";
|
||||
} else if (daysLeft <= expiryThreshold) {
|
||||
return "expiring_soon";
|
||||
} else if (daysLeft > expiryThreshold) {
|
||||
return "valid";
|
||||
} else {
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
|
||||
// SSL Checker response type
|
||||
export interface SSLCheckerResponse {
|
||||
version: string;
|
||||
app: string;
|
||||
host: string;
|
||||
response_time_sec: string;
|
||||
status: string; // "ok", "error"
|
||||
message?: string;
|
||||
error?: string;
|
||||
result: {
|
||||
host: string;
|
||||
resolved_ip?: string;
|
||||
issued_to: string;
|
||||
issued_o?: string | null;
|
||||
issuer_c?: string;
|
||||
issuer_o?: string | null;
|
||||
issuer_ou?: string | null;
|
||||
issuer_cn?: string;
|
||||
cert_sn: string;
|
||||
cert_sha1?: string;
|
||||
cert_alg: string;
|
||||
cert_ver?: number;
|
||||
cert_sans: string;
|
||||
cert_exp?: boolean;
|
||||
cert_valid?: boolean;
|
||||
valid_from?: string;
|
||||
valid_till?: string;
|
||||
validity_days?: number;
|
||||
days_left?: number;
|
||||
valid_days_to_expire?: number;
|
||||
hsts_header_enabled?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// SSL Certificate DTO for adding new certificates
|
||||
export interface AddSSLCertificateDto {
|
||||
domain: string;
|
||||
warning_threshold: number;
|
||||
expiry_threshold: number;
|
||||
notification_channel: string;
|
||||
}
|
||||
|
||||
// SSL Certificate model
|
||||
export interface SSLCertificate {
|
||||
id: string;
|
||||
domain: string;
|
||||
issued_to: string;
|
||||
issuer_o: string;
|
||||
status: string;
|
||||
cert_sans?: string;
|
||||
cert_alg?: string;
|
||||
serial_number?: number | string;
|
||||
valid_from: string;
|
||||
valid_till: string;
|
||||
validity_days: number;
|
||||
days_left: number;
|
||||
valid_days_to_expire?: number;
|
||||
warning_threshold: number;
|
||||
expiry_threshold: number;
|
||||
notification_channel: string;
|
||||
last_notified?: string;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
}
|
||||
|
||||
// SSL specific notification types
|
||||
export interface SSLNotification {
|
||||
certificateId: string;
|
||||
domain: string;
|
||||
message: string;
|
||||
isCritical: boolean;
|
||||
timestamp: string;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
import { SSLCheckerResponse } from "./types";
|
||||
|
||||
// Calculate days remaining from expiration date
|
||||
export function calculateDaysRemaining(validTo: string): number {
|
||||
try {
|
||||
const expirationDate = new Date(validTo);
|
||||
const currentDate = new Date();
|
||||
const diffTime = expirationDate.getTime() - currentDate.getTime();
|
||||
return Math.ceil(diffTime / (1000 * 3600 * 24)); // Convert ms to days
|
||||
} catch (error) {
|
||||
console.error("Error calculating days remaining:", error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the certificate is valid
|
||||
export function isValid(validTo: string): boolean {
|
||||
try {
|
||||
return new Date(validTo).getTime() > Date.now();
|
||||
} catch (error) {
|
||||
console.error("Error checking certificate validity:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert results to our expected response format
|
||||
export function convertResultToResponse(result: any): SSLCheckerResponse {
|
||||
return {
|
||||
version: "1.0",
|
||||
app: "ssl-checker",
|
||||
host: result.host || "",
|
||||
response_time_sec: result.response_time_sec || "0.5",
|
||||
status: result.status || "ok",
|
||||
result: {
|
||||
host: result.host || "",
|
||||
issued_to: result.subject || result.host || "",
|
||||
issuer_o: result.issuer || "Unknown",
|
||||
cert_sn: result.serial_number || "0",
|
||||
cert_alg: result.algorithm || "Unknown",
|
||||
cert_sans: result.sans || "",
|
||||
cert_exp: !result.is_valid,
|
||||
cert_valid: result.is_valid || false,
|
||||
valid_from: result.valid_from || new Date().toISOString(),
|
||||
valid_till: result.valid_to || new Date().toISOString(),
|
||||
validity_days: result.validity_days || 365,
|
||||
days_left: result.days_remaining || 0,
|
||||
valid_days_to_expire: result.days_remaining || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user