feat(ios): unify list cards and loading states
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
public protocol CredentialStore: Sendable {
|
||||
func get(instanceID: UUID) throws -> String?
|
||||
func set(instanceID: UUID, password: String) throws
|
||||
func remove(instanceID: UUID) throws
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import Foundation
|
||||
|
||||
enum DefaultContainer {
|
||||
private static let credentials: any CredentialStore = FileCredentialStore(
|
||||
location: documentFileURL("credentials.json")
|
||||
)
|
||||
|
||||
private static let logStore: any LogStore = FileLogStore(
|
||||
location: documentFileURL("log-center.json")
|
||||
)
|
||||
@@ -15,7 +19,7 @@ enum DefaultContainer {
|
||||
FleetViewModel(
|
||||
repository: FileInstanceRepository(location: documentFileURL("instances.json")),
|
||||
probe: ConnectionProbe(client: SimAdminClient()),
|
||||
credentials: KeychainCredentialStore()
|
||||
credentials: credentials
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,7 +30,7 @@ enum DefaultContainer {
|
||||
service: MessageService(
|
||||
client: SimAdminClient(),
|
||||
sessions: InstanceSessionStore(),
|
||||
credentials: KeychainCredentialStore()
|
||||
credentials: credentials
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -38,7 +42,7 @@ enum DefaultContainer {
|
||||
service: NotificationService(
|
||||
client: SimAdminClient(),
|
||||
sessions: InstanceSessionStore(),
|
||||
credentials: KeychainCredentialStore()
|
||||
credentials: credentials
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -51,7 +55,7 @@ enum DefaultContainer {
|
||||
service: LogCenterService(
|
||||
client: SimAdminClient(),
|
||||
sessions: InstanceSessionStore(),
|
||||
credentials: KeychainCredentialStore(),
|
||||
credentials: credentials,
|
||||
store: store
|
||||
),
|
||||
store: store
|
||||
@@ -70,7 +74,7 @@ enum DefaultContainer {
|
||||
service: DeviceService(
|
||||
client: SimAdminClient(),
|
||||
sessions: InstanceSessionStore(),
|
||||
credentials: KeychainCredentialStore()
|
||||
credentials: credentials
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import Foundation
|
||||
|
||||
public final class FileCredentialStore: @unchecked Sendable, CredentialStore {
|
||||
private struct FileModel: Codable {
|
||||
var credentials: [String: String] = [:]
|
||||
}
|
||||
|
||||
private let location: URL
|
||||
private let lock = NSLock()
|
||||
private var fileModel = FileModel()
|
||||
|
||||
public init(location: URL) {
|
||||
self.location = location
|
||||
lock.withLock {
|
||||
if let data = try? Data(contentsOf: location),
|
||||
let decoded = try? JSONDecoder().decode(FileModel.self, from: data) {
|
||||
fileModel = decoded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func get(instanceID: UUID) throws -> String? {
|
||||
lock.withLock {
|
||||
fileModel.credentials[instanceID.uuidString]
|
||||
}
|
||||
}
|
||||
|
||||
public func set(instanceID: UUID, password: String) throws {
|
||||
try lock.withLock {
|
||||
fileModel.credentials[instanceID.uuidString] = password
|
||||
try write()
|
||||
}
|
||||
}
|
||||
|
||||
public func remove(instanceID: UUID) throws {
|
||||
try lock.withLock {
|
||||
fileModel.credentials.removeValue(forKey: instanceID.uuidString)
|
||||
try write()
|
||||
}
|
||||
}
|
||||
|
||||
private func write() throws {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.sortedKeys]
|
||||
let data = try encoder.encode(fileModel)
|
||||
try data.write(to: location, options: [.atomic, .completeFileProtection])
|
||||
try FileManager.default.setAttributes(
|
||||
[.posixPermissions: 0o600],
|
||||
ofItemAtPath: location.path
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
public protocol CredentialStore: Sendable {
|
||||
func get(instanceID: UUID) throws -> String?
|
||||
func set(instanceID: UUID, password: String) throws
|
||||
func remove(instanceID: UUID) throws
|
||||
}
|
||||
|
||||
public struct KeychainCredentialStore: CredentialStore {
|
||||
private let service = "fun.chick.multi-simadmin.instance-password"
|
||||
|
||||
public init() {}
|
||||
|
||||
public func get(instanceID: UUID) throws -> String? {
|
||||
var query = baseQuery(instanceID)
|
||||
query[kSecReturnData as String] = true
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
guard status == errSecSuccess else {
|
||||
if status == errSecItemNotFound { return nil }
|
||||
throw CredentialStoreError.keychainFailed
|
||||
}
|
||||
guard let data = item as? Data else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
public func set(instanceID: UUID, password: String) throws {
|
||||
try self.remove(instanceID: instanceID)
|
||||
let attributes: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: instanceID.uuidString,
|
||||
kSecValueData as String: Data(password.utf8),
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
||||
]
|
||||
let status = SecItemAdd(attributes as CFDictionary, nil)
|
||||
guard status == errSecSuccess else {
|
||||
throw CredentialStoreError.keychainFailed
|
||||
}
|
||||
}
|
||||
|
||||
public func remove(instanceID: UUID) throws {
|
||||
let status = SecItemDelete(baseQuery(instanceID) as CFDictionary)
|
||||
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||
throw CredentialStoreError.keychainFailed
|
||||
}
|
||||
}
|
||||
|
||||
private func baseQuery(_ instanceID: UUID) -> [String: Any] {
|
||||
[
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: instanceID.uuidString
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
public enum CredentialStoreError: Error, Equatable {
|
||||
case keychainFailed
|
||||
}
|
||||
Reference in New Issue
Block a user