Use deterministic ids for accounts

This commit is contained in:
Shadowfacts 2022-03-06 15:34:30 -05:00
parent dec7a6e57f
commit 7f5006c629
5 changed files with 44 additions and 21 deletions

View File

@ -7,10 +7,11 @@
import Foundation import Foundation
public struct Token: Decodable { public struct Token: Codable, Sendable {
public let accessToken: String public let accessToken: String
public let expiresIn: Int? public let expiresIn: Int?
public let refreshToken: String? public let refreshToken: String?
public let owner: String?
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) 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.accessToken = try container.decode(String.self, forKey: .accessToken)
self.expiresIn = try container.decodeIfPresent(Int.self, forKey: .expiresIn) self.expiresIn = try container.decodeIfPresent(Int.self, forKey: .expiresIn)
self.refreshToken = try container.decodeIfPresent(String.self, forKey: .refreshToken) 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 { private enum CodingKeys: String, CodingKey {
case accessToken = "access_token" case accessToken = "access_token"
case expiresIn = "expires_in" case expiresIn = "expires_in"
case refreshToken = "refresh_token" case refreshToken = "refresh_token"
case owner
} }
} }

View File

@ -29,7 +29,9 @@ class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentContainer") private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentContainer")
init(account: LocalData.Account) { 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 loadPersistentStores { description, error in
if let error = error { if let error = error {

View File

@ -22,7 +22,7 @@ actor FervorController {
nonisolated let account: LocalData.Account? nonisolated let account: LocalData.Account?
private(set) var clientID: String? private(set) var clientID: String?
private(set) var clientSecret: String? private(set) var clientSecret: String?
private(set) var accessToken: String? private(set) var token: Token?
nonisolated let persistentContainer: PersistentContainer! nonisolated let persistentContainer: PersistentContainer!
@ -32,7 +32,7 @@ actor FervorController {
init(instanceURL: URL, account: LocalData.Account?) async { init(instanceURL: URL, account: LocalData.Account?) async {
self.instanceURL = instanceURL self.instanceURL = instanceURL
self.client = FervorClient(instanceURL: instanceURL, accessToken: account?.accessToken) self.client = FervorClient(instanceURL: instanceURL, accessToken: account?.token.accessToken)
self.account = account self.account = account
self.clientID = account?.clientID self.clientID = account?.clientID
self.clientSecret = account?.clientSecret self.clientSecret = account?.clientSecret
@ -63,8 +63,7 @@ actor FervorController {
} }
func getToken(authCode: String) async throws { func getToken(authCode: String) async throws {
let token = try await client.token(authCode: authCode, redirectURI: FervorController.oauthRedirectURI, clientID: clientID!, clientSecret: clientSecret!) token = try await client.token(authCode: authCode, redirectURI: FervorController.oauthRedirectURI, clientID: clientID!, clientSecret: clientSecret!)
accessToken = token.accessToken
} }
func syncAll() async throws { func syncAll() async throws {
@ -93,6 +92,7 @@ actor FervorController {
await ExcerptGenerator.generateAll(self) await ExcerptGenerator.generateAll(self)
} }
@MainActor
func syncReadToServer() async { func syncReadToServer() async {
var count = 0 var count = 0

View File

@ -6,6 +6,8 @@
// //
import Foundation import Foundation
import Fervor
import CryptoKit
struct LocalData { struct LocalData {
@ -28,15 +30,12 @@ struct LocalData {
} }
} }
static var mostRecentAccountID: UUID? { static var mostRecentAccountID: Data? {
get { get {
guard let str = UserDefaults.standard.string(forKey: "mostRecentAccountID") else { return UserDefaults.standard.data(forKey: "mostRecentAccountID")
return nil
}
return UUID(uuidString: str)
} }
set { 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 { guard let id = mostRecentAccountID else {
return nil return nil
} }
return account(with: id)
}
static func account(with id: Data) -> Account? {
return accounts.first(where: { $0.id == id }) return accounts.first(where: { $0.id == id })
} }
struct Account: Codable { struct Account: Codable {
let id: UUID let id: Data
let instanceURL: URL let instanceURL: URL
let clientID: String let clientID: String
let clientSecret: String let clientSecret: String
let accessToken: String let token: Token
// todo: refresh tokens
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())
init(instanceURL: URL, clientID: String, clientSecret: String, accessToken: String) {
self.id = UUID()
self.instanceURL = instanceURL self.instanceURL = instanceURL
self.clientID = clientID self.clientID = clientID
self.clientSecret = clientSecret self.clientSecret = clientSecret
self.accessToken = accessToken self.token = token
} }
} }

View File

@ -5,7 +5,7 @@
// Created by Shadowfacts on 10/29/21. // Created by Shadowfacts on 10/29/21.
// //
@preconcurrency import Foundation import Foundation
import UIKit import UIKit
import OSLog import OSLog
@ -33,7 +33,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
loginVC.delegate = self loginVC.delegate = self
window!.rootViewController = loginVC window!.rootViewController = loginVC
} else if activity?.activityType == NSUserActivity.activateAccountType, } 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 Task { @MainActor in
fervorController = await FervorController(account: account) fervorController = await FervorController(account: account)
syncFromServer() syncFromServer()
@ -144,7 +146,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
extension SceneDelegate: LoginViewControllerDelegate { extension SceneDelegate: LoginViewControllerDelegate {
func didLogin(with controller: FervorController) { func didLogin(with controller: FervorController) {
Task { @MainActor in 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.accounts.append(account)
LocalData.mostRecentAccountID = account.id LocalData.mostRecentAccountID = account.id
fervorController = await FervorController(account: account) fervorController = await FervorController(account: account)