import Foundation class HealthAPIService { static let shared = HealthAPIService() let baseURL = "https://health.digital-home.site" private var cachedToken: String? { get { KeychainService.load(key: KeychainService.healthTokenKey) } set { if let v = newValue { KeychainService.save(key: KeychainService.healthTokenKey, value: v) } else { KeychainService.delete(key: KeychainService.healthTokenKey) } } } func ensureToken() async throws -> String { if let t = cachedToken { return t } return try await refreshToken() } func refreshToken() async throws -> String { // Use credentials from Keychain (set during first login or onboarding) guard let email = KeychainService.load(key: "health_email"), let password = KeychainService.load(key: "health_password") else { throw APIError.unauthorized } var req = URLRequest(url: URL(string: "\(baseURL)/api/auth/login")!) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = try? JSONEncoder().encode(["email": email, "password": password]) req.timeoutInterval = 15 let (data, response) = try await URLSession.shared.data(for: req) guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { throw APIError.unauthorized } struct LoginResp: Decodable { let token: String } let resp = try JSONDecoder().decode(LoginResp.self, from: data) cachedToken = resp.token return resp.token } /// Call once during setup to store health credentials securely static func configureCredentials(email: String, password: String) { KeychainService.save(key: "health_email", value: email) KeychainService.save(key: "health_password", value: password) } private func fetch(_ path: String) async throws -> T { let token = try await ensureToken() guard let url = URL(string: "\(baseURL)\(path)") else { throw APIError.networkError("Неверный URL") } var req = URLRequest(url: url) req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.timeoutInterval = 15 let (data, response) = try await URLSession.shared.data(for: req) guard let http = response as? HTTPURLResponse else { throw APIError.networkError("Нет ответа") } if http.statusCode == 401 { cachedToken = nil let newToken = try await refreshToken() guard let retryURL = URL(string: "\(baseURL)\(path)") else { throw APIError.networkError("Неверный URL") } var req2 = URLRequest(url: retryURL) req2.setValue("Bearer \(newToken)", forHTTPHeaderField: "Authorization") req2.setValue("application/json", forHTTPHeaderField: "Content-Type") req2.timeoutInterval = 15 let (data2, _) = try await URLSession.shared.data(for: req2) return try JSONDecoder().decode(T.self, from: data2) } if http.statusCode >= 400 { throw APIError.serverError(http.statusCode, String(data: data, encoding: .utf8) ?? "") } return try JSONDecoder().decode(T.self, from: data) } func getLatest() async throws -> LatestHealthResponse { return try await fetch("/api/health/latest") } func getReadiness() async throws -> ReadinessResponse { return try await fetch("/api/health/readiness") } func getHeatmap(days: Int = 30) async throws -> [HeatmapEntry] { let token = try await ensureToken() guard let url = URL(string: "\(baseURL)/api/health/heatmap?days=\(days)") else { return [] } var req = URLRequest(url: url) req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") req.timeoutInterval = 15 let (data, _) = try await URLSession.shared.data(for: req) if let entries = try? JSONDecoder().decode([HeatmapEntry].self, from: data) { return entries } struct HeatmapResponse: Decodable { let data: [HeatmapEntry] } let wrapped = try JSONDecoder().decode(HeatmapResponse.self, from: data) return wrapped.data } }