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
parent 243a16e3c4
commit 973fbb4099
8 changed files with 118 additions and 166 deletions

View file

@ -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).")

View file

@ -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).")

View file

@ -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).")

View file

@ -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

View file

@ -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<DebridType> = [] {
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<IAStatus> = []
@ -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
}
}

View file

@ -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()

View file

@ -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) ?? ""
}
}
}

View file

@ -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)