fix(#688): route expired auth sessions to sign-in (#689)

* fix(#688): route expired auth sessions to sign-in

Adopt global 401 latch, WS disconnect, and Back/profile guards.

* chore: clean up formatting noise

* fix(ktlint): reorder imports to place java and kotlinx at end

* fix(ktlint): place java.io.IOException at end of imports

* test: fix NavBackStack type parameter in AuthenticationRoutingTest

* test: add AuthSessionState tests to AuthLoginViewModelTest

* fix(ktlint): fix import order in AuthLoginViewModelTest
This commit is contained in:
M57 authored and GitHub committed 2026-07-23 00:44:17 +03:00
1 parent def122ab58
commit a5baf41545
10 files changed
+217 -4

No files matched your search

@@ -0,0 +1,31 @@
package com.m57.hermescontrol.data.local
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Single source of truth for app-wide auth session expiry latch.
* When a REST call returns 401 (AuthExpired), [requireSignIn] is invoked,
* triggering central navigation reset to [AuthLoginScreen] and WS disconnect.
*/
object AuthSessionState {
private val _signInRequired = MutableStateFlow(false)
val signInRequired: StateFlow<Boolean> = _signInRequired.asStateFlow()
fun requireSignIn() {
_signInRequired.value = true
}
fun markAuthenticated() {
_signInRequired.value = false
}
fun clearSignInRequired() {
_signInRequired.value = false
}
fun resetForTest() {
_signInRequired.value = false
}
}
@@ -1,5 +1,6 @@
package com.m57.hermescontrol.data.remote
import com.m57.hermescontrol.data.local.AuthSessionState
import kotlinx.coroutines.delay
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
@@ -90,6 +91,7 @@ fun jitteredBackoff(baseMs: Long): Long {
suspend inline fun <reified T> safeApiCall(
retries: Int = 2,
reportAuthExpiry: Boolean = true,
crossinline call: suspend () -> Response<T>,
): NetworkResult<T> {
var lastException: IOException? = null
@@ -118,7 +120,11 @@ suspend inline fun <reified T> safeApiCall(
delay(backoff)
continue
}
return NetworkResult.Failure(mapHttpError(code, errorBody))
val failure = NetworkResult.Failure(mapHttpError(code, errorBody))
if (reportAuthExpiry && failure.error is NetworkError.AuthExpired) {
AuthSessionState.requireSignIn()
}
return failure
}
} catch (e: IOException) {
lastException = e