PONGo_ios/SettingsView.swift
Jon Vanvik 6bb65d2aa3 Remove self-signed TLS option (system trust only); add session changelog
Parity with the Android client: drop the "Allow self-signed certificate"
escape hatch. The app now performs system TLS trust only — a self-signed
appliance must have its CA installed on the device (MDM/profile). A code-only
public-key pinning policy remains (no UI).

- ServerTrustEvaluator: remove ServerTrustPolicy.allowSelfSignedForHost and its
  challenge handler; keep .system (default) and .pinPublicKeySHA256.
- SettingsView: remove the self-signed toggle/state/prefill; makeConfig uses
  the default .system policy.
- MCMSConnection / APIConfiguration: update usage-sketch + doc comments.
- README / GUI-NOTES / MCMS_API.md: document system-trust-only; self-signed
  boxes need their CA installed on the device.
- Add CHANGELOG.md for this session, flagged for the Android port.

Note: configs previously persisted with the self-signed policy fail to decode
and reset to defaults (one-time re-entry of the server URL).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 17:15:38 +02:00

145 lines
5.3 KiB
Swift

import SwiftUI
/// Connection configuration. Doubles as initial setup (no config yet) and the
/// in-app Settings tab. Credentials are entered on the Login screen, not here.
struct SettingsView: View {
let isInitialSetup: Bool
@Environment(AppEnvironment.self) private var env
@Environment(\.dismiss) private var dismiss
@State private var urlString = "https://"
@State private var apiVersion = "v1"
@State private var databaseId = ""
@State private var testPhase: LoadPhase = .idle
@State private var testResult: String?
@State private var saveError: String?
var body: some View {
Form {
Section {
TextField("https://host[/api]", text: $urlString)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.URL)
TextField("API version", text: $apiVersion)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
} header: {
Text("Server")
} footer: {
Text("Host or origin, e.g. https://mcms.lab.svorka.net — “/api” is appended automatically unless you enter a path. Endpoints are v1.")
}
Section("Database (optional)") {
TextField("Database ID", text: $databaseId)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
}
Section {
Button {
Task { await runTest() }
} label: {
HStack {
Text("Test connection")
Spacer()
switch testPhase {
case .loading: ProgressView()
case .loaded: Image(systemName: "checkmark.circle.fill").foregroundStyle(.green)
case .failed: Image(systemName: "xmark.circle.fill").foregroundStyle(.red)
case .idle: EmptyView()
}
}
}
.disabled(!canSave)
if let testResult {
Text(testResult).font(.footnote).foregroundStyle(.secondary)
}
} footer: {
if case .failed(let msg) = testPhase {
Text(msg).foregroundStyle(.red)
}
}
if !isInitialSetup {
Section {
Button("Sign out", role: .destructive) {
Task { await env.signOut() }
}
}
}
if let saveError {
Section { Text(saveError).foregroundStyle(.red) }
}
}
.navigationTitle(isInitialSetup ? "Set up connection" : "Settings")
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Save") { save() }.disabled(!canSave)
}
}
.onAppear(perform: prefill)
}
private var canSave: Bool {
Self.normalizedBaseURL(urlString) != nil && !apiVersion.trimmingCharacters(in: .whitespaces).isEmpty
}
private func prefill() {
guard let config = env.configuration else { return }
urlString = config.baseURL.absoluteString
apiVersion = config.apiVersion
databaseId = config.databaseId ?? ""
}
private func makeConfig() -> APIConfiguration? {
// TLS uses system trust only (no self-signed bypass); trustPolicy defaults to .system.
guard let url = Self.normalizedBaseURL(urlString), url.host != nil else { return nil }
return APIConfiguration(
baseURL: url,
apiVersion: apiVersion.trimmingCharacters(in: .whitespaces),
databaseId: databaseId.isEmpty ? nil : databaseId
)
}
/// Normalizes the entered server URL: requires scheme + host, trims any
/// trailing slash, and defaults the path to `/api` when none is given every
/// MCMS REST path lives under `/api` (dev guide). A reverse proxy mounted
/// elsewhere can still be entered with an explicit path.
static func normalizedBaseURL(_ raw: String) -> URL? {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard var comps = URLComponents(string: trimmed),
comps.scheme != nil, let host = comps.host, !host.isEmpty else { return nil }
var path = comps.path
while path.hasSuffix("/") { path.removeLast() }
if path.isEmpty { path = "/api" }
comps.path = path
return comps.url
}
private func save() {
guard let config = makeConfig() else {
saveError = "Enter a valid URL including scheme (https://)."
return
}
saveError = nil
env.configure(config)
if !isInitialSetup { dismiss() }
}
private func runTest() async {
guard let config = makeConfig() else { return }
testPhase = .loading
testResult = nil
do {
let result = try await env.probe(config)
testResult = "Connected · \(result)"
testPhase = .loaded
} catch {
testPhase = .failed((error as? APIError)?.localizedDescription ?? error.localizedDescription)
}
}
}