feat: implement Renew All (Bulk CA Chain Refresh) functionality

This commit is contained in:
dyzulk
2026-01-07 05:27:38 +07:00
parent 713fab3fba
commit cde7e66b73
4 changed files with 56 additions and 30 deletions

View File

@@ -14,38 +14,10 @@ import PageLoader from "@/components/ui/PageLoader";
import { useTranslations } from "next-intl";
import CdnManagementCard from "@/components/admin/CdnManagementCard";
import ArchiveManagementTable from "@/components/admin/ArchiveManagementTable";
import Button from "@/components/ui/button/Button";
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() {
const t = useTranslations("RootCA");
const { user } = useAuth();
@@ -53,9 +25,11 @@ export default function RootCaManagementClient() {
const { addToast } = useToast();
const { data, error, mutate, isLoading } = useSWR("/api/admin/ca-certificates", fetcher);
const [isRenewing, setIsRenewing] = useState(false);
const [isBulkRenewing, setIsBulkRenewing] = useState(false);
const [activeSync, setActiveSync] = useState<string | null>(null);
const [isPromoting, setIsPromoting] = useState(false);
const [confirmRenewUuid, setConfirmRenewUuid] = useState<string | null>(null);
const [showBulkRenewConfirm, setShowBulkRenewConfirm] = useState(false);
// Redirect if not admin or owner (double security, backend also checks)
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>;
const certificates = data?.data || [];
@@ -133,6 +124,15 @@ export default function RootCaManagementClient() {
<div className="space-y-6">
<div className="flex flex-col md:flex-row md:items-center justify-between mb-2 gap-4">
<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 className="space-y-6">
@@ -196,6 +196,18 @@ export default function RootCaManagementClient() {
variant="warning"
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>
);
}