From 3a4db0cb12bd5e6cfd513add90b1e0b8a4b6f371 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Wed, 18 Feb 2026 18:35:41 +0000 Subject: [PATCH] Always load user's theme from API if available, and update the localStorage cache. Previously in various situations the old cached value was being used instead, leading to inconsistent theming or wrong themeselector readout. --- frontend/auth/App.vue | 2 ++ frontend/auth/admin/AdminApp.vue | 2 ++ frontend/auth/restricted/index.html | 2 +- frontend/auth/restricted/theme.js | 4 ++-- frontend/int/reset/ResetApp.vue | 2 ++ frontend/src/components/RestrictedAuth.vue | 2 ++ frontend/src/stores/auth.js | 2 +- paskia/fastapi/api.py | 1 + paskia/util/apistructs.py | 3 ++- 9 files changed, 15 insertions(+), 5 deletions(-) diff --git a/frontend/auth/App.vue b/frontend/auth/App.vue index 70a005c..ed0591d 100644 --- a/frontend/auth/App.vue +++ b/frontend/auth/App.vue @@ -15,6 +15,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue' import { useAuthStore } from '@/stores/auth' import { apiJson, SessionValidator, createAuthIframe, removeAuthIframe } from 'paskia' import { getAuthIframeUrl } from '@/utils/api' +import { updateThemeFromSession } from '@/utils/theme' import StatusMessage from '@/components/StatusMessage.vue' import ProfileView from '@/components/ProfileView.vue' import HostProfileView from '@/components/HostProfileView.vue' @@ -67,6 +68,7 @@ async function loadUserInfo() { ]) store.userInfo = userInfoData store.ctx = validateData.ctx + updateThemeFromSession(store.userInfo) // Verify that the user UUIDs match between user-info and validate responses if (store.userInfo.user.uuid !== store.ctx.user.uuid) { console.error('User UUID mismatch between user-info and validate responses') diff --git a/frontend/auth/admin/AdminApp.vue b/frontend/auth/admin/AdminApp.vue index 17e7e28..c66770f 100644 --- a/frontend/auth/admin/AdminApp.vue +++ b/frontend/auth/admin/AdminApp.vue @@ -14,6 +14,7 @@ import AdminDialogs from '@/admin/AdminDialogs.vue' import { useAuthStore } from '@/stores/auth' import { adminUiPath, makeUiHref } from '@/utils/settings' import { apiJson, SessionValidator } from 'paskia' +import { updateThemeFromSession } from '@/utils/theme' import { uuidv7 } from 'uuidv7' import { getDirection } from '@/utils/keynav' import { goBack } from '@/utils/helpers' @@ -197,6 +198,7 @@ function orgUserCount(org) { async function loadUserInfo() { const data = await apiJson('/auth/api/validate', { method: 'POST' }) info.value = data + updateThemeFromSession(data.ctx) authenticated.value = true } diff --git a/frontend/auth/restricted/index.html b/frontend/auth/restricted/index.html index 1df4c7f..a71e29b 100644 --- a/frontend/auth/restricted/index.html +++ b/frontend/auth/restricted/index.html @@ -3,7 +3,7 @@ - + diff --git a/frontend/auth/restricted/theme.js b/frontend/auth/restricted/theme.js index 78858e6..8337c33 100644 --- a/frontend/auth/restricted/theme.js +++ b/frontend/auth/restricted/theme.js @@ -1,9 +1,9 @@ -// Early theme for restricted app - first URL param wins, then localStorage +// Early theme for restricted app - user preference (localStorage) wins, then URL param import { applyTheme, getCachedTheme } from '@/utils/theme.js' function getTheme() { const params = new URLSearchParams(location.hash.slice(1)) - return params.get('theme') || getCachedTheme() || '' + return getCachedTheme() || params.get('theme') || '' } // Apply theme class to document root diff --git a/frontend/int/reset/ResetApp.vue b/frontend/int/reset/ResetApp.vue index bb80886..3e9c419 100644 --- a/frontend/int/reset/ResetApp.vue +++ b/frontend/int/reset/ResetApp.vue @@ -60,6 +60,7 @@ import { computed, onMounted, reactive, ref } from 'vue' import passkey from '@/utils/passkey' import { getSettings, uiBasePath } from '@/utils/settings' import { apiJson, ApiError, getUserFriendlyErrorMessage } from 'paskia' +import { updateThemeFromSession } from '@/utils/theme' const status = reactive({ show: false, @@ -117,6 +118,7 @@ async function fetchTokenInfo() { headers: { 'Authorization': `Bearer ${token.value}` }, }) displayName.value = tokenInfo.value.display_name + if (tokenInfo.value.theme) updateThemeFromSession({ user: { theme: tokenInfo.value.theme } }) } catch (error) { console.error('Failed to load token info', error) const message = error instanceof ApiError diff --git a/frontend/src/components/RestrictedAuth.vue b/frontend/src/components/RestrictedAuth.vue index 6bc6645..4796ae5 100644 --- a/frontend/src/components/RestrictedAuth.vue +++ b/frontend/src/components/RestrictedAuth.vue @@ -61,6 +61,7 @@ import { getSettings, uiBasePath } from '@/utils/settings' import { fetchJson, getUserFriendlyErrorMessage } from 'paskia' import RemoteAuthRequest from '@/components/RemoteAuthRequest.vue' import { focusDialogButton } from '@/utils/keynav' +import { updateThemeFromSession } from '@/utils/theme' const props = defineProps({ mode: { @@ -147,6 +148,7 @@ async function fetchSettings() { async function validateSession() { try { session.value = await fetchJson('/auth/api/validate', { method: 'POST' }) + updateThemeFromSession(session.value?.ctx) if (isAuthenticated.value && props.mode !== 'reauth') { currentView.value = 'forbidden' emit('forbidden', session.value) diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js index 1f10d95..ae8ee5f 100644 --- a/frontend/src/stores/auth.js +++ b/frontend/src/stores/auth.js @@ -88,7 +88,7 @@ export const useAuthStore = defineStore('auth', { async loadUserInfo() { try { this.userInfo = await apiJson('/auth/api/user-info', { method: 'GET' }) - updateThemeFromSession(this.ctx) + updateThemeFromSession(this.userInfo) console.log('User info loaded:', this.userInfo) } catch (error) { // Suppress toast for 401/403 errors - the auth iframe will handle these diff --git a/paskia/fastapi/api.py b/paskia/fastapi/api.py index ffdd541..7519c89 100644 --- a/paskia/fastapi/api.py +++ b/paskia/fastapi/api.py @@ -244,6 +244,7 @@ async def token_info(credentials=Depends(bearer_auth)): ApiTokenInfo( token_type=reset_token.token_type, display_name=u.display_name, + theme=u.theme, ) ) diff --git a/paskia/util/apistructs.py b/paskia/util/apistructs.py index 5efad0f..a1cecef 100644 --- a/paskia/util/apistructs.py +++ b/paskia/util/apistructs.py @@ -171,11 +171,12 @@ class ApiSettings(msgspec.Struct): version: str -class ApiTokenInfo(msgspec.Struct): +class ApiTokenInfo(msgspec.Struct, omit_defaults=True): """Token info response struct.""" token_type: str display_name: str + theme: str = "" class ApiUuidResponse(msgspec.Struct):