From 0d5eef1b8713d1e65478131ab576af85685d717a Mon Sep 17 00:00:00 2001 From: Cosmo Date: Wed, 25 Mar 2026 17:47:33 +0000 Subject: [PATCH] fix: health API uses JWT auth, Finance tab replaced with Health --- PulseHealth/Services/HealthAPIService.swift | 86 ++++++++++++++------- PulseHealth/Views/Health/HealthView.swift | 13 +--- PulseHealth/Views/MainTabView.swift | 6 +- 3 files changed, 62 insertions(+), 43 deletions(-) diff --git a/PulseHealth/Services/HealthAPIService.swift b/PulseHealth/Services/HealthAPIService.swift index 7b0bf3d..18ffaee 100644 --- a/PulseHealth/Services/HealthAPIService.swift +++ b/PulseHealth/Services/HealthAPIService.swift @@ -3,43 +3,72 @@ 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 makeRequest(_ path: String, token: String? = nil, apiKey: String? = nil) -> URLRequest { + private func fetch(_ 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 - if let t = token { req.setValue("Bearer \(t)", forHTTPHeaderField: "Authorization") } - if let k = apiKey { req.setValue(k, forHTTPHeaderField: "x-api-key") } - return req + 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(apiKey: String) async throws -> LatestHealthResponse { - let req = makeRequest("/api/health/latest", apiKey: apiKey) - let (data, response) = try await URLSession.shared.data(for: req) - guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { - throw APIError.networkError("Latest недоступен") - } - return try JSONDecoder().decode(LatestHealthResponse.self, from: data) + func getLatest() async throws -> LatestHealthResponse { + return try await fetch("/api/health/latest") } - func getReadiness(apiKey: String) async throws -> ReadinessResponse { - let req = makeRequest("/api/health/readiness", apiKey: apiKey) - let (data, response) = try await URLSession.shared.data(for: req) - guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { - throw APIError.networkError("Readiness недоступен") - } - return try JSONDecoder().decode(ReadinessResponse.self, from: data) + func getReadiness() async throws -> ReadinessResponse { + return try await fetch("/api/health/readiness") } - func getHeatmap(apiKey: String, days: Int = 7) async throws -> [HeatmapEntry] { - let req = makeRequest("/api/health/heatmap?days=\(days)", apiKey: apiKey) - let (data, response) = try await URLSession.shared.data(for: req) - guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { - throw APIError.networkError("Heatmap недоступен") - } - if let entries = try? JSONDecoder().decode([HeatmapEntry].self, from: data) { - return entries - } + 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 } @@ -53,8 +82,7 @@ class HealthAPIService { req.timeoutInterval = 30 let (_, response) = try await URLSession.shared.data(for: req) guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else { - let code = (response as? HTTPURLResponse)?.statusCode ?? 0 - throw APIError.serverError(code, "Ошибка отправки health данных") + throw APIError.serverError((response as? HTTPURLResponse)?.statusCode ?? 0, "Ошибка отправки") } } } diff --git a/PulseHealth/Views/Health/HealthView.swift b/PulseHealth/Views/Health/HealthView.swift index be3cb9c..2c6b4c6 100644 --- a/PulseHealth/Views/Health/HealthView.swift +++ b/PulseHealth/Views/Health/HealthView.swift @@ -177,11 +177,9 @@ struct HealthView: View { func loadData(refresh: Bool = false) async { if !refresh { isLoading = true } - let apiKey = authManager.healthApiKey - - async let r = HealthAPIService.shared.getReadiness(apiKey: apiKey) - async let l = HealthAPIService.shared.getLatest(apiKey: apiKey) - async let h = HealthAPIService.shared.getHeatmap(apiKey: apiKey, days: 7) + async let r = HealthAPIService.shared.getReadiness() + async let l = HealthAPIService.shared.getLatest() + async let h = HealthAPIService.shared.getHeatmap(days: 7) readiness = try? await r latest = try? await l @@ -198,11 +196,6 @@ struct HealthView: View { return } - guard !authManager.healthApiKey.isEmpty else { - showToastMessage("Health API ключ не найден", success: false) - return - } - UIImpactFeedbackGenerator(style: .medium).impactOccurred() do { diff --git a/PulseHealth/Views/MainTabView.swift b/PulseHealth/Views/MainTabView.swift index 8c7baf5..0e80117 100644 --- a/PulseHealth/Views/MainTabView.swift +++ b/PulseHealth/Views/MainTabView.swift @@ -16,10 +16,8 @@ struct MainTabView: View { TrackerView() .tabItem { Label("Трекер", systemImage: "chart.bar.fill") } - if authManager.userId == 1 { - FinanceView() - .tabItem { Label("Финансы", systemImage: "creditcard.fill") } - } + HealthView() + .tabItem { Label("Здоровье", systemImage: "heart.fill") } SavingsView() .tabItem { Label("Накопления", systemImage: "building.columns.fill") }