From 0b01318aa6e4342df8c422ba16cd40476ea6fab2 Mon Sep 17 00:00:00 2001 From: Alaguraj0361 Date: Fri, 19 Dec 2025 22:05:09 +0530 Subject: [PATCH] Implement SSL checker page with Hestia API integration, add email expiry alerts, and introduce new header and sidebar layout components. --- .../reverse-proxy/page.tsx | 2 +- app/(defaults)/ssl-checker/page.tsx | 363 +++-- app/api/hestia/[...path]/route.ts | 56 + components/layouts/header.tsx | 8 +- components/layouts/sidebar.tsx | 6 +- package-lock.json | 1336 ++++++++++++++++- package.json | 2 + public/assets/images/logo.webp | Bin 0 -> 2962 bytes 8 files changed, 1662 insertions(+), 111 deletions(-) rename app/{(auth) => (defaults)}/reverse-proxy/page.tsx (99%) create mode 100644 app/api/hestia/[...path]/route.ts create mode 100644 public/assets/images/logo.webp diff --git a/app/(auth)/reverse-proxy/page.tsx b/app/(defaults)/reverse-proxy/page.tsx similarity index 99% rename from app/(auth)/reverse-proxy/page.tsx rename to app/(defaults)/reverse-proxy/page.tsx index 7156460..d8c41d5 100644 --- a/app/(auth)/reverse-proxy/page.tsx +++ b/app/(defaults)/reverse-proxy/page.tsx @@ -66,7 +66,7 @@ export default function ReverseProxyPage() { return (
{ - const [data, setData] = useState([]); +const SSLChecker = () => { + const [domains, setDomains] = useState([]); + const [filteredDomains, setFilteredDomains] = useState([]); const [loading, setLoading] = useState(true); + const [actionLoading, setActionLoading] = useState<{ [key: string]: string | null }>({}); + const [globalLoading, setGlobalLoading] = useState(false); + const [checkingExpiry, setCheckingExpiry] = useState(false); + const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null); + + // Filter states const [search, setSearch] = useState(""); - const [lastChecked, setLastChecked] = useState(""); + const [filterStatus, setFilterStatus] = useState<'all' | 'secured' | 'unsecured' | 'expiring'>('all'); - // Fetch SSL Data - const loadSSL = async () => { + const fetchDomains = async () => { + setLoading(true); try { - setLoading(true); - - const response = await axios.get("http://localhost:3010/api/ssl/status"); - setData(response.data); - - const now = new Date().toLocaleString(); - setLastChecked(now); + const res = await axios.get(`${API_BASE_URL}/dns/domains`); + const rawParsed = res.data.rawParsed || []; + // If rawParsed is empty but domains exists (simple list), mapped it to objects for consistency + // but user provided rawParsed structure, so we prioritize that. + setDomains(rawParsed); + setFilteredDomains(rawParsed); } catch (error) { - console.error("SSL API Error:", error); + console.error("Error fetching domains:", error); + setMessage({ type: 'error', text: "Failed to fetch domains." }); } finally { setLoading(false); } }; - // Auto load on mount useEffect(() => { - loadSSL(); + fetchDomains(); }, []); - // Filter domains - const filteredData = data.filter((item) => - item.domain.toLowerCase().includes(search.toLowerCase()) - ); + // Filter Logic + useEffect(() => { + let result = domains; + + // Search + if (search) { + result = result.filter(d => d.domain.toLowerCase().includes(search.toLowerCase())); + } + + // Status Filter + if (filterStatus === 'secured') { + result = result.filter(d => d.ssl && d.ssl.success); + } else if (filterStatus === 'unsecured') { + result = result.filter(d => !d.ssl || !d.ssl.success); + } else if (filterStatus === 'expiring') { + const today = new Date(); + const oneDayMs = 24 * 60 * 60 * 1000; + result = result.filter(d => { + if (!d.ssl || !d.ssl.success || !d.ssl.notAfter) return false; + const expiryDate = new Date(d.ssl.notAfter); + const diffTime = expiryDate.getTime() - today.getTime(); + const diffDays = Math.ceil(diffTime / oneDayMs); + return diffDays <= 7; // Show warning for 7 days in UI, though email is 1 day + }); + } + + setFilteredDomains(result); + }, [search, filterStatus, domains]); + + + const handleAction = async (domain: string, action: 'add' | 'remove' | 'refresh') => { + setActionLoading(prev => ({ ...prev, [domain]: action })); + setMessage(null); + try { + const endpoint = action === 'add' ? '/ssl/add' + : action === 'remove' ? '/ssl/remove' + : '/ssl/refresh'; + + // Note: The API likely expects just { domain: "example.com" } + await axios.post(`${API_BASE_URL}${endpoint}`, { domain }); + setMessage({ type: 'success', text: `Successfully ${action}ed SSL for ${domain}` }); + + // Refresh list to update status + fetchDomains(); + } catch (error: any) { + console.error(`Error during ${action} SSL:`, error); + setMessage({ type: 'error', text: `Failed to ${action} SSL for ${domain}: ${error.response?.data?.message || error.message}` }); + } finally { + setActionLoading(prev => ({ ...prev, [domain]: null })); + } + }; + + const handleRefreshAll = async () => { + setGlobalLoading(true); + setMessage(null); + try { + await axios.post(`${API_BASE_URL}/ssl/refresh-all`); + setMessage({ type: 'success', text: "Initiated SSL refresh for all domains." }); + setTimeout(fetchDomains, 2000); // Wait a bit then refresh list + } catch (error: any) { + console.error("Error refreshing all SSL:", error); + setMessage({ type: 'error', text: `Failed to refresh all SSL: ${error.response?.data?.message || error.message}` }); + } finally { + setGlobalLoading(false); + } + }; + + const handleCheckExpiryAndNotify = async () => { + setCheckingExpiry(true); + setMessage(null); + try { + const res = await axios.post('/api/send-expiry-alert', { domains }); + if (res.data.success) { + setMessage({ type: 'success', text: res.data.message }); + } else { + setMessage({ type: 'error', text: "Failed to process expiry check." }); + } + } catch (error: any) { + console.error("Error checking expiry:", error); + setMessage({ type: 'error', text: "Error checking expiry: " + error.message }); + } finally { + setCheckingExpiry(false); + } + }; + + const getDaysRemaining = (dateStr?: string) => { + if (!dateStr) return null; + const today = new Date(); + const expiryDate = new Date(dateStr); + const diffTime = expiryDate.getTime() - today.getTime(); + return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + }; return ( -
-

- SSL Expiry Status Checker -

- - {/* Search + Refresh */} -
- setSearch(e.target.value)} - className="w-1/3 p-2 border border-gray-300 rounded-md focus:ring focus:ring-blue-300" - /> - - +
+
+
SSL Management
+
+ + +
- {/* Last checked time */} -

- Last checked: {lastChecked} -

- - {/* Loading Shimmer */} - {loading ? ( -
- Checking SSL certificates... -
- ) : ( -
- - - - - - - - - - - {filteredData.map((d) => ( - - - - - - ))} - -
- Domain - - Valid Until - - Status -
{d.domain}{d.validTo} - - {d.expired ? "Expired" : "Valid"} - -
- - {filteredData.length === 0 && ( -

- No matching domains found. -

- )} + {message && ( +
+ {message.text}
)} + +
+
+ setSearch(e.target.value)} + /> +
+
+ + + + +
+
+ +
+ + + + + + + + + + + + {loading ? ( + + + + ) : filteredDomains.length === 0 ? ( + + + + ) : ( + filteredDomains.map((item: any, index: number) => { + const hasSSL = item.ssl && item.ssl.success; + const daysLeft = getDaysRemaining(item.ssl?.notAfter); + const isExpiring = daysLeft !== null && daysLeft <= 30; // Visible warning for 30 days + const isCritical = daysLeft !== null && daysLeft <= 3; // Critical warning + + return ( + + + + + + + + ); + }) + )} + +
DomainIP AddressSSL StatusExpiryActions
+
+ +
+
No domains found matching criteria.
{item.domain}{item.ip} + {hasSSL ? ( + Secured + ) : ( + Not Secured + )} + + {hasSSL && item.ssl.notAfter ? ( +
+ + {new Date(item.ssl.notAfter).toLocaleDateString()} + + + {daysLeft} days left + +
+ ) : ( + - + )} +
+
+ {!hasSSL && ( + + )} + + + + +
+
+
); }; diff --git a/app/api/hestia/[...path]/route.ts b/app/api/hestia/[...path]/route.ts new file mode 100644 index 0000000..7cb487b --- /dev/null +++ b/app/api/hestia/[...path]/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from 'next/server'; +import axios from 'axios'; + +export async function GET(req: NextRequest) { + const { pathname, search } = new URL(req.url); + const path = pathname.replace('/api/hestia', ''); + const targetUrl = `https://api.hestiacp.metatronhost.com${path}${search}`; + + try { + const response = await axios.get(targetUrl, { + headers: { + // Forward relevant headers if needed, but for now just basic GET + 'Accept': 'application/json', + }, + }); + + return NextResponse.json(response.data); + } catch (error: any) { + console.error(`Proxy error for ${targetUrl}:`, error.message); + return NextResponse.json( + { + message: 'Failed to fetch from Hestia API', + error: error.message, + details: error.response?.data + }, + { status: error.response?.status || 500 } + ); + } +} + +export async function POST(req: NextRequest) { + const { pathname } = new URL(req.url); + const path = pathname.replace('/api/hestia', ''); + const targetUrl = `https://api.hestiacp.metatronhost.com${path}`; + + try { + const body = await req.json(); + const response = await axios.post(targetUrl, body, { + headers: { + 'Content-Type': 'application/json', + }, + }); + + return NextResponse.json(response.data); + } catch (error: any) { + console.error(`Proxy POST error for ${targetUrl}:`, error.message); + return NextResponse.json( + { + message: 'Failed to post to Hestia API', + error: error.message, + details: error.response?.data + }, + { status: error.response?.status || 500 } + ); + } +} diff --git a/components/layouts/header.tsx b/components/layouts/header.tsx index 805b7c3..18a2954 100644 --- a/components/layouts/header.tsx +++ b/components/layouts/header.tsx @@ -157,12 +157,12 @@ const Header = () => { } return ( -
+
-
+
- - logo + + logo {/* CrawlerX */}