diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d03d4fa9d491bbdeb66240931f2e94074a13a73e..583530902065fdf6e6960cb19c7973dededcadab 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,11 +14,20 @@ android:supportsRtl="true" android:theme="@style/Theme.BondoYap" tools:targetApi="31"> + + <service + android:name=".service.jwt.JwtService" + android:enabled="true" + android:exported="true" + android:permission="android.permission.INTERNET"> + </service> + <activity android:name=".ui.login.LoginActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> diff --git a/app/src/main/java/com/example/bondoyap/ApiClient.kt b/app/src/main/java/com/example/bondoyap/ApiClient.kt deleted file mode 100644 index d1858bc3d5c3c4d5ec7b2a3de8f17f3427e5f6eb..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/example/bondoyap/ApiClient.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.example.bondoyap - -import retrofit2.Call -import retrofit2.http.Body -import retrofit2.http.POST - - -object Constants { - const val BASE_URL: String = "https://pbd-backend-2024.vercel.app/api/" - const val SHARED_PREFS_NAME = "Prefs" -} - -interface ApiClient{ - @POST("auth/login") - fun login( - @Body loginRequest: LoginRequest - ): Call<LoginResponse> - - @POST("auth/token") - fun verifyToken( - ): Call<TokenResponse> - - @POST("bill/upload") - fun uploadPicture( - ) -} - -data class LoginRequest( - val email: String, - val password: String -) - -data class LoginResponse( - val token: String -) - -data class TokenResponse( - val nim: String, - val iat: Number, - val exp: Number -) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/MainActivity.kt b/app/src/main/java/com/example/bondoyap/MainActivity.kt index 7002b983d3102e2c405b8e123671fa18da86b984..bb0a8bfb7024ffb846255ee25e928a26a381d598 100644 --- a/app/src/main/java/com/example/bondoyap/MainActivity.kt +++ b/app/src/main/java/com/example/bondoyap/MainActivity.kt @@ -10,8 +10,9 @@ import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController -import com.example.bondoyap.Constants.SHARED_PREFS_NAME +import com.example.bondoyap.service.api.Constants.SHARED_PREFS_NAME import com.example.bondoyap.databinding.ActivityMainBinding +import com.example.bondoyap.service.jwt.JwtService import com.example.bondoyap.ui.login.LoginActivity class MainActivity : AppCompatActivity() { @@ -29,6 +30,10 @@ class MainActivity : AppCompatActivity() { startActivity(intent) finish() } + + val serviceIntent = Intent(this, JwtService::class.java) + this.startService(serviceIntent) + binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) diff --git a/app/src/main/java/com/example/bondoyap/service/SessionManager.kt b/app/src/main/java/com/example/bondoyap/service/SessionManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..2a58fbedd688c66addcce7c0d7e46097fd22abd4 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/SessionManager.kt @@ -0,0 +1,52 @@ +package com.example.bondoyap.service + +import android.content.Context +import android.content.SharedPreferences +import com.example.bondoyap.service.api.Constants.SHARED_PREFS_NAME +import com.example.bondoyap.ui.login.data.model.LoggedInUser +import com.squareup.moshi.Moshi +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory + +class SessionManager(context: Context) { + + private var prefs: SharedPreferences = context.getSharedPreferences(SHARED_PREFS_NAME, + Context.MODE_PRIVATE) + private val editor: SharedPreferences.Editor = prefs.edit() + + + private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + private val userAdapter: JsonAdapter<LoggedInUser> = moshi.adapter(LoggedInUser::class.java) + + private fun getUser(): LoggedInUser? { + val userJson = prefs.getString("loggedInUser", null) + return userJson?.let { + userAdapter.fromJson(it) + } + } + + fun saveExp(exp: Long){ + editor.putLong("exp", exp) + editor.apply() + } + + fun hasExp(): Boolean{ + return getExp().toInt() != -1 + } + + fun getExp(): Long { + return prefs.getLong("exp", -1) + } + + fun getToken(): String? { + val user = getUser() + return user?.let { + user.token + } + } + + fun logout(){ + editor.clear() + editor.apply() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/api/ApiClient.kt b/app/src/main/java/com/example/bondoyap/service/api/ApiClient.kt new file mode 100644 index 0000000000000000000000000000000000000000..02b03eedad67243043d2ec12012358a46c6ca360 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/ApiClient.kt @@ -0,0 +1,40 @@ +package com.example.bondoyap.service.api + +import android.content.Context +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory + +class ApiClient { + private lateinit var apiService: ApiService + fun getApiService(context: Context): ApiService { + + if (!::apiService.isInitialized) { + + val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + val retrofit = Retrofit.Builder() + .baseUrl(Constants.BASE_URL) + .client(okhttpClient(context)) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + + apiService = retrofit.create(ApiService::class.java) + } + return apiService + } + + private fun okhttpClient(context: Context): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(AuthInterceptor(context)) + .build() + } + +} + + + diff --git a/app/src/main/java/com/example/bondoyap/service/api/ApiService.kt b/app/src/main/java/com/example/bondoyap/service/api/ApiService.kt new file mode 100644 index 0000000000000000000000000000000000000000..a7bf474abba94624133338ce218ad87295d7e584 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/ApiService.kt @@ -0,0 +1,24 @@ +package com.example.bondoyap.service.api + +import com.example.bondoyap.service.api.data.LoginRequest +import com.example.bondoyap.service.api.data.LoginResponse +import com.example.bondoyap.service.api.data.TokenResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +interface ApiService { + @POST("auth/login") + fun login( + @Body loginRequest: LoginRequest + ): Call<LoginResponse> + + @POST("auth/token") + fun verifyToken( + @Body token: String + ): Call<TokenResponse> + + @POST("bill/upload") + fun uploadPicture( + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/api/AuthInterceptor.kt b/app/src/main/java/com/example/bondoyap/service/api/AuthInterceptor.kt new file mode 100644 index 0000000000000000000000000000000000000000..62f933c2fb87585dbdccb4095fd668ea970d4d1c --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/AuthInterceptor.kt @@ -0,0 +1,21 @@ +package com.example.bondoyap.service.api + +import android.content.Context +import com.example.bondoyap.service.SessionManager +import okhttp3.Interceptor +import okhttp3.Response + +class AuthInterceptor(context: Context) : Interceptor { + + private val sessionManager = SessionManager(context) + + override fun intercept(chain: Interceptor.Chain): Response { + val requestBuilder = chain.request().newBuilder() + + sessionManager.getToken()?.let { + requestBuilder.addHeader("Authorization", "Bearer $it") + } + + return chain.proceed(requestBuilder.build()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/api/Constants.kt b/app/src/main/java/com/example/bondoyap/service/api/Constants.kt new file mode 100644 index 0000000000000000000000000000000000000000..76fb345217e505ad47427a35808c9e2a256b86ef --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/Constants.kt @@ -0,0 +1,6 @@ +package com.example.bondoyap.service.api + +object Constants { + const val BASE_URL: String = "https://pbd-backend-2024.vercel.app/api/" + const val SHARED_PREFS_NAME = "BondoYap" +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/LoginRequest.kt b/app/src/main/java/com/example/bondoyap/service/api/data/LoginRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..583c050d0c129816df5e0901d18bdc0a60534c14 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/LoginRequest.kt @@ -0,0 +1,6 @@ +package com.example.bondoyap.service.api.data + +data class LoginRequest( + val email: String, + val password: String +) diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/LoginResponse.kt b/app/src/main/java/com/example/bondoyap/service/api/data/LoginResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..ca1bb881dc6e0eabc38cb64374fd2eee615d6a52 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/LoginResponse.kt @@ -0,0 +1,5 @@ +package com.example.bondoyap.service.api.data + +data class LoginResponse( + val token: String +) diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/TokenResponse.kt b/app/src/main/java/com/example/bondoyap/service/api/data/TokenResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..7f8a2be82dbdbfd2846355c965f5072680a02efb --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/TokenResponse.kt @@ -0,0 +1,7 @@ +package com.example.bondoyap.service.api.data + +data class TokenResponse( + val nim: String, + val iat: Long, + val exp: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/jwt/JwtService.kt b/app/src/main/java/com/example/bondoyap/service/jwt/JwtService.kt new file mode 100644 index 0000000000000000000000000000000000000000..946a43539a7fc621e1cdf0e928e164eaa3d301b1 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/jwt/JwtService.kt @@ -0,0 +1,115 @@ +package com.example.bondoyap.service.jwt + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.IBinder +import android.util.Log +import android.widget.Toast +import com.example.bondoyap.service.SessionManager +import com.example.bondoyap.service.api.ApiClient +import com.example.bondoyap.service.api.data.TokenResponse +import com.example.bondoyap.ui.login.LoginActivity +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + + +class JwtService : Service() { + + private lateinit var handler : Handler + + private lateinit var session: SessionManager + + private val task = object : Runnable { + override fun run() { + Log.d("JwtService", "Current Time: ${System.currentTimeMillis()}") + + session = SessionManager(applicationContext) + val token = session.getToken() + + if (token != null) { + verifyJwt(applicationContext, token) + } + // 5 second + handler.postDelayed(this, 5000) + } + } + + private lateinit var apiClient: ApiClient + + override fun onCreate() { + super.onCreate() + handler = Handler(mainLooper) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + handler.post(task) + return START_STICKY + } + + override fun onBind(intent: Intent): IBinder? { + return null + } + + override fun onDestroy() { + super.onDestroy() + handler.removeCallbacks(task) + } + + fun verifyJwt(context: Context, token: String){ + if (session.hasExp()) { + checkJwt(session.getExp()) + return + } + apiClient = ApiClient() + apiClient.getApiService(context).verifyToken(token).enqueue(object: Callback<TokenResponse>{ + override fun onFailure(call: Call<TokenResponse>, t: Throwable) { + Log.d("JwtService", "Error Failure") + } + + override fun onResponse(call: Call<TokenResponse>, response: Response<TokenResponse>) { + Log.d("JwtService", "Getting Response") + + val tokenResponse = response.body() + + if (tokenResponse != null){ + Log.d("JwtService", "Updating Exp") + + session.saveExp(tokenResponse.exp) + checkJwt(tokenResponse.exp) + } else { + Log.d("JwtService", "Error response") + Log.d("JwtService", "Error expired?") + handleExpired() + } + + } + }) + } + + fun checkJwt(exp: Long) { + val currentTimestamp = System.currentTimeMillis() / 1000 + + Log.d("JwtService", "exp : ${exp}") + Log.d("JwtService", "currentTime : ${currentTimestamp}") + + if (currentTimestamp >= exp) { + Log.d("JwtService", "JWT is expired") + handleExpired() + } else { + Log.d("JwtService", "JWT is still valid") + } + } + + fun handleExpired(){ + session.logout() + Toast.makeText(applicationContext, "Session Expired! \n Logging out ...", Toast.LENGTH_LONG).show() + + val intent = Intent(applicationContext, LoginActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + stopSelf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt index dc379ed4e8b62e97c645bf7be327eab0c35480ad..e64b43e08ad5bb67e76fc8892e9ef61387d0e9ee 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt @@ -14,7 +14,7 @@ import android.view.inputmethod.EditorInfo import android.widget.EditText import android.widget.Toast import androidx.appcompat.app.AppCompatActivity -import com.example.bondoyap.Constants.SHARED_PREFS_NAME +import com.example.bondoyap.service.api.Constants.SHARED_PREFS_NAME import com.example.bondoyap.MainActivity import com.example.bondoyap.R import com.example.bondoyap.databinding.ActivityLoginBinding diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt index b0e9039da4e6694319d086bdc6fca269138c6c17..0b7abfa109b689c808959179ec0729362d3c2a8d 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt @@ -13,7 +13,7 @@ class LoginViewModelFactory(private val context: Context) : ViewModelProvider.Fa if (modelClass.isAssignableFrom(LoginViewModel::class.java)) { return LoginViewModel( loginRepository = LoginRepository( - dataSource = LoginDataSource(), + dataSource = LoginDataSource(context), context = context ) diff --git a/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt index cf54c3009a664f4756b194be6d01189ef6d85582..00f4b562726b80158e1de36da69e93f895b347ed 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt @@ -1,21 +1,20 @@ package com.example.bondoyap.ui.login.data -import com.example.bondoyap.ApiClient -import com.example.bondoyap.Constants -import com.example.bondoyap.LoginRequest +import android.content.Context +import android.util.Log +import com.example.bondoyap.service.api.ApiClient +import com.example.bondoyap.service.api.data.LoginRequest import com.example.bondoyap.ui.login.data.model.LoggedInUser -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import retrofit2.Retrofit -import retrofit2.converter.moshi.MoshiConverterFactory import java.io.IOException /** * Class that handles authentication w/ login credentials and retrieves user information. */ -class LoginDataSource { +class LoginDataSource(private val context: Context) { + + private lateinit var apiClient: ApiClient suspend fun login(email: String, password: String): Result<LoggedInUser> { return try { @@ -35,29 +34,23 @@ class LoginDataSource { private suspend fun postLogin(email: String, password: String):String { - val moshi = Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() - - val api = Retrofit.Builder() - .baseUrl(Constants.BASE_URL) - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .build() - - val apiClient = api.create(ApiClient::class.java) + apiClient = ApiClient() + val service = apiClient.getApiService(context) return withContext(Dispatchers.IO) { try { val loginRequest = LoginRequest(email, password) - val response = apiClient.login(loginRequest).execute() + val response = service.login(loginRequest).execute() if (response.isSuccessful) { val loginResponse = response.body() loginResponse?.token ?: throw IOException("Token not received") } else { + Log.d("LoginDataSource", "${response.code()} ${response.message()}") throw IOException("${response.code()} ${response.message()}") } } catch (e: Exception) { + Log.d("LoginDataSource", "Login failed: ${e.message}") throw IOException("Login failed: ${e.message}") } } diff --git a/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt index bcc2777cdcfc8f2fb2f901308341f767067d0a7b..1d7293786899ffa28dc67352cbcd15d0149e34ab 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt @@ -3,6 +3,7 @@ package com.example.bondoyap.ui.login.data import com.example.bondoyap.ui.login.data.model.LoggedInUser import android.content.Context import android.content.SharedPreferences +import com.example.bondoyap.service.api.Constants.SHARED_PREFS_NAME import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory @@ -14,7 +15,7 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory class LoginRepository(val dataSource: LoginDataSource, context: Context) { - private val sharedPreferences: SharedPreferences = context.getSharedPreferences("Prefs", Context.MODE_PRIVATE) + private val sharedPreferences: SharedPreferences = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) private val editor: SharedPreferences.Editor = sharedPreferences.edit() private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() diff --git a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModelFactory.kt b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModelFactory.kt index b3b7f9747a79b29fc94a46c775d0ee604a767c45..ae367283563ca18363bc458743d2df24b2c51472 100644 --- a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModelFactory.kt +++ b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModelFactory.kt @@ -13,7 +13,7 @@ class SettingsViewModelFactory(private val context: Context) : ViewModelProvider if (modelClass.isAssignableFrom(SettingsViewModel::class.java)) { return SettingsViewModel( loginRepository = LoginRepository( - dataSource = LoginDataSource(), + dataSource = LoginDataSource(context), context = context ) ) as T