Extend authentication methods (#259)
* Add profile authentication method - Persistent (default, fallback) - Interactive (may expire through reconnections) - TOTP (seed-based) - currently disabled * Disable on-demand if login is interactive * Present interactive prompt on VPN toggle
This commit is contained in:
parent
44ccd21536
commit
2e10aab039
|
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Prompt for password interactively. [#3](https://github.com/passepartoutvpn/passepartout-apple/issues/3)
|
||||||
- Ukranian translations (Dmitry Chirkin). [#243](https://github.com/passepartoutvpn/passepartout-apple/pull/243)
|
- Ukranian translations (Dmitry Chirkin). [#243](https://github.com/passepartoutvpn/passepartout-apple/pull/243)
|
||||||
- OpenVPN: Full implementation of Tunnelblick XOR patch (tmthecoder). [#245](https://github.com/passepartoutvpn/passepartout-apple/pull/245), [tunnelkit#255][https://github.com/passepartoutvpn/tunnelkit/pull/255]
|
- OpenVPN: Full implementation of Tunnelblick XOR patch (tmthecoder). [#245](https://github.com/passepartoutvpn/passepartout-apple/pull/245), [tunnelkit#255][https://github.com/passepartoutvpn/tunnelkit/pull/255]
|
||||||
|
|
||||||
|
|
|
@ -146,12 +146,13 @@
|
||||||
0EB34BCC27C6F41D00B126DA /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB34BCB27C6F41D00B126DA /* Theme.swift */; };
|
0EB34BCC27C6F41D00B126DA /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB34BCB27C6F41D00B126DA /* Theme.swift */; };
|
||||||
0EB4042C27CA0E8C00378B1A /* Unlocalized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB4042B27CA0E8B00378B1A /* Unlocalized.swift */; };
|
0EB4042C27CA0E8C00378B1A /* Unlocalized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB4042B27CA0E8B00378B1A /* Unlocalized.swift */; };
|
||||||
0EB4042E27CA136300378B1A /* AddingTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB4042D27CA136200378B1A /* AddingTextField.swift */; };
|
0EB4042E27CA136300378B1A /* AddingTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB4042D27CA136200378B1A /* AddingTextField.swift */; };
|
||||||
|
0EB90CC129C25BBD00E64628 /* InteractiveConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB90CC029C25BBD00E64628 /* InteractiveConnectionView.swift */; };
|
||||||
0EBC074C27EB673C00208AD9 /* ProfileView+Rename.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC074B27EB673C00208AD9 /* ProfileView+Rename.swift */; };
|
0EBC074C27EB673C00208AD9 /* ProfileView+Rename.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC074B27EB673C00208AD9 /* ProfileView+Rename.swift */; };
|
||||||
0EBC075527EBC83800208AD9 /* MailComposerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075427EBC83800208AD9 /* MailComposerView.swift */; };
|
0EBC075527EBC83800208AD9 /* MailComposerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075427EBC83800208AD9 /* MailComposerView.swift */; };
|
||||||
0EBC075B27EC4FFF00208AD9 /* ReportIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075A27EC4FFF00208AD9 /* ReportIssueView.swift */; };
|
0EBC075B27EC4FFF00208AD9 /* ReportIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075A27EC4FFF00208AD9 /* ReportIssueView.swift */; };
|
||||||
0EBC075D27EC529000208AD9 /* DebugLog+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075C27EC529000208AD9 /* DebugLog+Constants.swift */; };
|
0EBC075D27EC529000208AD9 /* DebugLog+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075C27EC529000208AD9 /* DebugLog+Constants.swift */; };
|
||||||
0EBC076027EC587900208AD9 /* SwiftGen+Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075F27EC587900208AD9 /* SwiftGen+Strings.swift */; };
|
0EBC076027EC587900208AD9 /* SwiftGen+Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075F27EC587900208AD9 /* SwiftGen+Strings.swift */; };
|
||||||
0EBE880F281B18DE0090D9E6 /* ProfileRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBE880E281B18DE0090D9E6 /* ProfileRow.swift */; };
|
0EBE880F281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBE880E281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift */; };
|
||||||
0ECB78E9285F5DE300B0E460 /* PassepartoutMac.bundle in Embed Plugins */ = {isa = PBXBuildFile; fileRef = 0ECB78DA285F52F700B0E460 /* PassepartoutMac.bundle */; platformFilter = maccatalyst; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
0ECB78E9285F5DE300B0E460 /* PassepartoutMac.bundle in Embed Plugins */ = {isa = PBXBuildFile; fileRef = 0ECB78DA285F52F700B0E460 /* PassepartoutMac.bundle */; platformFilter = maccatalyst; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
0ECB78EC2863A21600B0E460 /* PassepartoutLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0ECB78EB2863A21600B0E460 /* PassepartoutLibrary */; };
|
0ECB78EC2863A21600B0E460 /* PassepartoutLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0ECB78EB2863A21600B0E460 /* PassepartoutLibrary */; };
|
||||||
0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECF71ED27B6A99300CDB528 /* AccountView.swift */; };
|
0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECF71ED27B6A99300CDB528 /* AccountView.swift */; };
|
||||||
|
@ -445,6 +446,7 @@
|
||||||
0EB34BCB27C6F41D00B126DA /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
|
0EB34BCB27C6F41D00B126DA /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
|
||||||
0EB4042B27CA0E8B00378B1A /* Unlocalized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Unlocalized.swift; sourceTree = "<group>"; };
|
0EB4042B27CA0E8B00378B1A /* Unlocalized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Unlocalized.swift; sourceTree = "<group>"; };
|
||||||
0EB4042D27CA136200378B1A /* AddingTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddingTextField.swift; sourceTree = "<group>"; };
|
0EB4042D27CA136200378B1A /* AddingTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddingTextField.swift; sourceTree = "<group>"; };
|
||||||
|
0EB90CC029C25BBD00E64628 /* InteractiveConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveConnectionView.swift; sourceTree = "<group>"; };
|
||||||
0EBC074B27EB673C00208AD9 /* ProfileView+Rename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Rename.swift"; sourceTree = "<group>"; };
|
0EBC074B27EB673C00208AD9 /* ProfileView+Rename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Rename.swift"; sourceTree = "<group>"; };
|
||||||
0EBC075427EBC83800208AD9 /* MailComposerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MailComposerView.swift; sourceTree = "<group>"; };
|
0EBC075427EBC83800208AD9 /* MailComposerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MailComposerView.swift; sourceTree = "<group>"; };
|
||||||
0EBC075A27EC4FFF00208AD9 /* ReportIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportIssueView.swift; sourceTree = "<group>"; };
|
0EBC075A27EC4FFF00208AD9 /* ReportIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportIssueView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -459,7 +461,7 @@
|
||||||
0EBE2FD62360F89500F0D5AB /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
0EBE2FD62360F89500F0D5AB /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
0EBE2FD72360F89600F0D5AB /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
0EBE2FD72360F89600F0D5AB /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
0EBE2FD82360F89600F0D5AB /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
0EBE2FD82360F89600F0D5AB /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
0EBE880E281B18DE0090D9E6 /* ProfileRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRow.swift; sourceTree = "<group>"; };
|
0EBE880E281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+ProfileRow.swift"; sourceTree = "<group>"; };
|
||||||
0ECB78DA285F52F700B0E460 /* PassepartoutMac.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PassepartoutMac.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
|
0ECB78DA285F52F700B0E460 /* PassepartoutMac.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PassepartoutMac.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
0ECB78E1285F53ED00B0E460 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
0ECB78E1285F53ED00B0E460 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
0ECB78EA2861D1F300B0E460 /* PassepartoutLibrary */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PassepartoutLibrary; sourceTree = "<group>"; };
|
0ECB78EA2861D1F300B0E460 /* PassepartoutLibrary */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PassepartoutLibrary; sourceTree = "<group>"; };
|
||||||
|
@ -639,17 +641,18 @@
|
||||||
0E71ACEA27C1060D00F85C4B /* EndpointView.swift */,
|
0E71ACEA27C1060D00F85C4B /* EndpointView.swift */,
|
||||||
0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */,
|
0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */,
|
||||||
0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */,
|
0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */,
|
||||||
|
0EB90CC029C25BBD00E64628 /* InteractiveConnectionView.swift */,
|
||||||
0E0BD27227B2EA2C00583AC5 /* MainView.swift */,
|
0E0BD27227B2EA2C00583AC5 /* MainView.swift */,
|
||||||
0E71ACE827C1055200F85C4B /* NetworkSettingsView.swift */,
|
0E71ACE827C1055200F85C4B /* NetworkSettingsView.swift */,
|
||||||
0EB34BC927C6A70200B126DA /* OnDemandView.swift */,
|
0EB34BC927C6A70200B126DA /* OnDemandView.swift */,
|
||||||
0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */,
|
0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */,
|
||||||
0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */,
|
0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */,
|
||||||
|
0EBE880E281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift */,
|
||||||
0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */,
|
0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */,
|
||||||
0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */,
|
0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */,
|
||||||
0EF0FAF527DD0211007EB181 /* PaywallView.swift */,
|
0EF0FAF527DD0211007EB181 /* PaywallView.swift */,
|
||||||
0ED30DCE27EA1EF80057D8A3 /* PaywallView+Beta.swift */,
|
0ED30DCE27EA1EF80057D8A3 /* PaywallView+Beta.swift */,
|
||||||
0ED30DD127EA1F650057D8A3 /* PaywallView+Purchase.swift */,
|
0ED30DD127EA1F650057D8A3 /* PaywallView+Purchase.swift */,
|
||||||
0EBE880E281B18DE0090D9E6 /* ProfileRow.swift */,
|
|
||||||
0E44689527B051C300A14CE4 /* ProfileView.swift */,
|
0E44689527B051C300A14CE4 /* ProfileView.swift */,
|
||||||
0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */,
|
0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */,
|
||||||
0E92D7C827F1042A0033CB7B /* ProfileView+Extra.swift */,
|
0E92D7C827F1042A0033CB7B /* ProfileView+Extra.swift */,
|
||||||
|
@ -1389,6 +1392,7 @@
|
||||||
0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */,
|
0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */,
|
||||||
0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */,
|
0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */,
|
||||||
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */,
|
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */,
|
||||||
|
0EB90CC129C25BBD00E64628 /* InteractiveConnectionView.swift in Sources */,
|
||||||
0E35C09A280E95BB0071FA35 /* ProviderProfileAvailability.swift in Sources */,
|
0E35C09A280E95BB0071FA35 /* ProviderProfileAvailability.swift in Sources */,
|
||||||
0E04F0092883466500BFCE1C /* DefaultLightUtils.swift in Sources */,
|
0E04F0092883466500BFCE1C /* DefaultLightUtils.swift in Sources */,
|
||||||
0E5349C827C176D100C71BB3 /* EndpointView+WireGuard.swift in Sources */,
|
0E5349C827C176D100C71BB3 /* EndpointView+WireGuard.swift in Sources */,
|
||||||
|
@ -1415,7 +1419,7 @@
|
||||||
0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */,
|
0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */,
|
||||||
0E5468002867AC9A00F74D1C /* MacUtils.swift in Sources */,
|
0E5468002867AC9A00F74D1C /* MacUtils.swift in Sources */,
|
||||||
0E96D3052872010A005EFBCF /* DefaultLightVPNManager.swift in Sources */,
|
0E96D3052872010A005EFBCF /* DefaultLightVPNManager.swift in Sources */,
|
||||||
0EBE880F281B18DE0090D9E6 /* ProfileRow.swift in Sources */,
|
0EBE880F281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift in Sources */,
|
||||||
0ED30DCF27EA1EF80057D8A3 /* PaywallView+Beta.swift in Sources */,
|
0ED30DCF27EA1EF80057D8A3 /* PaywallView+Beta.swift in Sources */,
|
||||||
0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */,
|
0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */,
|
||||||
0E71ACF727C107CA00F85C4B /* DebugLogView.swift in Sources */,
|
0E71ACF727C107CA00F85C4B /* DebugLogView.swift in Sources */,
|
||||||
|
|
|
@ -41,8 +41,6 @@ struct AccountView: View {
|
||||||
|
|
||||||
@State private var liveAccount = Profile.Account()
|
@State private var liveAccount = Profile.Account()
|
||||||
|
|
||||||
@State private var isPasswordRevealed = false
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
providerName: ProviderName?,
|
providerName: ProviderName?,
|
||||||
vpnProtocol: VPNProtocolType,
|
vpnProtocol: VPNProtocolType,
|
||||||
|
@ -60,6 +58,13 @@ struct AccountView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
|
Section {
|
||||||
|
themeTextPicker(L10n.Endpoint.Advanced.Openvpn.Items.Digest.caption, selection: $liveAccount.authenticationMethod ?? .persistent, values: [
|
||||||
|
.persistent,
|
||||||
|
.interactive
|
||||||
|
// .totp // TODO: interactive, support OTP-based authentication
|
||||||
|
], description: \.localizedDescription)
|
||||||
|
}
|
||||||
Section {
|
Section {
|
||||||
TextField(usernamePlaceholder ?? L10n.Account.Items.Username.placeholder, text: $liveAccount.username)
|
TextField(usernamePlaceholder ?? L10n.Account.Items.Username.placeholder, text: $liveAccount.username)
|
||||||
.textContentType(.username)
|
.textContentType(.username)
|
||||||
|
@ -67,6 +72,11 @@ struct AccountView: View {
|
||||||
.themeRawTextStyle()
|
.themeRawTextStyle()
|
||||||
.withLeadingText(L10n.Account.Items.Username.caption)
|
.withLeadingText(L10n.Account.Items.Username.caption)
|
||||||
|
|
||||||
|
switch liveAccount.authenticationMethod {
|
||||||
|
case nil, .persistent, .interactive:
|
||||||
|
if liveAccount.authenticationMethod == .interactive {
|
||||||
|
EmptyView()
|
||||||
|
} else {
|
||||||
RevealingSecureField(L10n.Account.Items.Password.placeholder, text: $liveAccount.password) {
|
RevealingSecureField(L10n.Account.Items.Password.placeholder, text: $liveAccount.password) {
|
||||||
themeConceilImage.asSystemImage
|
themeConceilImage.asSystemImage
|
||||||
.themeAccentForegroundStyle()
|
.themeAccentForegroundStyle()
|
||||||
|
@ -76,6 +86,20 @@ struct AccountView: View {
|
||||||
}.textContentType(.password)
|
}.textContentType(.password)
|
||||||
.themeRawTextStyle()
|
.themeRawTextStyle()
|
||||||
.withLeadingText(L10n.Account.Items.Password.caption)
|
.withLeadingText(L10n.Account.Items.Password.caption)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: interactive, scan QR code
|
||||||
|
case .totp:
|
||||||
|
RevealingSecureField(L10n.Account.Items.Password.placeholder, text: $liveAccount.password) {
|
||||||
|
themeConceilImage.asSystemImage
|
||||||
|
.themeAccentForegroundStyle()
|
||||||
|
} revealImage: {
|
||||||
|
themeRevealImage.asSystemImage
|
||||||
|
.themeAccentForegroundStyle()
|
||||||
|
}.textContentType(.oneTimeCode)
|
||||||
|
.themeRawTextStyle()
|
||||||
|
.withLeadingText(L10n.Account.Items.Seed.caption)
|
||||||
|
}
|
||||||
} footer: {
|
} footer: {
|
||||||
metadata?.localizedGuidanceString.map {
|
metadata?.localizedGuidanceString.map {
|
||||||
Text($0)
|
Text($0)
|
||||||
|
@ -125,3 +149,18 @@ extension AccountView {
|
||||||
return providerManager.provider(withName: name)
|
return providerManager.provider(withName: name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension Profile.Account.AuthenticationMethod {
|
||||||
|
var localizedDescription: String {
|
||||||
|
switch self {
|
||||||
|
case .persistent:
|
||||||
|
return L10n.Account.Items.AuthenticationMethod.persistent
|
||||||
|
|
||||||
|
case .interactive:
|
||||||
|
return L10n.Account.Items.AuthenticationMethod.interactive
|
||||||
|
|
||||||
|
case .totp:
|
||||||
|
return Unlocalized.Other.totp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
//
|
||||||
|
// InteractiveConnectionView.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 3/15/23.
|
||||||
|
// Copyright (c) 2022 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/passepartoutvpn
|
||||||
|
//
|
||||||
|
// This file is part of Passepartout.
|
||||||
|
//
|
||||||
|
// Passepartout is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Passepartout is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import PassepartoutLibrary
|
||||||
|
|
||||||
|
struct InteractiveConnectionView: View {
|
||||||
|
@Environment(\.presentationMode) private var presentationMode
|
||||||
|
|
||||||
|
@ObservedObject private var profileManager: ProfileManager
|
||||||
|
|
||||||
|
@ObservedObject private var vpnManager: VPNManager
|
||||||
|
|
||||||
|
private let profile: Profile
|
||||||
|
|
||||||
|
@State private var password = ""
|
||||||
|
|
||||||
|
init(profile: Profile) {
|
||||||
|
profileManager = .shared
|
||||||
|
vpnManager = .shared
|
||||||
|
self.profile = profile
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
TextField(L10n.Account.Items.Username.placeholder, text: .constant(profile.account.username))
|
||||||
|
.withLeadingText(L10n.Account.Items.Username.caption)
|
||||||
|
.disabled(true)
|
||||||
|
|
||||||
|
RevealingSecureField(L10n.Account.Items.Password.placeholder, text: $password) {
|
||||||
|
themeConceilImage.asSystemImage
|
||||||
|
.themeAccentForegroundStyle()
|
||||||
|
} revealImage: {
|
||||||
|
themeRevealImage.asSystemImage
|
||||||
|
.themeAccentForegroundStyle()
|
||||||
|
}.textContentType(.password)
|
||||||
|
.themeRawTextStyle()
|
||||||
|
.withLeadingText(L10n.Account.Items.Password.caption)
|
||||||
|
} header: {
|
||||||
|
Text(L10n.Account.title)
|
||||||
|
}
|
||||||
|
}.toolbar {
|
||||||
|
themeCloseItem(presentationMode: presentationMode)
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
Button(action: saveAccount) {
|
||||||
|
Text(L10n.Global.Strings.connect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.navigationTitle(profile.header.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveAccount() {
|
||||||
|
Task {
|
||||||
|
try? await vpnManager.connect(with: profile.id, newPassword: password)
|
||||||
|
}
|
||||||
|
presentationMode.wrappedValue.dismiss()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
//
|
||||||
|
// OrganizerView+ProfileRow.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 4/28/22.
|
||||||
|
// Copyright (c) 2022 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/passepartoutvpn
|
||||||
|
//
|
||||||
|
// This file is part of Passepartout.
|
||||||
|
//
|
||||||
|
// Passepartout is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Passepartout is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import PassepartoutLibrary
|
||||||
|
|
||||||
|
extension OrganizerView {
|
||||||
|
struct ProfileRow: View {
|
||||||
|
private let profile: Profile
|
||||||
|
|
||||||
|
private let isActiveProfile: Bool
|
||||||
|
|
||||||
|
@Binding private var modalType: ModalType?
|
||||||
|
|
||||||
|
private var interactiveProfile: Binding<Profile?> {
|
||||||
|
.init {
|
||||||
|
if case .interactiveAccount(let profile) = modalType {
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
} set: {
|
||||||
|
if let profile = $0 {
|
||||||
|
modalType = .interactiveAccount(profile: profile)
|
||||||
|
} else {
|
||||||
|
modalType = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(profile: Profile, isActiveProfile: Bool, modalType: Binding<ModalType?>) {
|
||||||
|
self.profile = profile
|
||||||
|
self.isActiveProfile = isActiveProfile
|
||||||
|
_modalType = modalType
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
debugChanges()
|
||||||
|
return HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Text(profile.header.name)
|
||||||
|
.font(.headline)
|
||||||
|
.themeLongTextStyle()
|
||||||
|
|
||||||
|
VPNStatusText(isActiveProfile: isActiveProfile)
|
||||||
|
.font(.subheadline)
|
||||||
|
.themeSecondaryTextStyle()
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
VPNToggle(
|
||||||
|
profile: profile,
|
||||||
|
interactiveProfile: interactiveProfile,
|
||||||
|
rateLimit: Constants.RateLimit.vpnToggle
|
||||||
|
).labelsHidden()
|
||||||
|
}.padding([.top, .bottom], 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,8 +30,11 @@ extension OrganizerView {
|
||||||
struct ProfilesList: View {
|
struct ProfilesList: View {
|
||||||
@ObservedObject private var profileManager: ProfileManager
|
@ObservedObject private var profileManager: ProfileManager
|
||||||
|
|
||||||
init() {
|
@Binding private var modalType: ModalType?
|
||||||
|
|
||||||
|
init(modalType: Binding<ModalType?>) {
|
||||||
profileManager = .shared
|
profileManager = .shared
|
||||||
|
_modalType = modalType
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -92,7 +95,8 @@ extension OrganizerView {
|
||||||
private func profileLabel(forProfile profile: Profile) -> some View {
|
private func profileLabel(forProfile profile: Profile) -> some View {
|
||||||
ProfileRow(
|
ProfileRow(
|
||||||
profile: profile,
|
profile: profile,
|
||||||
isActiveProfile: profileManager.isActiveProfile(profile.id)
|
isActiveProfile: profileManager.isActiveProfile(profile.id),
|
||||||
|
modalType: $modalType
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,17 @@ import SwiftUI
|
||||||
import PassepartoutLibrary
|
import PassepartoutLibrary
|
||||||
|
|
||||||
struct OrganizerView: View {
|
struct OrganizerView: View {
|
||||||
|
enum ModalType: Identifiable {
|
||||||
|
case interactiveAccount(profile: Profile)
|
||||||
|
|
||||||
|
// XXX: alert ids
|
||||||
|
var id: Int {
|
||||||
|
switch self {
|
||||||
|
case .interactiveAccount: return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum AlertType: Identifiable {
|
enum AlertType: Identifiable {
|
||||||
case subscribeReddit
|
case subscribeReddit
|
||||||
|
|
||||||
|
@ -44,6 +55,8 @@ struct OrganizerView: View {
|
||||||
|
|
||||||
@State private var addProfileModalType: AddProfileMenu.ModalType?
|
@State private var addProfileModalType: AddProfileMenu.ModalType?
|
||||||
|
|
||||||
|
@State private var modalType: ModalType?
|
||||||
|
|
||||||
@State private var alertType: AlertType?
|
@State private var alertType: AlertType?
|
||||||
|
|
||||||
@State private var isHostFileImporterPresented = false
|
@State private var isHostFileImporterPresented = false
|
||||||
|
@ -58,7 +71,7 @@ struct OrganizerView: View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
return ZStack {
|
return ZStack {
|
||||||
hiddenSceneView
|
hiddenSceneView
|
||||||
ProfilesList()
|
ProfilesList(modalType: $modalType)
|
||||||
}.toolbar {
|
}.toolbar {
|
||||||
ToolbarItem(placement: .primaryAction) {
|
ToolbarItem(placement: .primaryAction) {
|
||||||
AddProfileMenu(
|
AddProfileMenu(
|
||||||
|
@ -71,7 +84,8 @@ struct OrganizerView: View {
|
||||||
SettingsButton()
|
SettingsButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.alert(item: $alertType, content: presentedAlert)
|
}.sheet(item: $modalType, content: presentedModal)
|
||||||
|
.alert(item: $alertType, content: presentedAlert)
|
||||||
.fileImporter(
|
.fileImporter(
|
||||||
isPresented: $isHostFileImporterPresented,
|
isPresented: $isHostFileImporterPresented,
|
||||||
allowedContentTypes: hostFileTypes,
|
allowedContentTypes: hostFileTypes,
|
||||||
|
@ -119,6 +133,16 @@ extension OrganizerView {
|
||||||
addProfileModalType = .addHost(url, false)
|
addProfileModalType = .addHost(url, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func presentedModal(_ modalType: ModalType) -> some View {
|
||||||
|
switch modalType {
|
||||||
|
case .interactiveAccount(let profile):
|
||||||
|
NavigationView {
|
||||||
|
InteractiveConnectionView(profile: profile)
|
||||||
|
}.themeGlobal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func presentedAlert(_ alertType: AlertType) -> Alert {
|
private func presentedAlert(_ alertType: AlertType) -> Alert {
|
||||||
switch alertType {
|
switch alertType {
|
||||||
case .subscribeReddit:
|
case .subscribeReddit:
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
//
|
|
||||||
// ProfileRow.swift
|
|
||||||
// Passepartout
|
|
||||||
//
|
|
||||||
// Created by Davide De Rosa on 4/28/22.
|
|
||||||
// Copyright (c) 2022 Davide De Rosa. All rights reserved.
|
|
||||||
//
|
|
||||||
// https://github.com/passepartoutvpn
|
|
||||||
//
|
|
||||||
// This file is part of Passepartout.
|
|
||||||
//
|
|
||||||
// Passepartout is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// Passepartout is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import PassepartoutLibrary
|
|
||||||
|
|
||||||
struct ProfileRow: View {
|
|
||||||
let profile: Profile
|
|
||||||
|
|
||||||
let isActiveProfile: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
debugChanges()
|
|
||||||
return HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
|
||||||
Text(profile.header.name)
|
|
||||||
.font(.headline)
|
|
||||||
.themeLongTextStyle()
|
|
||||||
|
|
||||||
VPNStatusText(isActiveProfile: isActiveProfile)
|
|
||||||
.font(.subheadline)
|
|
||||||
.themeSecondaryTextStyle()
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
VPNToggle(profileId: profile.id, rateLimit: Constants.RateLimit.vpnToggle)
|
|
||||||
.labelsHidden()
|
|
||||||
}.padding([.top, .bottom], 10)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -30,15 +30,26 @@ extension ProfileView {
|
||||||
struct VPNSection: View {
|
struct VPNSection: View {
|
||||||
@ObservedObject private var profileManager: ProfileManager
|
@ObservedObject private var profileManager: ProfileManager
|
||||||
|
|
||||||
private let profileId: UUID
|
private let profile: Profile
|
||||||
|
|
||||||
private var isActiveProfile: Bool {
|
@Binding private var modalType: ModalType?
|
||||||
profileManager.isActiveProfile(profileId)
|
|
||||||
|
private var interactiveProfile: Binding<Profile?> {
|
||||||
|
.init {
|
||||||
|
modalType == .interactiveAccount ? profile : nil
|
||||||
|
} set: {
|
||||||
|
modalType = $0 != nil ? .interactiveAccount : nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(profileId: UUID) {
|
private var isActiveProfile: Bool {
|
||||||
|
profileManager.isActiveProfile(profile.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(profile: Profile, modalType: Binding<ModalType?>) {
|
||||||
profileManager = .shared
|
profileManager = .shared
|
||||||
self.profileId = profileId
|
self.profile = profile
|
||||||
|
_modalType = modalType
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -54,7 +65,11 @@ extension ProfileView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var toggleView: some View {
|
private var toggleView: some View {
|
||||||
VPNToggle(profileId: profileId, rateLimit: Constants.RateLimit.vpnToggle)
|
VPNToggle(
|
||||||
|
profile: profile,
|
||||||
|
interactiveProfile: interactiveProfile,
|
||||||
|
rateLimit: Constants.RateLimit.vpnToggle
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var statusView: some View {
|
private var statusView: some View {
|
||||||
|
|
|
@ -28,6 +28,8 @@ import PassepartoutLibrary
|
||||||
|
|
||||||
struct ProfileView: View {
|
struct ProfileView: View {
|
||||||
enum ModalType: Int, Identifiable {
|
enum ModalType: Int, Identifiable {
|
||||||
|
case interactiveAccount
|
||||||
|
|
||||||
case shortcuts
|
case shortcuts
|
||||||
|
|
||||||
case rename
|
case rename
|
||||||
|
@ -89,7 +91,10 @@ struct ProfileView: View {
|
||||||
private var mainView: some View {
|
private var mainView: some View {
|
||||||
List {
|
List {
|
||||||
if !isLoading {
|
if !isLoading {
|
||||||
VPNSection(profileId: currentProfile.value.id)
|
VPNSection(
|
||||||
|
profile: currentProfile.value,
|
||||||
|
modalType: $modalType
|
||||||
|
)
|
||||||
ProviderSection(currentProfile: currentProfile)
|
ProviderSection(currentProfile: currentProfile)
|
||||||
ConfigurationSection(
|
ConfigurationSection(
|
||||||
currentProfile: currentProfile,
|
currentProfile: currentProfile,
|
||||||
|
@ -105,6 +110,11 @@ struct ProfileView: View {
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func presentedModal(_ modalType: ModalType) -> some View {
|
private func presentedModal(_ modalType: ModalType) -> some View {
|
||||||
switch modalType {
|
switch modalType {
|
||||||
|
case .interactiveAccount:
|
||||||
|
NavigationView {
|
||||||
|
InteractiveConnectionView(profile: currentProfile.value)
|
||||||
|
}.themeGlobal()
|
||||||
|
|
||||||
case .shortcuts:
|
case .shortcuts:
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ShortcutsView(target: currentProfile.value)
|
ShortcutsView(target: currentProfile.value)
|
||||||
|
|
|
@ -35,14 +35,20 @@ struct VPNToggle: View {
|
||||||
|
|
||||||
@ObservedObject private var productManager: ProductManager
|
@ObservedObject private var productManager: ProductManager
|
||||||
|
|
||||||
private let profileId: UUID
|
private let profile: Profile
|
||||||
|
|
||||||
|
@Binding private var interactiveProfile: Profile?
|
||||||
|
|
||||||
private let rateLimit: Int
|
private let rateLimit: Int
|
||||||
|
|
||||||
private var isEnabled: Binding<Bool> {
|
private var isEnabled: Binding<Bool> {
|
||||||
.init {
|
.init {
|
||||||
isActiveProfile && currentVPNState.isEnabled
|
isActiveProfile && currentVPNState.isEnabled && !shouldPromptForAccount
|
||||||
} set: { newValue in
|
} set: { newValue in
|
||||||
|
guard !shouldPromptForAccount else {
|
||||||
|
interactiveProfile = profile
|
||||||
|
return
|
||||||
|
}
|
||||||
guard newValue else {
|
guard newValue else {
|
||||||
disableVPN()
|
disableVPN()
|
||||||
return
|
return
|
||||||
|
@ -52,7 +58,11 @@ struct VPNToggle: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isActiveProfile: Bool {
|
private var isActiveProfile: Bool {
|
||||||
profileManager.isActiveProfile(profileId)
|
profileManager.isActiveProfile(profile.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var shouldPromptForAccount: Bool {
|
||||||
|
profile.account.authenticationMethod == .interactive && (currentVPNState.vpnStatus == .disconnecting || currentVPNState.vpnStatus == .disconnected)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isEligibleForSiri: Bool {
|
private var isEligibleForSiri: Bool {
|
||||||
|
@ -61,12 +71,13 @@ struct VPNToggle: View {
|
||||||
|
|
||||||
@State private var canToggle = true
|
@State private var canToggle = true
|
||||||
|
|
||||||
init(profileId: UUID, rateLimit: Int) {
|
init(profile: Profile, interactiveProfile: Binding<Profile?>, rateLimit: Int) {
|
||||||
profileManager = .shared
|
profileManager = .shared
|
||||||
vpnManager = .shared
|
vpnManager = .shared
|
||||||
currentVPNState = .shared
|
currentVPNState = .shared
|
||||||
productManager = .shared
|
productManager = .shared
|
||||||
self.profileId = profileId
|
self.profile = profile
|
||||||
|
_interactiveProfile = interactiveProfile
|
||||||
self.rateLimit = rateLimit
|
self.rateLimit = rateLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,10 +93,10 @@ struct VPNToggle: View {
|
||||||
await Task.maybeWait(forMilliseconds: rateLimit)
|
await Task.maybeWait(forMilliseconds: rateLimit)
|
||||||
canToggle = true
|
canToggle = true
|
||||||
do {
|
do {
|
||||||
let profile = try await vpnManager.connect(with: profileId)
|
let profile = try await vpnManager.connect(with: profile.id)
|
||||||
donateIntents(withProfile: profile)
|
donateIntents(withProfile: profile)
|
||||||
} catch {
|
} catch {
|
||||||
pp_log.warning("Unable to connect to profile \(profileId): \(error)")
|
pp_log.warning("Unable to connect to profile \(profile.id): \(error)")
|
||||||
canToggle = true
|
canToggle = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,12 @@ internal enum L10n {
|
||||||
/// MARK: ProfileView -> AccountView
|
/// MARK: ProfileView -> AccountView
|
||||||
internal static let title = L10n.tr("Localizable", "account.title", fallback: "Account")
|
internal static let title = L10n.tr("Localizable", "account.title", fallback: "Account")
|
||||||
internal enum Items {
|
internal enum Items {
|
||||||
|
internal enum AuthenticationMethod {
|
||||||
|
/// Interactive
|
||||||
|
internal static let interactive = L10n.tr("Localizable", "account.items.authentication_method.interactive", fallback: "Interactive")
|
||||||
|
/// Persistent
|
||||||
|
internal static let persistent = L10n.tr("Localizable", "account.items.authentication_method.persistent", fallback: "Persistent")
|
||||||
|
}
|
||||||
internal enum OpenGuide {
|
internal enum OpenGuide {
|
||||||
/// See your credentials
|
/// See your credentials
|
||||||
internal static let caption = L10n.tr("Localizable", "account.items.open_guide.caption", fallback: "See your credentials")
|
internal static let caption = L10n.tr("Localizable", "account.items.open_guide.caption", fallback: "See your credentials")
|
||||||
|
@ -64,6 +70,10 @@ internal enum L10n {
|
||||||
/// secret
|
/// secret
|
||||||
internal static let placeholder = L10n.tr("Localizable", "account.items.password.placeholder", fallback: "secret")
|
internal static let placeholder = L10n.tr("Localizable", "account.items.password.placeholder", fallback: "secret")
|
||||||
}
|
}
|
||||||
|
internal enum Seed {
|
||||||
|
/// Seed
|
||||||
|
internal static let caption = L10n.tr("Localizable", "account.items.seed.caption", fallback: "Seed")
|
||||||
|
}
|
||||||
internal enum Signup {
|
internal enum Signup {
|
||||||
/// Register with %@
|
/// Register with %@
|
||||||
internal static func caption(_ p1: Any) -> String {
|
internal static func caption(_ p1: Any) -> String {
|
||||||
|
|
|
@ -252,5 +252,7 @@ enum Unlocalized {
|
||||||
|
|
||||||
enum Other {
|
enum Other {
|
||||||
static let siri = "Siri"
|
static let siri = "Siri"
|
||||||
|
|
||||||
|
static let totp = "TOTP"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,10 +179,13 @@
|
||||||
"account.title" = "Account";
|
"account.title" = "Account";
|
||||||
"account.sections.credentials.header" = "Credentials";
|
"account.sections.credentials.header" = "Credentials";
|
||||||
"account.sections.registration.footer" = "Go get an account on the %@ website.";
|
"account.sections.registration.footer" = "Go get an account on the %@ website.";
|
||||||
|
"account.items.authentication_method.persistent" = "Persistent";
|
||||||
|
"account.items.authentication_method.interactive" = "Interactive";
|
||||||
"account.items.username.caption" = "Username";
|
"account.items.username.caption" = "Username";
|
||||||
"account.items.username.placeholder" = "username";
|
"account.items.username.placeholder" = "username";
|
||||||
"account.items.password.caption" = "Password";
|
"account.items.password.caption" = "Password";
|
||||||
"account.items.password.placeholder" = "secret";
|
"account.items.password.placeholder" = "secret";
|
||||||
|
"account.items.seed.caption" = "Seed";
|
||||||
"account.items.open_guide.caption" = "See your credentials";
|
"account.items.open_guide.caption" = "See your credentials";
|
||||||
"account.items.signup.caption" = "Register with %@";
|
"account.items.signup.caption" = "Register with %@";
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,16 @@ import Foundation
|
||||||
|
|
||||||
extension Profile {
|
extension Profile {
|
||||||
public struct Account: Codable, Equatable {
|
public struct Account: Codable, Equatable {
|
||||||
|
public enum AuthenticationMethod: String, Codable {
|
||||||
|
case persistent
|
||||||
|
|
||||||
|
case interactive
|
||||||
|
|
||||||
|
case totp
|
||||||
|
}
|
||||||
|
|
||||||
|
public var authenticationMethod: AuthenticationMethod?
|
||||||
|
|
||||||
public var username: String
|
public var username: String
|
||||||
|
|
||||||
public var password: String
|
public var password: String
|
||||||
|
|
|
@ -42,8 +42,17 @@ extension NEOnDemandRuleInterfaceType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Profile.OnDemand {
|
extension Profile {
|
||||||
func rules(withCustomRules: Bool) -> [NEOnDemandRule] {
|
func onDemandRules(withCustomRules: Bool) -> [NEOnDemandRule] {
|
||||||
|
onDemand.rules(isInteractive: account.authenticationMethod == .interactive, withCustomRules: withCustomRules)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Profile.OnDemand {
|
||||||
|
func rules(isInteractive: Bool, withCustomRules: Bool) -> [NEOnDemandRule] {
|
||||||
|
guard isEnabled && !isInteractive else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: on-demand, drop hardcoding when "trusted networks" -> "on-demand"
|
// TODO: on-demand, drop hardcoding when "trusted networks" -> "on-demand"
|
||||||
// isEnabled = true
|
// isEnabled = true
|
||||||
|
|
|
@ -49,9 +49,9 @@ extension VPNManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public func connect(with profileId: UUID) async throws -> Profile {
|
public func connect(with profileId: UUID, newPassword: String? = nil) async throws -> Profile {
|
||||||
let result = try profileManager.liveProfileEx(withId: profileId)
|
let result = try profileManager.liveProfileEx(withId: profileId)
|
||||||
let profile = result.profile
|
var profile = result.profile
|
||||||
guard !profileManager.isActiveProfile(profileId) ||
|
guard !profileManager.isActiveProfile(profileId) ||
|
||||||
currentState.vpnStatus != .connected else {
|
currentState.vpnStatus != .connected else {
|
||||||
|
|
||||||
|
@ -63,6 +63,9 @@ extension VPNManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
pp_log.info("Connecting to: \(profile.logDescription)")
|
pp_log.info("Connecting to: \(profile.logDescription)")
|
||||||
|
if let newPassword {
|
||||||
|
profile.account.password = newPassword
|
||||||
|
}
|
||||||
let cfg = try vpnConfiguration(withProfile: profile)
|
let cfg = try vpnConfiguration(withProfile: profile)
|
||||||
|
|
||||||
profileManager.activateProfile(profile)
|
profileManager.activateProfile(profile)
|
||||||
|
|
|
@ -60,7 +60,7 @@ struct VPNConfigurationParameters {
|
||||||
username = !profile.account.username.isEmpty ? profile.account.username : nil
|
username = !profile.account.username.isEmpty ? profile.account.username : nil
|
||||||
self.passwordReference = passwordReference
|
self.passwordReference = passwordReference
|
||||||
self.withNetworkSettings = withNetworkSettings
|
self.withNetworkSettings = withNetworkSettings
|
||||||
onDemandRules = profile.onDemand.rules(withCustomRules: withCustomRules)
|
onDemandRules = profile.onDemandRules(withCustomRules: withCustomRules)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue