feat: implement global bundle UI, granular sync buttons, and integration with new API endpoints

This commit is contained in:
dyzulk
2026-01-06 15:56:25 +07:00
parent a5e7312795
commit eaea8dbd2a
4 changed files with 131 additions and 22 deletions

View File

@@ -149,6 +149,51 @@ function OsGuideContent({ title, steps, selectedOs, certificates, t }: { title:
</ul>
</div>
{/* Global Bundle Section (Recommendations) */}
<div className="pt-8 border-t border-dashed border-gray-100 dark:border-gray-700">
<div className="bg-brand-500/5 dark:bg-brand-400/5 rounded-2xl p-6 border border-brand-500/10 dark:border-brand-400/10">
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-brand-500 text-white flex items-center justify-center flex-shrink-0 shadow-lg shadow-brand-500/20">
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<div className="flex-1">
<h5 className="font-bold text-gray-900 dark:text-white flex items-center gap-2 mb-1">
{t('bundle_guide_title')}
<Badge variant="brand">Recommended</Badge>
</h5>
<p className="text-xs text-gray-500 dark:text-gray-400 leading-relaxed mb-4">
{t('bundle_guide_desc')}
</p>
{(selectedOs === 'linux' || selectedOs === 'windows' || selectedOs === 'macos') && (
<div className="space-y-4">
{(selectedOs === 'linux') && (
<CliSnippet
label={t('bundle_cli_label')}
command={`curl -sL https://cdn.trustlab.dyzulk.com/ca/bundles/trustlab-all.sh | sudo bash`}
t={t}
/>
)}
{(selectedOs === 'windows' || selectedOs === 'macos') && (
<div className="flex flex-wrap gap-2">
<DownloadBtn
href={`https://cdn.trustlab.dyzulk.com/ca/bundles/trustlab-all.${selectedOs === 'windows' ? 'bat' : 'mobileconfig'}`}
label={t('download_all_bundle')}
icon={selectedOs === 'windows' ? <WindowsIcon className="w-4 h-4" /> : <AppleIcon className="w-4 h-4" />}
onClick={() => {}}
variant="blue"
/>
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
{selectedOs === 'linux' && (
<div className="pt-10 border-t border-gray-100 dark:border-gray-700 space-y-6">
<div>

View File

@@ -15,6 +15,35 @@ import { useTranslations } from "next-intl";
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();
@@ -50,15 +79,29 @@ export default function RootCaManagementClient() {
}
};
const handleSyncCdn = async () => {
const handleSyncCdn = async (type: 'all' | 'crt' | 'installers' | 'bundles') => {
setIsSyncing(true);
try {
const response = await axios.post("/api/admin/ca-certificates/sync-cdn");
addToast(response.data.message || "Sync to CDN successful", "success");
let endpoint = "/api/admin/ca-certificates/sync-cdn";
let msg = "Full Sync successful";
if (type === 'crt') {
endpoint = "/api/admin/ca-certificates/sync-crt";
msg = "CRT Files Sync successful";
} else if (type === 'installers') {
endpoint = "/api/admin/ca-certificates/sync-installers";
msg = "Individual Installers Sync successful";
} else if (type === 'bundles') {
endpoint = "/api/admin/ca-certificates/sync-bundles";
msg = "Global Bundles Sync successful";
}
const response = await axios.post(endpoint);
addToast(response.data.message || msg, "success");
mutate();
} catch (err: any) {
console.error(err);
addToast(err.response?.data?.message || "Sync to CDN failed", "error");
addToast(err.response?.data?.message || "Sync failed", "error");
} finally {
setIsSyncing(false);
}
@@ -73,23 +116,36 @@ export default function RootCaManagementClient() {
<div className="flex flex-col md:flex-row md:items-center justify-between mb-4 gap-4">
<PageBreadcrumb pageTitle={t("management_title")} />
<button
onClick={handleSyncCdn}
disabled={isSyncing || isLoading}
className="flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white text-sm font-medium rounded-lg transition-colors shadow-sm"
>
{isSyncing ? (
<svg className="animate-spin h-4 w-4 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-4 w-4" 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>
)}
{isSyncing ? "Syncing..." : "Sync All to CDN"}
</button>
<div className="flex flex-wrap items-center gap-2">
<SyncButton
onClick={() => handleSyncCdn('crt')}
disabled={isSyncing || isLoading}
isLoading={isSyncing}
label="Sync CRT Only"
variant="gray"
/>
<SyncButton
onClick={() => handleSyncCdn('installers')}
disabled={isSyncing || isLoading}
isLoading={isSyncing}
label="Sync Installers Only"
variant="blue"
/>
<SyncButton
onClick={() => handleSyncCdn('bundles')}
disabled={isSyncing || isLoading}
isLoading={isSyncing}
label="Sync Full Bundles"
variant="purple"
/>
<SyncButton
onClick={() => handleSyncCdn('all')}
disabled={isSyncing || isLoading}
isLoading={isSyncing}
label="Sync All to CDN"
variant="indigo"
/>
</div>
</div>
<div className="space-y-6">

View File

@@ -218,6 +218,10 @@
],
"guide_linux_shortcut_title": "Instant Installation (CLI)",
"guide_linux_shortcut_desc": "Run the corresponding command for the certificate you wish to trust:",
"bundle_guide_title": "Trust All Certificates (Quick Start)",
"bundle_guide_desc": "If you are setting up a new device, we recommend using the Bundle Installer. This will instantly untrust all TrustLab Root and Intermediate CAs in one go.",
"bundle_cli_label": "Global Bundle Installer (All-in-One)",
"download_all_bundle": "Download Full Bundle",
"guide_steps_mobile": [
"Android: Settings > Security > Install from storage > CA Certificate.",
"iOS: Install the profile, then Settings > General > About > Certificate Trust Settings."

View File

@@ -217,7 +217,11 @@
"Skrip akan secara otomatis mendeteksi distro dan memperbarui penyimpanan CA Anda."
],
"guide_linux_shortcut_title": "Instalasi Instan (CLI)",
"guide_linux_shortcut_desc": "Jalankan perintah yang sesuai untuk sertifikat yang ingin Anda percayai:",
"guide_linux_shortcut_desc": "Jalankan perintah sesuai dengan sertifikat yang ingin Anda percayai:",
"bundle_guide_title": "Percayai Semua Sertifikat (Mulai Cepat)",
"bundle_guide_desc": "Jika Anda baru menyiapkan perangkat baru, kami menyarankan Anda menggunakan Bundle Installer. Ini akan langsung memercayai semua Root dan Intermediate CA TrustLab sekaligus.",
"bundle_cli_label": "Global Bundle Installer (Sapujagat)",
"download_all_bundle": "Unduh Bundle Lengkap",
"guide_steps_mobile": [
"Android: Pengaturan > Keamanan > Instal dari penyimpanan > Sertifikat CA.",
"iOS: Instal profil, lalu Pengaturan > Umum > Mengenai > Pengaturan Kepercayaan Sertifikat."