From d5d979aa5d8c44423efda5fe869812dbecb9dea2 Mon Sep 17 00:00:00 2001 From: Jarek Krochmalski Date: Tue, 25 Aug 2026 05:42:29 +0200 Subject: [PATCH] hide tls certificate fields for backends that never use tls --- src/lib/shared/repo-predicates.ts | 16 ++++++++++ .../settings/backups/DestinationModal.svelte | 17 +++++++--- tests/repo-predicates.test.ts | 32 ++++++++++++++++++- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/lib/shared/repo-predicates.ts b/src/lib/shared/repo-predicates.ts index aef2f53..728d0ff 100644 --- a/src/lib/shared/repo-predicates.ts +++ b/src/lib/shared/repo-predicates.ts @@ -13,6 +13,22 @@ export function isLocalRepo(repository: string): boolean { return repository.startsWith('/') || repository.startsWith('./'); } +/** + * Whether a backup destination's TLS-certificate fields (CA / client cert) are + * meaningful for a given backend type. Only backends that can be self-hosted over + * HTTPS with a private/self-signed CA qualify: `s3` (MinIO/Ceph) and `rest`. A + * local path has no network, and `b2`/`azure`/`gs` are managed clouds served from + * public CAs, so the TLS fields are noise there. When editing a destination that + * already has a cert stored, return true regardless so its data is never hidden. + */ +export function backendSupportsTls( + backendType: string, + opts?: { isEditing?: boolean; hasStoredCert?: boolean } +): boolean { + if (backendType === 's3' || backendType === 'rest') return true; + return !!(opts?.isEditing && opts?.hasStoredCert); +} + /** An environment reachable over the network (hawser or direct-with-host). */ export function isRemoteEnvironment(env?: { connectionType?: string | null; host?: string | null } | null): boolean { if (!env) return false; diff --git a/src/routes/settings/backups/DestinationModal.svelte b/src/routes/settings/backups/DestinationModal.svelte index 5c04525..7d77ff2 100644 --- a/src/routes/settings/backups/DestinationModal.svelte +++ b/src/routes/settings/backups/DestinationModal.svelte @@ -18,6 +18,7 @@ import { FieldLabel } from '$lib/components/ui/field-label'; import { toast } from 'svelte-sonner'; import { focusFirstInput } from '$lib/utils'; + import { backendSupportsTls } from '$lib/shared/repo-predicates'; interface Destination { id: number; @@ -265,6 +266,12 @@ const repoFields = $derived(selectedBackend.fields.filter(f => !f.envKey)); const credentialFields = $derived(selectedBackend.fields.filter(f => f.envKey)); + // TLS certs only apply to backends self-hostable over HTTPS with a private CA + // (S3/REST); hidden for local/B2/Azure/Google Cloud. See backendSupportsTls. + const showTlsSection = $derived( + backendSupportsTls(formBackendType, { isEditing, hasStoredCert: hadCacert || hadTlsClientCert }) + ); + // A field is mandatory unless it opts out (optional) or is a toggle (skipHostKey). function fieldRequired(f: FormField): boolean { return !f.optional && f.key !== 'skipHostKey'; @@ -727,10 +734,11 @@ - + + {#if showTlsSection}
TLS certificates (optional)

@@ -783,6 +791,7 @@

+ {/if} diff --git a/tests/repo-predicates.test.ts b/tests/repo-predicates.test.ts index 27e9f85..0067312 100644 --- a/tests/repo-predicates.test.ts +++ b/tests/repo-predicates.test.ts @@ -6,7 +6,7 @@ * now advisory-only for the UI hint. */ import { describe, test, expect } from 'bun:test'; -import { isLocalRepo, isRemoteEnvironment, localRepoNeedsSameHost } from '../src/lib/shared/repo-predicates'; +import { isLocalRepo, isRemoteEnvironment, localRepoNeedsSameHost, backendSupportsTls } from '../src/lib/shared/repo-predicates'; describe('isLocalRepo', () => { test('absolute and relative filesystem paths are local', () => { @@ -55,3 +55,33 @@ describe('localRepoNeedsSameHost (#1316 advisory)', () => { expect(localRepoNeedsSameHost(localDest, undefined)).toBe(false); }); }); + +describe('backendSupportsTls (destination TLS fields visibility)', () => { + test('s3 and rest support TLS (self-hostable over HTTPS with a private CA)', () => { + expect(backendSupportsTls('s3')).toBe(true); + expect(backendSupportsTls('rest')).toBe(true); + }); + + test('local path and managed clouds do NOT show TLS fields', () => { + expect(backendSupportsTls('local')).toBe(false); + expect(backendSupportsTls('b2')).toBe(false); + expect(backendSupportsTls('azure')).toBe(false); + expect(backendSupportsTls('gs')).toBe(false); + }); + + test('editing a non-TLS backend that already has a cert stored still shows the section', () => { + // A destination created before this filter that carries a CA must not have its + // stored cert hidden on edit. + expect(backendSupportsTls('gs', { isEditing: true, hasStoredCert: true })).toBe(true); + expect(backendSupportsTls('local', { isEditing: true, hasStoredCert: true })).toBe(true); + }); + + test('editing without a stored cert keeps the section hidden for non-TLS backends', () => { + expect(backendSupportsTls('gs', { isEditing: true, hasStoredCert: false })).toBe(false); + expect(backendSupportsTls('azure', { isEditing: true, hasStoredCert: false })).toBe(false); + }); + + test('creating (not editing) never shows TLS for a non-TLS backend regardless of flags', () => { + expect(backendSupportsTls('local', { isEditing: false, hasStoredCert: true })).toBe(false); + }); +});