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
|
||||
|
||||
- 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)
|
||||
- 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 */; };
|
||||
0EB4042C27CA0E8C00378B1A /* Unlocalized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB4042B27CA0E8B00378B1A /* Unlocalized.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 */; };
|
||||
0EBC075527EBC83800208AD9 /* MailComposerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075427EBC83800208AD9 /* MailComposerView.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 */; };
|
||||
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, ); }; };
|
||||
0ECB78EC2863A21600B0E460 /* PassepartoutLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0ECB78EB2863A21600B0E460 /* PassepartoutLibrary */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -459,7 +461,7 @@
|
|||
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>"; };
|
||||
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; };
|
||||
0ECB78E1285F53ED00B0E460 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
0ECB78EA2861D1F300B0E460 /* PassepartoutLibrary */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PassepartoutLibrary; sourceTree = "<group>"; };
|
||||
|
@ -639,17 +641,18 @@
|
|||
0E71ACEA27C1060D00F85C4B /* EndpointView.swift */,
|
||||
0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */,
|
||||
0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */,
|
||||
0EB90CC029C25BBD00E64628 /* InteractiveConnectionView.swift */,
|
||||
0E0BD27227B2EA2C00583AC5 /* MainView.swift */,
|
||||
0E71ACE827C1055200F85C4B /* NetworkSettingsView.swift */,
|
||||
0EB34BC927C6A70200B126DA /* OnDemandView.swift */,
|
||||
0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */,
|
||||
0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */,
|
||||
0EBE880E281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift */,
|
||||
0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */,
|
||||
0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */,
|
||||
0EF0FAF527DD0211007EB181 /* PaywallView.swift */,
|
||||
0ED30DCE27EA1EF80057D8A3 /* PaywallView+Beta.swift */,
|
||||
0ED30DD127EA1F650057D8A3 /* PaywallView+Purchase.swift */,
|
||||
0EBE880E281B18DE0090D9E6 /* ProfileRow.swift */,
|
||||
0E44689527B051C300A14CE4 /* ProfileView.swift */,
|
||||
0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */,
|
||||
0E92D7C827F1042A0033CB7B /* ProfileView+Extra.swift */,
|
||||
|
@ -1389,6 +1392,7 @@
|
|||
0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */,
|
||||
0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */,
|
||||
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */,
|
||||
0EB90CC129C25BBD00E64628 /* InteractiveConnectionView.swift in Sources */,
|
||||
0E35C09A280E95BB0071FA35 /* ProviderProfileAvailability.swift in Sources */,
|
||||
0E04F0092883466500BFCE1C /* DefaultLightUtils.swift in Sources */,
|
||||
0E5349C827C176D100C71BB3 /* EndpointView+WireGuard.swift in Sources */,
|
||||
|
@ -1415,7 +1419,7 @@
|
|||
0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */,
|
||||
0E5468002867AC9A00F74D1C /* MacUtils.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 */,
|
||||
0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */,
|
||||
0E71ACF727C107CA00F85C4B /* DebugLogView.swift in Sources */,
|
||||
|
|
|
@ -41,8 +41,6 @@ struct AccountView: View {
|
|||
|
||||
@State private var liveAccount = Profile.Account()
|
||||
|
||||
@State private var isPasswordRevealed = false
|
||||
|
||||
init(
|
||||
providerName: ProviderName?,
|
||||
vpnProtocol: VPNProtocolType,
|
||||
|
@ -60,6 +58,13 @@ struct AccountView: View {
|
|||
|
||||
var body: some View {
|
||||
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 {
|
||||
TextField(usernamePlaceholder ?? L10n.Account.Items.Username.placeholder, text: $liveAccount.username)
|
||||
.textContentType(.username)
|
||||
|
@ -67,6 +72,11 @@ struct AccountView: View {
|
|||
.themeRawTextStyle()
|
||||
.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) {
|
||||
themeConceilImage.asSystemImage
|
||||
.themeAccentForegroundStyle()
|
||||
|
@ -76,6 +86,20 @@ struct AccountView: View {
|
|||
}.textContentType(.password)
|
||||
.themeRawTextStyle()
|
||||
.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: {
|
||||
metadata?.localizedGuidanceString.map {
|
||||
Text($0)
|
||||
|
@ -125,3 +149,18 @@ extension AccountView {
|
|||
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 {
|
||||
@ObservedObject private var profileManager: ProfileManager
|
||||
|
||||
init() {
|
||||
@Binding private var modalType: ModalType?
|
||||
|
||||
init(modalType: Binding<ModalType?>) {
|
||||
profileManager = .shared
|
||||
_modalType = modalType
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
@ -92,7 +95,8 @@ extension OrganizerView {
|
|||
private func profileLabel(forProfile profile: Profile) -> some View {
|
||||
ProfileRow(
|
||||
profile: profile,
|
||||
isActiveProfile: profileManager.isActiveProfile(profile.id)
|
||||
isActiveProfile: profileManager.isActiveProfile(profile.id),
|
||||
modalType: $modalType
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,17 @@ import SwiftUI
|
|||
import PassepartoutLibrary
|
||||
|
||||
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 {
|
||||
case subscribeReddit
|
||||
|
||||
|
@ -44,6 +55,8 @@ struct OrganizerView: View {
|
|||
|
||||
@State private var addProfileModalType: AddProfileMenu.ModalType?
|
||||
|
||||
@State private var modalType: ModalType?
|
||||
|
||||
@State private var alertType: AlertType?
|
||||
|
||||
@State private var isHostFileImporterPresented = false
|
||||
|
@ -58,7 +71,7 @@ struct OrganizerView: View {
|
|||
debugChanges()
|
||||
return ZStack {
|
||||
hiddenSceneView
|
||||
ProfilesList()
|
||||
ProfilesList(modalType: $modalType)
|
||||
}.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
AddProfileMenu(
|
||||
|
@ -71,7 +84,8 @@ struct OrganizerView: View {
|
|||
SettingsButton()
|
||||
}
|
||||
}
|
||||
}.alert(item: $alertType, content: presentedAlert)
|
||||
}.sheet(item: $modalType, content: presentedModal)
|
||||
.alert(item: $alertType, content: presentedAlert)
|
||||
.fileImporter(
|
||||
isPresented: $isHostFileImporterPresented,
|
||||
allowedContentTypes: hostFileTypes,
|
||||
|
@ -119,6 +133,16 @@ extension OrganizerView {
|
|||
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 {
|
||||
switch alertType {
|
||||
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 {
|
||||
@ObservedObject private var profileManager: ProfileManager
|
||||
|
||||
private let profileId: UUID
|
||||
private let profile: Profile
|
||||
|
||||
private var isActiveProfile: Bool {
|
||||
profileManager.isActiveProfile(profileId)
|
||||
@Binding private var modalType: ModalType?
|
||||
|
||||
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
|
||||
self.profileId = profileId
|
||||
self.profile = profile
|
||||
_modalType = modalType
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
@ -54,7 +65,11 @@ extension ProfileView {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -28,6 +28,8 @@ import PassepartoutLibrary
|
|||
|
||||
struct ProfileView: View {
|
||||
enum ModalType: Int, Identifiable {
|
||||
case interactiveAccount
|
||||
|
||||
case shortcuts
|
||||
|
||||
case rename
|
||||
|
@ -89,7 +91,10 @@ struct ProfileView: View {
|
|||
private var mainView: some View {
|
||||
List {
|
||||
if !isLoading {
|
||||
VPNSection(profileId: currentProfile.value.id)
|
||||
VPNSection(
|
||||
profile: currentProfile.value,
|
||||
modalType: $modalType
|
||||
)
|
||||
ProviderSection(currentProfile: currentProfile)
|
||||
ConfigurationSection(
|
||||
currentProfile: currentProfile,
|
||||
|
@ -105,6 +110,11 @@ struct ProfileView: View {
|
|||
@ViewBuilder
|
||||
private func presentedModal(_ modalType: ModalType) -> some View {
|
||||
switch modalType {
|
||||
case .interactiveAccount:
|
||||
NavigationView {
|
||||
InteractiveConnectionView(profile: currentProfile.value)
|
||||
}.themeGlobal()
|
||||
|
||||
case .shortcuts:
|
||||
NavigationView {
|
||||
ShortcutsView(target: currentProfile.value)
|
||||
|
|
|
@ -35,14 +35,20 @@ struct VPNToggle: View {
|
|||
|
||||
@ObservedObject private var productManager: ProductManager
|
||||
|
||||
private let profileId: UUID
|
||||
private let profile: Profile
|
||||
|
||||
@Binding private var interactiveProfile: Profile?
|
||||
|
||||
private let rateLimit: Int
|
||||
|
||||
private var isEnabled: Binding<Bool> {
|
||||
.init {
|
||||
isActiveProfile && currentVPNState.isEnabled
|
||||
isActiveProfile && currentVPNState.isEnabled && !shouldPromptForAccount
|
||||
} set: { newValue in
|
||||
guard !shouldPromptForAccount else {
|
||||
interactiveProfile = profile
|
||||
return
|
||||
}
|
||||
guard newValue else {
|
||||
disableVPN()
|
||||
return
|
||||
|
@ -52,7 +58,11 @@ struct VPNToggle: View {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -61,12 +71,13 @@ struct VPNToggle: View {
|
|||
|
||||
@State private var canToggle = true
|
||||
|
||||
init(profileId: UUID, rateLimit: Int) {
|
||||
init(profile: Profile, interactiveProfile: Binding<Profile?>, rateLimit: Int) {
|
||||
profileManager = .shared
|
||||
vpnManager = .shared
|
||||
currentVPNState = .shared
|
||||
productManager = .shared
|
||||
self.profileId = profileId
|
||||
self.profile = profile
|
||||
_interactiveProfile = interactiveProfile
|
||||
self.rateLimit = rateLimit
|
||||
}
|
||||
|
||||
|
@ -82,10 +93,10 @@ struct VPNToggle: View {
|
|||
await Task.maybeWait(forMilliseconds: rateLimit)
|
||||
canToggle = true
|
||||
do {
|
||||
let profile = try await vpnManager.connect(with: profileId)
|
||||
let profile = try await vpnManager.connect(with: profile.id)
|
||||
donateIntents(withProfile: profile)
|
||||
} catch {
|
||||
pp_log.warning("Unable to connect to profile \(profileId): \(error)")
|
||||
pp_log.warning("Unable to connect to profile \(profile.id): \(error)")
|
||||
canToggle = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,6 +54,12 @@ internal enum L10n {
|
|||
/// MARK: ProfileView -> AccountView
|
||||
internal static let title = L10n.tr("Localizable", "account.title", fallback: "Account")
|
||||
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 {
|
||||
/// 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
|
||||
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 {
|
||||
/// Register with %@
|
||||
internal static func caption(_ p1: Any) -> String {
|
||||
|
|
|
@ -252,5 +252,7 @@ enum Unlocalized {
|
|||
|
||||
enum Other {
|
||||
static let siri = "Siri"
|
||||
|
||||
static let totp = "TOTP"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -179,10 +179,13 @@
|
|||
"account.title" = "Account";
|
||||
"account.sections.credentials.header" = "Credentials";
|
||||
"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.placeholder" = "username";
|
||||
"account.items.password.caption" = "Password";
|
||||
"account.items.password.placeholder" = "secret";
|
||||
"account.items.seed.caption" = "Seed";
|
||||
"account.items.open_guide.caption" = "See your credentials";
|
||||
"account.items.signup.caption" = "Register with %@";
|
||||
|
||||
|
|
|
@ -27,6 +27,16 @@ import Foundation
|
|||
|
||||
extension Profile {
|
||||
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 password: String
|
||||
|
|
|
@ -42,8 +42,17 @@ extension NEOnDemandRuleInterfaceType {
|
|||
}
|
||||
}
|
||||
|
||||
extension Profile.OnDemand {
|
||||
func rules(withCustomRules: Bool) -> [NEOnDemandRule] {
|
||||
extension Profile {
|
||||
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"
|
||||
// isEnabled = true
|
||||
|
|
|
@ -49,9 +49,9 @@ extension VPNManager {
|
|||
}
|
||||
|
||||
@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 profile = result.profile
|
||||
var profile = result.profile
|
||||
guard !profileManager.isActiveProfile(profileId) ||
|
||||
currentState.vpnStatus != .connected else {
|
||||
|
||||
|
@ -63,6 +63,9 @@ extension VPNManager {
|
|||
}
|
||||
|
||||
pp_log.info("Connecting to: \(profile.logDescription)")
|
||||
if let newPassword {
|
||||
profile.account.password = newPassword
|
||||
}
|
||||
let cfg = try vpnConfiguration(withProfile: profile)
|
||||
|
||||
profileManager.activateProfile(profile)
|
||||
|
|
|
@ -60,7 +60,7 @@ struct VPNConfigurationParameters {
|
|||
username = !profile.account.username.isEmpty ? profile.account.username : nil
|
||||
self.passwordReference = passwordReference
|
||||
self.withNetworkSettings = withNetworkSettings
|
||||
onDemandRules = profile.onDemand.rules(withCustomRules: withCustomRules)
|
||||
onDemandRules = profile.onDemandRules(withCustomRules: withCustomRules)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue