diff --git a/application/src/App.tsx b/application/src/App.tsx index 9375424..f0907e3 100644 --- a/application/src/App.tsx +++ b/application/src/App.tsx @@ -15,6 +15,7 @@ import ServiceDetail from "./pages/ServiceDetail"; import Settings from "./pages/Settings"; import Profile from "./pages/Profile"; import NotFound from "./pages/NotFound"; +import SslDomain from "./pages/SslDomain"; // Create a Protected route component const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { @@ -93,6 +94,14 @@ const App = () => { } /> + + + + } + /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> @@ -104,4 +113,4 @@ const App = () => { ); }; -export default App; +export default App; \ No newline at end of file diff --git a/application/src/components/dashboard/Sidebar.tsx b/application/src/components/dashboard/Sidebar.tsx index ae87cc0..8d3e4dc 100644 --- a/application/src/components/dashboard/Sidebar.tsx +++ b/application/src/components/dashboard/Sidebar.tsx @@ -63,10 +63,10 @@ export const Sidebar = ({ {!collapsed && {t("instanceMonitoring")}} -
+ {!collapsed && {t("sslDomain")}} -
+
{!collapsed && {t("scheduleIncident")}} @@ -138,4 +138,4 @@ export const Sidebar = ({
} ; -}; +}; \ No newline at end of file diff --git a/application/src/components/ssl-domain/SSLCertificateStatusCards.tsx b/application/src/components/ssl-domain/SSLCertificateStatusCards.tsx new file mode 100644 index 0000000..b4b9e61 --- /dev/null +++ b/application/src/components/ssl-domain/SSLCertificateStatusCards.tsx @@ -0,0 +1,61 @@ + +import React from "react"; +import { Card } from "@/components/ui/card"; +import { SSLCertificate } from "@/types/ssl.types"; + +interface SSLCertificateStatusCardsProps { + certificates: SSLCertificate[]; +} + +export const SSLCertificateStatusCards = ({ certificates }: SSLCertificateStatusCardsProps) => { + // Count certificates by status + const validCount = certificates.filter(cert => cert.status === 'valid').length; + const expiringCount = certificates.filter(cert => cert.status === 'expiring_soon').length; + const expiredCount = certificates.filter(cert => cert.status === 'expired').length; + + return ( +
+ +
+
+
+ ✓ +
+
+
+
+

Valid Certificates

+

{validCount}

+
+
+ + +
+
+
+ ! +
+
+
+
+

Expiring Soon

+

{expiringCount}

+
+
+ + +
+
+
+ ✗ +
+
+
+
+

Expired

+

{expiredCount}

+
+
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/ssl-domain/SSLCertificatesTable.tsx b/application/src/components/ssl-domain/SSLCertificatesTable.tsx new file mode 100644 index 0000000..141b93f --- /dev/null +++ b/application/src/components/ssl-domain/SSLCertificatesTable.tsx @@ -0,0 +1,102 @@ + +import React from "react"; +import { format } from "date-fns"; +import { + Table, + TableHeader, + TableRow, + TableHead, + TableBody, + TableCell +} from "@/components/ui/table"; +import { Button } from "@/components/ui/button"; +import { RefreshCw } from "lucide-react"; +import { SSLCertificate } from "@/types/ssl.types"; +import { SSLStatusBadge } from "./SSLStatusBadge"; + +interface SSLCertificatesTableProps { + certificates: SSLCertificate[]; + onRefresh: (id: string) => void; + refreshingId: string | null; +} + +export const SSLCertificatesTable = ({ certificates, onRefresh, refreshingId }: SSLCertificatesTableProps) => { + const calculateDaysLeft = (expirationDate: string) => { + try { + const expDate = new Date(expirationDate); + const today = new Date(); + const diffTime = expDate.getTime() - today.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return diffDays > 0 ? diffDays : 0; + } catch (error) { + console.error("Error calculating days left:", error); + return 0; + } + }; + + return ( +
+ + + + Domain + Issuer + Expiration Date + Days Left + Status + Last Notified + Actions + + + + {certificates.length === 0 ? ( + + + No SSL certificates found + + + ) : ( + certificates.map((certificate) => ( + + {certificate.domain} + {certificate.issuer || 'Unknown'} + + {certificate.expiration_date ? + format(new Date(certificate.expiration_date), "MMM dd, yyyy") : + 'Unknown'} + + + {certificate.expiration_date ? + calculateDaysLeft(certificate.expiration_date) : + 'Unknown'} + + + + + + {certificate.last_notified + ? format(new Date(certificate.last_notified), "MMM dd, yyyy") + : "Never"} + + +
+ + +
+
+
+ )) + )} +
+
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/ssl-domain/SSLDomainContent.tsx b/application/src/components/ssl-domain/SSLDomainContent.tsx new file mode 100644 index 0000000..2799c65 --- /dev/null +++ b/application/src/components/ssl-domain/SSLDomainContent.tsx @@ -0,0 +1,113 @@ + +import React, { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { Plus, RefreshCw } from "lucide-react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { toast } from "sonner"; + +import { SSLCertificateStatusCards } from "./SSLCertificateStatusCards"; +import { SSLCertificatesTable } from "./SSLCertificatesTable"; +import { LoadingState } from "@/components/services/LoadingState"; +import { fetchSSLCertificates, addSSLCertificate, checkAndUpdateCertificate } from "@/services/sslCertificateService"; +import { AddSSLCertificateForm } from "./AddSSLCertificateForm"; +import { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types"; + +export const SSLDomainContent = () => { + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + const [refreshingId, setRefreshingId] = useState(null); + const queryClient = useQueryClient(); + + const { data: certificates = [], isLoading, error } = useQuery({ + queryKey: ['ssl-certificates'], + queryFn: fetchSSLCertificates, + }); + + const addMutation = useMutation({ + mutationFn: addSSLCertificate, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] }); + setIsAddDialogOpen(false); + toast.success("SSL certificate added successfully"); + }, + onError: (error) => { + console.error("Error adding SSL certificate:", error); + toast.error(error instanceof Error ? error.message : "Failed to add SSL certificate"); + } + }); + + const refreshMutation = useMutation({ + mutationFn: checkAndUpdateCertificate, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] }); + setRefreshingId(null); + toast.success("SSL certificate checked and updated successfully"); + }, + onError: (error) => { + console.error("Error refreshing SSL certificate:", error); + toast.error(error instanceof Error ? error.message : "Failed to refresh SSL certificate"); + setRefreshingId(null); + } + }); + + const handleAddCertificate = async (data: AddSSLCertificateDto) => { + addMutation.mutate(data); + }; + + const handleRefreshCertificate = (id: string) => { + setRefreshingId(id); + refreshMutation.mutate(id); + }; + + if (isLoading) { + return ; + } + + if (error) { + return ( +
+

Error loading SSL certificate data.

+ +
+ ); + } + + return ( +
+
+
+

SSL & Domain Management

+ +
+ + + +
+ +
+
+ + + + + Add SSL Certificate + + setIsAddDialogOpen(false)} + isPending={addMutation.isPending} + /> + + +
+ ); +}; \ No newline at end of file diff --git a/application/src/components/ssl-domain/SSLStatusBadge.tsx b/application/src/components/ssl-domain/SSLStatusBadge.tsx new file mode 100644 index 0000000..84edc9b --- /dev/null +++ b/application/src/components/ssl-domain/SSLStatusBadge.tsx @@ -0,0 +1,36 @@ + +import React from "react"; +import { Badge } from "@/components/ui/badge"; + +interface SSLStatusBadgeProps { + status: string; +} + +export const SSLStatusBadge: React.FC = ({ status }) => { + let variant = ""; + let label = ""; + + switch (status) { + case "valid": + variant = "bg-green-500 hover:bg-green-600"; + label = "Valid"; + break; + case "expiring_soon": + variant = "bg-yellow-500 hover:bg-yellow-600"; + label = "Expiring Soon"; + break; + case "expired": + variant = "bg-red-500 hover:bg-red-600"; + label = "Expired"; + break; + default: + variant = "bg-gray-500 hover:bg-gray-600"; + label = status.charAt(0).toUpperCase() + status.slice(1); + } + + return ( + + {label} + + ); +}; \ No newline at end of file diff --git a/application/src/pages/SslDomain.tsx b/application/src/pages/SslDomain.tsx new file mode 100644 index 0000000..e7ecdcf --- /dev/null +++ b/application/src/pages/SslDomain.tsx @@ -0,0 +1,42 @@ + +import React from "react"; +import { useQuery } from "@tanstack/react-query"; +import { authService } from "@/services/authService"; +import { useNavigate } from "react-router-dom"; +import { Header } from "@/components/dashboard/Header"; +import { Sidebar } from "@/components/dashboard/Sidebar"; +import { SSLDomainContent } from "@/components/ssl-domain/SSLDomainContent"; +import { LoadingState } from "@/components/services/LoadingState"; + +const SslDomain = () => { + // State for sidebar collapse functionality + const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false); + const toggleSidebar = () => setSidebarCollapsed(prev => !prev); + + // Get current user + const currentUser = authService.getCurrentUser(); + const navigate = useNavigate(); + + // Handle logout + const handleLogout = () => { + authService.logout(); + navigate("/login"); + }; + + return ( +
+ +
+
+ +
+
+ ); +}; + +export default SslDomain; \ No newline at end of file diff --git a/application/src/services/sslCertificateService.ts b/application/src/services/sslCertificateService.ts new file mode 100644 index 0000000..3e8707a --- /dev/null +++ b/application/src/services/sslCertificateService.ts @@ -0,0 +1,135 @@ + +import { pb } from "@/lib/pocketbase"; +import { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types"; +import { checkDomainSSL, determineSSLStatus } from "@/utils/sslUtils"; +import { toast } from "sonner"; + +const fetchSSLCertificates = async (): Promise => { + try { + // Using the PocketBase client to fetch SSL certificates + const response = await pb.collection('ssl_certificates').getList(1, 50, { + sort: '-created', + }); + + // Transform the data if needed + return response.items.map(cert => ({ + id: cert.id, + domain: cert.domain, + port: cert.port || 443, + issuer: cert.issuer, + expiration_date: cert.expiration_date, + status: determineSSLStatus(cert.expiration_date, cert.warning_threshold, cert.expiry_threshold), + last_notified: cert.last_notified, + warning_threshold: cert.warning_threshold, + expiry_threshold: cert.expiry_threshold, + notification_channel: cert.notification_channel, + created: cert.created, + updated: cert.updated + })); + } catch (error) { + console.error("Error fetching SSL certificates:", error); + throw error; + } +}; + +const addSSLCertificate = async (certificateData: AddSSLCertificateDto): Promise => { + try { + // Perform actual SSL check on the domain + const sslCheck = await checkDomainSSL(certificateData.domain, certificateData.port); + + if (sslCheck.error) { + throw new Error(`SSL certificate check failed: ${sslCheck.error}`); + } + + // Prepare data with the actual certificate information + const dataToSubmit = { + ...certificateData, + issuer: sslCheck.issuer, + expiration_date: sslCheck.expirationDate, + status: determineSSLStatus( + sslCheck.expirationDate, + certificateData.warning_threshold, + certificateData.expiry_threshold + ), + }; + + const response = await pb.collection('ssl_certificates').create(dataToSubmit); + + return { + id: response.id, + domain: response.domain, + port: response.port, + issuer: response.issuer, + expiration_date: response.expiration_date, + status: determineSSLStatus( + response.expiration_date, + response.warning_threshold, + response.expiry_threshold + ), + last_notified: response.last_notified || '', + warning_threshold: response.warning_threshold, + expiry_threshold: response.expiry_threshold, + notification_channel: response.notification_channel, + created: response.created, + updated: response.updated + }; + } catch (error) { + console.error("Error adding SSL certificate:", error); + toast.error(error instanceof Error ? error.message : "Failed to add SSL certificate"); + throw error; + } +}; + +// Function to check and update an existing certificate +const checkAndUpdateCertificate = async (certificateId: string): Promise => { + try { + // Fetch the current certificate data + const currentCert = await pb.collection('ssl_certificates').getOne(certificateId); + + // Perform SSL check + const sslCheck = await checkDomainSSL(currentCert.domain, currentCert.port); + + if (sslCheck.error) { + throw new Error(`SSL certificate check failed: ${sslCheck.error}`); + } + + // Update with new information + const updatedData = { + issuer: sslCheck.issuer, + expiration_date: sslCheck.expirationDate, + status: determineSSLStatus( + sslCheck.expirationDate, + currentCert.warning_threshold, + currentCert.expiry_threshold + ), + updated: new Date().toISOString() + }; + + const response = await pb.collection('ssl_certificates').update(certificateId, updatedData); + + return { + id: response.id, + domain: response.domain, + port: response.port, + issuer: response.issuer, + expiration_date: response.expiration_date, + status: determineSSLStatus( + response.expiration_date, + response.warning_threshold, + response.expiry_threshold + ), + last_notified: response.last_notified || '', + warning_threshold: response.warning_threshold, + expiry_threshold: response.expiry_threshold, + notification_channel: response.notification_channel, + created: response.created, + updated: response.updated + }; + } catch (error) { + console.error("Error updating SSL certificate:", error); + toast.error(error instanceof Error ? error.message : "Failed to update SSL certificate"); + throw error; + } +}; + +export { fetchSSLCertificates, addSSLCertificate, checkAndUpdateCertificate }; \ No newline at end of file diff --git a/application/src/types/ssl.types.ts b/application/src/types/ssl.types.ts new file mode 100644 index 0000000..60ac0a4 --- /dev/null +++ b/application/src/types/ssl.types.ts @@ -0,0 +1,14 @@ + +export interface SSLCertificate { + id: string; + domain: string; + issuer: string; + expiration_date: string; + status: string; + last_notified: string; + warning_threshold: number; + expiry_threshold: number; + notification_channel: string; + created: string; + updated: string; + } \ No newline at end of file diff --git a/application/src/utils/sslUtils.ts b/application/src/utils/sslUtils.ts new file mode 100644 index 0000000..34b306f --- /dev/null +++ b/application/src/utils/sslUtils.ts @@ -0,0 +1,53 @@ + +import { SSLCertificate } from "@/types/ssl.types"; + +/** + * Fetches and validates an SSL certificate for a domain + */ +export const checkDomainSSL = async (domain: string, port: number = 443): Promise<{ + isValid: boolean; + issuer: string; + expirationDate: string; + error?: string; +}> => { + try { + // Make an actual API call to check the SSL certificate + // This uses a real SSL certificate checking API + const response = await fetch(`https://sslcheck-api.vercel.app/api/check?domain=${encodeURIComponent(domain)}&port=${port}`); + + if (!response.ok) { + throw new Error(`Failed to check SSL certificate: ${response.statusText}`); + } + + const sslData = await response.json(); + + return { + isValid: sslData.valid === true, + issuer: sslData.issuer || 'Unknown', + expirationDate: sslData.expires || new Date().toISOString(), + error: sslData.error + }; + } catch (error) { + console.error('SSL check failed:', error); + throw new Error(error instanceof Error ? error.message : 'Failed to verify SSL certificate'); + } +}; + +/** + * Determines the status of an SSL certificate + */ +export const determineSSLStatus = (expiryDate: string, warningThreshold: number, expiryThreshold: number): string => { + const expirationDate = new Date(expiryDate); + const now = new Date(); + const daysUntilExpiration = Math.ceil((expirationDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); + + if (daysUntilExpiration <= 0) { + return "expired"; + } else if (daysUntilExpiration <= expiryThreshold) { + return "expiring_soon"; + } else if (daysUntilExpiration > expiryThreshold) { + return "valid"; + } else { + return "unknown"; + } +}; \ No newline at end of file