89 lines
4.0 KiB
Swift
89 lines
4.0 KiB
Swift
import Foundation
|
||
|
||
class HealthAPIService {
|
||
static let shared = HealthAPIService()
|
||
let baseURL = "https://health.digital-home.site"
|
||
|
||
private var cachedToken: String? {
|
||
get { UserDefaults.standard.string(forKey: "healthJWTToken") }
|
||
set { UserDefaults.standard.set(newValue, forKey: "healthJWTToken") }
|
||
}
|
||
|
||
// Логин в health сервис (отдельный JWT)
|
||
func ensureToken() async throws -> String {
|
||
if let t = cachedToken { return t }
|
||
return try await refreshToken()
|
||
}
|
||
|
||
func refreshToken() async throws -> String {
|
||
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": "daniilklimov25@gmail.com", "password": "cosmo-health-2026"])
|
||
req.timeoutInterval = 15
|
||
let (data, _) = try await URLSession.shared.data(for: req)
|
||
struct LoginResp: Decodable { let token: String }
|
||
let resp = try JSONDecoder().decode(LoginResp.self, from: data)
|
||
cachedToken = resp.token
|
||
return resp.token
|
||
}
|
||
|
||
private func fetch<T: Decodable>(_ path: String) async throws -> T {
|
||
let token = try await ensureToken()
|
||
var req = URLRequest(url: URL(string: "\(baseURL)\(path)")!)
|
||
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("No response") }
|
||
if http.statusCode == 401 {
|
||
// Token expired, retry once
|
||
cachedToken = nil
|
||
let newToken = try await refreshToken()
|
||
var req2 = URLRequest(url: URL(string: "\(baseURL)\(path)")!)
|
||
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()
|
||
var req = URLRequest(url: URL(string: "\(baseURL)/api/health/heatmap?days=\(days)")!)
|
||
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
|
||
}
|
||
|
||
func sendHealthData(apiKey: String, payload: Data) async throws {
|
||
let url = URL(string: "\(baseURL)/api/health?key=\(apiKey)")!
|
||
var req = URLRequest(url: url)
|
||
req.httpMethod = "POST"
|
||
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||
req.httpBody = payload
|
||
req.timeoutInterval = 30
|
||
let (_, response) = try await URLSession.shared.data(for: req)
|
||
guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {
|
||
throw APIError.serverError((response as? HTTPURLResponse)?.statusCode ?? 0, "Ошибка отправки")
|
||
}
|
||
}
|
||
}
|