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):