refactor(auth): token store, Apollo auth, and email UI

Update ApiTokenStore, AuthServiceImpl, and interceptor; adjust login/register screens, dashboard, chart, and Firebase auth tests.

Made-with: Cursor
This commit is contained in:
2026-04-06 22:20:13 +02:00
parent d34b3bf70e
commit 9165d26b72
9 changed files with 140 additions and 26 deletions

View File

@@ -1,7 +1,12 @@
package org.db3.airmq.sdk.auth
import kotlinx.coroutines.flow.Flow
interface ApiTokenStore {
fun getToken(): String?
fun saveToken(token: String): Result<Unit>
fun clearToken(): Result<Unit>
/** Emits the current token and whenever it changes (including after process start). */
fun observeToken(): Flow<String?>
}

View File

@@ -21,15 +21,16 @@ class FirebaseAuthService @Inject constructor(
) : AuthService {
override suspend fun getUser(): User? {
firebaseSessionManager.getUser()?.let { return it }
if (apiTokenStore.getToken().isNullOrBlank()) return null
val profile = localEmailAuthStore.getProfile() ?: return null
return User(
userId = profile.userId,
email = profile.email,
displayName = profile.displayName,
isAuthenticated = true
)
localEmailAuthStore.getProfile()?.let { profile ->
return User(
userId = profile.userId,
email = profile.email,
displayName = profile.displayName,
isAuthenticated = true
)
}
return firebaseSessionManager.getUser()
}
override suspend fun isAuthenticated(): Boolean {

View File

@@ -4,6 +4,9 @@ import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@Singleton
class SharedPreferencesApiTokenStore @Inject constructor(
@@ -13,17 +16,27 @@ class SharedPreferencesApiTokenStore @Inject constructor(
private val sharedPreferences =
context.getSharedPreferences(AirMqAuthPreferences.FILE_NAME, Context.MODE_PRIVATE)
override fun getToken(): String? = sharedPreferences.getString(KEY_API_TOKEN, null)
private val tokenFlow = MutableStateFlow(sharedPreferences.getString(KEY_API_TOKEN, null))
override fun getToken(): String? = tokenFlow.value
override fun saveToken(token: String): Result<Unit> = runCatching {
require(token.isNotBlank()) { "API token cannot be blank." }
sharedPreferences.edit().putString(KEY_API_TOKEN, token).apply()
if (!sharedPreferences.edit().putString(KEY_API_TOKEN, token).commit()) {
error("Failed to persist API token.")
}
tokenFlow.value = token
}
override fun clearToken(): Result<Unit> = runCatching {
sharedPreferences.edit().remove(KEY_API_TOKEN).apply()
if (!sharedPreferences.edit().remove(KEY_API_TOKEN).commit()) {
error("Failed to clear API token.")
}
tokenFlow.value = null
}
override fun observeToken(): Flow<String?> = tokenFlow.asStateFlow()
private companion object {
private const val KEY_API_TOKEN = "api_token"
}

View File

@@ -1,5 +1,6 @@
package org.db3.airmq.sdk.network
import android.util.Log
import com.apollographql.apollo.api.ApolloRequest
import com.apollographql.apollo.api.ApolloResponse
import com.apollographql.apollo.api.Operation
@@ -16,6 +17,7 @@ class ApolloAuthInterceptor(
chain: ApolloInterceptorChain
): Flow<ApolloResponse<D>> {
val token = apiTokenStore.getToken()
Log.d("DEBUG", token.toString())
val requestWithAuth = if (token.isNullOrBlank()) {
request
} else {

View File

@@ -4,6 +4,8 @@ import com.apollographql.apollo.ApolloClient
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.test.runTest
import org.db3.airmq.sdk.AuthGoogleNewMutation
import org.db3.airmq.sdk.auth.model.AuthProvider
@@ -87,6 +89,41 @@ class FirebaseAuthServiceTest {
assertTrue(service.isAuthenticated())
}
@Test
fun getUser_returnsNullWhenFirebaseUserButNoApiToken() = runTest {
val sessionManager = FakeFirebaseSessionManager(
signInResult = FirebaseSessionUser(
user = User("uid-fb", "fb@test.dev", "FB", true),
firebaseAccessToken = "ft"
)
)
val apolloClient = mockk<ApolloClient>()
val tokenStore = FakeApiTokenStore(storedToken = null)
val localEmailStore = FakeLocalEmailAuthStore()
val service = FirebaseAuthService(sessionManager, apolloClient, tokenStore, localEmailStore)
assertNull(service.getUser())
}
@Test
fun getUser_returnsFirebaseUserWhenApiTokenPresentAndNoLocalProfile() = runTest {
val sessionManager = FakeFirebaseSessionManager(
signInResult = FirebaseSessionUser(
user = User("uid-fb", "fb@test.dev", "FB", true),
firebaseAccessToken = "ft"
)
)
val apolloClient = mockk<ApolloClient>()
val tokenStore = FakeApiTokenStore(storedToken = "api-token")
val localEmailStore = FakeLocalEmailAuthStore()
val service = FirebaseAuthService(sessionManager, apolloClient, tokenStore, localEmailStore)
val user = service.getUser()
assertNotNull(user)
assertEquals("uid-fb", user!!.userId)
assertEquals("fb@test.dev", user.email)
}
@Test
fun getUser_returnsLocalProfileWhenNoFirebaseUser() = runTest {
val sessionManager = FakeFirebaseSessionManager(isSignedIn = false, signInResult = null)
@@ -153,18 +190,24 @@ private class FakeApiTokenStore(
var storedToken: String? = null,
private val saveError: Throwable? = null
) : ApiTokenStore {
private val tokenFlow = MutableStateFlow<String?>(storedToken)
override fun getToken(): String? = storedToken
override fun saveToken(token: String): Result<Unit> {
saveError?.let { return Result.failure(it) }
storedToken = token
tokenFlow.value = token
return Result.success(Unit)
}
override fun clearToken(): Result<Unit> {
storedToken = null
tokenFlow.value = null
return Result.success(Unit)
}
override fun observeToken() = tokenFlow.asStateFlow()
}
private class FakeLocalEmailAuthStore(