From b0a53b5991c2cfe7a6f65e7bb74fb0a0bb041e10 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 7 Nov 2021 11:49:52 +0300 Subject: [PATCH] Implement initial authentication flow --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 2 + .../main/java/gq/kirmanak/mealie/MealieApp.kt | 7 ++ .../kirmanak/mealie/data/RetrofitBuilder.kt | 20 ++++++ .../mealie/data/auth/AuthDataSource.kt | 8 +++ .../mealie/data/auth/AuthDataSourceImpl.kt | 25 +++++++ .../kirmanak/mealie/data/auth/AuthModule.kt | 21 ++++++ .../gq/kirmanak/mealie/data/auth/AuthRepo.kt | 7 ++ .../kirmanak/mealie/data/auth/AuthRepoImpl.kt | 22 ++++++ .../kirmanak/mealie/data/auth/AuthService.kt | 18 +++++ .../kirmanak/mealie/data/auth/AuthStorage.kt | 6 ++ .../mealie/data/auth/AuthStorageImpl.kt | 24 +++++++ .../mealie/data/auth/GetTokenResponse.kt | 10 +++ .../mealie/ui/auth/AuthenticationFragment.kt | 71 +++++++++++++++++++ .../mealie/ui/auth/AuthenticationViewModel.kt | 17 +++++ .../res/layout/fragment_authentication.xml | 63 ++++++++++++++-- app/src/main/res/values/strings.xml | 4 ++ build.gradle | 4 +- 18 files changed, 322 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/gq/kirmanak/mealie/MealieApp.kt create mode 100644 app/src/main/java/gq/kirmanak/mealie/data/RetrofitBuilder.kt create mode 100644 app/src/main/java/gq/kirmanak/mealie/data/auth/AuthDataSource.kt create mode 100644 app/src/main/java/gq/kirmanak/mealie/data/auth/AuthDataSourceImpl.kt create mode 100644 app/src/main/java/gq/kirmanak/mealie/data/auth/AuthModule.kt create mode 100644 app/src/main/java/gq/kirmanak/mealie/data/auth/AuthRepo.kt create mode 100644 app/src/main/java/gq/kirmanak/mealie/data/auth/AuthRepoImpl.kt create mode 100644 app/src/main/java/gq/kirmanak/mealie/data/auth/AuthService.kt create mode 100644 app/src/main/java/gq/kirmanak/mealie/data/auth/AuthStorage.kt create mode 100644 app/src/main/java/gq/kirmanak/mealie/data/auth/AuthStorageImpl.kt create mode 100644 app/src/main/java/gq/kirmanak/mealie/data/auth/GetTokenResponse.kt create mode 100644 app/src/main/java/gq/kirmanak/mealie/ui/auth/AuthenticationViewModel.kt diff --git a/app/build.gradle b/app/build.gradle index d6e92d1..f3b6630 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ plugins { id 'com.android.application' id 'androidx.navigation.safeargs.kotlin' id 'dagger.hilt.android.plugin' + id 'org.jetbrains.kotlin.plugin.serialization' } android { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55ec3d1..d54dfef 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthDataSourceImpl.kt new file mode 100644 index 0000000..f8f4712 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthDataSourceImpl.kt @@ -0,0 +1,25 @@ +package gq.kirmanak.mealie.data.auth + +import gq.kirmanak.mealie.data.RetrofitBuilder +import kotlinx.serialization.ExperimentalSerializationApi +import retrofit2.create +import javax.inject.Inject + +@ExperimentalSerializationApi +class AuthDataSourceImpl @Inject constructor( + private val retrofitBuilder: RetrofitBuilder +) : AuthDataSource { + override suspend fun authenticate( + username: String, + password: String, + baseUrl: String + ): Result { + val authService = retrofitBuilder.buildRetrofit(baseUrl).create() + val response = try { + authService.getToken(username, password) + } catch (e: Exception) { + return Result.failure(e) + } + return Result.success(response.accessToken) + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthModule.kt b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthModule.kt new file mode 100644 index 0000000..9bce83c --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthModule.kt @@ -0,0 +1,21 @@ +package gq.kirmanak.mealie.data.auth + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import kotlinx.serialization.ExperimentalSerializationApi + +@ExperimentalSerializationApi +@Module +@InstallIn(ViewModelComponent::class) +abstract class AuthModule { + @Binds + abstract fun bindAuthDataSource(authDataSourceImpl: AuthDataSourceImpl): AuthDataSource + + @Binds + abstract fun bindAuthStorage(authStorageImpl: AuthStorageImpl): AuthStorage + + @Binds + abstract fun bindAuthRepo(authRepo: AuthRepoImpl): AuthRepo +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthRepo.kt b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthRepo.kt new file mode 100644 index 0000000..7b5dbe9 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthRepo.kt @@ -0,0 +1,7 @@ +package gq.kirmanak.mealie.data.auth + +interface AuthRepo { + suspend fun isAuthenticated(): Boolean + + suspend fun authenticate(username: String, password: String, baseUrl: String): Throwable? +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthRepoImpl.kt new file mode 100644 index 0000000..cf5b3e4 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthRepoImpl.kt @@ -0,0 +1,22 @@ +package gq.kirmanak.mealie.data.auth + +import javax.inject.Inject + +class AuthRepoImpl @Inject constructor( + private val dataSource: AuthDataSource, + private val storage: AuthStorage +) : AuthRepo { + override suspend fun isAuthenticated(): Boolean = storage.isAuthenticated() + + override suspend fun authenticate( + username: String, + password: String, + baseUrl: String + ): Throwable? { + val authResult = dataSource.authenticate(username, password, baseUrl) + if (authResult.isFailure) return authResult.exceptionOrNull() + val token = checkNotNull(authResult.getOrNull()) + storage.storeToken(token) + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthService.kt b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthService.kt new file mode 100644 index 0000000..8524c0d --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthService.kt @@ -0,0 +1,18 @@ +package gq.kirmanak.mealie.data.auth + +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.POST + +interface AuthService { + @FormUrlEncoded + @POST("/api/auth/token") + suspend fun getToken( + @Field("username") username: String, + @Field("password") password: String, + @Field("grant_type") grantType: String? = null, + @Field("scope") scope: String? = null, + @Field("client_id") clientId: String? = null, + @Field("client_secret") clientSecret: String? = null + ): GetTokenResponse +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthStorage.kt b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthStorage.kt new file mode 100644 index 0000000..4375fb8 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthStorage.kt @@ -0,0 +1,6 @@ +package gq.kirmanak.mealie.data.auth + +interface AuthStorage { + suspend fun isAuthenticated(): Boolean + suspend fun storeToken(token: String) +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthStorageImpl.kt new file mode 100644 index 0000000..666eed0 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealie/data/auth/AuthStorageImpl.kt @@ -0,0 +1,24 @@ +package gq.kirmanak.mealie.data.auth + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceManager +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +private const val TOKEN_KEY = "AUTH_TOKEN" + +class AuthStorageImpl @Inject constructor(@ApplicationContext private val context: Context) : AuthStorage { + private val sharedPreferences: SharedPreferences + get() = PreferenceManager.getDefaultSharedPreferences(context) + + override suspend fun isAuthenticated(): Boolean = withContext(Dispatchers.IO) { + sharedPreferences.getString(TOKEN_KEY, null) != null + } + + override suspend fun storeToken(token: String) { + sharedPreferences.edit().putString(TOKEN_KEY, token).apply() + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/data/auth/GetTokenResponse.kt b/app/src/main/java/gq/kirmanak/mealie/data/auth/GetTokenResponse.kt new file mode 100644 index 0000000..372615d --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealie/data/auth/GetTokenResponse.kt @@ -0,0 +1,10 @@ +package gq.kirmanak.mealie.data.auth + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetTokenResponse( + @SerialName("access_token") val accessToken: String, + @SerialName("token_type") val tokenType: String +) \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealie/ui/auth/AuthenticationFragment.kt b/app/src/main/java/gq/kirmanak/mealie/ui/auth/AuthenticationFragment.kt index 05bd2d9..b0d010a 100644 --- a/app/src/main/java/gq/kirmanak/mealie/ui/auth/AuthenticationFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealie/ui/auth/AuthenticationFragment.kt @@ -1,18 +1,32 @@ package gq.kirmanak.mealie.ui.auth import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.EditText +import android.widget.Toast import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.google.android.material.textfield.TextInputLayout import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealie.databinding.FragmentAuthenticationBinding +private const val TAG = "AuthenticationFragment" + @AndroidEntryPoint class AuthenticationFragment : Fragment() { private var _binding: FragmentAuthenticationBinding? = null private val binding: FragmentAuthenticationBinding get() = checkNotNull(_binding) { "Binding requested when fragment is off screen" } + private val viewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + checkIfAuthenticatedAlready() + } override fun onCreateView( inflater: LayoutInflater, @@ -23,6 +37,63 @@ class AuthenticationFragment : Fragment() { return binding.root } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.button.setOnClickListener { + onLoginClicked() + } + } + + private fun checkIfAuthenticatedAlready() { + lifecycleScope.launchWhenCreated { + Toast.makeText( + requireContext(), + if (viewModel.isAuthenticated()) "User is authenticated" + else "User isn't authenticated", + Toast.LENGTH_SHORT + ).show() + } + } + + private fun onLoginClicked() { + val email: String + val pass: String + val url: String + with(binding) { + email = checkIfInputIsEmpty(emailInput, emailInputLayout) { + "Email is empty" + } ?: return + pass = checkIfInputIsEmpty(passwordInput, passwordInputLayout) { + "Pass is empty" + } ?: return + url = checkIfInputIsEmpty(urlInput, urlInputLayout) { + "URL is empty" + } ?: return + } + lifecycleScope.launchWhenResumed { + val exception = viewModel.authenticate(email, pass, url) + Log.e(TAG, "onLoginClicked: ", exception) + Toast.makeText( + requireContext(), + "Exception is ${exception?.message ?: "null"}", + Toast.LENGTH_SHORT + ).show() + } + } + + private fun checkIfInputIsEmpty( + input: EditText, + inputLayout: TextInputLayout, + errorText: () -> String + ): String? { + val text = input.text?.toString() + if (text.isNullOrBlank()) { + inputLayout.error = errorText() + return null + } + return text + } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/src/main/java/gq/kirmanak/mealie/ui/auth/AuthenticationViewModel.kt b/app/src/main/java/gq/kirmanak/mealie/ui/auth/AuthenticationViewModel.kt new file mode 100644 index 0000000..c19fec8 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealie/ui/auth/AuthenticationViewModel.kt @@ -0,0 +1,17 @@ +package gq.kirmanak.mealie.ui.auth + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import gq.kirmanak.mealie.data.auth.AuthRepo +import javax.inject.Inject + +@HiltViewModel +class AuthenticationViewModel @Inject constructor( + private val authRepo: AuthRepo +) : ViewModel() { + suspend fun isAuthenticated(): Boolean = authRepo.isAuthenticated() + + suspend fun authenticate(username: String, password: String, baseUrl: String): Throwable? { + return authRepo.authenticate(username, password, baseUrl) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_authentication.xml b/app/src/main/res/layout/fragment_authentication.xml index 8999df0..9f0cc80 100644 --- a/app/src/main/res/layout/fragment_authentication.xml +++ b/app/src/main/res/layout/fragment_authentication.xml @@ -6,15 +6,64 @@ android:layout_height="match_parent" tools:context=".ui.auth.AuthenticationFragment"> - - + + + + + + + + + + + + + + +