Fix session expiration handling
This commit is contained in:
@@ -18,8 +18,8 @@ android {
|
||||
applicationId "net.linkedcorp.linkedsign"
|
||||
minSdk 26
|
||||
targetSdk 35
|
||||
versionCode 10002
|
||||
versionName "1.0.2"
|
||||
versionCode 10003
|
||||
versionName "1.0.3"
|
||||
|
||||
buildConfigField "String", "API_BASE_URL", "\"${apiBaseUrl}\""
|
||||
}
|
||||
|
||||
@@ -214,8 +214,13 @@ class LinkedSignRepository(private val context: Context) {
|
||||
} catch (error: HttpException) {
|
||||
if (error.code() != 401) throw mapHttpException(error)
|
||||
|
||||
val refreshToken = session.refreshToken ?: throw AppException("Tu sesión expiró. Ingresa nuevamente.")
|
||||
val refresh = unwrap(service.refresh(RefreshRequest(refreshToken)))
|
||||
val refreshToken = session.refreshToken ?: throwSessionExpired()
|
||||
val refresh = try {
|
||||
unwrap(service.refresh(RefreshRequest(refreshToken)))
|
||||
} catch (refreshError: Exception) {
|
||||
sessionStore.clearSession()
|
||||
throwSessionExpired()
|
||||
}
|
||||
val updated = session.copy(
|
||||
accessToken = refresh.accessToken,
|
||||
refreshToken = refresh.refreshToken
|
||||
@@ -226,6 +231,10 @@ class LinkedSignRepository(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun throwSessionExpired(): Nothing {
|
||||
throw AppException("Tu sesión expiró. Vuelve a iniciar sesión.", "SESSION_EXPIRED")
|
||||
}
|
||||
|
||||
private suspend fun <T> execute(block: suspend () -> T): T {
|
||||
return try {
|
||||
block()
|
||||
|
||||
@@ -156,7 +156,8 @@ data class SignData(
|
||||
data class SessionSnapshot(
|
||||
val accessToken: String? = null,
|
||||
val refreshToken: String? = null,
|
||||
val user: SessionUser? = null
|
||||
val user: SessionUser? = null,
|
||||
val expiresAt: Long? = null
|
||||
)
|
||||
|
||||
data class BiometricAccessState(
|
||||
|
||||
@@ -3,8 +3,10 @@ package net.linkedcorp.linkedsign.data
|
||||
import android.content.Context
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import android.util.Base64
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import org.json.JSONObject
|
||||
|
||||
class SessionStore(context: Context) {
|
||||
private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
||||
@@ -26,15 +28,27 @@ class SessionStore(context: Context) {
|
||||
val accessToken = preferences.getString(KEY_ACCESS, null)
|
||||
val refreshToken = preferences.getString(KEY_REFRESH, null)
|
||||
val userJson = preferences.getString(KEY_USER, null)
|
||||
val expiresAt = preferences.getLong(KEY_EXPIRES_AT, 0L).takeIf { it > 0L }
|
||||
|
||||
if (expiresAt != null && expiresAt <= System.currentTimeMillis()) {
|
||||
clearSession()
|
||||
return SessionSnapshot()
|
||||
}
|
||||
|
||||
val user = userJson?.let { userAdapter.fromJson(it) }
|
||||
return SessionSnapshot(accessToken, refreshToken, user)
|
||||
return SessionSnapshot(accessToken, refreshToken, user, expiresAt)
|
||||
}
|
||||
|
||||
fun save(session: SessionSnapshot) {
|
||||
val expiresAt = session.expiresAt
|
||||
?: session.accessToken?.let { readAccessTokenExpiry(it) }
|
||||
?: (System.currentTimeMillis() + DEFAULT_SESSION_MS)
|
||||
|
||||
preferences.edit()
|
||||
.putString(KEY_ACCESS, session.accessToken)
|
||||
.putString(KEY_REFRESH, session.refreshToken)
|
||||
.putString(KEY_USER, session.user?.let { userAdapter.toJson(it) })
|
||||
.putLong(KEY_EXPIRES_AT, expiresAt)
|
||||
.apply()
|
||||
}
|
||||
|
||||
@@ -47,9 +61,21 @@ class SessionStore(context: Context) {
|
||||
.remove(KEY_ACCESS)
|
||||
.remove(KEY_REFRESH)
|
||||
.remove(KEY_USER)
|
||||
.remove(KEY_EXPIRES_AT)
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun readAccessTokenExpiry(accessToken: String): Long? {
|
||||
return try {
|
||||
val payload = accessToken.split(".").getOrNull(1) ?: return null
|
||||
val decoded = String(Base64.decode(payload, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING))
|
||||
val exp = JSONObject(decoded).optLong("exp", 0L)
|
||||
if (exp > 0L) exp * 1000L else null
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun loadBiometricState(): BiometricAccessState {
|
||||
val enabled = preferences.getBoolean(KEY_BIO_ENABLED, false)
|
||||
val configured = preferences.getBoolean(KEY_BIO_CONFIGURED, false)
|
||||
@@ -106,9 +132,11 @@ class SessionStore(context: Context) {
|
||||
private const val KEY_ACCESS = "access_token"
|
||||
private const val KEY_REFRESH = "refresh_token"
|
||||
private const val KEY_USER = "user"
|
||||
private const val KEY_EXPIRES_AT = "expires_at"
|
||||
private const val KEY_BIO_ENABLED = "bio_enabled"
|
||||
private const val KEY_BIO_CONFIGURED = "bio_configured"
|
||||
private const val KEY_BIO_EMAIL = "bio_email"
|
||||
private const val KEY_BIO_REFRESH = "bio_refresh_token"
|
||||
private const val DEFAULT_SESSION_MS = 15 * 60 * 1000L
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +189,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
uiState = buildState(session, cache, loading = false, syncing = uiState.syncing)
|
||||
}
|
||||
|
||||
private fun normalize(error: Exception): AppException =
|
||||
if (error is AppException) error else AppException(error.message ?: "Ocurrió un error inesperado.")
|
||||
private fun normalize(error: Exception): AppException {
|
||||
if (error is AppException && error.code == "SESSION_EXPIRED") {
|
||||
refreshState()
|
||||
}
|
||||
|
||||
return if (error is AppException) error else AppException(error.message ?: "Ocurrió un error inesperado.")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user