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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8a7545bf5b
commit
6b86a6bf91
@@ -0,0 +1,145 @@
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user