initial commit
This commit is contained in:
commit
8de962eeb8
44 changed files with 4843 additions and 0 deletions
158
SettingsView.swift
Normal file
158
SettingsView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue