fix: security hardening — Keychain, no hardcoded creds, safe URLs
- Add KeychainService for encrypted token storage (auth, refresh, health JWT, API key) - Remove hardcoded email/password from HealthAPIService, store in Keychain - Move all tokens from UserDefaults to Keychain - API key sent via X-API-Key header instead of URL query parameter - Replace force unwrap URL(string:)! with guard let + throws - Fix force unwrap Calendar.date() in HealthKitService - Mark HealthKitService @MainActor for thread-safe @Published - Use withTaskGroup for parallel habit log fetching in TrackerView - Check notification permission before scheduling reminders - Add input validation (title max 200 chars) - Add privacy policy and terms links in Settings - Update CLAUDE.md with security section Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import HealthKit
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
class HealthKitService: ObservableObject {
|
||||
let healthStore = HKHealthStore()
|
||||
@Published var isSyncing = false
|
||||
@@ -20,9 +21,14 @@ class HealthKitService: ObservableObject {
|
||||
]
|
||||
|
||||
func requestAuthorization() async throws {
|
||||
guard isAvailable else { throw HealthKitError.notAvailable }
|
||||
try await healthStore.requestAuthorization(toShare: [], read: typesToRead)
|
||||
}
|
||||
|
||||
func checkAuthorization(for type: HKObjectType) -> Bool {
|
||||
healthStore.authorizationStatus(for: type) == .sharingAuthorized
|
||||
}
|
||||
|
||||
// MARK: - Collect All Metrics
|
||||
|
||||
func collectAllMetrics() async -> [[String: Any]] {
|
||||
@@ -150,7 +156,7 @@ class HealthKitService: ObservableObject {
|
||||
|
||||
// Берём последние 24 часа, чтобы захватить ночной сон
|
||||
let now = Date()
|
||||
let yesterday = Calendar.current.date(byAdding: .hour, value: -24, to: now)!
|
||||
guard let yesterday = Calendar.current.date(byAdding: .hour, value: -24, to: now) else { return [] }
|
||||
let sleepPredicate = HKQuery.predicateForSamples(withStart: yesterday, end: now)
|
||||
|
||||
return await withCheckedContinuation { cont in
|
||||
@@ -240,8 +246,8 @@ class HealthKitService: ObservableObject {
|
||||
// MARK: - Send to Server
|
||||
|
||||
func syncToServer(apiKey: String) async throws {
|
||||
await MainActor.run { isSyncing = true }
|
||||
defer { Task { @MainActor in isSyncing = false } }
|
||||
isSyncing = true
|
||||
defer { isSyncing = false }
|
||||
|
||||
guard isAvailable else {
|
||||
throw HealthKitError.notAvailable
|
||||
@@ -262,14 +268,14 @@ class HealthKitService: ObservableObject {
|
||||
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: payload)
|
||||
|
||||
let urlStr = "\(HealthAPIService.shared.baseURL)/api/health?key=\(apiKey)"
|
||||
guard let url = URL(string: urlStr) else {
|
||||
guard let url = URL(string: "\(HealthAPIService.shared.baseURL)/api/health") else {
|
||||
throw HealthKitError.invalidURL
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
|
||||
request.httpBody = jsonData
|
||||
request.timeoutInterval = 30
|
||||
|
||||
@@ -322,7 +328,7 @@ class HealthKitService: ObservableObject {
|
||||
func fetchSleepSegments() async -> [SleepSegment] {
|
||||
guard let sleepType = HKCategoryType.categoryType(forIdentifier: .sleepAnalysis) else { return [] }
|
||||
let now = Date()
|
||||
let yesterday = Calendar.current.date(byAdding: .hour, value: -24, to: now)!
|
||||
guard let yesterday = Calendar.current.date(byAdding: .hour, value: -24, to: now) else { return [] }
|
||||
let predicate = HKQuery.predicateForSamples(withStart: yesterday, end: now)
|
||||
|
||||
return await withCheckedContinuation { cont in
|
||||
|
||||
Reference in New Issue
Block a user