refactor(android): share auth and clean uploaded signatures

This commit is contained in:
2026-09-08 17:16:55 -03:00
parent 516445fea6
commit 33584d545f
@@ -4,15 +4,12 @@ import android.content.Context
import com.korexlabs.dhinspeccion.BuildConfig import com.korexlabs.dhinspeccion.BuildConfig
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.HttpException
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.Body import retrofit2.http.Body
@@ -303,14 +300,10 @@ private interface MobileActsApi {
@Path("visitId") visitId: String, @Path("visitId") visitId: String,
@Body request: MobileCloseVisitRequest, @Body request: MobileCloseVisitRequest,
): VisitDetail ): VisitDetail
@POST("auth/mobile/refresh")
suspend fun refresh(@Body request: RefreshRequest): MobileSessionResponse
} }
class MobileActsRepository(context: Context) { class MobileActsRepository(context: Context) {
private val store = SecureSessionStore(context.applicationContext) private val sessions = MobileSessionCoordinator.get(context.applicationContext)
private val refreshMutex = Mutex()
private val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() private val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
private val api: MobileActsApi = Retrofit.Builder() private val api: MobileActsApi = Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL) .baseUrl(BuildConfig.API_BASE_URL)
@@ -319,11 +312,11 @@ class MobileActsRepository(context: Context) {
.build() .build()
.create(MobileActsApi::class.java) .create(MobileActsApi::class.java)
suspend fun list(visitId: String): MobileActListResponse = authorized { session -> suspend fun list(visitId: String): MobileActListResponse = sessions.authorized { session ->
api.listActs("Bearer ${session.accessToken}", visitId) api.listActs("Bearer ${session.accessToken}", visitId)
} }
suspend fun get(actId: String): MobileActDetail = authorized { session -> suspend fun get(actId: String): MobileActDetail = sessions.authorized { session ->
api.act("Bearer ${session.accessToken}", actId) api.act("Bearer ${session.accessToken}", actId)
} }
@@ -332,7 +325,7 @@ class MobileActsRepository(context: Context) {
assetId: String, assetId: String,
visitCode: String, visitCode: String,
urgency: String = "NON_URGENT", urgency: String = "NON_URGENT",
): MobileActDetail = authorized { session -> ): MobileActDetail = sessions.authorized { session ->
api.createAct( api.createAct(
"Bearer ${session.accessToken}", "Bearer ${session.accessToken}",
visitId, visitId,
@@ -350,20 +343,20 @@ class MobileActsRepository(context: Context) {
val detail = get(actId) val detail = get(actId)
if (detail.assets.any { it.id == assetId }) return detail if (detail.assets.any { it.id == assetId }) return detail
val ids = (detail.assets.map { it.id } + assetId).distinct() val ids = (detail.assets.map { it.id } + assetId).distinct()
return authorized { session -> return sessions.authorized { session ->
api.updateAct("Bearer ${session.accessToken}", actId, UpdateMobileActRequest(ids)) api.updateAct("Bearer ${session.accessToken}", actId, UpdateMobileActRequest(ids))
} }
} }
suspend fun closure(actId: String): MobileActClosure = authorized { session -> suspend fun closure(actId: String): MobileActClosure = sessions.authorized { session ->
api.closure("Bearer ${session.accessToken}", actId) api.closure("Bearer ${session.accessToken}", actId)
} }
suspend fun setResponsible(actId: String, request: MobileResponsibleRequest): MobileActClosure = authorized { session -> suspend fun setResponsible(actId: String, request: MobileResponsibleRequest): MobileActClosure = sessions.authorized { session ->
api.responsible("Bearer ${session.accessToken}", actId, request) api.responsible("Bearer ${session.accessToken}", actId, request)
} }
suspend fun lock(actId: String): MobileActClosure = authorized { session -> suspend fun lock(actId: String): MobileActClosure = sessions.authorized { session ->
api.lock("Bearer ${session.accessToken}", actId) api.lock("Bearer ${session.accessToken}", actId)
} }
@@ -394,7 +387,7 @@ class MobileActsRepository(context: Context) {
statement = statement, statement = statement,
) )
suspend fun companyOutcome(actId: String, status: String, reason: String): MobileActClosure = authorized { session -> suspend fun companyOutcome(actId: String, status: String, reason: String): MobileActClosure = sessions.authorized { session ->
api.companyOutcome( api.companyOutcome(
"Bearer ${session.accessToken}", "Bearer ${session.accessToken}",
actId, actId,
@@ -402,11 +395,11 @@ class MobileActsRepository(context: Context) {
) )
} }
suspend fun seal(actId: String): MobileActClosure = authorized { session -> suspend fun seal(actId: String): MobileActClosure = sessions.authorized { session ->
api.sealAct("Bearer ${session.accessToken}", actId, MobileSealActRequest()) api.sealAct("Bearer ${session.accessToken}", actId, MobileSealActRequest())
} }
suspend fun closeVisit(visitId: String): VisitDetail = authorized { session -> suspend fun closeVisit(visitId: String): VisitDetail = sessions.authorized { session ->
api.closeVisit("Bearer ${session.accessToken}", visitId, MobileCloseVisitRequest()) api.closeVisit("Bearer ${session.accessToken}", visitId, MobileCloseVisitRequest())
} }
@@ -419,53 +412,35 @@ class MobileActsRepository(context: Context) {
company: Boolean, company: Boolean,
manifestation: String? = null, manifestation: String? = null,
statement: String? = null, statement: String? = null,
): MobileActClosure = authorized { session -> ): MobileActClosure {
val text = "text/plain".toMediaType() val response = sessions.authorized { session ->
val file = MultipartBody.Part.createFormData( val text = "text/plain".toMediaType()
"file", val filePart = MultipartBody.Part.createFormData(
png.name, "file",
png.asRequestBody("image/png".toMediaType()), png.name,
) png.asRequestBody("image/png".toMediaType()),
val consent = "true".toRequestBody(text)
val signedAt = Instant.now().toString().toRequestBody(text)
val device = "DH Android".toRequestBody(text)
val lat = latitude?.toString()?.toRequestBody(text)
val lon = longitude?.toString()?.toRequestBody(text)
val accuracy = accuracyM?.toString()?.toRequestBody(text)
if (company) {
api.signCompany(
"Bearer ${session.accessToken}", actId, file, consent, signedAt,
lat, lon, accuracy, device,
manifestation?.toRequestBody(text),
statement?.trim()?.takeIf { it.isNotBlank() }?.toRequestBody(text),
)
} else {
api.signInspector(
"Bearer ${session.accessToken}", actId, file, consent, signedAt,
lat, lon, accuracy, device,
) )
val consent = "true".toRequestBody(text)
val signedAt = Instant.now().toString().toRequestBody(text)
val device = "DH Android".toRequestBody(text)
val lat = latitude?.toString()?.toRequestBody(text)
val lon = longitude?.toString()?.toRequestBody(text)
val accuracy = accuracyM?.toString()?.toRequestBody(text)
if (company) {
api.signCompany(
"Bearer ${session.accessToken}", actId, filePart, consent, signedAt,
lat, lon, accuracy, device,
manifestation?.toRequestBody(text),
statement?.trim()?.takeIf { it.isNotBlank() }?.toRequestBody(text),
)
} else {
api.signInspector(
"Bearer ${session.accessToken}", actId, filePart, consent, signedAt,
lat, lon, accuracy, device,
)
}
} }
} png.delete()
return response
private 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 = refresh(session.refreshToken)
return block(session)
}
private suspend fun refresh(previousRefreshToken: String): StoredSession = refreshMutex.withLock {
val latest = store.load() ?: throw IllegalStateException("Sesión no iniciada")
if (latest.refreshToken != previousRefreshToken) return@withLock latest
try {
store.save(api.refresh(RefreshRequest(previousRefreshToken)))
} catch (error: Throwable) {
store.clear()
throw error
}
} }
} }