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 <bdashore3@proton.me>
This commit is contained in:
kingbri 2024-06-08 01:09:18 -04:00 committed by Brian Dashore
parent eeb9cbdf65
commit 5b4cef7ef0
8 changed files with 118 additions and 166 deletions

View file

@ -19,6 +19,14 @@ public class AllDebrid: PollingDebridSource, ObservableObject {
getToken() != nil 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 IAValues: [DebridIA] = []
@Published public var cloudDownloads: [DebridCloudDownload] = [] @Published public var cloudDownloads: [DebridCloudDownload] = []
@Published public var cloudTorrents: [DebridCloudTorrent] = [] @Published public var cloudTorrents: [DebridCloudTorrent] = []
@ -101,11 +109,9 @@ public class AllDebrid: PollingDebridSource, ObservableObject {
} }
// Adds a manual API key instead of web auth // 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") FerriteKeychain.shared.set(key, forKey: "AllDebrid.ApiKey")
UserDefaults.standard.set(true, forKey: "AllDebrid.UseManualKey") UserDefaults.standard.set(true, forKey: "AllDebrid.UseManualKey")
return FerriteKeychain.shared.get("AllDebrid.ApiKey") == key
} }
public func getToken() -> String? { public func getToken() -> String? {
@ -137,7 +143,6 @@ public class AllDebrid: PollingDebridSource, ObservableObject {
if response.statusCode >= 200, response.statusCode <= 299 { if response.statusCode >= 200, response.statusCode <= 299 {
return data return data
} else if response.statusCode == 401 { } else if response.statusCode == 401 {
logout()
throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to AllDebrid in Settings.") throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to AllDebrid in Settings.")
} else { } else {
throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")

View file

@ -13,7 +13,15 @@ public class Premiumize: OAuthDebridSource, ObservableObject {
public let website = "https://premiumize.me" public let website = "https://premiumize.me"
@Published public var authProcessing: Bool = false @Published public var authProcessing: Bool = false
public var isLoggedIn: Bool { 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] = [] @Published public var IAValues: [DebridIA] = []
@ -62,11 +70,9 @@ public class Premiumize: OAuthDebridSource, ObservableObject {
} }
// Adds a manual API key instead of web auth // 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") FerriteKeychain.shared.set(key, forKey: "Premiumize.AccessToken")
UserDefaults.standard.set(true, forKey: "Premiumize.UseManualKey") UserDefaults.standard.set(true, forKey: "Premiumize.UseManualKey")
return FerriteKeychain.shared.get("Premiumize.AccessToken") == key
} }
public func getToken() -> String? { public func getToken() -> String? {
@ -118,7 +124,6 @@ public class Premiumize: OAuthDebridSource, ObservableObject {
if response.statusCode >= 200, response.statusCode <= 299 { if response.statusCode >= 200, response.statusCode <= 299 {
return data return data
} else if response.statusCode == 401 { } else if response.statusCode == 401 {
logout()
throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.") throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.")
} else { } else {
throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")

View file

@ -15,11 +15,19 @@ public class RealDebrid: PollingDebridSource, ObservableObject {
@Published public var authProcessing: Bool = false @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 { public var isLoggedIn: Bool {
FerriteKeychain.shared.get("RealDebrid.AccessToken") != nil 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 IAValues: [DebridIA] = []
@Published public var cloudDownloads: [DebridCloudDownload] = [] @Published public var cloudDownloads: [DebridCloudDownload] = []
@Published public var cloudTorrents: [DebridCloudTorrent] = [] @Published public var cloudTorrents: [DebridCloudTorrent] = []
@ -175,14 +183,12 @@ public class RealDebrid: PollingDebridSource, ObservableObject {
// Adds a manual API key instead of web auth // Adds a manual API key instead of web auth
// Clear out existing refresh tokens and timestamps // 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.set(key, forKey: "RealDebrid.AccessToken")
FerriteKeychain.shared.delete("RealDebrid.RefreshToken") FerriteKeychain.shared.delete("RealDebrid.RefreshToken")
FerriteKeychain.shared.delete("RealDebrid.AccessTokenStamp") FerriteKeychain.shared.delete("RealDebrid.AccessTokenStamp")
UserDefaults.standard.set(true, forKey: "RealDebrid.UseManualKey") UserDefaults.standard.set(true, forKey: "RealDebrid.UseManualKey")
return FerriteKeychain.shared.get("RealDebrid.AccessToken") == key
} }
// Deletes tokens from device and RD's servers // Deletes tokens from device and RD's servers
@ -222,7 +228,6 @@ public class RealDebrid: PollingDebridSource, ObservableObject {
if response.statusCode >= 200, response.statusCode <= 299 { if response.statusCode >= 200, response.statusCode <= 299 {
return data return data
} else if response.statusCode == 401 { } 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.") throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to RealDebrid in Settings.")
} else { } else {
throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")

View file

@ -18,8 +18,11 @@ public protocol DebridSource: AnyObservableObject {
var authProcessing: Bool { get set } var authProcessing: Bool { get set }
var isLoggedIn: Bool { get } var isLoggedIn: Bool { get }
// Manual API key
var manualToken: String? { get }
// Common authentication functions // Common authentication functions
func setApiKey(_ key: String) -> Bool func setApiKey(_ key: String)
func logout() async func logout() async
// Instant availability variables // Instant availability variables

View file

@ -26,6 +26,10 @@ public class DebridManager: ObservableObject {
debridSources.contains { $0.isLoggedIn } debridSources.contains { $0.isLoggedIn }
} }
var enabledDebridCount: Int {
debridSources.filter{ $0.isLoggedIn }.count
}
@Published var selectedDebridSource: DebridSource? { @Published var selectedDebridSource: DebridSource? {
didSet { didSet {
UserDefaults.standard.set(selectedDebridSource?.id ?? "", forKey: "Debrid.PreferredService") UserDefaults.standard.set(selectedDebridSource?.id ?? "", forKey: "Debrid.PreferredService")
@ -34,18 +38,8 @@ public class DebridManager: ObservableObject {
var selectedDebridItem: DebridIA? var selectedDebridItem: DebridIA?
var selectedDebridFile: DebridIAFile? var selectedDebridFile: DebridIAFile?
// Service agnostic variables // TODO: Figure out a way to remove this var
@Published var enabledDebrids: Set<DebridType> = [] { var selectedOAuthDebridSource: OAuthDebridSource?
didSet {
UserDefaults.standard.set(enabledDebrids.rawValue, forKey: "Debrid.EnabledArray")
}
}
@Published var selectedDebridType: DebridType? {
didSet {
UserDefaults.standard.set(selectedDebridType?.rawValue ?? 0, forKey: "Debrid.PreferredService")
}
}
@Published var filteredIAStatus: Set<IAStatus> = [] @Published var filteredIAStatus: Set<IAStatus> = []
@ -53,22 +47,6 @@ public class DebridManager: ObservableObject {
var downloadUrl: String = "" var downloadUrl: String = ""
var authUrl: URL? 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 // RealDebrid auth variables
var realDebridAuthProcessing: Bool = false var realDebridAuthProcessing: Bool = false
@ -89,7 +67,6 @@ public class DebridManager: ObservableObject {
if let preferredServiceInt = Int(rawPreferredService) { if let preferredServiceInt = Int(rawPreferredService) {
debridServiceId = migratePreferredService(preferredServiceInt) debridServiceId = migratePreferredService(preferredServiceInt)
} else { } else {
print(rawPreferredService)
debridServiceId = rawPreferredService debridServiceId = rawPreferredService
} }
@ -207,73 +184,62 @@ public class DebridManager: ObservableObject {
// MARK: - Authentication UI linked functions // MARK: - Authentication UI linked functions
// Common function to delegate what debrid service to authenticate with // Common function to delegate what debrid service to authenticate with
public func authenticateDebrid(debridType: DebridType, apiKey: String?) async { public func authenticateDebrid(_ debridSource: some DebridSource, apiKey: String?) async {
switch debridType { defer {
case .realDebrid: // Don't cancel processing if using OAuth
let success = apiKey == nil ? await authenticateRd() : realDebrid.setApiKey(apiKey!) if !(debridSource is OAuthDebridSource) {
completeDebridAuth(debridType, success: success) debridSource.authProcessing = false
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()
} }
}
}
// Callback to finish debrid auth since functions can be split if enabledDebridCount == 1 {
func completeDebridAuth(_ debridType: DebridType, success: Bool) { selectedDebridSource = debridSource
if success {
enabledDebrids.insert(debridType)
if enabledDebrids.count == 1 {
selectedDebridType = enabledDebrids.first
} }
} }
switch debridType { // Set an API key if manually provided
case .realDebrid: if let apiKey {
realDebridAuthProcessing = false debridSource.setApiKey(apiKey)
case .allDebrid: return
allDebridAuthProcessing = false }
case .premiumize:
premiumizeAuthProcessing = false // 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 // Get a truncated manual API key if it's being used
func getManualAuthKey(_ passedDebridType: DebridType?) async -> String? { func getManualAuthKey(_ debridSource: some DebridSource) async -> String? {
guard let debridType = passedDebridType ?? selectedDebridType else { if let debridToken = debridSource.manualToken {
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 {
let splitString = debridToken.suffix(4) let splitString = debridToken.suffix(4)
if debridToken.count > 4 { if debridToken.count > 4 {
@ -303,74 +269,42 @@ public class DebridManager: ObservableObject {
return true 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 // 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 { do {
guard let oauthDebridSource = selectedOAuthDebridSource else {
throw DebridError.AuthQuery(description: "OAuth source couldn't be found for callback. Aborting.")
}
if let error { if let error {
throw DebridError.AuthQuery(description: "OAuth callback Error: \(error)") throw DebridError.AuthQuery(description: "OAuth callback Error: \(error)")
} }
if let callbackUrl = url { if let callbackUrl = url {
try premiumize.handleAuthCallback(url: callbackUrl) try oauthDebridSource.handleAuthCallback(url: callbackUrl)
completeDebridAuth(.premiumize, success: true)
} else { } else {
throw DebridError.AuthQuery(description: "The callback URL was invalid") throw DebridError.AuthQuery(description: "The callback URL was invalid")
} }
} catch { } catch {
await sendDebridError(error, prefix: "Premiumize authentication error (callback)") 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
} }
} }

View file

@ -31,7 +31,7 @@ struct DebridCloudView: View {
.refreshable { .refreshable {
await debridManager.fetchDebridCloud(bypassTTL: true) await debridManager.fetchDebridCloud(bypassTTL: true)
} }
.onChange(of: debridManager.selectedDebridType) { newType in .onChange(of: debridManager.selectedDebridSource?.id) { newType in
if newType != nil { if newType != nil {
Task { Task {
await debridManager.fetchDebridCloud() await debridManager.fetchDebridCloud()

View file

@ -31,12 +31,12 @@ struct SettingsDebridInfoView: View {
Button { Button {
Task { Task {
if debridSource.isLoggedIn { if debridSource.isLoggedIn {
await debridSource.logout() await debridManager.logout(debridSource)
} else if !debridSource.authProcessing { } 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: { } label: {
Text( Text(
@ -57,8 +57,8 @@ struct SettingsDebridInfoView: View {
onCommit: { onCommit: {
Task { Task {
if !apiKeyTempText.isEmpty { if !apiKeyTempText.isEmpty {
//await debridManager.authenticateDebrid(debridType: debridType, apiKey: apiKeyTempText) await debridManager.authenticateDebrid(debridSource, apiKey: apiKeyTempText)
//apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" apiKeyTempText = await debridManager.getManualAuthKey(debridSource) ?? ""
} }
} }
} }
@ -67,7 +67,7 @@ struct SettingsDebridInfoView: View {
} }
.onAppear { .onAppear {
Task { Task {
//apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" apiKeyTempText = await debridManager.getManualAuthKey(debridSource) ?? ""
} }
} }
} }

View file

@ -227,7 +227,7 @@ struct SettingsView: View {
callbackURLScheme: "ferrite" callbackURLScheme: "ferrite"
) { callbackURL, error in ) { callbackURL, error in
Task { Task {
await debridManager.handleCallback(url: callbackURL, error: error) await debridManager.handleAuthCallback(url: callbackURL, error: error)
} }
} }
.prefersEphemeralWebBrowserSession(useEphemeralAuth) .prefersEphemeralWebBrowserSession(useEphemeralAuth)