diff --git a/core/lib/Controllers/DefaultController.php b/core/lib/Controllers/DefaultController.php index 63ae379..567aaa8 100644 --- a/core/lib/Controllers/DefaultController.php +++ b/core/lib/Controllers/DefaultController.php @@ -29,7 +29,7 @@ class DefaultController extends ControllerAbstract return new FileResponse( $this->rootDir . '/public/private.html', Response::HTTP_OK, - ['Content-Type' => 'text/html'] + ['Content-Type' => 'text/html', 'Cache-Control' => 'no-store'] ); } @@ -38,7 +38,7 @@ class DefaultController extends ControllerAbstract $response = new FileResponse( $this->rootDir . '/public/public.html', Response::HTTP_OK, - ['Content-Type' => 'text/html'] + ['Content-Type' => 'text/html', 'Cache-Control' => 'no-store'] ); // Clear any stale auth cookies since the user is not authenticated @@ -58,7 +58,7 @@ class DefaultController extends ControllerAbstract return new FileResponse( $this->rootDir . '/public/public.html', Response::HTTP_OK, - ['Content-Type' => 'text/html'] + ['Content-Type' => 'text/html', 'Cache-Control' => 'no-store'] ); } @@ -120,7 +120,7 @@ class DefaultController extends ControllerAbstract return new FileResponse( $this->rootDir . '/public/private.html', Response::HTTP_OK, - ['Content-Type' => 'text/html'] + ['Content-Type' => 'text/html', 'Cache-Control' => 'no-store'] ); } @@ -128,7 +128,7 @@ class DefaultController extends ControllerAbstract $response = new FileResponse( $this->rootDir . '/public/public.html', Response::HTTP_OK, - ['Content-Type' => 'text/html'] + ['Content-Type' => 'text/html', 'Cache-Control' => 'no-store'] ); // Clear any stale auth cookies since the user is not authenticated diff --git a/core/src/private.ts b/core/src/private.ts index 94e9221..2a7153b 100644 --- a/core/src/private.ts +++ b/core/src/private.ts @@ -8,6 +8,7 @@ import { useModuleStore } from '@KTXC/stores/moduleStore' import { useTenantStore } from '@KTXC/stores/tenantStore' import { useUserStore } from '@KTXC/stores/userStore' import { fetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper' +import { FetchError } from '@KTXC/utils/helpers/fetch-wrapper-core' import { initializeModules } from '@KTXC/utils/modules' import { createSessionMonitor } from '@KTXC/services/authManager' import App from './App.vue' @@ -62,5 +63,11 @@ globalWindow.Pinia = PiniaLib as unknown app.mount('#app'); } catch (e) { console.error('Bootstrap failed:', e); + // The private shell is unusable without /init. If the server rejected + // the session, drop the stale local identity and return to login. + if (e instanceof FetchError && (e.status === 401 || e.status === 403)) { + userStore.clearAuth(); + window.location.href = '/login'; + } } })(); diff --git a/core/src/public.ts b/core/src/public.ts index fc68a25..059f16f 100644 --- a/core/src/public.ts +++ b/core/src/public.ts @@ -2,16 +2,18 @@ import { createApp } from 'vue'; import { createPinia } from 'pinia'; import App from './App.vue'; import { router } from './router'; +import { AUTH_STORAGE_KEY } from '@KTXC/stores/userStore'; import vuetify from './plugins/vuetify/index'; // Material Design Icons (Vuetify mdi icon set) import '@mdi/font/css/materialdesignicons.min.css'; import '@fontsource/public-sans/index.css' -// The public app is served when the user has no valid server session. -// Clear any stale identity data from localStorage to ensure the client -// state matches the server's determination that the user is unauthenticated. -//localStorage.removeItem('identityStore.self'); +// The public app is only served when the server has determined the user is +// unauthenticated. Clear any stale identity persisted by a previous session, +// otherwise the shared router guard would treat the user as authenticated +// and render the private shell without any of its state loaded. +localStorage.removeItem(AUTH_STORAGE_KEY); const app = createApp(App); const pinia = createPinia(); @@ -19,15 +21,4 @@ app.use(pinia); app.use(router); app.use(vuetify); -// Wait for router to be ready, then ensure we're on a public route -//router.isReady().then(() => { - // If the current route requires auth, redirect to login - // This handles the case where user navigates to / with an expired session - //const currentRoute = router.currentRoute.value; - //const requiresAuth = currentRoute.matched.some(record => record.meta?.requiresAuth); - //if (requiresAuth || currentRoute.path === '/') { - // router.replace('/login'); - //} -//}); - app.mount('#app'); diff --git a/core/src/stores/userStore.ts b/core/src/stores/userStore.ts index be4b034..d9c68b3 100644 --- a/core/src/stores/userStore.ts +++ b/core/src/stores/userStore.ts @@ -8,7 +8,8 @@ import type { UserSettingsInterface } from '@KTXC/types/user/userSettingsTypes'; import { UserProfile } from '@KTXC/models/userProfile'; import { UserSettings } from '@KTXC/models/userSettings'; -const STORAGE_KEY = 'userStore.auth'; +export const AUTH_STORAGE_KEY = 'userStore.auth'; +const STORAGE_KEY = AUTH_STORAGE_KEY; // Flush pending updates before page unload if (typeof window !== 'undefined') { diff --git a/core/src/utils/helpers/fetch-wrapper-core.ts b/core/src/utils/helpers/fetch-wrapper-core.ts index 0b81bd8..a320bfe 100644 --- a/core/src/utils/helpers/fetch-wrapper-core.ts +++ b/core/src/utils/helpers/fetch-wrapper-core.ts @@ -22,6 +22,14 @@ export interface RequestCallOptions { headers?: Record; } +/** Error thrown for non-2xx responses, carrying the HTTP status code */ +export class FetchError extends Error { + constructor(message: string, public readonly status: number) { + super(message); + this.name = 'FetchError'; + } +} + function getCsrfToken(): string | null { if (typeof document === 'undefined') return null; const cookie = document.cookie.split('; ').find(r => r.startsWith('X-CSRF-TOKEN=')); @@ -61,8 +69,11 @@ export function createFetchWrapper(options: FetchWrapperOptions = {}) { if (!response.ok) { const text = await response.text(); - const data = text ? JSON.parse(text) : null; - throw new Error(data?.message || response.statusText); + let data = null; + try { + data = text ? JSON.parse(text) : null; + } catch { /* non-JSON error body */ } + throw new FetchError(data?.message || response.statusText, response.status); } if (callOptions?.onStream) {