Page Menu
Home
Musing Studio
Search
Configure Global Search
Log In
Files
F10669132
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
10 KB
Subscribers
None
View Options
diff --git a/Shared/Account/AccountLogoutView.swift b/Shared/Account/AccountLogoutView.swift
index 16b9fd0..b86cf18 100644
--- a/Shared/Account/AccountLogoutView.swift
+++ b/Shared/Account/AccountLogoutView.swift
@@ -1,30 +1,30 @@
import SwiftUI
struct AccountLogoutView: View {
@EnvironmentObject var model: WriteFreelyModel
var body: some View {
VStack {
Spacer()
VStack {
- Text("Logged in as \(model.account.user?.username ?? "Anonymous")")
+ Text("Logged in as \(model.account.username ?? "Anonymous")")
Text("on \(model.account.server)")
}
Spacer()
Button(action: logoutHandler, label: {
Text("Logout")
})
}
}
func logoutHandler() {
model.logout()
}
}
struct AccountLogoutView_Previews: PreviewProvider {
static var previews: some View {
AccountLogoutView()
.environmentObject(WriteFreelyModel())
}
}
diff --git a/Shared/Account/AccountModel.swift b/Shared/Account/AccountModel.swift
index 5328018..d8b2473 100644
--- a/Shared/Account/AccountModel.swift
+++ b/Shared/Account/AccountModel.swift
@@ -1,63 +1,71 @@
import Foundation
import WriteFreely
enum AccountError: Error {
case invalidPassword
case usernameNotFound
case serverNotFound
}
extension AccountError: LocalizedError {
public var errorDescription: String? {
switch self {
case .serverNotFound:
return NSLocalizedString(
"The server could not be found. Please check the information you've entered and try again.",
comment: ""
)
case .invalidPassword:
return NSLocalizedString(
"Invalid password. Please check that you've entered your password correctly and try logging in again.",
comment: ""
)
case .usernameNotFound:
return NSLocalizedString(
"Username not found. Did you use your email address by mistake?",
comment: ""
)
}
}
}
struct AccountModel {
private let defaults = UserDefaults.standard
let isLoggedInFlag = "isLoggedInFlag"
let usernameStringKey = "usernameStringKey"
let serverStringKey = "serverStringKey"
var server: String = ""
+ var username: String = ""
var hasError: Bool = false
var currentError: AccountError? {
didSet {
hasError = true
}
}
private(set) var user: WFUser?
private(set) var isLoggedIn: Bool = false
mutating func login(_ user: WFUser) {
self.user = user
+ self.username = user.username ?? ""
self.isLoggedIn = true
defaults.set(true, forKey: isLoggedInFlag)
defaults.set(user.username, forKey: usernameStringKey)
defaults.set(server, forKey: serverStringKey)
}
mutating func logout() {
self.user = nil
self.isLoggedIn = false
defaults.set(false, forKey: isLoggedInFlag)
defaults.removeObject(forKey: usernameStringKey)
defaults.removeObject(forKey: serverStringKey)
}
+
+ mutating func restoreState() {
+ isLoggedIn = defaults.bool(forKey: isLoggedInFlag)
+ server = defaults.string(forKey: serverStringKey) ?? ""
+ username = defaults.string(forKey: usernameStringKey) ?? ""
+ }
}
diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift
index 96971c3..eacfcff 100644
--- a/Shared/Models/WriteFreelyModel.swift
+++ b/Shared/Models/WriteFreelyModel.swift
@@ -1,166 +1,178 @@
import Foundation
import WriteFreely
import Security
// MARK: - WriteFreelyModel
class WriteFreelyModel: ObservableObject {
@Published var account = AccountModel()
@Published var preferences = PreferencesModel()
@Published var store = PostStore()
@Published var post: Post?
@Published var isLoggingIn: Bool = false
private var client: WFClient?
init() {
#if DEBUG
for post in testPostData { store.add(post) }
#endif
+
+ DispatchQueue.main.async {
+ self.account.restoreState()
+ }
}
}
// MARK: - WriteFreelyModel API
extension WriteFreelyModel {
func login(to server: URL, as username: String, password: String) {
isLoggingIn = true
account.server = server.absoluteString
client = WFClient(for: server)
client?.login(username: username, password: password, completion: loginHandler)
}
func logout() {
- guard let loggedInClient = client else { return }
+ guard let loggedInClient = client else {
+ do {
+ try purgeTokenFromKeychain(username: account.username, server: account.server)
+ account.logout()
+ } catch {
+ fatalError("Failed to log out persisted state")
+ }
+ return
+ }
loggedInClient.logout(completion: logoutHandler)
}
}
private extension WriteFreelyModel {
func loginHandler(result: Result<WFUser, Error>) {
DispatchQueue.main.async {
self.isLoggingIn = false
}
do {
let user = try result.get()
saveTokenToKeychain(user.token, username: user.username, server: account.server)
DispatchQueue.main.async {
self.account.login(user)
}
} catch WFError.notFound {
DispatchQueue.main.async {
self.account.currentError = AccountError.usernameNotFound
}
} catch WFError.unauthorized {
DispatchQueue.main.async {
self.account.currentError = AccountError.invalidPassword
}
} catch {
if let error = error as? NSError, error.domain == NSURLErrorDomain, error.code == -1003 {
DispatchQueue.main.async {
self.account.currentError = AccountError.serverNotFound
}
}
}
}
func logoutHandler(result: Result<Bool, Error>) {
do {
_ = try result.get()
do {
try purgeTokenFromKeychain(username: account.user?.username, server: account.server)
client = nil
DispatchQueue.main.async {
self.account.logout()
}
} catch {
print("Something went wrong purging the token from the Keychain.")
}
} catch WFError.notFound {
// The user token is invalid or doesn't exist, so it's been invalidated by the server. Proceed with
// purging the token from the Keychain, destroying the client object, and setting the AccountModel to its
// logged-out state.
do {
try purgeTokenFromKeychain(username: account.user?.username, server: account.server)
client = nil
DispatchQueue.main.async {
self.account.logout()
}
} catch {
print("Something went wrong purging the token from the Keychain.")
}
} catch {
// We get a 'cannot parse response' (similar to what we were seeing in the Swift package) NSURLError here,
// so we're using a hacky workaround — if we get the NSURLError, but the AccountModel still thinks we're
// logged in, try calling the logout function again and see what we get.
// Conditional cast from 'Error' to 'NSError' always succeeds but is the only way to check error properties.
if let error = error as? NSError,
error.domain == NSURLErrorDomain,
error.code == NSURLErrorCannotParseResponse {
if account.isLoggedIn {
self.logout()
}
}
}
}
}
private extension WriteFreelyModel {
// MARK: - Keychain Helpers
func saveTokenToKeychain(_ token: String, username: String?, server: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecValueData as String: token.data(using: .utf8)!,
kSecAttrAccount as String: username ?? "anonymous",
kSecAttrService as String: server
]
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecDuplicateItem || status == errSecSuccess else {
fatalError("Error storing in Keychain with OSStatus: \(status)")
}
}
func purgeTokenFromKeychain(username: String?, server: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: username ?? "anonymous",
kSecAttrService as String: server
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
fatalError("Error deleting from Keychain with OSStatus: \(status)")
}
}
func fetchTokenFromKeychain(username: String?, server: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: username ?? "anonymous",
kSecAttrService as String: server,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnAttributes as String: true,
kSecReturnData as String: true
]
var secItem: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &secItem)
guard status != errSecItemNotFound else {
return nil
}
guard status == errSecSuccess else {
fatalError("Error fetching from Keychain with OSStatus: \(status)")
}
guard let existingSecItem = secItem as? [String: Any],
let tokenData = existingSecItem[kSecValueData as String] as? Data,
let token = String(data: tokenData, encoding: .utf8) else {
return nil
}
return token
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, May 15, 3:59 PM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3239006
Attached To
rWFSUI WriteFreely SwiftUI
Event Timeline
Log In to Comment