import { useMutation } from "@tanstack/react-query"; import { Download } from "lucide-react"; import { type ReactNode, useState } from "react"; import { toast } from "sonner"; import { exportFullConfigMutation } from "~/client/api-client/@tanstack/react-query.gen"; import { Button } from "~/client/components/ui/button"; import { Checkbox } from "~/client/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "~/client/components/ui/dialog"; import { Input } from "~/client/components/ui/input"; import { Label } from "~/client/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/client/components/ui/select"; type SecretsMode = "exclude" | "encrypted" | "cleartext"; const DEFAULT_EXPORT_FILENAME = "zerobyte-full-config"; function downloadAsJson(data: unknown, filename: string): void { const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${filename}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } type BaseExportDialogProps = { /** Optional custom filename (without extension). Defaults to `zerobyte-full-config`. */ filename?: string; }; type ExportDialogWithTrigger = BaseExportDialogProps & { /** Custom trigger element. When provided, variant/size/triggerLabel/showIcon are ignored. */ trigger: ReactNode; variant?: never; size?: never; triggerLabel?: never; showIcon?: never; }; type ExportDialogWithDefaultTrigger = BaseExportDialogProps & { trigger?: never; variant?: "default" | "outline" | "ghost" | "card"; size?: "default" | "sm" | "lg" | "icon"; triggerLabel?: string; showIcon?: boolean; }; type ExportDialogProps = ExportDialogWithTrigger | ExportDialogWithDefaultTrigger; export function ExportDialog({ filename = DEFAULT_EXPORT_FILENAME, trigger, variant = "outline", size = "default", triggerLabel, showIcon = true, }: ExportDialogProps) { const [open, setOpen] = useState(false); const [includeMetadata, setIncludeMetadata] = useState(false); const [includeRecoveryKey, setIncludeRecoveryKey] = useState(false); const [includePasswordHash, setIncludePasswordHash] = useState(false); const [secretsMode, setSecretsMode] = useState("exclude"); const [password, setPassword] = useState(""); const handleExportSuccess = (data: unknown) => { downloadAsJson(data, filename); toast.success("Configuration exported successfully"); setOpen(false); setPassword(""); }; const handleExportError = (error: unknown) => { const message = error && typeof error === "object" && "error" in error ? (error as { error: string }).error : "Unknown error"; toast.error("Export failed", { description: message, }); }; const exportMutation = useMutation({ ...exportFullConfigMutation(), onSuccess: handleExportSuccess, onError: handleExportError, }); const handleExport = (e: React.FormEvent) => { e.preventDefault(); if (!password) { toast.error("Password is required"); return; } exportMutation.mutate({ body: { password, includeMetadata, secretsMode, includeRecoveryKey, includePasswordHash, }, }); }; const handleDialogChange = (isOpen: boolean) => { setOpen(isOpen); if (!isOpen) { setPassword(""); } }; const defaultTrigger = variant === "card" ? ( ) : ( ); return ( {trigger ?? defaultTrigger}
Export Full Configuration Export the complete Zerobyte configuration.
setIncludeMetadata(checked === true)} />

Include database IDs, timestamps, and runtime state (status, health checks, last backup info).

{secretsMode === "exclude" && "Sensitive fields (passwords, API keys, webhooks) will be removed from the export."} {secretsMode === "encrypted" && "Secrets will be exported in encrypted form. Requires the same recovery key to decrypt on import."} {secretsMode === "cleartext" && ( ⚠️ Secrets will be decrypted and exported as plaintext. Keep this export secure! )}

setIncludeRecoveryKey(checked === true)} />

⚠️ Security sensitive: The recovery key is the master encryption key for all repositories. Keep this export secure and never share it.

setIncludePasswordHash(checked === true)} />

Include the hashed user passwords for seamless migration. The passwords are already securely hashed (argon2).

setPassword(e.target.value)} placeholder="Enter your password to export" required />

Password is required to verify your identity before exporting configuration.

); }