158 lines
6 KiB
Swift
158 lines
6 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 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)
|
|
}
|
|
}
|
|
}
|