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> </ul>
</div> </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' && ( {selectedOs === 'linux' && (
<div className="pt-10 border-t border-gray-100 dark:border-gray-700 space-y-6"> <div className="pt-10 border-t border-gray-100 dark:border-gray-700 space-y-6">
<div> <div>

View File

@@ -15,6 +15,35 @@ import { useTranslations } from "next-intl";
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();
@@ -50,15 +79,29 @@ export default function RootCaManagementClient() {
} }
}; };
const handleSyncCdn = async () => { const handleSyncCdn = async (type: 'all' | 'crt' | 'installers' | 'bundles') => {
setIsSyncing(true); setIsSyncing(true);
try { try {
const response = await axios.post("/api/admin/ca-certificates/sync-cdn"); let endpoint = "/api/admin/ca-certificates/sync-cdn";
addToast(response.data.message || "Sync to CDN successful", "success"); 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(); mutate();
} catch (err: any) { } catch (err: any) {
console.error(err); console.error(err);
addToast(err.response?.data?.message || "Sync to CDN failed", "error"); addToast(err.response?.data?.message || "Sync failed", "error");
} finally { } finally {
setIsSyncing(false); 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"> <div className="flex flex-col md:flex-row md:items-center justify-between mb-4 gap-4">
<PageBreadcrumb pageTitle={t("management_title")} /> <PageBreadcrumb pageTitle={t("management_title")} />
<button <div className="flex flex-wrap items-center gap-2">
onClick={handleSyncCdn} <SyncButton
onClick={() => handleSyncCdn('crt')}
disabled={isSyncing || isLoading} 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" isLoading={isSyncing}
> label="Sync CRT Only"
{isSyncing ? ( variant="gray"
<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> <SyncButton
<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> onClick={() => handleSyncCdn('installers')}
</svg> disabled={isSyncing || isLoading}
) : ( isLoading={isSyncing}
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> label="Sync Installers Only"
<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" /> variant="blue"
</svg> />
)} <SyncButton
{isSyncing ? "Syncing..." : "Sync All to CDN"} onClick={() => handleSyncCdn('bundles')}
</button> 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>
<div className="space-y-6"> <div className="space-y-6">

View File

@@ -218,6 +218,10 @@
], ],
"guide_linux_shortcut_title": "Instant Installation (CLI)", "guide_linux_shortcut_title": "Instant Installation (CLI)",
"guide_linux_shortcut_desc": "Run the corresponding command for the certificate you wish to trust:", "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": [ "guide_steps_mobile": [
"Android: Settings > Security > Install from storage > CA Certificate.", "Android: Settings > Security > Install from storage > CA Certificate.",
"iOS: Install the profile, then Settings > General > About > Certificate Trust Settings." "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." "Skrip akan secara otomatis mendeteksi distro dan memperbarui penyimpanan CA Anda."
], ],
"guide_linux_shortcut_title": "Instalasi Instan (CLI)", "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": [ "guide_steps_mobile": [
"Android: Pengaturan > Keamanan > Instal dari penyimpanan > Sertifikat CA.", "Android: Pengaturan > Keamanan > Instal dari penyimpanan > Sertifikat CA.",
"iOS: Instal profil, lalu Pengaturan > Umum > Mengenai > Pengaturan Kepercayaan Sertifikat." "iOS: Instal profil, lalu Pengaturan > Umum > Mengenai > Pengaturan Kepercayaan Sertifikat."