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:
@@ -183,7 +183,19 @@ struct SettingsView: View {
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
Text("Pulse v1.1 • Made with ❤️").font(.caption).foregroundColor(Color(hex: "8888aa"))
|
||||
// Legal
|
||||
HStack(spacing: 16) {
|
||||
if let url = URL(string: "https://pulse.digital-home.site/privacy") {
|
||||
Link("Политика конфиденциальности", destination: url)
|
||||
.font(.caption).foregroundColor(Theme.textSecondary)
|
||||
}
|
||||
if let url = URL(string: "https://pulse.digital-home.site/terms") {
|
||||
Link("Условия", destination: url)
|
||||
.font(.caption).foregroundColor(Theme.textSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
Text("Pulse v1.1").font(.caption).foregroundColor(Color(hex: "8888aa"))
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ struct AddTaskView: View {
|
||||
TextField("Что нужно сделать?", text: $title, axis: .vertical)
|
||||
.lineLimit(1...3).foregroundColor(.white).padding(14)
|
||||
.background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.07)))
|
||||
.onChange(of: title) { if title.count > 200 { title = String(title.prefix(200)) } }
|
||||
}
|
||||
// Description
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user