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:
2026-04-06 14:11:10 +03:00
parent 28fca1de89
commit 44c759c190
10 changed files with 167 additions and 58 deletions

View File

@@ -127,12 +127,22 @@ struct HabitListView: View {
func loadHabits(refresh: Bool = false) async {
if !refresh { isLoading = true }
var loaded = (try? await APIService.shared.getHabits(token: authManager.token, includeArchived: true)) ?? []
// Enrich with completedToday
let today = todayStr()
for i in loaded.indices where loaded[i].isArchived != true {
let logs = (try? await APIService.shared.getHabitLogs(token: authManager.token, habitId: loaded[i].id, days: 1)) ?? []
loaded[i].completedToday = logs.contains { $0.dateOnly == today }
// Fetch all logs in parallel, then update array
let activeIndices = loaded.indices.filter { loaded[$0].isArchived != true }
let logResults = await withTaskGroup(of: (Int, Bool).self) { group in
for i in activeIndices {
let habitId = loaded[i].id
group.addTask {
let logs = (try? await APIService.shared.getHabitLogs(token: self.authManager.token, habitId: habitId, days: 1)) ?? []
return (i, logs.contains { $0.dateOnly == today })
}
}
var results: [(Int, Bool)] = []
for await result in group { results.append(result) }
return results
}
for (i, completed) in logResults { loaded[i].completedToday = completed }
habits = loaded
isLoading = false
}