163 lines
6.7 KiB
Swift
163 lines
6.7 KiB
Swift
import SwiftUI
|
|
|
|
struct HostEditor: View {
|
|
let store: ProxyStore
|
|
let isNew: Bool
|
|
@State private var host: ProxyHost
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var domains: String
|
|
@State private var port: String
|
|
@State private var saving = false
|
|
@State private var error: String?
|
|
|
|
init(store: ProxyStore, host: ProxyHost, isNew: Bool) {
|
|
self.store = store
|
|
self.isNew = isNew
|
|
_host = State(initialValue: host)
|
|
_domains = State(initialValue: host.domainNames.joined(separator: ", "))
|
|
_port = State(initialValue: String(host.forwardPort))
|
|
}
|
|
|
|
private var valid: Bool {
|
|
let names = domains.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
return !names.isEmpty && names.allSatisfy { !$0.isEmpty && !$0.contains(" ") && !$0.contains("/") }
|
|
&& !host.forwardHost.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
&& (1...65535).contains(Int(port) ?? 0)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
HStack {
|
|
Text(isNew ? "New Proxy Host" : "Edit Proxy Host").font(.headline)
|
|
Spacer()
|
|
}
|
|
.padding(20)
|
|
Form {
|
|
Section {
|
|
TextField("Domain Names", text: $domains, prompt: Text("app.example.com"))
|
|
Picker("Forward Scheme", selection: $host.forwardScheme) {
|
|
Text("HTTP").tag("http")
|
|
Text("HTTPS").tag("https")
|
|
}
|
|
TextField("Forward Host", text: $host.forwardHost, prompt: Text("192.168.1.20"))
|
|
TextField("Forward Port", text: $port, prompt: Text("8080"))
|
|
} header: {
|
|
Text("Forwarding")
|
|
} footer: {
|
|
Text("Separate multiple domain names with commas.")
|
|
}
|
|
Section("Security") {
|
|
Picker("SSL Certificate", selection: $host.certificateId) {
|
|
Text("None").tag(0)
|
|
ForEach(store.certificates) { certificate in
|
|
Text(certificate.niceName).tag(certificate.id)
|
|
}
|
|
}
|
|
Toggle("Force HTTPS", isOn: $host.sslForced).disabled(host.certificateId == 0)
|
|
Toggle("Block Common Exploits", isOn: $host.blockExploits)
|
|
}
|
|
Section("Options") {
|
|
Toggle("HTTP/2 Support", isOn: $host.http2Support)
|
|
Toggle("WebSocket Support", isOn: $host.allowWebsocketUpgrade)
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
.disabled(saving)
|
|
if let error {
|
|
Label(error, systemImage: "exclamationmark.triangle")
|
|
.foregroundStyle(.red).padding(.horizontal, 20).padding(.bottom, 12)
|
|
}
|
|
Divider()
|
|
HStack {
|
|
if saving { ProgressView().controlSize(.small) }
|
|
if store.isDemo { Text("Demo workspace").font(.callout).foregroundStyle(.secondary) }
|
|
Spacer()
|
|
Button("Cancel") { dismiss() }.keyboardShortcut(.cancelAction).disabled(saving)
|
|
Button(isNew ? "Add" : "Save") { Task { await save() } }
|
|
.keyboardShortcut(.defaultAction)
|
|
.disabled(!valid || saving)
|
|
}
|
|
.padding(20)
|
|
}
|
|
.frame(width: 510, height: 610)
|
|
.interactiveDismissDisabled(saving)
|
|
}
|
|
|
|
private func save() async {
|
|
saving = true
|
|
error = nil
|
|
host.domainNames = domains.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
host.forwardHost = host.forwardHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
host.forwardPort = Int(port) ?? 80
|
|
if host.certificateId == 0 { host.sslForced = false }
|
|
do {
|
|
try await store.save(host, isNew: isNew)
|
|
dismiss()
|
|
} catch { self.error = error.localizedDescription }
|
|
saving = false
|
|
}
|
|
}
|
|
|
|
struct ConnectionSheet: View {
|
|
let store: ProxyStore
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var url = ""
|
|
@State private var email = ""
|
|
@State private var password = ""
|
|
@State private var connecting = false
|
|
@State private var error: String?
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
Text("Connect to Server").font(.headline)
|
|
Text("Enter the address and credentials for your Nginx Proxy Manager server.")
|
|
.foregroundStyle(.secondary)
|
|
Form {
|
|
TextField("Server URL:", text: $url, prompt: Text("https://proxy.example.com"))
|
|
TextField("Email:", text: $email, prompt: Text("admin@example.com"))
|
|
SecureField("Password:", text: $password)
|
|
}
|
|
.textFieldStyle(.roundedBorder)
|
|
.disabled(connecting || store.isBusy)
|
|
Text("Your login is saved in Keychain after connecting and used to reconnect when you open the app.")
|
|
.font(.callout).foregroundStyle(.secondary)
|
|
Button("Forget Saved Login") {
|
|
do {
|
|
try store.forgetCredentials()
|
|
password = ""
|
|
error = nil
|
|
} catch { self.error = error.localizedDescription }
|
|
}
|
|
.disabled(connecting || store.isBusy)
|
|
Text("Forgetting the saved login leaves the current session connected.")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
if let error {
|
|
Label(error, systemImage: "exclamationmark.triangle").foregroundStyle(.red)
|
|
}
|
|
HStack {
|
|
if connecting { ProgressView().controlSize(.small) }
|
|
Spacer()
|
|
Button("Cancel") { dismiss() }.keyboardShortcut(.cancelAction).disabled(connecting)
|
|
Button("Connect") { Task { await connect() } }
|
|
.keyboardShortcut(.defaultAction)
|
|
.disabled(connecting || store.isBusy || url.isEmpty || email.isEmpty || password.isEmpty)
|
|
}
|
|
}
|
|
.padding(24)
|
|
.frame(width: 460)
|
|
.onAppear { url = store.baseURL }
|
|
.interactiveDismissDisabled(connecting)
|
|
}
|
|
|
|
private func connect() async {
|
|
connecting = true
|
|
error = nil
|
|
do {
|
|
try await store.connect(url: url, email: email, password: password)
|
|
password = ""
|
|
dismiss()
|
|
} catch { self.error = error.localizedDescription }
|
|
connecting = false
|
|
}
|
|
}
|