Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8d640ebb5 | |||
| 9f38e23b22 | |||
| 1ea81a23b2 | |||
| 8ee933563e | |||
| 687619ce0d | |||
| 9e931ad439 | |||
| c7fd7f4fbe | |||
| eafc4c33ab | |||
| 7644480782 | |||
| 1515ae56af | |||
| caf3e73dc3 | |||
| 98c86ff5dd | |||
| 42441b615a | |||
| e5651c152a | |||
| fc84922959 | |||
| 093300c38a | |||
| 2dcedf7b30 | |||
| 38e06b9bec | |||
| 9f2ab1a79b | |||
| 54ebe0604d | |||
| cf28eb41d6 | |||
| 8ef3614908 | |||
| 7773c1bc66 | |||
| 580dbc424c | |||
| 981bc972f0 | |||
| a9043e8088 | |||
| 1ae961c5d9 | |||
| 51226a7afe | |||
| dfd57a6b62 | |||
| 12f4d18521 | |||
| aea32f10b6 | |||
| a16b1d228a | |||
| 930622b5be |
-44
@@ -1,44 +0,0 @@
|
|||||||
# Stage 1: Build the frontend
|
|
||||||
FROM node:18-alpine AS frontend-builder
|
|
||||||
WORKDIR /app
|
|
||||||
COPY application/package*.json ./
|
|
||||||
RUN npm install
|
|
||||||
COPY application/ ./
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Stage 2: Download PocketBase binary
|
|
||||||
FROM alpine:3.17 AS pb-builder
|
|
||||||
WORKDIR /pb
|
|
||||||
ARG PB_VERSION=0.27.2
|
|
||||||
RUN apk add --no-cache wget unzip \
|
|
||||||
&& wget https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip \
|
|
||||||
&& unzip pocketbase_${PB_VERSION}_linux_amd64.zip \
|
|
||||||
&& chmod +x /pb/pocketbase
|
|
||||||
|
|
||||||
# Stage 3: Final runtime image
|
|
||||||
FROM alpine:3.17
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy PocketBase binary
|
|
||||||
COPY --from=pb-builder /pb/pocketbase /app/pocketbase
|
|
||||||
|
|
||||||
# Copy frontend build to PocketBase public directory
|
|
||||||
COPY --from=frontend-builder /app/dist /app/pb_public
|
|
||||||
|
|
||||||
# Copy backend files
|
|
||||||
COPY server/pb_migrations /app/pb_migrations
|
|
||||||
COPY server/pb_hooks /app/pb_hooks
|
|
||||||
COPY server/pb_data /app/pb_data
|
|
||||||
|
|
||||||
# Mark pb_data as a volume for runtime persistence
|
|
||||||
VOLUME /app/pb_data
|
|
||||||
|
|
||||||
# Expose default PocketBase port
|
|
||||||
EXPOSE 8090
|
|
||||||
|
|
||||||
# Launch PocketBase
|
|
||||||
#CMD ["/app/pocketbase", "serve", "--dir", "/app/pb_data"]
|
|
||||||
|
|
||||||
|
|
||||||
# Launch PocketBase and bind to 0.0.0.0 for external access
|
|
||||||
CMD ["/app/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir", "/app/pb_data"]
|
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
Copyright (c) 2022 - Present, Tola Leng
|
Copyright (c) 2025, Tola Leng
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
and associated documentation files (the "Software"), to deal in the Software without restriction,
|
and associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
CheckCle is an Open Source solution for seamless, real-time monitoring of full-stack systems, applications, and infrastructure. It provides developers, sysadmins, and DevOps teams with deep insights and actionable data across every layer of their environment—whether it's servers, applications, or services. With CheckCle, you gain visibility, control, and the ability to ensure optimal performance throughout your entire technology stack.
|
CheckCle is an Open Source solution for seamless, real-time monitoring of full-stack systems, applications, and infrastructure. It provides developers, sysadmins, and DevOps teams with deep insights and actionable data across every layer of their environment—whether it's servers, applications, or services. With CheckCle, you gain visibility, control, and the ability to ensure optimal performance throughout your entire technology stack.
|
||||||
|
|
||||||
## 🎯 Live Demo
|
## 🎯 Live Demo
|
||||||
👉 **Try it now:** [CheckCle Live Demo](https://demo.checkcle.io) Coming Soon!
|
👉 **Try it now:** [CheckCle Live Demo](https://demo.checkcle.io)
|
||||||
|
|
||||||
## 🌟 Core Features
|
## 🌟 Core Features
|
||||||
|
|
||||||
@@ -25,16 +25,16 @@ CheckCle is an Open Source solution for seamless, real-time monitoring of full-s
|
|||||||
## #️⃣ Getting Started
|
## #️⃣ Getting Started
|
||||||
|
|
||||||
### Installation with Docker Compose
|
### Installation with Docker Compose
|
||||||
1. Clone the repository and run
|
1. Copy ready docker run command
|
||||||
```bash
|
```bash
|
||||||
#Clone the repository
|
# Create Docker Volume for data persistence
|
||||||
|
|
||||||
git clone https://github.com/operacle/checkcle.git
|
docker volume create pb_data
|
||||||
cd checkcle
|
|
||||||
|
|
||||||
# Run docker compose
|
|
||||||
|
|
||||||
docker compose up -d
|
# Docker Run Command
|
||||||
|
|
||||||
|
docker run --name checkcle --restart unless-stopped -p 8090:8090 -v pb_data:/app/pb_data --ulimit nofile=4096:8192 operacle/checkcle:latest
|
||||||
|
|
||||||
```
|
```
|
||||||
2. Docker Compose - Recommended
|
2. Docker Compose - Recommended
|
||||||
@@ -78,14 +78,16 @@ volumes:
|
|||||||
- [x] Auth with Multi-users system (admin)
|
- [x] Auth with Multi-users system (admin)
|
||||||
- [x] Notifications (Telegram)
|
- [x] Notifications (Telegram)
|
||||||
- [x] Docker containerization
|
- [x] Docker containerization
|
||||||
- [ ] SSL & Domain Monitoring
|
- [x] CheckCle Website
|
||||||
|
- [x] CheckCle Demo Server
|
||||||
|
- [ ] SSL & Domain Monitoring (Inprogress)
|
||||||
|
- [ ] Uptime monitoring (PING - Inprogress)
|
||||||
- [ ] Infrastructure Server Monitoring
|
- [ ] Infrastructure Server Monitoring
|
||||||
- [ ] Schedule Maintenance & Incident Management
|
- [ ] Schedule Maintenance & Incident Management
|
||||||
- [ ] Operational Status / Public Status Pages
|
- [ ] Operational Status / Public Status Pages
|
||||||
- [ ] Uptime monitoring (TCP, PING, DNS)
|
- [ ] Uptime monitoring (TCP, PING, DNS)
|
||||||
- [ ] User Permission Roles & Service Group
|
- [ ] User Permission Roles & Service Group
|
||||||
- [ ] Notifications (Email/Slack/Discord/Signal)
|
- [ ] Notifications (Email/Slack/Discord/Signal)
|
||||||
- [ ] Mobile PWA version
|
|
||||||
- [ ] Open-source release with full documentation
|
- [ ] Open-source release with full documentation
|
||||||
|
|
||||||
## 🌟 CheckCle for Communities?
|
## 🌟 CheckCle for Communities?
|
||||||
|
|||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
# 🔐 Security Policy
|
||||||
|
|
||||||
|
## 📦 Project: [checkcle](https://github.com/operacle/checkcle)
|
||||||
|
|
||||||
|
**checkcle** is a lightweight, TypeScript-based built for uptime and server monitoring tools.
|
||||||
|
|
||||||
|
We care about the security and privacy of users running this project in production environments.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📣 Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you believe you have found a security vulnerability in this project:
|
||||||
|
|
||||||
|
- **DO NOT** open a public issue to report it.
|
||||||
|
- Please report it responsibly via one of the following methods:
|
||||||
|
|
||||||
|
### 🔐 Preferred: [Report a Vulnerability via GitHub](https://github.com/operacle/checkcle/security/advisories/new)
|
||||||
|
|
||||||
|
- Use the GitHub security advisory form (private and secure).
|
||||||
|
- Attach as much detail as possible:
|
||||||
|
- Description of the issue
|
||||||
|
- Affected version or commit hash
|
||||||
|
- Reproduction steps
|
||||||
|
- Impact and any potential mitigations
|
||||||
|
- Logs or screenshots (if available)
|
||||||
|
|
||||||
|
### 📧 Alternatively: Contact the Maintainer
|
||||||
|
- Email: `security@checkcle.io`
|
||||||
|
- Optionally include a PGP public key for encrypted messages
|
||||||
|
|
||||||
|
We aim to respond within **3–5 business days**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Supported Versions
|
||||||
|
|
||||||
|
We support the latest stable release of `checkcle`. Security patches may also be applied to recent versions at our discretion.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
|---------|-----------|
|
||||||
|
| `main` (latest) | ✅ Yes |
|
||||||
|
| Older versions | ⚠️ Best-effort |
|
||||||
|
| Pre-release or forks | ❌ No |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Security Practices
|
||||||
|
|
||||||
|
This project adheres to the following practices to enhance security:
|
||||||
|
|
||||||
|
- 🔎 Regular vulnerability checks using `npm audit`
|
||||||
|
- ⛓️ Dependency pinning via `package-lock.json`
|
||||||
|
- ✅ Type-safe code using `TypeScript`
|
||||||
|
- 🧪 Continuous testing and CI
|
||||||
|
- 🔐 No data is stored or transmitted unless explicitly configured by the user
|
||||||
|
- 🧑💻 All contributions are reviewed before being merged
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Known Security Limitations
|
||||||
|
|
||||||
|
- `checkcle` makes outbound HTTPS requests to check certificate details — avoid running in untrusted or high-risk environments without proper network policies.
|
||||||
|
- Output may contain certificate metadata (e.g., CN, SANs, expiry dates) — avoid exposing this to public logs unless sanitized.
|
||||||
|
- The data may be lost upon system restarts or crashes. Always ensure that backup and recovery mechanisms are in place in production environments.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is released under the [MIT License](./LICENSE). Use at your own risk. The Creator and contributors are not liable for misuse, data loss, or operational impact resulting from use of the software.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙌 Acknowledgements
|
||||||
|
|
||||||
|
We appreciate responsible disclosures from the community. Your efforts help us make the open-source ecosystem safer for everyone.
|
||||||
|
|
||||||
|
Thanks & Regards,
|
||||||
|
|
||||||
|
— [Tola Leng](https://github.com/tolaleng)
|
||||||
@@ -15,6 +15,7 @@ import ServiceDetail from "./pages/ServiceDetail";
|
|||||||
import Settings from "./pages/Settings";
|
import Settings from "./pages/Settings";
|
||||||
import Profile from "./pages/Profile";
|
import Profile from "./pages/Profile";
|
||||||
import NotFound from "./pages/NotFound";
|
import NotFound from "./pages/NotFound";
|
||||||
|
import SslDomain from "./pages/SslDomain";
|
||||||
|
|
||||||
// Create a Protected route component
|
// Create a Protected route component
|
||||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||||
@@ -93,6 +94,14 @@ const App = () => {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/ssl-domain"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<SslDomain />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useSystemSettings } from "@/hooks/useSystemSettings";
|
import { useSystemSettings } from "@/hooks/useSystemSettings";
|
||||||
|
import QuickActionsDialog from "./QuickActionsDialog";
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
currentUser: AuthUser | null;
|
currentUser: AuthUser | null;
|
||||||
@@ -28,6 +29,7 @@ export const Header = ({
|
|||||||
const [greeting, setGreeting] = useState<string>("");
|
const [greeting, setGreeting] = useState<string>("");
|
||||||
const { systemName } = useSystemSettings();
|
const { systemName } = useSystemSettings();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [quickActionsOpen, setQuickActionsOpen] = useState(false);
|
||||||
|
|
||||||
// Set greeting based on time of day
|
// Set greeting based on time of day
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -88,8 +90,18 @@ export const Header = ({
|
|||||||
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="mr-2">
|
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="mr-2">
|
||||||
{sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
|
{sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
|
{/* Quick Actions Button */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setQuickActionsOpen(true)}
|
||||||
|
className="mr-2"
|
||||||
|
>
|
||||||
<Grid3x3 className="h-5 w-5 text-green-500" />
|
<Grid3x3 className="h-5 w-5 text-green-500" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
<h1 className="text-lg font-medium">{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋 ✨</h1>
|
<h1 className="text-lg font-medium">{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋 ✨</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,25 +130,45 @@ export const Header = ({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
{/* Documentation */}
|
{/* Documentation */}
|
||||||
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-full w-8 h-8 border-border"
|
||||||
|
onClick={() => window.open("https://docs.checkcle.io", "_blank")}
|
||||||
|
>
|
||||||
<span className="sr-only">{t("documentation")}</span>
|
<span className="sr-only">{t("documentation")}</span>
|
||||||
<FileText className="w-4 h-4" />
|
<FileText className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* GitHub */}
|
{/* GitHub */}
|
||||||
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-full w-8 h-8 border-border"
|
||||||
|
onClick={() => window.open("https://github.com/operacle/checkcle", "_blank")}
|
||||||
|
>
|
||||||
<span className="sr-only">GitHub</span>
|
<span className="sr-only">GitHub</span>
|
||||||
<Github className="w-4 h-4" />
|
<Github className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* X (Twitter) */}
|
{/* X (Twitter) */}
|
||||||
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-full w-8 h-8 border-border"
|
||||||
|
onClick={() => window.open("https://x.com/tlengoss", "_blank")}
|
||||||
|
>
|
||||||
<span className="sr-only">X (Twitter)</span>
|
<span className="sr-only">X (Twitter)</span>
|
||||||
<Twitter className="w-4 h-4" />
|
<Twitter className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Discord (replaced with MessageSquare) */}
|
{/* Discord */}
|
||||||
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-full w-8 h-8 border-border"
|
||||||
|
onClick={() => window.open("https://discord.gg/xs9gbubGwX", "_blank")}
|
||||||
|
>
|
||||||
<span className="sr-only">Discord</span>
|
<span className="sr-only">Discord</span>
|
||||||
<MessageSquare className="w-4 h-4" />
|
<MessageSquare className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -185,6 +217,9 @@ export const Header = ({
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions Dialog */}
|
||||||
|
<QuickActionsDialog isOpen={quickActionsOpen} setIsOpen={setQuickActionsOpen} />
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogClose
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
|
||||||
|
interface QuickActionsDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuickActionsDialog: React.FC<QuickActionsDialogProps> = ({ isOpen, setIsOpen }) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[800px] bg-background max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogClose className="absolute right-4 top-4">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogClose>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-xl font-bold">{t('quickActions')}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t('quickActionsDescription')}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
|
||||||
|
{/* Website Monitoring Card */}
|
||||||
|
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
|
||||||
|
<h3 className="font-semibold text-lg mb-2">{t('monitorWebsite')}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{t('monitorWebsiteDesc')}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium">{t('quickTips')}:</p>
|
||||||
|
<ul className="list-disc pl-5 text-sm space-y-1">
|
||||||
|
<li>{t('monitorMultipleEndpoints')}</li>
|
||||||
|
<li>{t('trackResponseTimes')}</li>
|
||||||
|
<li>{t('setCustomAlerts')}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Server Monitoring Card */}
|
||||||
|
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
|
||||||
|
<h3 className="font-semibold text-lg mb-2">{t('monitorServer')}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{t('monitorServerDesc')}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium">{t('quickTips')}:</p>
|
||||||
|
<ul className="list-disc pl-5 text-sm space-y-1">
|
||||||
|
<li>{t('monitorCPURam')}</li>
|
||||||
|
<li>{t('trackNetworkMetrics')}</li>
|
||||||
|
<li>{t('viewProcesses')}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SSL Certificate Card */}
|
||||||
|
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
|
||||||
|
<h3 className="font-semibold text-lg mb-2">{t('sslCertificate')}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{t('sslCertificateDesc')}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium">{t('quickTips')}:</p>
|
||||||
|
<ul className="list-disc pl-5 text-sm space-y-1">
|
||||||
|
<li>{t('trackSSLExpiration')}</li>
|
||||||
|
<li>{t('getEarlyRenewal')}</li>
|
||||||
|
<li>{t('monitorMultipleDomains')}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Incidents Management Card */}
|
||||||
|
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
|
||||||
|
<h3 className="font-semibold text-lg mb-2">{t('incidentsManagement')}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{t('incidentsManagementDesc')}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium">{t('quickTips')}:</p>
|
||||||
|
<ul className="list-disc pl-5 text-sm space-y-1">
|
||||||
|
<li>{t('createTrackIncidents')}</li>
|
||||||
|
<li>{t('assignIncidents')}</li>
|
||||||
|
<li>{t('monitorResolution')}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Operational Status Card */}
|
||||||
|
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
|
||||||
|
<h3 className="font-semibold text-lg mb-2">{t('operationalStatus')}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{t('operationalStatusDesc')}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium">{t('quickTips')}:</p>
|
||||||
|
<ul className="list-disc pl-5 text-sm space-y-1">
|
||||||
|
<li>{t('createCustomStatus')}</li>
|
||||||
|
<li>{t('displayRealTime')}</li>
|
||||||
|
<li>{t('shareIncidentUpdates')}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reports & Analytics Card */}
|
||||||
|
<div className="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
|
||||||
|
<h3 className="font-semibold text-lg mb-2">{t('reportsAnalytics')}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{t('reportsAnalyticsDesc')}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium">{t('quickTips')}:</p>
|
||||||
|
<ul className="list-disc pl-5 text-sm space-y-1">
|
||||||
|
<li>{t('generateUptimeReports')}</li>
|
||||||
|
<li>{t('analyzePerformance')}</li>
|
||||||
|
<li>{t('exportMonitoringData')}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
||||||
|
{t('close')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuickActionsDialog;
|
||||||
@@ -63,10 +63,10 @@ export const Sidebar = ({
|
|||||||
<Boxes className={`${mainIconSize} text-blue-400`} />
|
<Boxes className={`${mainIconSize} text-blue-400`} />
|
||||||
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("instanceMonitoring")}</span>}
|
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("instanceMonitoring")}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200`}>
|
<Link to="/ssl-domain" className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg ${location.pathname === '/ssl-domain' ? theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200`}>
|
||||||
<Radar className={`${mainIconSize} text-cyan-400`} />
|
<Radar className={`${mainIconSize} text-cyan-400`} />
|
||||||
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("sslDomain")}</span>}
|
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("sslDomain")}</span>}
|
||||||
</div>
|
</Link>
|
||||||
<div className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200`}>
|
<div className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200`}>
|
||||||
<Calendar className={`${mainIconSize} text-emerald-400`} />
|
<Calendar className={`${mainIconSize} text-emerald-400`} />
|
||||||
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("scheduleIncident")}</span>}
|
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("scheduleIncident")}</span>}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { authService } from "@/services/authService";
|
import { authService } from "@/services/authService";
|
||||||
|
import { AlertCircle } from "lucide-react";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
|
||||||
// Profile update form schema
|
// Profile update form schema
|
||||||
const profileFormSchema = z.object({
|
const profileFormSchema = z.object({
|
||||||
@@ -31,6 +33,8 @@ interface UpdateProfileFormProps {
|
|||||||
|
|
||||||
export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [emailChangeRequested, setEmailChangeRequested] = useState(false);
|
||||||
|
const [updateError, setUpdateError] = useState<string | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Initialize the form with current user data
|
// Initialize the form with current user data
|
||||||
@@ -45,21 +49,45 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
|||||||
|
|
||||||
async function onSubmit(data: ProfileFormValues) {
|
async function onSubmit(data: ProfileFormValues) {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
setUpdateError(null);
|
||||||
|
setEmailChangeRequested(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await userService.updateUser(user.id, {
|
console.log("Submitting profile update with data:", data);
|
||||||
|
|
||||||
|
// Detect if email is being changed
|
||||||
|
const isEmailChanged = data.email !== user.email;
|
||||||
|
|
||||||
|
// Create update payload with all fields
|
||||||
|
const updateData = {
|
||||||
full_name: data.full_name,
|
full_name: data.full_name,
|
||||||
username: data.username,
|
username: data.username,
|
||||||
email: data.email,
|
email: isEmailChanged ? data.email : undefined,
|
||||||
});
|
// Only set emailVisibility if email is being changed
|
||||||
|
emailVisibility: isEmailChanged ? true : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Sending update payload:", updateData);
|
||||||
|
|
||||||
|
// Update user data using the userService
|
||||||
|
await userService.updateUser(user.id, updateData);
|
||||||
|
|
||||||
// Refresh user data in auth context
|
// Refresh user data in auth context
|
||||||
await authService.refreshUserData();
|
await authService.refreshUserData();
|
||||||
|
|
||||||
|
// If email was changed, show a specific message
|
||||||
|
if (isEmailChanged) {
|
||||||
|
setEmailChangeRequested(true);
|
||||||
|
toast({
|
||||||
|
title: "Email change requested",
|
||||||
|
description: "A verification email has been sent to your new email address. Please check your inbox and follow the instructions to complete the change.",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
toast({
|
toast({
|
||||||
title: "Profile updated",
|
title: "Profile updated",
|
||||||
description: "Your profile information has been updated successfully.",
|
description: "Your profile information has been updated successfully.",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Profile update error:", error);
|
console.error("Profile update error:", error);
|
||||||
|
|
||||||
@@ -68,6 +96,8 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
|||||||
errorMessage = error.message;
|
errorMessage = error.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setUpdateError(errorMessage);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Update failed",
|
title: "Update failed",
|
||||||
description: errorMessage,
|
description: errorMessage,
|
||||||
@@ -81,6 +111,23 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
|||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
{updateError && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>{updateError}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{emailChangeRequested && (
|
||||||
|
<Alert className="bg-yellow-50 border-yellow-200 text-yellow-800">
|
||||||
|
<AlertCircle className="h-4 w-4 text-yellow-600" />
|
||||||
|
<AlertDescription>
|
||||||
|
A verification email has been sent to your new email address.
|
||||||
|
Please check your inbox and follow the instructions to complete the change.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="full_name"
|
name="full_name"
|
||||||
@@ -119,6 +166,11 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
|||||||
<Input type="email" placeholder="your.email@example.com" {...field} />
|
<Input type="email" placeholder="your.email@example.com" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
{field.value !== user.email && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Changing your email requires verification. A verification email will be sent.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ export function UserProfileDetails({ user }: UserProfileDetailsProps) {
|
|||||||
return user.username[0].toUpperCase();
|
return user.username[0].toUpperCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Determine if the user is active based on the status field
|
||||||
|
const isActive = user.status === "Active";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
<Avatar className="h-32 w-32">
|
<Avatar className="h-32 w-32">
|
||||||
@@ -54,8 +57,8 @@ export function UserProfileDetails({ user }: UserProfileDetailsProps) {
|
|||||||
{user.role}
|
{user.role}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<Badge variant={user.isActive ? "default" : "outline"} className="px-2 py-1">
|
<Badge variant={isActive ? "default" : "outline"} className="px-2 py-1">
|
||||||
{user.isActive ? "Active" : "Inactive"}
|
{user.status || (isActive ? "Active" : "Inactive")}
|
||||||
</Badge>
|
</Badge>
|
||||||
{user.verified && (
|
{user.verified && (
|
||||||
<Badge className="bg-green-600 hover:bg-green-700 px-2 py-1">
|
<Badge className="bg-green-600 hover:bg-green-700 px-2 py-1">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
@@ -19,7 +20,7 @@ export const AboutSystem: React.FC = () => {
|
|||||||
return <div className="space-y-6 animate-fade-in">
|
return <div className="space-y-6 animate-fade-in">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">{t('aboutSystem')}</h1>
|
<h1 className="text-3xl font-bold tracking-tight">{t('aboutSystem')}</h1>
|
||||||
<p className="text-muted-foreground text-base leading-relaxed mt-2">{t('aboutCheckCle')}</p>
|
<p className="text-muted-foreground text-base leading-relaxed mt-2">{t('aboutReamStack')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -31,10 +32,13 @@ export const AboutSystem: React.FC = () => {
|
|||||||
<ServerIcon className={`h-5 w-5 ${theme === 'dark' ? 'text-sky-400' : 'text-sky-600'}`} />
|
<ServerIcon className={`h-5 w-5 ${theme === 'dark' ? 'text-sky-400' : 'text-sky-600'}`} />
|
||||||
<span className="font-thin text-xl">{t('systemDescription')}</span>
|
<span className="font-thin text-xl">{t('systemDescription')}</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
<CardDescription className="font-medium text-base">{systemName || 'ReamStack'}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6 pt-6">
|
<CardContent className="space-y-6 pt-6">
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
|
<p className="text-base leading-relaxed">
|
||||||
|
{t('systemVersion')} <span className="font-semibold">1.0.0</span> {t('isAModern')} {t('serverMonitoring')} {t('solution')} {t('designedFor')} {t('enterpriseUse')}. {t('providesRealtime')} {t('uptimeTracking')}, {t('performanceAnalytics')}, {t('andAlertNotifications')}.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="flex flex-col space-y-3 pt-2">
|
<div className="flex flex-col space-y-3 pt-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
@@ -49,7 +53,7 @@ export const AboutSystem: React.FC = () => {
|
|||||||
<Separator className="my-1" />
|
<Separator className="my-1" />
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-muted-foreground">{t('releasedOn')}</span>
|
<span className="text-muted-foreground">{t('releasedOn')}</span>
|
||||||
<span className="text-foreground font-medium">May 10, 2025</span>
|
<span className="text-foreground font-medium">January 15, 2025</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,7 +66,7 @@ export const AboutSystem: React.FC = () => {
|
|||||||
<Code2 className={`h-5 w-5 ${theme === 'dark' ? 'text-emerald-400' : 'text-emerald-600'}`} />
|
<Code2 className={`h-5 w-5 ${theme === 'dark' ? 'text-emerald-400' : 'text-emerald-600'}`} />
|
||||||
<span>{t('links')}</span>
|
<span>{t('links')}</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="font-medium text-base">{systemName || 'CheckCle'} {t('resources').toLowerCase()}</CardDescription>
|
<CardDescription className="font-medium text-base">{systemName || 'ReamStack'} {t('resources').toLowerCase()}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 pt-6">
|
<CardContent className="space-y-4 pt-6">
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
@@ -78,7 +82,7 @@ export const AboutSystem: React.FC = () => {
|
|||||||
<Twitter className={`h-5 w-5 ${theme === 'dark' ? 'text-white/80' : 'text-gray-700'}`} />
|
<Twitter className={`h-5 w-5 ${theme === 'dark' ? 'text-white/80' : 'text-gray-700'}`} />
|
||||||
<span>{t('followOnX')}</span>
|
<span>{t('followOnX')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" className="flex items-center justify-start gap-3 h-12 hover:bg-muted/50 transition-all duration-200" onClick={() => window.open("#", "_blank")}>
|
<Button variant="outline" className="flex items-center justify-start gap-3 h-12 hover:bg-muted/50 transition-all duration-200" onClick={() => window.open("https://discord.gg/xs9gbubGwX", "_blank")}>
|
||||||
<MessageCircle className={`h-5 w-5 ${theme === 'dark' ? 'text-white/80' : 'text-gray-700'}`} />
|
<MessageCircle className={`h-5 w-5 ${theme === 'dark' ? 'text-white/80' : 'text-gray-700'}`} />
|
||||||
<span>{t('joinDiscord')}</span>
|
<span>{t('joinDiscord')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import * as z from "zod";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Bell } from "lucide-react";
|
||||||
|
|
||||||
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { DialogFooter } from "@/components/ui/dialog";
|
||||||
|
import { AddSSLCertificateDto } from "@/types/ssl.types";
|
||||||
|
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
domain: z.string().min(1, "Domain is required"),
|
||||||
|
warning_threshold: z.coerce.number().int().min(1).max(365),
|
||||||
|
expiry_threshold: z.coerce.number().int().min(1).max(30),
|
||||||
|
notification_channel: z.string().min(1, "Notification channel is required")
|
||||||
|
});
|
||||||
|
|
||||||
|
interface AddSSLCertificateFormProps {
|
||||||
|
onSubmit: (data: AddSSLCertificateDto) => Promise<void>;
|
||||||
|
onCancel: () => void;
|
||||||
|
isPending?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddSSLCertificateForm = ({
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
isPending = false
|
||||||
|
}: AddSSLCertificateFormProps) => {
|
||||||
|
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
domain: "",
|
||||||
|
warning_threshold: 30,
|
||||||
|
expiry_threshold: 7,
|
||||||
|
notification_channel: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch notification channels when form loads
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchNotificationChannels = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const configs = await alertConfigService.getAlertConfigurations();
|
||||||
|
console.log("Fetched notification channels:", configs);
|
||||||
|
// Only include enabled channels
|
||||||
|
const enabledConfigs = configs.filter(config => {
|
||||||
|
// Handle the possibility of enabled being a string
|
||||||
|
if (typeof config.enabled === 'string') {
|
||||||
|
return config.enabled === "true";
|
||||||
|
}
|
||||||
|
// Otherwise treat as boolean
|
||||||
|
return config.enabled === true;
|
||||||
|
});
|
||||||
|
setAlertConfigs(enabledConfigs);
|
||||||
|
|
||||||
|
// Set default if available, preferring Telegram channels
|
||||||
|
const telegramChannel = enabledConfigs.find(c => c.notification_type === "telegram");
|
||||||
|
const defaultChannel = telegramChannel || enabledConfigs[0];
|
||||||
|
|
||||||
|
if (defaultChannel?.id) {
|
||||||
|
form.setValue("notification_channel", defaultChannel.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching notification channels:", error);
|
||||||
|
toast.error("Failed to load notification channels");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchNotificationChannels();
|
||||||
|
}, [form]);
|
||||||
|
|
||||||
|
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
|
try {
|
||||||
|
// Convert the form values to the required DTO format with required properties
|
||||||
|
const certData: AddSSLCertificateDto = {
|
||||||
|
domain: values.domain,
|
||||||
|
warning_threshold: values.warning_threshold,
|
||||||
|
expiry_threshold: values.expiry_threshold,
|
||||||
|
notification_channel: values.notification_channel
|
||||||
|
};
|
||||||
|
|
||||||
|
await onSubmit(certData);
|
||||||
|
form.reset();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error adding SSL certificate:", error);
|
||||||
|
toast.error("Failed to add SSL certificate");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="domain"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Domain</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="example.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="warning_threshold"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Warning Threshold (days)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Get notified when certificates are about to expire
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="expiry_threshold"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Expiry Threshold (days)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Get notified when certificates are critically close to expiring
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="notification_channel"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Notification Channel</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select notification channel" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{alertConfigs.length > 0 ? (
|
||||||
|
alertConfigs.map((config) => (
|
||||||
|
<SelectItem key={config.id} value={config.id || ""}>
|
||||||
|
{config.notify_name} ({config.notification_type})
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : isLoading ? (
|
||||||
|
<SelectItem value="loading" disabled>Loading channels...</SelectItem>
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" disabled>No notification channels found</SelectItem>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription className="flex items-center gap-1">
|
||||||
|
<Bell className="h-4 w-4" />
|
||||||
|
Choose where to receive SSL certificate alerts
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="outline" onClick={onCancel}>Cancel</Button>
|
||||||
|
<Button type="submit" disabled={isPending || isLoading}>
|
||||||
|
Add Certificate
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
|
import { SSLCertificate } from "@/types/ssl.types";
|
||||||
|
import { Loader2, Bell } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
domain: z.string().min(1, "Domain is required"),
|
||||||
|
warning_threshold: z.coerce.number().min(1, "Warning threshold must be at least 1 day"),
|
||||||
|
expiry_threshold: z.coerce.number().min(1, "Expiry threshold must be at least 1 day"),
|
||||||
|
notification_channel: z.string().min(1, "Notification channel is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
interface EditSSLCertificateFormProps {
|
||||||
|
certificate: SSLCertificate;
|
||||||
|
onSubmit: (data: SSLCertificate) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
isPending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditSSLCertificateForm = ({ certificate, onSubmit, onCancel, isPending }: EditSSLCertificateFormProps) => {
|
||||||
|
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
domain: certificate.domain,
|
||||||
|
warning_threshold: certificate.warning_threshold,
|
||||||
|
expiry_threshold: certificate.expiry_threshold,
|
||||||
|
notification_channel: certificate.notification_channel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch notification channels when form loads
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchNotificationChannels = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const configs = await alertConfigService.getAlertConfigurations();
|
||||||
|
console.log("Fetched notification channels:", configs);
|
||||||
|
// Only include enabled channels
|
||||||
|
const enabledConfigs = configs.filter(config => {
|
||||||
|
// Handle the possibility of enabled being a string
|
||||||
|
if (typeof config.enabled === 'string') {
|
||||||
|
return config.enabled === "true";
|
||||||
|
}
|
||||||
|
// Otherwise treat as boolean
|
||||||
|
return config.enabled === true;
|
||||||
|
});
|
||||||
|
setAlertConfigs(enabledConfigs);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching notification channels:", error);
|
||||||
|
toast.error("Failed to load notification channels");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchNotificationChannels();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = (data: FormValues) => {
|
||||||
|
// Merge the updated values with the original certificate
|
||||||
|
const updatedCertificate: SSLCertificate = {
|
||||||
|
...certificate,
|
||||||
|
...data,
|
||||||
|
// Ensure values are correctly typed as numbers
|
||||||
|
warning_threshold: Number(data.warning_threshold),
|
||||||
|
expiry_threshold: Number(data.expiry_threshold)
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Submitting updated certificate:", updatedCertificate);
|
||||||
|
onSubmit(updatedCertificate);
|
||||||
|
};
|
||||||
|
|
||||||
|
// For debugging
|
||||||
|
console.log("Certificate data:", certificate);
|
||||||
|
console.log("Form default values:", form.getValues());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="domain"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Domain</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="example.com"
|
||||||
|
disabled={true} // Domain shouldn't be editable
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Domain name cannot be changed. To monitor a different domain, add a new certificate.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="warning_threshold"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Warning Threshold (Days)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
placeholder="30"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Days before expiration to send warning
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="expiry_threshold"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Expiry Threshold (Days)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
placeholder="7"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Days before expiration to send critical alert
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="notification_channel"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Notification Channel</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
value={field.value}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select notification channel" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{alertConfigs.length > 0 ? (
|
||||||
|
alertConfigs.map((config) => (
|
||||||
|
<SelectItem key={config.id} value={config.id || ""}>
|
||||||
|
{config.notify_name} ({config.notification_type})
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : isLoading ? (
|
||||||
|
<SelectItem value="loading" disabled>Loading channels...</SelectItem>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SelectItem value="email">Email</SelectItem>
|
||||||
|
<SelectItem value="telegram">Telegram</SelectItem>
|
||||||
|
<SelectItem value="slack">Slack</SelectItem>
|
||||||
|
<SelectItem value="webhook">Webhook</SelectItem>
|
||||||
|
<SelectItem value="none">None</SelectItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription className="flex items-center gap-1">
|
||||||
|
<Bell className="h-4 w-4" />
|
||||||
|
Where to send notifications about this certificate
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Updating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Save Changes'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { SSLCertificate } from "@/types/ssl.types";
|
||||||
|
|
||||||
|
interface SSLCertificateStatusCardsProps {
|
||||||
|
certificates: SSLCertificate[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SSLCertificateStatusCards = ({ certificates }: SSLCertificateStatusCardsProps) => {
|
||||||
|
// Count certificates by status
|
||||||
|
const validCount = certificates.filter(cert => cert.status === 'valid').length;
|
||||||
|
const expiringCount = certificates.filter(cert => cert.status === 'expiring_soon').length;
|
||||||
|
const expiredCount = certificates.filter(cert => cert.status === 'expired').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Card className="p-6 flex items-center space-x-4">
|
||||||
|
<div className="bg-green-100 dark:bg-green-900/20 p-3 rounded-full">
|
||||||
|
<div className="h-12 w-12 rounded-full bg-green-500/20 flex items-center justify-center">
|
||||||
|
<div className="h-8 w-8 rounded-full bg-green-500 flex items-center justify-center text-white">
|
||||||
|
✓
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">Valid Certificates</p>
|
||||||
|
<p className="text-3xl font-bold">{validCount}</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 flex items-center space-x-4">
|
||||||
|
<div className="bg-yellow-100 dark:bg-yellow-900/20 p-3 rounded-full">
|
||||||
|
<div className="h-12 w-12 rounded-full bg-yellow-500/20 flex items-center justify-center">
|
||||||
|
<div className="h-8 w-8 rounded-full bg-yellow-500 flex items-center justify-center text-white">
|
||||||
|
!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">Expiring Soon</p>
|
||||||
|
<p className="text-3xl font-bold">{expiringCount}</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 flex items-center space-x-4">
|
||||||
|
<div className="bg-red-100 dark:bg-red-900/20 p-3 rounded-full">
|
||||||
|
<div className="h-12 w-12 rounded-full bg-red-500/20 flex items-center justify-center">
|
||||||
|
<div className="h-8 w-8 rounded-full bg-red-500 flex items-center justify-center text-white">
|
||||||
|
✗
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">Expired</p>
|
||||||
|
<p className="text-3xl font-bold">{expiredCount}</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
TableHead,
|
||||||
|
TableBody,
|
||||||
|
TableCell
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { RefreshCw, Eye, Edit, Trash2, MoreHorizontal } from "lucide-react";
|
||||||
|
import { SSLCertificate } from "@/types/ssl.types";
|
||||||
|
import { SSLStatusBadge } from "./SSLStatusBadge";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface SSLCertificatesTableProps {
|
||||||
|
certificates: SSLCertificate[];
|
||||||
|
onRefresh: (id: string) => void;
|
||||||
|
refreshingId: string | null;
|
||||||
|
onEdit?: (certificate: SSLCertificate) => void;
|
||||||
|
onDelete?: (certificate: SSLCertificate) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SSLCertificatesTable = ({
|
||||||
|
certificates,
|
||||||
|
onRefresh,
|
||||||
|
refreshingId,
|
||||||
|
onEdit,
|
||||||
|
onDelete
|
||||||
|
}: SSLCertificatesTableProps) => {
|
||||||
|
const [selectedCert, setSelectedCert] = useState<SSLCertificate | null>(null);
|
||||||
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||||
|
const [certToDelete, setCertToDelete] = useState<SSLCertificate | null>(null);
|
||||||
|
|
||||||
|
const formatDate = (dateString: string | undefined) => {
|
||||||
|
if (!dateString) return 'Unknown';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
console.warn("Invalid date for formatting:", dateString);
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
return format(date, "MMM dd, yyyy");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error formatting date:", error);
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewCertificate = (certificate: SSLCertificate) => {
|
||||||
|
setSelectedCert(certificate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditCertificate = (certificate: SSLCertificate) => {
|
||||||
|
if (onEdit) {
|
||||||
|
onEdit(certificate);
|
||||||
|
} else {
|
||||||
|
toast.error("Edit functionality not implemented yet");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteCertificate = (certificate: SSLCertificate) => {
|
||||||
|
setCertToDelete(certificate);
|
||||||
|
setDeleteConfirmOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = () => {
|
||||||
|
if (certToDelete && onDelete) {
|
||||||
|
onDelete(certToDelete);
|
||||||
|
setDeleteConfirmOpen(false);
|
||||||
|
setCertToDelete(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="rounded-md border relative">
|
||||||
|
{refreshingId && (
|
||||||
|
<div className="absolute inset-0 bg-background/50 flex items-center justify-center z-10">
|
||||||
|
<div className="bg-background p-4 rounded-md shadow flex items-center gap-2">
|
||||||
|
<RefreshCw className="h-5 w-5 animate-spin text-primary" />
|
||||||
|
<span>Checking SSL certificate...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Domain</TableHead>
|
||||||
|
<TableHead>Issuer</TableHead>
|
||||||
|
<TableHead>Expiration Date</TableHead>
|
||||||
|
<TableHead>Days Left</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Last Notified</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{certificates.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="text-center py-8">
|
||||||
|
No SSL certificates found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
certificates.map((certificate) => (
|
||||||
|
<TableRow key={certificate.id}>
|
||||||
|
<TableCell className="font-medium">{certificate.domain}</TableCell>
|
||||||
|
<TableCell>{certificate.issuer_o || 'Unknown'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{formatDate(certificate.valid_till)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{typeof certificate.days_left === 'number' ? certificate.days_left : 'Unknown'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<SSLStatusBadge status={certificate.status} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{certificate.last_notified
|
||||||
|
? formatDate(certificate.last_notified)
|
||||||
|
: "Never"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="bg-background border border-border">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleViewCertificate(certificate)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Eye className="mr-2 h-4 w-4" /> View
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (refreshingId === null) {
|
||||||
|
onRefresh(certificate.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={refreshingId !== null}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`mr-2 h-4 w-4 ${refreshingId === certificate.id ? 'animate-spin text-primary' : ''}`} />
|
||||||
|
Check
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleEditCertificate(certificate)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Edit className="mr-2 h-4 w-4" /> Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDeleteCertificate(certificate)}
|
||||||
|
className="cursor-pointer text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" /> Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SSL Certificate Details Dialog */}
|
||||||
|
<Dialog open={!!selectedCert} onOpenChange={(open) => !open && setSelectedCert(null)}>
|
||||||
|
<DialogContent className="max-w-3xl p-0 gap-0 overflow-hidden">
|
||||||
|
<DialogHeader className="p-6 pb-2">
|
||||||
|
<DialogTitle className="text-xl">
|
||||||
|
SSL Certificate Details
|
||||||
|
</DialogTitle>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Detailed information about the SSL certificate for {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">Basic Information</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<span className="text-muted-foreground">Domain:</span>
|
||||||
|
<span>{selectedCert.domain}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<span className="text-muted-foreground">Status:</span>
|
||||||
|
<span><SSLStatusBadge status={selectedCert.status} /></span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<span className="text-muted-foreground">Issued To:</span>
|
||||||
|
<span>{selectedCert.issued_to || 'Unknown'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<span className="text-muted-foreground">Resolved IP:</span>
|
||||||
|
<span>{selectedCert.resolved_ip || 'Unknown'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Validity */}
|
||||||
|
<div className="border rounded-md p-4">
|
||||||
|
<h3 className="font-semibold mb-3">Validity</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<span className="text-muted-foreground">Valid From:</span>
|
||||||
|
<span>{selectedCert.valid_from ? formatDate(selectedCert.valid_from) : 'Unknown'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<span className="text-muted-foreground">Valid Until:</span>
|
||||||
|
<span>{formatDate(selectedCert.valid_till)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<span className="text-muted-foreground">Days Left:</span>
|
||||||
|
<span>{selectedCert.days_left}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<span className="text-muted-foreground">Validity Days:</span>
|
||||||
|
<span>{selectedCert.validity_days || 'Unknown'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Issuer */}
|
||||||
|
<div className="border rounded-md p-4">
|
||||||
|
<h3 className="font-semibold mb-3">Issuer</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<span className="text-muted-foreground">Organization:</span>
|
||||||
|
<span>{selectedCert.issuer_o || 'Unknown'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<span className="text-muted-foreground">Common Name:</span>
|
||||||
|
<span>{selectedCert.issuer_cn || 'Unknown'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Technical Details */}
|
||||||
|
<div className="border rounded-md p-4">
|
||||||
|
<h3 className="font-semibold mb-3">Technical Details</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<span className="text-muted-foreground">Serial Number:</span>
|
||||||
|
<span>{selectedCert.serial_number || 'Unknown'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<span className="text-muted-foreground">Algorithm:</span>
|
||||||
|
<span>{selectedCert.cert_alg || 'Unknown'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subject Alternative Names */}
|
||||||
|
<div className="border rounded-md p-4">
|
||||||
|
<h3 className="font-semibold mb-3">Subject Alternative Names (SANs)</h3>
|
||||||
|
<div>
|
||||||
|
{selectedCert.cert_sans ? (
|
||||||
|
<p className="break-words">{selectedCert.cert_sans}</p>
|
||||||
|
) : (
|
||||||
|
<p>None</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Monitoring Configuration */}
|
||||||
|
<div className="border rounded-md p-4">
|
||||||
|
<h3 className="font-semibold mb-3">Monitoring Configuration</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-muted-foreground">Warning Threshold:</div>
|
||||||
|
<div>{selectedCert.warning_threshold} days</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-muted-foreground">Expiry Threshold:</div>
|
||||||
|
<div>{selectedCert.expiry_threshold} days</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-muted-foreground">Notification Channel:</div>
|
||||||
|
<div>{selectedCert.notification_channel}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timestamps */}
|
||||||
|
<div className="border rounded-md p-4">
|
||||||
|
<h3 className="font-semibold mb-3">Record Information</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-muted-foreground">Created:</div>
|
||||||
|
<div>{selectedCert.created ? formatDate(selectedCert.created) : 'Unknown'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-muted-foreground">Last Updated:</div>
|
||||||
|
<div>{selectedCert.updated ? formatDate(selectedCert.updated) : 'Unknown'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-muted-foreground">Last Notification:</div>
|
||||||
|
<div>{selectedCert.last_notified ? formatDate(selectedCert.last_notified) : 'Never'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-muted-foreground">Collection ID:</div>
|
||||||
|
<div>{selectedCert.collectionId || 'Unknown'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter className="p-6 pt-0 border-t">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={() => setSelectedCert(null)}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete SSL Certificate</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<p>Are you sure you want to delete the SSL certificate for <strong>{certToDelete?.domain}</strong>?</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2">This action cannot be undone.</p>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setDeleteConfirmOpen(false)}>Cancel</Button>
|
||||||
|
<Button variant="destructive" onClick={confirmDelete}>Delete</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Plus, RefreshCw } from "lucide-react";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { SSLCertificateStatusCards } from "./SSLCertificateStatusCards";
|
||||||
|
import { SSLCertificatesTable } from "./SSLCertificatesTable";
|
||||||
|
import { LoadingState } from "@/components/services/LoadingState";
|
||||||
|
import { fetchSSLCertificates, addSSLCertificate, checkAndUpdateCertificate, refreshAllCertificates, deleteSSLCertificate } from "@/services/ssl";
|
||||||
|
import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
|
||||||
|
import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
|
||||||
|
import type { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types";
|
||||||
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
|
||||||
|
export const SSLDomainContent = () => {
|
||||||
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||||
|
const [refreshingId, setRefreshingId] = useState<string | null>(null);
|
||||||
|
const [isRefreshingAll, setIsRefreshingAll] = useState(false);
|
||||||
|
const [selectedCertificate, setSelectedCertificate] = useState<SSLCertificate | null>(null);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// Fetch SSL certificates with explicit error handling
|
||||||
|
const { data: certificates = [], isLoading, error } = useQuery({
|
||||||
|
queryKey: ['ssl-certificates'],
|
||||||
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
|
console.log("Fetching SSL certificates from SSLDomainContent...");
|
||||||
|
const result = await fetchSSLCertificates();
|
||||||
|
console.log("Received SSL certificates:", result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching certificates:", error);
|
||||||
|
toast.error("Failed to load SSL certificates");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchInterval: 300000, // Refresh every 5 minutes
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add certificate mutation
|
||||||
|
const addMutation = useMutation({
|
||||||
|
mutationFn: addSSLCertificate,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||||
|
setIsAddDialogOpen(false);
|
||||||
|
toast.success("SSL certificate added successfully");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Error adding SSL certificate:", error);
|
||||||
|
toast.error(error instanceof Error ? error.message : "Failed to add SSL certificate. Make sure the domain is valid and accessible.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit certificate mutation - Updated to ensure thresholds are properly updated
|
||||||
|
const editMutation = useMutation({
|
||||||
|
mutationFn: async (certificate: SSLCertificate) => {
|
||||||
|
console.log("Updating certificate with data:", certificate);
|
||||||
|
|
||||||
|
// Create the update data object
|
||||||
|
const updateData = {
|
||||||
|
warning_threshold: Number(certificate.warning_threshold),
|
||||||
|
expiry_threshold: Number(certificate.expiry_threshold),
|
||||||
|
notification_channel: certificate.notification_channel,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Update data to be sent:", updateData);
|
||||||
|
|
||||||
|
// Update certificate in the database using PocketBase directly
|
||||||
|
const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData);
|
||||||
|
|
||||||
|
console.log("PocketBase update response:", updated);
|
||||||
|
|
||||||
|
// After updating the settings, refresh the certificate to ensure it's up to date
|
||||||
|
// This will also check if notification needs to be sent based on updated thresholds
|
||||||
|
const refreshedCert = await checkAndUpdateCertificate(certificate.id);
|
||||||
|
|
||||||
|
return refreshedCert;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||||
|
setIsEditDialogOpen(false);
|
||||||
|
setSelectedCertificate(null);
|
||||||
|
toast.success("SSL certificate updated successfully");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Error updating SSL certificate:", error);
|
||||||
|
toast.error(error instanceof Error ? error.message : "Failed to update SSL certificate");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete certificate mutation
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: deleteSSLCertificate,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||||
|
toast.success("SSL certificate deleted successfully");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Error deleting SSL certificate:", error);
|
||||||
|
toast.error(error instanceof Error ? error.message : "Failed to delete SSL certificate");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh certificate mutation - Updated to ensure notifications are sent
|
||||||
|
const refreshMutation = useMutation({
|
||||||
|
mutationFn: checkAndUpdateCertificate,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||||
|
setRefreshingId(null);
|
||||||
|
toast.success(`SSL certificate for ${data.domain} updated successfully`);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Error refreshing SSL certificate:", error);
|
||||||
|
|
||||||
|
let errorMessage = "Failed to check SSL certificate.";
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(errorMessage);
|
||||||
|
setRefreshingId(null);
|
||||||
|
|
||||||
|
// Still refresh the data to show any partial information that was saved
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh all certificates mutation
|
||||||
|
const refreshAllMutation = useMutation({
|
||||||
|
mutationFn: refreshAllCertificates,
|
||||||
|
onSuccess: (result) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||||
|
setIsRefreshingAll(false);
|
||||||
|
|
||||||
|
if (result.failed === 0) {
|
||||||
|
toast.success(`Successfully refreshed all ${result.success} certificates`);
|
||||||
|
} else {
|
||||||
|
toast.info(`Refreshed ${result.success} certificates, ${result.failed} failed`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Error refreshing all certificates:", error);
|
||||||
|
toast.error("Failed to refresh all certificates");
|
||||||
|
setIsRefreshingAll(false);
|
||||||
|
|
||||||
|
// Still refresh the data to show any partial information
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddCertificate = async (data: AddSSLCertificateDto) => {
|
||||||
|
addMutation.mutate(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefreshCertificate = (id: string) => {
|
||||||
|
if (refreshingId) return; // Prevent multiple refreshes
|
||||||
|
setRefreshingId(id);
|
||||||
|
refreshMutation.mutate(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditCertificate = (certificate: SSLCertificate) => {
|
||||||
|
setSelectedCertificate(certificate);
|
||||||
|
setIsEditDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateCertificate = (certificate: SSLCertificate) => {
|
||||||
|
console.log("Handling certificate update with data:", certificate);
|
||||||
|
editMutation.mutate(certificate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteCertificate = (certificate: SSLCertificate) => {
|
||||||
|
deleteMutation.mutate(certificate.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefreshAll = async () => {
|
||||||
|
if (certificates.length === 0) {
|
||||||
|
toast.info("No certificates to refresh");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRefreshingAll(true);
|
||||||
|
toast.info(`Starting refresh of all ${certificates.length} certificates...`);
|
||||||
|
refreshAllMutation.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingState />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
|
||||||
|
<p>Error loading SSL certificate data.</p>
|
||||||
|
<Button onClick={() => queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] })}>Retry</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
|
||||||
|
<div className="flex flex-col flex-1">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-foreground">SSL & Domain Management</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">Monitor SSL certificates and their expiration dates</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleRefreshAll}
|
||||||
|
disabled={isRefreshingAll || refreshingId !== null}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshingAll ? 'animate-spin' : ''}`} />
|
||||||
|
Refresh All
|
||||||
|
{isRefreshingAll && (
|
||||||
|
<span className="absolute top-0 right-0 -mt-2 -mr-2 bg-primary text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
|
||||||
|
...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="text-primary-foreground"
|
||||||
|
onClick={() => setIsAddDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" /> Add Domain
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SSLCertificateStatusCards certificates={certificates} />
|
||||||
|
|
||||||
|
<div className="mt-6 flex-1 flex flex-col pb-6">
|
||||||
|
<SSLCertificatesTable
|
||||||
|
certificates={certificates}
|
||||||
|
onRefresh={handleRefreshCertificate}
|
||||||
|
refreshingId={refreshingId}
|
||||||
|
onEdit={handleEditCertificate}
|
||||||
|
onDelete={handleDeleteCertificate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add SSL Certificate</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<AddSSLCertificateForm
|
||||||
|
onSubmit={handleAddCertificate}
|
||||||
|
onCancel={() => setIsAddDialogOpen(false)}
|
||||||
|
isPending={addMutation.isPending}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{selectedCertificate && (
|
||||||
|
<Dialog open={isEditDialogOpen} onOpenChange={(open) => {
|
||||||
|
setIsEditDialogOpen(open);
|
||||||
|
if (!open) setSelectedCertificate(null);
|
||||||
|
}}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit SSL Certificate</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<EditSSLCertificateForm
|
||||||
|
certificate={selectedCertificate}
|
||||||
|
onSubmit={handleUpdateCertificate}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsEditDialogOpen(false);
|
||||||
|
setSelectedCertificate(null);
|
||||||
|
}}
|
||||||
|
isPending={editMutation.isPending}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
|
interface SSLStatusBadgeProps {
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SSLStatusBadge: React.FC<SSLStatusBadgeProps> = ({ status }) => {
|
||||||
|
let variant = "";
|
||||||
|
let label = "";
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "valid":
|
||||||
|
variant = "bg-green-500 hover:bg-green-600";
|
||||||
|
label = "Valid";
|
||||||
|
break;
|
||||||
|
case "expiring_soon":
|
||||||
|
variant = "bg-yellow-500 hover:bg-yellow-600";
|
||||||
|
label = "Expiring Soon";
|
||||||
|
break;
|
||||||
|
case "expired":
|
||||||
|
variant = "bg-red-500 hover:bg-red-600";
|
||||||
|
label = "Expired";
|
||||||
|
break;
|
||||||
|
case "pending":
|
||||||
|
variant = "bg-blue-500 hover:bg-blue-600";
|
||||||
|
label = "Pending";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
variant = "bg-gray-500 hover:bg-gray-600";
|
||||||
|
label = status.charAt(0).toUpperCase() + status.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge className={`${variant} text-white`}>
|
||||||
|
{label}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
import PocketBase from 'pocketbase';
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
// Dynamically detect API base URL from current host (for use in browser)
|
// Auto-detect base URL from browser location
|
||||||
const dynamicBaseUrl =
|
const dynamicBaseUrl =
|
||||||
typeof window !== 'undefined'
|
typeof window !== 'undefined'
|
||||||
? `${window.location.protocol}//${window.location.hostname}:8090`
|
? window.location.origin
|
||||||
: 'http://localhost:8090';
|
: 'http://localhost:8090';
|
||||||
|
|
||||||
// Define available API endpoints
|
// Define API endpoints
|
||||||
export const API_ENDPOINTS = {
|
export const API_ENDPOINTS = {
|
||||||
REMOTE: dynamicBaseUrl
|
REMOTE: dynamicBaseUrl
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the current endpoint from localStorage or use remote as default
|
|
||||||
export const getCurrentEndpoint = (): string => {
|
export const getCurrentEndpoint = (): string => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const savedEndpoint = localStorage.getItem('pocketbase_endpoint');
|
const savedEndpoint = localStorage.getItem('pocketbase_endpoint');
|
||||||
@@ -20,26 +19,19 @@ export const getCurrentEndpoint = (): string => {
|
|||||||
return API_ENDPOINTS.REMOTE;
|
return API_ENDPOINTS.REMOTE;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set the API endpoint and reinitialize PocketBase
|
|
||||||
export const setApiEndpoint = (endpoint: string): void => {
|
export const setApiEndpoint = (endpoint: string): void => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
localStorage.setItem('pocketbase_endpoint', endpoint);
|
localStorage.setItem('pocketbase_endpoint', endpoint);
|
||||||
window.location.reload(); // Reload to reinitialize PocketBase with new endpoint
|
window.location.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize the PocketBase client with the current API URL
|
|
||||||
export const pb = new PocketBase(getCurrentEndpoint());
|
export const pb = new PocketBase(getCurrentEndpoint());
|
||||||
|
|
||||||
// Helper to check if user is authenticated
|
export const isAuthenticated = () => pb.authStore.isValid;
|
||||||
export const isAuthenticated = () => {
|
|
||||||
return pb.authStore.isValid;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Export the auth store for use in components
|
|
||||||
export const authStore = pb.authStore;
|
export const authStore = pb.authStore;
|
||||||
|
|
||||||
// Configure PocketBase to persist authentication between page reloads
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const storedAuthData = localStorage.getItem('pocketbase_auth');
|
const storedAuthData = localStorage.getItem('pocketbase_auth');
|
||||||
if (storedAuthData) {
|
if (storedAuthData) {
|
||||||
@@ -52,13 +44,15 @@ if (typeof window !== 'undefined') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to authStore changes to persist authentication
|
|
||||||
pb.authStore.onChange(() => {
|
pb.authStore.onChange(() => {
|
||||||
if (pb.authStore.isValid) {
|
if (pb.authStore.isValid) {
|
||||||
localStorage.setItem('pocketbase_auth', JSON.stringify({
|
localStorage.setItem(
|
||||||
|
'pocketbase_auth',
|
||||||
|
JSON.stringify({
|
||||||
token: pb.authStore.token,
|
token: pb.authStore.token,
|
||||||
model: pb.authStore.model
|
model: pb.authStore.model
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('pocketbase_auth');
|
localStorage.removeItem('pocketbase_auth');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,11 +79,7 @@ const Login = () => {
|
|||||||
<div className="text-center relative">
|
<div className="text-center relative">
|
||||||
<div className="absolute right-0 top-0">
|
<div className="absolute right-0 top-0">
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button variant="outline" size="icon" aria-label="API Settings">
|
|
||||||
<Settings className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
{/* <PopoverContent className="w-[260px] sm:w-80"> //this allow test api connection in login page
|
{/* <PopoverContent className="w-[260px] sm:w-80"> //this allow test api connection in login page
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h4 className="font-medium">API Endpoint Settings</h4>
|
<h4 className="font-medium">API Endpoint Settings</h4>
|
||||||
@@ -109,38 +105,10 @@ const Login = () => {
|
|||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-xl sm:text-2xl font-bold tracking-tight text-foreground">{t("signInToYourAccount")}</h1>
|
<h1 className="text-xl sm:text-2xl font-bold tracking-tight text-foreground">{t("signInToYourAccount")}</h1>
|
||||||
<p className="mt-2 text-xs sm:text-sm text-muted-foreground">
|
|
||||||
{t("dontHaveAccount")} <a href="#" className="font-medium text-emerald-500 hover:text-emerald-400">{t("createOne")}</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full py-4 sm:py-6 flex items-center justify-center gap-2 bg-background"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4 sm:w-5 sm:h-5" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
||||||
fill="#4285F4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
||||||
fill="#34A853"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
||||||
fill="#FBBC05"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
||||||
fill="#EA4335"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span className="text-sm sm:text-base">{t("signInWithGoogle")}</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="mt-4 sm:mt-6 relative">
|
<div className="mt-4 sm:mt-6 relative">
|
||||||
<div className="absolute inset-0 flex items-center">
|
<div className="absolute inset-0 flex items-center">
|
||||||
<div className="w-full border-t border-border"></div>
|
<div className="w-full border-t border-border"></div>
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { authService } from "@/services/authService";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Header } from "@/components/dashboard/Header";
|
||||||
|
import { Sidebar } from "@/components/dashboard/Sidebar";
|
||||||
|
import { SSLDomainContent } from "@/components/ssl-domain/SSLDomainContent";
|
||||||
|
import { LoadingState } from "@/components/services/LoadingState";
|
||||||
|
import { fetchSSLCertificates, shouldRunDailyCheck, checkAllCertificatesAndNotify } from "@/services/sslCertificateService";
|
||||||
|
|
||||||
|
const SslDomain = () => {
|
||||||
|
// State for sidebar collapse functionality
|
||||||
|
const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false);
|
||||||
|
const toggleSidebar = () => setSidebarCollapsed(prev => !prev);
|
||||||
|
|
||||||
|
// Get current user
|
||||||
|
const currentUser = authService.getCurrentUser();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Fetch SSL certificates with error handling and debugging
|
||||||
|
const { data: certificates = [], isLoading, error, refetch } = useQuery({
|
||||||
|
queryKey: ['ssl-certificates'],
|
||||||
|
queryFn: async () => {
|
||||||
|
console.log("Fetching SSL certificates from SslDomain page...");
|
||||||
|
try {
|
||||||
|
const result = await fetchSSLCertificates();
|
||||||
|
console.log("SSL certificates fetch successful, count:", result.length);
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching SSL certificates from page:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchInterval: 300000, // Refresh every 5 minutes
|
||||||
|
retry: 3, // Retry failed requests 3 times
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check all SSL certificates once per day
|
||||||
|
useEffect(() => {
|
||||||
|
const checkCertificates = async () => {
|
||||||
|
// Check if we should run daily check
|
||||||
|
if (shouldRunDailyCheck()) {
|
||||||
|
console.log("Running daily SSL certificate check...");
|
||||||
|
await checkAllCertificatesAndNotify();
|
||||||
|
// Refresh certificate list after daily check
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run check when component mounts
|
||||||
|
checkCertificates();
|
||||||
|
}, [refetch]);
|
||||||
|
|
||||||
|
// Handle logout
|
||||||
|
const handleLogout = () => {
|
||||||
|
authService.logout();
|
||||||
|
navigate("/login");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show loading state while fetching data
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen overflow-hidden bg-background text-foreground">
|
||||||
|
<Sidebar collapsed={sidebarCollapsed} />
|
||||||
|
<div className="flex flex-col flex-1">
|
||||||
|
<Header
|
||||||
|
currentUser={currentUser}
|
||||||
|
onLogout={handleLogout}
|
||||||
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
|
toggleSidebar={toggleSidebar}
|
||||||
|
/>
|
||||||
|
<LoadingState />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error state
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen overflow-hidden bg-background text-foreground">
|
||||||
|
<Sidebar collapsed={sidebarCollapsed} />
|
||||||
|
<div className="flex flex-col flex-1">
|
||||||
|
<Header
|
||||||
|
currentUser={currentUser}
|
||||||
|
onLogout={handleLogout}
|
||||||
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
|
toggleSidebar={toggleSidebar}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col items-center justify-center h-full p-6">
|
||||||
|
<h2 className="text-xl font-bold mb-2">Error Loading SSL Certificates</h2>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
{error instanceof Error ? error.message : "Unknown error occurred"}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
|
||||||
|
onClick={() => refetch()}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render with data
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen overflow-hidden bg-background text-foreground">
|
||||||
|
<Sidebar collapsed={sidebarCollapsed} />
|
||||||
|
<div className="flex flex-col flex-1">
|
||||||
|
<Header
|
||||||
|
currentUser={currentUser}
|
||||||
|
onLogout={handleLogout}
|
||||||
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
|
toggleSidebar={toggleSidebar}
|
||||||
|
/>
|
||||||
|
<SSLDomainContent />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SslDomain;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
// Re-export all SSL-related functionality for domain SSL checking
|
||||||
|
// Use explicit re-exports to avoid naming conflicts
|
||||||
|
|
||||||
|
// SSL Checker service
|
||||||
|
export { checkSSLCertificate, checkSSLApi } from './sslCheckerService';
|
||||||
|
|
||||||
|
// SSL Status utilities
|
||||||
|
export { determineSSLStatus } from './sslStatusUtils';
|
||||||
|
|
||||||
|
// Primary export for fetchSSLCertificates
|
||||||
|
export { fetchSSLCertificates } from './sslFetchService';
|
||||||
|
|
||||||
|
// Certificate operations
|
||||||
|
export {
|
||||||
|
addSSLCertificate,
|
||||||
|
checkAndUpdateCertificate,
|
||||||
|
deleteSSLCertificate,
|
||||||
|
refreshAllCertificates
|
||||||
|
} from './sslCertificateOperations';
|
||||||
|
|
||||||
|
// SSL-specific notification service
|
||||||
|
export {
|
||||||
|
checkAllCertificatesAndNotify,
|
||||||
|
checkCertificateAndNotify,
|
||||||
|
shouldRunDailyCheck,
|
||||||
|
sendSSLNotification
|
||||||
|
} from './notification';
|
||||||
|
|
||||||
|
// Export types
|
||||||
|
export type { SSLCheckerResponse, SSLCertificate, AddSSLCertificateDto, SSLNotification } from './types';
|
||||||
|
|
||||||
|
// Export utility functions
|
||||||
|
export { normalizeDomain, createErrorResponse } from './sslCheckerUtils';
|
||||||
|
|
||||||
|
// Export checking mechanisms
|
||||||
|
export { checkWithFetch } from './sslPrimaryChecker';
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
// Export all SSL notification related functionality
|
||||||
|
export {
|
||||||
|
checkAllCertificatesAndNotify,
|
||||||
|
checkCertificateAndNotify,
|
||||||
|
testCertificateNotification
|
||||||
|
} from './sslCheckNotifier';
|
||||||
|
|
||||||
|
export { sendSSLNotification } from './sslNotificationSender';
|
||||||
|
export { shouldRunDailyCheck } from './sslScheduleUtils';
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
|
||||||
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
import { SSLCertificate } from "../types";
|
||||||
|
import { checkSSLCertificate } from "../sslCheckerService";
|
||||||
|
import { determineSSLStatus } from "../sslStatusUtils";
|
||||||
|
import { sendSSLNotification } from "./sslNotificationSender";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks all SSL certificates and sends notifications for expiring ones
|
||||||
|
* This should be called once per day
|
||||||
|
*/
|
||||||
|
export async function checkAllCertificatesAndNotify(): Promise<void> {
|
||||||
|
console.log("Starting daily SSL certificates check...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch all SSL certificates from database
|
||||||
|
const response = await pb.collection('ssl_certificates').getList(1, 100, {});
|
||||||
|
// Properly cast the items as SSLCertificate
|
||||||
|
const certificates = response.items as unknown as SSLCertificate[];
|
||||||
|
|
||||||
|
console.log(`Found ${certificates.length} certificates to check`);
|
||||||
|
|
||||||
|
// Check each certificate
|
||||||
|
for (const cert of certificates) {
|
||||||
|
await checkCertificateAndNotify(cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Daily SSL certificates check completed");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during SSL certificates daily check:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks a specific SSL certificate and sends notification if needed
|
||||||
|
* This respects the Warning and Expiry Thresholds set on the certificate
|
||||||
|
*/
|
||||||
|
export async function checkCertificateAndNotify(certificate: SSLCertificate): Promise<boolean> {
|
||||||
|
console.log(`Checking certificate for ${certificate.domain}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get fresh SSL data
|
||||||
|
const sslData = await checkSSLCertificate(certificate.domain);
|
||||||
|
if (!sslData || !sslData.result) {
|
||||||
|
console.error(`Failed to check SSL for ${certificate.domain}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract days left from the check result
|
||||||
|
const daysLeft = sslData.result.days_left || 0;
|
||||||
|
|
||||||
|
// Get threshold values (ensure they are numbers)
|
||||||
|
const warningThreshold = Number(certificate.warning_threshold) || 30;
|
||||||
|
const expiryThreshold = Number(certificate.expiry_threshold) || 7;
|
||||||
|
|
||||||
|
console.log(`Certificate ${certificate.domain} thresholds: warning=${warningThreshold}, expiry=${expiryThreshold}, days left=${daysLeft}`);
|
||||||
|
|
||||||
|
// Update status based on thresholds
|
||||||
|
const status = determineSSLStatus(daysLeft, warningThreshold, expiryThreshold);
|
||||||
|
|
||||||
|
// Check if we should send a notification based on thresholds
|
||||||
|
let shouldNotify = false;
|
||||||
|
let isCritical = false;
|
||||||
|
|
||||||
|
// Critical notifications - when days left is less than or equal to expiry threshold
|
||||||
|
if (daysLeft <= expiryThreshold) {
|
||||||
|
shouldNotify = true;
|
||||||
|
isCritical = true;
|
||||||
|
}
|
||||||
|
// Warning notifications - when days left is less than or equal to warning threshold but greater than expiry threshold
|
||||||
|
else if (daysLeft <= warningThreshold) {
|
||||||
|
shouldNotify = true;
|
||||||
|
isCritical = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${certificate.domain}: ${daysLeft} days left, status: ${status}, should notify: ${shouldNotify}, critical: ${isCritical}`);
|
||||||
|
|
||||||
|
// Update certificate data in database
|
||||||
|
const updateData: Partial<SSLCertificate> = {
|
||||||
|
days_left: daysLeft,
|
||||||
|
status: status,
|
||||||
|
// Other fields from the SSL check that should be updated
|
||||||
|
issuer_o: sslData.result.issuer_o || certificate.issuer_o,
|
||||||
|
valid_from: sslData.result.valid_from || certificate.valid_from,
|
||||||
|
valid_till: sslData.result.valid_till || certificate.valid_till,
|
||||||
|
validity_days: sslData.result.validity_days || certificate.validity_days,
|
||||||
|
cert_sans: sslData.result.cert_sans,
|
||||||
|
cert_alg: sslData.result.cert_alg,
|
||||||
|
serial_number: sslData.result.cert_sn
|
||||||
|
};
|
||||||
|
|
||||||
|
await pb.collection('ssl_certificates').update(certificate.id, updateData);
|
||||||
|
|
||||||
|
// Send notification if needed
|
||||||
|
if (shouldNotify && certificate.notification_channel) {
|
||||||
|
console.log(`Sending notification for ${certificate.domain}`);
|
||||||
|
|
||||||
|
// Different message based on expiry threshold
|
||||||
|
const message = isCritical
|
||||||
|
? `🚨 CRITICAL: SSL Certificate for ${certificate.domain} will expire in ${daysLeft} days!`
|
||||||
|
: `⚠️ WARNING: SSL Certificate for ${certificate.domain} will expire in ${daysLeft} days.`;
|
||||||
|
|
||||||
|
// Send the notification using our specialized SSL notification sender
|
||||||
|
const notificationSent = await sendSSLNotification(certificate, message, isCritical);
|
||||||
|
|
||||||
|
if (notificationSent) {
|
||||||
|
// Update last_notified timestamp
|
||||||
|
await pb.collection('ssl_certificates').update(certificate.id, {
|
||||||
|
last_notified: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Notification sent for ${certificate.domain}`);
|
||||||
|
// Show toast for manual checks
|
||||||
|
toast.success(`Notification sent for ${certificate.domain}`);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to send notification for ${certificate.domain}`);
|
||||||
|
// Show error toast for manual checks
|
||||||
|
toast.error(`Failed to send notification for ${certificate.domain}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (shouldNotify && !certificate.notification_channel) {
|
||||||
|
console.log(`No notification channel set for ${certificate.domain}, skipping notification`);
|
||||||
|
toast.info(`No notification channel set for ${certificate.domain}, skipping notification`);
|
||||||
|
} else {
|
||||||
|
console.log(`No notification needed for ${certificate.domain} (${daysLeft} days left)`);
|
||||||
|
// For manual checks, inform the user that thresholds weren't met
|
||||||
|
toast.info(`Certificate for ${certificate.domain} is valid (${daysLeft} days left)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error checking certificate for ${certificate.domain}:`, error);
|
||||||
|
toast.error(`Error checking certificate: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manual trigger for certificate notification test
|
||||||
|
* Useful for testing notification channels
|
||||||
|
*/
|
||||||
|
export async function testCertificateNotification(certificate: SSLCertificate): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
console.log(`Testing notification for ${certificate.domain}...`);
|
||||||
|
|
||||||
|
// Create test message
|
||||||
|
const message = `🧪 TEST: SSL Certificate notification for ${certificate.domain}.`;
|
||||||
|
|
||||||
|
// We set isCritical to false for test notifications
|
||||||
|
const notificationSent = await sendSSLNotification(certificate, message, false);
|
||||||
|
|
||||||
|
if (notificationSent) {
|
||||||
|
console.log(`Test notification sent for ${certificate.domain}`);
|
||||||
|
toast.success(`Test notification sent for ${certificate.domain}`);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to send test notification for ${certificate.domain}`);
|
||||||
|
toast.error(`Failed to send test notification for ${certificate.domain}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error sending test notification for ${certificate.domain}:`, error);
|
||||||
|
toast.error(`Error sending test notification: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
|
||||||
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
import { SSLCertificate, SSLNotification } from "../types";
|
||||||
|
import { AlertConfiguration } from "@/services/alertConfigService";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dedicated function to send SSL certificate notifications
|
||||||
|
*/
|
||||||
|
export async function sendSSLNotification(
|
||||||
|
certificate: SSLCertificate,
|
||||||
|
message: string,
|
||||||
|
isCritical: boolean
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Check if notification channel is set
|
||||||
|
if (!certificate.notification_channel) {
|
||||||
|
console.log(`No notification channel set for certificate: ${certificate.domain}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the notification configuration
|
||||||
|
const alertConfigRecord = await pb.collection('alert_configurations').getOne(certificate.notification_channel);
|
||||||
|
|
||||||
|
if (!alertConfigRecord) {
|
||||||
|
console.error(`Alert configuration not found for ID: ${certificate.notification_channel}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alertConfigRecord.enabled) {
|
||||||
|
console.log(`Alert configuration is disabled for certificate: ${certificate.domain}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert PocketBase record to AlertConfiguration
|
||||||
|
const alertConfig: AlertConfiguration = {
|
||||||
|
id: alertConfigRecord.id,
|
||||||
|
collectionId: alertConfigRecord.collectionId,
|
||||||
|
collectionName: alertConfigRecord.collectionName,
|
||||||
|
service_id: alertConfigRecord.service_id || "",
|
||||||
|
notification_type: alertConfigRecord.notification_type,
|
||||||
|
telegram_chat_id: alertConfigRecord.telegram_chat_id,
|
||||||
|
discord_webhook_url: alertConfigRecord.discord_webhook_url,
|
||||||
|
signal_number: alertConfigRecord.signal_number,
|
||||||
|
notify_name: alertConfigRecord.notify_name,
|
||||||
|
bot_token: alertConfigRecord.bot_token,
|
||||||
|
template_id: alertConfigRecord.template_id,
|
||||||
|
slack_webhook_url: alertConfigRecord.slack_webhook_url,
|
||||||
|
enabled: alertConfigRecord.enabled,
|
||||||
|
created: alertConfigRecord.created,
|
||||||
|
updated: alertConfigRecord.updated
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create an SSL notification object for tracking
|
||||||
|
const sslNotification: SSLNotification = {
|
||||||
|
certificateId: certificate.id,
|
||||||
|
domain: certificate.domain,
|
||||||
|
message: message,
|
||||||
|
isCritical: isCritical,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send the notification based on the notification type
|
||||||
|
return await sendNotificationByType(alertConfig, certificate, message, isCritical, sslNotification);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending SSL notification:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send notification based on notification channel type
|
||||||
|
*/
|
||||||
|
async function sendNotificationByType(
|
||||||
|
alertConfig: AlertConfiguration,
|
||||||
|
certificate: SSLCertificate,
|
||||||
|
message: string,
|
||||||
|
isCritical: boolean,
|
||||||
|
notification: SSLNotification
|
||||||
|
): Promise<boolean> {
|
||||||
|
switch (alertConfig.notification_type) {
|
||||||
|
case 'telegram':
|
||||||
|
return await sendTelegramNotification(alertConfig, certificate, message, isCritical);
|
||||||
|
// Add other notification types as they are implemented
|
||||||
|
// case 'discord':
|
||||||
|
// return await sendDiscordNotification(alertConfig, certificate, message, isCritical);
|
||||||
|
// case 'slack':
|
||||||
|
// return await sendSlackNotification(alertConfig, certificate, message, isCritical);
|
||||||
|
default:
|
||||||
|
console.log(`Notification type ${alertConfig.notification_type} not implemented yet for SSL certificates`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send notification via Telegram
|
||||||
|
*/
|
||||||
|
async function sendTelegramNotification(
|
||||||
|
alertConfig: AlertConfiguration,
|
||||||
|
certificate: SSLCertificate,
|
||||||
|
message: string,
|
||||||
|
isCritical: boolean
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!alertConfig.bot_token || !alertConfig.telegram_chat_id) {
|
||||||
|
console.error("Missing Telegram bot token or chat ID");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const telegramUrl = `https://api.telegram.org/bot${alertConfig.bot_token}/sendMessage`;
|
||||||
|
const severity = isCritical ? "🚨 CRITICAL" : "⚠️ WARNING";
|
||||||
|
|
||||||
|
// Enhanced message with more SSL certificate details
|
||||||
|
const enhancedMessage = `${severity}: SSL Certificate Alert
|
||||||
|
|
||||||
|
Domain: ${certificate.domain}
|
||||||
|
Days Remaining: ${certificate.days_left}
|
||||||
|
Expiration Date: ${certificate.valid_till}
|
||||||
|
Status: ${certificate.status.toUpperCase()}
|
||||||
|
|
||||||
|
${message}
|
||||||
|
|
||||||
|
This is an automated notification from your SSL certificate monitoring system.`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(telegramUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
chat_id: alertConfig.telegram_chat_id,
|
||||||
|
text: enhancedMessage,
|
||||||
|
parse_mode: 'Markdown'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result.ok === true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending Telegram notification:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Check if SSL certificate daily check should run
|
||||||
|
* This helps implement the once-per-day check logic
|
||||||
|
*/
|
||||||
|
export function shouldRunDailyCheck(): boolean {
|
||||||
|
const lastRunKey = 'ssl_daily_check_last_run';
|
||||||
|
const lastRun = localStorage.getItem(lastRunKey);
|
||||||
|
|
||||||
|
if (!lastRun) {
|
||||||
|
// First time running, save timestamp and run
|
||||||
|
localStorage.setItem(lastRunKey, new Date().toISOString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastRunDate = new Date(lastRun).setHours(0, 0, 0, 0);
|
||||||
|
const today = new Date().setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// If last run was not today, run again
|
||||||
|
if (lastRunDate < today) {
|
||||||
|
localStorage.setItem(lastRunKey, new Date().toISOString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already ran today
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
|
||||||
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
import type { AddSSLCertificateDto, SSLCertificate } from "./types";
|
||||||
|
import { checkSSLCertificate } from "./sslCheckerService";
|
||||||
|
import { determineSSLStatus } from "./sslStatusUtils";
|
||||||
|
import { checkCertificateAndNotify } from "./notification"; // Import notification service
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new SSL certificate to monitor
|
||||||
|
*/
|
||||||
|
export const addSSLCertificate = async (
|
||||||
|
certificateData: AddSSLCertificateDto
|
||||||
|
): Promise<SSLCertificate> => {
|
||||||
|
try {
|
||||||
|
// First check if the SSL certificate is valid and can be fetched
|
||||||
|
const sslData = await checkSSLCertificate(certificateData.domain);
|
||||||
|
|
||||||
|
if (!sslData || !sslData.result) {
|
||||||
|
throw new Error(`Could not fetch SSL certificate for ${certificateData.domain}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the data for saving to database
|
||||||
|
const data = {
|
||||||
|
domain: certificateData.domain,
|
||||||
|
issued_to: sslData.result.issued_to || certificateData.domain,
|
||||||
|
issuer_o: sslData.result.issuer_o || "",
|
||||||
|
status: determineSSLStatus(
|
||||||
|
sslData.result.days_left || 0,
|
||||||
|
certificateData.warning_threshold,
|
||||||
|
certificateData.expiry_threshold
|
||||||
|
),
|
||||||
|
cert_sans: sslData.result.cert_sans || "",
|
||||||
|
cert_alg: sslData.result.cert_alg || "",
|
||||||
|
serial_number: sslData.result.cert_sn || "",
|
||||||
|
valid_from: sslData.result.valid_from || new Date().toISOString(),
|
||||||
|
valid_till: sslData.result.valid_till || new Date().toISOString(),
|
||||||
|
validity_days: sslData.result.validity_days || 0,
|
||||||
|
days_left: sslData.result.days_left || 0,
|
||||||
|
warning_threshold: Number(certificateData.warning_threshold) || 30,
|
||||||
|
expiry_threshold: Number(certificateData.expiry_threshold) || 7,
|
||||||
|
notification_channel: certificateData.notification_channel || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save to database
|
||||||
|
const record = await pb.collection("ssl_certificates").create(data);
|
||||||
|
|
||||||
|
return record as unknown as SSLCertificate;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error adding SSL certificate:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and update a specific SSL certificate
|
||||||
|
*/
|
||||||
|
export const checkAndUpdateCertificate = async (
|
||||||
|
certificateId: string
|
||||||
|
): Promise<SSLCertificate> => {
|
||||||
|
try {
|
||||||
|
// Get the certificate from database
|
||||||
|
const certificate = await pb.collection("ssl_certificates").getOne(certificateId);
|
||||||
|
|
||||||
|
if (!certificate) {
|
||||||
|
throw new Error(`Certificate with ID ${certificateId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// After updating, check if notification should be sent
|
||||||
|
// This will respect the Warning and Expiry Thresholds
|
||||||
|
await checkCertificateAndNotify(updatedCertificate);
|
||||||
|
|
||||||
|
return updatedCertificate;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating SSL certificate:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an SSL certificate from monitoring
|
||||||
|
*/
|
||||||
|
export const deleteSSLCertificate = async (id: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
await pb.collection("ssl_certificates").delete(id);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting SSL certificate:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh all SSL certificates
|
||||||
|
*/
|
||||||
|
export const refreshAllCertificates = async (): Promise<{ success: number; failed: number }> => {
|
||||||
|
try {
|
||||||
|
const response = await pb.collection("ssl_certificates").getList(1, 100);
|
||||||
|
const certificates = response.items as unknown as SSLCertificate[];
|
||||||
|
|
||||||
|
console.log(`Refreshing ${certificates.length} certificates...`);
|
||||||
|
|
||||||
|
let success = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const cert of certificates) {
|
||||||
|
try {
|
||||||
|
await checkAndUpdateCertificate(cert.id);
|
||||||
|
success++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to refresh certificate ${cert.domain}:`, error);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success, failed };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refreshing certificates:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
import type { SSLCheckerResponse } from "./types";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { checkWithFetch } from "./sslPrimaryChecker";
|
||||||
|
import { normalizeDomain, createErrorResponse } from "./sslCheckerUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check SSL certificate for a domain
|
||||||
|
* Uses the reliable CORS proxy approach
|
||||||
|
*/
|
||||||
|
export const checkSSLCertificate = async (domain: string): Promise<SSLCheckerResponse> => {
|
||||||
|
try {
|
||||||
|
console.log(`Checking SSL certificate for domain: ${domain}`);
|
||||||
|
|
||||||
|
// Normalize domain (remove protocol if present)
|
||||||
|
const normalizedDomain = normalizeDomain(domain);
|
||||||
|
|
||||||
|
if (!normalizedDomain) {
|
||||||
|
throw new Error("Invalid domain provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the working CORS proxy approach
|
||||||
|
const result = await checkWithFetch(normalizedDomain);
|
||||||
|
console.log("SSL check result:", result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SSL check failed completely:", error);
|
||||||
|
toast.error(`SSL check failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
return createErrorResponse(domain, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check SSL certificate using the CORS proxy
|
||||||
|
* This is kept for backward compatibility
|
||||||
|
*/
|
||||||
|
export const checkSSLApi = async (domain: string): Promise<SSLCheckerResponse> => {
|
||||||
|
return checkSSLCertificate(domain);
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize domain by removing protocol if present
|
||||||
|
*/
|
||||||
|
export const normalizeDomain = (domain: string): string => {
|
||||||
|
if (!domain) return '';
|
||||||
|
|
||||||
|
// Remove any protocol (http://, https://)
|
||||||
|
return domain.replace(/^(https?:\/\/)/, '').trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create error response for SSL check failures
|
||||||
|
*/
|
||||||
|
export const createErrorResponse = (domain: string, error: unknown): any => {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown SSL check error';
|
||||||
|
|
||||||
|
const errorResponse = {
|
||||||
|
version: "1.0",
|
||||||
|
app: "ssl-checker",
|
||||||
|
host: domain,
|
||||||
|
response_time_sec: "0",
|
||||||
|
status: "error",
|
||||||
|
message: errorMessage,
|
||||||
|
error: errorMessage,
|
||||||
|
result: {
|
||||||
|
host: domain,
|
||||||
|
resolved_ip: domain,
|
||||||
|
issued_to: domain,
|
||||||
|
issued_o: null,
|
||||||
|
issuer_c: "Unknown",
|
||||||
|
issuer_o: "Unknown",
|
||||||
|
issuer_ou: null,
|
||||||
|
issuer_cn: "Unknown",
|
||||||
|
cert_sn: "Unknown",
|
||||||
|
cert_sha1: "",
|
||||||
|
cert_alg: "Unknown",
|
||||||
|
cert_ver: 0,
|
||||||
|
cert_sans: domain,
|
||||||
|
cert_exp: true,
|
||||||
|
cert_valid: false,
|
||||||
|
valid_from: "Unknown",
|
||||||
|
valid_till: "Unknown",
|
||||||
|
validity_days: 0,
|
||||||
|
days_left: 0,
|
||||||
|
valid_days_to_expire: 0,
|
||||||
|
hsts_header_enabled: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return errorResponse;
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
|
||||||
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
import { SSLCertificate } from "@/types/ssl.types";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch SSL certificates from PocketBase
|
||||||
|
*/
|
||||||
|
export const fetchSSLCertificates = async (): Promise<SSLCertificate[]> => {
|
||||||
|
try {
|
||||||
|
console.log("Fetching SSL certificates from PocketBase...");
|
||||||
|
|
||||||
|
// Using the direct API path to fetch SSL certificates
|
||||||
|
const endpoint = "/api/collections/ssl_certificates/records";
|
||||||
|
const params = {
|
||||||
|
page: 1,
|
||||||
|
perPage: 50,
|
||||||
|
sort: "-created",
|
||||||
|
cache: Date.now() // Prevent caching by adding a timestamp
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert params to query string
|
||||||
|
const queryString = new URLSearchParams(params as any).toString();
|
||||||
|
const fullEndpoint = `${endpoint}?${queryString}`;
|
||||||
|
|
||||||
|
console.log("Fetching SSL certificates from:", fullEndpoint);
|
||||||
|
const response = await pb.send(fullEndpoint, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Pragma': 'no-cache'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if response has expected structure
|
||||||
|
if (!response || !response.items) {
|
||||||
|
console.error("Invalid response format:", response);
|
||||||
|
toast.error("Failed to fetch SSL certificates: Invalid response format");
|
||||||
|
throw new Error("Invalid response format from PocketBase API");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Received SSL certificates:", response.items.length);
|
||||||
|
|
||||||
|
// Map items to SSLCertificate[] type with validation
|
||||||
|
return response.items.map(item => {
|
||||||
|
const cert = item as SSLCertificate;
|
||||||
|
|
||||||
|
// Log certificate details for debugging
|
||||||
|
console.log(`Certificate for ${cert.domain}: Issued by ${cert.issuer_o}, Days left: ${cert.days_left}`);
|
||||||
|
|
||||||
|
// Ensure dates are valid
|
||||||
|
try {
|
||||||
|
if (cert.valid_from) new Date(cert.valid_from).toISOString();
|
||||||
|
if (cert.valid_till) new Date(cert.valid_till).toISOString();
|
||||||
|
if (cert.last_notified) new Date(cert.last_notified).toISOString();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Invalid date found in certificate", cert.id, e);
|
||||||
|
// Fix invalid dates if needed
|
||||||
|
if (cert.valid_from && isNaN(new Date(cert.valid_from).getTime())) {
|
||||||
|
cert.valid_from = new Date().toISOString();
|
||||||
|
}
|
||||||
|
if (cert.valid_till && isNaN(new Date(cert.valid_till).getTime())) {
|
||||||
|
const futureDate = new Date();
|
||||||
|
futureDate.setFullYear(futureDate.getFullYear() + 1);
|
||||||
|
cert.valid_till = futureDate.toISOString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate days left if not provided
|
||||||
|
if (typeof cert.days_left !== 'number' && cert.valid_till) {
|
||||||
|
try {
|
||||||
|
const expirationDate = new Date(cert.valid_till);
|
||||||
|
const currentDate = new Date();
|
||||||
|
const diffTime = expirationDate.getTime() - currentDate.getTime();
|
||||||
|
cert.days_left = Math.ceil(diffTime / (1000 * 3600 * 24));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error calculating days_left for certificate", cert.id, e);
|
||||||
|
cert.days_left = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching SSL certificates:", error);
|
||||||
|
toast.error("Failed to fetch SSL certificates. Please try again later.");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
import { createErrorResponse } from "./sslCheckerUtils";
|
||||||
|
import type { SSLCheckerResponse } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check SSL certificate using fetch with CORS proxy
|
||||||
|
* This is our primary method that's proven to work
|
||||||
|
*/
|
||||||
|
export const checkWithFetch = async (domain: string): Promise<SSLCheckerResponse> => {
|
||||||
|
console.log(`Checking SSL via CORS proxy for: ${domain}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the working CORS proxy
|
||||||
|
const corsProxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(`https://ssl-checker.io/api/v1/check/${domain}`)}`;
|
||||||
|
console.log("Using CORS proxy for SSL check:", corsProxyUrl);
|
||||||
|
|
||||||
|
const proxyResponse = await fetch(corsProxyUrl);
|
||||||
|
if (!proxyResponse.ok) {
|
||||||
|
throw new Error(`CORS proxy request failed with status: ${proxyResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyData = await proxyResponse.json();
|
||||||
|
console.log("CORS proxy returned SSL data:", proxyData);
|
||||||
|
return proxyData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SSL check failed:", error);
|
||||||
|
return createErrorResponse(domain, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Determine SSL certificate status based on days left
|
||||||
|
*/
|
||||||
|
export const determineSSLStatus = (daysLeft: number, warningThreshold: number, expiryThreshold: number): string => {
|
||||||
|
if (daysLeft <= 0) {
|
||||||
|
return "expired";
|
||||||
|
} else if (daysLeft <= expiryThreshold) {
|
||||||
|
return "expiring_soon";
|
||||||
|
} else if (daysLeft > expiryThreshold) {
|
||||||
|
return "valid";
|
||||||
|
} else {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
|
||||||
|
// SSL Checker response type
|
||||||
|
export interface SSLCheckerResponse {
|
||||||
|
version: string;
|
||||||
|
app: string;
|
||||||
|
host: string;
|
||||||
|
response_time_sec: string;
|
||||||
|
status: string; // "ok", "error"
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
result: {
|
||||||
|
host: string;
|
||||||
|
resolved_ip?: string;
|
||||||
|
issued_to: string;
|
||||||
|
issued_o?: string | null;
|
||||||
|
issuer_c?: string;
|
||||||
|
issuer_o?: string | null;
|
||||||
|
issuer_ou?: string | null;
|
||||||
|
issuer_cn?: string;
|
||||||
|
cert_sn: string;
|
||||||
|
cert_sha1?: string;
|
||||||
|
cert_alg: string;
|
||||||
|
cert_ver?: number;
|
||||||
|
cert_sans: string;
|
||||||
|
cert_exp?: boolean;
|
||||||
|
cert_valid?: boolean;
|
||||||
|
valid_from?: string;
|
||||||
|
valid_till?: string;
|
||||||
|
validity_days?: number;
|
||||||
|
days_left?: number;
|
||||||
|
valid_days_to_expire?: number;
|
||||||
|
hsts_header_enabled?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSL Certificate DTO for adding new certificates
|
||||||
|
export interface AddSSLCertificateDto {
|
||||||
|
domain: string;
|
||||||
|
warning_threshold: number;
|
||||||
|
expiry_threshold: number;
|
||||||
|
notification_channel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSL Certificate model
|
||||||
|
export interface SSLCertificate {
|
||||||
|
id: string;
|
||||||
|
domain: string;
|
||||||
|
issued_to: string;
|
||||||
|
issuer_o: string;
|
||||||
|
status: string;
|
||||||
|
cert_sans?: string;
|
||||||
|
cert_alg?: string;
|
||||||
|
serial_number?: number | string;
|
||||||
|
valid_from: string;
|
||||||
|
valid_till: string;
|
||||||
|
validity_days: number;
|
||||||
|
days_left: number;
|
||||||
|
valid_days_to_expire?: number;
|
||||||
|
warning_threshold: number;
|
||||||
|
expiry_threshold: number;
|
||||||
|
notification_channel: string;
|
||||||
|
last_notified?: string;
|
||||||
|
created?: string;
|
||||||
|
updated?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSL specific notification types
|
||||||
|
export interface SSLNotification {
|
||||||
|
certificateId: string;
|
||||||
|
domain: string;
|
||||||
|
message: string;
|
||||||
|
isCritical: boolean;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
|
||||||
|
import { SSLCheckerResponse } from "./types";
|
||||||
|
|
||||||
|
// Calculate days remaining from expiration date
|
||||||
|
export function calculateDaysRemaining(validTo: string): number {
|
||||||
|
try {
|
||||||
|
const expirationDate = new Date(validTo);
|
||||||
|
const currentDate = new Date();
|
||||||
|
const diffTime = expirationDate.getTime() - currentDate.getTime();
|
||||||
|
return Math.ceil(diffTime / (1000 * 3600 * 24)); // Convert ms to days
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error calculating days remaining:", error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the certificate is valid
|
||||||
|
export function isValid(validTo: string): boolean {
|
||||||
|
try {
|
||||||
|
return new Date(validTo).getTime() > Date.now();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking certificate validity:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert results to our expected response format
|
||||||
|
export function convertResultToResponse(result: any): SSLCheckerResponse {
|
||||||
|
return {
|
||||||
|
version: "1.0",
|
||||||
|
app: "ssl-checker",
|
||||||
|
host: result.host || "",
|
||||||
|
response_time_sec: result.response_time_sec || "0.5",
|
||||||
|
status: result.status || "ok",
|
||||||
|
result: {
|
||||||
|
host: result.host || "",
|
||||||
|
issued_to: result.subject || result.host || "",
|
||||||
|
issuer_o: result.issuer || "Unknown",
|
||||||
|
cert_sn: result.serial_number || "0",
|
||||||
|
cert_alg: result.algorithm || "Unknown",
|
||||||
|
cert_sans: result.sans || "",
|
||||||
|
cert_exp: !result.is_valid,
|
||||||
|
cert_valid: result.is_valid || false,
|
||||||
|
valid_from: result.valid_from || new Date().toISOString(),
|
||||||
|
valid_till: result.valid_to || new Date().toISOString(),
|
||||||
|
validity_days: result.validity_days || 365,
|
||||||
|
days_left: result.days_remaining || 0,
|
||||||
|
valid_days_to_expire: result.days_remaining || 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
// This file re-exports all SSL certificate related services for backward compatibility
|
||||||
|
import {
|
||||||
|
checkSSLCertificate,
|
||||||
|
fetchSSLCertificates,
|
||||||
|
addSSLCertificate,
|
||||||
|
checkAndUpdateCertificate
|
||||||
|
} from './ssl';
|
||||||
|
|
||||||
|
import { determineSSLStatus } from './ssl/sslStatusUtils';
|
||||||
|
|
||||||
|
// Import from the new refactored location
|
||||||
|
import {
|
||||||
|
checkAllCertificatesAndNotify,
|
||||||
|
checkCertificateAndNotify,
|
||||||
|
shouldRunDailyCheck,
|
||||||
|
testCertificateNotification
|
||||||
|
} from './ssl/notification';
|
||||||
|
|
||||||
|
export {
|
||||||
|
checkSSLCertificate,
|
||||||
|
determineSSLStatus,
|
||||||
|
fetchSSLCertificates,
|
||||||
|
addSSLCertificate,
|
||||||
|
checkAndUpdateCertificate,
|
||||||
|
checkAllCertificatesAndNotify,
|
||||||
|
checkCertificateAndNotify,
|
||||||
|
shouldRunDailyCheck,
|
||||||
|
testCertificateNotification
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
// This file re-exports the SSL checker functionality from different implementations
|
||||||
|
// Primary implementation is in sslCheckerService.ts in the ssl folder
|
||||||
|
|
||||||
|
export { checkSSLCertificate, checkSSLApi } from './ssl/sslCheckerService';
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
@@ -13,6 +12,7 @@ export interface User {
|
|||||||
avatar?: string;
|
avatar?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
status?: string; // Added this field to support the backend status
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateUserData {
|
export interface CreateUserData {
|
||||||
@@ -89,17 +89,81 @@ export const userService = {
|
|||||||
return currentUser;
|
return currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Important fix: Don't send avatar field if it's a local path
|
// Handle email updates separately using the request-email-change endpoint
|
||||||
// PocketBase API doesn't accept local file paths as valid files
|
const hasEmailChange = cleanData.email !== undefined;
|
||||||
if (cleanData.avatar && typeof cleanData.avatar === 'string' && cleanData.avatar.startsWith('/upload/profile/')) {
|
const emailToUpdate = hasEmailChange ? cleanData.email : null;
|
||||||
console.log("Removing local avatar path from update data");
|
|
||||||
delete cleanData.avatar;
|
// Remove email from regular update if it's being changed
|
||||||
|
if (hasEmailChange) {
|
||||||
|
console.log("Email change detected, will handle separately:", emailToUpdate);
|
||||||
|
delete cleanData.email;
|
||||||
|
delete cleanData.emailVisibility; // Don't set this yet, it will be set after email change
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the PocketBase API to update the user
|
let updatedUser: User | null = null;
|
||||||
const result = await pb.collection('users').update(id, cleanData);
|
|
||||||
console.log("Update result:", result);
|
// Only perform the regular update if there are fields to update
|
||||||
return result as unknown as User;
|
if (Object.keys(cleanData).length > 0) {
|
||||||
|
console.log("Final update payload to PocketBase:", cleanData);
|
||||||
|
|
||||||
|
// Use the direct API approach for more control over the update process
|
||||||
|
const apiUrl = `${pb.baseUrl}/api/collections/users/records/${id}`;
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': pb.authStore.token
|
||||||
|
},
|
||||||
|
body: JSON.stringify(cleanData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error("PocketBase API error:", errorData);
|
||||||
|
throw new Error(errorData.message || "Failed to update user");
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedUser = await response.json() as unknown as User;
|
||||||
|
console.log("PocketBase update response for regular fields:", updatedUser);
|
||||||
|
} else {
|
||||||
|
// If no other fields to update, get the current user
|
||||||
|
updatedUser = await this.getUser(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now handle email change separately if needed
|
||||||
|
if (emailToUpdate) {
|
||||||
|
try {
|
||||||
|
console.log("Processing email change request for new email:", emailToUpdate);
|
||||||
|
// Use the request-email-change endpoint directly
|
||||||
|
const emailChangeUrl = `${pb.baseUrl}/api/collections/users/request-email-change`;
|
||||||
|
const emailChangeResponse = await fetch(emailChangeUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': pb.authStore.token
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
newEmail: emailToUpdate
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!emailChangeResponse.ok) {
|
||||||
|
const errorData = await emailChangeResponse.json();
|
||||||
|
console.error("Email change request failed:", errorData);
|
||||||
|
throw new Error(errorData.message || "Failed to request email change");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Email change request sent successfully");
|
||||||
|
// The email won't be updated immediately as verification is required
|
||||||
|
// We'll return the current user data, and email will be updated after verification
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to request email change:", error);
|
||||||
|
// Continue with the function and return updated user for the fields that were updated
|
||||||
|
// We don't throw here because we want to save the other fields even if email change fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedUser;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update user:", error);
|
console.error("Failed to update user:", error);
|
||||||
throw error; // Re-throw to handle in the component
|
throw error; // Re-throw to handle in the component
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ export const enTranslations = {
|
|||||||
signInToYourAccount: "Sign in to your account",
|
signInToYourAccount: "Sign in to your account",
|
||||||
dontHaveAccount: "Don't have an account?",
|
dontHaveAccount: "Don't have an account?",
|
||||||
createOne: "Create one",
|
createOne: "Create one",
|
||||||
signInWithGoogle: "Sign in with Google",
|
//signInWithGoogle: "Sign in with Google",
|
||||||
orContinueWith: "OR",
|
orContinueWith: "To Access the CheckCle's Dashboard",
|
||||||
email: "Email",
|
email: "Email",
|
||||||
password: "Password",
|
password: "Password",
|
||||||
forgot: "Forgot?",
|
forgot: "Forgot?",
|
||||||
@@ -72,5 +72,46 @@ export const enTranslations = {
|
|||||||
viewOnGithub: "View on GitHub",
|
viewOnGithub: "View on GitHub",
|
||||||
viewDocumentation: "View Documentation",
|
viewDocumentation: "View Documentation",
|
||||||
followOnX: "Follow on X",
|
followOnX: "Follow on X",
|
||||||
joinDiscord: "Join Discord"
|
joinDiscord: "Join Discord",
|
||||||
|
// Quick Actions Dialog translations
|
||||||
|
quickActions: "Quick Actions",
|
||||||
|
quickActionsDescription: "Access common monitoring operations and features quickly. Select an action below to get started.",
|
||||||
|
close: "Close",
|
||||||
|
quickTips: "Quick Tips",
|
||||||
|
// Website Monitoring Card
|
||||||
|
monitorWebsite: "Website Monitoring",
|
||||||
|
monitorWebsiteDesc: "Monitor website availability and performance with detailed metrics and alerts.",
|
||||||
|
monitorMultipleEndpoints: "Monitor multiple endpoints",
|
||||||
|
trackResponseTimes: "Track response times",
|
||||||
|
setCustomAlerts: "Set custom alerts",
|
||||||
|
// Server Monitoring Card
|
||||||
|
monitorServer: "Server Monitoring",
|
||||||
|
monitorServerDesc: "Track server health metrics including CPU, memory, disk usage, and network traffic.",
|
||||||
|
monitorCPURam: "Monitor CPU & RAM usage",
|
||||||
|
trackNetworkMetrics: "Track network metrics",
|
||||||
|
viewProcesses: "View active processes",
|
||||||
|
// SSL Certificate Card
|
||||||
|
sslCertificate: "SSL Certificate",
|
||||||
|
sslCertificateDesc: "Monitor SSL certificate validity and receive alerts before expiration dates.",
|
||||||
|
trackSSLExpiration: "Track SSL expiration dates",
|
||||||
|
getEarlyRenewal: "Get early renewal notices",
|
||||||
|
monitorMultipleDomains: "Monitor multiple domains",
|
||||||
|
// Incidents Management Card
|
||||||
|
incidentsManagement: "Incidents Management",
|
||||||
|
incidentsManagementDesc: "Create, track, and manage incidents including assignment and resolution workflows.",
|
||||||
|
createTrackIncidents: "Create & track incidents",
|
||||||
|
assignIncidents: "Assign incidents to team",
|
||||||
|
monitorResolution: "Monitor resolution progress",
|
||||||
|
// Operational Status Card
|
||||||
|
operationalStatus: "Operational Status",
|
||||||
|
operationalStatusDesc: "Create and manage public status pages to communicate system health to users.",
|
||||||
|
createCustomStatus: "Create custom status pages",
|
||||||
|
displayRealTime: "Display real-time metrics",
|
||||||
|
shareIncidentUpdates: "Share incident updates",
|
||||||
|
// Reports & Analytics Card
|
||||||
|
reportsAnalytics: "Reports & Analytics",
|
||||||
|
reportsAnalyticsDesc: "Generate reports on system performance, uptime history, and incident metrics.",
|
||||||
|
generateUptimeReports: "Generate uptime reports",
|
||||||
|
analyzePerformance: "Analyze performance trends",
|
||||||
|
exportMonitoringData: "Export monitoring data"
|
||||||
};
|
};
|
||||||
@@ -47,8 +47,8 @@ export const kmTranslations = {
|
|||||||
signInToYourAccount: "ចូលគណនីរបស់អ្នក",
|
signInToYourAccount: "ចូលគណនីរបស់អ្នក",
|
||||||
dontHaveAccount: "មិនមានគណនីមែនទេ?",
|
dontHaveAccount: "មិនមានគណនីមែនទេ?",
|
||||||
createOne: "បង្កើតមួយ",
|
createOne: "បង្កើតមួយ",
|
||||||
signInWithGoogle: "ចូលជាមួយ Google",
|
//signInWithGoogle: "ចូលជាមួយ Google",
|
||||||
orContinueWith: "ឬ",
|
orContinueWith: "ជាមួយឆែកខល",
|
||||||
email: "អ៊ីមែល",
|
email: "អ៊ីមែល",
|
||||||
password: "ពាក្យសម្ងាត់",
|
password: "ពាក្យសម្ងាត់",
|
||||||
forgot: "ភ្លេច?",
|
forgot: "ភ្លេច?",
|
||||||
@@ -72,5 +72,46 @@ export const kmTranslations = {
|
|||||||
viewOnGithub: "មើលលើ GitHub",
|
viewOnGithub: "មើលលើ GitHub",
|
||||||
viewDocumentation: "មើលឯកសារ",
|
viewDocumentation: "មើលឯកសារ",
|
||||||
followOnX: "តាមដានលើ X",
|
followOnX: "តាមដានលើ X",
|
||||||
joinDiscord: "ចូលរួមក្នុង Discord"
|
joinDiscord: "ចូលរួមក្នុង Discord",
|
||||||
|
// Quick Actions Dialog translations
|
||||||
|
quickActions: "សកម្មភាពរហ័ស",
|
||||||
|
quickActionsDescription: "ចូលប្រើប្រតិបត្តិការត្រួតពិនិត្យ និងមុខងារទូទៅយ៉ាងរហ័ស។ ជ្រើសរើសសកម្មភាពខាងក្រោមដើម្បីចាប់ផ្តើម។",
|
||||||
|
close: "បិទ",
|
||||||
|
quickTips: "គន្លឹះរហ័ស",
|
||||||
|
// Website Monitoring Card
|
||||||
|
monitorWebsite: "ការត្រួតពិនិត្យគេហទំព័រ",
|
||||||
|
monitorWebsiteDesc: "ត្រួតពិនិត្យការអាចប្រើបាននៃគេហទំព័រ និងដំណើរការដោយមានព័ត៌មានលម្អិត និងការជូនដំណឹង។",
|
||||||
|
monitorMultipleEndpoints: "ត្រួតពិនិត្យចំណុចចប់ច្រើន",
|
||||||
|
trackResponseTimes: "តាមដានពេលវេលាឆ្លើយតប",
|
||||||
|
setCustomAlerts: "កំណត់ការជូនដំណឹងផ្ទាល់ខ្លួន",
|
||||||
|
// Server Monitoring Card
|
||||||
|
monitorServer: "ការត្រួតពិនិត្យម៉ាស៊ីនមេ",
|
||||||
|
monitorServerDesc: "តាមដានសុខភាពម៉ាស៊ីនមេរួមទាំងការប្រើប្រាស់ CPU, អង្គចងចាំ, ថាស, និងចរាចរណ៍បណ្តាញ។",
|
||||||
|
monitorCPURam: "ត្រួតពិនិត្យការប្រើប្រាស់ CPU និង RAM",
|
||||||
|
trackNetworkMetrics: "តាមដានចរាចរណ៍បណ្តាញ",
|
||||||
|
viewProcesses: "មើលដំណើរការសកម្ម",
|
||||||
|
// SSL Certificate Card
|
||||||
|
sslCertificate: "វិញ្ញាបនបត្រ SSL",
|
||||||
|
sslCertificateDesc: "ត្រួតពិនិត្យសុពលភាពវិញ្ញាបនបត្រ SSL និងទទួលបានការជូនដំណឹងមុនថ្ងៃផុតកំណត់។",
|
||||||
|
trackSSLExpiration: "តាមដានកាលបរិច្ឆេទផុតកំណត់ SSL",
|
||||||
|
getEarlyRenewal: "ទទួលបានការជូនដំណឹងបន្តមុនពេល",
|
||||||
|
monitorMultipleDomains: "ត្រួតពិនិត្យដែនជាច្រើន",
|
||||||
|
// Incidents Management Card
|
||||||
|
incidentsManagement: "ការគ្រប់គ្រងឧប្បត្តិហេតុ",
|
||||||
|
incidentsManagementDesc: "បង្កើត, តាមដាន និងគ្រប់គ្រងឧប្បត្តិហេតុរួមទាំងការចាត់តាំង និងដំណើរការដោះស្រាយ។",
|
||||||
|
createTrackIncidents: "បង្កើត និងតាមដានឧប្បត្តិហេតុ",
|
||||||
|
assignIncidents: "ចាត់តាំងឧប្បត្តិហេតុទៅក្រុម",
|
||||||
|
monitorResolution: "តាមដានដំណើរការដោះស្រាយ",
|
||||||
|
// Operational Status Card
|
||||||
|
operationalStatus: "ស្ថានភាពប្រតិបត្តិការ",
|
||||||
|
operationalStatusDesc: "បង្កើត និងគ្រប់គ្រងទំព័រស្ថានភាពសាធារណៈដើម្បីទំនាក់ទំនងសុខភាពប្រព័ន្ធទៅកាន់អ្នកប្រើប្រាស់។",
|
||||||
|
createCustomStatus: "បង្កើតទំព័រស្ថានភាពតាមបំណង",
|
||||||
|
displayRealTime: "បង្ហាញទិន្នន័យជាក់ស្តែង",
|
||||||
|
shareIncidentUpdates: "ចែករំលែកព័ត៌មានថ្មីៗអំពីឧប្បត្តិហេតុ",
|
||||||
|
// Reports & Analytics Card
|
||||||
|
reportsAnalytics: "របាយការណ៍និងការវិភាគ",
|
||||||
|
reportsAnalyticsDesc: "បង្កើតរបាយការណ៍អំពីដំណើរការប្រព័ន្ធ, ប្រវត្តិនៃពេលវេលាដំណើរការ, និងទិន្នន័យឧប្បត្តិហេតុ។",
|
||||||
|
generateUptimeReports: "បង្កើតរបាយការណ៍ពេលវេលាដំណើរការ",
|
||||||
|
analyzePerformance: "វិភាគនិន្នាការដំណើរការ",
|
||||||
|
exportMonitoringData: "នាំចេញទិន្នន័យត្រួតពិនិត្យ"
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
export interface SSLCertificate {
|
||||||
|
id: string;
|
||||||
|
domain: string;
|
||||||
|
issued_to: string;
|
||||||
|
issuer_o: string;
|
||||||
|
status: string;
|
||||||
|
cert_sans?: string;
|
||||||
|
cert_alg?: string;
|
||||||
|
serial_number?: number | string;
|
||||||
|
valid_from: string;
|
||||||
|
valid_till: string;
|
||||||
|
validity_days: number;
|
||||||
|
days_left: number;
|
||||||
|
valid_days_to_expire?: number;
|
||||||
|
warning_threshold: number;
|
||||||
|
expiry_threshold: number;
|
||||||
|
notification_channel: string;
|
||||||
|
last_notified?: string;
|
||||||
|
created?: string;
|
||||||
|
updated?: string;
|
||||||
|
// New fields based on the provided structure
|
||||||
|
collectionId?: string;
|
||||||
|
collectionName?: string;
|
||||||
|
resolved_ip?: string;
|
||||||
|
issuer_cn?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddSSLCertificateDto {
|
||||||
|
domain: string;
|
||||||
|
warning_threshold: number;
|
||||||
|
expiry_threshold: number;
|
||||||
|
notification_channel: string;
|
||||||
|
}
|
||||||
Vendored
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
+5098
-4795
File diff suppressed because it is too large
Load Diff
@@ -229,7 +229,7 @@
|
|||||||
"name": "email",
|
"name": "email",
|
||||||
"onlyDomains": null,
|
"onlyDomains": null,
|
||||||
"presentable": false,
|
"presentable": false,
|
||||||
"required": true,
|
"required": false,
|
||||||
"system": true,
|
"system": true,
|
||||||
"type": "email"
|
"type": "email"
|
||||||
},
|
},
|
||||||
@@ -299,6 +299,20 @@
|
|||||||
"system": false,
|
"system": false,
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "select2063623452",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"name": "status",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "select",
|
||||||
|
"values": [
|
||||||
|
"Active",
|
||||||
|
"Inactive"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"id": "autodate2990389176",
|
"id": "autodate2990389176",
|
||||||
@@ -3186,7 +3200,7 @@
|
|||||||
"id": "text2910474005",
|
"id": "text2910474005",
|
||||||
"max": 0,
|
"max": 0,
|
||||||
"min": 0,
|
"min": 0,
|
||||||
"name": "issuer",
|
"name": "issuer_o",
|
||||||
"pattern": "",
|
"pattern": "",
|
||||||
"presentable": false,
|
"presentable": false,
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
@@ -3194,17 +3208,6 @@
|
|||||||
"system": false,
|
"system": false,
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "date4098502798",
|
|
||||||
"max": "",
|
|
||||||
"min": "",
|
|
||||||
"name": "expiration_date",
|
|
||||||
"presentable": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "date"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"autogeneratePattern": "",
|
"autogeneratePattern": "",
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
@@ -3268,6 +3271,146 @@
|
|||||||
"system": false,
|
"system": false,
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "date2077745078",
|
||||||
|
"max": "",
|
||||||
|
"min": "",
|
||||||
|
"name": "valid_from",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number227839714",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "serial_number",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text2790380089",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "issued_to",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "date3449712486",
|
||||||
|
"max": "",
|
||||||
|
"min": "",
|
||||||
|
"name": "valid_till",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number1533826704",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "validity_days",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number4007784154",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "days_left",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number437829758",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "valid_days_to_expire",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3452125334",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "resolved_ip",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text2712104570",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "issuer_cn",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text1754636037",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "cert_alg",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text1208359189",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "cert_sans",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"id": "autodate2990389176",
|
"id": "autodate2990389176",
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user