1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
|
//
// Account.swift
// MullvadVPN
//
// Created by pronebird on 16/05/2019.
// Copyright © 2019 Mullvad VPN AB. All rights reserved.
//
import Combine
import Foundation
import NetworkExtension
import os
/// A enum describing the errors emitted by `Account`
enum AccountError: Error {
/// A failure to perform the login
case login(AccountLoginError)
/// A failure to log out
case logout(TunnelManagerError)
}
/// A enum describing the error emitted during login
enum AccountLoginError: Error {
case invalidAccount
case tunnelConfiguration(TunnelManagerError)
}
extension AccountError: LocalizedError {
var errorDescription: String? {
switch self {
case .login:
return NSLocalizedString("Log in error", comment: "")
case .logout:
return NSLocalizedString("Log out error", comment: "")
}
}
var failureReason: String? {
switch self {
case .login(.invalidAccount):
return NSLocalizedString("Invalid account", comment: "")
case .login(.tunnelConfiguration(.setAccount(let setAccountError))):
switch setAccountError {
case .pushWireguardKey(.transport(.network)):
return NSLocalizedString("Network error", comment: "")
case .pushWireguardKey(.server(let serverError)):
return serverError.errorDescription ?? serverError.message
case .setup(.saveTunnel(let systemError as NEVPNError))
where systemError.code == .configurationReadWriteFailed:
return NSLocalizedString("Permission denied to add a VPN profile", comment: "")
default:
return NSLocalizedString("Internal error", comment: "")
}
case .logout:
return NSLocalizedString("Internal error", comment: "")
default:
return nil
}
}
}
/// A enum holding the `UserDefaults` string keys
private enum UserDefaultsKeys: String {
case isAgreedToTermsOfService = "isAgreedToTermsOfService"
case accountToken = "accountToken"
case accountExpiry = "accountExpiry"
}
/// A class that groups the account related operations
class Account {
static let shared = Account()
private let apiClient = MullvadAPI()
/// Returns true if user agreed to terms of service, otherwise false
var isAgreedToTermsOfService: Bool {
return UserDefaults.standard.bool(forKey: UserDefaultsKeys.isAgreedToTermsOfService.rawValue)
}
/// Returns the currently used account token
var token: String? {
return UserDefaults.standard.string(forKey: UserDefaultsKeys.accountToken.rawValue)
}
/// Returns the account expiry for the currently used account token
var expiry: Date? {
return UserDefaults.standard.object(forKey: UserDefaultsKeys.accountExpiry.rawValue) as? Date
}
var isLoggedIn: Bool {
return token != nil
}
/// Save the boolean flag in preferences indicating that the user agreed to terms of service.
func agreeToTermsOfService() {
UserDefaults.standard.set(true, forKey: UserDefaultsKeys.isAgreedToTermsOfService.rawValue)
}
/// Perform the login and save the account token along with expiry (if available) to the
/// application preferences.
func login(with accountToken: String) -> AnyPublisher<(), AccountError> {
return apiClient.verifyAccount(accountToken: accountToken)
.setFailureType(to: AccountLoginError.self)
.handleEvents(receiveOutput: { (accountVerification) in
if case .deferred(let error) = accountVerification {
os_log(.error, "Failed to verify the account: %{public}s", error.localizedDescription)
}
})
.flatMap {
self.handleVerification($0).publisher
.flatMap { (expiry) in
TunnelManager.shared.setAccount(accountToken: accountToken)
.mapError { AccountLoginError.tunnelConfiguration($0) }
.map { expiry }
}
}.mapError { AccountError.login($0) }
.receive(on: DispatchQueue.main).map { (expiry) in
self.saveAccountToPreferences(accountToken: accountToken, expiry: expiry)
}.eraseToAnyPublisher()
}
/// Perform the logout by erasing the account token and expiry from the application preferences.
func logout() -> AnyPublisher<(), AccountError> {
return TunnelManager.shared.unsetAccount()
.receive(on: DispatchQueue.main)
.mapError { AccountError.logout($0) }
.map(self.removeAccountFromPreferences)
.eraseToAnyPublisher()
}
private func handleVerification(_ verification: AccountVerification) -> Result<Date?, AccountLoginError> {
switch verification {
case .deferred:
return .success(nil)
case .verified(let expiry):
return .success(expiry)
case .invalid:
return .failure(.invalidAccount)
}
}
private func saveAccountToPreferences(accountToken: String, expiry: Date?) {
let preferences = UserDefaults.standard
preferences.set(accountToken, forKey: UserDefaultsKeys.accountToken.rawValue)
preferences.set(expiry, forKey: UserDefaultsKeys.accountExpiry.rawValue)
}
private func removeAccountFromPreferences() {
let preferences = UserDefaults.standard
preferences.removeObject(forKey: UserDefaultsKeys.accountToken.rawValue)
preferences.removeObject(forKey: UserDefaultsKeys.accountExpiry.rawValue)
}
}
|