Files
evgeniyChepelev 0327624b30 Multi-profiles (#80)
* fix layout on ipad

* Create multi profiles

* multi-profile server config, connection cache, and auth fixes

 Split ProfilesView into ProfilesListView, AddProfileSheet, ProfileBadge, AddProfileViewModel
- Add server URL + setup key fields to Add Profile screen
- Store per-profile connection data (ip/fqdn/managementURL) as typed model in ProfileConnectionCache
- Show cached connection info immediately on profile switch; empty if no prior data
- Fix profile deletion persistence via tombstone in profiles.json
- Fix logout to remove both netbird.cfg and state.json (cfg holds auth tokens)
- Preserve managementURL in UI after logout via cache fallback
- Guard polling from overwriting new profile's data during disconnect/reconnect cycle

* fix(multi-profile): reinitialize VPN adapter on profile switch and show current server URL

* Update project.pbxproj

* fix(multi-profile): prevent default server overwrite on logout/re-login

* Update PacketTunnelProvider.swift

* fix(multi-profile): address code review findings

- Normalize only scheme and host to lowercase when saving management
  server URL; previously the full URL was lowercased which could corrupt
  case-sensitive paths on self-hosted servers
- Move switchConnectionInfo(to:) inside the do-block so the connection
  UI is only updated after a successful profile switch
- Add ProfileConnectionCache.remove(for:) and clearConnectionData(for:)
  to prevent stale ip/fqdn/managementURL from persisting after a profile
  is deleted or logged out; call them from removeProfile and logoutProfile
- Use ASWebAuthenticationSession.Callback.customScheme on iOS 17.4+,
  falling back to the deprecated callbackURLScheme initializer on older
  versions; add a comment explaining why "http" is used and that a
  proper fix requires custom-scheme support in the SDK
- Guard presentationAnchor against a missing key window with
  assertionFailure in debug builds instead of silently returning an
  empty UIWindow
2026-04-22 10:06:51 +02:00

172 lines
6.5 KiB
Swift

//
// AddProfileSheet.swift
// NetBird
//
import SwiftUI
#if os(iOS)
struct AddProfileSheet: View {
@Environment(\.dismiss) private var dismiss
@StateObject private var addVM = AddProfileViewModel()
@State private var profileName = ""
@State private var managementServerUrl = ""
@State private var setupKey = ""
@State private var showSetupKeyField = false
@State private var showNameValidationAlert = false
var onCreated: (() -> Void)?
private let defaultManagementServerUrl = "https://api.netbird.io"
private var isNameValid: Bool {
!profileName.isEmpty && profileName.range(of: "^[a-zA-Z0-9_-]+$", options: .regularExpression) != nil
}
var body: some View {
NavigationView {
List {
// Profile name
Section {
TextField("Profile name", text: $profileName)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.disabled(addVM.isLoading)
} header: {
Text("Profile")
} footer: {
if let error = addVM.profileError {
Text(error)
.foregroundColor(.red)
} else {
Text("Only letters, numbers, underscores and hyphens allowed")
.foregroundColor(Color("TextSecondary"))
}
}
// Server URL
Section(header: Text("Server")) {
TextField("https://api.netbird.io", text: $managementServerUrl)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.keyboardType(.URL)
.disabled(addVM.isLoading)
.onChange(of: managementServerUrl) { _ in
addVM.urlError = nil
addVM.generalError = nil
addVM.ssoNotSupportedError = nil
}
if let error = addVM.urlError {
Text(error).foregroundColor(.red).font(.footnote)
}
if let error = addVM.generalError {
Text(error).foregroundColor(.red).font(.footnote)
}
}
// Setup key (optional)
Section {
DisclosureGroup("Add this device with a setup key", isExpanded: $showSetupKeyField) {
TextField("0EF79C2F-DEE1-419B-BFC8-1BF529332998", text: $setupKey)
.disableAutocorrection(true)
.autocapitalization(.allCharacters)
.disabled(addVM.isLoading)
.onChange(of: setupKey) { _ in
addVM.setupKeyError = nil
}
if let error = addVM.setupKeyError {
Text(error).foregroundColor(.red).font(.footnote)
}
if let error = addVM.ssoNotSupportedError {
Text(error).foregroundColor(.red).font(.footnote)
}
Text("Using setup keys for user devices is not recommended. SSO with MFA provides stronger security, proper user-device association, and periodic re-authentication.")
.font(.footnote)
.foregroundColor(.accentColor)
.padding(.vertical, 4)
}
}
.onChange(of: showSetupKeyField) { expanded in
if !expanded {
setupKey = ""
addVM.setupKeyError = nil
}
}
// Use NetBird server shortcut
Section {
Button {
managementServerUrl = defaultManagementServerUrl
} label: {
HStack {
Spacer()
Image("icon-netbird-button")
.resizable()
.scaledToFit()
.frame(width: 22, height: 22)
Text("Use NetBird server")
Spacer()
}
}
.disabled(addVM.isLoading)
}
}
.listStyle(.insetGrouped)
.navigationTitle("New Profile")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
.foregroundColor(.accentColor)
.disabled(addVM.isLoading)
}
ToolbarItem(placement: .navigationBarTrailing) {
if addVM.isLoading {
ProgressView()
} else {
Button("Create") {
guard isNameValid else {
showNameValidationAlert = true
return
}
addVM.create(
name: profileName,
serverUrl: managementServerUrl,
setupKey: setupKey
)
}
.foregroundColor(.accentColor)
}
}
}
.alert("Invalid Profile Name", isPresented: $showNameValidationAlert) {
Button("OK", role: .cancel) {}
} message: {
Text("Only letters, numbers, underscores and hyphens are allowed.")
}
.onChange(of: addVM.ssoNotSupportedError) { error in
if error != nil {
showSetupKeyField = true
}
}
.onChange(of: addVM.isSuccess) { success in
if success {
onCreated?()
dismiss()
}
}
}
.navigationViewStyle(.stack)
}
}
#Preview {
AddProfileSheet()
}
#endif