Page MenuHomeMusing Studio

No OneTemporary

diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift
index e5361cb..b7fb3d7 100644
--- a/Shared/Models/WriteFreelyModel.swift
+++ b/Shared/Models/WriteFreelyModel.swift
@@ -1,97 +1,158 @@
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
}
}
// 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 }
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()
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()
client = nil
DispatchQueue.main.async {
self.account.logout()
}
} catch WFError.notFound {
// The user token is invalid or doesn't exist, so it's been invalidated by the server. Proceed with
// destroying the client object and setting the AccountModel to its logged-out state.
client = nil
DispatchQueue.main.async {
self.account.logout()
}
} 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

Mime Type
text/x-diff
Expires
Thu, Mar 12, 7:28 PM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3653650

Event Timeline