33 Commits

Author SHA1 Message Date
Tola Leng c8d640ebb5 Updated SSL Domain page and API 2025-05-16 22:22:03 +08:00
Tola Leng 9f38e23b22 Implement the SSL Certs Service Checker 2025-05-16 22:21:39 +08:00
Tola Leng 1ea81a23b2 feat: Implement dropdown actions in SSL table
Action column in the SSL & Domain table to use a dropdown menu for View, Check, Edit, and Delete actions. And updated SSL Dashboard UI Component
2025-05-16 22:20:57 +08:00
Tola Leng 8ee933563e Notification (Telegram) for SSL notification thresholds
Ensure Telegram notifications respect warning/expiry thresholds, including manual checks.
2025-05-16 22:18:35 +08:00
Tola Leng 687619ce0d Feat: Implement SSL notification channel selection
- Added notification channel selection to the Add SSL Certificate form.
- Display available Telegram notification channels.
- Integrate SSL expiry and warning thresholds for notifications.
- Ensure daily auto-checks and manual check button functionality.
2025-05-16 22:17:02 +08:00
Tola Leng 9e931ad439 Updated the SSL detail dialog in SSLCertificatesTable.tsx 2025-05-16 22:14:05 +08:00
Tola Leng c7fd7f4fbe Updated the SSL detail dialog in SSLCertificatesTable.tsx to display data based on the ssl DB structure, including fields like collectionId, collectionName, and other properties from the PocketBase API response. 2025-05-16 22:13:03 +08:00
Tola Leng eafc4c33ab Remove unused sslUtils.ts file
Remove the file /src/utils/sslUtils.ts as it is no longer used.
2025-05-16 22:10:34 +08:00
Tola Leng 7644480782 Upgraded the pocketbase version V0.28.1 2025-05-16 22:09:44 +08:00
Tola Leng 1515ae56af updated data for server backend 2025-05-16 22:08:50 +08:00
Tola Leng caf3e73dc3 Update database Schemar for SSL & Domain 2025-05-16 22:05:15 +08:00
Tola Leng 98c86ff5dd Display user's active/inactive status from the backend in the profile details. 2025-05-15 22:45:28 +08:00
Tola Leng 42441b615a Feat: Display user status 2025-05-15 22:44:07 +08:00
Tola Leng e5651c152a Update Profile Form for Changing email requires verification. A verification email will be sent. 2025-05-15 22:32:16 +08:00
Tola Leng fc84922959 Update to handle email change using the request-email-change endpoint 2025-05-15 22:31:27 +08:00
Tola Leng 093300c38a 📜 Updated Official CheckCle Website and Demo Link 2025-05-15 20:45:00 +08:00
Tola Leng 2dcedf7b30 🔐 Added Security Policy 2025-05-15 00:12:16 +08:00
Tola Leng 38e06b9bec 📜 Updated Docker Run Command 2025-05-14 21:05:04 +08:00
Tola Leng 9f2ab1a79b Updated ENG & KH Translations 2025-05-12 23:24:53 +08:00
Tola Leng 54ebe0604d Updated Login page (Removed unnecessary and unsued button) 2025-05-12 23:23:40 +08:00
Tola Leng cf28eb41d6 Fixed: fix to correct API base URL at runtime without hardcoding the domain or port. Now it works under any custom domain via reverse proxy 2025-05-12 23:21:46 +08:00
Tola Leng 8ef3614908 Remove .DS_Store from repository 2025-05-12 18:27:02 +08:00
Tola Leng 7773c1bc66 features: Implement domain validation in SSL form
Integrate domain validation logic into the SSL Domain Form, including checks for warnings, expiry, and overall domain validity.
2025-05-11 20:52:39 +08:00
Tola Leng 580dbc424c Implement the SSL & Domain Features (Domain List, Real-time information for Expiration Date, Issuer, and Days Left.) 2025-05-11 20:51:38 +08:00
Tola Leng 981bc972f0 📜 Update Project Developement Progress 2025-05-11 17:37:05 +08:00
Tola Leng a9043e8088 📝 Updated License 2025-05-11 14:44:11 +08:00
Tola Leng 1ae961c5d9 Removed Dockerfile while we keep only one file in Docker directory 2025-05-10 22:52:47 +07:00
Tola Leng 51226a7afe Merge branch 'main' of https://github.com/operacle/checkcle 2025-05-10 22:47:48 +07:00
Tola Leng dfd57a6b62 Updated Readme 2025-05-10 22:47:21 +07:00
Tola Leng 12f4d18521 Added text into transaction (English and Khmer) 2025-05-10 21:08:22 +08:00
Tola Leng aea32f10b6 Add quick guide dialog to header (Implement a dialog to display a quick guide) 2025-05-10 21:06:23 +08:00
Tola Leng a16b1d228a Added links to header icons to the documentation, GitHub, X (Twitter), and Discord icons in the header. 2025-05-10 20:39:40 +08:00
Tola Leng 930622b5be Updated the AboutSystem component to include the correct links. 2025-05-10 20:35:10 +08:00
47 changed files with 8062 additions and 4963 deletions
Vendored
BIN
View File
Binary file not shown.
-44
View File
@@ -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
View File
@@ -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,
+11 -9
View File
@@ -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
View File
@@ -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 **35 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)
+9
View File
@@ -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>
);
};
+10 -16
View File
@@ -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');
} }
+2 -34
View File
@@ -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>
+126
View File
@@ -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;
+37
View File
@@ -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";
}
};
+74
View File
@@ -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;
}
+51
View File
@@ -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';
+74 -10
View File
@@ -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
+44 -3
View File
@@ -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"
}; };
+44 -3
View File
@@ -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: "នាំចេញទិន្នន័យត្រួតពិនិត្យ"
}; };
+34
View File
@@ -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;
}
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5098 -4795
View File
File diff suppressed because it is too large Load Diff
+156 -13
View File
@@ -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",
BIN
View File
Binary file not shown.