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:
@@ -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,6 +121,7 @@ export const AddSSLCertificateForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="warning_threshold"
|
name="warning_threshold"
|
||||||
@@ -150,6 +155,24 @@ export const AddSSLCertificateForm = ({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="check_interval"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Check Interval (Days)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" min="1" max="30" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
How often to check the SSL certificate (in days)
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 { Plus } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableHead,
|
|
||||||
TableBody,
|
|
||||||
TableCell
|
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { RefreshCw, Eye, Edit, Trash2, MoreHorizontal } from "lucide-react";
|
|
||||||
import { SSLCertificate } from "@/types/ssl.types";
|
|
||||||
import { SSLStatusBadge } from "./SSLStatusBadge";
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
Dialog,
|
||||||
DropdownMenuContent,
|
DialogContent,
|
||||||
DropdownMenuItem,
|
DialogDescription,
|
||||||
DropdownMenuTrigger,
|
DialogHeader,
|
||||||
} from "@/components/ui/dropdown-menu";
|
DialogTitle,
|
||||||
import { toast } from "sonner";
|
} 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 [selectedCertificate, setSelectedCertificate] = useState<SSLCertificate | null>(null);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const formatDate = (dateString: string | undefined) => {
|
const { data: certificates = [], isLoading, isError } = useQuery({
|
||||||
if (!dateString) return t('unknown');
|
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, {
|
||||||
|
warning_threshold: updatedCertificate.warning_threshold,
|
||||||
|
expiry_threshold: updatedCertificate.expiry_threshold,
|
||||||
|
notification_channel: updatedCertificate.notification_channel,
|
||||||
|
check_interval: updatedCertificate.check_interval,
|
||||||
|
});
|
||||||
|
|
||||||
const handleEditCertificate = (certificate: SSLCertificate) => {
|
await queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||||
if (onEdit) {
|
setShowEditDialog(false);
|
||||||
onEdit(certificate);
|
setSelectedCertificate(null);
|
||||||
} else {
|
toast.success(t('certificateUpdated'));
|
||||||
toast.error("Edit functionality not implemented yet");
|
} 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>
|
<TableRow>
|
||||||
<TableHead>{t('domain')}</TableHead>
|
<TableHead>{t('domain')}</TableHead>
|
||||||
<TableHead>{t('issuer')}</TableHead>
|
|
||||||
<TableHead>{t('expirationDate')}</TableHead>
|
|
||||||
<TableHead>{t('daysLeft')}</TableHead>
|
|
||||||
<TableHead>{t('status')}</TableHead>
|
<TableHead>{t('status')}</TableHead>
|
||||||
<TableHead>{t('lastNotified')}</TableHead>
|
<TableHead>{t('issuer')}</TableHead>
|
||||||
<TableHead className="text-right">{t('actions')}</TableHead>
|
<TableHead>{t('validUntil')}</TableHead>
|
||||||
|
<TableHead>{t('daysLeft')}</TableHead>
|
||||||
|
<TableHead>Check Interval</TableHead>
|
||||||
|
<TableHead className="w-[50px]"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{certificates.length === 0 ? (
|
{certificates.map((certificate) => (
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={7} className="text-center py-8">
|
|
||||||
{t('noSSLCertificates')}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
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>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<DialogFooter className="p-6 pt-0 border-t">
|
{/* Add Certificate Dialog */}
|
||||||
<Button
|
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||||||
variant="default"
|
|
||||||
onClick={() => setSelectedCert(null)}
|
|
||||||
className="w-full sm:w-auto"
|
|
||||||
>
|
|
||||||
{t('close')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Delete Confirmation Dialog */}
|
|
||||||
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('deleteSSLCertificate')}</DialogTitle>
|
<DialogTitle>{t('addSSLCertificate')}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t('addCertificateDescription')}
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="py-4">
|
<AddSSLCertificateForm
|
||||||
<p>{t('deleteConfirmation')} <strong>{certToDelete?.domain}</strong>?</p>
|
onSubmit={handleAddCertificate}
|
||||||
<p className="text-sm text-muted-foreground mt-2">{t('deleteWarning')}</p>
|
onCancel={() => setShowAddDialog(false)}
|
||||||
</div>
|
isPending={isSubmitting}
|
||||||
<DialogFooter>
|
/>
|
||||||
<Button variant="outline" onClick={() => setDeleteConfirmOpen(false)}>{t('cancel')}</Button>
|
|
||||||
<Button variant="destructive" onClick={confirmDelete}>{t('delete')}</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Edit Certificate Dialog */}
|
||||||
|
<Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('editSSLCertificate')}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t('editCertificateDescription')}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{selectedCertificate && (
|
||||||
|
<EditSSLCertificateForm
|
||||||
|
certificate={selectedCertificate}
|
||||||
|
onSubmit={handleEditCertificate}
|
||||||
|
onCancel={() => setShowEditDialog(false)}
|
||||||
|
isPending={isSubmitting}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user