import Foundation import Observation enum AuthError: LocalizedError { case invalidURL case badResponse(Int) case noToken case unknown(String) var errorDescription: String? { switch self { case .invalidURL: return "Ungültige Server-URL." case .badResponse(let code): return "Server antwortete mit Status \(code)." case .noToken: return "Login fehlgeschlagen: kein Token erhalten." case .unknown(let msg): return msg } } } @Observable @MainActor final class AuthStore { var isLoggedIn: Bool = false var serverURL: String = "" var username: String = "" var token: String = "" var errorMessage: String? func restoreSession() { guard let creds = KeychainStore.load() else { return } self.serverURL = creds.serverURL self.username = creds.username self.token = creds.token self.isLoggedIn = true } func login(serverURL rawURL: String, username: String, password: String, remember: Bool) async { errorMessage = nil let normalized = Self.normalizeURL(rawURL) guard let url = URL(string: normalized + "/login") else { errorMessage = AuthError.invalidURL.errorDescription return } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let body = ["username": username, "password": password] request.httpBody = try? JSONEncoder().encode(body) do { let (data, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse else { errorMessage = "Keine HTTP-Antwort vom Server." return } guard (200..<300).contains(http.statusCode) else { errorMessage = AuthError.badResponse(http.statusCode).errorDescription return } let decoded = try JSONDecoder().decode(LoginResponseDTO.self, from: data) self.serverURL = normalized self.username = decoded.user.username ?? username self.token = decoded.user.token self.isLoggedIn = true if remember { try? KeychainStore.save(StoredCredentials( serverURL: normalized, username: self.username, token: self.token )) } else { KeychainStore.delete() } } catch { errorMessage = "Login fehlgeschlagen: \(error.localizedDescription)" } } func logout() { KeychainStore.delete() token = "" isLoggedIn = false } static func normalizeURL(_ raw: String) -> String { var s = raw.trimmingCharacters(in: .whitespacesAndNewlines) while s.hasSuffix("/") { s.removeLast() } if !s.lowercased().hasPrefix("http://") && !s.lowercased().hasPrefix("https://") { s = "https://" + s } return s } }