sync app filled in
This commit is contained in:
152
apps/macos-sync/Sources/CairnquireSync/Settings/Config.swift
Normal file
152
apps/macos-sync/Sources/CairnquireSync/Settings/Config.swift
Normal file
@@ -0,0 +1,152 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
public struct SyncConfig: Codable, Equatable {
|
||||
public var serverURL: String
|
||||
public var apiToken: String?
|
||||
public var syncFolder: String?
|
||||
public var defaultUploadFolder: String?
|
||||
public var autoSync: Bool
|
||||
public var syncInterval: Int // seconds
|
||||
public var deviceId: String
|
||||
public var iconName: String // SF Symbol name
|
||||
|
||||
public static let iconOptions = [
|
||||
"tray.circle.fill",
|
||||
"arrow.triangle.2.circlepath",
|
||||
"arrow.up.doc",
|
||||
"doc.badge.arrow.up",
|
||||
"arrow.up.and.down.and.arrow.left.and.right",
|
||||
"cloud",
|
||||
"externaldrive.connected.to.line.below",
|
||||
"note.text",
|
||||
"doc.text",
|
||||
"arrow.clockwise.circle",
|
||||
"rectangle.stack.badge.plus"
|
||||
]
|
||||
|
||||
public init(
|
||||
serverURL: String = "http://localhost:8080",
|
||||
apiToken: String? = nil,
|
||||
syncFolder: String? = nil,
|
||||
defaultUploadFolder: String? = nil,
|
||||
autoSync: Bool = true,
|
||||
syncInterval: Int = 30,
|
||||
deviceId: String? = nil,
|
||||
iconName: String = "tray.circle.fill"
|
||||
) {
|
||||
self.serverURL = serverURL
|
||||
self.apiToken = apiToken
|
||||
self.syncFolder = syncFolder
|
||||
self.defaultUploadFolder = defaultUploadFolder
|
||||
self.autoSync = autoSync
|
||||
self.syncInterval = syncInterval
|
||||
self.deviceId = deviceId ?? SyncConfig.generateDeviceId()
|
||||
self.iconName = iconName
|
||||
}
|
||||
|
||||
private static func generateDeviceId() -> String {
|
||||
if let existing = UserDefaults.standard.string(forKey: "cairnquire_device_id") {
|
||||
return existing
|
||||
}
|
||||
let id = "mac-\(UUID().uuidString.replacingOccurrences(of: "-", with: "").prefix(12))"
|
||||
UserDefaults.standard.set(id, forKey: "cairnquire_device_id")
|
||||
return id
|
||||
}
|
||||
|
||||
public var serverBaseURL: URL? {
|
||||
URL(string: serverURL)
|
||||
}
|
||||
}
|
||||
|
||||
public actor ConfigStore {
|
||||
public static let shared = ConfigStore()
|
||||
|
||||
private let defaults = UserDefaults.standard
|
||||
private let keychainService = "com.cairnquire.sync"
|
||||
private let configKey = "cairnquire_config"
|
||||
|
||||
public init() {}
|
||||
|
||||
nonisolated public func load() -> SyncConfig {
|
||||
let defaults = UserDefaults.standard
|
||||
if let data = defaults.data(forKey: configKey),
|
||||
let config = try? JSONDecoder().decode(SyncConfig.self, from: data) {
|
||||
var config = config
|
||||
// Load token from keychain
|
||||
if let token = loadTokenFromKeychain() {
|
||||
config.apiToken = token
|
||||
}
|
||||
return config
|
||||
}
|
||||
return SyncConfig()
|
||||
}
|
||||
|
||||
public func save(_ config: SyncConfig) {
|
||||
let defaults = UserDefaults.standard
|
||||
var configToSave = config
|
||||
let token = config.apiToken
|
||||
configToSave.apiToken = nil
|
||||
|
||||
if let data = try? JSONEncoder().encode(configToSave) {
|
||||
defaults.set(data, forKey: configKey)
|
||||
}
|
||||
|
||||
if let token = token {
|
||||
saveTokenToKeychain(token)
|
||||
} else {
|
||||
deleteTokenFromKeychain()
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func saveTokenToKeychain(_ token: String) {
|
||||
let data = token.data(using: .utf8)!
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: keychainService,
|
||||
kSecAttrAccount as String: "api_token"
|
||||
]
|
||||
|
||||
let attributes: [String: Any] = [
|
||||
kSecValueData as String: data,
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
|
||||
]
|
||||
|
||||
if SecItemUpdate(query as CFDictionary, attributes as CFDictionary) == errSecItemNotFound {
|
||||
var newItem = query
|
||||
newItem.merge(attributes) { _, new in new }
|
||||
SecItemAdd(newItem as CFDictionary, nil)
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func loadTokenFromKeychain() -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: keychainService,
|
||||
kSecAttrAccount as String: "api_token",
|
||||
kSecReturnData as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne
|
||||
]
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
guard status == errSecSuccess,
|
||||
let data = result as? Data,
|
||||
let token = String(data: data, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
private nonisolated func deleteTokenFromKeychain() {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: keychainService,
|
||||
kSecAttrAccount as String: "api_token"
|
||||
]
|
||||
SecItemDelete(query as CFDictionary)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user