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 } } } }