From 7f5006c629f3870448858d4b1e16304068a682ed Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 6 Mar 2022 15:34:30 -0500 Subject: [PATCH] Use deterministic ids for accounts --- Fervor/Token.swift | 13 ++++++++- Reader/CoreData/PersistentContainer.swift | 4 ++- Reader/FervorController.swift | 8 +++--- Reader/LocalData.swift | 32 ++++++++++++++--------- Reader/SceneDelegate.swift | 8 +++--- 5 files changed, 44 insertions(+), 21 deletions(-) diff --git a/Fervor/Token.swift b/Fervor/Token.swift index d50d9ec..c85708c 100644 --- a/Fervor/Token.swift +++ b/Fervor/Token.swift @@ -7,10 +7,11 @@ import Foundation -public struct Token: Decodable { +public struct Token: Codable, Sendable { public let accessToken: String public let expiresIn: Int? public let refreshToken: String? + public let owner: String? public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -18,11 +19,21 @@ public struct Token: Decodable { self.accessToken = try container.decode(String.self, forKey: .accessToken) self.expiresIn = try container.decodeIfPresent(Int.self, forKey: .expiresIn) self.refreshToken = try container.decodeIfPresent(String.self, forKey: .refreshToken) + self.owner = try container.decodeIfPresent(String.self, forKey: .owner) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(accessToken, forKey: .accessToken) + try container.encodeIfPresent(expiresIn, forKey: .expiresIn) + try container.encodeIfPresent(refreshToken, forKey: .refreshToken) + try container.encodeIfPresent(owner, forKey: .owner) } private enum CodingKeys: String, CodingKey { case accessToken = "access_token" case expiresIn = "expires_in" case refreshToken = "refresh_token" + case owner } } diff --git a/Reader/CoreData/PersistentContainer.swift b/Reader/CoreData/PersistentContainer.swift index cc7c251..f3cc83f 100644 --- a/Reader/CoreData/PersistentContainer.swift +++ b/Reader/CoreData/PersistentContainer.swift @@ -29,7 +29,9 @@ class PersistentContainer: NSPersistentContainer, @unchecked Sendable { private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentContainer") init(account: LocalData.Account) { - super.init(name: "\(account.id)", managedObjectModel: PersistentContainer.managedObjectModel) + // slashes the base64 string turn into subdirectories which we don't want + let name = account.id.base64EncodedString().replacingOccurrences(of: "/", with: "_") + super.init(name: name, managedObjectModel: PersistentContainer.managedObjectModel) loadPersistentStores { description, error in if let error = error { diff --git a/Reader/FervorController.swift b/Reader/FervorController.swift index 3a1e657..f2956ab 100644 --- a/Reader/FervorController.swift +++ b/Reader/FervorController.swift @@ -22,7 +22,7 @@ actor FervorController { nonisolated let account: LocalData.Account? private(set) var clientID: String? private(set) var clientSecret: String? - private(set) var accessToken: String? + private(set) var token: Token? nonisolated let persistentContainer: PersistentContainer! @@ -32,7 +32,7 @@ actor FervorController { init(instanceURL: URL, account: LocalData.Account?) async { self.instanceURL = instanceURL - self.client = FervorClient(instanceURL: instanceURL, accessToken: account?.accessToken) + self.client = FervorClient(instanceURL: instanceURL, accessToken: account?.token.accessToken) self.account = account self.clientID = account?.clientID self.clientSecret = account?.clientSecret @@ -63,8 +63,7 @@ actor FervorController { } func getToken(authCode: String) async throws { - let token = try await client.token(authCode: authCode, redirectURI: FervorController.oauthRedirectURI, clientID: clientID!, clientSecret: clientSecret!) - accessToken = token.accessToken + token = try await client.token(authCode: authCode, redirectURI: FervorController.oauthRedirectURI, clientID: clientID!, clientSecret: clientSecret!) } func syncAll() async throws { @@ -93,6 +92,7 @@ actor FervorController { await ExcerptGenerator.generateAll(self) } + @MainActor func syncReadToServer() async { var count = 0 diff --git a/Reader/LocalData.swift b/Reader/LocalData.swift index bf696e4..3069580 100644 --- a/Reader/LocalData.swift +++ b/Reader/LocalData.swift @@ -6,6 +6,8 @@ // import Foundation +import Fervor +import CryptoKit struct LocalData { @@ -28,15 +30,12 @@ struct LocalData { } } - static var mostRecentAccountID: UUID? { + static var mostRecentAccountID: Data? { get { - guard let str = UserDefaults.standard.string(forKey: "mostRecentAccountID") else { - return nil - } - return UUID(uuidString: str) + return UserDefaults.standard.data(forKey: "mostRecentAccountID") } set { - UserDefaults.standard.set(newValue?.uuidString, forKey: "mostRecentAccountID") + UserDefaults.standard.set(newValue, forKey: "mostRecentAccountID") } } @@ -44,23 +43,32 @@ struct LocalData { guard let id = mostRecentAccountID else { return nil } + return account(with: id) + } + + static func account(with id: Data) -> Account? { return accounts.first(where: { $0.id == id }) } struct Account: Codable { - let id: UUID + let id: Data let instanceURL: URL let clientID: String let clientSecret: String - let accessToken: String - // todo: refresh tokens + let token: Token - init(instanceURL: URL, clientID: String, clientSecret: String, accessToken: String) { - self.id = UUID() + init(instanceURL: URL, clientID: String, clientSecret: String, token: Token) { + // we use a hash of instance host and account id rather than random ids so that + // user activites can uniquely identify accounts across devices + var hasher = SHA256() + hasher.update(data: instanceURL.host!.data(using: .utf8)!) + hasher.update(data: token.owner!.data(using: .utf8)!) + self.id = Data(hasher.finalize()) + self.instanceURL = instanceURL self.clientID = clientID self.clientSecret = clientSecret - self.accessToken = accessToken + self.token = token } } diff --git a/Reader/SceneDelegate.swift b/Reader/SceneDelegate.swift index 7f01cce..2621a0f 100644 --- a/Reader/SceneDelegate.swift +++ b/Reader/SceneDelegate.swift @@ -5,7 +5,7 @@ // Created by Shadowfacts on 10/29/21. // -@preconcurrency import Foundation +import Foundation import UIKit import OSLog @@ -33,7 +33,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { loginVC.delegate = self window!.rootViewController = loginVC } else if activity?.activityType == NSUserActivity.activateAccountType, - let account = LocalData.accounts.first(where: { $0.id.uuidString == activity!.userInfo?["accountID"] as? String }) { + let idStr = activity!.userInfo?["accountID"] as? String, + let id = Data(base64Encoded: idStr), + let account = LocalData.account(with: id) { Task { @MainActor in fervorController = await FervorController(account: account) syncFromServer() @@ -144,7 +146,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { extension SceneDelegate: LoginViewControllerDelegate { func didLogin(with controller: FervorController) { Task { @MainActor in - let account = LocalData.Account(instanceURL: controller.instanceURL, clientID: await controller.clientID!, clientSecret: await controller.clientSecret!, accessToken: await controller.accessToken!) + let account = LocalData.Account(instanceURL: controller.instanceURL, clientID: await controller.clientID!, clientSecret: await controller.clientSecret!, token: await controller.token!) LocalData.accounts.append(account) LocalData.mostRecentAccountID = account.id fervorController = await FervorController(account: account)