From 62406dbb40a98e768fbd95933d3c8f1ea231d05c Mon Sep 17 00:00:00 2001 From: Altair1618 <farhannabilsuryono3@gmail.com> Date: Fri, 22 Mar 2024 15:56:21 +0700 Subject: [PATCH] feat: implement check login on open --- .../data/repositories/UserRepository.kt | 5 ++ .../bondoman/data/utils/PreferencesManager.kt | 53 +++++++++++++++++++ .../data/viewmodels/login/LoginViewModel.kt | 27 ++++++++-- .../bondoman/networks/RetrofitClient.kt | 45 ++++++++++++---- .../networks/interceptors/AuthInterceptor.kt | 16 ++++++ .../networks/responses/TokenResponse.kt | 8 +-- .../bondoman/networks/services/UserService.kt | 4 ++ .../views/activities/LoginActivity.kt | 45 ++++++++++------ 8 files changed, 170 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/com/example/bondoman/networks/interceptors/AuthInterceptor.kt diff --git a/app/src/main/java/com/example/bondoman/data/repositories/UserRepository.kt b/app/src/main/java/com/example/bondoman/data/repositories/UserRepository.kt index 68acf1a..2c5283b 100644 --- a/app/src/main/java/com/example/bondoman/data/repositories/UserRepository.kt +++ b/app/src/main/java/com/example/bondoman/data/repositories/UserRepository.kt @@ -2,6 +2,7 @@ package com.example.bondoman.data.repositories import com.example.bondoman.networks.requests.LoginRequest import com.example.bondoman.networks.responses.LoginResponse +import com.example.bondoman.networks.responses.TokenResponse import com.example.bondoman.networks.services.UserService import retrofit2.Response @@ -9,4 +10,8 @@ class UserRepository (private val service: UserService) { suspend fun login(email: String, password: String): Response<LoginResponse> { return service.login(LoginRequest(email, password)) } + + suspend fun checkToken(): Response<TokenResponse> { + return service.checkToken() + } } diff --git a/app/src/main/java/com/example/bondoman/data/utils/PreferencesManager.kt b/app/src/main/java/com/example/bondoman/data/utils/PreferencesManager.kt index 789e1cf..7df9205 100644 --- a/app/src/main/java/com/example/bondoman/data/utils/PreferencesManager.kt +++ b/app/src/main/java/com/example/bondoman/data/utils/PreferencesManager.kt @@ -25,4 +25,57 @@ object PreferencesManager { EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) } + + fun putString(context: Context, key: String, value: String, encrypted: Boolean = false) { + val editor = if (encrypted) { + getEncryptedSharedPreferences(context).edit() + } else { + getSharedPreferences(context).edit() + } + + editor.putString(key, value) + editor.apply() + } + + fun getString(context: Context, key: String, encrypted: Boolean = false): String? { + val sharedPreferences = if (encrypted) { + getEncryptedSharedPreferences(context) + } else { + getSharedPreferences(context) + } + + return sharedPreferences.getString(key, null) + } + + fun putBoolean(context: Context, key: String, value: Boolean, encrypted: Boolean = false) { + val editor = if (encrypted) { + getEncryptedSharedPreferences(context).edit() + } else { + getSharedPreferences(context).edit() + } + + editor.putBoolean(key, value) + editor.apply() + } + + fun getBoolean(context: Context, key: String, encrypted: Boolean = false): Boolean { + val sharedPreferences = if (encrypted) { + getEncryptedSharedPreferences(context) + } else { + getSharedPreferences(context) + } + + return sharedPreferences.getBoolean(key, false) + } + + fun remove(context: Context, key: String, encrypted: Boolean = false) { + val editor = if (encrypted) { + getEncryptedSharedPreferences(context).edit() + } else { + getSharedPreferences(context).edit() + } + + editor.remove(key) + editor.apply() + } } diff --git a/app/src/main/java/com/example/bondoman/data/viewmodels/login/LoginViewModel.kt b/app/src/main/java/com/example/bondoman/data/viewmodels/login/LoginViewModel.kt index 9f085ca..e7b5618 100644 --- a/app/src/main/java/com/example/bondoman/data/viewmodels/login/LoginViewModel.kt +++ b/app/src/main/java/com/example/bondoman/data/viewmodels/login/LoginViewModel.kt @@ -6,22 +6,39 @@ import androidx.lifecycle.viewModelScope import com.example.bondoman.data.repositories.UserRepository import com.example.bondoman.networks.RetrofitClient import com.example.bondoman.networks.responses.LoginResponse +import com.example.bondoman.networks.responses.TokenResponse import com.example.bondoman.networks.services.UserService import kotlinx.coroutines.launch class LoginViewModel : ViewModel() { - private val _response = MutableLiveData<LoginResponse>() - val response = _response + private val _loginResponse = MutableLiveData<LoginResponse>() + val loginResponse = _loginResponse + + private val _tokenResponse = MutableLiveData<TokenResponse>() + val tokenResponse = _tokenResponse fun login(email: String, password: String) { viewModelScope.launch { - val repository = UserRepository(RetrofitClient().instance.create(UserService::class.java)) + val repository = UserRepository(RetrofitClient.getInstance().create(UserService::class.java)) val loginResponse = repository.login(email, password) if (loginResponse.isSuccessful) { - _response.value = loginResponse.body() + _loginResponse.value = loginResponse.body() + } else { + _loginResponse.value = LoginResponse(null, loginResponse.errorBody()?.string() ?: "Unknown error") + } + } + } + + fun checkToken(bearerToken: String) { + viewModelScope.launch { + val repository = UserRepository(RetrofitClient.getInstanceWithAuth(bearerToken).create(UserService::class.java)) + val tokenResponse = repository.checkToken() + + if (tokenResponse.isSuccessful) { + _tokenResponse.value = tokenResponse.body() } else { - _response.value = LoginResponse(null, loginResponse.errorBody()?.string() ?: "Unknown error") + _tokenResponse.value = TokenResponse("", 0, 0, tokenResponse.errorBody()?.string() ?: "Unknown error") } } } diff --git a/app/src/main/java/com/example/bondoman/networks/RetrofitClient.kt b/app/src/main/java/com/example/bondoman/networks/RetrofitClient.kt index e42a3ac..fd17a0c 100644 --- a/app/src/main/java/com/example/bondoman/networks/RetrofitClient.kt +++ b/app/src/main/java/com/example/bondoman/networks/RetrofitClient.kt @@ -1,21 +1,46 @@ package com.example.bondoman.networks +import com.example.bondoman.networks.interceptors.AuthInterceptor import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory -class RetrofitClient { - private val baseUrl = "https://pbd-backend-2024.vercel.app/" +object RetrofitClient { + private const val BASE_URL = "https://pbd-backend-2024.vercel.app/" - val instance: Retrofit by lazy { - val moshi = Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() + private val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() - Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .build() + private var instance: Retrofit? = null + private var instanceWithAuth: Retrofit? = null + + fun getInstance(): Retrofit { + if (instance == null) { + instance = Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + } + + return instance!! + } + + fun getInstanceWithAuth(bearerToken: String): Retrofit { + if (instanceWithAuth == null) { + val client = OkHttpClient.Builder() + .addInterceptor(AuthInterceptor(bearerToken)) + .build() + + instanceWithAuth = Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .client(client) + .build() + } + + return instanceWithAuth!! } } diff --git a/app/src/main/java/com/example/bondoman/networks/interceptors/AuthInterceptor.kt b/app/src/main/java/com/example/bondoman/networks/interceptors/AuthInterceptor.kt new file mode 100644 index 0000000..c3d8dc1 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/networks/interceptors/AuthInterceptor.kt @@ -0,0 +1,16 @@ +package com.example.bondoman.networks.interceptors + +import okhttp3.Interceptor +import okhttp3.Response + +class AuthInterceptor(private val bearerToken: String) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val newRequest = request.newBuilder() + .addHeader("Authorization", "Bearer $bearerToken") + .build() + + return chain.proceed(newRequest) + } +} diff --git a/app/src/main/java/com/example/bondoman/networks/responses/TokenResponse.kt b/app/src/main/java/com/example/bondoman/networks/responses/TokenResponse.kt index 2779b5c..09e84fe 100644 --- a/app/src/main/java/com/example/bondoman/networks/responses/TokenResponse.kt +++ b/app/src/main/java/com/example/bondoman/networks/responses/TokenResponse.kt @@ -4,11 +4,13 @@ import com.squareup.moshi.Json data class TokenResponse( @Json(name = "nim") - val nim: String, + val nim: String?, @Json(name = "iat") - val iat: Int, + val iat: Int?, @Json(name = "exp") - val exp: Int, + val exp: Int?, + + val error: String? = null ) diff --git a/app/src/main/java/com/example/bondoman/networks/services/UserService.kt b/app/src/main/java/com/example/bondoman/networks/services/UserService.kt index 8e172e3..1bea689 100644 --- a/app/src/main/java/com/example/bondoman/networks/services/UserService.kt +++ b/app/src/main/java/com/example/bondoman/networks/services/UserService.kt @@ -2,6 +2,7 @@ package com.example.bondoman.networks.services import com.example.bondoman.networks.requests.LoginRequest import com.example.bondoman.networks.responses.LoginResponse +import com.example.bondoman.networks.responses.TokenResponse import retrofit2.Response import retrofit2.http.Body import retrofit2.http.POST @@ -9,4 +10,7 @@ import retrofit2.http.POST interface UserService { @POST("/api/auth/login") suspend fun login(@Body request: LoginRequest): Response<LoginResponse> + + @POST("/api/auth/token") + suspend fun checkToken(): Response<TokenResponse> } diff --git a/app/src/main/java/com/example/bondoman/views/activities/LoginActivity.kt b/app/src/main/java/com/example/bondoman/views/activities/LoginActivity.kt index 62cd75c..8acdbca 100644 --- a/app/src/main/java/com/example/bondoman/views/activities/LoginActivity.kt +++ b/app/src/main/java/com/example/bondoman/views/activities/LoginActivity.kt @@ -2,6 +2,7 @@ package com.example.bondoman.views.activities import android.content.Intent import android.os.Bundle +import android.util.Log import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -11,6 +12,7 @@ import com.example.bondoman.R import com.example.bondoman.data.utils.PreferencesManager import com.example.bondoman.data.viewmodels.login.LoginViewModel import com.example.bondoman.networks.responses.LoginResponse +import com.example.bondoman.networks.responses.TokenResponse import com.google.android.material.button.MaterialButton import com.google.android.material.textfield.TextInputEditText @@ -27,6 +29,8 @@ class LoginActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) + checkLoginStatus() + loginLayout = findViewById(R.id.login_layout) emailInput = findViewById(R.id.email_text_input) passwordInput = findViewById(R.id.password_text_input) @@ -72,32 +76,43 @@ class LoginActivity : AppCompatActivity() { if (response.error != null) { Toast.makeText(this, response.error, Toast.LENGTH_SHORT).show() } else { + PreferencesManager.putString(this, "token", response.token ?: "", true) val intent = Intent(this, MainActivity::class.java) startActivity(intent) finish() } } - viewModel.response.observe(this, observer) + viewModel.loginResponse.observe(this, observer) } - private fun saveToken(token: String) { - val sharedPreferences = PreferencesManager.getEncryptedSharedPreferences(applicationContext) - val editor = sharedPreferences.edit() - editor.putString("token", token) - editor.apply() - } + private fun checkLoginStatus() { + val token: String? = PreferencesManager.getString(this, "token", true) - private fun getToken(): String? { - val sharedPreferences = PreferencesManager.getEncryptedSharedPreferences(applicationContext) - return sharedPreferences.getString("token", null) - } + if (token.isNullOrEmpty()) { + return + } - private fun isLoggedIn(): Boolean { - val token = getToken() ?: return false + viewModel.checkToken(token) - // TODO: Check if JWT token is expired + val observer = Observer<TokenResponse> { response -> + if (response.error != null) { + Log.e("LoginActivity", response.error) + return@Observer + } + + val currentTime = System.currentTimeMillis() / 1000 + if (response.exp != null && response.exp < currentTime) { + Log.i("LoginActivity", "Token expired") + PreferencesManager.remove(this, "token", true) + return@Observer + } + + val intent = Intent(this, MainActivity::class.java) + startActivity(intent) + finish() + } - return true + viewModel.tokenResponse.observe(this, observer) } } -- GitLab