Files
certain/ios/Modelle/Grundmodelle.swift
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

146 lines
4.4 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
// 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 }
}
}
}