From 973fbb4099590682b8f7a0a4a1ffeb16db18bbfa Mon Sep 17 00:00:00 2001 From: kingbri Date: Sat, 8 Jun 2024 01:09:18 -0400 Subject: [PATCH] Debrid: Migrate auth to protocol Unify authentication to the new protocol. Also remove logout on invalid requests. This became annoying and didn't update the UI properly. Signed-off-by: kingbri --- Ferrite/API/AllDebridWrapper.swift | 13 +- Ferrite/API/PremiumizeWrapper.swift | 15 +- Ferrite/API/RealDebridWrapper.swift | 15 +- Ferrite/Protocols/Debrid.swift | 5 +- Ferrite/ViewModels/DebridManager.swift | 220 ++++++------------ .../Library/DebridCloudView.swift | 2 +- .../Settings/SettingsDebridInfoView.swift | 12 +- Ferrite/Views/SettingsView.swift | 2 +- 8 files changed, 118 insertions(+), 166 deletions(-) diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index cc0abb2..eaa9133 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -19,6 +19,14 @@ public class AllDebrid: PollingDebridSource, ObservableObject { getToken() != nil } + public var manualToken: String? { + if UserDefaults.standard.bool(forKey: "AllDebrid.UseManualKey") { + return getToken() + } else { + return nil + } + } + @Published public var IAValues: [DebridIA] = [] @Published public var cloudDownloads: [DebridCloudDownload] = [] @Published public var cloudTorrents: [DebridCloudTorrent] = [] @@ -101,11 +109,9 @@ public class AllDebrid: PollingDebridSource, ObservableObject { } // Adds a manual API key instead of web auth - public func setApiKey(_ key: String) -> Bool { + public func setApiKey(_ key: String) { FerriteKeychain.shared.set(key, forKey: "AllDebrid.ApiKey") UserDefaults.standard.set(true, forKey: "AllDebrid.UseManualKey") - - return FerriteKeychain.shared.get("AllDebrid.ApiKey") == key } public func getToken() -> String? { @@ -137,7 +143,6 @@ public class AllDebrid: PollingDebridSource, ObservableObject { if response.statusCode >= 200, response.statusCode <= 299 { return data } else if response.statusCode == 401 { - logout() throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to AllDebrid in Settings.") } else { throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 0909fb3..b45f8f2 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -13,7 +13,15 @@ public class Premiumize: OAuthDebridSource, ObservableObject { public let website = "https://premiumize.me" @Published public var authProcessing: Bool = false public var isLoggedIn: Bool { - getToken() != nil + return getToken() != nil + } + + public var manualToken: String? { + if UserDefaults.standard.bool(forKey: "Premiumize.UseManualKey") { + return getToken() + } else { + return nil + } } @Published public var IAValues: [DebridIA] = [] @@ -62,11 +70,9 @@ public class Premiumize: OAuthDebridSource, ObservableObject { } // Adds a manual API key instead of web auth - public func setApiKey(_ key: String) -> Bool { + public func setApiKey(_ key: String) { FerriteKeychain.shared.set(key, forKey: "Premiumize.AccessToken") UserDefaults.standard.set(true, forKey: "Premiumize.UseManualKey") - - return FerriteKeychain.shared.get("Premiumize.AccessToken") == key } public func getToken() -> String? { @@ -118,7 +124,6 @@ public class Premiumize: OAuthDebridSource, ObservableObject { if response.statusCode >= 200, response.statusCode <= 299 { return data } else if response.statusCode == 401 { - logout() throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.") } else { throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index ea6aab7..cb1dd13 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -15,11 +15,19 @@ public class RealDebrid: PollingDebridSource, ObservableObject { @Published public var authProcessing: Bool = false - // Directly checked because the request fetch uses async + // Check the manual token since getTokens() is async public var isLoggedIn: Bool { FerriteKeychain.shared.get("RealDebrid.AccessToken") != nil } + public var manualToken: String? { + if UserDefaults.standard.bool(forKey: "RealDebrid.UseManualKey") { + return FerriteKeychain.shared.get("RealDebrid.AccessToken") + } else { + return nil + } + } + @Published public var IAValues: [DebridIA] = [] @Published public var cloudDownloads: [DebridCloudDownload] = [] @Published public var cloudTorrents: [DebridCloudTorrent] = [] @@ -175,14 +183,12 @@ public class RealDebrid: PollingDebridSource, ObservableObject { // Adds a manual API key instead of web auth // Clear out existing refresh tokens and timestamps - public func setApiKey(_ key: String) -> Bool { + public func setApiKey(_ key: String) { FerriteKeychain.shared.set(key, forKey: "RealDebrid.AccessToken") FerriteKeychain.shared.delete("RealDebrid.RefreshToken") FerriteKeychain.shared.delete("RealDebrid.AccessTokenStamp") UserDefaults.standard.set(true, forKey: "RealDebrid.UseManualKey") - - return FerriteKeychain.shared.get("RealDebrid.AccessToken") == key } // Deletes tokens from device and RD's servers @@ -222,7 +228,6 @@ public class RealDebrid: PollingDebridSource, ObservableObject { if response.statusCode >= 200, response.statusCode <= 299 { return data } else if response.statusCode == 401 { - await logout() throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to RealDebrid in Settings.") } else { throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index eef1a33..75a84d6 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -18,8 +18,11 @@ public protocol DebridSource: AnyObservableObject { var authProcessing: Bool { get set } var isLoggedIn: Bool { get } + // Manual API key + var manualToken: String? { get } + // Common authentication functions - func setApiKey(_ key: String) -> Bool + func setApiKey(_ key: String) func logout() async // Instant availability variables diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index ede4ffd..3675c26 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -26,6 +26,10 @@ public class DebridManager: ObservableObject { debridSources.contains { $0.isLoggedIn } } + var enabledDebridCount: Int { + debridSources.filter{ $0.isLoggedIn }.count + } + @Published var selectedDebridSource: DebridSource? { didSet { UserDefaults.standard.set(selectedDebridSource?.id ?? "", forKey: "Debrid.PreferredService") @@ -34,18 +38,8 @@ public class DebridManager: ObservableObject { var selectedDebridItem: DebridIA? var selectedDebridFile: DebridIAFile? - // Service agnostic variables - @Published var enabledDebrids: Set = [] { - didSet { - UserDefaults.standard.set(enabledDebrids.rawValue, forKey: "Debrid.EnabledArray") - } - } - - @Published var selectedDebridType: DebridType? { - didSet { - UserDefaults.standard.set(selectedDebridType?.rawValue ?? 0, forKey: "Debrid.PreferredService") - } - } + // TODO: Figure out a way to remove this var + var selectedOAuthDebridSource: OAuthDebridSource? @Published var filteredIAStatus: Set = [] @@ -53,22 +47,6 @@ public class DebridManager: ObservableObject { var downloadUrl: String = "" var authUrl: URL? - // Is the current debrid type processing an auth request - func authProcessing(_ passedDebridType: DebridType?) -> Bool { - guard let debridType = passedDebridType ?? selectedDebridType else { - return false - } - - switch debridType { - case .realDebrid: - return realDebridAuthProcessing - case .allDebrid: - return allDebridAuthProcessing - case .premiumize: - return premiumizeAuthProcessing - } - } - // RealDebrid auth variables var realDebridAuthProcessing: Bool = false @@ -89,7 +67,6 @@ public class DebridManager: ObservableObject { if let preferredServiceInt = Int(rawPreferredService) { debridServiceId = migratePreferredService(preferredServiceInt) } else { - print(rawPreferredService) debridServiceId = rawPreferredService } @@ -207,73 +184,62 @@ public class DebridManager: ObservableObject { // MARK: - Authentication UI linked functions // Common function to delegate what debrid service to authenticate with - public func authenticateDebrid(debridType: DebridType, apiKey: String?) async { - switch debridType { - case .realDebrid: - let success = apiKey == nil ? await authenticateRd() : realDebrid.setApiKey(apiKey!) - completeDebridAuth(debridType, success: success) - case .allDebrid: - // Async can't work with nil mapping method - let success = apiKey == nil ? await authenticateAd() : allDebrid.setApiKey(apiKey!) - completeDebridAuth(debridType, success: success) - case .premiumize: - if let apiKey { - let success = premiumize.setApiKey(apiKey) - completeDebridAuth(debridType, success: success) - } else { - await authenticatePm() + public func authenticateDebrid(_ debridSource: some DebridSource, apiKey: String?) async { + defer { + // Don't cancel processing if using OAuth + if !(debridSource is OAuthDebridSource) { + debridSource.authProcessing = false } - } - } - // Callback to finish debrid auth since functions can be split - func completeDebridAuth(_ debridType: DebridType, success: Bool) { - if success { - enabledDebrids.insert(debridType) - if enabledDebrids.count == 1 { - selectedDebridType = enabledDebrids.first + if enabledDebridCount == 1 { + selectedDebridSource = debridSource } } - switch debridType { - case .realDebrid: - realDebridAuthProcessing = false - case .allDebrid: - allDebridAuthProcessing = false - case .premiumize: - premiumizeAuthProcessing = false + // Set an API key if manually provided + if let apiKey { + debridSource.setApiKey(apiKey) + return + } + + // Processing has started + debridSource.authProcessing = true + + if let pollingSource = debridSource as? PollingDebridSource { + do { + let authUrl = try await pollingSource.getAuthUrl() + + if validateAuthUrl(authUrl) { + try await pollingSource.authTask?.value + } else { + throw DebridError.AuthQuery(description: "The authentication URL was invalid") + } + } catch { + await sendDebridError(error, prefix: "\(debridSource.id) authentication error") + + pollingSource.authTask?.cancel() + } + } else if let oauthSource = debridSource as? OAuthDebridSource { + do { + let tempAuthUrl = try oauthSource.getAuthUrl() + selectedOAuthDebridSource = oauthSource + + validateAuthUrl(tempAuthUrl, useAuthSession: true) + } catch { + await sendDebridError(error, prefix: "\(debridSource.id) authentication error") + } + } else { + logManager?.error( + "DebridManager: Auth: Could not figure out the authentication type for \(debridSource.id). Is this configured properly?" + ) + + return } } // Get a truncated manual API key if it's being used - func getManualAuthKey(_ passedDebridType: DebridType?) async -> String? { - guard let debridType = passedDebridType ?? selectedDebridType else { - return nil - } - - let debridToken: String? - switch debridType { - case .realDebrid: - if UserDefaults.standard.bool(forKey: "RealDebrid.UseManualKey") { - debridToken = FerriteKeychain.shared.get("RealDebrid.AccessToken") - } else { - debridToken = nil - } - case .allDebrid: - if UserDefaults.standard.bool(forKey: "AllDebrid.UseManualKey") { - debridToken = FerriteKeychain.shared.get("AllDebrid.ApiKey") - } else { - debridToken = nil - } - case .premiumize: - if UserDefaults.standard.bool(forKey: "Premiumize.UseManualKey") { - debridToken = FerriteKeychain.shared.get("Premiumize.AccessToken") - } else { - debridToken = nil - } - } - - if let debridToken { + func getManualAuthKey(_ debridSource: some DebridSource) async -> String? { + if let debridToken = debridSource.manualToken { let splitString = debridToken.suffix(4) if debridToken.count > 4 { @@ -303,74 +269,42 @@ public class DebridManager: ObservableObject { return true } - private func authenticateRd() async -> Bool { - do { - realDebridAuthProcessing = true - let authUrl = try await realDebrid.getAuthUrl() - - if validateAuthUrl(authUrl) { - try await realDebrid.authTask?.value - return true - } else { - throw DebridError.AuthQuery(description: "The verification URL was invalid") - } - } catch { - await sendDebridError(error, prefix: "RealDebrid authentication error") - - realDebrid.authTask?.cancel() - return false - } - } - - private func authenticateAd() async -> Bool { - do { - allDebridAuthProcessing = true - let authUrl = try await allDebrid.getAuthUrl() - - if validateAuthUrl(authUrl) { - try await allDebrid.authTask?.value - return true - } else { - throw DebridError.AuthQuery(description: "The PIN URL was invalid") - } - } catch { - await sendDebridError(error, prefix: "AllDebrid authentication error") - - allDebrid.authTask?.cancel() - return false - } - } - - private func authenticatePm() async { - do { - premiumizeAuthProcessing = true - let tempAuthUrl = try premiumize.getAuthUrl() - - validateAuthUrl(tempAuthUrl, useAuthSession: true) - } catch { - await sendDebridError(error, prefix: "Premiumize authentication error") - - completeDebridAuth(.premiumize, success: false) - } - } - // Currently handles Premiumize callback - public func handleCallback(url: URL?, error: Error?) async { + public func handleAuthCallback(url: URL?, error: Error?) async { + defer { + if enabledDebridCount == 1 { + selectedDebridSource = selectedOAuthDebridSource + } + + selectedOAuthDebridSource?.authProcessing = false + } + do { + guard let oauthDebridSource = selectedOAuthDebridSource else { + throw DebridError.AuthQuery(description: "OAuth source couldn't be found for callback. Aborting.") + } + if let error { throw DebridError.AuthQuery(description: "OAuth callback Error: \(error)") - } + } if let callbackUrl = url { - try premiumize.handleAuthCallback(url: callbackUrl) - completeDebridAuth(.premiumize, success: true) + try oauthDebridSource.handleAuthCallback(url: callbackUrl) } else { throw DebridError.AuthQuery(description: "The callback URL was invalid") } } catch { await sendDebridError(error, prefix: "Premiumize authentication error (callback)") + } + } - completeDebridAuth(.premiumize, success: false) + // MARK: - Logout UI functions + + public func logout(_ debridSource: some DebridSource) async { + await debridSource.logout() + + if selectedDebridSource?.id == debridSource.id { + selectedDebridSource = nil } } diff --git a/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift index a52120a..9a612a8 100644 --- a/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift @@ -31,7 +31,7 @@ struct DebridCloudView: View { .refreshable { await debridManager.fetchDebridCloud(bypassTTL: true) } - .onChange(of: debridManager.selectedDebridType) { newType in + .onChange(of: debridManager.selectedDebridSource?.id) { newType in if newType != nil { Task { await debridManager.fetchDebridCloud() diff --git a/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift index 47d81c9..001cb3a 100644 --- a/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift +++ b/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift @@ -31,12 +31,12 @@ struct SettingsDebridInfoView: View { Button { Task { if debridSource.isLoggedIn { - await debridSource.logout() + await debridManager.logout(debridSource) } else if !debridSource.authProcessing { - //await debridManager.authenticateDebrid(debridType: debridType, apiKey: nil) + await debridManager.authenticateDebrid(debridSource, apiKey: nil) } - //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" + apiKeyTempText = await debridManager.getManualAuthKey(debridSource) ?? "" } } label: { Text( @@ -57,8 +57,8 @@ struct SettingsDebridInfoView: View { onCommit: { Task { if !apiKeyTempText.isEmpty { - //await debridManager.authenticateDebrid(debridType: debridType, apiKey: apiKeyTempText) - //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" + await debridManager.authenticateDebrid(debridSource, apiKey: apiKeyTempText) + apiKeyTempText = await debridManager.getManualAuthKey(debridSource) ?? "" } } } @@ -67,7 +67,7 @@ struct SettingsDebridInfoView: View { } .onAppear { Task { - //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" + apiKeyTempText = await debridManager.getManualAuthKey(debridSource) ?? "" } } } diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 4171c96..3c8ffcd 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -227,7 +227,7 @@ struct SettingsView: View { callbackURLScheme: "ferrite" ) { callbackURL, error in Task { - await debridManager.handleCallback(url: callbackURL, error: error) + await debridManager.handleAuthCallback(url: callbackURL, error: error) } } .prefersEphemeralWebBrowserSession(useEphemeralAuth)