Replace the manual session-cookie interceptor in ApiClient and the manual cookie branch in TokenRefreshAuthenticator with a shared PersistentCookieJar backed by an encrypted, per-server EncryptedCookieStore. - Add CookieStore iface + EncryptedCookieStore (EncryptedSharedPreferences, per-server scope, legacy single-cookie migration) - Add CookieSerialization (OkHttp Cookie <-> holder) + PersistentCookieJar (in-mem cache, async persist, atomic useStore scope switching, prune, session-cookie accessor) - Add CookieManager coordinator (init/useStore/prune/clear) and wire its cookieJar into OkHttpProvider base/websocket/probe (lazy) - AuthManager.get/setSessionCookie now delegate to CookieManager; add pruneServerCache(); clear path drops the scope's session cookie - AuthLoginViewModel + HermesWsClient rely on the jar for Set-Cookie capture and session-cookie re-attachment (no manual header injection) - Add fakes (FakeEncryptedCookieStore/buildFakePersistentCookieJar) + unit tests (PersistentCookieJarTest, CookieManagerTest); inject fake jar into ApiClientTest/HermesWsClientTest setup WS token-out-of-URL (step 5) deferred: backend hermes-agent authenticates WS only via ws.query_params, so moving it requires a coordinated backend change (noted for follow-up). Verified: testDebugUnitTest (334 passing) + assembleDebug green via local SDK.
This commit is contained in:
15 files changed
+953
-88
No files matched your search
@@ -11,6 +11,7 @@ import com.m57.hermescontrol.data.config.ServerStoreSerializer
|
||||
import com.m57.hermescontrol.data.config.resolvedHost
|
||||
import com.m57.hermescontrol.data.config.resolvedPort
|
||||
import com.m57.hermescontrol.data.model.PinnedModel
|
||||
import com.m57.hermescontrol.data.remote.CookieManager
|
||||
import com.m57.hermescontrol.theme.BottomNavDisplayMode
|
||||
import com.m57.hermescontrol.theme.ThemePreference
|
||||
import com.m57.hermescontrol.theme.ThemePreset
|
||||
@@ -96,16 +97,6 @@ object AuthManager {
|
||||
val store = ServerStore(dataStore, scope)
|
||||
_serverStore = store
|
||||
|
||||
scope.launch {
|
||||
store.stateFlow.collect { state ->
|
||||
_bottomNavItemsFlow.value = state.bottomNavItems
|
||||
_themePreferenceFlow.value = state.themePreference
|
||||
_useDynamicColorsFlow.value = state.useDynamicColors
|
||||
_themePresetFlow.value = state.themePreset
|
||||
_bottomNavDisplayModeFlow.value = state.bottomNavDisplayMode
|
||||
}
|
||||
}
|
||||
|
||||
if (prefsDeferred == null) {
|
||||
prefsDeferred =
|
||||
CoroutineScope(Dispatchers.IO).async {
|
||||
@@ -123,6 +114,22 @@ object AuthManager {
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the encrypted cookie store (issue #470). The legacy
|
||||
// session-cookie prefs are passed as a Deferred so existing gated
|
||||
// sessions can be migrated on first load WITHOUT blocking startup.
|
||||
// The Deferred is created above, before this call.
|
||||
CookieManager.initialize(context, prefsDeferred)
|
||||
|
||||
scope.launch {
|
||||
store.stateFlow.collect { state ->
|
||||
_bottomNavItemsFlow.value = state.bottomNavItems
|
||||
_themePreferenceFlow.value = state.themePreference
|
||||
_useDynamicColorsFlow.value = state.useDynamicColors
|
||||
_themePresetFlow.value = state.themePreset
|
||||
_bottomNavDisplayModeFlow.value = state.bottomNavDisplayMode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,13 +162,25 @@ object AuthManager {
|
||||
/**
|
||||
* In gated mode (basic auth), the dashboard authenticates REST API
|
||||
* requests via the `hermes_session_at` cookie, not via
|
||||
* `Authorization: *** We store it here so [ApiClient]'s
|
||||
* authInterceptor can add it as a `Cookie` header.
|
||||
* `Authorization: Bearer`. The cookie is now owned by the shared
|
||||
* [CookieManager]/[PersistentCookieJar] (issue #470) which attaches it
|
||||
* automatically on every REST call, follows redirects, and persists it
|
||||
* encrypted. This accessor is a thin read-through to that store.
|
||||
*/
|
||||
fun getSessionCookie(): String? = requirePrefs().getString(KEY_SESSION_COOKIE, null)
|
||||
fun getSessionCookie(): String? = CookieManager.getSessionCookie()
|
||||
|
||||
fun setSessionCookie(cookie: String?) {
|
||||
requirePrefs().edit().putString(KEY_SESSION_COOKIE, cookie).apply()
|
||||
// The session cookie is host-scoped to the current dashboard host so
|
||||
// the CookieJar only attaches it to matching requests.
|
||||
CookieManager.setSessionCookie(cookie, AuthManager.getHost())
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict expired (non-session) cookies for the active server scope to
|
||||
* bound cookie growth (issue #470 step 7).
|
||||
*/
|
||||
fun pruneServerCache() {
|
||||
CookieManager.pruneServerCache()
|
||||
}
|
||||
|
||||
// ── Database Master Password ─────────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user