feat: Add check interval and check_at update

- Added a "Check Interval" field to the SSL certificate create/edit forms.
- Implemented functionality to update the `check_at` field in Pocketbase with the current time when the "Check" button is clicked.
This commit is contained in:
Tola Leng
2025-06-20 17:17:34 +07:00
parent 11408dcba0
commit 614c10188d
10 changed files with 390 additions and 357 deletions
@@ -1,3 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -18,7 +19,8 @@ const formSchema = z.object({
domain: z.string().min(1, "Domain is required"), domain: z.string().min(1, "Domain is required"),
warning_threshold: z.coerce.number().int().min(1).max(365), warning_threshold: z.coerce.number().int().min(1).max(365),
expiry_threshold: z.coerce.number().int().min(1).max(30), expiry_threshold: z.coerce.number().int().min(1).max(30),
notification_channel: z.string().min(1, "Notification channel is required") notification_channel: z.string().min(1, "Notification channel is required"),
check_interval: z.coerce.number().int().min(1).max(30).optional()
}); });
interface AddSSLCertificateFormProps { interface AddSSLCertificateFormProps {
@@ -42,7 +44,8 @@ export const AddSSLCertificateForm = ({
domain: "", domain: "",
warning_threshold: 30, warning_threshold: 30,
expiry_threshold: 7, expiry_threshold: 7,
notification_channel: "" notification_channel: "",
check_interval: 1
} }
}); });
@@ -89,7 +92,8 @@ export const AddSSLCertificateForm = ({
domain: values.domain, domain: values.domain,
warning_threshold: values.warning_threshold, warning_threshold: values.warning_threshold,
expiry_threshold: values.expiry_threshold, expiry_threshold: values.expiry_threshold,
notification_channel: values.notification_channel notification_channel: values.notification_channel,
check_interval: values.check_interval
}; };
await onSubmit(certData); await onSubmit(certData);
@@ -117,34 +121,53 @@ export const AddSSLCertificateForm = ({
)} )}
/> />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="warning_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>{t('warningThreshold')}</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormDescription>
{t('getNotifiedExpiration')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expiry_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>{t('expiryThreshold')}</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormDescription>
{t('getNotifiedCritical')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField <FormField
control={form.control} control={form.control}
name="warning_threshold" name="check_interval"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('warningThreshold')}</FormLabel> <FormLabel>Check Interval (Days)</FormLabel>
<FormControl> <FormControl>
<Input type="number" {...field} /> <Input type="number" min="1" max="30" {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{t('getNotifiedExpiration')} How often to check the SSL certificate (in days)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expiry_threshold"
render={({ field }) => (
<FormItem>
<FormLabel>{t('expiryThreshold')}</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormDescription>
{t('getNotifiedCritical')}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -1,3 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@@ -17,6 +18,7 @@ const formSchema = z.object({
warning_threshold: z.coerce.number().min(1, "Warning threshold must be at least 1 day"), 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"), 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"), notification_channel: z.string().min(1, "Notification channel is required"),
check_interval: z.coerce.number().int().min(1).max(30).optional(),
}); });
type FormValues = z.infer<typeof formSchema>; type FormValues = z.infer<typeof formSchema>;
@@ -40,6 +42,7 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
warning_threshold: certificate.warning_threshold, warning_threshold: certificate.warning_threshold,
expiry_threshold: certificate.expiry_threshold, expiry_threshold: certificate.expiry_threshold,
notification_channel: certificate.notification_channel, notification_channel: certificate.notification_channel,
check_interval: certificate.check_interval || 1,
}, },
}); });
@@ -78,7 +81,8 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
...data, ...data,
// Ensure values are correctly typed as numbers // Ensure values are correctly typed as numbers
warning_threshold: Number(data.warning_threshold), warning_threshold: Number(data.warning_threshold),
expiry_threshold: Number(data.expiry_threshold) expiry_threshold: Number(data.expiry_threshold),
check_interval: data.check_interval ? Number(data.check_interval) : undefined
}; };
console.log("Submitting updated certificate:", updatedCertificate); console.log("Submitting updated certificate:", updatedCertificate);
@@ -113,7 +117,7 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
)} )}
/> />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField <FormField
control={form.control} control={form.control}
name="warning_threshold" name="warning_threshold"
@@ -157,6 +161,29 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="check_interval"
render={({ field }) => (
<FormItem>
<FormLabel>Check Interval (Days)</FormLabel>
<FormControl>
<Input
{...field}
type="number"
min="1"
max="30"
placeholder="1"
/>
</FormControl>
<FormDescription>
How often to check
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div> </div>
<FormField <FormField
@@ -230,4 +257,4 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
</form> </form>
</Form> </Form>
); );
}; };
@@ -0,0 +1,64 @@
import React from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { MoreHorizontal, RefreshCw, Edit, Trash2 } from "lucide-react";
import { SSLCertificate } from "@/types/ssl.types";
import { triggerImmediateCheck } from "@/services/sslCertificateService";
import { toast } from "sonner";
import { useLanguage } from "@/contexts/LanguageContext";
interface SSLCertificateActionsProps {
certificate: SSLCertificate;
onEdit: (certificate: SSLCertificate) => void;
onDelete: (certificate: SSLCertificate) => void;
}
export const SSLCertificateActions = ({
certificate,
onEdit,
onDelete
}: SSLCertificateActionsProps) => {
const { t } = useLanguage();
const handleCheck = async () => {
try {
await triggerImmediateCheck(certificate.id);
} catch (error) {
console.error("Error triggering SSL check:", error);
}
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleCheck}>
<RefreshCw className="mr-2 h-4 w-4" />
Check
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onEdit(certificate)}>
<Edit className="mr-2 h-4 w-4" />
{t('edit')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onDelete(certificate)}
className="text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
{t('delete')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -1,362 +1,248 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { format } from "date-fns"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { import { Plus } from "lucide-react";
Table,
TableHeader,
TableRow,
TableHead,
TableBody,
TableCell
} from "@/components/ui/table";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { RefreshCw, Eye, Edit, Trash2, MoreHorizontal } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { SSLCertificate } from "@/types/ssl.types";
import { SSLStatusBadge } from "./SSLStatusBadge";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { import {
DropdownMenu, Table,
DropdownMenuContent, TableBody,
DropdownMenuItem, TableCell,
DropdownMenuTrigger, TableHead,
} from "@/components/ui/dropdown-menu"; TableHeader,
import { toast } from "sonner"; TableRow,
} from "@/components/ui/table";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { SSLStatusBadge } from "./SSLStatusBadge";
import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
import { SSLCertificateActions } from "./SSLCertificateActions";
import { fetchSSLCertificates, addSSLCertificate, deleteSSLCertificate } from "@/services/sslCertificateService";
import { pb } from "@/lib/pocketbase";
import { SSLCertificate } from "@/types/ssl.types";
import { useLanguage } from "@/contexts/LanguageContext"; import { useLanguage } from "@/contexts/LanguageContext";
import { toast } from "sonner";
interface SSLCertificatesTableProps { export const SSLCertificatesTable = () => {
certificates: SSLCertificate[];
onRefresh: (id: string) => void;
refreshingId: string | null;
onEdit?: (certificate: SSLCertificate) => void;
onDelete?: (certificate: SSLCertificate) => void;
}
export const SSLCertificatesTable = ({
certificates,
onRefresh,
refreshingId,
onEdit,
onDelete
}: SSLCertificatesTableProps) => {
const { t } = useLanguage(); const { t } = useLanguage();
const [selectedCert, setSelectedCert] = useState<SSLCertificate | null>(null); const queryClient = useQueryClient();
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [showAddDialog, setShowAddDialog] = useState(false);
const [certToDelete, setCertToDelete] = useState<SSLCertificate | null>(null); const [showEditDialog, setShowEditDialog] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const formatDate = (dateString: string | undefined) => { const [selectedCertificate, setSelectedCertificate] = useState<SSLCertificate | null>(null);
if (!dateString) return t('unknown'); const [isSubmitting, setIsSubmitting] = useState(false);
const { data: certificates = [], isLoading, isError } = useQuery({
queryKey: ['ssl-certificates'],
queryFn: fetchSSLCertificates,
});
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error loading certificates</div>;
const handleAddCertificate = async (data: any) => {
setIsSubmitting(true);
try { try {
const date = new Date(dateString); await addSSLCertificate(data);
if (isNaN(date.getTime())) { await queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
console.warn("Invalid date for formatting:", dateString); setShowAddDialog(false);
return t('unknown'); toast.success(t('certificateAdded'));
}
return format(date, "MMM dd, yyyy");
} catch (error) { } catch (error) {
console.error("Error formatting date:", error); console.error("Error adding certificate:", error);
return t('unknown'); toast.error(t('failedToAddCertificate'));
} finally {
setIsSubmitting(false);
} }
}; };
const handleViewCertificate = (certificate: SSLCertificate) => { const handleEditCertificate = async (updatedCertificate: SSLCertificate) => {
setSelectedCert(certificate); setIsSubmitting(true);
}; try {
await pb.collection('ssl_certificates').update(updatedCertificate.id, {
const handleEditCertificate = (certificate: SSLCertificate) => { warning_threshold: updatedCertificate.warning_threshold,
if (onEdit) { expiry_threshold: updatedCertificate.expiry_threshold,
onEdit(certificate); notification_channel: updatedCertificate.notification_channel,
} else { check_interval: updatedCertificate.check_interval,
toast.error("Edit functionality not implemented yet"); });
await queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setShowEditDialog(false);
setSelectedCertificate(null);
toast.success(t('certificateUpdated'));
} catch (error) {
console.error("Error updating certificate:", error);
toast.error(t('failedToUpdateCertificate'));
} finally {
setIsSubmitting(false);
} }
}; };
const handleDeleteCertificate = (certificate: SSLCertificate) => { const handleDeleteCertificate = async () => {
setCertToDelete(certificate); if (!selectedCertificate) return;
setDeleteConfirmOpen(true);
setIsSubmitting(true);
try {
await deleteSSLCertificate(selectedCertificate.id);
await queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
setShowDeleteDialog(false);
setSelectedCertificate(null);
toast.success(t('certificateDeleted'));
} catch (error) {
console.error("Error deleting certificate:", error);
toast.error(t('failedToDeleteCertificate'));
} finally {
setIsSubmitting(false);
}
}; };
const confirmDelete = () => { const openEditDialog = (certificate: SSLCertificate) => {
if (certToDelete && onDelete) { setSelectedCertificate(certificate);
onDelete(certToDelete); setShowEditDialog(true);
setDeleteConfirmOpen(false); };
setCertToDelete(null);
} const openDeleteDialog = (certificate: SSLCertificate) => {
setSelectedCertificate(certificate);
setShowDeleteDialog(true);
}; };
return ( return (
<> <>
<div className="rounded-md border relative"> <Card>
{refreshingId && ( <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<div className="absolute inset-0 bg-background/50 flex items-center justify-center z-10"> <CardTitle>{t('sslCertificates')}</CardTitle>
<div className="bg-background p-4 rounded-md shadow flex items-center gap-2"> <Button onClick={() => setShowAddDialog(true)}>
<RefreshCw className="h-5 w-5 animate-spin text-primary" /> <Plus className="h-4 w-4 mr-2" />
<span>{t('checkingSSLCertificate')}</span> {t('addCertificate')}
</div> </Button>
</div> </CardHeader>
)} <CardContent>
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow>
<TableHead>{t('domain')}</TableHead>
<TableHead>{t('issuer')}</TableHead>
<TableHead>{t('expirationDate')}</TableHead>
<TableHead>{t('daysLeft')}</TableHead>
<TableHead>{t('status')}</TableHead>
<TableHead>{t('lastNotified')}</TableHead>
<TableHead className="text-right">{t('actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{certificates.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={7} className="text-center py-8"> <TableHead>{t('domain')}</TableHead>
{t('noSSLCertificates')} <TableHead>{t('status')}</TableHead>
</TableCell> <TableHead>{t('issuer')}</TableHead>
<TableHead>{t('validUntil')}</TableHead>
<TableHead>{t('daysLeft')}</TableHead>
<TableHead>Check Interval</TableHead>
<TableHead className="w-[50px]"></TableHead>
</TableRow> </TableRow>
) : ( </TableHeader>
certificates.map((certificate) => ( <TableBody>
{certificates.map((certificate) => (
<TableRow key={certificate.id}> <TableRow key={certificate.id}>
<TableCell className="font-medium">{certificate.domain}</TableCell> <TableCell className="font-medium">
<TableCell>{certificate.issuer_o || t('unknown')}</TableCell> {certificate.domain}
<TableCell>
{formatDate(certificate.valid_till)}
</TableCell>
<TableCell>
{typeof certificate.days_left === 'number' ? certificate.days_left : t('unknown')}
</TableCell> </TableCell>
<TableCell> <TableCell>
<SSLStatusBadge status={certificate.status} /> <SSLStatusBadge status={certificate.status} />
</TableCell> </TableCell>
<TableCell>{certificate.issuer_o || certificate.issuer_cn || 'Unknown'}</TableCell>
<TableCell> <TableCell>
{certificate.last_notified {certificate.valid_till ? new Date(certificate.valid_till).toLocaleDateString() : 'N/A'}
? formatDate(certificate.last_notified)
: t('never')}
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell>
<DropdownMenu> <span className={certificate.days_left <= 7 ? 'text-red-600 font-semibold' : certificate.days_left <= 30 ? 'text-yellow-600 font-semibold' : 'text-green-600'}>
<DropdownMenuTrigger asChild> {certificate.days_left} {t('days')}
<Button variant="ghost" size="sm" className="h-8 w-8 p-0"> </span>
<span className="sr-only">Open menu</span> </TableCell>
<MoreHorizontal className="h-4 w-4" /> <TableCell>
</Button> {certificate.check_interval || 1} {t('days')}
</DropdownMenuTrigger> </TableCell>
<DropdownMenuContent align="end" className="bg-background border border-border"> <TableCell>
<DropdownMenuItem <SSLCertificateActions
onClick={() => handleViewCertificate(certificate)} certificate={certificate}
className="cursor-pointer" onEdit={openEditDialog}
> onDelete={openDeleteDialog}
<Eye className="mr-2 h-4 w-4" /> {t('view')} />
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
if (refreshingId === null) {
onRefresh(certificate.id);
}
}}
disabled={refreshingId !== null}
className="cursor-pointer"
>
<RefreshCw className={`mr-2 h-4 w-4 ${refreshingId === certificate.id ? 'animate-spin text-primary' : ''}`} />
{t('check')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleEditCertificate(certificate)}
className="cursor-pointer"
>
<Edit className="mr-2 h-4 w-4" /> {t('edit')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteCertificate(certificate)}
className="cursor-pointer text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" /> {t('delete')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))}
)} </TableBody>
</TableBody> </Table>
</Table>
</div>
{/* SSL Certificate Details Dialog */} {certificates.length === 0 && (
<Dialog open={!!selectedCert} onOpenChange={(open) => !open && setSelectedCert(null)}> <div className="text-center py-8 text-muted-foreground">
<DialogContent className="max-w-3xl p-0 gap-0 overflow-hidden"> {t('noCertificatesFound')}
<DialogHeader className="p-6 pb-2">
<DialogTitle className="text-xl">
{t('sslCertificateDetails')}
</DialogTitle>
<p className="text-sm text-muted-foreground mt-1">
{t('detailedInfo')} {selectedCert?.domain}
</p>
</DialogHeader>
{selectedCert && (
<div className="p-6 pt-2 space-y-4 max-h-[70vh] overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Basic Information */}
<div className="border rounded-md p-4">
<h3 className="font-semibold mb-3">{t('basicInformation')}</h3>
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('domain')}:</span>
<span>{selectedCert.domain}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('status')}:</span>
<span><SSLStatusBadge status={selectedCert.status} /></span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('issuer')}:</span>
<span>{selectedCert.issued_to || t('unknown')}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">IP:</span>
<span>{selectedCert.resolved_ip || t('unknown')}</span>
</div>
</div>
</div>
{/* Validity */}
<div className="border rounded-md p-4">
<h3 className="font-semibold mb-3">{t('validity')}</h3>
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('validFrom')}:</span>
<span>{selectedCert.valid_from ? formatDate(selectedCert.valid_from) : t('unknown')}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('validUntil')}:</span>
<span>{formatDate(selectedCert.valid_till)}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('daysLeft')}:</span>
<span>{selectedCert.days_left}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('validityDays')}:</span>
<span>{selectedCert.validity_days || t('unknown')}</span>
</div>
</div>
</div>
{/* Issuer */}
<div className="border rounded-md p-4">
<h3 className="font-semibold mb-3">{t('issuerInfo')}</h3>
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('organization')}:</span>
<span>{selectedCert.issuer_o || t('unknown')}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('commonName')}:</span>
<span>{selectedCert.issuer_cn || t('unknown')}</span>
</div>
</div>
</div>
{/* Technical Details */}
<div className="border rounded-md p-4">
<h3 className="font-semibold mb-3">{t('technicalDetails')}</h3>
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('serialNumber')}:</span>
<span>{selectedCert.serial_number || t('unknown')}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<span className="text-muted-foreground">{t('algorithm')}:</span>
<span>{selectedCert.cert_alg || t('unknown')}</span>
</div>
</div>
</div>
</div>
{/* Subject Alternative Names */}
<div className="border rounded-md p-4">
<h3 className="font-semibold mb-3">{t('subjectAltNames')}</h3>
<div>
{selectedCert.cert_sans ? (
<p className="break-words">{selectedCert.cert_sans}</p>
) : (
<p>{t('none')}</p>
)}
</div>
</div>
{/* Monitoring Configuration */}
<div className="border rounded-md p-4">
<h3 className="font-semibold mb-3">{t('monitoringConfig')}</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div className="space-y-1">
<div className="text-muted-foreground">{t('warningThreshold')}:</div>
<div>{selectedCert.warning_threshold} {t('daysLeft').toLowerCase()}</div>
</div>
<div className="space-y-1">
<div className="text-muted-foreground">{t('expiryThreshold')}:</div>
<div>{selectedCert.expiry_threshold} {t('daysLeft').toLowerCase()}</div>
</div>
<div className="space-y-1">
<div className="text-muted-foreground">{t('notificationChannel')}:</div>
<div>{selectedCert.notification_channel}</div>
</div>
</div>
</div>
{/* Timestamps */}
<div className="border rounded-md p-4">
<h3 className="font-semibold mb-3">{t('recordInfo')}</h3>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<div className="text-muted-foreground">{t('created')}:</div>
<div>{selectedCert.created ? formatDate(selectedCert.created) : t('unknown')}</div>
</div>
<div className="space-y-1">
<div className="text-muted-foreground">{t('lastUpdated')}:</div>
<div>{selectedCert.updated ? formatDate(selectedCert.updated) : t('unknown')}</div>
</div>
<div className="space-y-1">
<div className="text-muted-foreground">{t('lastNotification')}:</div>
<div>{selectedCert.last_notified ? formatDate(selectedCert.last_notified) : t('never')}</div>
</div>
<div className="space-y-1">
<div className="text-muted-foreground">{t('collectionId')}:</div>
<div>{selectedCert.collectionId || t('unknown')}</div>
</div>
</div>
</div>
</div> </div>
)} )}
</CardContent>
<DialogFooter className="p-6 pt-0 border-t"> </Card>
<Button
variant="default" {/* Add Certificate Dialog */}
onClick={() => setSelectedCert(null)} <Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
className="w-full sm:w-auto" <DialogContent>
> <DialogHeader>
{t('close')} <DialogTitle>{t('addSSLCertificate')}</DialogTitle>
</Button> <DialogDescription>
</DialogFooter> {t('addCertificateDescription')}
</DialogDescription>
</DialogHeader>
<AddSSLCertificateForm
onSubmit={handleAddCertificate}
onCancel={() => setShowAddDialog(false)}
isPending={isSubmitting}
/>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Delete Confirmation Dialog */} {/* Edit Certificate Dialog */}
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}> <Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
<DialogContent> <DialogContent className="max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>{t('deleteSSLCertificate')}</DialogTitle> <DialogTitle>{t('editSSLCertificate')}</DialogTitle>
<DialogDescription>
{t('editCertificateDescription')}
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="py-4"> {selectedCertificate && (
<p>{t('deleteConfirmation')} <strong>{certToDelete?.domain}</strong>?</p> <EditSSLCertificateForm
<p className="text-sm text-muted-foreground mt-2">{t('deleteWarning')}</p> certificate={selectedCertificate}
</div> onSubmit={handleEditCertificate}
<DialogFooter> onCancel={() => setShowEditDialog(false)}
<Button variant="outline" onClick={() => setDeleteConfirmOpen(false)}>{t('cancel')}</Button> isPending={isSubmitting}
<Button variant="destructive" onClick={confirmDelete}>{t('delete')}</Button> />
</DialogFooter> )}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Delete Certificate Dialog */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('deleteCertificate')}</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteCertificateConfirmation')} {selectedCertificate?.domain}?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteCertificate}
disabled={isSubmitting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isSubmitting ? t('deleting') : t('delete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</> </>
); );
}; };
@@ -1,3 +1,4 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -242,13 +243,7 @@ export const SSLDomainContent = () => {
<SSLCertificateStatusCards certificates={certificates} /> <SSLCertificateStatusCards certificates={certificates} />
<div className="mt-6 flex-1 flex flex-col pb-6"> <div className="mt-6 flex-1 flex flex-col pb-6">
<SSLCertificatesTable <SSLCertificatesTable />
certificates={certificates}
onRefresh={handleRefreshCertificate}
refreshingId={refreshingId}
onEdit={handleEditCertificate}
onDelete={handleDeleteCertificate}
/>
</div> </div>
</div> </div>
+2 -1
View File
@@ -13,7 +13,8 @@ export {
addSSLCertificate, addSSLCertificate,
checkAndUpdateCertificate, checkAndUpdateCertificate,
deleteSSLCertificate, deleteSSLCertificate,
refreshAllCertificates refreshAllCertificates,
triggerImmediateCheck
} from './sslCertificateOperations'; } from './sslCertificateOperations';
// SSL-specific notification service // SSL-specific notification service
@@ -30,6 +30,7 @@ export const addSSLCertificate = async (
warning_threshold: Number(certificateData.warning_threshold) || 30, warning_threshold: Number(certificateData.warning_threshold) || 30,
expiry_threshold: Number(certificateData.expiry_threshold) || 7, expiry_threshold: Number(certificateData.expiry_threshold) || 7,
notification_channel: certificateData.notification_channel || "", notification_channel: certificateData.notification_channel || "",
check_interval: Number(certificateData.check_interval) || 1, // New field
}; };
// Save to database // Save to database
@@ -71,6 +72,27 @@ export const checkAndUpdateCertificate = async (
} }
}; };
/**
* Trigger immediate SSL check by setting check_at to current time
*/
export const triggerImmediateCheck = async (certificateId: string): Promise<void> => {
try {
const currentTime = new Date().toISOString();
// Update the check_at field to current time to trigger immediate check by Go service
await pb.collection("ssl_certificates").update(certificateId, {
check_at: currentTime
});
console.log(`Triggered immediate check for certificate ${certificateId} at ${currentTime}`);
toast.success("SSL check scheduled - certificate will be checked shortly");
} catch (error) {
console.error("Error triggering immediate SSL check:", error);
toast.error("Failed to schedule SSL check");
throw error;
}
};
/** /**
* Delete an SSL certificate from monitoring * Delete an SSL certificate from monitoring
*/ */
+9 -1
View File
@@ -1,10 +1,10 @@
// SSL Certificate DTO for adding new certificates // SSL Certificate DTO for adding new certificates
export interface AddSSLCertificateDto { export interface AddSSLCertificateDto {
domain: string; domain: string;
warning_threshold: number; warning_threshold: number;
expiry_threshold: number; expiry_threshold: number;
notification_channel: string; notification_channel: string;
check_interval?: number; // New field for check interval in days
} }
// SSL Certificate model // SSL Certificate model
@@ -28,6 +28,14 @@ export interface SSLCertificate {
last_notified?: string; last_notified?: string;
created?: string; created?: string;
updated?: string; updated?: string;
// New fields
check_interval?: number; // Check interval in days
check_at?: string; // Next check time
// Existing fields based on the provided structure
collectionId?: string;
collectionName?: string;
resolved_ip?: string;
issuer_cn?: string;
} }
// SSL specific notification types // SSL specific notification types
@@ -3,7 +3,9 @@
import { import {
fetchSSLCertificates, fetchSSLCertificates,
addSSLCertificate, addSSLCertificate,
checkAndUpdateCertificate checkAndUpdateCertificate,
triggerImmediateCheck,
deleteSSLCertificate
} from './ssl'; } from './ssl';
import { determineSSLStatus } from './ssl/sslStatusUtils'; import { determineSSLStatus } from './ssl/sslStatusUtils';
@@ -20,6 +22,8 @@ export {
fetchSSLCertificates, fetchSSLCertificates,
addSSLCertificate, addSSLCertificate,
checkAndUpdateCertificate, checkAndUpdateCertificate,
triggerImmediateCheck,
deleteSSLCertificate,
checkAllCertificatesAndNotify, checkAllCertificatesAndNotify,
checkCertificateAndNotify, checkCertificateAndNotify,
shouldRunDailyCheck shouldRunDailyCheck
+5 -2
View File
@@ -1,4 +1,3 @@
export interface SSLCertificate { export interface SSLCertificate {
id: string; id: string;
domain: string; domain: string;
@@ -19,7 +18,10 @@ export interface SSLCertificate {
last_notified?: string; last_notified?: string;
created?: string; created?: string;
updated?: string; updated?: string;
// New fields based on the provided structure // New fields
check_interval?: number; // Check interval in days
check_at?: string; // Next check time
// Existing fields based on the provided structure
collectionId?: string; collectionId?: string;
collectionName?: string; collectionName?: string;
resolved_ip?: string; resolved_ip?: string;
@@ -31,4 +33,5 @@ export interface AddSSLCertificateDto {
warning_threshold: number; warning_threshold: number;
expiry_threshold: number; expiry_threshold: number;
notification_channel: string; notification_channel: string;
check_interval?: number; // New field for check interval in days
} }