2023-05-25 16:26:17 +02:00
|
|
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import Combine
|
|
|
|
import CoreDataStack
|
|
|
|
import MastodonSDK
|
2023-05-26 11:36:08 +02:00
|
|
|
import KeychainAccess
|
|
|
|
import MastodonCommon
|
2023-06-05 14:02:44 +02:00
|
|
|
import os.log
|
2023-05-25 16:26:17 +02:00
|
|
|
|
|
|
|
public class AuthenticationServiceProvider: ObservableObject {
|
2023-06-05 14:02:44 +02:00
|
|
|
private let logger = Logger(subsystem: "AuthenticationServiceProvider", category: "Authentication")
|
|
|
|
|
2023-05-25 16:26:17 +02:00
|
|
|
public static let shared = AuthenticationServiceProvider()
|
2023-05-26 11:36:08 +02:00
|
|
|
private static let keychain = Keychain(service: "org.joinmastodon.app.authentications", accessGroup: AppName.groupID)
|
2023-06-05 14:02:44 +02:00
|
|
|
private let userDefaults: UserDefaults = .shared
|
2023-05-26 11:36:08 +02:00
|
|
|
|
2023-05-25 16:26:17 +02:00
|
|
|
private init() {}
|
|
|
|
|
2023-05-26 11:36:08 +02:00
|
|
|
@Published public var authentications: [MastodonAuthentication] = [] {
|
|
|
|
didSet {
|
|
|
|
persist() // todo: Is this too heavy and too often here???
|
|
|
|
}
|
|
|
|
}
|
2023-05-25 16:26:17 +02:00
|
|
|
|
|
|
|
func update(instance: Instance, where domain: String) {
|
|
|
|
authentications = authentications.map { authentication in
|
|
|
|
guard authentication.domain == domain else { return authentication }
|
|
|
|
return authentication.updating(instance: instance)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func delete(authentication: MastodonAuthentication) {
|
|
|
|
authentications.removeAll(where: { $0 == authentication })
|
|
|
|
}
|
|
|
|
|
|
|
|
func activateAuthentication(in domain: String, for userID: String) {
|
|
|
|
authentications = authentications.map { authentication in
|
|
|
|
guard authentication.domain == domain, authentication.userID == userID else {
|
|
|
|
return authentication
|
|
|
|
}
|
|
|
|
return authentication.updating(activatedAt: Date())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func getAuthentication(in domain: String, for userID: String) -> MastodonAuthentication? {
|
|
|
|
authentications.first(where: { $0.domain == domain && $0.userID == userID })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Public
|
|
|
|
public extension AuthenticationServiceProvider {
|
|
|
|
func getAuthentication(matching userAccessToken: String) -> MastodonAuthentication? {
|
|
|
|
authentications.first(where: { $0.userAccessToken == userAccessToken })
|
|
|
|
}
|
|
|
|
|
2023-06-02 11:10:29 +02:00
|
|
|
func authenticationSortedByActivation() -> [MastodonAuthentication] { // fixme: why do we need this?
|
|
|
|
return authentications.sorted(by: { $0.activedAt > $1.activedAt })
|
2023-05-25 16:26:17 +02:00
|
|
|
}
|
2023-05-26 11:36:08 +02:00
|
|
|
|
|
|
|
func restore() {
|
|
|
|
authentications = Self.keychain.allKeys().compactMap {
|
|
|
|
guard
|
|
|
|
let encoded = Self.keychain[$0],
|
|
|
|
let data = Data(base64Encoded: encoded)
|
|
|
|
else { return nil }
|
|
|
|
return try? JSONDecoder().decode(MastodonAuthentication.self, from: data)
|
|
|
|
}
|
|
|
|
}
|
2023-06-05 14:02:44 +02:00
|
|
|
|
2023-06-13 13:05:05 +02:00
|
|
|
func migrateLegacyAuthentications(in context: NSManagedObjectContext) {
|
2023-06-05 14:02:44 +02:00
|
|
|
do {
|
2023-06-05 15:53:27 +02:00
|
|
|
let request = NSFetchRequest<NSManagedObject>(entityName: "MastodonAuthentication")
|
2023-06-05 14:02:44 +02:00
|
|
|
let legacyAuthentications = try context.fetch(request)
|
|
|
|
|
2023-06-13 13:05:05 +02:00
|
|
|
let migratedAuthentications = legacyAuthentications.compactMap { auth -> MastodonAuthentication? in
|
2023-06-05 15:53:27 +02:00
|
|
|
guard
|
|
|
|
let identifier = auth.value(forKey: "identifier") as? UUID,
|
|
|
|
let domain = auth.value(forKey: "domain") as? String,
|
|
|
|
let username = auth.value(forKey: "username") as? String,
|
|
|
|
let appAccessToken = auth.value(forKey: "appAccessToken") as? String,
|
|
|
|
let userAccessToken = auth.value(forKey: "userAccessToken") as? String,
|
|
|
|
let clientID = auth.value(forKey: "clientID") as? String,
|
|
|
|
let clientSecret = auth.value(forKey: "clientSecret") as? String,
|
|
|
|
let createdAt = auth.value(forKey: "createdAt") as? Date,
|
|
|
|
let updatedAt = auth.value(forKey: "updatedAt") as? Date,
|
|
|
|
let activedAt = auth.value(forKey: "activedAt") as? Date,
|
|
|
|
let userID = auth.value(forKey: "userID") as? String
|
|
|
|
|
|
|
|
else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return MastodonAuthentication(
|
|
|
|
identifier: identifier,
|
|
|
|
domain: domain,
|
|
|
|
username: username,
|
|
|
|
appAccessToken: appAccessToken,
|
|
|
|
userAccessToken: userAccessToken,
|
|
|
|
clientID: clientID,
|
|
|
|
clientSecret: clientSecret,
|
|
|
|
createdAt: createdAt,
|
|
|
|
updatedAt: updatedAt,
|
|
|
|
activedAt: activedAt,
|
|
|
|
userID: userID
|
2023-06-05 14:02:44 +02:00
|
|
|
)
|
|
|
|
}
|
2023-06-13 13:05:05 +02:00
|
|
|
|
|
|
|
if migratedAuthentications.count != legacyAuthentications.count {
|
|
|
|
logger.log(level: .default, "Not all mitgrations could be migrated.")
|
|
|
|
}
|
|
|
|
|
|
|
|
self.authentications = migratedAuthentications
|
|
|
|
userDefaults.didMigrateAuthentications = true
|
2023-06-05 14:02:44 +02:00
|
|
|
} catch {
|
2023-06-13 13:05:05 +02:00
|
|
|
userDefaults.didMigrateAuthentications = false
|
2023-06-05 14:02:44 +02:00
|
|
|
logger.log(level: .error, "Could not migrate legacy authentications")
|
|
|
|
}
|
|
|
|
}
|
2023-06-05 17:21:32 +02:00
|
|
|
|
|
|
|
var authenticationMigrationRequired: Bool {
|
2023-06-13 13:05:05 +02:00
|
|
|
userDefaults.didMigrateAuthentications == false
|
2023-06-05 17:21:32 +02:00
|
|
|
}
|
2023-05-26 11:36:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Private
|
|
|
|
private extension AuthenticationServiceProvider {
|
|
|
|
func persist() {
|
|
|
|
for authentication in authentications {
|
|
|
|
Self.keychain[authentication.persistenceIdentifier] = try? JSONEncoder().encode(authentication).base64EncodedString()
|
|
|
|
}
|
|
|
|
}
|
2023-05-25 16:26:17 +02:00
|
|
|
}
|