diff --git a/application/src/components/ssl-domain/AddSSLCertificateForm.tsx b/application/src/components/ssl-domain/AddSSLCertificateForm.tsx index 90a5581..3c2168d 100644 --- a/application/src/components/ssl-domain/AddSSLCertificateForm.tsx +++ b/application/src/components/ssl-domain/AddSSLCertificateForm.tsx @@ -1,9 +1,9 @@ - -import React from "react"; +import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { toast } from "sonner"; +import { Bell } from "lucide-react"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; @@ -11,10 +11,10 @@ import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { DialogFooter } from "@/components/ui/dialog"; import { AddSSLCertificateDto } from "@/types/ssl.types"; +import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; const formSchema = z.object({ domain: z.string().min(1, "Domain is required"), - port: z.coerce.number().int().min(1).max(65535), warning_threshold: z.coerce.number().int().min(1).max(365), expiry_threshold: z.coerce.number().int().min(1).max(30), notification_channel: z.string().min(1, "Notification channel is required") @@ -31,23 +31,60 @@ export const AddSSLCertificateForm = ({ onCancel, isPending = false }: AddSSLCertificateFormProps) => { + const [alertConfigs, setAlertConfigs] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { domain: "", - port: 443, warning_threshold: 30, expiry_threshold: 7, - notification_channel: "default" + notification_channel: "" } }); + // Fetch notification channels when form loads + useEffect(() => { + const fetchNotificationChannels = async () => { + setIsLoading(true); + try { + const configs = await alertConfigService.getAlertConfigurations(); + console.log("Fetched notification channels:", configs); + // Only include enabled channels + const enabledConfigs = configs.filter(config => { + // Handle the possibility of enabled being a string + if (typeof config.enabled === 'string') { + return config.enabled === "true"; + } + // Otherwise treat as boolean + return config.enabled === true; + }); + setAlertConfigs(enabledConfigs); + + // Set default if available, preferring Telegram channels + const telegramChannel = enabledConfigs.find(c => c.notification_type === "telegram"); + const defaultChannel = telegramChannel || enabledConfigs[0]; + + if (defaultChannel?.id) { + form.setValue("notification_channel", defaultChannel.id); + } + } catch (error) { + console.error("Error fetching notification channels:", error); + toast.error("Failed to load notification channels"); + } finally { + setIsLoading(false); + } + }; + + fetchNotificationChannels(); + }, [form]); + const handleSubmit = async (values: z.infer) => { try { // Convert the form values to the required DTO format with required properties const certData: AddSSLCertificateDto = { domain: values.domain, - port: values.port, warning_threshold: values.warning_threshold, expiry_threshold: values.expiry_threshold, notification_channel: values.notification_channel @@ -78,20 +115,6 @@ export const AddSSLCertificateForm = ({ )} /> - ( - - Port - - - - - - )} - /> - ( Notification Channel - - Default - Email - Slack - Telegram + {alertConfigs.length > 0 ? ( + alertConfigs.map((config) => ( + + {config.notify_name} ({config.notification_type}) + + )) + ) : isLoading ? ( + Loading channels... + ) : ( + No notification channels found + )} - + + Choose where to receive SSL certificate alerts @@ -155,7 +186,7 @@ export const AddSSLCertificateForm = ({ - diff --git a/application/src/components/ssl-domain/EditSSLCertificateForm.tsx b/application/src/components/ssl-domain/EditSSLCertificateForm.tsx new file mode 100644 index 0000000..0a6d547 --- /dev/null +++ b/application/src/components/ssl-domain/EditSSLCertificateForm.tsx @@ -0,0 +1,232 @@ + +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { SSLCertificate } from "@/types/ssl.types"; +import { Loader2, Bell } from "lucide-react"; +import { toast } from "sonner"; +import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService"; + +const formSchema = z.object({ + domain: z.string().min(1, "Domain is required"), + warning_threshold: z.coerce.number().min(1, "Warning threshold must be at least 1 day"), + expiry_threshold: z.coerce.number().min(1, "Expiry threshold must be at least 1 day"), + notification_channel: z.string().min(1, "Notification channel is required"), +}); + +type FormValues = z.infer; + +interface EditSSLCertificateFormProps { + certificate: SSLCertificate; + onSubmit: (data: SSLCertificate) => void; + onCancel: () => void; + isPending: boolean; +} + +export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPending }: EditSSLCertificateFormProps) => { + const [alertConfigs, setAlertConfigs] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + domain: certificate.domain, + warning_threshold: certificate.warning_threshold, + expiry_threshold: certificate.expiry_threshold, + notification_channel: certificate.notification_channel, + }, + }); + + // Fetch notification channels when form loads + useEffect(() => { + const fetchNotificationChannels = async () => { + setIsLoading(true); + try { + const configs = await alertConfigService.getAlertConfigurations(); + console.log("Fetched notification channels:", configs); + // Only include enabled channels + const enabledConfigs = configs.filter(config => { + // Handle the possibility of enabled being a string + if (typeof config.enabled === 'string') { + return config.enabled === "true"; + } + // Otherwise treat as boolean + return config.enabled === true; + }); + setAlertConfigs(enabledConfigs); + } catch (error) { + console.error("Error fetching notification channels:", error); + toast.error("Failed to load notification channels"); + } finally { + setIsLoading(false); + } + }; + + fetchNotificationChannels(); + }, []); + + const handleSubmit = (data: FormValues) => { + // Merge the updated values with the original certificate + const updatedCertificate: SSLCertificate = { + ...certificate, + ...data, + // Ensure values are correctly typed as numbers + warning_threshold: Number(data.warning_threshold), + expiry_threshold: Number(data.expiry_threshold) + }; + + console.log("Submitting updated certificate:", updatedCertificate); + onSubmit(updatedCertificate); + }; + + // For debugging + console.log("Certificate data:", certificate); + console.log("Form default values:", form.getValues()); + + return ( +
+ + ( + + Domain + + + + + Domain name cannot be changed. To monitor a different domain, add a new certificate. + + + + )} + /> + +
+ ( + + Warning Threshold (Days) + + + + + Days before expiration to send warning + + + + )} + /> + + ( + + Expiry Threshold (Days) + + + + + Days before expiration to send critical alert + + + + )} + /> +
+ + ( + + Notification Channel + + + + Where to send notifications about this certificate + + + + )} + /> + +
+ + +
+ + + ); +}; \ No newline at end of file diff --git a/application/src/components/ssl-domain/SSLDomainContent.tsx b/application/src/components/ssl-domain/SSLDomainContent.tsx index 2799c65..2805b10 100644 --- a/application/src/components/ssl-domain/SSLDomainContent.tsx +++ b/application/src/components/ssl-domain/SSLDomainContent.tsx @@ -1,4 +1,3 @@ - import React, { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; @@ -9,20 +8,40 @@ 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 { fetchSSLCertificates, addSSLCertificate, checkAndUpdateCertificate, refreshAllCertificates, deleteSSLCertificate } from "@/services/ssl"; import { AddSSLCertificateForm } from "./AddSSLCertificateForm"; -import { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types"; +import { EditSSLCertificateForm } from "./EditSSLCertificateForm"; +import type { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types"; +import { pb } from "@/lib/pocketbase"; export const SSLDomainContent = () => { const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [refreshingId, setRefreshingId] = useState(null); + const [isRefreshingAll, setIsRefreshingAll] = useState(false); + const [selectedCertificate, setSelectedCertificate] = useState(null); const queryClient = useQueryClient(); + // Fetch SSL certificates with explicit error handling const { data: certificates = [], isLoading, error } = useQuery({ queryKey: ['ssl-certificates'], - queryFn: fetchSSLCertificates, + queryFn: async () => { + try { + console.log("Fetching SSL certificates from SSLDomainContent..."); + const result = await fetchSSLCertificates(); + console.log("Received SSL certificates:", result); + return result; + } catch (error) { + console.error("Error fetching certificates:", error); + toast.error("Failed to load SSL certificates"); + throw error; + } + }, + refetchOnWindowFocus: false, + refetchInterval: 300000, // Refresh every 5 minutes }); + // Add certificate mutation const addMutation = useMutation({ mutationFn: addSSLCertificate, onSuccess: () => { @@ -32,21 +51,105 @@ export const SSLDomainContent = () => { }, onError: (error) => { console.error("Error adding SSL certificate:", error); - toast.error(error instanceof Error ? error.message : "Failed to add SSL certificate"); + toast.error(error instanceof Error ? error.message : "Failed to add SSL certificate. Make sure the domain is valid and accessible."); } }); - const refreshMutation = useMutation({ - mutationFn: checkAndUpdateCertificate, + // Edit certificate mutation - Updated to ensure thresholds are properly updated + const editMutation = useMutation({ + mutationFn: async (certificate: SSLCertificate) => { + console.log("Updating certificate with data:", certificate); + + // Create the update data object + const updateData = { + warning_threshold: Number(certificate.warning_threshold), + expiry_threshold: Number(certificate.expiry_threshold), + notification_channel: certificate.notification_channel, + }; + + console.log("Update data to be sent:", updateData); + + // Update certificate in the database using PocketBase directly + const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData); + + console.log("PocketBase update response:", updated); + + // After updating the settings, refresh the certificate to ensure it's up to date + // This will also check if notification needs to be sent based on updated thresholds + const refreshedCert = await checkAndUpdateCertificate(certificate.id); + + return refreshedCert; + }, onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] }); + setIsEditDialogOpen(false); + setSelectedCertificate(null); + toast.success("SSL certificate updated successfully"); + }, + onError: (error) => { + console.error("Error updating SSL certificate:", error); + toast.error(error instanceof Error ? error.message : "Failed to update SSL certificate"); + } + }); + + // Delete certificate mutation + const deleteMutation = useMutation({ + mutationFn: deleteSSLCertificate, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] }); + toast.success("SSL certificate deleted successfully"); + }, + onError: (error) => { + console.error("Error deleting SSL certificate:", error); + toast.error(error instanceof Error ? error.message : "Failed to delete SSL certificate"); + } + }); + + // Refresh certificate mutation - Updated to ensure notifications are sent + const refreshMutation = useMutation({ + mutationFn: checkAndUpdateCertificate, + onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] }); setRefreshingId(null); - toast.success("SSL certificate checked and updated successfully"); + toast.success(`SSL certificate for ${data.domain} updated successfully`); }, onError: (error) => { console.error("Error refreshing SSL certificate:", error); - toast.error(error instanceof Error ? error.message : "Failed to refresh SSL certificate"); + + let errorMessage = "Failed to check SSL certificate."; + + if (error instanceof Error) { + errorMessage = error.message; + } + + toast.error(errorMessage); setRefreshingId(null); + + // Still refresh the data to show any partial information that was saved + queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] }); + } + }); + + // Refresh all certificates mutation + const refreshAllMutation = useMutation({ + mutationFn: refreshAllCertificates, + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] }); + setIsRefreshingAll(false); + + if (result.failed === 0) { + toast.success(`Successfully refreshed all ${result.success} certificates`); + } else { + toast.info(`Refreshed ${result.success} certificates, ${result.failed} failed`); + } + }, + onError: (error) => { + console.error("Error refreshing all certificates:", error); + toast.error("Failed to refresh all certificates"); + setIsRefreshingAll(false); + + // Still refresh the data to show any partial information + queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] }); } }); @@ -55,10 +158,36 @@ export const SSLDomainContent = () => { }; const handleRefreshCertificate = (id: string) => { + if (refreshingId) return; // Prevent multiple refreshes setRefreshingId(id); refreshMutation.mutate(id); }; + const handleEditCertificate = (certificate: SSLCertificate) => { + setSelectedCertificate(certificate); + setIsEditDialogOpen(true); + }; + + const handleUpdateCertificate = (certificate: SSLCertificate) => { + console.log("Handling certificate update with data:", certificate); + editMutation.mutate(certificate); + }; + + const handleDeleteCertificate = (certificate: SSLCertificate) => { + deleteMutation.mutate(certificate.id); + }; + + const handleRefreshAll = async () => { + if (certificates.length === 0) { + toast.info("No certificates to refresh"); + return; + } + + setIsRefreshingAll(true); + toast.info(`Starting refresh of all ${certificates.length} certificates...`); + refreshAllMutation.mutate(); + }; + if (isLoading) { return ; } @@ -67,7 +196,7 @@ export const SSLDomainContent = () => { return (

Error loading SSL certificate data.

- +
); } @@ -76,13 +205,32 @@ export const SSLDomainContent = () => {
-

SSL & Domain Management

- +
+

SSL & Domain Management

+

Monitor SSL certificates and their expiration dates

+
+
+ + +
@@ -92,6 +240,8 @@ export const SSLDomainContent = () => { certificates={certificates} onRefresh={handleRefreshCertificate} refreshingId={refreshingId} + onEdit={handleEditCertificate} + onDelete={handleDeleteCertificate} />
@@ -108,6 +258,28 @@ export const SSLDomainContent = () => { /> + + {selectedCertificate && ( + { + setIsEditDialogOpen(open); + if (!open) setSelectedCertificate(null); + }}> + + + Edit SSL Certificate + + { + setIsEditDialogOpen(false); + setSelectedCertificate(null); + }} + isPending={editMutation.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 index 84edc9b..91c870d 100644 --- a/application/src/components/ssl-domain/SSLStatusBadge.tsx +++ b/application/src/components/ssl-domain/SSLStatusBadge.tsx @@ -23,6 +23,10 @@ export const SSLStatusBadge: React.FC = ({ status }) => { variant = "bg-red-500 hover:bg-red-600"; label = "Expired"; break; + case "pending": + variant = "bg-blue-500 hover:bg-blue-600"; + label = "Pending"; + break; default: variant = "bg-gray-500 hover:bg-gray-600"; label = status.charAt(0).toUpperCase() + status.slice(1);