Files
Flaro/Packages/FlixyKit/Sources/PlexKit/Auth/PlexAuthenticator.swift
T
GuidoandClaude Opus 5 70c620bad4 Anmeldung gibt nicht mehr vor dem Code auf
Die Wartezeit lag bei fünf Minuten und damit unter der Gültigkeit des
Codes bei Plex. Wer zwischendurch etwas anderes gemacht hat, stand
deshalb vor „Zeit abgelaufen" bei einem Code, der noch funktioniert
hätte. Jetzt 15 Minuten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 19:03:53 +02:00

114 lines
4.5 KiB
Swift

import Foundation
/// Führt die Anmeldung bei plex.tv durch.
public actor PlexAuthenticator {
/// Wo das Konto-Token im Schlüsselbund steht.
public static let accountTokenAccount = "plex.account"
private let client: PlexHTTPClient
private let identity: PlexClientIdentity
private let tokenStore: any TokenStoring
private let clock: any Clock<Duration>
public init(
identity: PlexClientIdentity,
tokenStore: any TokenStoring,
transport: any PlexTransport = URLSession.shared,
clock: any Clock<Duration> = ContinuousClock()
) {
self.identity = identity
self.tokenStore = tokenStore
self.clock = clock
self.client = PlexHTTPClient(
baseURL: URL(string: "https://plex.tv")!,
identity: identity,
transport: transport
)
}
/// Der bereits gespeicherte Token, falls vorhanden.
public nonisolated var storedToken: String? {
tokenStore.token(for: Self.accountTokenAccount)
}
/// Fordert einen neuen Anmeldecode an.
///
/// Bewusst ohne `strong=true`: das liefert zwar einen schwerer zu
/// erratenden Code, aber einen dreißigstelligen. Bei plex.tv/link
/// abzutippen ist der nicht — und genau das ist der Weg, den Flixy
/// anbietet. Der kurze Code ist vier Zeichen lang und läuft nach wenigen
/// Minuten ab, was das Raten ausreichend aussichtslos macht.
public func requestPin() async throws(PlexError) -> PlexPin {
try await client.send(
PlexRequest(method: .post, path: "/api/v2/pins"),
as: PlexPin.self
)
}
/// Wartet darauf, dass der Nutzer den Code im Browser bestätigt.
///
/// Plex bietet keinen Rückkanal — es bleibt nur regelmäßiges Nachfragen.
/// Das Intervall ist bewusst großzügig: der Weg über den Browser dauert
/// ohnehin Sekunden, und sekündliches Anklopfen brächte nichts außer Last.
///
/// Die Wartezeit ist an die Gültigkeit des Codes angelehnt. Kürzer wäre
/// verkehrt herum: die App gäbe auf, obwohl der Code noch gilt, und wer
/// zwischendurch etwas anderes gemacht hat, stünde vor „Zeit abgelaufen"
/// bei einem Code, der funktioniert hätte.
public func waitForAuthorization(
pin: PlexPin,
pollInterval: Duration = .seconds(2),
timeout: Duration = .seconds(900)
) async throws(PlexError) -> String {
// Über die Anzahl der Versuche statt über einen Zeitpunkt: eine
// existenzielle `any Clock` gibt ihren Instant-Typ nicht preis, und
// deshalb lassen sich zwei ihrer Zeitpunkte nicht vergleichen. Das
// Ergebnis ist dasselbe, und in Tests reicht eine Uhr, die sofort
// zurückkehrt.
//
// Nach oben begrenzt, damit ein versehentlich winziges Intervall nicht
// in einen Überlauf oder Millionen Anfragen läuft.
let attempts = pollInterval > .zero
? min(1000, max(1, Int(timeout / pollInterval)))
: 1
for _ in 0..<attempts {
do {
try await clock.sleep(for: pollInterval)
} catch {
// Abbruch durch die Oberfläche, etwa weil das Anmeldeblatt
// geschlossen wurde. Kein Fehlerfall.
throw .transport(description: "Anmeldung abgebrochen")
}
let current = try await client.send(
PlexRequest(path: "/api/v2/pins/\(pin.id)"),
as: PlexPin.self
)
if let token = current.authToken, !token.isEmpty {
try? tokenStore.store(token, for: Self.accountTokenAccount)
await client.setToken(token)
return token
}
if let expiresAt = current.expiresAt, expiresAt < Date() {
throw .transport(description: "Der Anmeldecode ist abgelaufen")
}
}
throw .transport(description: "Zeit für die Anmeldung abgelaufen")
}
/// Meldet das Gerät ab. Der Token verschwindet aus dem Schlüsselbund;
/// am Konto bleibt der Eintrag stehen, bis er dort entfernt wird.
///
/// `nonisolated`, weil nur der Schlüsselbund angefasst wird — der ist
/// selbst abgesichert. Die Oberfläche soll beim Abmelden nicht auf den
/// Actor warten müssen, denn das kann mitten in einer laufenden Abfrage
/// passieren.
public nonisolated func signOut() {
tokenStore.remove(for: Self.accountTokenAccount)
}
}