From fc04d0112f0f603009e2e5019203a94621458ca4 Mon Sep 17 00:00:00 2001 From: dyzulk <66510723+dyzulk@users.noreply.github.com> Date: Wed, 7 Jan 2026 04:43:50 +0700 Subject: [PATCH] feat: implement dual CDN strategy and archive management UI --- build_log.txt | 23 +++ .../admin/root-ca/RootCaManagementClient.tsx | 131 ++++++++------- .../admin/ArchiveManagementTable.tsx | 154 ++++++++++++++++++ src/components/admin/CdnManagementCard.tsx | 96 +++++++++++ src/messages/en.json | 2 +- src/messages/id.json | 2 +- 6 files changed, 345 insertions(+), 63 deletions(-) create mode 100644 build_log.txt create mode 100644 src/components/admin/ArchiveManagementTable.tsx create mode 100644 src/components/admin/CdnManagementCard.tsx diff --git a/build_log.txt b/build_log.txt new file mode 100644 index 0000000..4916928 --- /dev/null +++ b/build_log.txt @@ -0,0 +1,23 @@ + +> trustlab-web@1.0.0 build +> next build + + Γû▓ Next.js 16.0.10 (Turbopack) + - Environments: .env.local + + Creating an optimized production build ... + Γ£ô Compiled successfully in 13.8s + Running TypeScript ... +Failed to compile. + +./src/components/admin/ArchiveManagementTable.tsx:120:36 +Type error: Type '"xs"' is not assignable to type 'BadgeSize | undefined'. + + 118 | + 119 | {cert.is_latest && ( +> 120 | LIVE - LATEST + | ^ + 121 | )} + 122 | + 123 | +Next.js build worker exited with code: 1 and signal: null diff --git a/src/app/dashboard/admin/root-ca/RootCaManagementClient.tsx b/src/app/dashboard/admin/root-ca/RootCaManagementClient.tsx index 46832d4..e3a08ba 100644 --- a/src/app/dashboard/admin/root-ca/RootCaManagementClient.tsx +++ b/src/app/dashboard/admin/root-ca/RootCaManagementClient.tsx @@ -12,6 +12,8 @@ import { useToast } from "@/context/ToastContext"; import ConfirmationModal from "@/components/common/ConfirmationModal"; import PageLoader from "@/components/ui/PageLoader"; import { useTranslations } from "next-intl"; +import CdnManagementCard from "@/components/admin/CdnManagementCard"; +import ArchiveManagementTable from "@/components/admin/ArchiveManagementTable"; const fetcher = (url: string) => axios.get(url).then((res) => res.data); @@ -52,6 +54,7 @@ export default function RootCaManagementClient() { const { data, error, mutate, isLoading } = useSWR("/api/admin/ca-certificates", fetcher); const [isRenewing, setIsRenewing] = useState(false); const [isSyncing, setIsSyncing] = useState(false); + const [isPromoting, setIsPromoting] = useState(false); const [confirmRenewUuid, setConfirmRenewUuid] = useState(null); // Redirect if not admin or owner (double security, backend also checks) @@ -79,24 +82,24 @@ export default function RootCaManagementClient() { } }; - const handleSyncCdn = async (type: 'all' | 'crt' | 'installers' | 'bundles') => { + const handleSyncCdn = async (type: 'all' | 'crt' | 'installers' | 'bundles', mode: 'latest' | 'archive' | 'both' = 'both') => { setIsSyncing(true); try { let endpoint = "/api/admin/ca-certificates/sync-cdn"; - let msg = "Full Sync successful"; + let msg = `Sync successful (Mode: ${mode})`; if (type === 'crt') { endpoint = "/api/admin/ca-certificates/sync-crt"; - msg = "CRT Files Sync successful"; + msg = `CRT Files Sync successful (${mode})`; } else if (type === 'installers') { endpoint = "/api/admin/ca-certificates/sync-installers"; - msg = "Individual Installers Sync successful"; + msg = `Individual Installers Sync successful (${mode})`; } else if (type === 'bundles') { endpoint = "/api/admin/ca-certificates/sync-bundles"; msg = "Global Bundles Sync successful"; } - const response = await axios.post(endpoint); + const response = await axios.post(endpoint, { mode }); addToast(response.data.message || msg, "success"); mutate(); } catch (err: any) { @@ -107,6 +110,20 @@ export default function RootCaManagementClient() { } }; + const handlePromote = async (uuid: string) => { + setIsPromoting(true); + try { + const response = await axios.post(`/api/admin/ca-certificates/${uuid}/promote`); + addToast(response.data.message || "Promoted successfully", "success"); + mutate(); + } catch (err: any) { + console.error(err); + addToast(err.response?.data?.message || "Promotion failed", "error"); + } finally { + setIsPromoting(false); + } + }; + if (error) return
{t("load_failed")}
; const certificates = data?.data || []; @@ -115,66 +132,58 @@ export default function RootCaManagementClient() {
- -
- handleSyncCdn('crt')} - disabled={isSyncing || isLoading} - isLoading={isSyncing} - label="Sync CRT Only" - variant="gray" - /> - handleSyncCdn('installers')} - disabled={isSyncing || isLoading} - isLoading={isSyncing} - label="Sync Installers Only" - variant="blue" - /> - handleSyncCdn('bundles')} - disabled={isSyncing || isLoading} - isLoading={isSyncing} - label="Sync Full Bundles" - variant="purple" - /> - handleSyncCdn('all')} - disabled={isSyncing || isLoading} - isLoading={isSyncing} - label="Sync All to CDN" - variant="indigo" - /> -
-
- - +
+ {/* Left Column: Management & Renew Table */} +
+ - {(isLoading || isRenewing) && ( -
- + + c.is_latest || certificates.length === 1)} + onRenew={setConfirmRenewUuid} + isRenewing={isRenewing} + /> + + {(isLoading || isRenewing) && ( +
+ +
+ )} +
+ +
+

{t("info_title")}

+
    +
  • {t("info_point_1")}
  • +
  • {t("info_point_2")}
  • +
  • {t("info_point_3")}
  • +
  • {t("info_point_4")}
  • +
- )} - - -
-

{t("info_title")}

-
    -
  • {t("info_point_1")}
  • -
  • {t("info_point_2")}
  • -
  • {t("info_point_3")}
  • -
  • {t("info_point_4")}
  • -
+
+ + {/* Right Column: Archives */} +
+ + +
diff --git a/src/components/admin/ArchiveManagementTable.tsx b/src/components/admin/ArchiveManagementTable.tsx new file mode 100644 index 0000000..f2d844c --- /dev/null +++ b/src/components/admin/ArchiveManagementTable.tsx @@ -0,0 +1,154 @@ +"use client"; + +import React, { useState, useMemo } from "react"; +import { + Table, + TableBody, + TableCell, + TableHeader, + TableRow, +} from "../ui/table"; +import Badge from "../ui/badge/Badge"; +import Button from "../ui/button/Button"; +import InputField from "../form/input/InputField"; +import { useTranslations } from "next-intl"; + +interface CaCertificate { + uuid: string; + ca_type: string; + common_name: string; + serial_number: string; + valid_from: string; + valid_to: string; + status: string; + is_latest: boolean; +} + +interface ArchiveManagementTableProps { + certificates: CaCertificate[]; + onPromote: (uuid: string) => void; + isPromoting: boolean; +} + +export default function ArchiveManagementTable({ + certificates, + onPromote, + isPromoting, +}: ArchiveManagementTableProps) { + const t = useTranslations("RootCA"); + const [searchTerm, setSearchTerm] = useState(""); + + const formatType = (type: string) => { + return type.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); + }; + + const filteredCertificates = useMemo(() => { + return certificates.filter((cert) => { + const search = searchTerm.toLowerCase(); + return ( + cert.common_name.toLowerCase().includes(search) || + cert.ca_type.toLowerCase().includes(search) || + cert.serial_number.toLowerCase().includes(search) || + cert.uuid.toLowerCase().includes(search) + ); + }); + }, [certificates, searchTerm]); + + return ( +
+ {/* Table Header Controls */} +
+

CDN Archive History

+
+ setSearchTerm(e.target.value)} + className="!py-2" + /> +
+
+ +
+
+ + + + + Version (UUID) + + + {t("common_name_th")} + + + {t("validity_th")} + + + Status + + + {t("actions_th")} + + + + + + {filteredCertificates.map((cert) => ( + + + {cert.uuid} + + +
+ {cert.common_name} + {formatType(cert.ca_type)} +
+
+ +
+ {new Date(cert.valid_from).toLocaleDateString()} + {t("validity_to_label")} + {new Date(cert.valid_to).toLocaleDateString()} +
+
+ +
+ + {cert.status === "valid" ? t("status_valid") : t("status_expired")} + + {cert.is_latest && ( + LIVE - LATEST + )} +
+
+ + {!cert.is_latest && ( + + )} + {cert.is_latest && ( + Currently Public + )} + +
+ ))} + {filteredCertificates.length === 0 && ( + + + No versions found. + + + )} +
+
+
+
+
+ ); +} diff --git a/src/components/admin/CdnManagementCard.tsx b/src/components/admin/CdnManagementCard.tsx new file mode 100644 index 0000000..469cce7 --- /dev/null +++ b/src/components/admin/CdnManagementCard.tsx @@ -0,0 +1,96 @@ +"use client"; + +import React from "react"; +import ComponentCard from "@/components/common/ComponentCard"; +import { useTranslations } from "next-intl"; + +interface CdnManagementCardProps { + onSync: (type: "all" | "crt" | "installers" | "bundles", mode: "latest" | "archive" | "both") => void; + isSyncing: boolean; + disabled: boolean; +} + +export default function CdnManagementCard({ onSync, isSyncing, disabled }: CdnManagementCardProps) { + const t = useTranslations("RootCA"); + + const SyncButton = ({ + onClick, + label, + desc, + variant = "blue" + }: { + onClick: () => void; + label: string; + desc: string; + variant?: "blue" | "purple" | "indigo" | "gray" + }) => { + const variantClasses = { + blue: "bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400", + purple: "bg-purple-600 hover:bg-purple-700 disabled:bg-purple-400", + indigo: "bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400", + gray: "bg-gray-600 hover:bg-gray-700 disabled:bg-gray-400", + }; + + return ( +
+
+
+ {label} +

{desc}

+
+ +
+
+ ); + }; + + return ( + +
+ onSync("all", "both")} + variant="indigo" + /> + onSync("all", "latest")} + variant="blue" + /> + onSync("all", "archive")} + variant="purple" + /> + onSync("bundles", "latest")} + variant="gray" + /> +
+
+ ); +} diff --git a/src/messages/en.json b/src/messages/en.json index 1cd424b..c9b46a3 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -658,7 +658,7 @@ "validity_th": "Validity Period", "status_th": "Status", "actions_th": "Actions", - "to": "to", + "validity_to_label": "to", "status_valid": "Valid", "status_expired": "Expired", "renew_button": "Renew (10Y)", diff --git a/src/messages/id.json b/src/messages/id.json index 52df9d1..6fffd10 100644 --- a/src/messages/id.json +++ b/src/messages/id.json @@ -658,7 +658,7 @@ "validity_th": "Masa Berlaku", "status_th": "Status", "actions_th": "Aksi", - "to": "ke", + "validity_to_label": "ke", "status_valid": "Valid", "status_expired": "Kedaluwarsa", "renew_button": "Perbarui (10T)",