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'),
|
date: format(timestamp, 'MMM dd, yyyy'),
|
||||||
value: data.status === "paused" ? null : data.responseTime,
|
value: data.status === "paused" ? null : data.responseTime,
|
||||||
status: data.status,
|
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]);
|
}, [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
|
// Create a custom tooltip for the chart
|
||||||
const CustomTooltip = ({ active, payload, label }: any) => {
|
const CustomTooltip = ({ active, payload, label }: any) => {
|
||||||
if (active && payload && payload.length) {
|
if (active && payload && payload.length) {
|
||||||
@@ -79,31 +100,6 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
|||||||
return null;
|
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
|
// Check if we have any data to display - be more lenient by checking raw uptimeData
|
||||||
const hasData = uptimeData.length > 0;
|
const hasData = uptimeData.length > 0;
|
||||||
|
|
||||||
@@ -172,46 +168,40 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
|||||||
<YAxis
|
<YAxis
|
||||||
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
|
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
|
||||||
allowDecimals={false}
|
allowDecimals={false}
|
||||||
domain={['dataMin - 10', 'dataMax + 10']}
|
domain={yAxisDomain}
|
||||||
/>
|
/>
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<Tooltip content={<CustomTooltip />} />
|
||||||
|
|
||||||
{/* Area charts for different statuses */}
|
{/* Separate area charts for each status - positioned closer together */}
|
||||||
{segments.up.length > 0 && (
|
<Area
|
||||||
<Area
|
type="monotone"
|
||||||
type="monotone"
|
dataKey="upValue"
|
||||||
dataKey="value"
|
stroke="#10b981"
|
||||||
data={segments.up}
|
strokeWidth={2}
|
||||||
stroke="#10b981"
|
fillOpacity={0.4}
|
||||||
fillOpacity={1}
|
fill="url(#colorUp)"
|
||||||
fill="url(#colorUp)"
|
connectNulls={false}
|
||||||
connectNulls
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{segments.down.length > 0 && (
|
<Area
|
||||||
<Area
|
type="monotone"
|
||||||
type="monotone"
|
dataKey="downValue"
|
||||||
dataKey="value"
|
stroke="#ef4444"
|
||||||
data={segments.down}
|
strokeWidth={2}
|
||||||
stroke="#ef4444"
|
fillOpacity={0.4}
|
||||||
fillOpacity={1}
|
fill="url(#colorDown)"
|
||||||
fill="url(#colorDown)"
|
connectNulls={false}
|
||||||
connectNulls
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{segments.warning.length > 0 && (
|
<Area
|
||||||
<Area
|
type="monotone"
|
||||||
type="monotone"
|
dataKey="warningValue"
|
||||||
dataKey="value"
|
stroke="#f59e0b"
|
||||||
data={segments.warning}
|
strokeWidth={2}
|
||||||
stroke="#f59e0b"
|
fillOpacity={0.4}
|
||||||
fillOpacity={1}
|
fill="url(#colorWarning)"
|
||||||
fill="url(#colorWarning)"
|
connectNulls={false}
|
||||||
connectNulls
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Add reference lines for paused periods */}
|
{/* Add reference lines for paused periods */}
|
||||||
{chartData.map((entry, index) =>
|
{chartData.map((entry, index) =>
|
||||||
@@ -231,4 +221,4 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+3
-2
@@ -55,7 +55,8 @@ export const useRealTimeUpdates = ({
|
|||||||
setUptimeData(prev => {
|
setUptimeData(prev => {
|
||||||
const newData: UptimeData = {
|
const newData: UptimeData = {
|
||||||
id: e.record.id,
|
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,
|
timestamp: e.record.timestamp,
|
||||||
status: e.record.status,
|
status: e.record.status,
|
||||||
responseTime: e.record.response_time || 0,
|
responseTime: e.record.response_time || 0,
|
||||||
@@ -84,4 +85,4 @@ export const useRealTimeUpdates = ({
|
|||||||
console.error("Error setting up real-time updates:", error);
|
console.error("Error setting up real-time updates:", error);
|
||||||
}
|
}
|
||||||
}, [serviceId, startDate, endDate, setService, setUptimeData]);
|
}, [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) => ({
|
const placeholderHistory: UptimeData[] = Array(20).fill(null).map((_, index) => ({
|
||||||
id: `placeholder-${serviceId}-${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(),
|
timestamp: new Date(Date.now() - (index * interval * 1000)).toISOString(),
|
||||||
status: statusValue as "up" | "down" | "warning" | "paused",
|
status: statusValue as "up" | "down" | "warning" | "paused",
|
||||||
responseTime: 0
|
responseTime: 0
|
||||||
@@ -96,7 +97,8 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: `padding-${serviceId}-${index}`,
|
id: `padding-${serviceId}-${index}`,
|
||||||
serviceId: serviceId || "",
|
service_id: serviceId || "", // Include service_id
|
||||||
|
serviceId: serviceId || "", // Keep for backward compatibility
|
||||||
timestamp: new Date(baseTime - timeOffset).toISOString(),
|
timestamp: new Date(baseTime - timeOffset).toISOString(),
|
||||||
status: lastStatus,
|
status: lastStatus,
|
||||||
responseTime: 0
|
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("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("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'}>{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>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -52,10 +54,28 @@ export function IncidentTable({ incidents }: IncidentTableProps) {
|
|||||||
? `${check.responseTime}ms`
|
? `${check.responseTime}ms`
|
||||||
: "N/A"}
|
: "N/A"}
|
||||||
</TableCell>
|
</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>
|
</TableRow>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -18,7 +19,8 @@ const formSchema = z.object({
|
|||||||
domain: z.string().min(1, "Domain is required"),
|
domain: z.string().min(1, "Domain is required"),
|
||||||
warning_threshold: z.coerce.number().int().min(1).max(365),
|
warning_threshold: z.coerce.number().int().min(1).max(365),
|
||||||
expiry_threshold: z.coerce.number().int().min(1).max(30),
|
expiry_threshold: z.coerce.number().int().min(1).max(30),
|
||||||
notification_channel: z.string().min(1, "Notification channel is required")
|
notification_channel: z.string().min(1, "Notification channel is required"),
|
||||||
|
check_interval: z.coerce.number().int().min(1).max(30).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
interface AddSSLCertificateFormProps {
|
interface AddSSLCertificateFormProps {
|
||||||
@@ -42,7 +44,8 @@ export const AddSSLCertificateForm = ({
|
|||||||
domain: "",
|
domain: "",
|
||||||
warning_threshold: 30,
|
warning_threshold: 30,
|
||||||
expiry_threshold: 7,
|
expiry_threshold: 7,
|
||||||
notification_channel: ""
|
notification_channel: "",
|
||||||
|
check_interval: 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,7 +92,8 @@ export const AddSSLCertificateForm = ({
|
|||||||
domain: values.domain,
|
domain: values.domain,
|
||||||
warning_threshold: values.warning_threshold,
|
warning_threshold: values.warning_threshold,
|
||||||
expiry_threshold: values.expiry_threshold,
|
expiry_threshold: values.expiry_threshold,
|
||||||
notification_channel: values.notification_channel
|
notification_channel: values.notification_channel,
|
||||||
|
check_interval: values.check_interval
|
||||||
};
|
};
|
||||||
|
|
||||||
await onSubmit(certData);
|
await onSubmit(certData);
|
||||||
@@ -117,34 +121,53 @@ export const AddSSLCertificateForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="warning_threshold"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('warningThreshold')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t('getNotifiedExpiration')}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="expiry_threshold"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('expiryThreshold')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t('getNotifiedCritical')}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="warning_threshold"
|
name="check_interval"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('warningThreshold')}</FormLabel>
|
<FormLabel>Check Interval (Days)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" {...field} />
|
<Input type="number" min="1" max="30" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t('getNotifiedExpiration')}
|
How often to check the SSL certificate (in days)
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="expiry_threshold"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('expiryThreshold')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="number" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t('getNotifiedCritical')}
|
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -17,6 +18,7 @@ const formSchema = z.object({
|
|||||||
warning_threshold: z.coerce.number().min(1, "Warning threshold must be at least 1 day"),
|
warning_threshold: z.coerce.number().min(1, "Warning threshold must be at least 1 day"),
|
||||||
expiry_threshold: z.coerce.number().min(1, "Expiry threshold must be at least 1 day"),
|
expiry_threshold: z.coerce.number().min(1, "Expiry threshold must be at least 1 day"),
|
||||||
notification_channel: z.string().min(1, "Notification channel is required"),
|
notification_channel: z.string().min(1, "Notification channel is required"),
|
||||||
|
check_interval: z.coerce.number().int().min(1).max(30).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormValues = z.infer<typeof formSchema>;
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
@@ -40,6 +42,7 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
|
|||||||
warning_threshold: certificate.warning_threshold,
|
warning_threshold: certificate.warning_threshold,
|
||||||
expiry_threshold: certificate.expiry_threshold,
|
expiry_threshold: certificate.expiry_threshold,
|
||||||
notification_channel: certificate.notification_channel,
|
notification_channel: certificate.notification_channel,
|
||||||
|
check_interval: certificate.check_interval || 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,7 +81,8 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
|
|||||||
...data,
|
...data,
|
||||||
// Ensure values are correctly typed as numbers
|
// Ensure values are correctly typed as numbers
|
||||||
warning_threshold: Number(data.warning_threshold),
|
warning_threshold: Number(data.warning_threshold),
|
||||||
expiry_threshold: Number(data.expiry_threshold)
|
expiry_threshold: Number(data.expiry_threshold),
|
||||||
|
check_interval: data.check_interval ? Number(data.check_interval) : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Submitting updated certificate:", updatedCertificate);
|
console.log("Submitting updated certificate:", updatedCertificate);
|
||||||
@@ -113,7 +117,7 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="warning_threshold"
|
name="warning_threshold"
|
||||||
@@ -157,6 +161,29 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="check_interval"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Check Interval (Days)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="30"
|
||||||
|
placeholder="1"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
How often to check
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
@@ -230,4 +257,4 @@ export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPend
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,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 React, { useState } from "react";
|
||||||
import { format } from "date-fns";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import { Plus } from "lucide-react";
|
||||||
Table,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
TableHead,
|
|
||||||
TableBody,
|
|
||||||
TableCell
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RefreshCw, Eye, Edit, Trash2, MoreHorizontal } from "lucide-react";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { SSLCertificate } from "@/types/ssl.types";
|
|
||||||
import { SSLStatusBadge } from "./SSLStatusBadge";
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
Table,
|
||||||
DropdownMenuContent,
|
TableBody,
|
||||||
DropdownMenuItem,
|
TableCell,
|
||||||
DropdownMenuTrigger,
|
TableHead,
|
||||||
} from "@/components/ui/dropdown-menu";
|
TableHeader,
|
||||||
import { toast } from "sonner";
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { SSLStatusBadge } from "./SSLStatusBadge";
|
||||||
|
import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
|
||||||
|
import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
|
||||||
|
import { SSLCertificateActions } from "./SSLCertificateActions";
|
||||||
|
import { 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 { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface SSLCertificatesTableProps {
|
export const SSLCertificatesTable = () => {
|
||||||
certificates: SSLCertificate[];
|
|
||||||
onRefresh: (id: string) => void;
|
|
||||||
refreshingId: string | null;
|
|
||||||
onEdit?: (certificate: SSLCertificate) => void;
|
|
||||||
onDelete?: (certificate: SSLCertificate) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SSLCertificatesTable = ({
|
|
||||||
certificates,
|
|
||||||
onRefresh,
|
|
||||||
refreshingId,
|
|
||||||
onEdit,
|
|
||||||
onDelete
|
|
||||||
}: SSLCertificatesTableProps) => {
|
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const [selectedCert, setSelectedCert] = useState<SSLCertificate | null>(null);
|
const queryClient = useQueryClient();
|
||||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||||
const [certToDelete, setCertToDelete] = useState<SSLCertificate | null>(null);
|
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||||
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
const formatDate = (dateString: string | undefined) => {
|
const [showViewDialog, setShowViewDialog] = useState(false);
|
||||||
if (!dateString) return t('unknown');
|
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 {
|
try {
|
||||||
const date = new Date(dateString);
|
await addSSLCertificate(data);
|
||||||
if (isNaN(date.getTime())) {
|
await queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||||
console.warn("Invalid date for formatting:", dateString);
|
setShowAddDialog(false);
|
||||||
return t('unknown');
|
toast.success(t('certificateAdded'));
|
||||||
}
|
|
||||||
return format(date, "MMM dd, yyyy");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error formatting date:", error);
|
console.error("Error adding certificate:", error);
|
||||||
return t('unknown');
|
toast.error(t('failedToAddCertificate'));
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewCertificate = (certificate: SSLCertificate) => {
|
const handleEditCertificate = async (updatedCertificate: SSLCertificate) => {
|
||||||
setSelectedCert(certificate);
|
setIsSubmitting(true);
|
||||||
};
|
try {
|
||||||
|
await pb.collection('ssl_certificates').update(updatedCertificate.id, {
|
||||||
const handleEditCertificate = (certificate: SSLCertificate) => {
|
warning_threshold: updatedCertificate.warning_threshold,
|
||||||
if (onEdit) {
|
expiry_threshold: updatedCertificate.expiry_threshold,
|
||||||
onEdit(certificate);
|
notification_channel: updatedCertificate.notification_channel,
|
||||||
} else {
|
check_interval: updatedCertificate.check_interval,
|
||||||
toast.error("Edit functionality not implemented yet");
|
});
|
||||||
|
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||||
|
setShowEditDialog(false);
|
||||||
|
setSelectedCertificate(null);
|
||||||
|
toast.success(t('certificateUpdated'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating certificate:", error);
|
||||||
|
toast.error(t('failedToUpdateCertificate'));
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteCertificate = (certificate: SSLCertificate) => {
|
const handleDeleteCertificate = async () => {
|
||||||
setCertToDelete(certificate);
|
if (!selectedCertificate) return;
|
||||||
setDeleteConfirmOpen(true);
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await deleteSSLCertificate(selectedCertificate.id);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||||
|
setShowDeleteDialog(false);
|
||||||
|
setSelectedCertificate(null);
|
||||||
|
toast.success(t('certificateDeleted'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting certificate:", error);
|
||||||
|
toast.error(t('failedToDeleteCertificate'));
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmDelete = () => {
|
const openViewDialog = (certificate: SSLCertificate) => {
|
||||||
if (certToDelete && onDelete) {
|
setSelectedCertificate(certificate);
|
||||||
onDelete(certToDelete);
|
setShowViewDialog(true);
|
||||||
setDeleteConfirmOpen(false);
|
};
|
||||||
setCertToDelete(null);
|
|
||||||
}
|
const openEditDialog = (certificate: SSLCertificate) => {
|
||||||
|
setSelectedCertificate(certificate);
|
||||||
|
setShowEditDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDeleteDialog = (certificate: SSLCertificate) => {
|
||||||
|
setSelectedCertificate(certificate);
|
||||||
|
setShowDeleteDialog(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-md border relative">
|
<Card>
|
||||||
{refreshingId && (
|
<CardContent>
|
||||||
<div className="absolute inset-0 bg-background/50 flex items-center justify-center z-10">
|
<Table>
|
||||||
<div className="bg-background p-4 rounded-md shadow flex items-center gap-2">
|
<TableHeader>
|
||||||
<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 ? (
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={7} className="text-center py-8">
|
<TableHead>{t('domain')}</TableHead>
|
||||||
{t('noSSLCertificates')}
|
<TableHead>{t('status')}</TableHead>
|
||||||
</TableCell>
|
<TableHead>{t('issuer')}</TableHead>
|
||||||
|
<TableHead>{t('validUntil')}</TableHead>
|
||||||
|
<TableHead>{t('daysLeft')}</TableHead>
|
||||||
|
<TableHead>Check Interval</TableHead>
|
||||||
|
<TableHead className="w-[50px]"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
</TableHeader>
|
||||||
certificates.map((certificate) => (
|
<TableBody>
|
||||||
|
{certificates.map((certificate) => (
|
||||||
<TableRow key={certificate.id}>
|
<TableRow key={certificate.id}>
|
||||||
<TableCell className="font-medium">{certificate.domain}</TableCell>
|
<TableCell className="font-medium">
|
||||||
<TableCell>{certificate.issuer_o || t('unknown')}</TableCell>
|
{certificate.domain}
|
||||||
<TableCell>
|
|
||||||
{formatDate(certificate.valid_till)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{typeof certificate.days_left === 'number' ? certificate.days_left : t('unknown')}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<SSLStatusBadge status={certificate.status} />
|
<SSLStatusBadge status={certificate.status} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>{certificate.issuer_o || certificate.issuer_cn || 'Unknown'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{certificate.last_notified
|
{certificate.valid_till ? new Date(certificate.valid_till).toLocaleDateString() : 'N/A'}
|
||||||
? formatDate(certificate.last_notified)
|
|
||||||
: t('never')}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell>
|
||||||
<DropdownMenu>
|
<span className={certificate.days_left <= 7 ? 'text-red-600 font-semibold' : certificate.days_left <= 30 ? 'text-yellow-600 font-semibold' : 'text-green-600'}>
|
||||||
<DropdownMenuTrigger asChild>
|
{certificate.days_left} {t('days')}
|
||||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
</span>
|
||||||
<span className="sr-only">Open menu</span>
|
</TableCell>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<TableCell>
|
||||||
</Button>
|
{certificate.check_interval || 1} {t('days')}
|
||||||
</DropdownMenuTrigger>
|
</TableCell>
|
||||||
<DropdownMenuContent align="end" className="bg-background border border-border">
|
<TableCell>
|
||||||
<DropdownMenuItem
|
<SSLCertificateActions
|
||||||
onClick={() => handleViewCertificate(certificate)}
|
certificate={certificate}
|
||||||
className="cursor-pointer"
|
onView={openViewDialog}
|
||||||
>
|
onEdit={openEditDialog}
|
||||||
<Eye className="mr-2 h-4 w-4" /> {t('view')}
|
onDelete={openDeleteDialog}
|
||||||
</DropdownMenuItem>
|
/>
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
if (refreshingId === null) {
|
|
||||||
onRefresh(certificate.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={refreshingId !== null}
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<RefreshCw className={`mr-2 h-4 w-4 ${refreshingId === certificate.id ? 'animate-spin text-primary' : ''}`} />
|
|
||||||
{t('check')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => handleEditCertificate(certificate)}
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<Edit className="mr-2 h-4 w-4" /> {t('edit')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => handleDeleteCertificate(certificate)}
|
|
||||||
className="cursor-pointer text-destructive focus:text-destructive"
|
|
||||||
>
|
|
||||||
<Trash2 className="mr-2 h-4 w-4" /> {t('delete')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))}
|
||||||
)}
|
</TableBody>
|
||||||
</TableBody>
|
</Table>
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* SSL Certificate Details Dialog */}
|
{certificates.length === 0 && (
|
||||||
<Dialog open={!!selectedCert} onOpenChange={(open) => !open && setSelectedCert(null)}>
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
<DialogContent className="max-w-3xl p-0 gap-0 overflow-hidden">
|
{t('noCertificatesFound')}
|
||||||
<DialogHeader className="p-6 pb-2">
|
|
||||||
<DialogTitle className="text-xl">
|
|
||||||
{t('sslCertificateDetails')}
|
|
||||||
</DialogTitle>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
{t('detailedInfo')} {selectedCert?.domain}
|
|
||||||
</p>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{selectedCert && (
|
|
||||||
<div className="p-6 pt-2 space-y-4 max-h-[70vh] overflow-y-auto">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{/* Basic Information */}
|
|
||||||
<div className="border rounded-md p-4">
|
|
||||||
<h3 className="font-semibold mb-3">{t('basicInformation')}</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<span className="text-muted-foreground">{t('domain')}:</span>
|
|
||||||
<span>{selectedCert.domain}</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<span className="text-muted-foreground">{t('status')}:</span>
|
|
||||||
<span><SSLStatusBadge status={selectedCert.status} /></span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<span className="text-muted-foreground">{t('issuer')}:</span>
|
|
||||||
<span>{selectedCert.issued_to || t('unknown')}</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<span className="text-muted-foreground">IP:</span>
|
|
||||||
<span>{selectedCert.resolved_ip || t('unknown')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Validity */}
|
|
||||||
<div className="border rounded-md p-4">
|
|
||||||
<h3 className="font-semibold mb-3">{t('validity')}</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<span className="text-muted-foreground">{t('validFrom')}:</span>
|
|
||||||
<span>{selectedCert.valid_from ? formatDate(selectedCert.valid_from) : t('unknown')}</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<span className="text-muted-foreground">{t('validUntil')}:</span>
|
|
||||||
<span>{formatDate(selectedCert.valid_till)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<span className="text-muted-foreground">{t('daysLeft')}:</span>
|
|
||||||
<span>{selectedCert.days_left}</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<span className="text-muted-foreground">{t('validityDays')}:</span>
|
|
||||||
<span>{selectedCert.validity_days || t('unknown')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Issuer */}
|
|
||||||
<div className="border rounded-md p-4">
|
|
||||||
<h3 className="font-semibold mb-3">{t('issuerInfo')}</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<span className="text-muted-foreground">{t('organization')}:</span>
|
|
||||||
<span>{selectedCert.issuer_o || t('unknown')}</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<span className="text-muted-foreground">{t('commonName')}:</span>
|
|
||||||
<span>{selectedCert.issuer_cn || t('unknown')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Technical Details */}
|
|
||||||
<div className="border rounded-md p-4">
|
|
||||||
<h3 className="font-semibold mb-3">{t('technicalDetails')}</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<span className="text-muted-foreground">{t('serialNumber')}:</span>
|
|
||||||
<span>{selectedCert.serial_number || t('unknown')}</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<span className="text-muted-foreground">{t('algorithm')}:</span>
|
|
||||||
<span>{selectedCert.cert_alg || t('unknown')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Subject Alternative Names */}
|
|
||||||
<div className="border rounded-md p-4">
|
|
||||||
<h3 className="font-semibold mb-3">{t('subjectAltNames')}</h3>
|
|
||||||
<div>
|
|
||||||
{selectedCert.cert_sans ? (
|
|
||||||
<p className="break-words">{selectedCert.cert_sans}</p>
|
|
||||||
) : (
|
|
||||||
<p>{t('none')}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Monitoring Configuration */}
|
|
||||||
<div className="border rounded-md p-4">
|
|
||||||
<h3 className="font-semibold mb-3">{t('monitoringConfig')}</h3>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-muted-foreground">{t('warningThreshold')}:</div>
|
|
||||||
<div>{selectedCert.warning_threshold} {t('daysLeft').toLowerCase()}</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-muted-foreground">{t('expiryThreshold')}:</div>
|
|
||||||
<div>{selectedCert.expiry_threshold} {t('daysLeft').toLowerCase()}</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-muted-foreground">{t('notificationChannel')}:</div>
|
|
||||||
<div>{selectedCert.notification_channel}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Timestamps */}
|
|
||||||
<div className="border rounded-md p-4">
|
|
||||||
<h3 className="font-semibold mb-3">{t('recordInfo')}</h3>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-muted-foreground">{t('created')}:</div>
|
|
||||||
<div>{selectedCert.created ? formatDate(selectedCert.created) : t('unknown')}</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-muted-foreground">{t('lastUpdated')}:</div>
|
|
||||||
<div>{selectedCert.updated ? formatDate(selectedCert.updated) : t('unknown')}</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-muted-foreground">{t('lastNotification')}:</div>
|
|
||||||
<div>{selectedCert.last_notified ? formatDate(selectedCert.last_notified) : t('never')}</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-muted-foreground">{t('collectionId')}:</div>
|
|
||||||
<div>{selectedCert.collectionId || t('unknown')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</CardContent>
|
||||||
<DialogFooter className="p-6 pt-0 border-t">
|
</Card>
|
||||||
<Button
|
|
||||||
variant="default"
|
{/* View Certificate Dialog */}
|
||||||
onClick={() => setSelectedCert(null)}
|
<SSLCertificateDetailDialog
|
||||||
className="w-full sm:w-auto"
|
certificate={selectedCertificate}
|
||||||
>
|
open={showViewDialog}
|
||||||
{t('close')}
|
onOpenChange={setShowViewDialog}
|
||||||
</Button>
|
/>
|
||||||
</DialogFooter>
|
|
||||||
|
{/* 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>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Delete Confirmation Dialog */}
|
{/* Edit Certificate Dialog */}
|
||||||
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
<Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
|
||||||
<DialogContent>
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('deleteSSLCertificate')}</DialogTitle>
|
<DialogTitle>{t('editSSLCertificate')}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t('editCertificateDescription')}
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="py-4">
|
{selectedCertificate && (
|
||||||
<p>{t('deleteConfirmation')} <strong>{certToDelete?.domain}</strong>?</p>
|
<EditSSLCertificateForm
|
||||||
<p className="text-sm text-muted-foreground mt-2">{t('deleteWarning')}</p>
|
certificate={selectedCertificate}
|
||||||
</div>
|
onSubmit={handleEditCertificate}
|
||||||
<DialogFooter>
|
onCancel={() => setShowEditDialog(false)}
|
||||||
<Button variant="outline" onClick={() => setDeleteConfirmOpen(false)}>{t('cancel')}</Button>
|
isPending={isSubmitting}
|
||||||
<Button variant="destructive" onClick={confirmDelete}>{t('delete')}</Button>
|
/>
|
||||||
</DialogFooter>
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete Certificate Dialog */}
|
||||||
|
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{t('deleteCertificate')}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{t('deleteCertificateConfirmation')} {selectedCertificate?.domain}?
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleDeleteCertificate}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
>
|
||||||
|
{isSubmitting ? t('deleting') : t('delete')}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -242,13 +243,7 @@ export const SSLDomainContent = () => {
|
|||||||
<SSLCertificateStatusCards certificates={certificates} />
|
<SSLCertificateStatusCards certificates={certificates} />
|
||||||
|
|
||||||
<div className="mt-6 flex-1 flex flex-col pb-6">
|
<div className="mt-6 flex-1 flex flex-col pb-6">
|
||||||
<SSLCertificatesTable
|
<SSLCertificatesTable />
|
||||||
certificates={certificates}
|
|
||||||
onRefresh={handleRefreshCertificate}
|
|
||||||
refreshingId={refreshingId}
|
|
||||||
onEdit={handleEditCertificate}
|
|
||||||
onDelete={handleDeleteCertificate}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// Create a history record of this check with a more accurate timestamp
|
||||||
const uptimeData: UptimeData = {
|
const uptimeData: UptimeData = {
|
||||||
serviceId: service.id,
|
service_id: service.id, // Include service_id
|
||||||
|
serviceId: service.id, // Keep for backward compatibility
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
status: "up",
|
status: "up",
|
||||||
responseTime: responseTime,
|
responseTime: responseTime,
|
||||||
@@ -92,7 +93,8 @@ export async function handleServiceDown(service: any, formattedTime: string): Pr
|
|||||||
|
|
||||||
// Create a history record of this check
|
// Create a history record of this check
|
||||||
const uptimeData: UptimeData = {
|
const uptimeData: UptimeData = {
|
||||||
serviceId: service.id,
|
service_id: service.id, // Include service_id
|
||||||
|
serviceId: service.id, // Keep for backward compatibility
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
status: "down",
|
status: "down",
|
||||||
responseTime: 0,
|
responseTime: 0,
|
||||||
@@ -161,4 +163,4 @@ export async function handleServiceDown(service: any, formattedTime: string): Pr
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error handling service DOWN state:", error);
|
console.error("Error handling service DOWN state:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,7 @@ export async function resumeMonitoring(serviceId: string): Promise<void> {
|
|||||||
id: service.id,
|
id: service.id,
|
||||||
name: service.name,
|
name: service.name,
|
||||||
url: service.url || "",
|
url: service.url || "",
|
||||||
|
host: service.host || "", // Include host property
|
||||||
type: service.service_type || service.type || "HTTP",
|
type: service.service_type || service.type || "HTTP",
|
||||||
status: "up",
|
status: "up",
|
||||||
responseTime: service.response_time || 0,
|
responseTime: service.response_time || 0,
|
||||||
@@ -77,4 +78,4 @@ export async function resumeMonitoring(serviceId: string): Promise<void> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error resuming service:", error);
|
console.error("Error resuming service:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@ export function prepareServiceForNotification(pbRecord: any, status: string, res
|
|||||||
id: pbRecord.id,
|
id: pbRecord.id,
|
||||||
name: pbRecord.name,
|
name: pbRecord.name,
|
||||||
url: pbRecord.url,
|
url: pbRecord.url,
|
||||||
|
host: pbRecord.host || "", // Include host property with fallback
|
||||||
type: pbRecord.type || pbRecord.service_type || "HTTP",
|
type: pbRecord.type || pbRecord.service_type || "HTTP",
|
||||||
status: status as any,
|
status: status as any,
|
||||||
responseTime: responseTime,
|
responseTime: responseTime,
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
// Re-export all SSL-related functionality for domain SSL checking
|
// Re-export all SSL-related functionality for domain SSL checking
|
||||||
// Use explicit re-exports to avoid naming conflicts
|
// Use explicit re-exports to avoid naming conflicts
|
||||||
|
|
||||||
// SSL Checker service
|
|
||||||
export { checkSSLCertificate, checkSSLApi } from './sslCheckerService';
|
|
||||||
|
|
||||||
// SSL Status utilities
|
// SSL Status utilities
|
||||||
export { determineSSLStatus } from './sslStatusUtils';
|
export { determineSSLStatus } from './sslStatusUtils';
|
||||||
|
|
||||||
@@ -16,7 +13,8 @@ export {
|
|||||||
addSSLCertificate,
|
addSSLCertificate,
|
||||||
checkAndUpdateCertificate,
|
checkAndUpdateCertificate,
|
||||||
deleteSSLCertificate,
|
deleteSSLCertificate,
|
||||||
refreshAllCertificates
|
refreshAllCertificates,
|
||||||
|
triggerImmediateCheck
|
||||||
} from './sslCertificateOperations';
|
} from './sslCertificateOperations';
|
||||||
|
|
||||||
// SSL-specific notification service
|
// SSL-specific notification service
|
||||||
@@ -28,10 +26,7 @@ export {
|
|||||||
} from './notification';
|
} from './notification';
|
||||||
|
|
||||||
// Export types
|
// Export types
|
||||||
export type { SSLCheckerResponse, SSLCertificate, AddSSLCertificateDto, SSLNotification } from './types';
|
export type { SSLCertificate, AddSSLCertificateDto, SSLNotification } from './types';
|
||||||
|
|
||||||
// Export utility functions
|
// Export utility functions for SSL operations
|
||||||
export { normalizeDomain, createErrorResponse } from './sslCheckerUtils';
|
export { calculateDaysRemaining, isValid } from './utils';
|
||||||
|
|
||||||
// Export checking mechanisms
|
|
||||||
export { checkWithFetch } from './sslPrimaryChecker';
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import { SSLCertificate } from "../types";
|
import { SSLCertificate } from "../types";
|
||||||
import { checkSSLCertificate } from "../sslCheckerService";
|
|
||||||
import { determineSSLStatus } from "../sslStatusUtils";
|
import { determineSSLStatus } from "../sslStatusUtils";
|
||||||
import { sendSSLNotification } from "./sslNotificationSender";
|
import { sendSSLNotification } from "./sslNotificationSender";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -35,20 +34,14 @@ export async function checkAllCertificatesAndNotify(): Promise<void> {
|
|||||||
/**
|
/**
|
||||||
* Checks a specific SSL certificate and sends notification if needed
|
* Checks a specific SSL certificate and sends notification if needed
|
||||||
* This respects the Warning and Expiry Thresholds set on the certificate
|
* 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> {
|
export async function checkCertificateAndNotify(certificate: SSLCertificate): Promise<boolean> {
|
||||||
console.log(`Checking certificate for ${certificate.domain}...`);
|
console.log(`Checking certificate for ${certificate.domain}...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get fresh SSL data
|
// Use the current certificate data (updated by Go service)
|
||||||
const sslData = await checkSSLCertificate(certificate.domain);
|
const daysLeft = certificate.days_left || 0;
|
||||||
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;
|
|
||||||
|
|
||||||
// Get threshold values (ensure they are numbers)
|
// Get threshold values (ensure they are numbers)
|
||||||
const warningThreshold = Number(certificate.warning_threshold) || 30;
|
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}`);
|
console.log(`${certificate.domain}: ${daysLeft} days left, status: ${status}, should notify: ${shouldNotify}, critical: ${isCritical}`);
|
||||||
|
|
||||||
// Update certificate data in database
|
// Update certificate status in database
|
||||||
const updateData: Partial<SSLCertificate> = {
|
await pb.collection('ssl_certificates').update(certificate.id, {
|
||||||
days_left: daysLeft,
|
status: status
|
||||||
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);
|
|
||||||
|
|
||||||
// Send notification if needed
|
// Send notification if needed
|
||||||
if (shouldNotify && certificate.notification_channel) {
|
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"}`);
|
toast.error(`Error checking certificate: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||||
return false;
|
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 { pb } from "@/lib/pocketbase";
|
||||||
import type { AddSSLCertificateDto, SSLCertificate } from "./types";
|
import type { AddSSLCertificateDto, SSLCertificate } from "./types";
|
||||||
import { checkSSLCertificate } from "./sslCheckerService";
|
|
||||||
import { determineSSLStatus } from "./sslStatusUtils";
|
import { determineSSLStatus } from "./sslStatusUtils";
|
||||||
import { checkCertificateAndNotify } from "./notification"; // Import notification service
|
import { checkCertificateAndNotify } from "./notification"; // Import notification service
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new SSL certificate to monitor
|
* Add a new SSL certificate to monitor
|
||||||
|
* Note: SSL checking is now handled by the Go service
|
||||||
*/
|
*/
|
||||||
export const addSSLCertificate = async (
|
export const addSSLCertificate = async (
|
||||||
certificateData: AddSSLCertificateDto
|
certificateData: AddSSLCertificateDto
|
||||||
): Promise<SSLCertificate> => {
|
): Promise<SSLCertificate> => {
|
||||||
try {
|
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
|
// Prepare the data for saving to database
|
||||||
|
// The Go service will handle the actual SSL checking
|
||||||
const data = {
|
const data = {
|
||||||
domain: certificateData.domain,
|
domain: certificateData.domain,
|
||||||
issued_to: sslData.result.issued_to || certificateData.domain,
|
issued_to: certificateData.domain, // Will be updated by Go service
|
||||||
issuer_o: sslData.result.issuer_o || "",
|
issuer_o: "", // Will be updated by Go service
|
||||||
status: determineSSLStatus(
|
status: "pending", // Initial status
|
||||||
sslData.result.days_left || 0,
|
cert_sans: "",
|
||||||
certificateData.warning_threshold,
|
cert_alg: "",
|
||||||
certificateData.expiry_threshold
|
serial_number: "",
|
||||||
),
|
valid_from: new Date().toISOString(), // Will be updated by Go service
|
||||||
cert_sans: sslData.result.cert_sans || "",
|
valid_till: new Date().toISOString(), // Will be updated by Go service
|
||||||
cert_alg: sslData.result.cert_alg || "",
|
validity_days: 0, // Will be updated by Go service
|
||||||
serial_number: sslData.result.cert_sn || "",
|
days_left: 0, // Will be updated by Go service
|
||||||
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,
|
|
||||||
warning_threshold: Number(certificateData.warning_threshold) || 30,
|
warning_threshold: Number(certificateData.warning_threshold) || 30,
|
||||||
expiry_threshold: Number(certificateData.expiry_threshold) || 7,
|
expiry_threshold: Number(certificateData.expiry_threshold) || 7,
|
||||||
notification_channel: certificateData.notification_channel || "",
|
notification_channel: certificateData.notification_channel || "",
|
||||||
|
check_interval: Number(certificateData.check_interval) || 1, // New field
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save to database
|
// Save to database
|
||||||
@@ -54,6 +45,7 @@ export const addSSLCertificate = async (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check and update a specific SSL certificate
|
* Check and update a specific SSL certificate
|
||||||
|
* Note: This now relies on the Go service for SSL data fetching
|
||||||
*/
|
*/
|
||||||
export const checkAndUpdateCertificate = async (
|
export const checkAndUpdateCertificate = async (
|
||||||
certificateId: string
|
certificateId: string
|
||||||
@@ -67,51 +59,40 @@ export const checkAndUpdateCertificate = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const typedCertificate = certificate as unknown as SSLCertificate;
|
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
|
// Return the current certificate data
|
||||||
// This will respect the Warning and Expiry Thresholds
|
return typedCertificate;
|
||||||
await checkCertificateAndNotify(updatedCertificate);
|
|
||||||
|
|
||||||
return updatedCertificate;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating SSL certificate:", error);
|
console.error("Error updating SSL certificate:", error);
|
||||||
throw 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
|
* Delete an SSL certificate from monitoring
|
||||||
*/
|
*/
|
||||||
@@ -127,6 +108,7 @@ export const deleteSSLCertificate = async (id: string): Promise<boolean> => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh all SSL certificates
|
* Refresh all SSL certificates
|
||||||
|
* Note: The Go service handles the actual SSL checking
|
||||||
*/
|
*/
|
||||||
export const refreshAllCertificates = async (): Promise<{ success: number; failed: number }> => {
|
export const refreshAllCertificates = async (): Promise<{ success: number; failed: number }> => {
|
||||||
try {
|
try {
|
||||||
@@ -140,7 +122,7 @@ export const refreshAllCertificates = async (): Promise<{ success: number; faile
|
|||||||
|
|
||||||
for (const cert of certificates) {
|
for (const cert of certificates) {
|
||||||
try {
|
try {
|
||||||
await checkAndUpdateCertificate(cert.id);
|
await checkCertificateAndNotify(cert);
|
||||||
success++;
|
success++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to refresh certificate ${cert.domain}:`, 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
|
// SSL Certificate model
|
||||||
export interface SSLCheckerResponse {
|
export interface SSLCertificate {
|
||||||
version: string;
|
id: string;
|
||||||
app: string;
|
domain: string;
|
||||||
host: string;
|
issued_to: string;
|
||||||
response_time_sec: string;
|
issuer_o: string;
|
||||||
status: string; // "ok", "error"
|
status: string;
|
||||||
message?: string;
|
cert_sans?: string;
|
||||||
error?: string;
|
cert_alg?: string;
|
||||||
result: {
|
serial_number?: number | string;
|
||||||
host: string;
|
valid_from: string;
|
||||||
resolved_ip?: string;
|
valid_till: string;
|
||||||
issued_to: string;
|
validity_days: number;
|
||||||
issued_o?: string | null;
|
days_left: number;
|
||||||
issuer_c?: string;
|
valid_days_to_expire?: number;
|
||||||
issuer_o?: string | null;
|
warning_threshold: number;
|
||||||
issuer_ou?: string | null;
|
expiry_threshold: number;
|
||||||
issuer_cn?: string;
|
notification_channel: string;
|
||||||
cert_sn: string;
|
last_notified?: string;
|
||||||
cert_sha1?: string;
|
created?: string;
|
||||||
cert_alg: string;
|
updated?: string;
|
||||||
cert_ver?: number;
|
// New fields
|
||||||
cert_sans: string;
|
check_interval?: number; // Check interval in days
|
||||||
cert_exp?: boolean;
|
check_at?: string; // Next check time
|
||||||
cert_valid?: boolean;
|
// Existing fields based on the provided structure
|
||||||
valid_from?: string;
|
collectionId?: string;
|
||||||
valid_till?: string;
|
collectionName?: string;
|
||||||
validity_days?: number;
|
resolved_ip?: string;
|
||||||
days_left?: number;
|
issuer_cn?: string;
|
||||||
valid_days_to_expire?: number;
|
}
|
||||||
hsts_header_enabled?: boolean;
|
|
||||||
};
|
// SSL specific notification types
|
||||||
}
|
export interface SSLNotification {
|
||||||
|
certificateId: string;
|
||||||
// SSL Certificate DTO for adding new certificates
|
domain: string;
|
||||||
export interface AddSSLCertificateDto {
|
message: string;
|
||||||
domain: string;
|
isCritical: boolean;
|
||||||
warning_threshold: number;
|
timestamp: string;
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
|
|
||||||
import { SSLCheckerResponse } from "./types";
|
|
||||||
|
|
||||||
// Calculate days remaining from expiration date
|
// Calculate days remaining from expiration date
|
||||||
export function calculateDaysRemaining(validTo: string): number {
|
export function calculateDaysRemaining(validTo: string): number {
|
||||||
try {
|
try {
|
||||||
@@ -22,30 +20,4 @@ export function isValid(validTo: string): boolean {
|
|||||||
console.error("Error checking certificate validity:", error);
|
console.error("Error checking certificate validity:", error);
|
||||||
return false;
|
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
|
// This file re-exports all SSL certificate related services for backward compatibility
|
||||||
import {
|
import {
|
||||||
checkSSLCertificate,
|
|
||||||
fetchSSLCertificates,
|
fetchSSLCertificates,
|
||||||
addSSLCertificate,
|
addSSLCertificate,
|
||||||
checkAndUpdateCertificate
|
checkAndUpdateCertificate,
|
||||||
|
triggerImmediateCheck,
|
||||||
|
deleteSSLCertificate
|
||||||
} from './ssl';
|
} from './ssl';
|
||||||
|
|
||||||
import { determineSSLStatus } from './ssl/sslStatusUtils';
|
import { determineSSLStatus } from './ssl/sslStatusUtils';
|
||||||
@@ -17,11 +18,12 @@ import {
|
|||||||
} from './ssl/notification';
|
} from './ssl/notification';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
checkSSLCertificate,
|
|
||||||
determineSSLStatus,
|
determineSSLStatus,
|
||||||
fetchSSLCertificates,
|
fetchSSLCertificates,
|
||||||
addSSLCertificate,
|
addSSLCertificate,
|
||||||
checkAndUpdateCertificate,
|
checkAndUpdateCertificate,
|
||||||
|
triggerImmediateCheck,
|
||||||
|
deleteSSLCertificate,
|
||||||
checkAllCertificatesAndNotify,
|
checkAllCertificatesAndNotify,
|
||||||
checkCertificateAndNotify,
|
checkCertificateAndNotify,
|
||||||
shouldRunDailyCheck
|
shouldRunDailyCheck
|
||||||
|
|||||||
@@ -31,22 +31,23 @@ const getCollectionForServiceType = (serviceType: string): string => {
|
|||||||
export const uptimeService = {
|
export const uptimeService = {
|
||||||
async recordUptimeData(data: UptimeData): Promise<void> {
|
async recordUptimeData(data: UptimeData): Promise<void> {
|
||||||
try {
|
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 = {
|
const options = {
|
||||||
$autoCancel: false,
|
$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({
|
const record = await pb.collection('uptime_data').create({
|
||||||
service_id: data.serviceId,
|
service_id: data.service_id || data.serviceId,
|
||||||
timestamp: data.timestamp,
|
timestamp: data.timestamp,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
response_time: data.responseTime
|
response_time: data.responseTime
|
||||||
}, options);
|
}, options);
|
||||||
|
|
||||||
// Invalidate cache for this service
|
// 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));
|
keysToDelete.forEach(key => uptimeCache.delete(key));
|
||||||
|
|
||||||
console.log(`Uptime data recorded successfully with ID: ${record.id}`);
|
console.log(`Uptime data recorded successfully with ID: ${record.id}`);
|
||||||
@@ -116,12 +117,15 @@ export const uptimeService = {
|
|||||||
// Transform the response items to UptimeData format
|
// Transform the response items to UptimeData format
|
||||||
const uptimeData = response.items.map(item => ({
|
const uptimeData = response.items.map(item => ({
|
||||||
id: item.id,
|
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,
|
timestamp: item.timestamp,
|
||||||
status: item.status as "up" | "down" | "warning" | "paused",
|
status: item.status as "up" | "down" | "warning" | "paused",
|
||||||
responseTime: item.response_time || 0,
|
responseTime: item.response_time || 0,
|
||||||
date: item.timestamp,
|
date: item.timestamp,
|
||||||
uptime: 100
|
uptime: 100,
|
||||||
|
error_message: item.error_message,
|
||||||
|
details: item.details
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
|
|||||||
@@ -2,22 +2,36 @@
|
|||||||
export interface Service {
|
export interface Service {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url?: string;
|
||||||
host?: string; // Add host field for PING and TCP services
|
host?: string; // Make host optional since it's not always required
|
||||||
port?: number; // Add port field for TCP services
|
port?: number;
|
||||||
type: "HTTP" | "HTTPS" | "TCP" | "DNS" | "PING" | "HTTP" | "http" | "https" | "tcp" | "dns" | "ping" | "smtp" | "icmp";
|
domain?: string; // Add domain field for DNS services
|
||||||
status: "up" | "down" | "paused" | "pending" | "warning";
|
type: "http" | "https" | "tcp" | "ping" | "icmp" | "dns";
|
||||||
|
status: "up" | "down" | "paused" | "warning";
|
||||||
responseTime: number;
|
responseTime: number;
|
||||||
uptime: number;
|
uptime?: number;
|
||||||
lastChecked: string;
|
lastChecked: string;
|
||||||
interval: number;
|
interval: number;
|
||||||
|
timeout?: number;
|
||||||
retries: number;
|
retries: number;
|
||||||
notificationChannel?: string;
|
created?: string;
|
||||||
|
updated?: string;
|
||||||
|
notification_channel?: string;
|
||||||
|
notificationChannel?: string; // Keep for backward compatibility
|
||||||
alertTemplate?: string;
|
alertTemplate?: string;
|
||||||
muteAlerts?: boolean; // Keep this to avoid breaking existing code
|
|
||||||
alerts?: "muted" | "unmuted"; // Make sure alerts is properly typed as union
|
alerts?: "muted" | "unmuted"; // Make sure alerts is properly typed as union
|
||||||
|
muteAlerts?: boolean; // Keep this to avoid breaking existing code
|
||||||
muteChangedAt?: string;
|
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 {
|
export interface CreateServiceParams {
|
||||||
@@ -34,11 +48,54 @@ export interface CreateServiceParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UptimeData {
|
export interface UptimeData {
|
||||||
date?: string;
|
|
||||||
uptime?: number;
|
|
||||||
id?: string;
|
id?: string;
|
||||||
serviceId?: string;
|
service_id?: string; // Make service_id optional for backward compatibility
|
||||||
|
serviceId?: string; // Keep for backward compatibility
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
status: "up" | "down" | "paused" | "pending" | "warning";
|
status: "up" | "down" | "paused" | "warning";
|
||||||
responseTime: number;
|
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 {
|
export interface SSLCertificate {
|
||||||
id: string;
|
id: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
@@ -19,7 +18,10 @@ export interface SSLCertificate {
|
|||||||
last_notified?: string;
|
last_notified?: string;
|
||||||
created?: string;
|
created?: string;
|
||||||
updated?: string;
|
updated?: string;
|
||||||
// New fields based on the provided structure
|
// New fields
|
||||||
|
check_interval?: number; // Check interval in days
|
||||||
|
check_at?: string; // Next check time
|
||||||
|
// Existing fields based on the provided structure
|
||||||
collectionId?: string;
|
collectionId?: string;
|
||||||
collectionName?: string;
|
collectionName?: string;
|
||||||
resolved_ip?: string;
|
resolved_ip?: string;
|
||||||
@@ -31,4 +33,5 @@ export interface AddSSLCertificateDto {
|
|||||||
warning_threshold: number;
|
warning_threshold: number;
|
||||||
expiry_threshold: number;
|
expiry_threshold: number;
|
||||||
notification_channel: string;
|
notification_channel: string;
|
||||||
|
check_interval?: number; // New field for check interval in days
|
||||||
}
|
}
|
||||||
@@ -8,10 +8,10 @@ A Go-based microservice for service operations including ICMP ping, DNS resoluti
|
|||||||
- **ICMP Ping**: Full ping functionality with packet statistics
|
- **ICMP Ping**: Full ping functionality with packet statistics
|
||||||
- **DNS Resolution**: A, AAAA, MX, and TXT record lookups
|
- **DNS Resolution**: A, AAAA, MX, and TXT record lookups
|
||||||
- **TCP Connectivity**: Port connectivity testing
|
- **TCP Connectivity**: Port connectivity testing
|
||||||
|
- **SSL Certificate**: SSL Certificate Check
|
||||||
- REST API endpoints
|
- REST API endpoints
|
||||||
- Health check endpoint
|
- Health check endpoint
|
||||||
- Configurable via environment variables
|
- Configurable via environment variables
|
||||||
- Docker support
|
|
||||||
- Comprehensive operation statistics
|
- Comprehensive operation statistics
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func Load() *Config {
|
|||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
Port: getEnv("PORT", "8091"),
|
Port: getEnv("PORT", "8091"),
|
||||||
DefaultCount: getEnvInt("DEFAULT_COUNT", 4),
|
DefaultCount: getEnvInt("DEFAULT_COUNT", 4),
|
||||||
DefaultTimeout: getEnvDuration("DEFAULT_TIMEOUT", 3*time.Second),
|
DefaultTimeout: getEnvDuration("DEFAULT_TIMEOUT", 10*time.Second),
|
||||||
MaxCount: getEnvInt("MAX_COUNT", 20),
|
MaxCount: getEnvInt("MAX_COUNT", 20),
|
||||||
MaxTimeout: getEnvDuration("MAX_TIMEOUT", 30*time.Second),
|
MaxTimeout: getEnvDuration("MAX_TIMEOUT", 30*time.Second),
|
||||||
EnableLogging: getEnvBool("ENABLE_LOGGING", true),
|
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)
|
result, err = httpOp.Execute(url, method)
|
||||||
|
|
||||||
|
case types.OperationSSL:
|
||||||
|
sslOp := operations.NewSSLOperation(timeout)
|
||||||
|
result, err = sslOp.Execute(req.Host)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Invalid operation type", http.StatusBadRequest)
|
http.Error(w, "Invalid operation type", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ func main() {
|
|||||||
// Initialize PocketBase client (no credentials required)
|
// Initialize PocketBase client (no credentials required)
|
||||||
var pbClient *pocketbase.PocketBaseClient
|
var pbClient *pocketbase.PocketBaseClient
|
||||||
var monitoringService *monitoring.MonitoringService
|
var monitoringService *monitoring.MonitoringService
|
||||||
|
var sslMonitoringService *monitoring.SSLMonitoringService
|
||||||
|
|
||||||
if cfg.PocketBaseEnabled {
|
if cfg.PocketBaseEnabled {
|
||||||
var err error
|
var err error
|
||||||
@@ -35,6 +36,11 @@ func main() {
|
|||||||
monitoringService = monitoring.NewMonitoringService(pbClient)
|
monitoringService = monitoring.NewMonitoringService(pbClient)
|
||||||
go monitoringService.Start()
|
go monitoringService.Start()
|
||||||
log.Println("Monitoring service started (public access mode)")
|
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 {
|
if monitoringService != nil {
|
||||||
log.Printf("Automatic service monitoring enabled")
|
log.Printf("Automatic service monitoring enabled")
|
||||||
}
|
}
|
||||||
|
if sslMonitoringService != nil {
|
||||||
|
log.Printf("SSL certificate monitoring enabled")
|
||||||
|
}
|
||||||
log.Printf("Endpoints:")
|
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(" GET /operation/quick?type=<type>&host=<host> - Quick operation test")
|
||||||
log.Printf(" POST /ping - Legacy ping endpoint")
|
log.Printf(" POST /ping - Legacy ping endpoint")
|
||||||
log.Printf(" GET /ping/quick?host=<host> - Legacy quick ping test")
|
log.Printf(" GET /ping/quick?host=<host> - Legacy quick ping test")
|
||||||
log.Printf(" GET /health - Health check")
|
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
|
// Setup graceful shutdown
|
||||||
c := make(chan os.Signal, 1)
|
c := make(chan os.Signal, 1)
|
||||||
@@ -77,10 +86,13 @@ func main() {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-c
|
<-c
|
||||||
log.Println("Shutting down monitoring service...")
|
log.Println("Shutting down monitoring services...")
|
||||||
if monitoringService != nil {
|
if monitoringService != nil {
|
||||||
monitoringService.Stop()
|
monitoringService.Stop()
|
||||||
}
|
}
|
||||||
|
if sslMonitoringService != nil {
|
||||||
|
sslMonitoringService.Stop()
|
||||||
|
}
|
||||||
log.Println("Service stopped")
|
log.Println("Service stopped")
|
||||||
os.Exit(0)
|
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
|
package types
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
@@ -10,6 +9,7 @@ const (
|
|||||||
OperationDNS OperationType = "dns"
|
OperationDNS OperationType = "dns"
|
||||||
OperationTCP OperationType = "tcp"
|
OperationTCP OperationType = "tcp"
|
||||||
OperationHTTP OperationType = "http"
|
OperationHTTP OperationType = "http"
|
||||||
|
OperationSSL OperationType = "ssl"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OperationRequest struct {
|
type OperationRequest struct {
|
||||||
@@ -56,6 +56,17 @@ type OperationResult struct {
|
|||||||
ContentLength int64 `json:"content_length,omitempty"`
|
ContentLength int64 `json:"content_length,omitempty"`
|
||||||
ResponseBody string `json:"response_body,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"`
|
StartTime time.Time `json:"start_time"`
|
||||||
EndTime time.Time `json:"end_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