add keychain credential storage, persist account config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,10 @@ struct ContentView: View {
|
||||
guard let (config, credentials) = accountSetup.buildConfig() else { return }
|
||||
do {
|
||||
try viewModel.setup(config: config, credentials: credentials)
|
||||
try KeychainService.saveCredentials(credentials, for: config.id)
|
||||
if let data = try? JSONEncoder().encode(config) {
|
||||
UserDefaults.standard.set(data, forKey: "accountConfig")
|
||||
}
|
||||
Task {
|
||||
await viewModel.syncNow()
|
||||
await viewModel.loadMailboxes(accountId: config.id)
|
||||
@@ -57,6 +61,20 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
private func loadExistingAccount() {
|
||||
// Keychain loading added in Task 15
|
||||
guard let data = UserDefaults.standard.data(forKey: "accountConfig"),
|
||||
let config = try? JSONDecoder().decode(AccountConfig.self, from: data),
|
||||
let credentials = try? KeychainService.loadCredentials(for: config.id)
|
||||
else { return }
|
||||
do {
|
||||
try viewModel.setup(config: config, credentials: credentials)
|
||||
Task {
|
||||
await viewModel.loadMailboxes(accountId: config.id)
|
||||
viewModel.startObservingThreads(accountId: config.id)
|
||||
await viewModel.syncNow()
|
||||
viewModel.startPeriodicSync()
|
||||
}
|
||||
} catch {
|
||||
// Account config exists but setup failed — show account setup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
76
Apps/MagnumOpus/Services/KeychainService.swift
Normal file
76
Apps/MagnumOpus/Services/KeychainService.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
import Foundation
|
||||
import Security
|
||||
import Models
|
||||
|
||||
enum KeychainService {
|
||||
private static let service = "de.felixfoertsch.MagnumOpus"
|
||||
|
||||
static func saveCredentials(_ credentials: Credentials, for accountId: String) throws {
|
||||
let passwordData = Data(credentials.password.utf8)
|
||||
|
||||
let deleteQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: accountId,
|
||||
]
|
||||
SecItemDelete(deleteQuery as CFDictionary)
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: accountId,
|
||||
kSecAttrLabel as String: credentials.username,
|
||||
kSecValueData as String: passwordData,
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
|
||||
]
|
||||
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainError.saveFailed(status)
|
||||
}
|
||||
}
|
||||
|
||||
static func loadCredentials(for accountId: String) throws -> Credentials? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: accountId,
|
||||
kSecReturnData as String: true,
|
||||
kSecReturnAttributes as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
]
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
guard status == errSecSuccess,
|
||||
let attrs = result as? [String: Any],
|
||||
let data = attrs[kSecValueData as String] as? Data,
|
||||
let password = String(data: data, encoding: .utf8),
|
||||
let username = attrs[kSecAttrLabel as String] as? String
|
||||
else {
|
||||
if status == errSecItemNotFound { return nil }
|
||||
throw KeychainError.loadFailed(status)
|
||||
}
|
||||
|
||||
return Credentials(username: username, password: password)
|
||||
}
|
||||
|
||||
static func deleteCredentials(for accountId: String) throws {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: accountId,
|
||||
]
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||
throw KeychainError.deleteFailed(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum KeychainError: Error {
|
||||
case saveFailed(OSStatus)
|
||||
case loadFailed(OSStatus)
|
||||
case deleteFailed(OSStatus)
|
||||
}
|
||||
Reference in New Issue
Block a user