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 68acf1a760e58348de90897edf2e6f898d1991d6..2c5283b39f5eddba072ac60846b0d815131b6ce7 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 789e1cf2fafe74954312ead666ca048b9efa7b20..7df9205a5334716dd6505e01134a9f0d280d2290 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 9f085ca1e9feab84fd957a914a7062bb383209b2..e7b5618840222f27174e9ed3c3f6dfb94dd8b946 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 e42a3ac9af5949fdd0c8c99b18889ec43004ddbd..fd17a0c05fa348fb88f6f30d3d09fe02d643b374 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 0000000000000000000000000000000000000000..c3d8dc12c094fb584b80d71485f3ee04b3dd21d9 --- /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 2779b5cb43e28f87d300bebc9bb23ff855c896c2..09e84fe009aa2f46494c11acad7f3d29938adb85 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 8e172e322c4c888efae8ae3b1c7dcae02d75a291..1bea68903544d27fddb88454a67acbbd7ce8f4c6 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 62cd75c50e5277e8c4bf78863996d9dc1c1c2c15..8acdbca3cc4f29efa90f5f0ce992e991d07d51dc 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) } }