feat(android): centralize mobile session refresh and password change

This commit is contained in:
2026-09-08 17:11:54 -03:00
parent a976d40d45
commit ad72f3505c
@@ -0,0 +1,98 @@
package com.korexlabs.dhinspeccion.data
import android.content.Context
import com.korexlabs.dhinspeccion.BuildConfig
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okhttp3.OkHttpClient
import retrofit2.HttpException
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.POST
data class ChangeMobilePasswordRequest(
val currentPassword: String,
val newPassword: String,
)
data class ChangeMobilePasswordResponse(
val status: String,
val mustChangePassword: Boolean,
)
private interface MobileSessionApi {
@POST("auth/mobile/refresh")
suspend fun refresh(@Body request: RefreshRequest): MobileSessionResponse
@POST("auth/change-password")
suspend fun changePassword(
@Header("Authorization") authorization: String,
@Body request: ChangeMobilePasswordRequest,
): ChangeMobilePasswordResponse
}
/**
* Único coordinador de sesión del proceso Android.
*
* Todos los repositorios comparten el mismo gate de refresh para que un token rotativo no sea
* consumido en paralelo por Inventario, Hallazgos y Actas.
*/
class MobileSessionCoordinator private constructor(context: Context) {
private val store = SecureSessionStore(context.applicationContext)
private val gate = SessionRefreshGate()
private val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
private val api: MobileSessionApi = Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.client(OkHttpClient.Builder().build())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
.create(MobileSessionApi::class.java)
fun currentSession(): StoredSession? = store.load()
fun save(response: MobileSessionResponse): StoredSession = store.save(response)
fun clear() = store.clear()
suspend fun <T> authorized(block: suspend (StoredSession) -> T): T {
var session = store.load() ?: throw IllegalStateException("Sesión no iniciada")
try {
return block(session)
} catch (error: HttpException) {
if (error.code() != 401) throw error
}
session = gate.refreshIfNeeded(
previousRefreshToken = session.refreshToken,
load = store::load,
save = store::save,
clear = store::clear,
refresh = { token -> api.refresh(RefreshRequest(token)) },
)
return block(session)
}
suspend fun changePassword(currentPassword: String, newPassword: String): StoredSession {
val sessionUsed = authorized { session ->
api.changePassword(
authorization = "Bearer ${session.accessToken}",
request = ChangeMobilePasswordRequest(currentPassword, newPassword),
)
session
}
val latest = store.load() ?: sessionUsed
return store.save(latest.copy(mustChangePassword = false))
}
companion object {
@Volatile
private var instance: MobileSessionCoordinator? = null
fun get(context: Context): MobileSessionCoordinator =
instance ?: synchronized(this) {
instance ?: MobileSessionCoordinator(context.applicationContext).also { instance = it }
}
}
}