Neuer Ordner ios/ mit dem Geruest: Netzwerkschicht, Modelle, Offline-Ablage, Texte in Deutsch und Englisch, Navigation, der komplette Anmeldeweg (Server waehlen, anmelden, zweiter Faktor, Pflicht-Passwortwechsel) und als erste Fachansicht die Kontenliste. Die Anmeldung laeuft einmal ueber das Sitzungs-Cookie, holt sich dabei eine App-Marke und legt sie in den Schluesselbund. Danach traegt nur noch der Header. Kein Passwort auf dem Geraet, kein Ablauf nach 30 Tagen. Der Code ist nie uebersetzt worden - hier gibt es kein Xcode. Geprueft ist dafuer jedes Modellfeld gegen echte Antworten des laufenden Backends; dabei kam heraus, dass created_at ohne Zeitzone und mit Mikrosekunden kommt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
99 lines
3.5 KiB
Swift
99 lines
3.5 KiB
Swift
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)")
|
||
}
|
||
}
|