Files
ScarriffleandClaude Opus 5 6b86a6bf91 feat: Erste Stufe der iOS-App - Anmeldung und Kontenliste
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>
2026-10-20 16:20:03 +02:00

107 lines
4.1 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// Was beim Reden mit dem Certain-Backend schiefgehen kann.
///
/// Die Fälle sind nach dem geschnitten, was die App damit *tun* muss – nicht
/// nach HTTP-Status. `pflichtOffen` führt auf einen anderen Bildschirm,
/// `konflikt` ist eine Rückfrage, die sich mit „trotzdem" wiederholen lässt,
/// alles übrige ist eine Meldung.
enum APIFehler: Error, LocalizedError, Sendable, Equatable {
/// Netz weg, Server nicht erreichbar, Zeitüberschreitung.
case keineVerbindung(String)
/// Unter der Adresse antwortet etwas, aber kein Certain.
case keinCertainServer
/// 401 – Marke ungültig oder widerrufen, Sitzung abgelaufen.
case nichtAngemeldet
/// 403 mit maschinenlesbarem Schlüssel: erst muss etwas erledigt werden.
case pflichtOffen(Pflicht)
/// 403 aus einem anderen Grund.
case verboten(String)
/// 404 – gibt es nicht, oder gehört jemand anderem.
case nichtGefunden
/// 409 – meistens eine Rückfrage, die sich mit `trotzdem = true`
/// wiederholen lässt. Der Text ist für den Nutzer geschrieben.
case konflikt(String)
/// 400 oder 422.
case ungueltig(String)
/// Alles übrige, was der Server sagt.
case server(status: Int, text: String)
/// Die Antwort passt nicht zu dem, was wir erwarten.
case antwortUnlesbar(String)
/// Was der Benutzer erledigen muss, bevor die API ihn durchlässt.
/// Die Werte sind die Schlüssel aus `backend/app/deps.py`.
enum Pflicht: String, Sendable {
case passwortWechseln = "password_change_required"
case zweiterFaktor = "mfa_setup_required"
}
var errorDescription: String? {
switch self {
case .keineVerbindung(let grund):
return String(format: String(localized: "fehler.keineVerbindung"), grund)
case .keinCertainServer:
return String(localized: "fehler.keinCertainServer")
case .nichtAngemeldet:
return String(localized: "fehler.nichtAngemeldet")
case .pflichtOffen(.passwortWechseln):
return String(localized: "fehler.passwortWechseln")
case .pflichtOffen(.zweiterFaktor):
return String(localized: "fehler.zweiterFaktor")
case .verboten(let text), .konflikt(let text), .ungueltig(let text):
return text
case .nichtGefunden:
return String(localized: "fehler.nichtGefunden")
case .server(let status, let text):
return text.isEmpty
? String(format: String(localized: "fehler.server"), status)
: text
case .antwortUnlesbar(let grund):
return String(format: String(localized: "fehler.antwortUnlesbar"), grund)
}
}
/// Ob ein erneuter Versuch überhaupt Sinn ergibt.
var lohntWiederholung: Bool {
switch self {
case .keineVerbindung, .server:
return true
default:
return false
}
}
}
/// Der Fehlerkörper des Backends.
///
/// `detail` ist dort mal eine Zeichenkette (`{"detail": "Nicht gefunden"}`),
/// mal eine Liste von Pydantic-Meldungen (bei 422). Beides muss hier durch,
/// sonst verliert die App bei jedem Eingabefehler die Meldung.
struct Fehlerantwort: Decodable, Sendable {
let detail: String
private enum Schluessel: String, CodingKey { case detail }
private struct Einzelfehler: Decodable {
let msg: String?
}
init(from decoder: Decoder) throws {
let behaelter = try decoder.container(keyedBy: Schluessel.self)
if let text = try? behaelter.decode(String.self, forKey: .detail) {
detail = text
} else if let liste = try? behaelter.decode([Einzelfehler].self, forKey: .detail) {
detail = liste.compactMap(\.msg).joined(separator: ", ")
} else {
detail = ""
}
}
/// Versucht, aus Antwortdaten eine Meldung zu ziehen. Schlägt das fehl,
/// kommt eine leere Zeichenkette – nie ein Fehler, denn wir sind hier
/// bereits im Fehlerfall.
static func ausDaten(_ daten: Data) -> String {
(try? JSONDecoder().decode(Fehlerantwort.self, from: daten))?.detail ?? ""
}
}