diff --git a/AppImage/ProxMenux-1.2.2.2-beta.AppImage b/AppImage/ProxMenux-1.2.2.2-beta.AppImage index 64925981..265444e0 100755 Binary files a/AppImage/ProxMenux-1.2.2.2-beta.AppImage and b/AppImage/ProxMenux-1.2.2.2-beta.AppImage differ diff --git a/AppImage/ProxMenux-Monitor.AppImage.sha256 b/AppImage/ProxMenux-Monitor.AppImage.sha256 index 59952c3e..0881c0c6 100644 --- a/AppImage/ProxMenux-Monitor.AppImage.sha256 +++ b/AppImage/ProxMenux-Monitor.AppImage.sha256 @@ -1 +1 @@ -33e64043c50e9d90c1297019219c08d5196ab8181f0409e77f1dce9c42b0e73e ProxMenux-1.2.2.2-beta.AppImage +08b669193097449f8330ad430066574bf0e99f477aea97172e6b845ad4ee648c ProxMenux-1.2.2.2-beta.AppImage diff --git a/AppImage/README.md b/AppImage/README.md index 6bfa319b..8c221020 100644 --- a/AppImage/README.md +++ b/AppImage/README.md @@ -810,17 +810,9 @@ entities: ## License -This project is licensed under the **Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0)**. +This project is licensed under the **GNU General Public License, version 3 (GPL-3.0)**. -You are free to: -- Share — copy and redistribute the material in any medium or format -- Adapt — remix, transform, and build upon the material - -Under the following terms: -- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made -- NonCommercial — You may not use the material for commercial purposes - -For more details, see the [full license](https://creativecommons.org/licenses/by-nc/4.0/). +You are free to use, study, share and modify the software under the terms of the licence. Any distributed derivative work must be licensed under the same terms and include the full source code — see the [full licence text](https://www.gnu.org/licenses/gpl-3.0.html) or the [`LICENSE`](https://github.com/MacRimi/ProxMenux/blob/main/LICENSE) file at the repository root. diff --git a/AppImage/components/host-backup.tsx b/AppImage/components/host-backup.tsx index 4b2a1306..aab0eff3 100644 --- a/AppImage/components/host-backup.tsx +++ b/AppImage/components/host-backup.tsx @@ -1972,7 +1972,11 @@ interface JobDetail { pbs_backup_id: string | null pbs_fingerprint: string | null has_pbs_password: boolean + // Legacy — kept for backend responses that only track the on/off + // toggle. Newer builds also send pbs_encrypt_mode ("none" | "new" + // | "existing"); loadFromJobDetail prefers that when present. pbs_encrypt: boolean + pbs_encrypt_mode?: "none" | "new" | "existing" local_dest_dir: string | null local_archive_ext: string | null borg_repo: string | null @@ -2025,10 +2029,21 @@ function CreateJobDialog({ // Backend-specific fields const [pbsRepository, setPbsRepository] = useState("") const [pbsBackupId, setPbsBackupId] = useState("") - // Client-side PBS encryption — sent as `pbs_encrypt` in the payload. - // Backend resolves the shared keyfile + injects PBS_KEYFILE into - // the job .env so the runner adds `--keyfile` to the backup call. - const [pbsEncrypt, setPbsEncrypt] = useState(false) + // Client-side PBS encryption mode. Three modes match the shell wizard: + // - "none" — plain backup, no --keyfile + // - "new" — backend generates a fresh per-host keyfile (default) + // - "existing" — operator uploads their own keyfile (shared across + // hosts). The upload happens via a dedicated endpoint + // BEFORE the job create call — see handleCreate below. + const [pbsEncryptMode, setPbsEncryptMode] = useState<"none" | "new" | "existing">("none") + // File picked in the "existing" mode. Kept in a ref-like state so a + // stale reference doesn't linger across modal opens. + const [pbsImportFile, setPbsImportFile] = useState(null) + const [pbsImportBusy, setPbsImportBusy] = useState(false) + // Legacy alias for the many recovery / gating checks below — a truthy + // value means "the operator wants an encrypted backup" regardless of + // whether that keyfile came from a fresh generate or from an import. + const pbsEncrypt = pbsEncryptMode !== "none" // Optional recovery passphrase. When set, the backend encrypts the // keyfile with openssl and the runner uploads that blob to PBS with // every backup so the keyfile can be rebuilt on a fresh host with @@ -2123,7 +2138,15 @@ function CreateJobDialog({ if (r.keep_yearly !== undefined) setKeepYearly(String(r.keep_yearly)) if (jobDetail.pbs_repository) setPbsRepository(jobDetail.pbs_repository) if (jobDetail.pbs_backup_id) setPbsBackupId(jobDetail.pbs_backup_id) - setPbsEncrypt(!!jobDetail.pbs_encrypt) + // Prefer the new pbs_encrypt_mode string when the backend sends it; + // fall back to the legacy bool for older `.env` layouts that only + // remember the on/off toggle. + const jd = jobDetail as unknown as { pbs_encrypt_mode?: string; pbs_encrypt?: boolean } + if (jd.pbs_encrypt_mode === "new" || jd.pbs_encrypt_mode === "existing" || jd.pbs_encrypt_mode === "none") { + setPbsEncryptMode(jd.pbs_encrypt_mode) + } else { + setPbsEncryptMode(jd.pbs_encrypt ? "new" : "none") + } if (jobDetail.local_dest_dir) setLocalDestDir(jobDetail.local_dest_dir) if (jobDetail.borg_repo) setBorgRepoSelected(jobDetail.borg_repo) if (jobDetail.borg_encrypt_mode) { @@ -2214,7 +2237,9 @@ function CreateJobDialog({ setKeepYearly("0") setPbsRepository("") setPbsBackupId("") - setPbsEncrypt(false) + setPbsEncryptMode("none") + setPbsImportFile(null) + setPbsImportBusy(false) setPbsRecoveryPass("") setPbsRecoveryPass2("") setPbsRecoveryChange(false) @@ -2322,6 +2347,39 @@ function CreateJobDialog({ if (mode === "attach" && !selectedPveJob) return setSubmitting(true) setError(null) + // "Import existing" mode: push the user-supplied keyfile to the + // canonical shared path BEFORE anything else. This mirrors the + // shell wizard where hb_pbs_import_keyfile runs before recovery + // setup and before the job .env is written. If the upload fails + // we bail loudly instead of silently downgrading to "generate new". + if (backend === "pbs" && pbsEncryptMode === "existing") { + if (!pbsImportFile) { + setError("Pick a keyfile to import.") + setSubmitting(false) + return + } + setPbsImportBusy(true) + try { + const buf = new Uint8Array(await pbsImportFile.arrayBuffer()) + // btoa on binary requires a String.fromCharCode round-trip. + // The keyfile is small (~200 bytes), so no perf concern. + let bin = "" + for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i]) + const content_b64 = btoa(bin) + await fetchApi("/api/host-backups/pbs-encryption/import", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content_b64 }), + }) + mutatePbsRecovery() + } catch (e) { + setError(`Keyfile import failed: ${e instanceof Error ? e.message : String(e)}`) + setPbsImportBusy(false) + setSubmitting(false) + return + } + setPbsImportBusy(false) + } // Configure the PBS recovery escrow BEFORE creating the job so the // very first backup the runner triggers can already upload the // blob. Only fires when the operator typed a passphrase pair. @@ -2374,7 +2432,7 @@ function CreateJobDialog({ body.pbs_password = "" if (pbsBackupId) body.pbs_backup_id = pbsBackupId if (selectedPbs?.fingerprint) body.pbs_fingerprint = selectedPbs.fingerprint - body.pbs_encrypt = pbsEncrypt + body.pbs_encrypt_mode = pbsEncryptMode } else if (backend === "local") { if (localDestDir.trim()) body.local_dest_dir = localDestDir.trim() } else if (backend === "borg") { @@ -2938,23 +2996,50 @@ function CreateJobDialog({

{/* PBS client-side encryption — same flow as the - shell wizard. A shared keyfile under /usr/local/ - share/proxmenux/pbs-key.conf is generated on - first use and reused by every encrypted job. */} + shell wizard. The keyfile can be freshly generated + per host or imported from an existing one the + operator uses across every host. */}
-