From 97c66e4a6a601813c5e5b075fc08987475d8e9fb Mon Sep 17 00:00:00 2001 From: JojoIV <jonas.broeckmann@gmx.de> Date: Tue, 21 May 2024 14:32:45 +0200 Subject: [PATCH] Added support for persistent client sessions --- .../kaffeekasse/api/portal/PortalClient.kt | 42 ++++++++++++++++--- .../kaffeekasse/model/session/Session.kt | 23 ++++++---- .../kaffeekasse/repositories/Credentials.kt | 9 ++++ .../repositories/LoginRepository.kt | 40 +++++++++++++----- .../kaffeekasse/repositories/Settings.kt | 1 + .../repositories/i11/PortalRepository.kt | 11 ++++- .../kaffeekasse/ui/kaffeekasse/ManualBill.kt | 2 +- .../kaffeekasse/ui/login/UserSelection.kt | 3 -- 8 files changed, 101 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/api/portal/PortalClient.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/api/portal/PortalClient.kt index 4922e8c..220c91e 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/api/portal/PortalClient.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/api/portal/PortalClient.kt @@ -17,6 +17,8 @@ import io.ktor.http.Parameters import io.ktor.http.URLBuilder import io.ktor.http.Url import io.ktor.http.parameters +import io.ktor.http.parseServerSetCookieHeader +import io.ktor.http.renderSetCookieHeader import io.ktor.serialization.kotlinx.json.json import it.skrape.core.htmlDocument import it.skrape.selects.Doc @@ -74,20 +76,46 @@ class PortalClient( private val portal = object : Module(this, "portal") { - val currentSession = MutableStateFlow(PortalAPIResponse.Session()) - suspend fun updateSession(): Unit = withContext(computationScope) { + val currentSession = MutableStateFlow(Session()) + suspend fun updateSession(restore: String? = null): Unit = withContext(computationScope) { + if (restore != null) { + debug { "Restoring cookie: $restore" } + try { + val cookie = parseServerSetCookieHeader(restore) + cookiesStorage.addCookie(baseUrl, cookie) + debug { "Restored cookie: $cookie" } + } catch (e: Throwable) { + error(e) { "Failed to restore cookie" } + } + } + debug { "Updating session" } val response = requestAPI(updateSession = false).decodeAs<PortalAPIResponse>() - debug { "Session: ${response.session}" } - currentSession.value = response.session + val serializedCookie = cookiesStorage.get(baseUrl) + .firstOrNull { it.name == SessionCookieName } + ?.let { renderSetCookieHeader(it) } + debug { "Session: ${response.session}; Cookie: $serializedCookie" } + + currentSession.value = Session( + response.session, + serializedCookie + ) } + private val SessionCookieName = "PORTALSESSID" } val session get() = portal.currentSession.asStateFlow() + data class Session( + val response: PortalAPIResponse.Session = PortalAPIResponse.Session(), + val data: String? = null + ) + + suspend fun forceUpdateSession(restore: String? = null) = portal.updateSession(restore) + suspend fun login(login: Login) { require(login.isValid) { "Invalid login" } - if (session.value.username == login.username) return + if (session.value.response.username == login.username) return debug { "Logging in as ${login.username}" } post( portal.moduleUrl, @@ -98,7 +126,7 @@ class PortalClient( }, updateSession = true ) - debug { "Logged in: ${session.value.isLoggedIn}" } + debug { "Logged in: ${session.value.response.isLoggedIn}" } } suspend fun logoutAll() { @@ -182,3 +210,5 @@ class PortalClient( protected val computationScope get() = client.computationScope } } + + diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/model/session/Session.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/model/session/Session.kt index a44768c..4b2a28c 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/model/session/Session.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/model/session/Session.kt @@ -2,7 +2,10 @@ package net.novagamestudios.kaffeekasse.model.session sealed interface Session : java.io.Serializable { - data object Empty : Session + val data: String? + data object Empty : Session { + override val data: String? get() = null + } interface WithDevice : Session { val device: Device } @@ -14,13 +17,14 @@ sealed interface Session : java.io.Serializable { companion object { operator fun invoke( device: Device?, - user: User? + user: User?, + data: String? = null ): Session { val realUser = user?.takeIf { it.isRealUser } return when { - device != null && realUser != null -> SessionWithDeviceAndRealUser(device, realUser) - device != null -> SessionWithDevice(device) - realUser != null -> SessionWithRealUser(realUser) + device != null && realUser != null -> SessionWithDeviceAndRealUser(device, realUser, data) + device != null -> SessionWithDevice(device, data) + realUser != null -> SessionWithRealUser(realUser, data) else -> Empty } } @@ -37,14 +41,17 @@ private val User.isRealUser get() = !isKaffeekasse private data class SessionWithDevice( - override val device: Device + override val device: Device, + override val data: String? ) : Session.WithDevice private data class SessionWithRealUser( - override val realUser: User + override val realUser: User, + override val data: String? ) : Session.WithRealUser private data class SessionWithDeviceAndRealUser( override val device: Device, - override val realUser: User + override val realUser: User, + override val data: String? ) : Session.WithDeviceAndUser diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/Credentials.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/Credentials.kt index 6784cff..44c2047 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/Credentials.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/Credentials.kt @@ -57,6 +57,15 @@ class Credentials( } + suspend fun sessionData(): String? { + return settingsStore.value.savedSessionData + } + + suspend fun storeSessionData(data: String?) { + settingsStore.update { it.copy(savedSessionData = data) } + } + + context (Context) suspend fun userLogin(): Login? { getDebugUserLogin()?.let { return it } diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/LoginRepository.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/LoginRepository.kt index 1cc778e..421ae73 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/LoginRepository.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/LoginRepository.kt @@ -51,6 +51,10 @@ class LoginRepository( val errors = _errors.asStateFlow() + suspend fun forceUpdateSession() = action { + portal.forceUpdateSession() + } + val autoLogin by lazy { settings.values.mapState { it.autoLogin } } private var autoLoginAttemptAvailable = true @@ -77,6 +81,14 @@ class LoginRepository( debug { "Auto login enabled: ${autoLogin.value}" } if (!autoLogin.value) return@action null + + val sessionData = credentialsRepository.sessionData() + debug { "Auto login session data: $sessionData" } + if (sessionData != null) { + portal.forceUpdateSession(sessionData) + if (portal.session.value !is Session.Empty) return@action null + } + val login = with(activityContext) { credentialsRepository.userLogin() ?.takeIf { it.isValid } @@ -88,12 +100,13 @@ class LoginRepository( autoLoginAttemptAvailable = false info { "Trying to auto login user: ${login.username}" } performUserLogin(login) + credentialsRepository.storeSessionData(portal.session.value.data) return@action login } - suspend fun login(login: Login, autoLogin: Boolean, activityContext: Context) { + suspend fun login(login: Login, autoLogin: Boolean, activityContext: Context) = action { val cleanedLogin = login.copy(username = login.username.trim()) - if (!performUserLogin(cleanedLogin)) return + if (!performUserLogin(cleanedLogin)) return@action with(activityContext) { updateAutoLogin(cleanedLogin, autoLogin) } @@ -103,15 +116,19 @@ class LoginRepository( context (Context) private suspend fun updateAutoLogin(login: Login, autoLogin: Boolean) { settings.update { it.copy(autoLogin = autoLogin) } - if (autoLogin) when (val result = credentialsRepository.storeUserLogin(login)) { - Credentials.StoreResult.Success -> { } - Credentials.StoreResult.Cancelled -> { } - Credentials.StoreResult.Unsupported -> { - toastShort("Credential storage not supported") - settings.tryUpdate { it.copy(autoLogin = false) } - } - is Credentials.StoreResult.Error -> { - toastShort("Failed to store credentials: ${result.message}") + if (autoLogin) { + credentialsRepository.storeSessionData(portal.session.value.data) + when (val result = credentialsRepository.storeUserLogin(login)) { + Credentials.StoreResult.Success -> { } + Credentials.StoreResult.Cancelled -> { } + Credentials.StoreResult.Unsupported -> { + toastShort("Credential storage not supported") + settings.tryUpdate { it.copy(autoLogin = false) } + } + + is Credentials.StoreResult.Error -> { + toastShort("Failed to store credentials: ${result.message}") + } } } } @@ -149,6 +166,7 @@ class LoginRepository( suspend fun logoutUser() = action { + credentialsRepository.storeSessionData(null) portal.logoutUser() } diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/Settings.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/Settings.kt index 6ca7957..c80b801 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/Settings.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/Settings.kt @@ -25,6 +25,7 @@ data class Settings( val fullscreen: Boolean = false, val animationsEnabled: Boolean = true, val autoLogin: Boolean = false, + val savedSessionData: String? = null, val deviceCredentials: DeviceCredentials? = null, val developerMode: Boolean = false, val userSettings: Map<String, UserSettings> = emptyMap() diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/PortalRepository.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/PortalRepository.kt index b217d1c..48d3c7a 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/PortalRepository.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/i11/PortalRepository.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import net.novagamestudios.common_utils.Logger +import net.novagamestudios.common_utils.debug import net.novagamestudios.common_utils.info import net.novagamestudios.common_utils.warn import net.novagamestudios.kaffeekasse.api.kaffeekasse.KaffeekasseAPI @@ -31,12 +32,20 @@ class PortalRepository( client.session, kaffeekasse.currentDevice ) { session, device -> + val response = session.response Session( device = device, - user = if (session.isLoggedIn) User(session.username, session.displayName) else null + user = if (response.isLoggedIn) User(response.username, response.displayName) else null, + data = session.data ) }.stateIn(coroutineScope, SharingStarted.Eagerly, Session.Empty) + suspend fun forceUpdateSession(restore: String? = null) { + info { "Updating session" } + client.forceUpdateSession(restore) + debug { "Session updated" } + } + suspend fun loginUser(login: Login) { if (session.value is Session.WithRealUser) throw IllegalStateException("User already logged in") client.login(login) diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/ManualBill.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/ManualBill.kt index 1fdd587..1d121a2 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/ManualBill.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/ManualBill.kt @@ -145,7 +145,7 @@ class ManualBillScreenModel private constructor( get() = currentPagerIndex - 1 set(value) { currentPagerIndex = value + 1 } - @Suppress("unused") + @Suppress("unused", "RedundantSuspendModifier") suspend fun autoScrollToDeviceItemGroup() { val initialItemGroupIndex = session.deviceOrNull?.itemTypeId ?.let { id -> itemGroups.indexOfFirst { it.id == id }.takeIf { it >= 0 } } diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt index 208444f..00ced60 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt @@ -102,7 +102,6 @@ import net.novagamestudios.kaffeekasse.ui.util.navigation.BackNavigationHandler import net.novagamestudios.kaffeekasse.ui.util.rememberPullToRefreshState import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelFactory import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelProvider -import net.novagamestudios.kaffeekasse.ui.util.screenmodel.collectAsStateHere import net.novagamestudios.kaffeekasse.util.richdata.asRichDataFlow import net.novagamestudios.kaffeekasse.util.richdata.combineRich import net.novagamestudios.kaffeekasse.util.richdata.stateIn @@ -114,8 +113,6 @@ class UserSelectionScreenModel private constructor( val loginRepository: LoginRepository, kaffeekasse: KaffeekasseRepository ) : ScreenModel, Logger { - val isPerformingLoginAction by loginRepository.isPerformingAction.collectAsStateHere() - val users = kaffeekasse.basicUserInfoList val searchState = TopBarSearchFieldState() -- GitLab