diff --git a/NodePass.xcodeproj/project.pbxproj b/NodePass.xcodeproj/project.pbxproj index 4f7049c..87b52b8 100644 --- a/NodePass.xcodeproj/project.pbxproj +++ b/NodePass.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 57D9675B2F23B08C00AE3CCB /* ConnectionMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D967582F23B08C00AE3CCB /* ConnectionMode.swift */; }; 57D9675C2F23B08C00AE3CCB /* LoadBalancingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D967592F23B08C00AE3CCB /* LoadBalancingStrategy.swift */; }; 57D9675D2F23B08C00AE3CCB /* TLSMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D9675A2F23B08C00AE3CCB /* TLSMode.swift */; }; + 57D967602F24B72B00AE3CCB /* NetworkTuningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D9675E2F24B72B00AE3CCB /* NetworkTuningView.swift */; }; + 57D967612F24B72B00AE3CCB /* ProtocolControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D9675F2F24B72B00AE3CCB /* ProtocolControlView.swift */; }; CB1C10DF2F2495ED00B74CD8 /* TrafficBlockingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB1C10DE2F2495ED00B74CD8 /* TrafficBlockingView.swift */; }; CB1E97FF2E2262F600A175FD /* NATPassthroughDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB1E97FE2E2262E700A175FD /* NATPassthroughDetailView.swift */; }; CB1E98012E226C1E00A175FD /* DirectForwardDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB1E98002E226C0F00A175FD /* DirectForwardDetailView.swift */; }; @@ -165,6 +167,8 @@ 57D967582F23B08C00AE3CCB /* ConnectionMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionMode.swift; sourceTree = ""; }; 57D967592F23B08C00AE3CCB /* LoadBalancingStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadBalancingStrategy.swift; sourceTree = ""; }; 57D9675A2F23B08C00AE3CCB /* TLSMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLSMode.swift; sourceTree = ""; }; + 57D9675E2F24B72B00AE3CCB /* NetworkTuningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkTuningView.swift; sourceTree = ""; }; + 57D9675F2F24B72B00AE3CCB /* ProtocolControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolControlView.swift; sourceTree = ""; }; CB1C10DE2F2495ED00B74CD8 /* TrafficBlockingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrafficBlockingView.swift; sourceTree = ""; }; CB1E97FE2E2262E700A175FD /* NATPassthroughDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NATPassthroughDetailView.swift; sourceTree = ""; }; CB1E98002E226C0F00A175FD /* DirectForwardDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectForwardDetailView.swift; sourceTree = ""; }; @@ -446,6 +450,8 @@ CB6682BD2E174F9200A27696 /* Instance */ = { isa = PBXGroup; children = ( + 57D9675E2F24B72B00AE3CCB /* NetworkTuningView.swift */, + 57D9675F2F24B72B00AE3CCB /* ProtocolControlView.swift */, 57D967502F239AF600AE3CCB /* AddInstanceView.swift */, 57D967512F239AF600AE3CCB /* EditInstanceView.swift */, 57D967522F239AF600AE3CCB /* InstanceFormView.swift */, @@ -726,6 +732,8 @@ CB6682A32E172F3C00A27696 /* LoadingStateModifier.swift in Sources */, CBC6F6012ED815D20024F670 /* ImageAlignment.swift in Sources */, CB6682D02E1DD9C700A27696 /* UpdateInstanceStatusAction.swift in Sources */, + 57D967602F24B72B00AE3CCB /* NetworkTuningView.swift in Sources */, + 57D967612F24B72B00AE3CCB /* ProtocolControlView.swift in Sources */, CB1E98012E226C1E00A175FD /* DirectForwardDetailView.swift in Sources */, CB6682782E1181CA00A27696 /* Server.swift in Sources */, CB6682AE2E17345D00A27696 /* AddDirectForwardServiceView.swift in Sources */, diff --git a/NodePass/Server/Instance/InstanceCardView.swift b/NodePass/Server/Instance/InstanceCardView.swift index 3e9d004..54ede28 100644 --- a/NodePass/Server/Instance/InstanceCardView.swift +++ b/NodePass/Server/Instance/InstanceCardView.swift @@ -32,6 +32,13 @@ struct InstanceCardView: View { Badge("\(ping) ms", backgroundColor: .blue, textColor: .white) } Spacer() + if let alias = instance.alias, !alias.isEmpty { + Text(alias) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.tail) + } } Text(instance.url) diff --git a/NodePass/Server/Instance/InstanceFormView.swift b/NodePass/Server/Instance/InstanceFormView.swift index cbbea71..c4b5d14 100644 --- a/NodePass/Server/Instance/InstanceFormView.swift +++ b/NodePass/Server/Instance/InstanceFormView.swift @@ -42,11 +42,9 @@ struct InstanceFormView: View { @State private var disableTCP: Bool = false @State private var disableUDP: Bool = false @State private var enableProxy: Bool = false - @State private var blockHTTP: Bool = false @State private var blockTLS: Bool = false @State private var blockSOCKS: Bool = false - @State private var lbsStrategy: LoadBalancingStrategy = .roundRobin @State private var urlString: String = "" @State private var isShowErrorAlert: Bool = false @@ -68,6 +66,24 @@ struct InstanceFormView: View { return blockedTrafficStrings.joined(separator: ", ") } + private var networkTuningSummary: String { + var settings: [String] = [] + if !dnsCache.isEmpty { settings.append("DNS: \(dnsCache)") } + if !dialAddress.isEmpty { settings.append("Dial: \(dialAddress)") } + if !readTimeout.isEmpty { settings.append("Read: \(readTimeout)") } + if !rateLimit.isEmpty { settings.append("Rate: \(rateLimit)") } + if !maxSlots.isEmpty { settings.append("Slot: \(maxSlots)") } + return settings.isEmpty ? "Default" : settings.joined(separator: ", ") + } + + private var protocolControlSummary: String { + var settings: [String] = [] + if disableTCP { settings.append("TCP Off") } + if disableUDP { settings.append("UDP Off") } + if enableProxy { settings.append("PROXY On") } + return settings.isEmpty ? "Default" : settings.joined(separator: ", ") + } + enum InputMode: String, CaseIterable { case form = "Form" case url = "URL" @@ -98,10 +114,10 @@ struct InstanceFormView: View { Text("Input Method") } footer: { if inputMode == .form { - Text("Configure instance using form fields") + Text("Configure instance using form fields.") .foregroundStyle(.secondary) } else { - Text("Enter instance URL directly") + Text("Enter instance URL directly.") .foregroundStyle(.secondary) } } @@ -182,16 +198,16 @@ struct InstanceFormView: View { Text("Instance Type") } footer: { if instanceType == .server { - Text("Listen on tunnel address and forward to/from target") + Text("Listen on tunnel address and forward to/from target.") .foregroundStyle(.secondary) } else { - Text("Connect to tunnel address and forward from/to target") + Text("Connect to tunnel address and forward from/to target.") .foregroundStyle(.secondary) } } Section { - LabeledTextField("IP", prompt: instanceType == .server ? "" : "", text: $tunnelAddress) + LabeledTextField("IP", prompt: "Optional", text: $tunnelAddress) .autocorrectionDisabled() #if os(iOS) .textInputAutocapitalization(.never) @@ -203,10 +219,10 @@ struct InstanceFormView: View { } footer: { VStack(alignment: .leading) { if instanceType == .server { - Text("Tunnel address to bind, empty IP for all interfaces") + Text("Tunnel address to bind, empty IP for all interfaces.") .foregroundStyle(.secondary) } else { - Text("Server address to connect or Client address to bind") + Text("Server address to connect or Client address to bind.") .foregroundStyle(.secondary) } } @@ -300,7 +316,7 @@ struct InstanceFormView: View { #endif } } else { - LabeledTextField("IP", prompt: instanceType == .server ? "" : "", text: $targetAddress) + LabeledTextField("IP", prompt: "Optional", text: $targetAddress) .autocorrectionDisabled() #if os(iOS) .textInputAutocapitalization(.never) @@ -313,10 +329,10 @@ struct InstanceFormView: View { } footer: { VStack(alignment: .leading, spacing: 4) { if isMultipleTargets { - Text("Configure multiple target addresses for load balancing") + Text("Configure multiple target addresses for load balancing.") .foregroundStyle(.secondary) } else { - Text("Single target address to connect or to bind") + Text("Single target address to connect or to bind.") .foregroundStyle(.secondary) } } @@ -358,10 +374,10 @@ struct InstanceFormView: View { } footer: { VStack(alignment: .leading, spacing: 4) { if instanceType == .server { - Text("TLS encryption settings") + Text("TLS encryption settings.") .foregroundStyle(.secondary) } else { - Text("SNI hostname for TLS connections") + Text("SNI hostname for TLS connections.") .foregroundStyle(.secondary) } } @@ -391,64 +407,52 @@ struct InstanceFormView: View { Text("Connection Pool") } footer: { VStack(alignment: .leading, spacing: 4) { - Text("Configure connection pool behavior and limits") + Text("Configure connection pool behavior and limits.") .foregroundStyle(.secondary) } } Section { - LabeledTextField("DNS Cache Duration", prompt: "5m", text: $dnsCache) - .autocorrectionDisabled() -#if os(iOS) - .textInputAutocapitalization(.never) -#endif - - LabeledTextField("Dial Address", prompt: "auto", text: $dialAddress) - .autocorrectionDisabled() -#if os(iOS) - .textInputAutocapitalization(.never) -#endif - - LabeledTextField("Read Timeout", prompt: "0", text: $readTimeout) - .autocorrectionDisabled() -#if os(iOS) - .textInputAutocapitalization(.never) -#endif - - LabeledTextField("Rate Limit (Mbps)", prompt: "0", text: $rateLimit, isNumberOnly: true) + Picker("Logging Level", selection: $logLevel) { + ForEach(LogLevel.allCases, id: \.self) { level in + Text(level.rawValue).tag(level) + } + } - LabeledTextField("Max Connections", prompt: "65536", text: $maxSlots, isNumberOnly: true) - } header: { - Text("Network Tuning") - } footer: { - VStack(alignment: .leading, spacing: 4) { - Text("DNS: Cache TTL duration in '30s, 5m, 1h'") - .foregroundStyle(.secondary) - Text("Dial: Specific source IP or 'auto' by OS") - .foregroundStyle(.secondary) - Text("Read: Timeout duration or 0 to disable") - .foregroundStyle(.secondary) - Text("Rate: Bandwidth limit or 0 for unlimited") - .foregroundStyle(.secondary) - Text("Slot: Max concurrent connections allowed") - .foregroundStyle(.secondary) + NavigationLink { + NetworkTuningView( + dnsCache: $dnsCache, + dialAddress: $dialAddress, + readTimeout: $readTimeout, + rateLimit: $rateLimit, + maxSlots: $maxSlots + ) + } label: { + HStack { + Text("Network Tuning") + Spacer() + Text(networkTuningSummary) + .foregroundStyle(.secondary) + .lineLimit(1) + } } - } - - Section { - Toggle("Disable TCP", isOn: $disableTCP) - Toggle("Disable UDP", isOn: $disableUDP) - Toggle("Enable PROXY Protocol", isOn: $enableProxy) - } header: { - Text("Protocol Control") - } footer: { - VStack(alignment: .leading, spacing: 4) { - Text("Control protocol availability and PROXY protocol v1 support") - .foregroundStyle(.secondary) + + NavigationLink { + ProtocolControlView( + disableTCP: $disableTCP, + disableUDP: $disableUDP, + enableProxy: $enableProxy + ) + } label: { + HStack { + Text("Protocol Control") + Spacer() + Text(protocolControlSummary) + .foregroundStyle(.secondary) + .lineLimit(1) + } } - } - - Section { + NavigationLink { TrafficBlockingView(blockHTTP: $blockHTTP, blockTLS: $blockTLS, blockSOCKS: $blockSOCKS) } label: { @@ -457,23 +461,14 @@ struct InstanceFormView: View { Spacer() Text(blockedTrafficString) .foregroundStyle(.secondary) - } - } - } - - Section { - Picker("Log Level", selection: $logLevel) { - ForEach(LogLevel.allCases, id: \.self) { level in - Text(level.rawValue).tag(level) + .lineLimit(1) } } } header: { - Text("Logging") + Text("Advanced Settings") } footer: { - VStack(alignment: .leading, spacing: 4) { - Text("Set logging verbosity level") - .foregroundStyle(.secondary) - } + Text("Configure advanced settings and tuning parameters.") + .foregroundStyle(.secondary) } Section { @@ -517,6 +512,7 @@ struct InstanceFormView: View { } .buttonStyle(.borderless) } + .listRowSeparator(.hidden) } Button { @@ -533,7 +529,7 @@ struct InstanceFormView: View { Text("Additional Parameters") } footer: { VStack(alignment: .leading, spacing: 4) { - Text("Add custom URL query parameters not covered above") + Text("Add custom URL query parameters not covered above.") .foregroundStyle(.secondary) } } @@ -792,7 +788,7 @@ struct InstanceFormView: View { queryParams.append("lbs=\(lbsStrategy.rawValue)") } - if isAdvancedModeEnabled && logLevel != .info { + if logLevel != .info { queryParams.append("log=\(logLevel.rawValue)") } @@ -823,17 +819,20 @@ struct InstanceFormView: View { let instanceService = InstanceService() do { if let instance = instance { - _ = try await instanceService.updateInstance( - baseURLString: server.url, - apiKey: server.key, - id: instance.id, - url: url - ) + if instance.url != url { + _ = try await instanceService.updateInstance( + baseURLString: server.url, + apiKey: server.key, + id: instance.id, + url: url + ) + } } else { _ = try await instanceService.createInstance( baseURLString: server.url, apiKey: server.key, - url: url + url: url, + alias: String(localized: "Untitled") ) } await MainActor.run { diff --git a/NodePass/Server/Instance/InstanceListView.swift b/NodePass/Server/Instance/InstanceListView.swift index cc5a171..eff4067 100644 --- a/NodePass/Server/Instance/InstanceListView.swift +++ b/NodePass/Server/Instance/InstanceListView.swift @@ -17,6 +17,10 @@ struct InstanceListView: View { @State private var isShowAddInstanceSheet: Bool = false @State private var instanceToEdit: Instance? + @State private var isShowRenameAlert: Bool = false + @State private var instanceToRename: Instance? + @State private var renameText: String = "" + @State private var isShowDeleteInstanceAlert: Bool = false @State private var instanceToDelete: Instance? @@ -27,7 +31,9 @@ struct InstanceListView: View { Form { let validInstances = instances.filter({ [.running, .stopped, .error].contains($0.status) }) let filteredInstances = validInstances.filter { instance in - searchText.isEmpty || instance.url.localizedCaseInsensitiveContains(searchText) + searchText.isEmpty || + instance.url.localizedCaseInsensitiveContains(searchText) || + (instance.alias ?? "").localizedCaseInsensitiveContains(searchText) } ForEach(filteredInstances) { instance in instanceCard(instance: instance) @@ -76,6 +82,20 @@ struct InstanceListView: View { } message: { Text("You are about to delete this service. This action is irreversible. Are you sure?") } + .alert("Rename Instance", isPresented: $isShowRenameAlert) { + TextField("Name", text: $renameText) + Button("Cancel", role: .cancel) { + instanceToRename = nil + renameText = "" + } + Button("OK") { + renameInstance(instance: instanceToRename!, newAlias: renameText) + instanceToRename = nil + renameText = "" + } + } message: { + Text("Enter a new name for the instance.") + } .alert("Error", isPresented: $isShowErrorAlert) { Button("OK", role: .cancel) { listInstances() @@ -88,6 +108,31 @@ struct InstanceListView: View { @ViewBuilder private func instanceCard(instance: Instance) -> some View { InstanceCardView(instance: instance) + .swipeActions(edge: .leading) { + Button { + NPUI.copyToClipboard(instance.url) + } label: { + Label("Copy", systemImage: "doc.on.doc") + } + .tint(.blue) + } + .swipeActions(edge: .trailing) { + Button { + instanceToEdit = instance + } label: { + Label("Edit", systemImage: "pencil") + } + .tint(.orange) + + Button { + instanceToRename = instance + renameText = instance.alias ?? "" + isShowRenameAlert = true + } label: { + Label("Rename", systemImage: "character.cursor.ibeam") + } + .tint(.blue) + } .contextMenu { ControlGroup { Button { @@ -107,17 +152,6 @@ struct InstanceListView: View { } } Divider() - Button { - NPUI.copyToClipboard(instance.url) - } label: { - Label("Copy URL", systemImage: "document.on.document") - } - Button { - instanceToEdit = instance - } label: { - Label("Edit", systemImage: "pencil") - } - Divider() Button(role: .destructive) { instanceToDelete = instance isShowDeleteInstanceAlert = true @@ -162,6 +196,28 @@ struct InstanceListView: View { } } + private func renameInstance(instance: Instance, newAlias: String) { + Task { + let instanceService = InstanceService() + do { + let aliasValue = NPCore.noEmptyName(newAlias) + try await instanceService.updateInstanceAlias( + baseURLString: server.url, + apiKey: server.key, + id: instance.id, + alias: aliasValue + ) + listInstances() + } catch { +#if DEBUG + print("Error Renaming Instance: \(error.localizedDescription)") +#endif + errorMessage = error.localizedDescription + isShowErrorAlert = true + } + } + } + private func updateInstanceStatus(instance: Instance, action: UpdateInstanceStatusAction) { Task { let instanceService = InstanceService() diff --git a/NodePass/Server/Instance/NetworkTuningView.swift b/NodePass/Server/Instance/NetworkTuningView.swift new file mode 100644 index 0000000..955dbbc --- /dev/null +++ b/NodePass/Server/Instance/NetworkTuningView.swift @@ -0,0 +1,61 @@ +// +// NetworkTuningView.swift +// NodePass +// +// Created by Yosebyte on 1/24/26. +// + +import SwiftUI + +struct NetworkTuningView: View { + @Binding var dnsCache: String + @Binding var dialAddress: String + @Binding var readTimeout: String + @Binding var rateLimit: String + @Binding var maxSlots: String + + var body: some View { + Form { + Section { + LabeledTextField("DNS Cache Duration", prompt: "5m", text: $dnsCache) + .autocorrectionDisabled() +#if os(iOS) + .textInputAutocapitalization(.never) +#endif + + LabeledTextField("Dial Address", prompt: "auto", text: $dialAddress) + .autocorrectionDisabled() +#if os(iOS) + .textInputAutocapitalization(.never) +#endif + + LabeledTextField("Read Timeout", prompt: "0", text: $readTimeout) + .autocorrectionDisabled() +#if os(iOS) + .textInputAutocapitalization(.never) +#endif + + LabeledTextField("Rate Limit (Mbps)", prompt: "0", text: $rateLimit, isNumberOnly: true) + + LabeledTextField("Max Connections", prompt: "65536", text: $maxSlots, isNumberOnly: true) + } footer: { + VStack(alignment: .leading, spacing: 4) { + Text("DNS: Cache TTL duration in '30s, 5m, 1h'.") + .foregroundStyle(.secondary) + Text("Dial: Specific source IP or 'auto' by OS.") + .foregroundStyle(.secondary) + Text("Read: Timeout duration or 0 to disable.") + .foregroundStyle(.secondary) + Text("Rate: Bandwidth limit or 0 for unlimited.") + .foregroundStyle(.secondary) + Text("Slot: Max concurrent connections allowed.") + .foregroundStyle(.secondary) + } + } + } + .navigationTitle("Network Tuning") +#if os(iOS) + .navigationBarTitleDisplayMode(.inline) +#endif + } +} diff --git a/NodePass/Server/Instance/ProtocolControlView.swift b/NodePass/Server/Instance/ProtocolControlView.swift new file mode 100644 index 0000000..018eb2e --- /dev/null +++ b/NodePass/Server/Instance/ProtocolControlView.swift @@ -0,0 +1,33 @@ +// +// ProtocolControlView.swift +// NodePass +// +// Created by Yosebyte on 1/24/26. +// + +import SwiftUI + +struct ProtocolControlView: View { + @Binding var disableTCP: Bool + @Binding var disableUDP: Bool + @Binding var enableProxy: Bool + + var body: some View { + Form { + Section { + Toggle("Disable TCP", isOn: $disableTCP) + Toggle("Disable UDP", isOn: $disableUDP) + Toggle("Enable PROXY Protocol", isOn: $enableProxy) + } footer: { + VStack(alignment: .leading, spacing: 4) { + Text("Control protocol availability and PROXY v1 support.") + .foregroundStyle(.secondary) + } + } + } + .navigationTitle("Protocol Control") +#if os(iOS) + .navigationBarTitleDisplayMode(.inline) +#endif + } +} diff --git a/NodePass/Server/Instance/TrafficBlockingView.swift b/NodePass/Server/Instance/TrafficBlockingView.swift index a4efbc7..cee9300 100644 --- a/NodePass/Server/Instance/TrafficBlockingView.swift +++ b/NodePass/Server/Instance/TrafficBlockingView.swift @@ -19,9 +19,12 @@ struct TrafficBlockingView: View { Toggle("Block TLS", isOn: $blockTLS) Toggle("Block SOCKS", isOn: $blockSOCKS) } footer: { - Text("Block certain traffic from being tunneled") + Text("Block certain traffic from being tunneled.") } } .navigationTitle("Traffic Blocking") +#if os(iOS) + .navigationBarTitleDisplayMode(.inline) +#endif } } diff --git a/NodePass/Server/ServerListView.swift b/NodePass/Server/ServerListView.swift index 8667dcd..bef6b02 100644 --- a/NodePass/Server/ServerListView.swift +++ b/NodePass/Server/ServerListView.swift @@ -196,20 +196,21 @@ struct ServerListView: View { state.pathServers.append(server) } .contextMenu { - Button { - state.editServerSheetMode = .editing - state.editServerSheetServer = server - state.isShowEditServerSheet = true - } label: { - Label("Edit", systemImage: "pencil") - } - - let base64EncodedURL = server.url.data(using: .utf8)!.base64EncodedString(options: .lineLength64Characters) - let base64EncodedKey = server.key.data(using: .utf8)!.base64EncodedString(options: .lineLength64Characters) - ShareLink(item: "np://master?url=\(base64EncodedURL)&key=\(base64EncodedKey)") { - Label("Share", systemImage: "square.and.arrow.up") + ControlGroup { + Button { + state.editServerSheetMode = .editing + state.editServerSheetServer = server + state.isShowEditServerSheet = true + } label: { + Label("Edit", systemImage: "pencil") + } + let base64EncodedURL = server.url.data(using: .utf8)!.base64EncodedString(options: .lineLength64Characters) + let base64EncodedKey = server.key.data(using: .utf8)!.base64EncodedString(options: .lineLength64Characters) + ShareLink(item: "np://master?url=\(base64EncodedURL)&key=\(base64EncodedKey)") { + Label("Share", systemImage: "square.and.arrow.up") + } } - + Divider() Button(role: .destructive) { serverToDelete = server isShowDeleteServerAlert = true diff --git a/NodePass/Service/Add Service/AddDirectForwardServiceView.swift b/NodePass/Service/Add Service/AddDirectForwardServiceView.swift index bb02f32..26483be 100644 --- a/NodePass/Service/Add Service/AddDirectForwardServiceView.swift +++ b/NodePass/Service/Add Service/AddDirectForwardServiceView.swift @@ -265,16 +265,17 @@ struct AddDirectForwardServiceView: View { Task { let instanceService = InstanceService() do { + let name = NPCore.noEmptyName(name) let clientInstance = try await instanceService.createInstance( baseURLString: client.url, apiKey: client.key, - url: command + url: command, + alias: "\(name)-client" ) let fullCommand = clientInstance.config ?? command let serviceId = UUID() - let name = NPCore.noEmptyName(name) let service = Service( id: serviceId, name: name, diff --git a/NodePass/Service/Add Service/AddNATPassthroughServiceView.swift b/NodePass/Service/Add Service/AddNATPassthroughServiceView.swift index 034cfc8..345ec3e 100644 --- a/NodePass/Service/Add Service/AddNATPassthroughServiceView.swift +++ b/NodePass/Service/Add Service/AddNATPassthroughServiceView.swift @@ -390,15 +390,18 @@ struct AddNATPassthroughServiceView: View { Task { let instanceService = InstanceService() do { + let name = NPCore.noEmptyName(name) async let createServerInstance: (Instance) = instanceService.createInstance( baseURLString: remoteServer.url, apiKey: remoteServer.key, - url: serverCommand + url: serverCommand, + alias: "\(name)-server" ) async let createClientInstance: (Instance) = instanceService.createInstance( baseURLString: localServer.url, apiKey: localServer.key, - url: clientCommand + url: clientCommand, + alias: "\(name)-client" ) let (serverInstance, clientInstance) = try await (createServerInstance, createClientInstance) @@ -407,7 +410,6 @@ struct AddNATPassthroughServiceView: View { let clientFullCommand = clientInstance.config ?? clientCommand let serviceId = UUID() - let name = NPCore.noEmptyName(name) let service = Service( id: serviceId, name: name, diff --git a/NodePass/Service/Add Service/AddTunnelForwardServiceView.swift b/NodePass/Service/Add Service/AddTunnelForwardServiceView.swift index dcf74ef..255ac3b 100644 --- a/NodePass/Service/Add Service/AddTunnelForwardServiceView.swift +++ b/NodePass/Service/Add Service/AddTunnelForwardServiceView.swift @@ -421,15 +421,18 @@ struct AddTunnelForwardServiceView: View { Task { let instanceService = InstanceService() do { + let name = NPCore.noEmptyName(name) async let createRelayServerInstance = instanceService.createInstance( baseURLString: relayServer.url, apiKey: relayServer.key, - url: relayServerCommand + url: relayServerCommand, + alias: "\(name)-client" ) async let createDestinationServerInstance = instanceService.createInstance( baseURLString: destinationServer.url, apiKey: destinationServer.key, - url: destinationServerCommand + url: destinationServerCommand, + alias: "\(name)-server" ) let (relayServerInstance, destinationServerInstance) = try await (createRelayServerInstance, createDestinationServerInstance) @@ -438,7 +441,6 @@ struct AddTunnelForwardServiceView: View { let destinationServerFullCommand = destinationServerInstance.config ?? destinationServerCommand let serviceId = UUID() - let name = NPCore.noEmptyName(name) let service = Service( id: serviceId, name: name, diff --git a/NodePass/Service/Card/DirectForwardCardView.swift b/NodePass/Service/Card/DirectForwardCardView.swift index f905b26..94d5edf 100644 --- a/NodePass/Service/Card/DirectForwardCardView.swift +++ b/NodePass/Service/Card/DirectForwardCardView.swift @@ -36,10 +36,18 @@ struct DirectForwardCardView: View { VStack(spacing: 20) { HStack { VStack(alignment: .leading) { - Text("Direct Forward") - .foregroundStyle(.secondary) - .font(.caption) - .bold() + HStack { + Text("Direct Forward") + .foregroundStyle(.secondary) + .font(.caption) + .bold() + Spacer() + if service.isConfigurationInvalid { + Label("Configuration Invalid", systemImage: "exclamationmark.triangle.fill") + .font(.caption2) + .foregroundStyle(.red) + } + } Text(service.name) } Spacer() diff --git a/NodePass/Service/Card/NATPassthroughCardView.swift b/NodePass/Service/Card/NATPassthroughCardView.swift index 5bf1530..8cf1006 100644 --- a/NodePass/Service/Card/NATPassthroughCardView.swift +++ b/NodePass/Service/Card/NATPassthroughCardView.swift @@ -36,10 +36,18 @@ struct NATPassthroughCardView: View { VStack(spacing: 20) { HStack { VStack(alignment: .leading) { - Text("NAT Passthrough") - .foregroundStyle(.secondary) - .font(.caption) - .bold() + HStack { + Text("NAT Passthrough") + .foregroundStyle(.secondary) + .font(.caption) + .bold() + Spacer() + if service.isConfigurationInvalid { + Label("Configuration Invalid", systemImage: "exclamationmark.triangle.fill") + .font(.caption2) + .foregroundStyle(.red) + } + } Text(service.name) } Spacer() diff --git a/NodePass/Service/Card/TunnelForwardCardView.swift b/NodePass/Service/Card/TunnelForwardCardView.swift index a6c0a2f..5510044 100644 --- a/NodePass/Service/Card/TunnelForwardCardView.swift +++ b/NodePass/Service/Card/TunnelForwardCardView.swift @@ -49,10 +49,18 @@ struct TunnelForwardCardView: View { VStack(spacing: 20) { HStack { VStack(alignment: .leading) { - Text("Tunnel Forward") - .foregroundStyle(.secondary) - .font(.caption) - .bold() + HStack { + Text("Tunnel Forward") + .foregroundStyle(.secondary) + .font(.caption) + .bold() + Spacer() + if service.isConfigurationInvalid { + Label("Configuration Invalid", systemImage: "exclamationmark.triangle.fill") + .font(.caption2) + .foregroundStyle(.red) + } + } Text(service.name) } Spacer() diff --git a/NodePass/Service/Detail/DirectForwardDetailView.swift b/NodePass/Service/Detail/DirectForwardDetailView.swift index ae6a6fc..4edfc83 100644 --- a/NodePass/Service/Detail/DirectForwardDetailView.swift +++ b/NodePass/Service/Detail/DirectForwardDetailView.swift @@ -19,37 +19,18 @@ struct DirectForwardDetailView: View { var server: Server? { servers.first(where: { $0.id == implementation.serverID }) } - var addressesAndPorts: (tunnel: (address: String, port: String), destination: (address: String, port: String)) { - NPCore.parseAddressesAndPorts(urlString: implementation.command) - } @Query private var servers: [Server] - @State private var isShowEditRelayPortAlert: Bool = false - @State private var newRelayPort: String = "" - - @State private var isShowEditDestinationAddressAlert: Bool = false - @State private var newDestinationAddress: String = "" - - @State private var isShowEditDestinationPortAlert: Bool = false - @State private var newDestinationPort: String = "" + @State private var instance: Instance? + @State private var isShowEditInstanceSheet: Bool = false @State private var isShowErrorAlert: Bool = false @State private var errorMessage: String = "" - @State private var isSensoryFeedbackTriggered: Bool = false - var body: some View { if service.type == .directForward { Form { - if let server { - let connectionString = "\(server.getHost()):\(addressesAndPorts.tunnel.port)" - Section("You Should Connect To") { - Text(connectionString) - } - .copiable(connectionString) - } - Section("Relay Server") { if let server { VStack { @@ -60,122 +41,45 @@ struct DirectForwardDetailView: View { } } .frame(maxWidth: .infinity) - } - else { - LabeledContent("Server") { - Text("Not on this device") - } - } - HStack { - LabeledContent("Listen Port") { - Text(addressesAndPorts.tunnel.port) - } - if server != nil { - Button { - isShowEditRelayPortAlert = true - } label: { - Image(systemName: "pencil") + + if let instance { + VStack { + InstanceCardView(instance: instance) + .onTapGesture { + isShowEditInstanceSheet = true + } } + .frame(maxWidth: .infinity) + } + else { + ProgressView() + .frame(maxWidth: .infinity) } - } - .copiable(addressesAndPorts.tunnel.port) - LabeledContent("Command URL") { - Text(implementation.command) - .minimumScaleFactor(0.5) - } - .copiable(implementation.command) - } - - Section("Destination Server") { - if implementation.isMultipleDestination { - } else { - HStack { - LabeledContent("Address") { - Text(addressesAndPorts.destination.address) - .minimumScaleFactor(0.5) - } - if server != nil { - Button { - isShowEditDestinationAddressAlert = true - } label: { - Image(systemName: "pencil") - } - } - } - .copiable(addressesAndPorts.destination.address) - HStack { - LabeledContent("Port") { - Text(addressesAndPorts.destination.port) - } - if server != nil { - Button { - isShowEditDestinationPortAlert = true - } label: { - Image(systemName: "pencil") - } - } + LabeledContent("Server") { + Text("Not on this device") } - .copiable(addressesAndPorts.destination.port) } } } .formStyle(.grouped) .navigationTitle(service.name) - .alert("Edit Listen Port", isPresented: $isShowEditRelayPortAlert) { - TextField("Port", text: $newRelayPort) -#if os(iOS) - .keyboardType(.numberPad) -#endif - Button("OK") { - updateImplementation(newRelayPort: newRelayPort) - newRelayPort = "" - } - Button("Cancel", role: .cancel) { - newRelayPort = "" - } - } message: { - Text("Enter a new port.") + .onAppear { + fetchInstance() } - .alert("Edit Address", isPresented: $isShowEditDestinationAddressAlert) { - TextField("Address", text: $newDestinationAddress) - .autocorrectionDisabled() -#if os(iOS) - .textInputAutocapitalization(.never) - .keyboardType(.URL) -#endif - Button("OK") { - updateImplementation(newDestinationAddress: newDestinationAddress) - newDestinationAddress = "" - } - Button("Cancel", role: .cancel) { - newDestinationAddress = "" - } - } message: { - Text("Enter a new address.") - } - .alert("Edit Port", isPresented: $isShowEditDestinationPortAlert) { - TextField("Port", text: $newDestinationPort) -#if os(iOS) - .keyboardType(.numberPad) -#endif - Button("OK") { - updateImplementation(newDestinationPort: newDestinationPort) - newDestinationPort = "" - } - Button("Cancel", role: .cancel) { - newDestinationPort = "" + .sheet(isPresented: $isShowEditInstanceSheet) { + if let server, let instance { + EditInstanceView(server: server, instance: instance) { + fetchInstance() + } } - } message: { - Text("Enter a new port.") } .alert("Error", isPresented: $isShowErrorAlert) { Button("OK", role: .cancel) {} } message: { Text(errorMessage) } - .sensoryFeedback(.success, trigger: isSensoryFeedbackTriggered) } else { Image(systemName: "exclamationmark.circle") @@ -183,74 +87,18 @@ struct DirectForwardDetailView: View { } } - private func updateImplementation(newRelayPort: String) { - Task { - let instanceService = InstanceService() - do { - let implementation = implementation - let server = servers.first(where: { $0.id == implementation.serverID })! - let command = implementation.dryModifyTunnelPort(port: newRelayPort) - let updatedInstance = try await instanceService.updateInstance(baseURLString: server.url, apiKey: server.key, id: implementation.instanceID, url: command) - - implementation.command = command - implementation.fullCommand = updatedInstance.config ?? command - } - catch { -#if DEBUG - print("Error Updating Instances: \(error.localizedDescription)") -#endif - errorMessage = error.localizedDescription - isShowErrorAlert = true - } - } - } - - private func updateImplementation(newDestinationAddress: String) { - Task { - let instanceService = InstanceService() - do { - let implementation = implementation - let server = servers.first(where: { $0.id == implementation.serverID })! - let command = implementation.dryModifyDestinationAddress(address: newDestinationAddress) - let updatedInstance = try await instanceService.updateInstance(baseURLString: server.url, apiKey: server.key, id: implementation.instanceID, url: command) - - implementation.command = command - implementation.fullCommand = updatedInstance.config ?? command - } - catch { -#if DEBUG - print("Error Updating Instances: \(error.localizedDescription)") -#endif - errorMessage = error.localizedDescription - isShowErrorAlert = true - } - } - } - - private func updateImplementation(newDestinationPort: String) { + private func fetchInstance() { + guard let server else { return } Task { let instanceService = InstanceService() do { - let implementation = implementation - let server = servers.first(where: { $0.id == implementation.serverID })! - let command = implementation.dryModifyDestinationPort(port: newDestinationPort) - let updatedInstance = try await instanceService.updateInstance(baseURLString: server.url, apiKey: server.key, id: implementation.instanceID, url: command) - - implementation.command = command - implementation.fullCommand = updatedInstance.config ?? command - -#if os(iOS) - let drop = Drop(title: String(localized: "Success"), subtitle: String(localized: "Changes are now effective"), icon: UIImage(systemName: "checkmark.circle")) - Drops.show(drop) -#endif - isSensoryFeedbackTriggered.toggle() + let instances = try await instanceService.listInstances(baseURLString: server.url, apiKey: server.key) + self.instance = instances.first(where: { $0.id == implementation.instanceID }) } catch { #if DEBUG - print("Error Updating Instances: \(error.localizedDescription)") + print("Error Fetching Instance: \(error.localizedDescription)") #endif - errorMessage = error.localizedDescription - isShowErrorAlert = true } } } diff --git a/NodePass/Service/Detail/NATPassthroughDetailView.swift b/NodePass/Service/Detail/NATPassthroughDetailView.swift index b80e572..87eb015 100644 --- a/NodePass/Service/Detail/NATPassthroughDetailView.swift +++ b/NodePass/Service/Detail/NATPassthroughDetailView.swift @@ -9,11 +9,6 @@ import SwiftUI import SwiftData import Drops -fileprivate enum EditPortOption { - case tunnel - case destination -} - struct NATPassthroughDetailView: View { @Environment(NPState.self) var state @@ -24,67 +19,28 @@ struct NATPassthroughDetailView: View { var server0: Server? { servers.first(where: { $0.id == implementation0.serverID }) } - var addressesAndPorts0: (tunnel: (address: String, port: String), destination: (address: String, port: String)) { - NPCore.parseAddressesAndPorts(urlString: implementation0.command) - } - var queryParameters0: [String: String] { - NPCore.parseQueryParameters(urlString: implementation0.fullCommand) - } var implementation1: Implementation { service.implementations!.first(where: { $0.position == 1 })! } var server1: Server? { servers.first(where: { $0.id == implementation1.serverID }) } - var addressesAndPorts1: (tunnel: (address: String, port: String), destination: (address: String, port: String)) { - NPCore.parseAddressesAndPorts(urlString: implementation1.command) - } @Query private var servers: [Server] - @State private var isShowEditAddressAlert: Bool = false - @State private var newAddress: String = "" - - @State private var isShowEditPortAlert: Bool = false - private var editPortAlertTitle: String { - switch(editPortOption) { - case .tunnel: - return String(localized: "Edit Tunnel Port") - case .destination: - if implementationToEdit == implementation0 { - return String(localized: "Edit Listen Port") - } - if implementationToEdit == implementation1 { - return String(localized: "Edit Service Port") - } - return "" - } - } - @State private var newPort: String = "" - @State private var editPortOption: EditPortOption = .destination - @State private var implementationToEdit: Implementation? + @State private var instance0: Instance? + @State private var instance1: Instance? + @State private var isShowEditInstance0Sheet: Bool = false + @State private var isShowEditInstance1Sheet: Bool = false @State private var isShowErrorAlert: Bool = false @State private var errorMessage: String = "" - @State private var isSensoryFeedbackTriggered: Bool = false - var body: some View { if service.type == .natPassthrough { Form { - if let server = server0 { - let connectionString = "\(server.getHost()):\(addressesAndPorts0.destination.port)" - Section("You Should Connect To") { - Text(connectionString) - } - .copiable(connectionString) - } - Section("Remote Server") { - let implementation = implementation0 let server = server0 - let addressesAndPorts = addressesAndPorts0 - let queryParameters = queryParameters0 if let server { VStack { @@ -95,58 +51,30 @@ struct NATPassthroughDetailView: View { } } .frame(maxWidth: .infinity) - } - else { - LabeledContent("Server") { - Text("Not on this device") - } - } - HStack { - LabeledContent("Listen Port") { - Text(addressesAndPorts.destination.port) - } - if server != nil { - Button { - editPortOption = .destination - implementationToEdit = implementation0 - isShowEditPortAlert = true - } label: { - Image(systemName: "pencil") + + if let instance0 { + VStack { + InstanceCardView(instance: instance0) + .onTapGesture { + isShowEditInstance0Sheet = true + } } + .frame(maxWidth: .infinity) } - } - .copiable(addressesAndPorts.destination.port) - HStack { - LabeledContent("Tunnel Port") { - Text(addressesAndPorts.tunnel.port) - } - if server0 != nil && server1 != nil { - Button { - editPortOption = .tunnel - implementationToEdit = nil - isShowEditPortAlert = true - } label: { - Image(systemName: "pencil") - } + else { + ProgressView() + .frame(maxWidth: .infinity) } } - .copiable(addressesAndPorts.tunnel.port) - if let tlsLevel = queryParameters["tls"] { - LabeledContent("TLS Level") { - Text(NPCore.localizedTLSLevel(tlsLevel: tlsLevel)) + else { + LabeledContent("Server") { + Text("Not on this device") } } - LabeledContent("Command URL") { - Text(implementation.command) - .minimumScaleFactor(0.5) - } - .copiable(implementation.command) } Section("Local Server") { - let implementation = implementation1 let server = server1 - let addressesAndPorts = addressesAndPorts1 if let server { VStack { @@ -157,126 +85,52 @@ struct NATPassthroughDetailView: View { } } .frame(maxWidth: .infinity) - } - else { - LabeledContent("Server") { - Text("Not on this device") - } - } - if addressesAndPorts.destination.address == "127.0.0.1" { - HStack { - LabeledContent("Service Port") { - Text(addressesAndPorts.destination.port) - } - if server != nil { - Button { - editPortOption = .destination - implementationToEdit = implementation1 - isShowEditPortAlert = true - } label: { - Image(systemName: "pencil") - } + + if let instance1 { + VStack { + InstanceCardView(instance: instance1) + .onTapGesture { + isShowEditInstance1Sheet = true + } } - } - .copiable(addressesAndPorts.destination.port) - } - else { - if implementation.isMultipleDestination { - + .frame(maxWidth: .infinity) } else { - HStack { - LabeledContent("Target Address") { - Text(addressesAndPorts.destination.address) - } - if server != nil { - Button { - isShowEditAddressAlert = true - } label: { - Image(systemName: "pencil") - } - } - } - .copiable(addressesAndPorts.destination.address) - HStack { - LabeledContent("Target Port") { - Text(addressesAndPorts.destination.port) - } - if server != nil { - Button { - editPortOption = .destination - implementationToEdit = implementation1 - isShowEditPortAlert = true - } label: { - Image(systemName: "pencil") - } - } - } - .copiable(addressesAndPorts.destination.port) + ProgressView() + .frame(maxWidth: .infinity) } } - HStack { - LabeledContent("Tunnel Port") { - Text(addressesAndPorts.tunnel.port) - } - if server0 != nil && server1 != nil { - Button { - editPortOption = .tunnel - implementationToEdit = nil - isShowEditPortAlert = true - } label: { - Image(systemName: "pencil") - } + else { + LabeledContent("Server") { + Text("Not on this device") } } - .copiable(addressesAndPorts.tunnel.port) - LabeledContent("Command URL") { - Text(implementation.command) - .minimumScaleFactor(0.5) - } - .copiable(implementation.command) } } .formStyle(.grouped) .navigationTitle(service.name) - .alert("Edit Address", isPresented: $isShowEditAddressAlert) { - TextField("Address", text: $newAddress) - .autocorrectionDisabled() -#if os(iOS) - .textInputAutocapitalization(.never) - .keyboardType(.URL) -#endif - Button("OK") { - updateImplementation(newAddress: newAddress) - newAddress = "" - } - Button("Cancel", role: .cancel) { - newAddress = "" - } - } message: { - Text("Enter a new address.") + .onAppear { + fetchInstances() } - .alert(editPortAlertTitle, isPresented: $isShowEditPortAlert) { - TextField("Port", text: $newPort) -#if os(iOS) - .keyboardType(.numberPad) -#endif - Button("OK") { - updateImplementation(implementation: implementationToEdit, editPortOption: editPortOption, newPort: newPort) - newPort = "" + .sheet(isPresented: $isShowEditInstance0Sheet) { + if let server0, let instance0 { + EditInstanceView(server: server0, instance: instance0) { + fetchInstances() + } } - Button("Cancel", role: .cancel) { - newPort = "" + } + .sheet(isPresented: $isShowEditInstance1Sheet) { + if let server1, let instance1 { + EditInstanceView(server: server1, instance: instance1) { + fetchInstances() + } } - } message: { - Text("Enter a new port.") } .alert("Error", isPresented: $isShowErrorAlert) { Button("OK", role: .cancel) {} } message: { Text(errorMessage) } - .sensoryFeedback(.success, trigger: isSensoryFeedbackTriggered) } else { Image(systemName: "exclamationmark.circle") @@ -284,72 +138,33 @@ struct NATPassthroughDetailView: View { } } - private func updateImplementation(newAddress: String) { - Task { - let instanceService = InstanceService() - do { - let implementation = implementation1 - let server = servers.first(where: { $0.id == implementation.serverID })! - let command = implementation.dryModifyDestinationAddress(address: newAddress) - let updatedInstance = try await instanceService.updateInstance(baseURLString: server.url, apiKey: server.key, id: implementation.instanceID, url: command) - - implementation.command = command - implementation.fullCommand = updatedInstance.config ?? command - } - catch { + private func fetchInstances() { + if let server0 { + Task { + let instanceService = InstanceService() + do { + let instances = try await instanceService.listInstances(baseURLString: server0.url, apiKey: server0.key) + self.instance0 = instances.first(where: { $0.id == implementation0.instanceID }) + } + catch { #if DEBUG - print("Error Updating Instances: \(error.localizedDescription)") + print("Error Fetching Instance 0: \(error.localizedDescription)") #endif - errorMessage = error.localizedDescription - isShowErrorAlert = true + } } } - } - - private func updateImplementation(implementation: Implementation?, editPortOption: EditPortOption, newPort: String) { - Task { - let instanceService = InstanceService() - do { - switch(editPortOption) { - case .tunnel: - let implementation0 = implementation0 - let server0 = servers.first(where: { $0.id == implementation0.serverID })! - let command0 = implementation0.dryModifyTunnelPort(port: newPort) - async let updateInstance0: (Instance) = instanceService.updateInstance(baseURLString: server0.url, apiKey: server0.key, id: implementation0.instanceID, url: command0) - - let implementation1 = implementation1 - let server1 = servers.first(where: { $0.id == implementation1.serverID })! - let command1 = implementation1.dryModifyTunnelPort(port: newPort) - async let updateInstance1: (Instance) = instanceService.updateInstance(baseURLString: server1.url, apiKey: server1.key, id: implementation1.instanceID, url: command1) - - let (updatedInstance0, updatedInstance1) = try await (updateInstance0, updateInstance1) - - implementation0.command = command0 - implementation0.fullCommand = updatedInstance0.config ?? command0 - implementation1.command = command1 - implementation1.fullCommand = updatedInstance1.config ?? command1 - case .destination: - let implementation = implementation! - let server = servers.first(where: { $0.id == implementation.serverID })! - let command = implementation.dryModifyDestinationPort(port: newPort) - let updatedInstance = try await instanceService.updateInstance(baseURLString: server.url, apiKey: server.key, id: implementation.instanceID, url: command) - - implementation.command = command - implementation.fullCommand = updatedInstance.config ?? command + if let server1 { + Task { + let instanceService = InstanceService() + do { + let instances = try await instanceService.listInstances(baseURLString: server1.url, apiKey: server1.key) + self.instance1 = instances.first(where: { $0.id == implementation1.instanceID }) } - -#if os(iOS) - let drop = Drop(title: String(localized: "Success"), subtitle: String(localized: "Changes are now effective"), icon: UIImage(systemName: "checkmark.circle")) - Drops.show(drop) -#endif - isSensoryFeedbackTriggered.toggle() - } - catch { + catch { #if DEBUG - print("Error Updating Instances: \(error.localizedDescription)") + print("Error Fetching Instance 1: \(error.localizedDescription)") #endif - errorMessage = error.localizedDescription - isShowErrorAlert = true + } } } } diff --git a/NodePass/Service/Detail/TunnelForwardDetailView.swift b/NodePass/Service/Detail/TunnelForwardDetailView.swift index b5decc3..5576f71 100644 --- a/NodePass/Service/Detail/TunnelForwardDetailView.swift +++ b/NodePass/Service/Detail/TunnelForwardDetailView.swift @@ -9,11 +9,6 @@ import SwiftUI import SwiftData import Drops -fileprivate enum EditPortOption { - case tunnel - case destination -} - struct TunnelForwardDetailView: View { @Environment(NPState.self) var state @@ -24,69 +19,28 @@ struct TunnelForwardDetailView: View { var server0: Server? { servers.first(where: { $0.id == implementation0.serverID }) } - var addressesAndPorts0: (tunnel: (address: String, port: String), destination: (address: String, port: String)) { - NPCore.parseAddressesAndPorts(urlString: implementation0.command) - } - var queryParameters0: [String: String] { - NPCore.parseQueryParameters(urlString: implementation0.fullCommand) - } var implementation1: Implementation { service.implementations!.first(where: { $0.position == 1 })! } var server1: Server? { servers.first(where: { $0.id == implementation1.serverID }) } - var addressesAndPorts1: (tunnel: (address: String, port: String), destination: (address: String, port: String)) { - NPCore.parseAddressesAndPorts(urlString: implementation1.command) - } - var queryParameters1: [String: String] { - NPCore.parseQueryParameters(urlString: implementation1.fullCommand) - } @Query private var servers: [Server] - @State private var isShowEditAddressAlert: Bool = false - @State private var newAddress: String = "" - - @State private var isShowEditPortAlert: Bool = false - private var editPortAlertTitle: String { - switch(editPortOption) { - case .tunnel: - return String(localized: "Edit Tunnel Port") - case .destination: - if implementationToEdit == implementation0 { - return String(localized: "Edit Listen Port") - } - if implementationToEdit == implementation1 { - return String(localized: "Edit Service Port") - } - return "" - } - } - @State private var newPort: String = "" - @State private var editPortOption: EditPortOption = .destination - @State private var implementationToEdit: Implementation? + @State private var instance0: Instance? + @State private var instance1: Instance? + @State private var isShowEditInstance0Sheet: Bool = false + @State private var isShowEditInstance1Sheet: Bool = false @State private var isShowErrorAlert: Bool = false @State private var errorMessage: String = "" - @State private var isSensoryFeedbackTriggered: Bool = false - var body: some View { if service.type == .tunnelForward { Form { - if let server = server0 { - let connectionString = "\(server.getHost()):\(addressesAndPorts0.destination.port)" - Section("You Should Connect To") { - Text(connectionString) - } - .copiable(connectionString) - } - Section("Relay Server") { - let implementation = implementation0 let server = server0 - let addressesAndPorts = addressesAndPorts0 if let server { VStack { @@ -97,54 +51,30 @@ struct TunnelForwardDetailView: View { } } .frame(maxWidth: .infinity) + + if let instance0 { + VStack { + InstanceCardView(instance: instance0) + .onTapGesture { + isShowEditInstance0Sheet = true + } + } + .frame(maxWidth: .infinity) + } + else { + ProgressView() + .frame(maxWidth: .infinity) + } } else { LabeledContent("Server") { Text("Not on this device") } } - HStack { - LabeledContent("Listen Port") { - Text(addressesAndPorts.destination.port) - } - if server != nil { - Button { - editPortOption = .destination - implementationToEdit = implementation0 - isShowEditPortAlert = true - } label: { - Image(systemName: "pencil") - } - } - } - .copiable(addressesAndPorts.destination.port) - HStack { - LabeledContent("Tunnel Port") { - Text(addressesAndPorts.tunnel.port) - } - if server0 != nil && server1 != nil { - Button { - editPortOption = .tunnel - implementationToEdit = nil - isShowEditPortAlert = true - } label: { - Image(systemName: "pencil") - } - } - } - .copiable(addressesAndPorts.tunnel.port) - LabeledContent("Command URL") { - Text(implementation.command) - .minimumScaleFactor(0.5) - } - .copiable(implementation.command) } Section("Destination Server") { - let implementation = implementation1 let server = server1 - let addressesAndPorts = addressesAndPorts1 - let queryParameters = queryParameters1 if let server { VStack { @@ -155,131 +85,52 @@ struct TunnelForwardDetailView: View { } } .frame(maxWidth: .infinity) - } - else { - LabeledContent("Server") { - Text("Not on this device") - } - } - if addressesAndPorts.destination.address == "127.0.0.1" { - HStack { - LabeledContent("Service Port") { - Text(addressesAndPorts.destination.port) - } - if server != nil { - Button { - editPortOption = .destination - implementationToEdit = implementation1 - isShowEditPortAlert = true - } label: { - Image(systemName: "pencil") - } - } - } - .copiable(addressesAndPorts.destination.port) - } - else { - if implementation.isMultipleDestination { - - } - else { - HStack { - LabeledContent("Target Address") { - Text(addressesAndPorts.destination.address) - } - if server != nil { - Button { - isShowEditAddressAlert = true - } label: { - Image(systemName: "pencil") - } - } - } - .copiable(addressesAndPorts.destination.address) - HStack { - LabeledContent("Target Port") { - Text(addressesAndPorts.destination.port) - } - if server != nil { - Button { - editPortOption = .destination - implementationToEdit = implementation1 - isShowEditPortAlert = true - } label: { - Image(systemName: "pencil") + + if let instance1 { + VStack { + InstanceCardView(instance: instance1) + .onTapGesture { + isShowEditInstance1Sheet = true } - } } - .copiable(addressesAndPorts.destination.port) - } - } - HStack { - LabeledContent("Tunnel Port") { - Text(addressesAndPorts.tunnel.port) + .frame(maxWidth: .infinity) } - if server0 != nil && server1 != nil { - Button { - editPortOption = .tunnel - implementationToEdit = nil - isShowEditPortAlert = true - } label: { - Image(systemName: "pencil") - } + else { + ProgressView() + .frame(maxWidth: .infinity) } } - .copiable(addressesAndPorts.tunnel.port) - if let tlsLevel = queryParameters["tls"] { - LabeledContent("TLS Level") { - Text(NPCore.localizedTLSLevel(tlsLevel: tlsLevel)) + else { + LabeledContent("Server") { + Text("Not on this device") } } - LabeledContent("Command URL") { - Text(implementation.command) - .minimumScaleFactor(0.5) - } - .copiable(implementation.command) } } .formStyle(.grouped) .navigationTitle(service.name) - .alert("Edit Address", isPresented: $isShowEditAddressAlert) { - TextField("Address", text: $newAddress) - .autocorrectionDisabled() -#if os(iOS) - .textInputAutocapitalization(.never) - .keyboardType(.URL) -#endif - Button("OK") { - updateImplementation(newAddress: newAddress) - newAddress = "" - } - Button("Cancel", role: .cancel) { - newAddress = "" - } - } message: { - Text("Enter a new address.") + .onAppear { + fetchInstances() } - .alert(editPortAlertTitle, isPresented: $isShowEditPortAlert) { - TextField("Port", text: $newPort) -#if os(iOS) - .keyboardType(.numberPad) -#endif - Button("OK") { - updateImplementation(implementation: implementationToEdit, editPortOption: editPortOption, newPort: newPort) - newPort = "" + .sheet(isPresented: $isShowEditInstance0Sheet) { + if let server0, let instance0 { + EditInstanceView(server: server0, instance: instance0) { + fetchInstances() + } } - Button("Cancel", role: .cancel) { - newPort = "" + } + .sheet(isPresented: $isShowEditInstance1Sheet) { + if let server1, let instance1 { + EditInstanceView(server: server1, instance: instance1) { + fetchInstances() + } } - } message: { - Text("Enter a new port.") } .alert("Error", isPresented: $isShowErrorAlert) { Button("OK", role: .cancel) {} } message: { Text(errorMessage) } - .sensoryFeedback(.success, trigger: isSensoryFeedbackTriggered) } else { Image(systemName: "exclamationmark.circle") @@ -287,72 +138,33 @@ struct TunnelForwardDetailView: View { } } - private func updateImplementation(newAddress: String) { - Task { - let instanceService = InstanceService() - do { - let implementation = implementation1 - let server = servers.first(where: { $0.id == implementation.serverID })! - let command = implementation.dryModifyDestinationAddress(address: newAddress) - let updatedInstance = try await instanceService.updateInstance(baseURLString: server.url, apiKey: server.key, id: implementation.instanceID, url: command) - - implementation.command = command - implementation.fullCommand = updatedInstance.config ?? command - } - catch { + private func fetchInstances() { + if let server0 { + Task { + let instanceService = InstanceService() + do { + let instances = try await instanceService.listInstances(baseURLString: server0.url, apiKey: server0.key) + self.instance0 = instances.first(where: { $0.id == implementation0.instanceID }) + } + catch { #if DEBUG - print("Error Updating Instances: \(error.localizedDescription)") + print("Error Fetching Instance 0: \(error.localizedDescription)") #endif - errorMessage = error.localizedDescription - isShowErrorAlert = true + } } } - } - - private func updateImplementation(implementation: Implementation?, editPortOption: EditPortOption, newPort: String) { - Task { - let instanceService = InstanceService() - do { - switch(editPortOption) { - case .tunnel: - let implementation0 = implementation0 - let server0 = servers.first(where: { $0.id == implementation0.serverID })! - let command0 = implementation0.dryModifyTunnelPort(port: newPort) - async let updateInstance0: (Instance) = instanceService.updateInstance(baseURLString: server0.url, apiKey: server0.key, id: implementation0.instanceID, url: command0) - - let implementation1 = implementation1 - let server1 = servers.first(where: { $0.id == implementation1.serverID })! - let command1 = implementation1.dryModifyTunnelPort(port: newPort) - async let updateInstance1: (Instance) = instanceService.updateInstance(baseURLString: server1.url, apiKey: server1.key, id: implementation1.instanceID, url: command1) - - let (updatedInstance0, updatedInstance1) = try await (updateInstance0, updateInstance1) - - implementation0.command = command0 - implementation0.fullCommand = updatedInstance0.config ?? command0 - implementation1.command = command1 - implementation1.fullCommand = updatedInstance1.config ?? command1 - case .destination: - let implementation = implementation! - let server = servers.first(where: { $0.id == implementation.serverID })! - let command = implementation.dryModifyDestinationPort(port: newPort) - let updatedInstance = try await instanceService.updateInstance(baseURLString: server.url, apiKey: server.key, id: implementation.instanceID, url: command) - - implementation.command = command - implementation.fullCommand = updatedInstance.config ?? command + if let server1 { + Task { + let instanceService = InstanceService() + do { + let instances = try await instanceService.listInstances(baseURLString: server1.url, apiKey: server1.key) + self.instance1 = instances.first(where: { $0.id == implementation1.instanceID }) } - -#if os(iOS) - let drop = Drop(title: String(localized: "Success"), subtitle: String(localized: "Changes are now effective"), icon: UIImage(systemName: "checkmark.circle")) - Drops.show(drop) -#endif - isSensoryFeedbackTriggered.toggle() - } - catch { + catch { #if DEBUG - print("Error Updating Instances: \(error.localizedDescription)") + print("Error Fetching Instance 1: \(error.localizedDescription)") #endif - errorMessage = error.localizedDescription - isShowErrorAlert = true + } } } } diff --git a/NodePass/Service/ServiceListView.swift b/NodePass/Service/ServiceListView.swift index 63c5e49..42b6dd6 100644 --- a/NodePass/Service/ServiceListView.swift +++ b/NodePass/Service/ServiceListView.swift @@ -513,28 +513,45 @@ struct ServiceListView: View { if let serviceId = instance.metadata?.peer.serviceId, serviceId != "", !examinedServiceIds.contains(serviceId) { let serverId0 = serverId let instance0 = instance - if ["0", "5"].contains(instance0.metadata!.peer.serviceType) && !services.map({ $0.id.uuidString }).contains(serviceId) { - // Direct Forward + if ["0", "5"].contains(instance0.metadata!.peer.serviceType) { + // Direct Forward - must be client let clientId = serverId0 let clientInstance = instance0 - let service = Service( - id: UUID(uuidString: serviceId) ?? UUID(), - name: instance.metadata?.peer.alias ?? String(localized: "Untitled"), - type: .directForward, - implementations: [ - Implementation( - name: clientInstance.metadata!.peer.alias, - type: .directForwardClient, - position: 0, - serverID: clientId, - instanceID: clientInstance.id, - command: clientInstance.url, - fullCommand: clientInstance.config ?? clientInstance.url - ) - ] - ) - context.insert(service) - try? context.save() + let clientScheme = NPCore.parseScheme(urlString: clientInstance.url) + let isValidConfiguration = clientScheme == .client + + if let existingService = services.first(where: { $0.id.uuidString == serviceId }) { + // Update existing service + existingService.name = instance.metadata?.peer.alias ?? String(localized: "Untitled") + existingService.isConfigurationInvalid = !isValidConfiguration + if let implementation = existingService.implementations?.first { + implementation.name = clientInstance.metadata!.peer.alias + implementation.command = clientInstance.url + implementation.fullCommand = clientInstance.config ?? clientInstance.url + } + try? context.save() + } else { + // Create new service + let service = Service( + id: UUID(uuidString: serviceId) ?? UUID(), + name: instance.metadata?.peer.alias ?? String(localized: "Untitled"), + type: .directForward, + implementations: [ + Implementation( + name: clientInstance.metadata!.peer.alias, + type: .directForwardClient, + position: 0, + serverID: clientId, + instanceID: clientInstance.id, + command: clientInstance.url, + fullCommand: clientInstance.config ?? clientInstance.url + ) + ] + ) + service.isConfigurationInvalid = !isValidConfiguration + context.insert(service) + try? context.save() + } examinedServiceIds.append(serverId) continue @@ -542,34 +559,39 @@ struct ServiceListView: View { for serverId in store.keys { for instance in store[serverId]! { if instance.metadata?.peer.serviceId == serviceId && instance.id != instance0.id { - if !services.map({ $0.id.uuidString }).contains(serviceId) { - let serverId1 = serverId - let instance1 = instance + let serverId1 = serverId + let instance1 = instance + + switch(instance.metadata!.peer.serviceType) { + case "1", "3", "6": + // NAT Passthrough - must be one server and one client + let schemeOfInstance0 = NPCore.parseScheme(urlString: instance0.url) + let schemeOfInstance1 = NPCore.parseScheme(urlString: instance1.url) + let isValidConfiguration = (schemeOfInstance0 == .server && schemeOfInstance1 == .client) || (schemeOfInstance1 == .server && schemeOfInstance0 == .client) - switch(instance.metadata!.peer.serviceType) { - case "1", "3", "6": - // NAT Passthrough - let schemeOfInstance0 = NPCore.parseScheme(urlString: instance0.url) - let schemeOfInstance1 = NPCore.parseScheme(urlString: instance1.url) - let serverId: String - let clientId: String - let serverInstance: Instance - let clientInstance: Instance - if schemeOfInstance0 == .server && schemeOfInstance1 == .client { - serverId = serverId0 - serverInstance = instance0 - clientId = serverId1 - clientInstance = instance1 - } - else if schemeOfInstance1 == .server && schemeOfInstance0 == .client { - serverId = serverId1 - serverInstance = instance1 - clientId = serverId0 - clientInstance = instance0 - } - else { - continue + if let existingService = services.first(where: { $0.id.uuidString == serviceId }) { + // Update existing service + existingService.name = instance.metadata?.peer.alias ?? String(localized: "Untitled") + existingService.isConfigurationInvalid = !isValidConfiguration + if let implementations = existingService.implementations { + // Update by instanceID + if let impl0 = implementations.first(where: { $0.instanceID == instance0.id }) { + impl0.name = instance0.metadata!.peer.alias + impl0.command = instance0.url + impl0.fullCommand = instance0.config ?? instance0.url + } + if let impl1 = implementations.first(where: { $0.instanceID == instance1.id }) { + impl1.name = instance1.metadata!.peer.alias + impl1.command = instance1.url + impl1.fullCommand = instance1.config ?? instance1.url + } } + try? context.save() + } else { + // Create new service - determine positions based on scheme + let (serverInstance, clientInstance) = schemeOfInstance0 == .server ? (instance0, instance1) : (instance1, instance0) + let (serverId, clientId) = schemeOfInstance0 == .server ? (serverId0, serverId1) : (serverId1, serverId0) + let service = Service( id: UUID(uuidString: serviceId) ?? UUID(), name: instance.metadata?.peer.alias ?? String(localized: "Untitled"), @@ -595,72 +617,77 @@ struct ServiceListView: View { ) ] ) + service.isConfigurationInvalid = !isValidConfiguration context.insert(service) try? context.save() - - examinedServiceIds.append(serverId) - continue - case "2", "4", "7": - // Tunnel Forward - let schemeOfInstance0 = NPCore.parseScheme(urlString: instance0.url) - let modeOfInstance0 = NPCore.parseQueryParameters(urlString: instance0.url)["mode"] - let schemeOfInstance1 = NPCore.parseScheme(urlString: instance1.url) - let modeOfInstance1 = NPCore.parseQueryParameters(urlString: instance1.url)["mode"] - guard let modeOfInstance0, let modeOfInstance1 else { - continue - } - let relayServerId: String - let destinationServerId: String - let relayServerInstance: Instance - let destinationServerInstance: Instance - if (schemeOfInstance0 == .server && modeOfInstance0 == "1") || (schemeOfInstance1 == .server && modeOfInstance1 == "2") { - relayServerId = serverId0 - relayServerInstance = instance0 - destinationServerId = serverId1 - destinationServerInstance = instance1 - } - else if (schemeOfInstance1 == .server && modeOfInstance1 == "1") || (schemeOfInstance0 == .server && modeOfInstance0 == "2") { - relayServerId = serverId1 - relayServerInstance = instance1 - destinationServerId = serverId0 - destinationServerInstance = instance0 - } - else { - continue + } + + examinedServiceIds.append(serverId) + continue + case "2", "4", "7": + // Tunnel Forward - must be one server and one client + let schemeOfInstance0 = NPCore.parseScheme(urlString: instance0.url) + let schemeOfInstance1 = NPCore.parseScheme(urlString: instance1.url) + let isValidConfiguration = (schemeOfInstance0 == .server && schemeOfInstance1 == .client) || (schemeOfInstance1 == .server && schemeOfInstance0 == .client) + + if let existingService = services.first(where: { $0.id.uuidString == serviceId }) { + // Update existing service + existingService.name = instance.metadata?.peer.alias ?? String(localized: "Untitled") + existingService.isConfigurationInvalid = !isValidConfiguration + if let implementations = existingService.implementations { + // Update by instanceID + if let impl0 = implementations.first(where: { $0.instanceID == instance0.id }) { + impl0.name = instance0.metadata!.peer.alias + impl0.command = instance0.url + impl0.fullCommand = instance0.config ?? instance0.url + } + if let impl1 = implementations.first(where: { $0.instanceID == instance1.id }) { + impl1.name = instance1.metadata!.peer.alias + impl1.command = instance1.url + impl1.fullCommand = instance1.config ?? instance1.url + } } + try? context.save() + } else { + // Create new service - use mode to determine relay vs destination + let modeOfInstance0 = NPCore.parseQueryParameters(urlString: instance0.url)["mode"] + let (relayInstance, destInstance) = modeOfInstance0 == "1" ? (instance0, instance1) : (instance1, instance0) + let (relayServerId, destServerId) = modeOfInstance0 == "1" ? (serverId0, serverId1) : (serverId1, serverId0) + let service = Service( id: UUID(uuidString: serviceId) ?? UUID(), name: instance.metadata?.peer.alias ?? String(localized: "Untitled"), type: .tunnelForward, implementations: [ Implementation( - name: relayServerInstance.metadata!.peer.alias, + name: relayInstance.metadata!.peer.alias, type: .tunnelForwardRelay, position: 0, serverID: relayServerId, - instanceID: relayServerInstance.id, - command: relayServerInstance.url, - fullCommand: relayServerInstance.config ?? relayServerInstance.url + instanceID: relayInstance.id, + command: relayInstance.url, + fullCommand: relayInstance.config ?? relayInstance.url ), Implementation( - name: destinationServerInstance.metadata!.peer.alias, + name: destInstance.metadata!.peer.alias, type: .tunnelForwardDestination, position: 1, - serverID: destinationServerId, - instanceID: destinationServerInstance.id, - command: destinationServerInstance.url, - fullCommand: destinationServerInstance.config ?? destinationServerInstance.url + serverID: destServerId, + instanceID: destInstance.id, + command: destInstance.url, + fullCommand: destInstance.config ?? destInstance.url ) ] ) + service.isConfigurationInvalid = !isValidConfiguration context.insert(service) try? context.save() - - examinedServiceIds.append(serverId) - continue - default: - continue } + + examinedServiceIds.append(serverId) + continue + default: + continue } } } @@ -668,6 +695,28 @@ struct ServiceListView: View { } } } + + // Delete invalid local services + let allRemoteServiceIds = Set( + store.values + .flatMap { $0 } + .compactMap { $0.metadata?.peer.serviceId } + .filter { !$0.isEmpty } + ) + + let localServicesToDelete = services.filter { service in + let serviceIdString = service.id.uuidString + return !allRemoteServiceIds.contains(serviceIdString) + } + + for service in localServicesToDelete { + context.delete(service) + } + + if !localServicesToDelete.isEmpty { + try? context.save() + } + if errorStore.count == 0 { isSensoryFeedbackTriggered.toggle() } diff --git a/Shared/Handlers/InstanceService.swift b/Shared/Handlers/InstanceService.swift index c31d6c2..667b95e 100644 --- a/Shared/Handlers/InstanceService.swift +++ b/Shared/Handlers/InstanceService.swift @@ -9,18 +9,20 @@ import Foundation enum InstanceAPI: APIEndpoint { case listInstances(baseURLString: String, apiKey: String) - case createInstance(baseURLString: String, apiKey: String, url: String) + case createInstance(baseURLString: String, apiKey: String, url: String, alias: String?) case deleteInstance(baseURLString: String, apiKey: String, id: String) case updateInstance(baseURLString: String, apiKey: String, id: String, url: String) + case updateInstanceAlias(baseURLString: String, apiKey: String, id: String, alias: String?) case updateInstanceStatus(baseURLString: String, apiKey: String, id: String, action: String) case updateInstancePeer(baseURLString: String, apiKey: String, id: String, serviceAlias: String, serviceId: String, serviceType: String) var baseURL: URL { switch self { case .listInstances(let baseURLString, _): return URL(string: baseURLString)! - case .createInstance(let baseURLString, _, _): return URL(string: baseURLString)! + case .createInstance(let baseURLString, _, _, _): return URL(string: baseURLString)! case .deleteInstance(let baseURLString, _, _): return URL(string: baseURLString)! case .updateInstance(let baseURLString, _, _, _): return URL(string: baseURLString)! + case .updateInstanceAlias(let baseURLString, _, _, _): return URL(string: baseURLString)! case .updateInstanceStatus(let baseURLString, _, _, _): return URL(string: baseURLString)! case .updateInstancePeer(let baseURLString, _, _, _, _, _): return URL(string: baseURLString)! } @@ -32,6 +34,7 @@ enum InstanceAPI: APIEndpoint { case .createInstance: return "/instances" case .deleteInstance(_, _, let id): return "/instances/\(id)" case .updateInstance(_, _, let id, _): return "/instances/\(id)" + case .updateInstanceAlias(_, _, let id, _): return "/instances/\(id)" case .updateInstanceStatus(_, _, let id, _): return "/instances/\(id)" case .updateInstancePeer(_, _, let id, _, _, _): return "/instances/\(id)" } @@ -43,6 +46,7 @@ enum InstanceAPI: APIEndpoint { case .createInstance: return .post case .deleteInstance: return .delete case .updateInstance: return .put + case .updateInstanceAlias: return .patch case .updateInstanceStatus: return .patch case .updateInstancePeer: return .patch } @@ -51,9 +55,10 @@ enum InstanceAPI: APIEndpoint { var headers: [String: String]? { switch self { case .listInstances(_, let apiKey): return ["X-API-Key": apiKey] - case .createInstance(_, let apiKey, _): return ["X-API-Key": apiKey] + case .createInstance(_, let apiKey, _, _): return ["X-API-Key": apiKey] case .deleteInstance(_, let apiKey, _): return ["X-API-Key": apiKey] case .updateInstance(_, let apiKey, _, _): return ["X-API-Key": apiKey] + case .updateInstanceAlias(_, let apiKey, _, _): return ["X-API-Key": apiKey] case .updateInstanceStatus(_, let apiKey, _, _): return ["X-API-Key": apiKey] case .updateInstancePeer(_, let apiKey, _, _, _, _): return ["X-API-Key": apiKey] } @@ -65,6 +70,7 @@ enum InstanceAPI: APIEndpoint { case .createInstance: return nil case .deleteInstance: return nil case .updateInstance: return nil + case .updateInstanceAlias: return nil case .updateInstanceStatus: return nil case .updateInstancePeer: return nil } @@ -73,9 +79,20 @@ enum InstanceAPI: APIEndpoint { var parameters: [String: Any]? { switch self { case .listInstances: return nil - case .createInstance(_, _, let url): return ["url": url] + case .createInstance(_, _, let url, let alias): + var params: [String: Any] = ["url": url] + if let alias = alias { + params["alias"] = alias + } + return params case .deleteInstance: return nil case .updateInstance(_, _, _, let url): return ["url": url] + case .updateInstanceAlias(_, _, _, let alias): + var params: [String: Any] = [:] + if let alias = alias { + params["alias"] = alias + } + return params case .updateInstanceStatus(_, _, _, let action): return ["action": action] case .updateInstancePeer(_, _, _, let serviceAlias, let serviceId, let serviceType): return [ "meta": [ @@ -107,8 +124,8 @@ class InstanceService { } } - func createInstance(baseURLString: String, apiKey: String, url: String) async throws -> Instance { - let endpoint = InstanceAPI.createInstance(baseURLString: baseURLString, apiKey: apiKey, url: url) + func createInstance(baseURLString: String, apiKey: String, url: String, alias: String? = nil) async throws -> Instance { + let endpoint = InstanceAPI.createInstance(baseURLString: baseURLString, apiKey: apiKey, url: url, alias: alias) return try await withCheckedThrowingContinuation { continuation in networkService.request(endpoint, expecting: Instance.self) { result in @@ -137,6 +154,16 @@ class InstanceService { } } + func updateInstanceAlias(baseURLString: String, apiKey: String, id: String, alias: String?) async throws { + let endpoint = InstanceAPI.updateInstanceAlias(baseURLString: baseURLString, apiKey: apiKey, id: id, alias: alias) + + return try await withCheckedThrowingContinuation { continuation in + networkService.request(endpoint) { result in + continuation.resume(with: result) + } + } + } + func updateInstanceStatus(baseURLString: String, apiKey: String, id: String, action: String) async throws { let endpoint = InstanceAPI.updateInstanceStatus(baseURLString: baseURLString, apiKey: apiKey, id: id, action: action) diff --git a/Shared/Localizable.xcstrings b/Shared/Localizable.xcstrings index c3adbbc..305b798 100644 --- a/Shared/Localizable.xcstrings +++ b/Shared/Localizable.xcstrings @@ -116,7 +116,7 @@ "Add" : { }, - "Add custom URL query parameters not covered above" : { + "Add custom URL query parameters not covered above." : { }, "Add Direct Forward" : { @@ -157,6 +157,9 @@ }, "Advanced Mode" : { + }, + "Advanced Settings" : { + }, "An error occurred" : { @@ -167,7 +170,7 @@ "Automatic" : { }, - "Block certain traffic from being tunneled" : { + "Block certain traffic from being tunneled." : { }, "Block HTTP" : { @@ -185,19 +188,19 @@ "Certificate Path" : { }, - "Changes are now effective" : { + "Configuration Invalid" : { }, - "Command URL" : { + "Configure advanced settings and tuning parameters." : { }, - "Configure connection pool behavior and limits" : { + "Configure connection pool behavior and limits." : { }, - "Configure instance using form fields" : { + "Configure instance using form fields." : { }, - "Configure multiple target addresses for load balancing" : { + "Configure multiple target addresses for load balancing." : { }, "Configure Server Detail Widget" : { @@ -206,7 +209,7 @@ "Configure Widget" : { }, - "Connect to tunnel address and forward from/to target" : { + "Connect to tunnel address and forward from/to target." : { }, "Connection Mode" : { @@ -218,14 +221,11 @@ "Connection Type" : { }, - "Control protocol availability and PROXY protocol v1 support" : { + "Control protocol availability and PROXY v1 support." : { }, "Copy" : { - }, - "Copy URL" : { - }, "CPU" : { @@ -263,7 +263,7 @@ "Dial Address" : { }, - "Dial: Specific source IP or 'auto' by OS" : { + "Dial: Specific source IP or 'auto' by OS." : { }, "Direct Forward" : { @@ -278,7 +278,7 @@ "DNS Cache Duration" : { }, - "DNS: Cache TTL duration in '30s, 5m, 1h'" : { + "DNS: Cache TTL duration in '30s, 5m, 1h'." : { }, "Domain" : { @@ -289,41 +289,23 @@ }, "Edit" : { - }, - "Edit Address" : { - }, "Edit Instance" : { - }, - "Edit Listen Port" : { - - }, - "Edit Port" : { - }, "Edit Server" : { - }, - "Edit Service Port" : { - - }, - "Edit Tunnel Port" : { - }, "Enable PROXY Protocol" : { - }, - "Enter a new address." : { - }, "Enter a new name for the service." : { }, - "Enter a new port." : { + "Enter a new name for the instance." : { }, - "Enter instance URL directly" : { + "Enter instance URL directly." : { }, "Error" : { @@ -396,7 +378,7 @@ "Light" : { }, - "Listen on tunnel address and forward to/from target" : { + "Listen on tunnel address and forward to/from target." : { }, "Listen Port" : { @@ -423,7 +405,7 @@ "Log Level" : { }, - "Logging" : { + "Logging Level" : { }, "Matrix Unavailable" : { @@ -488,6 +470,9 @@ }, "OK" : { + }, + "Optional" : { + }, "Oldest to Newest" : { @@ -516,13 +501,13 @@ "Rate Limit (Mbps)" : { }, - "Rate: Bandwidth limit or 0 for unlimited" : { + "Rate: Bandwidth limit or 0 for unlimited." : { }, "Read Timeout" : { }, - "Read: Timeout duration or 0 to disable" : { + "Read: Timeout duration or 0 to disable." : { }, "Refresh Widget" : { @@ -545,6 +530,9 @@ }, "Rename" : { + }, + "Rename Instance" : { + }, "Rename Service" : { @@ -573,7 +561,7 @@ "Server" : { }, - "Server address to connect or Client address to bind" : { + "Server address to connect or Client address to bind." : { }, "Server Details" : { @@ -606,9 +594,6 @@ }, "Services" : { - }, - "Set logging verbosity level" : { - }, "Settings" : { @@ -616,16 +601,16 @@ "Share" : { }, - "Single target address to connect or to bind" : { + "Single target address to connect or to bind." : { }, - "Slot: Max concurrent connections allowed" : { + "Slot: Max concurrent connections allowed." : { }, "SNI Hostname" : { }, - "SNI hostname for TLS connections" : { + "SNI hostname for TLS connections." : { }, "Sort" : { @@ -636,9 +621,6 @@ }, "Stop" : { - }, - "Success" : { - }, "Support Us" : { @@ -731,10 +713,7 @@ "TLS" : { }, - "TLS encryption settings" : { - - }, - "TLS Level" : { + "TLS encryption settings." : { }, "TLS Mode" : { @@ -796,7 +775,7 @@ "Tunnel %@ Address" : { }, - "Tunnel address to bind, empty IP for all interfaces" : { + "Tunnel address to bind, empty IP for all interfaces." : { }, "Tunnel Forward" : { @@ -855,9 +834,6 @@ }, "You are about to force delete this service. This action is irreversible and any error will be ignored. Are you sure?" : { - }, - "You Should Connect To" : { - } }, "version" : "1.1" diff --git a/Shared/Models/Implementation.swift b/Shared/Models/Implementation.swift index 55ceafd..2a20ce1 100644 --- a/Shared/Models/Implementation.swift +++ b/Shared/Models/Implementation.swift @@ -37,26 +37,4 @@ class Implementation { self.command = command self.fullCommand = fullCommand } - - func dryModifyTunnelAddress(address: String, isReturnFullCommand: Bool = false) -> String { - let addressesAndPorts = NPCore.parseAddressesAndPorts(urlString: command) - return NPCore.extractSchemePrefix(urlString: command) + address + ":" + addressesAndPorts.tunnel.port + "/" + addressesAndPorts.destination.address + ":" + addressesAndPorts.destination.port + "?" + NPCore.extractQueryParameterString(urlString: command) - } - - func dryModifyTunnelPort(port: String, isReturnFullCommand: Bool = false) -> String { - let addressesAndPorts = NPCore.parseAddressesAndPorts(urlString: command) - return NPCore.extractSchemePrefix(urlString: command) + addressesAndPorts.tunnel.address + ":" + port + "/" + addressesAndPorts.destination.address + ":" + addressesAndPorts.destination.port + "?" + NPCore.extractQueryParameterString(urlString: command) - } - - func dryModifyDestinationAddress(address: String, isReturnFullCommand: Bool = false) -> String { - guard isMultipleDestination == false else { return command } - let addressesAndPorts = NPCore.parseAddressesAndPorts(urlString: command) - return NPCore.extractSchemePrefix(urlString: command) + addressesAndPorts.tunnel.address + ":" + addressesAndPorts.tunnel.port + "/" + address + ":" + addressesAndPorts.destination.port + NPCore.extractQueryParameterString(urlString: isReturnFullCommand ? fullCommand : command, withQuestionMark: true) - } - - func dryModifyDestinationPort(port: String, isReturnFullCommand: Bool = false) -> String { - guard isMultipleDestination == false else { return command } - let addressesAndPorts = NPCore.parseAddressesAndPorts(urlString: command) - return NPCore.extractSchemePrefix(urlString: command) + addressesAndPorts.tunnel.address + ":" + addressesAndPorts.tunnel.port + "/" + addressesAndPorts.destination.address + ":" + port + NPCore.extractQueryParameterString(urlString: isReturnFullCommand ? fullCommand : command, withQuestionMark: true) - } } diff --git a/Shared/Models/Instance.swift b/Shared/Models/Instance.swift index 92fa645..a35f680 100644 --- a/Shared/Models/Instance.swift +++ b/Shared/Models/Instance.swift @@ -13,6 +13,7 @@ struct Instance: Identifiable, Codable, Equatable { let status: Status let url: String let config: String? + let alias: String? let tcp: Int? let udp: Int? let tcpReceive: Int64 @@ -29,6 +30,7 @@ struct Instance: Identifiable, Codable, Equatable { case status case url case config + case alias case tcp = "tcps" case udp = "udps" case tcpReceive = "tcprx" diff --git a/Shared/Models/Service.swift b/Shared/Models/Service.swift index 09364c4..ec59f9a 100644 --- a/Shared/Models/Service.swift +++ b/Shared/Models/Service.swift @@ -15,6 +15,7 @@ class Service { var name: String = "" var type: ServiceType = ServiceType.directForward var implementations: [Implementation]? + var isConfigurationInvalid: Bool = false init(id: UUID, name: String, type: ServiceType, implementations: [Implementation]) { self.id = id @@ -22,6 +23,7 @@ class Service { self.name = name self.type = type self.implementations = implementations + self.isConfigurationInvalid = false } init(name: String, type: ServiceType, implementations: [Implementation]) { @@ -30,5 +32,6 @@ class Service { self.name = name self.type = type self.implementations = implementations + self.isConfigurationInvalid = false } }