mirror of
https://github.com/dyzulk/trustlab.git
synced 2026-01-26 13:32:06 +07:00
feat: implement Renew All (Bulk CA Chain Refresh) functionality
This commit is contained in:
@@ -14,38 +14,10 @@ import PageLoader from "@/components/ui/PageLoader";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import CdnManagementCard from "@/components/admin/CdnManagementCard";
|
import CdnManagementCard from "@/components/admin/CdnManagementCard";
|
||||||
import ArchiveManagementTable from "@/components/admin/ArchiveManagementTable";
|
import ArchiveManagementTable from "@/components/admin/ArchiveManagementTable";
|
||||||
|
import Button from "@/components/ui/button/Button";
|
||||||
|
|
||||||
const fetcher = (url: string) => axios.get(url).then((res) => res.data);
|
const fetcher = (url: string) => axios.get(url).then((res) => res.data);
|
||||||
|
|
||||||
function SyncButton({ onClick, disabled, isLoading, label, variant = 'blue' }: { onClick: () => void, disabled: boolean, isLoading: boolean, label: string, variant?: 'blue' | 'gray' | 'purple' | 'indigo' }) {
|
|
||||||
const variantClasses = {
|
|
||||||
blue: 'bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400',
|
|
||||||
gray: 'bg-gray-600 hover:bg-gray-700 disabled:bg-gray-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',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
className={`flex items-center justify-center gap-2 px-3 py-1.5 ${variantClasses[variant]} text-white text-xs font-medium rounded-lg transition-all shadow-sm active:scale-95`}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<svg className="animate-spin h-3.5 w-3.5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
{isLoading ? "Syncing..." : label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RootCaManagementClient() {
|
export default function RootCaManagementClient() {
|
||||||
const t = useTranslations("RootCA");
|
const t = useTranslations("RootCA");
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -53,9 +25,11 @@ export default function RootCaManagementClient() {
|
|||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
const { data, error, mutate, isLoading } = useSWR("/api/admin/ca-certificates", fetcher);
|
const { data, error, mutate, isLoading } = useSWR("/api/admin/ca-certificates", fetcher);
|
||||||
const [isRenewing, setIsRenewing] = useState(false);
|
const [isRenewing, setIsRenewing] = useState(false);
|
||||||
|
const [isBulkRenewing, setIsBulkRenewing] = useState(false);
|
||||||
const [activeSync, setActiveSync] = useState<string | null>(null);
|
const [activeSync, setActiveSync] = useState<string | null>(null);
|
||||||
const [isPromoting, setIsPromoting] = useState(false);
|
const [isPromoting, setIsPromoting] = useState(false);
|
||||||
const [confirmRenewUuid, setConfirmRenewUuid] = useState<string | null>(null);
|
const [confirmRenewUuid, setConfirmRenewUuid] = useState<string | null>(null);
|
||||||
|
const [showBulkRenewConfirm, setShowBulkRenewConfirm] = useState(false);
|
||||||
|
|
||||||
// Redirect if not admin or owner (double security, backend also checks)
|
// Redirect if not admin or owner (double security, backend also checks)
|
||||||
const { isAdminOrOwner } = useAuth();
|
const { isAdminOrOwner } = useAuth();
|
||||||
@@ -125,6 +99,23 @@ export default function RootCaManagementClient() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRenewAll = async () => {
|
||||||
|
setIsBulkRenewing(true);
|
||||||
|
try {
|
||||||
|
const response = await axios.post("/api/admin/ca-certificates/renew-all", { days: 3650 });
|
||||||
|
if (response.data.status === "success") {
|
||||||
|
addToast(t("toast_bulk_renew_success"), "success");
|
||||||
|
mutate();
|
||||||
|
setShowBulkRenewConfirm(false);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
|
addToast(err.response?.data?.message || t("toast_bulk_renew_failed"), "error");
|
||||||
|
} finally {
|
||||||
|
setIsBulkRenewing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (error) return <div className="p-10 text-center text-error-500">{t("load_failed")}</div>;
|
if (error) return <div className="p-10 text-center text-error-500">{t("load_failed")}</div>;
|
||||||
|
|
||||||
const certificates = data?.data || [];
|
const certificates = data?.data || [];
|
||||||
@@ -133,6 +124,15 @@ export default function RootCaManagementClient() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between mb-2 gap-4">
|
<div className="flex flex-col md:flex-row md:items-center justify-between mb-2 gap-4">
|
||||||
<PageBreadcrumb pageTitle={t("management_title")} />
|
<PageBreadcrumb pageTitle={t("management_title")} />
|
||||||
|
<Button
|
||||||
|
variant="warning"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowBulkRenewConfirm(true)}
|
||||||
|
loading={isBulkRenewing}
|
||||||
|
disabled={isLoading || isRenewing || isPromoting}
|
||||||
|
>
|
||||||
|
{t("renew_all_button")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -196,6 +196,18 @@ export default function RootCaManagementClient() {
|
|||||||
variant="warning"
|
variant="warning"
|
||||||
requiredInput="RENEW"
|
requiredInput="RENEW"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={showBulkRenewConfirm}
|
||||||
|
onClose={() => setShowBulkRenewConfirm(false)}
|
||||||
|
onConfirm={handleRenewAll}
|
||||||
|
title={t("bulk_renew_modal_title")}
|
||||||
|
message={t("bulk_renew_modal_msg")}
|
||||||
|
isLoading={isBulkRenewing}
|
||||||
|
confirmLabel={t("bulk_renew_confirm_label")}
|
||||||
|
variant="warning"
|
||||||
|
requiredInput="RENEW-ALL"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { ReactNode } from "react";
|
|||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
children: ReactNode; // Button text or content
|
children: ReactNode; // Button text or content
|
||||||
size?: "sm" | "md"; // Button size
|
size?: "sm" | "md"; // Button size
|
||||||
variant?: "primary" | "outline" | "danger" | "success"; // Button variant
|
variant?: "primary" | "outline" | "danger" | "success" | "warning"; // Button variant
|
||||||
startIcon?: ReactNode; // Icon before the text
|
startIcon?: ReactNode; // Icon before the text
|
||||||
endIcon?: ReactNode; // Icon after the text
|
endIcon?: ReactNode; // Icon after the text
|
||||||
onClick?: () => void; // Click handler
|
onClick?: () => void; // Click handler
|
||||||
@@ -41,6 +41,8 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
"bg-red-600 text-white shadow-theme-xs hover:bg-red-700 disabled:bg-red-300 dark:bg-red-600 dark:hover:bg-red-700",
|
"bg-red-600 text-white shadow-theme-xs hover:bg-red-700 disabled:bg-red-300 dark:bg-red-600 dark:hover:bg-red-700",
|
||||||
success:
|
success:
|
||||||
"bg-green-600 text-white shadow-theme-xs hover:bg-green-700 disabled:bg-green-300 dark:bg-green-600 dark:hover:bg-green-700",
|
"bg-green-600 text-white shadow-theme-xs hover:bg-green-700 disabled:bg-green-300 dark:bg-green-600 dark:hover:bg-green-700",
|
||||||
|
warning:
|
||||||
|
"bg-warning-500 text-white shadow-theme-xs hover:bg-warning-600 disabled:bg-warning-300 dark:bg-warning-600 dark:hover:bg-warning-700",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -643,6 +643,12 @@
|
|||||||
"cdn_group_assets_desc": "Update specific file types to the CDN (Uses 'Both' mode by default).",
|
"cdn_group_assets_desc": "Update specific file types to the CDN (Uses 'Both' mode by default).",
|
||||||
"cdn_group_modes": "Dual CDN Strategy",
|
"cdn_group_modes": "Dual CDN Strategy",
|
||||||
"cdn_group_modes_desc": "Control asset distribution paths (Clean URLs vs Global Archives).",
|
"cdn_group_modes_desc": "Control asset distribution paths (Clean URLs vs Global Archives).",
|
||||||
|
"renew_all_button": "Renew All (Chain-Refresh)",
|
||||||
|
"bulk_renew_modal_title": "Renew Entire CA Chain?",
|
||||||
|
"bulk_renew_modal_msg": "This will regenerate the Root CA and all Intermediate CAs sequentially. This is a significant action that will refresh the entire Chain of Trust. Type 'RENEW-ALL' to proceed.",
|
||||||
|
"bulk_renew_confirm_label": "Renew Everything",
|
||||||
|
"toast_bulk_renew_success": "Entire CA chain renewed successfully!",
|
||||||
|
"toast_bulk_renew_failed": "Bulk renewal failed",
|
||||||
"toast_renew_success": "CA Certificate renewed successfully.",
|
"toast_renew_success": "CA Certificate renewed successfully.",
|
||||||
"toast_renew_failed": "Failed to renew CA certificate",
|
"toast_renew_failed": "Failed to renew CA certificate",
|
||||||
"load_failed": "Failed to load CA certificates. Admin access required.",
|
"load_failed": "Failed to load CA certificates. Admin access required.",
|
||||||
|
|||||||
@@ -643,6 +643,12 @@
|
|||||||
"cdn_group_assets_desc": "Perbarui file spesifik ke CDN (Menggunakan mode 'Both' secara default).",
|
"cdn_group_assets_desc": "Perbarui file spesifik ke CDN (Menggunakan mode 'Both' secara default).",
|
||||||
"cdn_group_modes": "Strategi Dual CDN (Mode-specific)",
|
"cdn_group_modes": "Strategi Dual CDN (Mode-specific)",
|
||||||
"cdn_group_modes_desc": "Kontrol jalur distribusi aset (Clean URLs vs Global Archives).",
|
"cdn_group_modes_desc": "Kontrol jalur distribusi aset (Clean URLs vs Global Archives).",
|
||||||
|
"renew_all_button": "Pembaruan Massal (Renew All)",
|
||||||
|
"bulk_renew_modal_title": "Perbarui Seluruh Rantai CA?",
|
||||||
|
"bulk_renew_modal_msg": "Ini akan meregenerasi Root CA dan semua Intermediate CA secara berurutan. Ini adalah tindakan besar yang akan menyegarkan seluruh Rantai Kepercayaan (Chain of Trust). Ketik 'RENEW-ALL' untuk melanjutkan.",
|
||||||
|
"bulk_renew_confirm_label": "Perbarui Semuanya",
|
||||||
|
"toast_bulk_renew_success": "Seluruh rantai CA berhasil diperbarui!",
|
||||||
|
"toast_bulk_renew_failed": "Pembaruan massal gagal",
|
||||||
"toast_renew_success": "Sertifikat CA berhasil diperbarui.",
|
"toast_renew_success": "Sertifikat CA berhasil diperbarui.",
|
||||||
"toast_renew_failed": "Gagal memperbarui sertifikat CA",
|
"toast_renew_failed": "Gagal memperbarui sertifikat CA",
|
||||||
"load_failed": "Gagal memuat sertifikat CA. Diperlukan akses admin.",
|
"load_failed": "Gagal memuat sertifikat CA. Diperlukan akses admin.",
|
||||||
|
|||||||
Reference in New Issue
Block a user