fix: health API uses JWT auth, Finance tab replaced with Health

This commit is contained in:
Cosmo
2026-03-25 17:47:33 +00:00
parent 7748392c31
commit 0d5eef1b87
3 changed files with 62 additions and 43 deletions

View File

@@ -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<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
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, "Ошибка отправки")
}
}
}

View File

@@ -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 {

View File

@@ -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") }