import Foundation // Alle Pfade an einer Stelle. An der Aufrufstelle steht nie eine Zeichenkette – // wer einen Endpunkt umbenennt, bricht genau hier und nicht in zwölf Ansichten. // // Stufe 1: Server prüfen, anmelden, Marke holen, Konten lesen. extension CertainClient { // MARK: - Server /// Prüft, ob unter der Adresse überhaupt ein Certain läuft. /// /// Wichtig: Das Backend liefert für jeden unbekannten Pfad ausserhalb von /// `/api` die Weboberfläche mit Status 200 aus. `/api/version` kann das /// nicht – nur ein echter Server antwortet hier mit dem erwarteten JSON. func serverkennung() async throws -> Serverkennung { let kennung: Serverkennung do { kennung = try await hole("version") } catch APIFehler.antwortUnlesbar { throw APIFehler.keinCertainServer } guard kennung.istCertain else { throw APIFehler.keinCertainServer } return kennung } // MARK: - Anmeldung /// Meldet an und bekommt ein Sitzungs-Cookie. Steht der zweite Faktor /// aus, ist `status == "2fa_required"` und das Cookie gilt nur zehn /// Minuten für genau diesen Schritt. func melde(benutzer: String, passwort: String) async throws -> AnmeldeAntwort { try await schickeFormular("auth/login", felder: ["username": benutzer, "password": passwort]) } func pruefeZweitenFaktor(code: String, wiederherstellung: Bool = false) async throws -> AnmeldeAntwort { try await schicke(.post, "auth/2fa/verify", koerper: ZweiterFaktorEingabe(code: code, recovery: wiederherstellung)) } func ichSelbst() async throws -> Benutzer { try await hole("auth/me") } /// Wechselt das Passwort. Beendet dabei alle anderen Sitzungen des Kontos /// und setzt für den Aufrufer ein frisches Cookie. @discardableResult func wechslePasswort(alt: String, neu: String) async throws -> Benutzer { try await schicke(.post, "auth/change-password", koerper: PasswortWechsel(currentPassword: alt, newPassword: neu)) } func meldeAb() async throws { try await schickeStumm(.post, "auth/logout") } // MARK: - Gerätemarken /// Holt die Marke, mit der die App von da an arbeitet. Geht nur aus einer /// echten Anmeldung heraus – eine Marke darf keine weitere ausstellen. func legeAppMarkeAn(name: String) async throws -> Marke { try await schicke(.post, "inbox/tokens", koerper: MarkeAnlegen.fuerApp(name: name)) } func marken() async throws -> [Marke] { try await hole("inbox/tokens") } func widerrufeMarke(id: Int) async throws { try await schickeStumm(.delete, "inbox/tokens/\(id)") } // MARK: - Einstellungen func einstellungen() async throws -> Einstellungen { let roh: [Einstellung] = try await hole("settings") return Einstellungen(roh) } @discardableResult func setzeEinstellung(_ schluessel: String, _ wert: String) async throws -> Einstellung { // Query statt Körper – so will es das Backend (`routers/settings.py`). try await schicke(.put, "settings/\(schluessel)", abfrage: [URLQueryItem(name: "value", value: wert)]) } // MARK: - Konten func konten() async throws -> [Konto] { try await hole("accounts") } func konto(_ id: Int) async throws -> Konto { try await hole("accounts/\(id)") } }