mirror of
https://github.com/dyzulk/trustlab.git
synced 2026-01-26 05:25:36 +07:00
feat: implement global bundle UI, granular sync buttons, and integration with new API endpoints
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user