import SwiftUI struct LoginView: View { @EnvironmentObject private var session: Session @State private var server: String = "" @State private var username = "" @State private var password = "" @State private var error: String? @State private var busy = false @State private var stayLoggedIn = Session.shared.stayLoggedIn @State private var manageShown = false /// Am eingetragenen Server eingerichtete Anmeldedienste. Leer = nur Passwort. @State private var anbieter: [OidcProviderResponse] = [] var body: some View { NavigationStack { Form { Section("Server") { TextField("http://192.168.1.10:8080", text: $server) .textInputAutocapitalization(.never) .autocorrectionDisabled() .keyboardType(.URL) // Erst mit hinterlegter Adresse laesst sich der Server // nach seinen Anmeldediensten fragen. .onSubmit { session.setServer(server) Task { await anbieterLaden() } } // Mit mehreren Servern ist das hier der Weg zurueck, wenn man // am falschen gelandet ist. if session.profiles.count > 1 { Picker("Ausgewählt", selection: profileSelection) { ForEach(session.profiles) { profile in Text(profile.name).tag(profile.id) } } } } Section("Anmeldung") { TextField("Benutzername", text: $username) .textInputAutocapitalization(.never) .autocorrectionDisabled() SecureField("Passwort", text: $password) Toggle("Angemeldet bleiben", isOn: $stayLoggedIn) } if !stayLoggedIn { Section { Text("Die Anmeldung gilt nur, solange die App geoeffnet ist.") .font(.caption).foregroundStyle(.secondary) } } if let error { Section { Text(error).foregroundStyle(.red).font(.callout) } } Section { Button(busy ? "Anmelden…" : "Anmelden") { Task { await login() } } .disabled(busy || server.isEmpty || username.isEmpty) } // Nur wenn der Server einen App-Zugang eingerichtet hat. Ohne // Anmeldedienst sieht die Maske genauso aus wie bisher. if !anbieter.isEmpty { Section("Single Sign-on") { ForEach(anbieter) { dienst in Button("Mit \(dienst.displayName) anmelden") { Task { await ssoLogin(dienst) } } .disabled(busy || dienst.nativeClientID == nil) } } } } .navigationTitle(session.activeProfile?.name ?? "Vorrania") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("Server verwalten", systemImage: "server.rack") { manageShown = true } } } .sheet(isPresented: $manageShown) { ServerListView() } } // Beim Start und bei jedem Serverwechsel die Felder auf das aktive // Profil setzen - sonst steht die Adresse des vorigen Servers da. .task(id: session.activeProfileID) { syncFromProfile() await anbieterLaden() } } private var profileSelection: Binding { Binding( get: { session.activeProfileID ?? session.profiles.first?.id ?? UUID() }, set: { session.switchTo($0) } ) } private func syncFromProfile() { server = session.activeProfile?.urlString ?? "" username = session.activeProfile?.username ?? "" password = "" error = nil } /// Fragt den Server, welche Anmeldedienste er kennt. Der APIClient liest die /// Adresse aus der Sitzung – die Abfrage geht also erst, wenn ein Profil da /// ist. Ein Fehler bleibt still: der Passwort-Login muss auch dann gehen, /// wenn der Server alt ist und den Endpunkt gar nicht kennt. private func anbieterLaden() async { guard session.activeProfile != nil else { anbieter = []; return } anbieter = (try? await APIClient.shared.oidcProviders()) ?? [] } private func ssoLogin(_ dienst: OidcProviderResponse) async { error = nil busy = true defer { busy = false } session.setServer(server) guard let profilID = session.activeProfileID else { return } do { let (idToken, nonce) = try await OIDCLogin.shared.anmelden( provider: dienst, profileID: profilID ) let token = try await APIClient.shared.oidcNativeLogin( slug: dienst.slug, idToken: idToken, nonce: nonce ) session.store(token: token.accessToken, username: token.username, isAdmin: token.role == "admin", persist: stayLoggedIn, oidcSlug: dienst.slug) } catch { self.error = error.localizedDescription } } private func login() async { error = nil busy = true defer { busy = false } session.setServer(server) do { let token = try await APIClient.shared.login(username: username, password: password) session.store(token: token.accessToken, username: token.username, isAdmin: token.role == "admin", persist: stayLoggedIn) } catch { self.error = error.localizedDescription } } }