fix(conversation-assistant): add missing API and view files
- Add checkVerification method to APIClient - Update MenuBarView with registrationCode binding 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a67a2cc110
commit
190802c6f1
2 changed files with 111 additions and 61 deletions
|
|
@ -20,6 +20,8 @@ class APIClient {
|
|||
// Load saved auth
|
||||
authToken = KeychainHelper.load(key: "authToken")
|
||||
deviceId = UserDefaults.standard.string(forKey: "deviceId")
|
||||
|
||||
NSLog("APIClient: init - baseURL: \(baseURL), deviceId: \(deviceId ?? "nil"), authToken: \(authToken != nil ? "present" : "nil")")
|
||||
}
|
||||
|
||||
func registerDevice() async throws -> (deviceId: String, code: String) {
|
||||
|
|
@ -55,42 +57,39 @@ class APIClient {
|
|||
return (id, code)
|
||||
}
|
||||
|
||||
func verifyDevice(code: String) async throws {
|
||||
let currentDeviceId: String
|
||||
if let existingId = deviceId {
|
||||
currentDeviceId = existingId
|
||||
} else {
|
||||
// Register first if no device ID
|
||||
let (newId, _) = try await registerDevice()
|
||||
self.deviceId = newId
|
||||
currentDeviceId = newId
|
||||
func checkVerification() async throws -> Bool {
|
||||
guard let currentDeviceId = deviceId else {
|
||||
NSLog("APIClient: checkVerification - no deviceId")
|
||||
return false
|
||||
}
|
||||
|
||||
let params: [String: String] = [
|
||||
"deviceId": currentDeviceId,
|
||||
"code": code
|
||||
]
|
||||
let url = "\(baseURL)/api/devices/\(currentDeviceId)/status"
|
||||
NSLog("APIClient: checkVerification - calling \(url)")
|
||||
|
||||
let response = try await AF.request(
|
||||
"\(baseURL)/api/devices/verify",
|
||||
method: .post,
|
||||
parameters: params,
|
||||
encoding: JSONEncoding.default
|
||||
url,
|
||||
method: .get
|
||||
).serializingData().value
|
||||
|
||||
let json = try JSON(data: response)
|
||||
NSLog("APIClient: checkVerification - response: \(json)")
|
||||
|
||||
guard json["success"].boolValue else {
|
||||
throw APIError.requestFailed(json["error"]["message"].stringValue)
|
||||
if json["success"].boolValue && json["data"]["isActive"].boolValue {
|
||||
// Device is verified - get auth token
|
||||
if let token = json["data"]["token"].string {
|
||||
NSLog("APIClient: checkVerification - got token, saving to keychain")
|
||||
authToken = token
|
||||
KeychainHelper.save(key: "authToken", value: token)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
let token = json["data"]["token"].stringValue
|
||||
authToken = token
|
||||
KeychainHelper.save(key: "authToken", value: token)
|
||||
NSLog("APIClient: checkVerification - not active")
|
||||
return false
|
||||
}
|
||||
|
||||
func syncMessages(_ data: SyncMessagesPayload) async throws -> Int {
|
||||
guard let token = authToken else {
|
||||
NSLog("APIClient: syncMessages - no auth token!")
|
||||
throw APIError.notAuthenticated
|
||||
}
|
||||
|
||||
|
|
@ -98,21 +97,34 @@ class APIClient {
|
|||
"Authorization": "Bearer \(token)"
|
||||
]
|
||||
|
||||
let response = try await AF.request(
|
||||
"\(baseURL)/api/sync/messages",
|
||||
method: .post,
|
||||
parameters: data.dictionary,
|
||||
encoding: JSONEncoding.default,
|
||||
headers: headers
|
||||
).serializingData().value
|
||||
let url = "\(baseURL)/api/sync/messages"
|
||||
NSLog("APIClient: syncMessages - calling \(url) with \(data.messages.count) messages")
|
||||
|
||||
let json = try JSON(data: response)
|
||||
do {
|
||||
let response = try await AF.request(
|
||||
url,
|
||||
method: .post,
|
||||
parameters: data.dictionary,
|
||||
encoding: JSONEncoding.default,
|
||||
headers: headers
|
||||
).serializingData().value
|
||||
|
||||
guard json["success"].boolValue else {
|
||||
throw APIError.requestFailed(json["error"]["message"].stringValue)
|
||||
let json = try JSON(data: response)
|
||||
NSLog("APIClient: syncMessages - response: \(json)")
|
||||
|
||||
guard json["success"].boolValue else {
|
||||
let errorMsg = json["error"]["message"].stringValue
|
||||
let errorCode = json["statusCode"].intValue
|
||||
NSLog("APIClient: syncMessages - API error: \(errorMsg) (code: \(errorCode))")
|
||||
throw APIError.requestFailed(errorMsg.isEmpty ? "Unknown API error" : errorMsg)
|
||||
}
|
||||
|
||||
return json["data"]["synced"].intValue
|
||||
} catch {
|
||||
NSLog("APIClient: syncMessages - Error: \(error)")
|
||||
NSLog("APIClient: syncMessages - Error type: \(type(of: error))")
|
||||
throw error
|
||||
}
|
||||
|
||||
return json["data"]["synced"].intValue
|
||||
}
|
||||
|
||||
private func getHardwareId() -> String {
|
||||
|
|
@ -198,27 +210,51 @@ enum APIError: LocalizedError {
|
|||
}
|
||||
|
||||
class KeychainHelper {
|
||||
private static let service = "com.lilith.conversation-assistant"
|
||||
|
||||
static func save(key: String, value: String) {
|
||||
let data = value.data(using: .utf8)!
|
||||
let query: [String: Any] = [
|
||||
|
||||
// First try to delete any existing item
|
||||
let deleteQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecValueData as String: data
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key
|
||||
]
|
||||
SecItemDelete(query as CFDictionary)
|
||||
SecItemAdd(query as CFDictionary, nil)
|
||||
let deleteStatus = SecItemDelete(deleteQuery as CFDictionary)
|
||||
NSLog("KeychainHelper: delete status for \(key): \(deleteStatus)")
|
||||
|
||||
// Now add the new item
|
||||
let addQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecValueData as String: data,
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
|
||||
]
|
||||
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
|
||||
NSLog("KeychainHelper: add status for \(key): \(addStatus)")
|
||||
|
||||
if addStatus != errSecSuccess {
|
||||
NSLog("KeychainHelper: Failed to save \(key) - error: \(addStatus)")
|
||||
}
|
||||
}
|
||||
|
||||
static func load(key: String) -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecReturnData as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne
|
||||
]
|
||||
var result: AnyObject?
|
||||
SecItemCopyMatching(query as CFDictionary, &result)
|
||||
guard let data = result as? Data else { return nil }
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
NSLog("KeychainHelper: load status for \(key): \(status)")
|
||||
|
||||
guard status == errSecSuccess, let data = result as? Data else {
|
||||
return nil
|
||||
}
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,33 +88,47 @@ struct MenuBarView: View {
|
|||
|
||||
private var authenticationView: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "lock.shield")
|
||||
Image(systemName: "link.badge.plus")
|
||||
.font(.system(size: 48))
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
Text("Not Connected")
|
||||
Text("Connect This Device")
|
||||
.font(.headline)
|
||||
|
||||
Text("Enter the code shown in your admin panel to connect this device.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
if viewModel.isRegistering {
|
||||
ProgressView("Registering...")
|
||||
} else if !viewModel.registrationCode.isEmpty {
|
||||
Text("Enter this code in your admin panel:")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
TextField("Device Code", text: $viewModel.authCode)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
|
||||
Button(action: viewModel.authenticate) {
|
||||
HStack {
|
||||
if viewModel.isAuthenticating {
|
||||
ProgressView()
|
||||
.scaleEffect(0.7)
|
||||
Text(viewModel.registrationCode)
|
||||
.font(.system(size: 32, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(.accentColor)
|
||||
.padding(.vertical, 8)
|
||||
.onTapGesture {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(viewModel.registrationCode, forType: .string)
|
||||
}
|
||||
Text("Connect")
|
||||
|
||||
Text("Tap to copy • Waiting for verification...")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Button("Get New Code") {
|
||||
viewModel.refreshCode()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(.link)
|
||||
} else {
|
||||
Text("Unable to connect to server")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Button("Retry") {
|
||||
viewModel.registerDevice()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(viewModel.authCode.isEmpty || viewModel.isAuthenticating)
|
||||
|
||||
if let error = viewModel.authError {
|
||||
Text(error)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue