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 4922e8c8c86e285552ce73f1c32e6ac867988b7c..220c91ec8a9aa7dbc301c201c4dc6ee940a59f38 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 a44768c517814df3e5711974541cd247981d7b42..4b2a28c5896c24ed072671ed1cd60abe230d948a 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 6784cff22ca57949a5d01d4e1a9021dc96a0357e..44c20473d2786b93707d564744cf051938578004 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 1cc778ed77aadf60f4e1a7cfcb4c7b58f23f3af8..421ae736a2ecb1da986e12382be658594ab5c47c 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 6ca7957e5ed99e6a6da986750c5999332e650bd8..c80b801d8860c1a6f49e76a9f12186a7640b5a0c 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 b217d1cf924c0a0474466d5b8789a7e4533ec9df..48d3c7ad185788ebbb84409aaf84854f95275a84 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 1fdd587efa2cc3d8299f75c9c9aa48821a6e3ed2..1d121a2c67f49faad146c03e410668a0ae9a08d7 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 208444fb6fe700ef8348f7d4a644ff794cacce77..00ced609332f056205c39777cee1983887b89462 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()