import Foundation import Security import Combine // MARK: - Models struct User: Codable, Identifiable { let id: String let email: String let name: String let role: UserRole let createdAt: Date let lastLoginAt: Date? let active: Bool enum UserRole: String, Codable { case admin case `operator` case viewer } } struct Module: Codable, Identifiable { let id: String let name: String let description: String var enabled: Bool let version: String let status: ModuleStatus enum ModuleStatus: String, Codable { case running, stopped, error, degraded } } struct SystemMetrics: Codable { let cpuPercent: Double let memoryPercent: Double let diskPercent: Double let requestsPerMinute: Int let activeConnections: Int let errorRatePercent: Double let timestamp: Date } struct ServerSettings: Codable, Equatable { var systemName: String var timezone: String var jwtExpiryHours: Int var twoFactorEnabled: Bool var maintenanceMode: Bool var allowRegistration: Bool var webhookUrl: String? } struct APIError: Decodable, Error, LocalizedError { let message: String let code: String? var errorDescription: String? { message } } private struct LoginPayload: Encodable { let email: String let password: String } private struct LoginResponse: Decodable { let token: String let user: User let expiresAt: Date? } private struct TogglePayload: Encodable { let enabled: Bool } // MARK: - APIService @MainActor final class APIService: ObservableObject { static let shared = APIService() @Published private(set) var isAuthenticated: Bool @Published private(set) var currentUser: User? private let auth = AuthService.shared private let session: URLSession = { let cfg = URLSessionConfiguration.default cfg.timeoutIntervalForRequest = 30 cfg.timeoutIntervalForResource = 90 cfg.waitsForConnectivity = true return URLSession(configuration: cfg) }() private let decoder: JSONDecoder = { let d = JSONDecoder() d.keyDecodingStrategy = .convertFromSnakeCase d.dateDecodingStrategy = .iso8601 return d }() private let encoder: JSONEncoder = { let e = JSONEncoder() e.keyEncodingStrategy = .convertToSnakeCase e.dateEncodingStrategy = .iso8601 return e }() private static let baseURLKey = "com.aamos.admin.baseURL" static let defaultBaseURL = "https://amos.wavult.com" var baseURL: String { get { UserDefaults.standard.string(forKey: Self.baseURLKey) ?? Self.defaultBaseURL } set { UserDefaults.standard.set(newValue, forKey: Self.baseURLKey) } } private init() { isAuthenticated = AuthService.shared.isAuthenticated } // MARK: - Request building private func makeRequest(_ path: String, method: String = "GET", body: Data? = nil) throws -> URLRequest { guard let url = URL(string: baseURL + path) else { throw URLError(.badURL) } var req = URLRequest(url: url) req.httpMethod = method req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.setValue("application/json", forHTTPHeaderField: "Accept") if let token = auth.load() { req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } req.httpBody = body return req } // MARK: - Response dispatch private func send(_ req: URLRequest) async throws -> T { let (data, response) = try await session.data(for: req) guard let http = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } if http.statusCode == 401 { await logout() throw APIError(message: "Session utgƄngen. Logga in igen.", code: "UNAUTHORIZED") } guard (200..<300).contains(http.statusCode) else { if let err = try? decoder.decode(APIError.self, from: data) { throw err } throw APIError( message: "Servern svarade med statuskod \(http.statusCode).", code: "HTTP_\(http.statusCode)" ) } return try decoder.decode(T.self, from: data) } // MARK: - Auth func login(email: String, password: String) async throws { let body = try encoder.encode(LoginPayload(email: email, password: password)) let req = try makeRequest("/api/v1/auth/login", method: "POST", body: body) let resp: LoginResponse = try await send(req) try auth.save(token: resp.token) currentUser = resp.user isAuthenticated = true } func logout() async { if isAuthenticated, let req = try? makeRequest("/api/v1/auth/logout", method: "POST") { _ = try? await session.data(for: req) } auth.logout() currentUser = nil isAuthenticated = false } // MARK: - Users func getMe() async throws -> User { let req = try makeRequest("/api/v1/users/me") let user: User = try await send(req) currentUser = user return user } func getUsers() async throws -> [User] { let req = try makeRequest("/api/v1/users") return try await send(req) } // MARK: - Dashboard func getDashboard() async throws -> DashboardMetrics { let req = try makeRequest("/api/v1/dashboard") return try await send(req) } // MARK: - Modules func getModules() async throws -> [Module] { let req = try makeRequest("/api/v1/modules") return try await send(req) } func toggleModule(id: String, enabled: Bool) async throws -> Module { let body = try encoder.encode(TogglePayload(enabled: enabled)) let req = try makeRequest("/api/v1/modules/\(id)", method: "PATCH", body: body) return try await send(req) } // MARK: - Audit func getAudit(page: Int = 1, limit: Int = 50) async throws -> [AuditEntry] { let req = try makeRequest("/api/v1/audit?page=\(page)&limit=\(limit)") return try await send(req) } // MARK: - Metrics func getMetrics() async throws -> SystemMetrics { let req = try makeRequest("/api/v1/metrics") return try await send(req) } // MARK: - Settings func getSettings() async throws -> ServerSettings { let req = try makeRequest("/api/v1/settings") return try await send(req) } func updateSettings(_ settings: ServerSettings) async throws -> ServerSettings { let body = try encoder.encode(settings) let req = try makeRequest("/api/v1/settings", method: "PUT", body: body) return try await send(req) } } // MARK: - APIServiceProtocol conformance extension APIService: APIServiceProtocol { func fetchMetrics() async throws -> DashboardMetrics { try await getDashboard() } func fetchRecentAudit(limit: Int) async throws -> [AuditEntry] { try await getAudit(limit: limit) } }