Files
pulse-mobile/PulseHealth/App.swift
Daniil Klimov 44c759c190 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>
2026-04-06 14:11:10 +03:00

135 lines
4.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import BackgroundTasks
extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default: (a, r, g, b) = (255, 0, 0, 0)
}
self.init(.sRGB, red: Double(r)/255, green: Double(g)/255, blue: Double(b)/255, opacity: Double(a)/255)
}
}
@main
struct PulseApp: App {
@StateObject private var authManager = AuthManager()
init() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.daniil.pulsehealth.healthsync", using: nil) { task in
Self.handleHealthSync(task: task as! BGProcessingTask)
}
}
var body: some Scene {
WindowGroup {
Group {
if authManager.isLoggedIn {
MainTabView()
} else {
LoginView()
}
}
.environmentObject(authManager)
.onAppear {
APIService.shared.authManager = authManager
// Migrate: set health API key in Keychain if not yet
if authManager.healthApiKey.isEmpty {
authManager.setHealthApiKey("health-cosmo-2026")
HealthAPIService.configureCredentials(email: "daniilklimov25@gmail.com", password: "cosmo-health-2026")
}
Self.scheduleHealthSync()
}
}
}
static func scheduleHealthSync() {
let request = BGProcessingTaskRequest(identifier: "com.daniil.pulsehealth.healthsync")
request.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) // 30 минут
request.requiresNetworkConnectivity = true
try? BGTaskScheduler.shared.submit(request)
}
static func handleHealthSync(task: BGProcessingTask) {
// Запланировать следующий синк
scheduleHealthSync()
let syncTask = Task {
let service = HealthKitService()
let apiKey = KeychainService.load(key: KeychainService.healthApiKeyKey) ?? ""
guard !apiKey.isEmpty else { return }
try await service.syncToServer(apiKey: apiKey)
}
task.expirationHandler = { syncTask.cancel() }
Task {
do {
try await syncTask.value
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
}
}
class AuthManager: ObservableObject {
@Published var isLoggedIn: Bool = false
@Published var token: String = ""
@Published var refreshToken: String = ""
@Published var userName: String = ""
@Published var userId: Int = 0
@Published var healthApiKey: String = ""
init() {
token = KeychainService.load(key: KeychainService.tokenKey) ?? ""
refreshToken = KeychainService.load(key: KeychainService.refreshTokenKey) ?? ""
healthApiKey = KeychainService.load(key: KeychainService.healthApiKeyKey) ?? ""
userName = UserDefaults.standard.string(forKey: "userName") ?? ""
userId = UserDefaults.standard.integer(forKey: "userId")
isLoggedIn = !token.isEmpty
}
func login(token: String, refreshToken: String? = nil, user: UserInfo) {
self.token = token
self.refreshToken = refreshToken ?? ""
self.userName = user.displayName
self.userId = user.id
KeychainService.save(key: KeychainService.tokenKey, value: token)
if let rt = refreshToken { KeychainService.save(key: KeychainService.refreshTokenKey, value: rt) }
UserDefaults.standard.set(user.displayName, forKey: "userName")
UserDefaults.standard.set(user.id, forKey: "userId")
isLoggedIn = true
}
func updateTokens(accessToken: String, refreshToken: String?) {
self.token = accessToken
KeychainService.save(key: KeychainService.tokenKey, value: accessToken)
if let rt = refreshToken {
self.refreshToken = rt
KeychainService.save(key: KeychainService.refreshTokenKey, value: rt)
}
}
func setHealthApiKey(_ key: String) {
self.healthApiKey = key
KeychainService.save(key: KeychainService.healthApiKeyKey, value: key)
}
func logout() {
token = ""; refreshToken = ""; userName = ""; userId = 0
KeychainService.delete(key: KeychainService.tokenKey)
KeychainService.delete(key: KeychainService.refreshTokenKey)
UserDefaults.standard.removeObject(forKey: "userName")
UserDefaults.standard.removeObject(forKey: "userId")
isLoggedIn = false
}
}