From bfd327bd40cce2c4eecf7ec4a0918c0f6d51c85f Mon Sep 17 00:00:00 2001 From: rene Date: Sat, 30 May 2026 09:37:12 +0200 Subject: [PATCH] Settings: echtes Profil via /api/auth/me (Rolle, Founder, Abo, Avatar) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Login liefert nur {token, name, is_premium}. Für Admin-/Founder-/Tier-Info holen wir nach Login (und beim Erscheinen von MainTabView) /api/auth/me und zeigen ein echtes Profil mit Avatar, Email, Rolle und nur dann Premium-Status, wenn das relevant ist. --- BanYaroGo/API/DTOs.swift | 16 +++++ BanYaroGo/Auth/AuthSession.swift | 17 ++++++ BanYaroGo/Views/MainTabView.swift | 3 + BanYaroGo/Views/SettingsView.swift | 93 +++++++++++++++++++++++++++++- 4 files changed, 126 insertions(+), 3 deletions(-) diff --git a/BanYaroGo/API/DTOs.swift b/BanYaroGo/API/DTOs.swift index 4935cd3..8240152 100644 --- a/BanYaroGo/API/DTOs.swift +++ b/BanYaroGo/API/DTOs.swift @@ -13,6 +13,22 @@ struct LoginResponse: Decodable { let isPremium: Bool } +struct UserProfile: Decodable { + let id: Int + let name: String + let email: String + let realName: String? + let rolle: String? + let isPremium: Bool? + let isFounder: Bool? + let isPartner: Bool? + let founderNumber: Int? + let subscriptionTier: String? + let avatarUrl: String? + let wohnort: String? + let bio: String? +} + // MARK: - Dogs struct Dog: Decodable, Identifiable { diff --git a/BanYaroGo/Auth/AuthSession.swift b/BanYaroGo/Auth/AuthSession.swift index e34d740..73b1652 100644 --- a/BanYaroGo/Auth/AuthSession.swift +++ b/BanYaroGo/Auth/AuthSession.swift @@ -7,6 +7,7 @@ final class AuthSession { var token: String? var userName: String? var isPremium: Bool = false + var profile: UserProfile? var isLoggingIn: Bool = false var errorMessage: String? @@ -46,5 +47,21 @@ final class AuthSession { token = nil userName = nil isPremium = false + profile = nil + } + + /// Fetches the full user profile from /api/auth/me. Called after login and + /// when MainTabView appears, so admin/founder/role info shows up. + func loadProfile() async { + guard token != nil else { return } + do { + let me: UserProfile = try await APIClient.shared.get("/api/auth/me") + self.profile = me + if let premium = me.isPremium { + self.isPremium = premium + } + } catch { + // Profil-Refresh ist non-critical; Settings zeigt sonst nur die Basics. + } } } diff --git a/BanYaroGo/Views/MainTabView.swift b/BanYaroGo/Views/MainTabView.swift index 1a77171..fb10ad4 100644 --- a/BanYaroGo/Views/MainTabView.swift +++ b/BanYaroGo/Views/MainTabView.swift @@ -1,6 +1,8 @@ import SwiftUI struct MainTabView: View { + @Environment(AuthSession.self) private var auth + var body: some View { TabView { RoutesListView() @@ -12,5 +14,6 @@ struct MainTabView: View { SettingsView() .tabItem { Label("Mehr", systemImage: "person.crop.circle") } } + .task { await auth.loadProfile() } } } diff --git a/BanYaroGo/Views/SettingsView.swift b/BanYaroGo/Views/SettingsView.swift index eed2478..6e4b520 100644 --- a/BanYaroGo/Views/SettingsView.swift +++ b/BanYaroGo/Views/SettingsView.swift @@ -6,15 +6,50 @@ struct SettingsView: View { var body: some View { NavigationStack { Form { - Section("Account") { - LabeledContent("Name", value: auth.userName ?? "—") - LabeledContent("Premium", value: auth.isPremium ? "Ja" : "Nein") + Section { + HStack(spacing: 14) { + avatarView + .frame(width: 60, height: 60) + .clipShape(Circle()) + VStack(alignment: .leading, spacing: 2) { + Text(displayName) + .font(.headline) + if let email = auth.profile?.email { + Text(email) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + } + .padding(.vertical, 4) } + + Section("Account") { + LabeledContent("Rolle", value: rolleLabel) + if auth.profile?.isFounder == true { + LabeledContent("Founder", value: founderLabel) + } + if auth.profile?.isPartner == true { + LabeledContent("Partner", value: "Ja") + } + if let tier = auth.profile?.subscriptionTier, !tier.isEmpty { + LabeledContent("Abo", value: tier.capitalized) + } + LabeledContent("Premium", value: premiumValue ? "Ja" : "Nein") + } + + if let wohnort = auth.profile?.wohnort, !wohnort.isEmpty { + Section("Ort") { + Text(wohnort) + } + } + Section { Button("Abmelden", role: .destructive) { auth.logout() } } + Section("Über") { Text("Ban Yaro Go ist die native iOS-Ergänzung zur banyaro.app PWA. Phase 1: deine Touren ansehen.") .font(.footnote) @@ -22,6 +57,58 @@ struct SettingsView: View { } } .navigationTitle("Mehr") + .refreshable { await auth.loadProfile() } } } + + private var displayName: String { + auth.profile?.name ?? auth.userName ?? "—" + } + + private var premiumValue: Bool { + auth.profile?.isPremium ?? auth.isPremium + } + + private var rolleLabel: String { + switch auth.profile?.rolle?.lowercased() { + case "admin": return "Admin" + case "moderator": return "Moderator" + case nil: return "—" + default: return "Mitglied" + } + } + + private var founderLabel: String { + if let n = auth.profile?.founderNumber { return "#\(n)" } + return "Ja" + } + + @ViewBuilder + private var avatarView: some View { + if let path = auth.profile?.avatarUrl, !path.isEmpty, + let url = avatarURL(path) { + AsyncImage(url: url) { phase in + switch phase { + case .success(let img): img.resizable().scaledToFill() + default: avatarPlaceholder + } + } + } else { + avatarPlaceholder + } + } + + private var avatarPlaceholder: some View { + ZStack { + Color.accentColor.opacity(0.2) + Image(systemName: "person.crop.circle.fill") + .font(.system(size: 36)) + .foregroundStyle(Color.accentColor) + } + } + + private func avatarURL(_ path: String) -> URL? { + if path.hasPrefix("http") { return URL(string: path) } + return URL(string: "https://banyaro.app\(path)") + } }