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>
146 lines
4.4 KiB
Swift
146 lines
4.4 KiB
Swift
import Foundation
|
||
|
||
// Die Typen hier bilden die Antwortschemas aus `backend/app/schemas.py` nach.
|
||
// Feldnamen stimmen eins zu eins überein – deshalb braucht keiner von ihnen
|
||
// `CodingKeys`. Wer ein Feld umbenennt, bricht genau hier.
|
||
|
||
/// `GET /api/version` – die Prüfung, ob unter einer Adresse ein Certain läuft.
|
||
struct Serverkennung: Codable, Sendable {
|
||
let name: String
|
||
let version: String
|
||
let gitSha: String?
|
||
|
||
private enum CodingKeys: String, CodingKey {
|
||
case name, version
|
||
case gitSha = "git_sha"
|
||
}
|
||
|
||
/// Der Name, den ein echter Server trägt (`backend/app/branding.py`).
|
||
var istCertain: Bool { name.localizedCaseInsensitiveContains("certain") }
|
||
}
|
||
|
||
/// `UserOut`
|
||
struct Benutzer: Codable, Sendable, Identifiable, Hashable {
|
||
let id: Int
|
||
let username: String
|
||
let role: Rolle
|
||
let displayName: String?
|
||
let isActive: Bool
|
||
let mustChangePassword: Bool
|
||
let mustChangePasswordReason: String?
|
||
let totpEnabled: Bool
|
||
let createdAt: Zeitpunkt
|
||
let mfaSetupRequired: Bool
|
||
|
||
private enum CodingKeys: String, CodingKey {
|
||
case id, username, role
|
||
case displayName = "display_name"
|
||
case isActive = "is_active"
|
||
case mustChangePassword = "must_change_password"
|
||
case mustChangePasswordReason = "must_change_password_reason"
|
||
case totpEnabled = "totp_enabled"
|
||
case createdAt = "created_at"
|
||
case mfaSetupRequired = "mfa_setup_required"
|
||
}
|
||
|
||
enum Rolle: String, Codable, Sendable {
|
||
case admin, user
|
||
}
|
||
|
||
var anzeigename: String { displayName?.isEmpty == false ? displayName! : username }
|
||
}
|
||
|
||
/// `LoginOut` – `status` ist `"ok"` oder `"2fa_required"`.
|
||
struct AnmeldeAntwort: Codable, Sendable {
|
||
let status: String
|
||
let user: Benutzer?
|
||
|
||
var brauchtZweitenFaktor: Bool { status == "2fa_required" }
|
||
}
|
||
|
||
/// `TwoFactorVerify`
|
||
struct ZweiterFaktorEingabe: Codable, Sendable {
|
||
let code: String
|
||
let recovery: Bool
|
||
}
|
||
|
||
/// `PasswordChange`
|
||
struct PasswortWechsel: Codable, Sendable {
|
||
let currentPassword: String
|
||
let newPassword: String
|
||
|
||
private enum CodingKeys: String, CodingKey {
|
||
case currentPassword = "current_password"
|
||
case newPassword = "new_password"
|
||
}
|
||
}
|
||
|
||
/// `ApiTokenCreate`
|
||
struct MarkeAnlegen: Codable, Sendable {
|
||
let name: String
|
||
let scope: String
|
||
let defaultAccountId: Int?
|
||
|
||
private enum CodingKeys: String, CodingKey {
|
||
case name, scope
|
||
case defaultAccountId = "default_account_id"
|
||
}
|
||
|
||
/// Die Marke, mit der sich die App anmeldet.
|
||
static func fuerApp(name: String) -> MarkeAnlegen {
|
||
MarkeAnlegen(name: name, scope: "app", defaultAccountId: nil)
|
||
}
|
||
}
|
||
|
||
/// `ApiTokenOut` beziehungsweise `ApiTokenCreated`.
|
||
/// `token` trägt den Klartext – und zwar genau einmal, beim Anlegen.
|
||
struct Marke: Codable, Sendable, Identifiable, Hashable {
|
||
let id: Int
|
||
let name: String
|
||
let scope: String
|
||
let defaultAccountId: Int?
|
||
let lastUsedAt: Zeitpunkt?
|
||
let createdAt: Zeitpunkt?
|
||
let token: String?
|
||
|
||
private enum CodingKeys: String, CodingKey {
|
||
case id, name, scope, token
|
||
case defaultAccountId = "default_account_id"
|
||
case lastUsedAt = "last_used_at"
|
||
case createdAt = "created_at"
|
||
}
|
||
}
|
||
|
||
/// Ein Eintrag aus `GET /api/settings`. Werte sind immer Zeichenketten.
|
||
struct Einstellung: Codable, Sendable, Hashable {
|
||
let key: String
|
||
let value: String
|
||
}
|
||
|
||
/// Die Einstellungen als benutzbares Ganzes.
|
||
struct Einstellungen: Sendable, Hashable {
|
||
var basiswaehrung: String = "EUR"
|
||
var budgetStarttag: Int = 1
|
||
var sprache: String = "de"
|
||
var erscheinungsbild: String = "system"
|
||
var datumsformat: String = "de"
|
||
var aktiveWaehrungen: [String] = ["EUR", "CHF", "USD"]
|
||
|
||
init() {}
|
||
|
||
init(_ eintraege: [Einstellung]) {
|
||
var nach: [String: String] = [:]
|
||
for eintrag in eintraege { nach[eintrag.key] = eintrag.value }
|
||
basiswaehrung = nach["base_currency"] ?? basiswaehrung
|
||
budgetStarttag = min(31, max(1, Int(nach["budget_start_day"] ?? "") ?? 1))
|
||
sprache = nach["locale"] ?? sprache
|
||
erscheinungsbild = nach["theme"] ?? erscheinungsbild
|
||
datumsformat = nach["date_format"] ?? datumsformat
|
||
if let roh = nach["active_currencies"] {
|
||
aktiveWaehrungen = roh.split(separator: ",")
|
||
.map { $0.trimmingCharacters(in: .whitespaces).uppercased() }
|
||
.filter { !$0.isEmpty }
|
||
}
|
||
}
|
||
}
|