Ensure authentication token is always sent when it exists (#193)
* Ensure auth token is sent if it is present * Allow using login token for other requests while API token is created * Update version code
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.util.*
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("gq.kirmanak.mealient.application")
|
id("gq.kirmanak.mealient.application")
|
||||||
@@ -17,8 +17,8 @@ plugins {
|
|||||||
android {
|
android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "gq.kirmanak.mealient"
|
applicationId = "gq.kirmanak.mealient"
|
||||||
versionCode = 33
|
versionCode = 34
|
||||||
versionName = "0.4.4"
|
versionName = "0.4.5"
|
||||||
testInstrumentationRunner = "gq.kirmanak.mealient.MealientTestRunner"
|
testInstrumentationRunner = "gq.kirmanak.mealient.MealientTestRunner"
|
||||||
testInstrumentationRunnerArguments += mapOf("clearPackageData" to "true")
|
testInstrumentationRunnerArguments += mapOf("clearPackageData" to "true")
|
||||||
resourceConfigurations += listOf("en", "es", "ru", "fr", "nl", "pt", "de")
|
resourceConfigurations += listOf("en", "es", "ru", "fr", "nl", "pt", "de")
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import gq.kirmanak.mealient.data.auth.AuthDataSource
|
|||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||||
import gq.kirmanak.mealient.datasource.AuthenticationProvider
|
import gq.kirmanak.mealient.datasource.AuthenticationProvider
|
||||||
import gq.kirmanak.mealient.datasource.SignOutHandler
|
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -14,7 +13,6 @@ class AuthRepoImpl @Inject constructor(
|
|||||||
private val authStorage: AuthStorage,
|
private val authStorage: AuthStorage,
|
||||||
private val authDataSource: AuthDataSource,
|
private val authDataSource: AuthDataSource,
|
||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
private val signOutHandler: SignOutHandler,
|
|
||||||
private val credentialsLogRedactor: CredentialsLogRedactor,
|
private val credentialsLogRedactor: CredentialsLogRedactor,
|
||||||
) : AuthRepo, AuthenticationProvider {
|
) : AuthRepo, AuthenticationProvider {
|
||||||
|
|
||||||
@@ -23,12 +21,14 @@ class AuthRepoImpl @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun authenticate(email: String, password: String) {
|
override suspend fun authenticate(email: String, password: String) {
|
||||||
logger.v { "authenticate() called" }
|
logger.v { "authenticate() called" }
|
||||||
|
|
||||||
credentialsLogRedactor.set(email, password)
|
credentialsLogRedactor.set(email, password)
|
||||||
val token = authDataSource.authenticate(email, password)
|
val token = authDataSource.authenticate(email, password)
|
||||||
|
credentialsLogRedactor.clear()
|
||||||
authStorage.setAuthToken(token)
|
authStorage.setAuthToken(token)
|
||||||
|
|
||||||
val apiToken = authDataSource.createApiToken(API_TOKEN_NAME)
|
val apiToken = authDataSource.createApiToken(API_TOKEN_NAME)
|
||||||
authStorage.setAuthToken(apiToken)
|
authStorage.setAuthToken(apiToken)
|
||||||
credentialsLogRedactor.clear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAuthToken(): String? = authStorage.getAuthToken()
|
override suspend fun getAuthToken(): String? = authStorage.getAuthToken()
|
||||||
@@ -36,7 +36,6 @@ class AuthRepoImpl @Inject constructor(
|
|||||||
override suspend fun logout() {
|
override suspend fun logout() {
|
||||||
logger.v { "logout() called" }
|
logger.v { "logout() called" }
|
||||||
authStorage.setAuthToken(null)
|
authStorage.setAuthToken(null)
|
||||||
signOutHandler.signOut()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.SharedPreferences
|
|||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||||
|
import gq.kirmanak.mealient.datasource.TokenChangeListener
|
||||||
import gq.kirmanak.mealient.datastore.DataStoreModule.Companion.ENCRYPTED
|
import gq.kirmanak.mealient.datastore.DataStoreModule.Companion.ENCRYPTED
|
||||||
import gq.kirmanak.mealient.extensions.prefsChangeFlow
|
import gq.kirmanak.mealient.extensions.prefsChangeFlow
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
@@ -19,6 +20,7 @@ import javax.inject.Singleton
|
|||||||
@Singleton
|
@Singleton
|
||||||
class AuthStorageImpl @Inject constructor(
|
class AuthStorageImpl @Inject constructor(
|
||||||
@Named(ENCRYPTED) private val sharedPreferences: SharedPreferences,
|
@Named(ENCRYPTED) private val sharedPreferences: SharedPreferences,
|
||||||
|
private val tokenChangeListener: TokenChangeListener,
|
||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
) : AuthStorage {
|
) : AuthStorage {
|
||||||
|
|
||||||
@@ -28,7 +30,11 @@ class AuthStorageImpl @Inject constructor(
|
|||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
private val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
private val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||||
|
|
||||||
override suspend fun setAuthToken(authToken: String?) = putString(AUTH_TOKEN_KEY, authToken)
|
override suspend fun setAuthToken(authToken: String?) {
|
||||||
|
logger.v { "setAuthToken() called with: authToken = $authToken" }
|
||||||
|
tokenChangeListener.onTokenChange()
|
||||||
|
putString(AUTH_TOKEN_KEY, authToken)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getAuthToken(): String? = getString(AUTH_TOKEN_KEY)
|
override suspend fun getAuthToken(): String? = getString(AUTH_TOKEN_KEY)
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class AuthenticationViewModel @Inject constructor(
|
|||||||
_uiState.value = OperationUiState.Progress()
|
_uiState.value = OperationUiState.Progress()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = runCatchingExceptCancel { authRepo.authenticate(email, password) }
|
val result = runCatchingExceptCancel { authRepo.authenticate(email, password) }
|
||||||
|
logger.d { "Authentication result = $result" }
|
||||||
_uiState.value = OperationUiState.fromResult(result)
|
_uiState.value = OperationUiState.fromResult(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||||
import gq.kirmanak.mealient.datasource.SignOutHandler
|
|
||||||
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_API_TOKEN
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_API_TOKEN
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
|
||||||
@@ -31,9 +30,6 @@ class AuthRepoImplTest : BaseUnitTest() {
|
|||||||
@MockK(relaxUnitFun = true)
|
@MockK(relaxUnitFun = true)
|
||||||
lateinit var storage: AuthStorage
|
lateinit var storage: AuthStorage
|
||||||
|
|
||||||
@MockK(relaxUnitFun = true)
|
|
||||||
lateinit var signOutHandler: SignOutHandler
|
|
||||||
|
|
||||||
@RelaxedMockK
|
@RelaxedMockK
|
||||||
lateinit var credentialsLogRedactor: CredentialsLogRedactor
|
lateinit var credentialsLogRedactor: CredentialsLogRedactor
|
||||||
|
|
||||||
@@ -46,7 +42,6 @@ class AuthRepoImplTest : BaseUnitTest() {
|
|||||||
authStorage = storage,
|
authStorage = storage,
|
||||||
authDataSource = dataSource,
|
authDataSource = dataSource,
|
||||||
logger = logger,
|
logger = logger,
|
||||||
signOutHandler = signOutHandler,
|
|
||||||
credentialsLogRedactor = credentialsLogRedactor,
|
credentialsLogRedactor = credentialsLogRedactor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||||
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.AUTH_TOKEN_KEY
|
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.AUTH_TOKEN_KEY
|
||||||
|
import gq.kirmanak.mealient.datasource.TokenChangeListener
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN
|
||||||
import gq.kirmanak.mealient.test.HiltRobolectricTest
|
import gq.kirmanak.mealient.test.HiltRobolectricTest
|
||||||
import io.mockk.MockKAnnotations
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@@ -28,11 +30,14 @@ class AuthStorageImplTest : HiltRobolectricTest() {
|
|||||||
|
|
||||||
lateinit var sharedPreferences: SharedPreferences
|
lateinit var sharedPreferences: SharedPreferences
|
||||||
|
|
||||||
|
@MockK
|
||||||
|
lateinit var tokenChangeListener: TokenChangeListener
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockKAnnotations.init(this)
|
MockKAnnotations.init(this)
|
||||||
sharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE)
|
sharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE)
|
||||||
subject = AuthStorageImpl(sharedPreferences, logger)
|
subject = AuthStorageImpl(sharedPreferences, tokenChangeListener, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package gq.kirmanak.mealient.datasource
|
|
||||||
|
|
||||||
interface SignOutHandler {
|
|
||||||
|
|
||||||
fun signOut()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource
|
||||||
|
|
||||||
|
fun interface TokenChangeListener {
|
||||||
|
|
||||||
|
fun onTokenChange()
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import dagger.multibindings.IntoSet
|
import dagger.multibindings.IntoSet
|
||||||
import gq.kirmanak.mealient.datasource.SignOutHandler
|
import gq.kirmanak.mealient.datasource.TokenChangeListener
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -37,5 +37,5 @@ internal interface KtorModule {
|
|||||||
fun bindKtorClientBuilder(impl: KtorClientBuilderImpl) : KtorClientBuilder
|
fun bindKtorClientBuilder(impl: KtorClientBuilderImpl) : KtorClientBuilder
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
fun bindSignOutHandler(impl: SignOutHandlerKtor) : SignOutHandler
|
fun bindSignOutHandler(impl: TokenChangeListenerKtor): TokenChangeListener
|
||||||
}
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package gq.kirmanak.mealient.datasource.ktor
|
|
||||||
|
|
||||||
import gq.kirmanak.mealient.datasource.SignOutHandler
|
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import io.ktor.client.plugins.auth.Auth
|
|
||||||
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
|
|
||||||
import io.ktor.client.plugins.plugin
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class SignOutHandlerKtor @Inject constructor(
|
|
||||||
private val httpClient: HttpClient,
|
|
||||||
) : SignOutHandler {
|
|
||||||
|
|
||||||
override fun signOut() {
|
|
||||||
httpClient.plugin(Auth)
|
|
||||||
.providers
|
|
||||||
.filterIsInstance<BearerAuthProvider>()
|
|
||||||
.forEach { it.clearToken() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource.ktor
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.datasource.TokenChangeListener
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.plugins.auth.Auth
|
||||||
|
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
|
||||||
|
import io.ktor.client.plugins.plugin
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class TokenChangeListenerKtor @Inject constructor(
|
||||||
|
private val httpClient: HttpClient,
|
||||||
|
private val logger: Logger,
|
||||||
|
) : TokenChangeListener {
|
||||||
|
|
||||||
|
override fun onTokenChange() {
|
||||||
|
logger.v { "onTokenChange() called" }
|
||||||
|
httpClient.plugin(Auth)
|
||||||
|
.providers
|
||||||
|
.filterIsInstance<BearerAuthProvider>()
|
||||||
|
.forEach {
|
||||||
|
logger.d { "onTokenChange(): removing the token" }
|
||||||
|
it.clearToken()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
fastlane/metadata/android/en-US/changelogs/34.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/34.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Fix authentication issues with some Mealie instances.
|
||||||
|
Allow sending logs to the developer.
|
||||||
Reference in New Issue
Block a user