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>
145 lines
5.3 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|