Merge pull request #52 from operacle/develop
Implement SSL certificate checker in Go instead of TS and feat: Add error message and details columns
This commit is contained in:
@@ -37,10 +37,31 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
date: format(timestamp, 'MMM dd, yyyy'),
|
||||
value: data.status === "paused" ? null : data.responseTime,
|
||||
status: data.status,
|
||||
// Separate values for different statuses with proper positioning
|
||||
upValue: data.status === "up" ? data.responseTime : null,
|
||||
downValue: data.status === "down" ? data.responseTime : null,
|
||||
warningValue: data.status === "warning" ? data.responseTime : null,
|
||||
};
|
||||
});
|
||||
}, [uptimeData]);
|
||||
|
||||
// Calculate Y-axis domain for better positioning
|
||||
const yAxisDomain = useMemo(() => {
|
||||
if (!chartData.length) return ['dataMin - 10', 'dataMax + 10'];
|
||||
|
||||
const allValues = chartData
|
||||
.filter(d => d.value !== null && d.status !== 'paused')
|
||||
.map(d => d.value);
|
||||
|
||||
if (allValues.length === 0) return [0, 100];
|
||||
|
||||
const minValue = Math.min(...allValues);
|
||||
const maxValue = Math.max(...allValues);
|
||||
const padding = (maxValue - minValue) * 0.1 || 10;
|
||||
|
||||
return [Math.max(0, minValue - padding), maxValue + padding];
|
||||
}, [chartData]);
|
||||
|
||||
// Create a custom tooltip for the chart
|
||||
const CustomTooltip = ({ active, payload, label }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
@@ -79,31 +100,6 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
return null;
|
||||
};
|
||||
|
||||
// Compute status segments for different areas
|
||||
const getStatusSegments = () => {
|
||||
const segments = {
|
||||
up: [] as any[],
|
||||
down: [] as any[],
|
||||
warning: [] as any[]
|
||||
};
|
||||
|
||||
chartData.forEach(point => {
|
||||
if (point.status === "paused") return;
|
||||
|
||||
if (point.status === "up") {
|
||||
segments.up.push(point);
|
||||
} else if (point.status === "down") {
|
||||
segments.down.push(point);
|
||||
} else if (point.status === "warning") {
|
||||
segments.warning.push(point);
|
||||
}
|
||||
});
|
||||
|
||||
return segments;
|
||||
};
|
||||
|
||||
const segments = getStatusSegments();
|
||||
|
||||
// Check if we have any data to display - be more lenient by checking raw uptimeData
|
||||
const hasData = uptimeData.length > 0;
|
||||
|
||||
@@ -172,46 +168,40 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
<YAxis
|
||||
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
|
||||
allowDecimals={false}
|
||||
domain={['dataMin - 10', 'dataMax + 10']}
|
||||
domain={yAxisDomain}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
|
||||
{/* Area charts for different statuses */}
|
||||
{segments.up.length > 0 && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
data={segments.up}
|
||||
stroke="#10b981"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorUp)"
|
||||
connectNulls
|
||||
/>
|
||||
)}
|
||||
{/* Separate area charts for each status - positioned closer together */}
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="upValue"
|
||||
stroke="#10b981"
|
||||
strokeWidth={2}
|
||||
fillOpacity={0.4}
|
||||
fill="url(#colorUp)"
|
||||
connectNulls={false}
|
||||
/>
|
||||
|
||||
{segments.down.length > 0 && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
data={segments.down}
|
||||
stroke="#ef4444"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorDown)"
|
||||
connectNulls
|
||||
/>
|
||||
)}
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="downValue"
|
||||
stroke="#ef4444"
|
||||
strokeWidth={2}
|
||||
fillOpacity={0.4}
|
||||
fill="url(#colorDown)"
|
||||
connectNulls={false}
|
||||
/>
|
||||
|
||||
{segments.warning.length > 0 && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
data={segments.warning}
|
||||
stroke="#f59e0b"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorWarning)"
|
||||
connectNulls
|
||||
/>
|
||||
)}
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="warningValue"
|
||||
stroke="#f59e0b"
|
||||
strokeWidth={2}
|
||||
fillOpacity={0.4}
|
||||
fill="url(#colorWarning)"
|
||||
connectNulls={false}
|
||||
/>
|
||||
|
||||
{/* Add reference lines for paused periods */}
|
||||
{chartData.map((entry, index) =>
|
||||
@@ -231,4 +221,4 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -55,7 +55,8 @@ export const useRealTimeUpdates = ({
|
||||
setUptimeData(prev => {
|
||||
const newData: UptimeData = {
|
||||
id: e.record.id,
|
||||
serviceId: e.record.service_id,
|
||||
service_id: e.record.service_id, // Include service_id
|
||||
serviceId: e.record.service_id, // Keep for backward compatibility
|
||||
timestamp: e.record.timestamp,
|
||||
status: e.record.status,
|
||||
responseTime: e.record.response_time || 0,
|
||||
@@ -84,4 +85,4 @@ export const useRealTimeUpdates = ({
|
||||
console.error("Error setting up real-time updates:", error);
|
||||
}
|
||||
}, [serviceId, startDate, endDate, setService, setUptimeData]);
|
||||
};
|
||||
};
|
||||
@@ -68,7 +68,8 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
|
||||
|
||||
const placeholderHistory: UptimeData[] = Array(20).fill(null).map((_, index) => ({
|
||||
id: `placeholder-${serviceId}-${index}`,
|
||||
serviceId: serviceId || "",
|
||||
service_id: serviceId || "", // Include service_id
|
||||
serviceId: serviceId || "", // Keep for backward compatibility
|
||||
timestamp: new Date(Date.now() - (index * interval * 1000)).toISOString(),
|
||||
status: statusValue as "up" | "down" | "warning" | "paused",
|
||||
responseTime: 0
|
||||
@@ -96,7 +97,8 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
|
||||
|
||||
return {
|
||||
id: `padding-${serviceId}-${index}`,
|
||||
serviceId: serviceId || "",
|
||||
service_id: serviceId || "", // Include service_id
|
||||
serviceId: serviceId || "", // Keep for backward compatibility
|
||||
timestamp: new Date(baseTime - timeOffset).toISOString(),
|
||||
status: lastStatus,
|
||||
responseTime: 0
|
||||
|
||||
@@ -25,6 +25,8 @@ export function IncidentTable({ incidents }: IncidentTableProps) {
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("time")}</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("status")}</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("responseTime")}</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>Error Message</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>Details</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -52,10 +54,28 @@ export function IncidentTable({ incidents }: IncidentTableProps) {
|
||||
? `${check.responseTime}ms`
|
||||
: "N/A"}
|
||||
</TableCell>
|
||||
<TableCell className={`text-sm ${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} max-w-[200px]`}>
|
||||
{check.error_message ? (
|
||||
<div className="truncate" title={check.error_message}>
|
||||
{check.error_message}
|
||||
</div>
|
||||
) : (
|
||||
<span className={theme === 'dark' ? 'text-gray-500' : 'text-gray-400'}>-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={`text-sm ${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} max-w-[250px]`}>
|
||||
{check.details ? (
|
||||
<div className="truncate" title={check.details}>
|
||||
{check.details}
|
||||
</div>
|
||||
) : (
|
||||
<span className={theme === 'dark' ? 'text-gray-500' : 'text-gray-400'}>-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -18,7 +19,8 @@ const formSchema = z.object({
|
||||
domain: z.string().min(1, "Domain is required"),
|
||||
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")
|
||||
notification_channel: z.string().min(1, "Notification channel is required"),
|
||||
check_interval: z.coerce.number().int().min(1).max(30).optional()
|
||||
});
|
||||
|
||||
interface AddSSLCertificateFormProps {
|
||||
@@ -42,7 +44,8 @@ export const AddSSLCertificateForm = ({
|
||||
domain: "",
|
||||
warning_threshold: 30,
|
||||
expiry_threshold: 7,
|
||||
notification_channel: ""
|
||||
notification_channel: "",
|
||||
check_interval: 1
|
||||
}
|
||||
});
|
||||
|
||||
@@ -89,7 +92,8 @@ export const AddSSLCertificateForm = ({
|
||||
domain: values.domain,
|
||||
warning_threshold: values.warning_threshold,
|
||||
expiry_threshold: values.expiry_threshold,
|
||||
notification_channel: values.notification_channel
|
||||
notification_channel: values.notification_channel,
|
||||
check_interval: values.check_interval
|
||||
};
|
||||
|
||||
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
|
||||
control={form.control}
|
||||
name="warning_threshold"
|
||||
name="check_interval"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('warningThreshold')}</FormLabel>
|
||||
<FormLabel>Check Interval (Days)</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" {...field} />
|
||||
<Input type="number" min="1" max="30" {...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')}
|
||||
How often to check the SSL certificate (in days)
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
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"),
|
||||
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"),
|
||||
check_interval: z.coerce.number().int().min(1).max(30).optional(),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
@@ -40,6 +42,7 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
|
||||
warning_threshold: certificate.warning_threshold,
|
||||
expiry_threshold: certificate.expiry_threshold,
|
||||
notification_channel: certificate.notification_channel,
|
||||
check_interval: certificate.check_interval || 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,7 +81,8 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
|
||||
...data,
|
||||
// Ensure values are correctly typed as numbers
|
||||
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);
|
||||
@@ -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
|
||||
control={form.control}
|
||||
name="warning_threshold"
|
||||
@@ -157,6 +161,29 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
|
||||
</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>
|
||||
|
||||
<FormField
|
||||
@@ -230,4 +257,4 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
|
||||
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, Eye } 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;
|
||||
onView: (certificate: SSLCertificate) => void;
|
||||
onEdit: (certificate: SSLCertificate) => void;
|
||||
onDelete: (certificate: SSLCertificate) => void;
|
||||
}
|
||||
|
||||
export const SSLCertificateActions = ({
|
||||
certificate,
|
||||
onView,
|
||||
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={() => onView(certificate)}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
{t('view')}
|
||||
</DropdownMenuItem>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,219 @@
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SSLCertificate } from "@/types/ssl.types";
|
||||
import { SSLStatusBadge } from "./SSLStatusBadge";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface SSLCertificateDetailDialogProps {
|
||||
certificate: SSLCertificate | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const SSLCertificateDetailDialog = ({
|
||||
certificate,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: SSLCertificateDetailDialogProps) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
if (!certificate) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold">{t('sslCertificateDetails')}</DialogTitle>
|
||||
<DialogDescription className="text-sm text-muted-foreground">
|
||||
{t('viewDetailedInformation')} {certificate.domain}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Domain & Status Overview */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center justify-between">
|
||||
<span>{certificate.domain}</span>
|
||||
<SSLStatusBadge status={certificate.status} />
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('status')}</p>
|
||||
<SSLStatusBadge status={certificate.status} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('daysLeft')}</p>
|
||||
<Badge variant={certificate.days_left <= 7 ? 'destructive' : certificate.days_left <= 30 ? 'secondary' : 'default'}>
|
||||
{certificate.days_left} {t('days')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('validFrom')}</p>
|
||||
<p className="text-sm">
|
||||
{certificate.valid_from ? new Date(certificate.valid_from).toLocaleString() : 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('validUntil')}</p>
|
||||
<p className="text-sm">
|
||||
{certificate.valid_till ? new Date(certificate.valid_till).toLocaleString() : 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('validityPeriod')}</p>
|
||||
<p className="text-sm">{certificate.validity_days || 0} {t('days')}</p>
|
||||
</div>
|
||||
{certificate.resolved_ip && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('resolvedIP')}</p>
|
||||
<p className="text-sm font-mono bg-muted px-2 py-1 rounded">{certificate.resolved_ip}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Certificate Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('certificateDetails')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('issuedTo')}</p>
|
||||
<p className="text-sm bg-muted px-3 py-2 rounded">{certificate.issued_to || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('issuer')}</p>
|
||||
<p className="text-sm bg-muted px-3 py-2 rounded">{certificate.issuer_o || certificate.issuer_cn || 'N/A'}</p>
|
||||
</div>
|
||||
{certificate.issuer_cn && certificate.issuer_cn !== certificate.issuer_o && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Issuer Common Name</p>
|
||||
<p className="text-sm bg-muted px-3 py-2 rounded">{certificate.issuer_cn}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{certificate.serial_number && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('serialNumber')}</p>
|
||||
<p className="text-xs font-mono bg-muted px-3 py-2 rounded break-all">{certificate.serial_number}</p>
|
||||
</div>
|
||||
)}
|
||||
{certificate.cert_alg && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('algorithm')}</p>
|
||||
<p className="text-sm bg-muted px-3 py-2 rounded">{certificate.cert_alg}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{certificate.cert_sans && (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm font-medium text-muted-foreground mb-2">{t('subjectAlternativeNames')}</p>
|
||||
<div className="bg-muted px-3 py-2 rounded">
|
||||
<p className="text-sm whitespace-pre-wrap">{certificate.cert_sans}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Monitoring Configuration */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('monitoringConfiguration')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="bg-muted px-3 py-2 rounded">
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('warningThreshold')}</p>
|
||||
<p className="text-sm font-semibold">{certificate.warning_threshold} {t('days')}</p>
|
||||
</div>
|
||||
<div className="bg-muted px-3 py-2 rounded">
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('expiryThreshold')}</p>
|
||||
<p className="text-sm font-semibold">{certificate.expiry_threshold} {t('days')}</p>
|
||||
</div>
|
||||
<div className="bg-muted px-3 py-2 rounded">
|
||||
<p className="text-sm font-medium text-muted-foreground">Check Interval</p>
|
||||
<p className="text-sm font-semibold">{certificate.check_interval || 1} {t('days')}</p>
|
||||
</div>
|
||||
<div className="bg-muted px-3 py-2 rounded">
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('notificationChannel')}</p>
|
||||
<p className="text-sm font-semibold">{certificate.notification_channel || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{certificate.last_notified && (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('lastNotified')}</p>
|
||||
<p className="text-sm bg-muted px-3 py-2 rounded">{new Date(certificate.last_notified).toLocaleString()}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* System Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('technicalInformation')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-3">
|
||||
{certificate.check_at && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Next Check</p>
|
||||
<p className="text-sm bg-muted px-3 py-2 rounded">{new Date(certificate.check_at).toLocaleString()}</p>
|
||||
</div>
|
||||
)}
|
||||
{certificate.created && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('created')}</p>
|
||||
<p className="text-sm bg-muted px-3 py-2 rounded">{new Date(certificate.created).toLocaleString()}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{certificate.updated && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('lastUpdated')}</p>
|
||||
<p className="text-sm bg-muted px-3 py-2 rounded">{new Date(certificate.updated).toLocaleString()}</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Certificate ID</p>
|
||||
<p className="text-xs font-mono bg-muted px-3 py-2 rounded break-all">{certificate.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,362 +1,255 @@
|
||||
import React, { useState } from "react";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableCell
|
||||
} from "@/components/ui/table";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Plus } from "lucide-react";
|
||||
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { toast } from "sonner";
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
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 { SSLCertificateDetailDialog } from "./SSLCertificateDetailDialog";
|
||||
import { fetchSSLCertificates, addSSLCertificate, deleteSSLCertificate } from "@/services/sslCertificateService";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { SSLCertificate } from "@/types/ssl.types";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface SSLCertificatesTableProps {
|
||||
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) => {
|
||||
export const SSLCertificatesTable = () => {
|
||||
const { t } = useLanguage();
|
||||
const [selectedCert, setSelectedCert] = useState<SSLCertificate | null>(null);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [certToDelete, setCertToDelete] = useState<SSLCertificate | null>(null);
|
||||
|
||||
const formatDate = (dateString: string | undefined) => {
|
||||
if (!dateString) return t('unknown');
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [showViewDialog, setShowViewDialog] = useState(false);
|
||||
const [selectedCertificate, setSelectedCertificate] = useState<SSLCertificate | null>(null);
|
||||
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 {
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) {
|
||||
console.warn("Invalid date for formatting:", dateString);
|
||||
return t('unknown');
|
||||
}
|
||||
return format(date, "MMM dd, yyyy");
|
||||
await addSSLCertificate(data);
|
||||
await queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||
setShowAddDialog(false);
|
||||
toast.success(t('certificateAdded'));
|
||||
} catch (error) {
|
||||
console.error("Error formatting date:", error);
|
||||
return t('unknown');
|
||||
console.error("Error adding certificate:", error);
|
||||
toast.error(t('failedToAddCertificate'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewCertificate = (certificate: SSLCertificate) => {
|
||||
setSelectedCert(certificate);
|
||||
};
|
||||
|
||||
const handleEditCertificate = (certificate: SSLCertificate) => {
|
||||
if (onEdit) {
|
||||
onEdit(certificate);
|
||||
} else {
|
||||
toast.error("Edit functionality not implemented yet");
|
||||
const handleEditCertificate = async (updatedCertificate: SSLCertificate) => {
|
||||
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,
|
||||
});
|
||||
|
||||
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) => {
|
||||
setCertToDelete(certificate);
|
||||
setDeleteConfirmOpen(true);
|
||||
const handleDeleteCertificate = async () => {
|
||||
if (!selectedCertificate) return;
|
||||
|
||||
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 = () => {
|
||||
if (certToDelete && onDelete) {
|
||||
onDelete(certToDelete);
|
||||
setDeleteConfirmOpen(false);
|
||||
setCertToDelete(null);
|
||||
}
|
||||
const openViewDialog = (certificate: SSLCertificate) => {
|
||||
setSelectedCertificate(certificate);
|
||||
setShowViewDialog(true);
|
||||
};
|
||||
|
||||
const openEditDialog = (certificate: SSLCertificate) => {
|
||||
setSelectedCertificate(certificate);
|
||||
setShowEditDialog(true);
|
||||
};
|
||||
|
||||
const openDeleteDialog = (certificate: SSLCertificate) => {
|
||||
setSelectedCertificate(certificate);
|
||||
setShowDeleteDialog(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-md border relative">
|
||||
{refreshingId && (
|
||||
<div className="absolute inset-0 bg-background/50 flex items-center justify-center z-10">
|
||||
<div className="bg-background p-4 rounded-md shadow flex items-center gap-2">
|
||||
<RefreshCw className="h-5 w-5 animate-spin text-primary" />
|
||||
<span>{t('checkingSSLCertificate')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Table>
|
||||
<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 ? (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center py-8">
|
||||
{t('noSSLCertificates')}
|
||||
</TableCell>
|
||||
<TableHead>{t('domain')}</TableHead>
|
||||
<TableHead>{t('status')}</TableHead>
|
||||
<TableHead>{t('issuer')}</TableHead>
|
||||
<TableHead>{t('validUntil')}</TableHead>
|
||||
<TableHead>{t('daysLeft')}</TableHead>
|
||||
<TableHead>Check Interval</TableHead>
|
||||
<TableHead className="w-[50px]"></TableHead>
|
||||
</TableRow>
|
||||
) : (
|
||||
certificates.map((certificate) => (
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{certificates.map((certificate) => (
|
||||
<TableRow key={certificate.id}>
|
||||
<TableCell className="font-medium">{certificate.domain}</TableCell>
|
||||
<TableCell>{certificate.issuer_o || t('unknown')}</TableCell>
|
||||
<TableCell>
|
||||
{formatDate(certificate.valid_till)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{typeof certificate.days_left === 'number' ? certificate.days_left : t('unknown')}
|
||||
<TableCell className="font-medium">
|
||||
{certificate.domain}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SSLStatusBadge status={certificate.status} />
|
||||
</TableCell>
|
||||
<TableCell>{certificate.issuer_o || certificate.issuer_cn || 'Unknown'}</TableCell>
|
||||
<TableCell>
|
||||
{certificate.last_notified
|
||||
? formatDate(certificate.last_notified)
|
||||
: t('never')}
|
||||
{certificate.valid_till ? new Date(certificate.valid_till).toLocaleDateString() : 'N/A'}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" 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" className="bg-background border border-border">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleViewCertificate(certificate)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<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>
|
||||
<span className={certificate.days_left <= 7 ? 'text-red-600 font-semibold' : certificate.days_left <= 30 ? 'text-yellow-600 font-semibold' : 'text-green-600'}>
|
||||
{certificate.days_left} {t('days')}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{certificate.check_interval || 1} {t('days')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SSLCertificateActions
|
||||
certificate={certificate}
|
||||
onView={openViewDialog}
|
||||
onEdit={openEditDialog}
|
||||
onDelete={openDeleteDialog}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{/* SSL Certificate Details Dialog */}
|
||||
<Dialog open={!!selectedCert} onOpenChange={(open) => !open && setSelectedCert(null)}>
|
||||
<DialogContent className="max-w-3xl p-0 gap-0 overflow-hidden">
|
||||
<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>
|
||||
{certificates.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{t('noCertificatesFound')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter className="p-6 pt-0 border-t">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setSelectedCert(null)}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{t('close')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* View Certificate Dialog */}
|
||||
<SSLCertificateDetailDialog
|
||||
certificate={selectedCertificate}
|
||||
open={showViewDialog}
|
||||
onOpenChange={setShowViewDialog}
|
||||
/>
|
||||
|
||||
{/* Add Certificate Dialog */}
|
||||
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('addSSLCertificate')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('addCertificateDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<AddSSLCertificateForm
|
||||
onSubmit={handleAddCertificate}
|
||||
onCancel={() => setShowAddDialog(false)}
|
||||
isPending={isSubmitting}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||
<DialogContent>
|
||||
{/* Edit Certificate Dialog */}
|
||||
<Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('deleteSSLCertificate')}</DialogTitle>
|
||||
<DialogTitle>{t('editSSLCertificate')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('editCertificateDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<p>{t('deleteConfirmation')} <strong>{certToDelete?.domain}</strong>?</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">{t('deleteWarning')}</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDeleteConfirmOpen(false)}>{t('cancel')}</Button>
|
||||
<Button variant="destructive" onClick={confirmDelete}>{t('delete')}</Button>
|
||||
</DialogFooter>
|
||||
{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 { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -242,13 +243,7 @@ export const SSLDomainContent = () => {
|
||||
<SSLCertificateStatusCards certificates={certificates} />
|
||||
|
||||
<div className="mt-6 flex-1 flex flex-col pb-6">
|
||||
<SSLCertificatesTable
|
||||
certificates={certificates}
|
||||
onRefresh={handleRefreshCertificate}
|
||||
refreshingId={refreshingId}
|
||||
onEdit={handleEditCertificate}
|
||||
onDelete={handleDeleteCertificate}
|
||||
/>
|
||||
<SSLCertificatesTable />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ export async function handleServiceUp(service: any, responseTime: number, format
|
||||
|
||||
// Create a history record of this check with a more accurate timestamp
|
||||
const uptimeData: UptimeData = {
|
||||
serviceId: service.id,
|
||||
service_id: service.id, // Include service_id
|
||||
serviceId: service.id, // Keep for backward compatibility
|
||||
timestamp: new Date().toISOString(),
|
||||
status: "up",
|
||||
responseTime: responseTime,
|
||||
@@ -92,7 +93,8 @@ export async function handleServiceDown(service: any, formattedTime: string): Pr
|
||||
|
||||
// Create a history record of this check
|
||||
const uptimeData: UptimeData = {
|
||||
serviceId: service.id,
|
||||
service_id: service.id, // Include service_id
|
||||
serviceId: service.id, // Keep for backward compatibility
|
||||
timestamp: new Date().toISOString(),
|
||||
status: "down",
|
||||
responseTime: 0,
|
||||
@@ -161,4 +163,4 @@ export async function handleServiceDown(service: any, formattedTime: string): Pr
|
||||
} catch (error) {
|
||||
console.error("Error handling service DOWN state:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ export async function resumeMonitoring(serviceId: string): Promise<void> {
|
||||
id: service.id,
|
||||
name: service.name,
|
||||
url: service.url || "",
|
||||
host: service.host || "", // Include host property
|
||||
type: service.service_type || service.type || "HTTP",
|
||||
status: "up",
|
||||
responseTime: service.response_time || 0,
|
||||
@@ -77,4 +78,4 @@ export async function resumeMonitoring(serviceId: string): Promise<void> {
|
||||
} catch (error) {
|
||||
console.error("Error resuming service:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ export function prepareServiceForNotification(pbRecord: any, status: string, res
|
||||
id: pbRecord.id,
|
||||
name: pbRecord.name,
|
||||
url: pbRecord.url,
|
||||
host: pbRecord.host || "", // Include host property with fallback
|
||||
type: pbRecord.type || pbRecord.service_type || "HTTP",
|
||||
status: status as any,
|
||||
responseTime: responseTime,
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
// Re-export all SSL-related functionality for domain SSL checking
|
||||
// Use explicit re-exports to avoid naming conflicts
|
||||
|
||||
// SSL Checker service
|
||||
export { checkSSLCertificate, checkSSLApi } from './sslCheckerService';
|
||||
|
||||
// SSL Status utilities
|
||||
export { determineSSLStatus } from './sslStatusUtils';
|
||||
|
||||
@@ -16,7 +13,8 @@ export {
|
||||
addSSLCertificate,
|
||||
checkAndUpdateCertificate,
|
||||
deleteSSLCertificate,
|
||||
refreshAllCertificates
|
||||
refreshAllCertificates,
|
||||
triggerImmediateCheck
|
||||
} from './sslCertificateOperations';
|
||||
|
||||
// SSL-specific notification service
|
||||
@@ -28,10 +26,7 @@ export {
|
||||
} from './notification';
|
||||
|
||||
// Export types
|
||||
export type { SSLCheckerResponse, SSLCertificate, AddSSLCertificateDto, SSLNotification } from './types';
|
||||
export type { SSLCertificate, AddSSLCertificateDto, SSLNotification } from './types';
|
||||
|
||||
// Export utility functions
|
||||
export { normalizeDomain, createErrorResponse } from './sslCheckerUtils';
|
||||
|
||||
// Export checking mechanisms
|
||||
export { checkWithFetch } from './sslPrimaryChecker';
|
||||
// Export utility functions for SSL operations
|
||||
export { calculateDaysRemaining, isValid } from './utils';
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { SSLCertificate } from "../types";
|
||||
import { checkSSLCertificate } from "../sslCheckerService";
|
||||
import { determineSSLStatus } from "../sslStatusUtils";
|
||||
import { sendSSLNotification } from "./sslNotificationSender";
|
||||
import { toast } from "sonner";
|
||||
@@ -35,20 +34,14 @@ export async function checkAllCertificatesAndNotify(): Promise<void> {
|
||||
/**
|
||||
* Checks a specific SSL certificate and sends notification if needed
|
||||
* This respects the Warning and Expiry Thresholds set on the certificate
|
||||
* Note: SSL checking is now handled by the Go service, this function focuses on notifications
|
||||
*/
|
||||
export async function checkCertificateAndNotify(certificate: SSLCertificate): Promise<boolean> {
|
||||
console.log(`Checking certificate for ${certificate.domain}...`);
|
||||
|
||||
try {
|
||||
// Get fresh SSL data
|
||||
const sslData = await checkSSLCertificate(certificate.domain);
|
||||
if (!sslData || !sslData.result) {
|
||||
console.error(`Failed to check SSL for ${certificate.domain}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract days left from the check result
|
||||
const daysLeft = sslData.result.days_left || 0;
|
||||
// Use the current certificate data (updated by Go service)
|
||||
const daysLeft = certificate.days_left || 0;
|
||||
|
||||
// Get threshold values (ensure they are numbers)
|
||||
const warningThreshold = Number(certificate.warning_threshold) || 30;
|
||||
@@ -76,21 +69,10 @@ export async function checkCertificateAndNotify(certificate: SSLCertificate): Pr
|
||||
|
||||
console.log(`${certificate.domain}: ${daysLeft} days left, status: ${status}, should notify: ${shouldNotify}, critical: ${isCritical}`);
|
||||
|
||||
// Update certificate data in database
|
||||
const updateData: Partial<SSLCertificate> = {
|
||||
days_left: daysLeft,
|
||||
status: status,
|
||||
// Other fields from the SSL check that should be updated
|
||||
issuer_o: sslData.result.issuer_o || certificate.issuer_o,
|
||||
valid_from: sslData.result.valid_from || certificate.valid_from,
|
||||
valid_till: sslData.result.valid_till || certificate.valid_till,
|
||||
validity_days: sslData.result.validity_days || certificate.validity_days,
|
||||
cert_sans: sslData.result.cert_sans,
|
||||
cert_alg: sslData.result.cert_alg,
|
||||
serial_number: sslData.result.cert_sn
|
||||
};
|
||||
|
||||
await pb.collection('ssl_certificates').update(certificate.id, updateData);
|
||||
// Update certificate status in database
|
||||
await pb.collection('ssl_certificates').update(certificate.id, {
|
||||
status: status
|
||||
});
|
||||
|
||||
// Send notification if needed
|
||||
if (shouldNotify && certificate.notification_channel) {
|
||||
@@ -135,34 +117,4 @@ export async function checkCertificateAndNotify(certificate: SSLCertificate): Pr
|
||||
toast.error(`Error checking certificate: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual trigger for certificate notification test
|
||||
* Useful for testing notification channels
|
||||
*/
|
||||
export async function testCertificateNotification(certificate: SSLCertificate): Promise<boolean> {
|
||||
try {
|
||||
console.log(`Testing notification for ${certificate.domain}...`);
|
||||
|
||||
// Create test message
|
||||
const message = `🧪 TEST: SSL Certificate notification for ${certificate.domain}.`;
|
||||
|
||||
// We set isCritical to false for test notifications
|
||||
const notificationSent = await sendSSLNotification(certificate, message, false);
|
||||
|
||||
if (notificationSent) {
|
||||
console.log(`Test notification sent for ${certificate.domain}`);
|
||||
toast.success(`Test notification sent for ${certificate.domain}`);
|
||||
return true;
|
||||
} else {
|
||||
console.error(`Failed to send test notification for ${certificate.domain}`);
|
||||
toast.error(`Failed to send test notification for ${certificate.domain}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error sending test notification for ${certificate.domain}:`, error);
|
||||
toast.error(`Error sending test notification: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,36 @@
|
||||
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import type { AddSSLCertificateDto, SSLCertificate } from "./types";
|
||||
import { checkSSLCertificate } from "./sslCheckerService";
|
||||
import { determineSSLStatus } from "./sslStatusUtils";
|
||||
import { checkCertificateAndNotify } from "./notification"; // Import notification service
|
||||
import { toast } from "sonner";
|
||||
|
||||
/**
|
||||
* Add a new SSL certificate to monitor
|
||||
* Note: SSL checking is now handled by the Go service
|
||||
*/
|
||||
export const addSSLCertificate = async (
|
||||
certificateData: AddSSLCertificateDto
|
||||
): Promise<SSLCertificate> => {
|
||||
try {
|
||||
// First check if the SSL certificate is valid and can be fetched
|
||||
const sslData = await checkSSLCertificate(certificateData.domain);
|
||||
|
||||
if (!sslData || !sslData.result) {
|
||||
throw new Error(`Could not fetch SSL certificate for ${certificateData.domain}`);
|
||||
}
|
||||
|
||||
// Prepare the data for saving to database
|
||||
// The Go service will handle the actual SSL checking
|
||||
const data = {
|
||||
domain: certificateData.domain,
|
||||
issued_to: sslData.result.issued_to || certificateData.domain,
|
||||
issuer_o: sslData.result.issuer_o || "",
|
||||
status: determineSSLStatus(
|
||||
sslData.result.days_left || 0,
|
||||
certificateData.warning_threshold,
|
||||
certificateData.expiry_threshold
|
||||
),
|
||||
cert_sans: sslData.result.cert_sans || "",
|
||||
cert_alg: sslData.result.cert_alg || "",
|
||||
serial_number: sslData.result.cert_sn || "",
|
||||
valid_from: sslData.result.valid_from || new Date().toISOString(),
|
||||
valid_till: sslData.result.valid_till || new Date().toISOString(),
|
||||
validity_days: sslData.result.validity_days || 0,
|
||||
days_left: sslData.result.days_left || 0,
|
||||
issued_to: certificateData.domain, // Will be updated by Go service
|
||||
issuer_o: "", // Will be updated by Go service
|
||||
status: "pending", // Initial status
|
||||
cert_sans: "",
|
||||
cert_alg: "",
|
||||
serial_number: "",
|
||||
valid_from: new Date().toISOString(), // Will be updated by Go service
|
||||
valid_till: new Date().toISOString(), // Will be updated by Go service
|
||||
validity_days: 0, // Will be updated by Go service
|
||||
days_left: 0, // Will be updated by Go service
|
||||
warning_threshold: Number(certificateData.warning_threshold) || 30,
|
||||
expiry_threshold: Number(certificateData.expiry_threshold) || 7,
|
||||
notification_channel: certificateData.notification_channel || "",
|
||||
check_interval: Number(certificateData.check_interval) || 1, // New field
|
||||
};
|
||||
|
||||
// Save to database
|
||||
@@ -54,6 +45,7 @@ export const addSSLCertificate = async (
|
||||
|
||||
/**
|
||||
* Check and update a specific SSL certificate
|
||||
* Note: This now relies on the Go service for SSL data fetching
|
||||
*/
|
||||
export const checkAndUpdateCertificate = async (
|
||||
certificateId: string
|
||||
@@ -67,51 +59,40 @@ export const checkAndUpdateCertificate = async (
|
||||
}
|
||||
|
||||
const typedCertificate = certificate as unknown as SSLCertificate;
|
||||
const domain = typedCertificate.domain;
|
||||
|
||||
// Check SSL certificate
|
||||
const sslData = await checkSSLCertificate(domain);
|
||||
|
||||
if (!sslData || !sslData.result) {
|
||||
throw new Error(`Could not fetch SSL certificate for ${domain}`);
|
||||
}
|
||||
|
||||
// Update certificate data
|
||||
const updateData = {
|
||||
issued_to: sslData.result.issued_to || domain,
|
||||
issuer_o: sslData.result.issuer_o || typedCertificate.issuer_o,
|
||||
status: determineSSLStatus(
|
||||
sslData.result.days_left || 0,
|
||||
typedCertificate.warning_threshold,
|
||||
typedCertificate.expiry_threshold
|
||||
),
|
||||
cert_sans: sslData.result.cert_sans || typedCertificate.cert_sans,
|
||||
cert_alg: sslData.result.cert_alg || typedCertificate.cert_alg,
|
||||
serial_number: sslData.result.cert_sn || typedCertificate.serial_number,
|
||||
valid_from: sslData.result.valid_from || typedCertificate.valid_from,
|
||||
valid_till: sslData.result.valid_till || typedCertificate.valid_till,
|
||||
validity_days: sslData.result.validity_days || typedCertificate.validity_days,
|
||||
days_left: sslData.result.days_left || 0,
|
||||
};
|
||||
|
||||
// Update in database
|
||||
const updatedCert = await pb
|
||||
.collection("ssl_certificates")
|
||||
.update(certificateId, updateData);
|
||||
|
||||
const updatedCertificate = updatedCert as unknown as SSLCertificate;
|
||||
// The Go service will handle the actual SSL checking and updating
|
||||
// For now, we'll just trigger notifications based on current data
|
||||
await checkCertificateAndNotify(typedCertificate);
|
||||
|
||||
// After updating, check if notification should be sent
|
||||
// This will respect the Warning and Expiry Thresholds
|
||||
await checkCertificateAndNotify(updatedCertificate);
|
||||
|
||||
return updatedCertificate;
|
||||
// Return the current certificate data
|
||||
return typedCertificate;
|
||||
} catch (error) {
|
||||
console.error("Error updating SSL certificate:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -127,6 +108,7 @@ export const deleteSSLCertificate = async (id: string): Promise<boolean> => {
|
||||
|
||||
/**
|
||||
* Refresh all SSL certificates
|
||||
* Note: The Go service handles the actual SSL checking
|
||||
*/
|
||||
export const refreshAllCertificates = async (): Promise<{ success: number; failed: number }> => {
|
||||
try {
|
||||
@@ -140,7 +122,7 @@ export const refreshAllCertificates = async (): Promise<{ success: number; faile
|
||||
|
||||
for (const cert of certificates) {
|
||||
try {
|
||||
await checkAndUpdateCertificate(cert.id);
|
||||
await checkCertificateAndNotify(cert);
|
||||
success++;
|
||||
} catch (error) {
|
||||
console.error(`Failed to refresh certificate ${cert.domain}:`, error);
|
||||
|
||||
@@ -1,74 +1,48 @@
|
||||
// SSL Certificate DTO for adding new certificates
|
||||
export interface AddSSLCertificateDto {
|
||||
domain: string;
|
||||
warning_threshold: number;
|
||||
expiry_threshold: number;
|
||||
notification_channel: string;
|
||||
check_interval?: number; // New field for check interval in days
|
||||
}
|
||||
|
||||
// SSL Checker response type
|
||||
export interface SSLCheckerResponse {
|
||||
version: string;
|
||||
app: string;
|
||||
host: string;
|
||||
response_time_sec: string;
|
||||
status: string; // "ok", "error"
|
||||
message?: string;
|
||||
error?: string;
|
||||
result: {
|
||||
host: string;
|
||||
resolved_ip?: string;
|
||||
issued_to: string;
|
||||
issued_o?: string | null;
|
||||
issuer_c?: string;
|
||||
issuer_o?: string | null;
|
||||
issuer_ou?: string | null;
|
||||
issuer_cn?: string;
|
||||
cert_sn: string;
|
||||
cert_sha1?: string;
|
||||
cert_alg: string;
|
||||
cert_ver?: number;
|
||||
cert_sans: string;
|
||||
cert_exp?: boolean;
|
||||
cert_valid?: boolean;
|
||||
valid_from?: string;
|
||||
valid_till?: string;
|
||||
validity_days?: number;
|
||||
days_left?: number;
|
||||
valid_days_to_expire?: number;
|
||||
hsts_header_enabled?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// SSL Certificate DTO for adding new certificates
|
||||
export interface AddSSLCertificateDto {
|
||||
domain: string;
|
||||
warning_threshold: number;
|
||||
expiry_threshold: number;
|
||||
notification_channel: string;
|
||||
}
|
||||
|
||||
// SSL Certificate model
|
||||
export interface SSLCertificate {
|
||||
id: string;
|
||||
domain: string;
|
||||
issued_to: string;
|
||||
issuer_o: string;
|
||||
status: string;
|
||||
cert_sans?: string;
|
||||
cert_alg?: string;
|
||||
serial_number?: number | string;
|
||||
valid_from: string;
|
||||
valid_till: string;
|
||||
validity_days: number;
|
||||
days_left: number;
|
||||
valid_days_to_expire?: number;
|
||||
warning_threshold: number;
|
||||
expiry_threshold: number;
|
||||
notification_channel: string;
|
||||
last_notified?: string;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
}
|
||||
|
||||
// SSL specific notification types
|
||||
export interface SSLNotification {
|
||||
certificateId: string;
|
||||
domain: string;
|
||||
message: string;
|
||||
isCritical: boolean;
|
||||
timestamp: string;
|
||||
}
|
||||
// SSL Certificate model
|
||||
export interface SSLCertificate {
|
||||
id: string;
|
||||
domain: string;
|
||||
issued_to: string;
|
||||
issuer_o: string;
|
||||
status: string;
|
||||
cert_sans?: string;
|
||||
cert_alg?: string;
|
||||
serial_number?: number | string;
|
||||
valid_from: string;
|
||||
valid_till: string;
|
||||
validity_days: number;
|
||||
days_left: number;
|
||||
valid_days_to_expire?: number;
|
||||
warning_threshold: number;
|
||||
expiry_threshold: number;
|
||||
notification_channel: string;
|
||||
last_notified?: string;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
// 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
|
||||
export interface SSLNotification {
|
||||
certificateId: string;
|
||||
domain: string;
|
||||
message: string;
|
||||
isCritical: boolean;
|
||||
timestamp: string;
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
|
||||
import { SSLCheckerResponse } from "./types";
|
||||
|
||||
// Calculate days remaining from expiration date
|
||||
export function calculateDaysRemaining(validTo: string): number {
|
||||
try {
|
||||
@@ -22,30 +20,4 @@ export function isValid(validTo: string): boolean {
|
||||
console.error("Error checking certificate validity:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert results to our expected response format
|
||||
export function convertResultToResponse(result: any): SSLCheckerResponse {
|
||||
return {
|
||||
version: "1.0",
|
||||
app: "ssl-checker",
|
||||
host: result.host || "",
|
||||
response_time_sec: result.response_time_sec || "0.5",
|
||||
status: result.status || "ok",
|
||||
result: {
|
||||
host: result.host || "",
|
||||
issued_to: result.subject || result.host || "",
|
||||
issuer_o: result.issuer || "Unknown",
|
||||
cert_sn: result.serial_number || "0",
|
||||
cert_alg: result.algorithm || "Unknown",
|
||||
cert_sans: result.sans || "",
|
||||
cert_exp: !result.is_valid,
|
||||
cert_valid: result.is_valid || false,
|
||||
valid_from: result.valid_from || new Date().toISOString(),
|
||||
valid_till: result.valid_to || new Date().toISOString(),
|
||||
validity_days: result.validity_days || 365,
|
||||
days_left: result.days_remaining || 0,
|
||||
valid_days_to_expire: result.days_remaining || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
|
||||
// This file re-exports all SSL certificate related services for backward compatibility
|
||||
import {
|
||||
checkSSLCertificate,
|
||||
fetchSSLCertificates,
|
||||
addSSLCertificate,
|
||||
checkAndUpdateCertificate
|
||||
checkAndUpdateCertificate,
|
||||
triggerImmediateCheck,
|
||||
deleteSSLCertificate
|
||||
} from './ssl';
|
||||
|
||||
import { determineSSLStatus } from './ssl/sslStatusUtils';
|
||||
@@ -17,11 +18,12 @@ import {
|
||||
} from './ssl/notification';
|
||||
|
||||
export {
|
||||
checkSSLCertificate,
|
||||
determineSSLStatus,
|
||||
fetchSSLCertificates,
|
||||
addSSLCertificate,
|
||||
checkAndUpdateCertificate,
|
||||
triggerImmediateCheck,
|
||||
deleteSSLCertificate,
|
||||
checkAllCertificatesAndNotify,
|
||||
checkCertificateAndNotify,
|
||||
shouldRunDailyCheck
|
||||
|
||||
@@ -31,22 +31,23 @@ const getCollectionForServiceType = (serviceType: string): string => {
|
||||
export const uptimeService = {
|
||||
async recordUptimeData(data: UptimeData): Promise<void> {
|
||||
try {
|
||||
console.log(`Recording uptime data for service ${data.serviceId}: Status ${data.status}, Response time: ${data.responseTime}ms`);
|
||||
console.log(`Recording uptime data for service ${data.serviceId || data.service_id}: Status ${data.status}, Response time: ${data.responseTime}ms`);
|
||||
|
||||
const options = {
|
||||
$autoCancel: false,
|
||||
$cancelKey: `uptime_record_${data.serviceId}_${Date.now()}`
|
||||
$cancelKey: `uptime_record_${data.serviceId || data.service_id}_${Date.now()}`
|
||||
};
|
||||
|
||||
const record = await pb.collection('uptime_data').create({
|
||||
service_id: data.serviceId,
|
||||
service_id: data.service_id || data.serviceId,
|
||||
timestamp: data.timestamp,
|
||||
status: data.status,
|
||||
response_time: data.responseTime
|
||||
}, options);
|
||||
|
||||
// Invalidate cache for this service
|
||||
const keysToDelete = Array.from(uptimeCache.keys()).filter(key => key.includes(`uptime_${data.serviceId}`));
|
||||
const serviceId = data.service_id || data.serviceId;
|
||||
const keysToDelete = Array.from(uptimeCache.keys()).filter(key => key.includes(`uptime_${serviceId}`));
|
||||
keysToDelete.forEach(key => uptimeCache.delete(key));
|
||||
|
||||
console.log(`Uptime data recorded successfully with ID: ${record.id}`);
|
||||
@@ -116,12 +117,15 @@ export const uptimeService = {
|
||||
// Transform the response items to UptimeData format
|
||||
const uptimeData = response.items.map(item => ({
|
||||
id: item.id,
|
||||
serviceId: item.service_id,
|
||||
service_id: item.service_id, // Include service_id
|
||||
serviceId: item.service_id, // Keep for backward compatibility
|
||||
timestamp: item.timestamp,
|
||||
status: item.status as "up" | "down" | "warning" | "paused",
|
||||
responseTime: item.response_time || 0,
|
||||
date: item.timestamp,
|
||||
uptime: 100
|
||||
uptime: 100,
|
||||
error_message: item.error_message,
|
||||
details: item.details
|
||||
}));
|
||||
|
||||
// Cache the result
|
||||
|
||||
@@ -2,22 +2,36 @@
|
||||
export interface Service {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
host?: string; // Add host field for PING and TCP services
|
||||
port?: number; // Add port field for TCP services
|
||||
type: "HTTP" | "HTTPS" | "TCP" | "DNS" | "PING" | "HTTP" | "http" | "https" | "tcp" | "dns" | "ping" | "smtp" | "icmp";
|
||||
status: "up" | "down" | "paused" | "pending" | "warning";
|
||||
url?: string;
|
||||
host?: string; // Make host optional since it's not always required
|
||||
port?: number;
|
||||
domain?: string; // Add domain field for DNS services
|
||||
type: "http" | "https" | "tcp" | "ping" | "icmp" | "dns";
|
||||
status: "up" | "down" | "paused" | "warning";
|
||||
responseTime: number;
|
||||
uptime: number;
|
||||
uptime?: number;
|
||||
lastChecked: string;
|
||||
interval: number;
|
||||
timeout?: number;
|
||||
retries: number;
|
||||
notificationChannel?: string;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
notification_channel?: string;
|
||||
notificationChannel?: string; // Keep for backward compatibility
|
||||
alertTemplate?: string;
|
||||
muteAlerts?: boolean; // Keep this to avoid breaking existing code
|
||||
alerts?: "muted" | "unmuted"; // Make sure alerts is properly typed as union
|
||||
muteAlerts?: boolean; // Keep this to avoid breaking existing code
|
||||
muteChangedAt?: string;
|
||||
domain?: string; // Add domain field for DNS services
|
||||
follow_redirects?: boolean;
|
||||
verify_ssl?: boolean;
|
||||
expected_status_code?: number;
|
||||
keyword_check?: string;
|
||||
keyword_check_type?: "contains" | "not_contains";
|
||||
dns_record_type?: "A" | "AAAA" | "CNAME" | "MX" | "TXT" | "NS";
|
||||
dns_expected_value?: string;
|
||||
headers?: string;
|
||||
body?: string;
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
|
||||
}
|
||||
|
||||
export interface CreateServiceParams {
|
||||
@@ -34,11 +48,54 @@ export interface CreateServiceParams {
|
||||
}
|
||||
|
||||
export interface UptimeData {
|
||||
date?: string;
|
||||
uptime?: number;
|
||||
id?: string;
|
||||
serviceId?: string;
|
||||
service_id?: string; // Make service_id optional for backward compatibility
|
||||
serviceId?: string; // Keep for backward compatibility
|
||||
timestamp: string;
|
||||
status: "up" | "down" | "paused" | "pending" | "warning";
|
||||
status: "up" | "down" | "paused" | "warning";
|
||||
responseTime: number;
|
||||
error_message?: string;
|
||||
details?: string;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
date?: string; // Keep for backward compatibility
|
||||
uptime?: number; // Keep for backward compatibility
|
||||
}
|
||||
|
||||
export interface PingData {
|
||||
id?: string;
|
||||
service_id: string;
|
||||
timestamp: string;
|
||||
status: "up" | "down" | "paused" | "warning";
|
||||
responseTime: number;
|
||||
packet_loss?: number;
|
||||
error_message?: string;
|
||||
details?: string;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
}
|
||||
|
||||
export interface DNSData {
|
||||
id?: string;
|
||||
service_id: string;
|
||||
timestamp: string;
|
||||
status: "up" | "down" | "paused" | "warning";
|
||||
responseTime: number;
|
||||
resolved_ip?: string;
|
||||
error_message?: string;
|
||||
details?: string;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
}
|
||||
|
||||
export interface TCPData {
|
||||
id?: string;
|
||||
service_id: string;
|
||||
timestamp: string;
|
||||
status: "up" | "down" | "paused" | "warning";
|
||||
responseTime: number;
|
||||
error_message?: string;
|
||||
details?: string;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export interface SSLCertificate {
|
||||
id: string;
|
||||
domain: string;
|
||||
@@ -19,7 +18,10 @@ export interface SSLCertificate {
|
||||
last_notified?: string;
|
||||
created?: 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;
|
||||
collectionName?: string;
|
||||
resolved_ip?: string;
|
||||
@@ -31,4 +33,5 @@ export interface AddSSLCertificateDto {
|
||||
warning_threshold: number;
|
||||
expiry_threshold: number;
|
||||
notification_channel: string;
|
||||
check_interval?: number; // New field for check interval in days
|
||||
}
|
||||
@@ -8,10 +8,10 @@ A Go-based microservice for service operations including ICMP ping, DNS resoluti
|
||||
- **ICMP Ping**: Full ping functionality with packet statistics
|
||||
- **DNS Resolution**: A, AAAA, MX, and TXT record lookups
|
||||
- **TCP Connectivity**: Port connectivity testing
|
||||
- **SSL Certificate**: SSL Certificate Check
|
||||
- REST API endpoints
|
||||
- Health check endpoint
|
||||
- Configurable via environment variables
|
||||
- Docker support
|
||||
- Comprehensive operation statistics
|
||||
|
||||
## API Endpoints
|
||||
|
||||
@@ -23,7 +23,7 @@ func Load() *Config {
|
||||
cfg := &Config{
|
||||
Port: getEnv("PORT", "8091"),
|
||||
DefaultCount: getEnvInt("DEFAULT_COUNT", 4),
|
||||
DefaultTimeout: getEnvDuration("DEFAULT_TIMEOUT", 3*time.Second),
|
||||
DefaultTimeout: getEnvDuration("DEFAULT_TIMEOUT", 10*time.Second),
|
||||
MaxCount: getEnvInt("MAX_COUNT", 20),
|
||||
MaxTimeout: getEnvDuration("MAX_TIMEOUT", 30*time.Second),
|
||||
EnableLogging: getEnvBool("ENABLE_LOGGING", true),
|
||||
|
||||
@@ -79,6 +79,10 @@ func (h *OperationHandler) HandleOperation(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
result, err = httpOp.Execute(url, method)
|
||||
|
||||
case types.OperationSSL:
|
||||
sslOp := operations.NewSSLOperation(timeout)
|
||||
result, err = sslOp.Execute(req.Host)
|
||||
|
||||
default:
|
||||
http.Error(w, "Invalid operation type", http.StatusBadRequest)
|
||||
return
|
||||
|
||||
@@ -21,6 +21,7 @@ func main() {
|
||||
// Initialize PocketBase client (no credentials required)
|
||||
var pbClient *pocketbase.PocketBaseClient
|
||||
var monitoringService *monitoring.MonitoringService
|
||||
var sslMonitoringService *monitoring.SSLMonitoringService
|
||||
|
||||
if cfg.PocketBaseEnabled {
|
||||
var err error
|
||||
@@ -35,6 +36,11 @@ func main() {
|
||||
monitoringService = monitoring.NewMonitoringService(pbClient)
|
||||
go monitoringService.Start()
|
||||
log.Println("Monitoring service started (public access mode)")
|
||||
|
||||
// Initialize and start SSL monitoring service
|
||||
sslMonitoringService = monitoring.NewSSLMonitoringService(pbClient)
|
||||
go sslMonitoringService.Start()
|
||||
log.Println("SSL monitoring service started")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,13 +69,16 @@ func main() {
|
||||
if monitoringService != nil {
|
||||
log.Printf("Automatic service monitoring enabled")
|
||||
}
|
||||
if sslMonitoringService != nil {
|
||||
log.Printf("SSL certificate monitoring enabled")
|
||||
}
|
||||
log.Printf("Endpoints:")
|
||||
log.Printf(" POST /operation - Full operation test (ping, dns, tcp, http)")
|
||||
log.Printf(" POST /operation - Full operation test (ping, dns, tcp, http, ssl)")
|
||||
log.Printf(" GET /operation/quick?type=<type>&host=<host> - Quick operation test")
|
||||
log.Printf(" POST /ping - Legacy ping endpoint")
|
||||
log.Printf(" GET /ping/quick?host=<host> - Legacy quick ping test")
|
||||
log.Printf(" GET /health - Health check")
|
||||
log.Printf("Supported operations: ping, dns, tcp, http")
|
||||
log.Printf("Supported operations: ping, dns, tcp, http, ssl")
|
||||
|
||||
// Setup graceful shutdown
|
||||
c := make(chan os.Signal, 1)
|
||||
@@ -77,10 +86,13 @@ func main() {
|
||||
|
||||
go func() {
|
||||
<-c
|
||||
log.Println("Shutting down monitoring service...")
|
||||
log.Println("Shutting down monitoring services...")
|
||||
if monitoringService != nil {
|
||||
monitoringService.Stop()
|
||||
}
|
||||
if sslMonitoringService != nil {
|
||||
sslMonitoringService.Stop()
|
||||
}
|
||||
log.Println("Service stopped")
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"service-operation/operations"
|
||||
"service-operation/pocketbase"
|
||||
"service-operation/types"
|
||||
)
|
||||
|
||||
type SSLMonitoringService struct {
|
||||
pbClient *pocketbase.PocketBaseClient
|
||||
stopChan chan bool
|
||||
retryQueue map[string]int // Track retry count per certificate
|
||||
maxRetries int
|
||||
}
|
||||
|
||||
func NewSSLMonitoringService(pbClient *pocketbase.PocketBaseClient) *SSLMonitoringService {
|
||||
return &SSLMonitoringService{
|
||||
pbClient: pbClient,
|
||||
stopChan: make(chan bool),
|
||||
retryQueue: make(map[string]int),
|
||||
maxRetries: 3,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSLMonitoringService) Start() {
|
||||
ticker := time.NewTicker(1 * time.Minute) // Check every minute for scheduling
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Println("SSL monitoring service started with interval and check_at scheduling")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.checkSSLCertificates()
|
||||
case <-s.stopChan:
|
||||
log.Println("SSL monitoring service stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSLMonitoringService) Stop() {
|
||||
s.stopChan <- true
|
||||
}
|
||||
|
||||
func (s *SSLMonitoringService) checkSSLCertificates() {
|
||||
//log.Println("Fetching SSL certificates from PocketBase...")
|
||||
|
||||
certificates, err := s.pbClient.GetSSLCertificates()
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch SSL certificates: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
//log.Printf("Found %d SSL certificates to check", len(certificates))
|
||||
|
||||
for _, cert := range certificates {
|
||||
if s.shouldCheckCertificate(cert) {
|
||||
go s.checkSingleCertificateWithRetry(cert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSLMonitoringService) shouldCheckCertificate(cert types.SSLCertificate) bool {
|
||||
now := time.Now()
|
||||
|
||||
// Priority 1: Check if check_at is set and is due
|
||||
if cert.CheckAt != "" {
|
||||
if checkAt, err := s.parseFlexibleTime(cert.CheckAt); err == nil {
|
||||
if now.After(checkAt) || now.Equal(checkAt) {
|
||||
log.Printf("Certificate %s is due for manual check (check_at: %s)",
|
||||
cert.Domain, checkAt.Format("2006-01-02 15:04:05"))
|
||||
return true
|
||||
} else {
|
||||
//log.Printf("Certificate %s scheduled for later check (check_at: %s)",
|
||||
//cert.Domain, checkAt.Format("2006-01-02 15:04:05"))
|
||||
//return false
|
||||
}
|
||||
} else {
|
||||
log.Printf("Error parsing check_at for %s: %v", cert.Domain, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Check based on check_interval (in days) from last update
|
||||
if cert.Updated == "" {
|
||||
log.Printf("Certificate %s has never been checked, scheduling check", cert.Domain)
|
||||
return true
|
||||
}
|
||||
|
||||
// Parse last check time from updated field
|
||||
lastCheck, err := s.parseFlexibleTime(cert.Updated)
|
||||
if err != nil {
|
||||
log.Printf("Error parsing last check time for %s, scheduling check: %v", cert.Domain, err)
|
||||
return true
|
||||
}
|
||||
|
||||
// Get check interval in days (default to 1 day if not set or invalid)
|
||||
checkIntervalDays := cert.CheckInterval
|
||||
if checkIntervalDays <= 0 {
|
||||
checkIntervalDays = 1 // Default to 1 day
|
||||
}
|
||||
|
||||
// Adjust check interval based on certificate status for critical certificates
|
||||
adjustedIntervalDays := s.adjustCheckIntervalDays(cert, checkIntervalDays)
|
||||
|
||||
// Calculate next check time based on days
|
||||
nextCheck := lastCheck.Add(time.Duration(adjustedIntervalDays) * 24 * time.Hour)
|
||||
shouldCheck := now.After(nextCheck)
|
||||
|
||||
if shouldCheck {
|
||||
log.Printf("Certificate %s is due for interval check (last: %s, interval: %d days)",
|
||||
cert.Domain, lastCheck.Format("2006-01-02 15:04:05"), adjustedIntervalDays)
|
||||
} else {
|
||||
//log.Printf("Certificate %s not due yet (next check: %s, interval: %d days)",
|
||||
//cert.Domain, nextCheck.Format("2006-01-02 15:04:05"), adjustedIntervalDays)
|
||||
}
|
||||
|
||||
return shouldCheck
|
||||
}
|
||||
|
||||
// parseFlexibleTime tries multiple time formats to parse timestamps
|
||||
func (s *SSLMonitoringService) parseFlexibleTime(timeStr string) (time.Time, error) {
|
||||
formats := []string{
|
||||
time.RFC3339,
|
||||
time.RFC3339Nano,
|
||||
"2006-01-02 15:04:05.999Z", // ISO 8601 with milliseconds (PocketBase format)
|
||||
"2006-01-02 15:04:05.999999Z", // ISO 8601 with microseconds
|
||||
"2006-01-02 15:04:05Z", // ISO 8601 without milliseconds
|
||||
"2006-01-02T15:04:05.999Z", // RFC3339 with milliseconds
|
||||
"2006-01-02T15:04:05.999999Z", // RFC3339 with microseconds
|
||||
"2006-01-02T15:04:05Z", // RFC3339 without milliseconds
|
||||
"2006-01-02 15:04:05.999999999 -0700 MST",
|
||||
"2006-01-02 15:04:05.999999 -0700 MST",
|
||||
"2006-01-02 15:04:05 -0700 MST",
|
||||
"2006-01-02 15:04:05.999999999",
|
||||
"2006-01-02 15:04:05.999999",
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02T15:04:05.999999999Z",
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if t, err := time.Parse(format, timeStr); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("unable to parse time string: %s", timeStr)
|
||||
}
|
||||
|
||||
// adjustCheckIntervalDays adjusts the check interval based on certificate status and days left
|
||||
func (s *SSLMonitoringService) adjustCheckIntervalDays(cert types.SSLCertificate, defaultIntervalDays int) int {
|
||||
// Check more frequently for certificates that are expiring soon or have errors
|
||||
if cert.DaysLeft <= 7 {
|
||||
return 1 // Check daily for certificates expiring within 7 days
|
||||
} else if cert.DaysLeft <= 30 {
|
||||
// Check every 2 days for certificates expiring within 30 days
|
||||
if defaultIntervalDays > 2 {
|
||||
return 2
|
||||
}
|
||||
} else if cert.Status == "error" {
|
||||
return 1 // Check daily for certificates with errors
|
||||
}
|
||||
|
||||
return defaultIntervalDays
|
||||
}
|
||||
|
||||
func (s *SSLMonitoringService) checkSingleCertificateWithRetry(cert types.SSLCertificate) {
|
||||
retryCount := s.retryQueue[cert.ID]
|
||||
|
||||
log.Printf("🔍 Checking SSL certificate for domain: %s (attempt %d/%d)",
|
||||
cert.Domain, retryCount+1, s.maxRetries+1)
|
||||
|
||||
result, err := s.performSSLCheck(cert.Domain)
|
||||
|
||||
if err != nil && retryCount < s.maxRetries {
|
||||
// Increment retry count and schedule retry
|
||||
s.retryQueue[cert.ID] = retryCount + 1
|
||||
log.Printf("SSL check failed for %s, will retry (%d/%d): %v",
|
||||
cert.Domain, retryCount+1, s.maxRetries, err)
|
||||
|
||||
// Schedule retry with exponential backoff
|
||||
go func() {
|
||||
backoffDuration := time.Duration((retryCount + 1) * 30) * time.Second
|
||||
time.Sleep(backoffDuration)
|
||||
s.checkSingleCertificateWithRetry(cert)
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
// Reset retry count on success or max retries reached
|
||||
delete(s.retryQueue, cert.ID)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("❌ SSL check failed for domain %s after %d attempts: %v",
|
||||
cert.Domain, s.maxRetries+1, err)
|
||||
s.updateCertificateWithError(cert, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update certificate with successful results
|
||||
s.updateCertificateWithResults(cert, result)
|
||||
}
|
||||
|
||||
func (s *SSLMonitoringService) performSSLCheck(domain string) (*types.OperationResult, error) {
|
||||
log.Printf("Performing SSL check for domain: %s", domain)
|
||||
sslOp := operations.NewSSLOperation(30 * time.Second)
|
||||
result, err := sslOp.Execute(domain)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("SSL operation failed for %s: %v", domain, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
log.Printf("SSL operation returned nil result for %s", domain)
|
||||
return nil, fmt.Errorf("SSL check returned nil result")
|
||||
}
|
||||
|
||||
log.Printf("SSL check completed for %s: success=%v, days_left=%d",
|
||||
domain, result.Success, result.SSLDaysLeft)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *SSLMonitoringService) updateCertificateWithError(cert types.SSLCertificate, err error) {
|
||||
log.Printf("Updating certificate %s with error status", cert.Domain)
|
||||
|
||||
updateData := map[string]interface{}{
|
||||
"status": "error",
|
||||
"updated": time.Now().Format(time.RFC3339),
|
||||
"error_message": err.Error(),
|
||||
}
|
||||
|
||||
// Calculate next check time based on check_interval (in days) with shorter interval for errors
|
||||
checkIntervalDays := cert.CheckInterval
|
||||
if checkIntervalDays <= 0 {
|
||||
checkIntervalDays = 1
|
||||
}
|
||||
// For errors, check again in half the normal interval (minimum 1 day)
|
||||
errorIntervalDays := checkIntervalDays / 2
|
||||
if errorIntervalDays < 1 {
|
||||
errorIntervalDays = 1
|
||||
}
|
||||
|
||||
nextCheck := time.Now().Add(time.Duration(errorIntervalDays) * 24 * time.Hour)
|
||||
updateData["check_at"] = nextCheck.Format(time.RFC3339)
|
||||
|
||||
if updateErr := s.pbClient.UpdateSSLCertificate(cert.ID, updateData); updateErr != nil {
|
||||
log.Printf("Failed to update SSL certificate %s with error status: %v", cert.ID, updateErr)
|
||||
} else {
|
||||
log.Printf("📝 Updated certificate %s with error status (next check in %d days)",
|
||||
cert.Domain, errorIntervalDays)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSLMonitoringService) updateCertificateWithResults(cert types.SSLCertificate, result *types.OperationResult) {
|
||||
status := getSSLStatus(result)
|
||||
|
||||
log.Printf("Updating certificate %s with results: status=%s, days_left=%d, issuer=%s",
|
||||
cert.Domain, status, result.SSLDaysLeft, result.SSLIssuer)
|
||||
|
||||
updateData := map[string]interface{}{
|
||||
"status": status,
|
||||
"valid_from": result.SSLValidFrom.Format(time.RFC3339),
|
||||
"valid_till": result.SSLValidTill.Format(time.RFC3339),
|
||||
"days_left": result.SSLDaysLeft,
|
||||
"valid_days_to_expire": result.SSLDaysLeft,
|
||||
"resolved_ip": result.SSLResolvedIP,
|
||||
"issuer_cn": result.SSLIssuer, // Now contains organization name like "Google Trust Services"
|
||||
"issued_to": result.SSLSubject, // Now contains organization name
|
||||
"serial_number": result.SSLSerialNumber,
|
||||
"cert_alg": result.SSLAlgorithm,
|
||||
"cert_sans": result.SSLSANs,
|
||||
"updated": time.Now().Format(time.RFC3339),
|
||||
"error_message": "", // Clear any previous error
|
||||
}
|
||||
|
||||
// Calculate next check time based on check_interval (in days) and certificate status
|
||||
checkIntervalDays := cert.CheckInterval
|
||||
if checkIntervalDays <= 0 {
|
||||
checkIntervalDays = 1 // Default to 1 day
|
||||
}
|
||||
|
||||
adjustedIntervalDays := s.adjustCheckIntervalDays(cert, checkIntervalDays)
|
||||
nextCheck := time.Now().Add(time.Duration(adjustedIntervalDays) * 24 * time.Hour)
|
||||
updateData["check_at"] = nextCheck.Format(time.RFC3339)
|
||||
|
||||
if err := s.pbClient.UpdateSSLCertificate(cert.ID, updateData); err != nil {
|
||||
log.Printf("Failed to update SSL certificate %s: %v", cert.ID, err)
|
||||
} else {
|
||||
log.Printf("✅ SSL certificate updated for %s: %s (%d days left, issuer: %s, next check in %d days)",
|
||||
cert.Domain, status, result.SSLDaysLeft, result.SSLIssuer, adjustedIntervalDays)
|
||||
}
|
||||
}
|
||||
|
||||
func getSSLStatus(result *types.OperationResult) string {
|
||||
if !result.Success {
|
||||
return "error"
|
||||
}
|
||||
|
||||
if result.SSLDaysLeft <= 0 {
|
||||
return "expired"
|
||||
} else if result.SSLDaysLeft <= 7 {
|
||||
return "critical" // Very urgent
|
||||
} else if result.SSLDaysLeft <= 30 {
|
||||
return "expiring_soon"
|
||||
}
|
||||
|
||||
return "valid"
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
package operations
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// extractSANs extracts Subject Alternative Names from certificate
|
||||
func (op *SSLOperation) extractSANs(cert *x509.Certificate) []string {
|
||||
sans := make([]string, 0)
|
||||
|
||||
// Add DNS names
|
||||
sans = append(sans, cert.DNSNames...)
|
||||
|
||||
// Add IP addresses
|
||||
for _, ip := range cert.IPAddresses {
|
||||
sans = append(sans, ip.String())
|
||||
}
|
||||
|
||||
// Add email addresses
|
||||
sans = append(sans, cert.EmailAddresses...)
|
||||
|
||||
// Add URIs
|
||||
for _, uri := range cert.URIs {
|
||||
sans = append(sans, uri.String())
|
||||
}
|
||||
|
||||
return sans
|
||||
}
|
||||
|
||||
// getResolvedIP resolves the domain to its IP address
|
||||
func (op *SSLOperation) getResolvedIP(hostname string) string {
|
||||
ips, err := net.LookupIP(hostname)
|
||||
if err != nil || len(ips) == 0 {
|
||||
return ""
|
||||
}
|
||||
return ips[0].String()
|
||||
}
|
||||
|
||||
// getCertificateAlgorithm returns detailed algorithm information
|
||||
func (op *SSLOperation) getCertificateAlgorithm(cert *x509.Certificate) string {
|
||||
algorithm := cert.SignatureAlgorithm.String()
|
||||
|
||||
// Add key size information if available
|
||||
switch pub := cert.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
algorithm += fmt.Sprintf(" (RSA %d-bit)", pub.N.BitLen())
|
||||
case *ecdsa.PublicKey:
|
||||
algorithm += fmt.Sprintf(" (ECDSA %d-bit)", pub.Curve.Params().BitSize)
|
||||
}
|
||||
|
||||
return algorithm
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
|
||||
package operations
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service-operation/types"
|
||||
)
|
||||
|
||||
type SSLOperation struct {
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewSSLOperation(timeout time.Duration) *SSLOperation {
|
||||
return &SSLOperation{
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (op *SSLOperation) Execute(domain string) (*types.OperationResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// Clean and normalize domain
|
||||
domain = op.normalizeDomain(domain)
|
||||
|
||||
// Validate domain format
|
||||
if domain == "" {
|
||||
return op.createErrorResult(domain, startTime, "domain cannot be empty")
|
||||
}
|
||||
|
||||
// Add port if not present
|
||||
host := domain
|
||||
if !strings.Contains(host, ":") {
|
||||
host = host + ":443"
|
||||
}
|
||||
|
||||
// Set up TLS connection with timeout
|
||||
dialer := &net.Dialer{
|
||||
Timeout: op.timeout,
|
||||
}
|
||||
|
||||
// Create TLS config with proper verification
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: strings.Split(host, ":")[0],
|
||||
InsecureSkipVerify: false,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
// Attempt TLS connection
|
||||
conn, err := tls.DialWithDialer(dialer, "tcp", host, tlsConfig)
|
||||
|
||||
endTime := time.Now()
|
||||
responseTime := endTime.Sub(startTime)
|
||||
|
||||
if err != nil {
|
||||
return &types.OperationResult{
|
||||
Type: types.OperationSSL,
|
||||
Host: strings.Split(host, ":")[0],
|
||||
Success: false,
|
||||
ResponseTime: responseTime,
|
||||
Error: fmt.Sprintf("TLS connection failed: %v", err),
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
}, nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Get certificate chain information
|
||||
state := conn.ConnectionState()
|
||||
if len(state.PeerCertificates) == 0 {
|
||||
return &types.OperationResult{
|
||||
Type: types.OperationSSL,
|
||||
Host: strings.Split(host, ":")[0],
|
||||
Success: false,
|
||||
ResponseTime: responseTime,
|
||||
Error: "No certificates found in chain",
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
cert := state.PeerCertificates[0]
|
||||
hostname := strings.Split(host, ":")[0]
|
||||
|
||||
// Perform comprehensive certificate validation
|
||||
validationError := op.validateCertificate(cert, hostname)
|
||||
|
||||
// Calculate days left until expiration
|
||||
daysLeft := int(time.Until(cert.NotAfter).Hours() / 24)
|
||||
|
||||
// Extract Subject Alternative Names
|
||||
sans := op.extractSANs(cert)
|
||||
|
||||
// Get resolved IP address
|
||||
resolvedIP := op.getResolvedIP(hostname)
|
||||
|
||||
// Extract certificate algorithm information
|
||||
algorithm := op.getCertificateAlgorithm(cert)
|
||||
|
||||
// Extract issuer organization (O=) instead of full distinguished name
|
||||
issuerOrganization := op.extractIssuerOrganization(cert.Issuer)
|
||||
|
||||
// Extract subject organization (O=) instead of full distinguished name
|
||||
subjectOrganization := op.extractSubjectOrganization(cert.Subject)
|
||||
|
||||
// Check if certificate is valid (not expired and passes validation)
|
||||
isValid := validationError == nil && time.Now().Before(cert.NotAfter) && time.Now().After(cert.NotBefore)
|
||||
|
||||
// Build detailed error message if validation failed
|
||||
errorMsg := ""
|
||||
if validationError != nil {
|
||||
errorMsg = validationError.Error()
|
||||
} else if time.Now().After(cert.NotAfter) {
|
||||
errorMsg = "Certificate has expired"
|
||||
} else if time.Now().Before(cert.NotBefore) {
|
||||
errorMsg = "Certificate is not yet valid"
|
||||
}
|
||||
|
||||
// Create comprehensive result
|
||||
result := &types.OperationResult{
|
||||
Type: types.OperationSSL,
|
||||
Host: hostname,
|
||||
Success: isValid,
|
||||
ResponseTime: responseTime,
|
||||
Error: errorMsg,
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
|
||||
// SSL specific fields - using organization instead of full DN
|
||||
SSLValidFrom: cert.NotBefore,
|
||||
SSLValidTill: cert.NotAfter,
|
||||
SSLDaysLeft: daysLeft,
|
||||
SSLIssuer: issuerOrganization, // Now shows "Google Trust Services" instead of full DN
|
||||
SSLSubject: subjectOrganization, // Now shows organization instead of full DN
|
||||
SSLSerialNumber: cert.SerialNumber.String(),
|
||||
SSLAlgorithm: algorithm,
|
||||
SSLSANs: strings.Join(sans, ","),
|
||||
SSLResolvedIP: resolvedIP,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// createErrorResult creates a standardized error result
|
||||
func (op *SSLOperation) createErrorResult(domain string, startTime time.Time, errorMsg string) (*types.OperationResult, error) {
|
||||
return &types.OperationResult{
|
||||
Type: types.OperationSSL,
|
||||
Host: domain,
|
||||
Success: false,
|
||||
ResponseTime: time.Since(startTime),
|
||||
Error: errorMsg,
|
||||
StartTime: startTime,
|
||||
EndTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
|
||||
package operations
|
||||
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// normalizeDomain cleans and normalizes the domain input
|
||||
func (op *SSLOperation) normalizeDomain(domain string) string {
|
||||
// Remove protocol prefixes
|
||||
domain = strings.Replace(domain, "https://", "", 1)
|
||||
domain = strings.Replace(domain, "http://", "", 1)
|
||||
|
||||
// Remove trailing slash and path
|
||||
if idx := strings.Index(domain, "/"); idx != -1 {
|
||||
domain = domain[:idx]
|
||||
}
|
||||
|
||||
// Trim whitespace
|
||||
domain = strings.TrimSpace(domain)
|
||||
|
||||
return domain
|
||||
}
|
||||
|
||||
// formatDistinguishedName formats the certificate distinguished name
|
||||
func (op *SSLOperation) formatDistinguishedName(name pkix.Name) string {
|
||||
var parts []string
|
||||
|
||||
if name.CommonName != "" {
|
||||
parts = append(parts, fmt.Sprintf("CN=%s", name.CommonName))
|
||||
}
|
||||
|
||||
for _, org := range name.Organization {
|
||||
parts = append(parts, fmt.Sprintf("O=%s", org))
|
||||
}
|
||||
|
||||
for _, orgUnit := range name.OrganizationalUnit {
|
||||
parts = append(parts, fmt.Sprintf("OU=%s", orgUnit))
|
||||
}
|
||||
|
||||
for _, country := range name.Country {
|
||||
parts = append(parts, fmt.Sprintf("C=%s", country))
|
||||
}
|
||||
|
||||
for _, locality := range name.Locality {
|
||||
parts = append(parts, fmt.Sprintf("L=%s", locality))
|
||||
}
|
||||
|
||||
for _, province := range name.Province {
|
||||
parts = append(parts, fmt.Sprintf("ST=%s", province))
|
||||
}
|
||||
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// extractIssuerOrganization extracts only the organization (O=) from the issuer distinguished name
|
||||
func (op *SSLOperation) extractIssuerOrganization(name pkix.Name) string {
|
||||
// Return the first organization if available
|
||||
if len(name.Organization) > 0 {
|
||||
return name.Organization[0]
|
||||
}
|
||||
|
||||
// Fallback to Common Name if no organization
|
||||
if name.CommonName != "" {
|
||||
return name.CommonName
|
||||
}
|
||||
|
||||
// Last resort fallback
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// extractSubjectOrganization extracts only the organization (O=) from the subject distinguished name
|
||||
func (op *SSLOperation) extractSubjectOrganization(name pkix.Name) string {
|
||||
// Return the first organization if available
|
||||
if len(name.Organization) > 0 {
|
||||
return name.Organization[0]
|
||||
}
|
||||
|
||||
// Fallback to Common Name if no organization
|
||||
if name.CommonName != "" {
|
||||
return name.CommonName
|
||||
}
|
||||
|
||||
// Last resort fallback
|
||||
return "Unknown"
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package operations
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// validateCertificate performs comprehensive certificate validation
|
||||
func (op *SSLOperation) validateCertificate(cert *x509.Certificate, hostname string) error {
|
||||
now := time.Now()
|
||||
|
||||
// Check if certificate is expired or not yet valid
|
||||
if now.Before(cert.NotBefore) {
|
||||
return fmt.Errorf("certificate is not yet valid (valid from: %v)", cert.NotBefore.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
if now.After(cert.NotAfter) {
|
||||
return fmt.Errorf("certificate has expired (expired on: %v)", cert.NotAfter.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
// Verify hostname matches certificate
|
||||
if err := cert.VerifyHostname(hostname); err != nil {
|
||||
return fmt.Errorf("hostname verification failed: %v", err)
|
||||
}
|
||||
|
||||
// Check key usage - certificates should have digital signature capability
|
||||
if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
|
||||
return fmt.Errorf("certificate missing required digital signature key usage")
|
||||
}
|
||||
|
||||
// Check if certificate is self-signed (basic check)
|
||||
if cert.Issuer.CommonName == cert.Subject.CommonName && len(cert.Subject.Organization) == 0 {
|
||||
return fmt.Errorf("certificate appears to be self-signed")
|
||||
}
|
||||
|
||||
// Validate certificate chain if intermediate certificates are present
|
||||
if err := op.validateCertificateChain(cert); err != nil {
|
||||
return fmt.Errorf("certificate chain validation failed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateCertificateChain performs basic certificate chain validation
|
||||
func (op *SSLOperation) validateCertificateChain(cert *x509.Certificate) error {
|
||||
// Check if the certificate has proper extensions for SSL/TLS
|
||||
hasServerAuth := false
|
||||
for _, usage := range cert.ExtKeyUsage {
|
||||
if usage == x509.ExtKeyUsageServerAuth {
|
||||
hasServerAuth = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasServerAuth {
|
||||
return fmt.Errorf("certificate does not have server authentication extension")
|
||||
}
|
||||
|
||||
// Check certificate version (should be v3 for modern certificates)
|
||||
if cert.Version < 3 {
|
||||
return fmt.Errorf("certificate version %d is outdated (should be v3)", cert.Version)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
|
||||
package pocketbase
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"service-operation/types"
|
||||
)
|
||||
|
||||
type SSLCertificatesResponse struct {
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
TotalItems int `json:"totalItems"`
|
||||
TotalPages int `json:"totalPages"`
|
||||
Items []types.SSLCertificate `json:"items"`
|
||||
}
|
||||
|
||||
func (c *PocketBaseClient) GetSSLCertificates() ([]types.SSLCertificate, error) {
|
||||
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records", c.baseURL)
|
||||
|
||||
resp, err := c.httpClient.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch SSL certificates: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("PocketBase returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var response SSLCertificatesResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode SSL certificates response: %v", err)
|
||||
}
|
||||
|
||||
return response.Items, nil
|
||||
}
|
||||
|
||||
func (c *PocketBaseClient) UpdateSSLCertificate(id string, data map[string]interface{}) error {
|
||||
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records/%s", c.baseURL, id)
|
||||
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal SSL certificate data: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create SSL certificate update request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update SSL certificate: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to update SSL certificate, status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PocketBaseClient) GetSSLCertificateByID(id string) (*types.SSLCertificate, error) {
|
||||
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records/%s", c.baseURL, id)
|
||||
|
||||
resp, err := c.httpClient.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch SSL certificate: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("PocketBase returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var cert types.SSLCertificate
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cert); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode SSL certificate response: %v", err)
|
||||
}
|
||||
|
||||
return &cert, nil
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
package types
|
||||
|
||||
import "time"
|
||||
@@ -10,6 +9,7 @@ const (
|
||||
OperationDNS OperationType = "dns"
|
||||
OperationTCP OperationType = "tcp"
|
||||
OperationHTTP OperationType = "http"
|
||||
OperationSSL OperationType = "ssl"
|
||||
)
|
||||
|
||||
type OperationRequest struct {
|
||||
@@ -56,6 +56,17 @@ type OperationResult struct {
|
||||
ContentLength int64 `json:"content_length,omitempty"`
|
||||
ResponseBody string `json:"response_body,omitempty"`
|
||||
|
||||
// SSL specific fields
|
||||
SSLValidFrom time.Time `json:"ssl_valid_from,omitempty"`
|
||||
SSLValidTill time.Time `json:"ssl_valid_till,omitempty"`
|
||||
SSLDaysLeft int `json:"ssl_days_left,omitempty"`
|
||||
SSLIssuer string `json:"ssl_issuer,omitempty"`
|
||||
SSLSubject string `json:"ssl_subject,omitempty"`
|
||||
SSLSerialNumber string `json:"ssl_serial_number,omitempty"`
|
||||
SSLAlgorithm string `json:"ssl_algorithm,omitempty"`
|
||||
SSLSANs string `json:"ssl_sans,omitempty"`
|
||||
SSLResolvedIP string `json:"ssl_resolved_ip,omitempty"`
|
||||
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SSLCertificate struct {
|
||||
ID string `json:"id"`
|
||||
CollectionID string `json:"collectionId"`
|
||||
CollectionName string `json:"collectionName"`
|
||||
Domain string `json:"domain"`
|
||||
IssuerO string `json:"issuer_o"`
|
||||
Status string `json:"status"`
|
||||
LastNotified string `json:"last_notified"`
|
||||
WarningThreshold int `json:"warning_threshold"`
|
||||
ExpiryThreshold int `json:"expiry_threshold"`
|
||||
NotificationChannel string `json:"notification_channel"`
|
||||
ValidFrom string `json:"valid_from"`
|
||||
SerialNumber string `json:"serial_number"` // Changed to string to handle large numbers
|
||||
IssuedTo string `json:"issued_to"`
|
||||
ValidTill string `json:"valid_till"`
|
||||
ValidityDays int `json:"validity_days"`
|
||||
DaysLeft int `json:"days_left"`
|
||||
ValidDaysToExpire int `json:"valid_days_to_expire"`
|
||||
ResolvedIP string `json:"resolved_ip"`
|
||||
IssuerCN string `json:"issuer_cn"`
|
||||
CertAlg string `json:"cert_alg"`
|
||||
CertSans string `json:"cert_sans"`
|
||||
CheckInterval int `json:"check_interval"`
|
||||
CheckAt string `json:"check_at"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
}
|
||||
|
||||
// Custom unmarshaler to handle check_interval as both string and int, and serial_number as string
|
||||
func (s *SSLCertificate) UnmarshalJSON(data []byte) error {
|
||||
// Create a temporary struct with flexible types
|
||||
type Alias SSLCertificate
|
||||
aux := &struct {
|
||||
CheckInterval interface{} `json:"check_interval"`
|
||||
SerialNumber interface{} `json:"serial_number"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(s),
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle check_interval conversion
|
||||
switch v := aux.CheckInterval.(type) {
|
||||
case string:
|
||||
if v == "" {
|
||||
s.CheckInterval = 1440 // Default 24 hours in minutes
|
||||
} else {
|
||||
interval, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
s.CheckInterval = 1440 // Default on error
|
||||
} else {
|
||||
s.CheckInterval = interval
|
||||
}
|
||||
}
|
||||
case float64:
|
||||
s.CheckInterval = int(v)
|
||||
case int:
|
||||
s.CheckInterval = v
|
||||
default:
|
||||
s.CheckInterval = 1440 // Default 24 hours in minutes
|
||||
}
|
||||
|
||||
// Handle serial_number conversion to string
|
||||
switch v := aux.SerialNumber.(type) {
|
||||
case string:
|
||||
s.SerialNumber = v
|
||||
case float64:
|
||||
// Handle scientific notation by converting to string
|
||||
s.SerialNumber = strconv.FormatFloat(v, 'f', 0, 64)
|
||||
case int64:
|
||||
s.SerialNumber = strconv.FormatInt(v, 10)
|
||||
case int:
|
||||
s.SerialNumber = strconv.Itoa(v)
|
||||
default:
|
||||
s.SerialNumber = ""
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type SSLCheckResult struct {
|
||||
Domain string `json:"domain"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ValidFrom time.Time `json:"valid_from"`
|
||||
ValidTill time.Time `json:"valid_till"`
|
||||
DaysLeft int `json:"days_left"`
|
||||
Issuer string `json:"issuer"`
|
||||
Subject string `json:"subject"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
SANs []string `json:"sans"`
|
||||
ResponseTime time.Duration `json:"response_time"`
|
||||
ResolvedIP string `json:"resolved_ip"`
|
||||
CheckedAt time.Time `json:"checked_at"`
|
||||
}
|
||||
Reference in New Issue
Block a user