Extract Base URL from authentication
This commit is contained in:
@@ -2,7 +2,7 @@ package gq.kirmanak.mealient.data.auth
|
||||
|
||||
interface AuthDataSource {
|
||||
/**
|
||||
* Tries to acquire authentication token using the provided credentials on specified server.
|
||||
* Tries to acquire authentication token using the provided credentials
|
||||
*/
|
||||
suspend fun authenticate(username: String, password: String, baseUrl: String): String
|
||||
suspend fun authenticate(username: String, password: String): String
|
||||
}
|
||||
@@ -3,11 +3,8 @@ package gq.kirmanak.mealient.data.auth
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AuthRepo {
|
||||
suspend fun authenticate(username: String, password: String, baseUrl: String)
|
||||
|
||||
suspend fun getBaseUrl(): String?
|
||||
|
||||
suspend fun requireBaseUrl(): String
|
||||
suspend fun authenticate(username: String, password: String)
|
||||
|
||||
suspend fun getAuthHeader(): String?
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ package gq.kirmanak.mealient.data.auth
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AuthStorage {
|
||||
suspend fun storeAuthData(authHeader: String, baseUrl: String)
|
||||
|
||||
suspend fun getBaseUrl(): String?
|
||||
suspend fun storeAuthData(authHeader: String)
|
||||
|
||||
suspend fun getAuthHeader(): String?
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package gq.kirmanak.mealient.data.auth.impl
|
||||
|
||||
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
||||
import gq.kirmanak.mealient.data.auth.impl.AuthenticationError.*
|
||||
import gq.kirmanak.mealient.data.network.ErrorDetail
|
||||
import gq.kirmanak.mealient.data.network.NetworkError.*
|
||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||
import gq.kirmanak.mealient.extensions.decodeErrorBodyOrNull
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -20,13 +20,14 @@ class AuthDataSourceImpl @Inject constructor(
|
||||
private val json: Json,
|
||||
) : AuthDataSource {
|
||||
|
||||
override suspend fun authenticate(
|
||||
username: String,
|
||||
password: String,
|
||||
baseUrl: String
|
||||
): String {
|
||||
Timber.v("authenticate() called with: username = $username, password = $password, baseUrl = $baseUrl")
|
||||
val authService = authServiceFactory.provideService(baseUrl)
|
||||
override suspend fun authenticate(username: String, password: String): String {
|
||||
Timber.v("authenticate() called with: username = $username, password = $password")
|
||||
val authService = try {
|
||||
authServiceFactory.provideService()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "authenticate: can't create Retrofit service")
|
||||
throw MalformedUrl(e)
|
||||
}
|
||||
val response = sendRequest(authService, username, password)
|
||||
val accessToken = parseToken(response)
|
||||
Timber.v("authenticate() returned: $accessToken")
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
package gq.kirmanak.mealient.data.auth.impl
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||
import gq.kirmanak.mealient.data.auth.impl.AuthenticationError.MalformedUrl
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -19,23 +15,13 @@ class AuthRepoImpl @Inject constructor(
|
||||
private val storage: AuthStorage,
|
||||
) : AuthRepo {
|
||||
|
||||
override suspend fun authenticate(
|
||||
username: String,
|
||||
password: String,
|
||||
baseUrl: String
|
||||
) {
|
||||
Timber.v("authenticate() called with: username = $username, password = $password, baseUrl = $baseUrl")
|
||||
val url = parseBaseUrl(baseUrl)
|
||||
val accessToken = dataSource.authenticate(username, password, url)
|
||||
override suspend fun authenticate(username: String, password: String) {
|
||||
Timber.v("authenticate() called with: username = $username, password = $password")
|
||||
val accessToken = dataSource.authenticate(username, password)
|
||||
Timber.d("authenticate result is \"$accessToken\"")
|
||||
storage.storeAuthData(AUTH_HEADER_FORMAT.format(accessToken), url)
|
||||
storage.storeAuthData(AUTH_HEADER_FORMAT.format(accessToken))
|
||||
}
|
||||
|
||||
override suspend fun getBaseUrl(): String? = storage.getBaseUrl()
|
||||
|
||||
override suspend fun requireBaseUrl(): String =
|
||||
checkNotNull(getBaseUrl()) { "Base URL is null when it was required" }
|
||||
|
||||
override suspend fun getAuthHeader(): String? = storage.getAuthHeader()
|
||||
|
||||
override suspend fun requireAuthHeader(): String =
|
||||
@@ -51,18 +37,6 @@ class AuthRepoImpl @Inject constructor(
|
||||
storage.clearAuthData()
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun parseBaseUrl(baseUrl: String): String = try {
|
||||
val withScheme = Uri.parse(baseUrl).let {
|
||||
if (it.scheme == null) it.buildUpon().scheme("https").build()
|
||||
else it
|
||||
}.toString()
|
||||
withScheme.toHttpUrl().toString()
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "authenticate: can't parse base url $baseUrl")
|
||||
throw MalformedUrl(e)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val AUTH_HEADER_FORMAT = "Bearer %s"
|
||||
}
|
||||
|
||||
@@ -13,20 +13,10 @@ class AuthStorageImpl @Inject constructor(
|
||||
) : AuthStorage {
|
||||
|
||||
private val authHeaderKey by preferencesStorage::authHeaderKey
|
||||
private val baseUrlKey by preferencesStorage::baseUrlKey
|
||||
|
||||
override suspend fun storeAuthData(authHeader: String, baseUrl: String) {
|
||||
Timber.v("storeAuthData() called with: authHeader = $authHeader, baseUrl = $baseUrl")
|
||||
preferencesStorage.storeValues(
|
||||
Pair(authHeaderKey, authHeader),
|
||||
Pair(baseUrlKey, baseUrl),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getBaseUrl(): String? {
|
||||
val baseUrl = preferencesStorage.getValue(baseUrlKey)
|
||||
Timber.d("getBaseUrl: base url is $baseUrl")
|
||||
return baseUrl
|
||||
override suspend fun storeAuthData(authHeader: String) {
|
||||
Timber.v("storeAuthData() called with: authHeader = $authHeader")
|
||||
preferencesStorage.storeValues(Pair(authHeaderKey, authHeader))
|
||||
}
|
||||
|
||||
override suspend fun getAuthHeader(): String? {
|
||||
@@ -43,6 +33,6 @@ class AuthStorageImpl @Inject constructor(
|
||||
|
||||
override suspend fun clearAuthData() {
|
||||
Timber.v("clearAuthData() called")
|
||||
preferencesStorage.removeValues(authHeaderKey, baseUrlKey)
|
||||
preferencesStorage.removeValues(authHeaderKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.auth.impl
|
||||
|
||||
sealed class AuthenticationError(cause: Throwable) : RuntimeException(cause) {
|
||||
class Unauthorized(cause: Throwable) : AuthenticationError(cause)
|
||||
class NoServerConnection(cause: Throwable) : AuthenticationError(cause)
|
||||
class NotMealie(cause: Throwable) : AuthenticationError(cause)
|
||||
class MalformedUrl(cause: Throwable) : AuthenticationError(cause)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
interface BaseURLStorage {
|
||||
|
||||
suspend fun getBaseURL(): String?
|
||||
|
||||
suspend fun requireBaseURL(): String
|
||||
|
||||
suspend fun storeBaseURL(baseURL: String)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
import gq.kirmanak.mealient.data.storage.PreferencesStorage
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class BaseURLStorageImpl @Inject constructor(
|
||||
private val preferencesStorage: PreferencesStorage,
|
||||
) : BaseURLStorage {
|
||||
|
||||
private val baseUrlKey by preferencesStorage::baseUrlKey
|
||||
|
||||
override suspend fun getBaseURL(): String? = preferencesStorage.getValue(baseUrlKey)
|
||||
|
||||
override suspend fun requireBaseURL(): String = checkNotNull(getBaseURL()) {
|
||||
"Base URL was null when it was required"
|
||||
}
|
||||
|
||||
override suspend fun storeBaseURL(baseURL: String) {
|
||||
preferencesStorage.storeValues(Pair(baseUrlKey, baseURL))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
import gq.kirmanak.mealient.data.network.NetworkError
|
||||
|
||||
interface VersionDataSource {
|
||||
|
||||
@Throws(NetworkError::class)
|
||||
suspend fun getVersionInfo(baseUrl: String): VersionInfo
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
import gq.kirmanak.mealient.data.network.NetworkError
|
||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||
import gq.kirmanak.mealient.extensions.versionInfo
|
||||
import kotlinx.serialization.SerializationException
|
||||
import retrofit2.HttpException
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class VersionDataSourceImpl @Inject constructor(
|
||||
private val serviceFactory: ServiceFactory<VersionService>,
|
||||
) : VersionDataSource {
|
||||
|
||||
override suspend fun getVersionInfo(baseUrl: String): VersionInfo {
|
||||
Timber.v("getVersionInfo() called with: baseUrl = $baseUrl")
|
||||
|
||||
val service = try {
|
||||
serviceFactory.provideService(baseUrl)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "getVersionInfo: can't create service")
|
||||
throw NetworkError.MalformedUrl(e)
|
||||
}
|
||||
|
||||
val response = try {
|
||||
service.getVersion()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "getVersionInfo: can't request version")
|
||||
when (e) {
|
||||
is HttpException, is SerializationException -> throw NetworkError.NotMealie(e)
|
||||
else -> throw NetworkError.NoServerConnection(e)
|
||||
}
|
||||
}
|
||||
|
||||
return response.versionInfo()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
data class VersionInfo(
|
||||
val production: Boolean,
|
||||
val version: String,
|
||||
val demoStatus: Boolean,
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class VersionResponse(
|
||||
@SerialName("production")
|
||||
val production: Boolean,
|
||||
@SerialName("version")
|
||||
val version: String,
|
||||
@SerialName("demoStatus")
|
||||
val demoStatus: Boolean,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface VersionService {
|
||||
@GET("api/debug/version")
|
||||
suspend fun getVersion(): VersionResponse
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package gq.kirmanak.mealient.data.network
|
||||
|
||||
sealed class NetworkError(cause: Throwable) : RuntimeException(cause) {
|
||||
class Unauthorized(cause: Throwable) : NetworkError(cause)
|
||||
class NoServerConnection(cause: Throwable) : NetworkError(cause)
|
||||
class NotMealie(cause: Throwable) : NetworkError(cause)
|
||||
class MalformedUrl(cause: Throwable) : NetworkError(cause)
|
||||
}
|
||||
@@ -1,28 +1,29 @@
|
||||
package gq.kirmanak.mealient.data.network
|
||||
|
||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||
import timber.log.Timber
|
||||
|
||||
inline fun <reified T> RetrofitBuilder.createServiceFactory() =
|
||||
RetrofitServiceFactory(T::class.java, this)
|
||||
inline fun <reified T> RetrofitBuilder.createServiceFactory(baseURLStorage: BaseURLStorage) =
|
||||
RetrofitServiceFactory(T::class.java, this, baseURLStorage)
|
||||
|
||||
class RetrofitServiceFactory<T>(
|
||||
private val serviceClass: Class<T>,
|
||||
private val retrofitBuilder: RetrofitBuilder,
|
||||
private val baseURLStorage: BaseURLStorage,
|
||||
) : ServiceFactory<T> {
|
||||
|
||||
private val cache: MutableMap<String, T> = mutableMapOf()
|
||||
|
||||
@Synchronized
|
||||
override fun provideService(baseUrl: String): T {
|
||||
override suspend fun provideService(baseUrl: String?): T {
|
||||
Timber.v("provideService() called with: baseUrl = $baseUrl, class = ${serviceClass.simpleName}")
|
||||
val cached = cache[baseUrl]
|
||||
return if (cached == null) {
|
||||
Timber.d("provideService: cache is empty, creating new")
|
||||
val new = retrofitBuilder.buildRetrofit(baseUrl).create(serviceClass)
|
||||
cache[baseUrl] = new
|
||||
new
|
||||
} else {
|
||||
cached
|
||||
}
|
||||
val url = baseUrl ?: baseURLStorage.requireBaseURL()
|
||||
return synchronized(cache) { cache[url] ?: createService(url, serviceClass) }
|
||||
}
|
||||
|
||||
private fun createService(url: String, serviceClass: Class<T>): T {
|
||||
Timber.v("createService() called with: url = $url, serviceClass = ${serviceClass.simpleName}")
|
||||
val service = retrofitBuilder.buildRetrofit(url).create(serviceClass)
|
||||
cache[url] = service
|
||||
return service
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,5 @@ package gq.kirmanak.mealient.data.network
|
||||
|
||||
interface ServiceFactory<T> {
|
||||
|
||||
fun provideService(baseUrl: String): T
|
||||
suspend fun provideService(baseUrl: String? = null): T
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package gq.kirmanak.mealient.data.recipes.impl
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeImageLoader
|
||||
import gq.kirmanak.mealient.ui.ImageLoader
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
@@ -14,7 +14,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class RecipeImageLoaderImpl @Inject constructor(
|
||||
private val imageLoader: ImageLoader,
|
||||
private val authRepo: AuthRepo
|
||||
private val baseURLStorage: BaseURLStorage,
|
||||
): RecipeImageLoader {
|
||||
|
||||
override suspend fun loadRecipeImage(view: ImageView, slug: String?) {
|
||||
@@ -25,7 +25,7 @@ class RecipeImageLoaderImpl @Inject constructor(
|
||||
@VisibleForTesting
|
||||
suspend fun generateImageUrl(slug: String?): String? {
|
||||
Timber.v("generateImageUrl() called with: slug = $slug")
|
||||
val result = authRepo.getBaseUrl()
|
||||
val result = baseURLStorage.getBaseURL()
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.takeUnless { slug.isNullOrBlank() }
|
||||
?.toHttpUrlOrNull()
|
||||
|
||||
@@ -30,8 +30,8 @@ class RecipeDataSourceImpl @Inject constructor(
|
||||
|
||||
private suspend fun getRecipeService(): RecipeService {
|
||||
Timber.v("getRecipeService() called")
|
||||
return recipeServiceFactory.provideService(authRepo.requireBaseUrl())
|
||||
return recipeServiceFactory.provideService()
|
||||
}
|
||||
|
||||
private suspend fun getToken(): String = authRepo.requireAuthHeader()
|
||||
private suspend fun getToken(): String? = authRepo.getAuthHeader()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user