Add device access token authentication

This commit is contained in:
diyaa
2026-06-09 12:05:15 +02:00
parent 2945257ea7
commit 45c270c187
49 changed files with 1345 additions and 233 deletions
@@ -469,7 +469,12 @@ final class iPhoneLibraryViewModel {
baseURL: ServerEnvironment.defaultLocalBaseURL,
appVersion: "0.1.0"
)
let apiClient = URLSessionVelodyAPIClient(environment: environment)
let apiClient = URLSessionVelodyAPIClient(
environment: environment,
deviceAccessTokenProvider: {
try await keychainService.loadValue(forKey: Self.deviceAccessTokenKey)
}
)
let store = Self.makeRemoteLibraryStore()
let downloadStateStore = Self.makeRemoteTrackDownloadStateStore()
let audioFileStore = Self.makeOfflineAudioFileStore()
@@ -657,7 +662,10 @@ final class iPhoneLibraryViewModel {
private func currentOrRegisterDeviceID() async throws -> String {
if let existingDeviceID = try await keychainService.loadValue(
forKey: Self.deviceIDKey
), !existingDeviceID.isEmpty {
), !existingDeviceID.isEmpty,
let existingDeviceAccessToken = try await keychainService.loadValue(
forKey: Self.deviceAccessTokenKey
), !existingDeviceAccessToken.isEmpty {
return existingDeviceID
}
@@ -670,6 +678,10 @@ final class iPhoneLibraryViewModel {
)
try await keychainService.save(response.deviceId, forKey: Self.deviceIDKey)
try await keychainService.save(
response.deviceAccessToken,
forKey: Self.deviceAccessTokenKey
)
try await keychainService.save(
response.bootstrapToken,
forKey: Self.bootstrapTokenKey
@@ -914,6 +926,7 @@ final class iPhoneLibraryViewModel {
#endif
private static let deviceIDKey = "velody.iphone.device-id"
private static let deviceAccessTokenKey = "velody.iphone.device-access-token"
private static let bootstrapTokenKey = "velody.iphone.bootstrap-token"
private static let remoteLibraryEmptyMessage = LibrarySectionMessage(
title: "No music synced yet",
@@ -454,6 +454,71 @@ final class iPhoneLibraryViewModelPolishTests: XCTestCase {
}
}
@MainActor
final class iPhoneLibraryViewModelDeviceAuthTests: XCTestCase {
func testRefreshSyncStoresDeviceAccessTokenWhenRegistrationRuns() async throws {
let keychain = MemoryKeychainService()
let counter = RegisterCallCounter()
let viewModel = makeViewModel(
remoteTracks: [
makeRemoteTrack(
trackId: "remote-auth-store",
assetId: "asset-auth-store",
title: "Store Token"
),
],
apiClient: TestRegisterAPIClient(counter: counter),
keychainService: keychain
)
await viewModel.loadIfNeeded()
await viewModel.refreshSync()
let storedDeviceID = try await keychain.loadValue(forKey: "velody.iphone.device-id")
let storedDeviceAccessToken = try await keychain.loadValue(
forKey: "velody.iphone.device-access-token"
)
XCTAssertEqual(await counter.count, 1)
XCTAssertFalse((storedDeviceID ?? "").isEmpty)
XCTAssertFalse((storedDeviceAccessToken ?? "").isEmpty)
}
func testExistingDeviceWithoutAccessTokenReregistersCleanly() async throws {
let keychain = MemoryKeychainService()
let legacyDeviceID = "legacy-device-id"
try await keychain.save(legacyDeviceID, forKey: "velody.iphone.device-id")
try await keychain.save(
"legacy-bootstrap-token",
forKey: "velody.iphone.bootstrap-token"
)
let counter = RegisterCallCounter()
let viewModel = makeViewModel(
remoteTracks: [
makeRemoteTrack(
trackId: "remote-auth-reregister",
assetId: "asset-auth-reregister",
title: "Re-register Token"
),
],
apiClient: TestRegisterAPIClient(counter: counter),
keychainService: keychain
)
await viewModel.loadIfNeeded()
await viewModel.refreshSync()
let storedDeviceID = try await keychain.loadValue(forKey: "velody.iphone.device-id")
let storedDeviceAccessToken = try await keychain.loadValue(
forKey: "velody.iphone.device-access-token"
)
XCTAssertEqual(await counter.count, 1)
XCTAssertNotEqual(storedDeviceID, legacyDeviceID)
XCTAssertFalse((storedDeviceAccessToken ?? "").isEmpty)
}
}
private actor RegisterCallCounter {
private(set) var count = 0