initial commit

This commit is contained in:
Jon Vanvik 2026-05-31 14:11:29 +02:00
commit 8de962eeb8
44 changed files with 4843 additions and 0 deletions

158
SettingsView.swift Normal file
View file

@ -0,0 +1,158 @@
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 allowSelfSigned = false
@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 {
Toggle("Allow self-signed certificate", isOn: $allowSelfSigned)
} header: {
Text("Security")
} footer: {
Text(allowSelfSigned
? "Trusts the certificate for this host only. Use only on a controlled management network/VPN. For production, install the CA via MDM and leave this off, or add public-key pinning in code."
: "Uses standard system trust evaluation (recommended).")
}
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 ?? ""
if case .allowSelfSignedForHost = config.trustPolicy { allowSelfSigned = true }
}
private func makeConfig() -> APIConfiguration? {
guard let url = Self.normalizedBaseURL(urlString), let host = url.host else { return nil }
let policy: ServerTrustPolicy = allowSelfSigned ? .allowSelfSignedForHost(host) : .system
return APIConfiguration(
baseURL: url,
apiVersion: apiVersion.trimmingCharacters(in: .whitespaces),
databaseId: databaseId.isEmpty ? nil : databaseId,
trustPolicy: policy
)
}
/// 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)
}
}
}