Files
oxspeak_server/.junie/plans/session-store-frontend-refactor.md
T
2026-07-02 00:20:42 +02:00

14 KiB

sessionId
sessionId
session-260701-224904-11t1

Requirements

Overview & Goals

Refactor the frontend session lifecycle so a new Pinia sessionStore becomes the single orchestrator for bootstrap, authenticated startup, initial private data loading, login, logout, gateway connection, and cleanup.

In Scope

  • Add frontend/src/stores/session.ts with isBootstrapping, isReady, and lifecycle actions.
  • Refactor authStore to manage identity only.
  • Refactor appStore to keep only global non-user app state such as baseurl.
  • Refactor gatewayStore to manage WebSocket connection state only and remove global window orchestration events.
  • Add reset() actions to server, category, channel, and message stores.
  • Update app startup, router guards, login flow, and any logout call sites to use sessionStore.
  • Verify backend WebSocket authentication remains enforced.

Out of Scope

  • Rewriting business data fetching APIs beyond moving initial orchestration into sessionStore.
  • Adding a new UI design or a new full-screen loader component unless needed for compilation/runtime correctness.
  • Changing backend auth semantics unless /ws/gateway is found not to reject unauthenticated users.

Acceptance Criteria

  • authStore no longer knows about gateway, bootstrap, or business stores.
  • gatewayStore no longer dispatches or relies on gateway:connected/gateway:disconnected browser events.
  • appStore.initialize() no longer orchestrates auth/gateway/data loading, and app startup uses sessionStore.bootstrap() instead.
  • No WebSocket /ws/gateway connection is attempted for unauthenticated users.
  • On login, user identity is loaded, gateway connects once, and server, category, and channel initial data are loaded once.
  • On logout, gateway disconnects, private business stores reset, and authStore.user becomes null.
  • Protected/admin routing still redirects correctly based on authStore.isAuthenticated and authStore.isAdmin after session readiness.

Technical Design

Current Implementation

  • frontend/src/stores/app.ts currently imports authStore, gatewayStore, and business stores, defines loadAppData() inside initialize(), registers window.addEventListener('gateway:connected', loadAppData), calls gatewayStore.connect() unconditionally, and loads servers/categories/channels from the event or connected status.
  • frontend/src/stores/auth.ts exposes user, isInitialized, initialize(), and logout(). initialize() calls GET /auth/me; login is currently performed directly in frontend/src/pages/auth/login.vue.
  • frontend/src/stores/gateway.ts builds ws://.../ws/gateway from appStore.baseurl, sets status, dispatches window.dispatchEvent(new CustomEvent('gateway:connected')), and schedules reconnect on every close.
  • frontend/src/main.ts currently calls appStore.initialize() before app.mount('#app'), without awaiting it.
  • frontend/src/router/index.ts currently calls authStore.initialize() inside beforeEach when !authStore.isInitialized.
  • Backend gateway route src/routes/gateway/handlers.rs requires CurrentUser in ws_handler, and CurrentUser rejects unauthenticated requests. However src/routes/mod.rs nests /ws routes outside secure_routes; protection therefore depends on global auth/context middleware in src/http/server.rs, which is currently applied globally.

Key Decisions

  • Use a dedicated sessionStore orchestration layer. It will be the only place that sequences authStore.fetchCurrentUser(), gatewayStore.connect(), business data loading, and cleanup.
  • Use bootstrap-before-mount as the primary startup model. Existing App.vue has only <router-view/> and no loader UI, so main.ts will await sessionStore.bootstrap() before app.mount('#app'). Router guards will still be safe/idempotent by calling or awaiting sessionStore.bootstrap() only when needed.
  • Keep appStore for baseurl only. gatewayStore still needs a base URL source for direct WebSocket URL construction, while useApi.ts currently uses Vite proxy path /api.
  • Make authStore.login() match the existing backend contract. POST /api/auth/login returns LoginResponse { token }, not a user payload, so authStore.login(username, password) will post credentials and then call fetchCurrentUser() to populate this.user from GET /auth/me.
  • Prevent reconnect after intentional disconnect. gatewayStore.disconnect() should close and clear the socket without scheduling an automatic reconnect that would violate logout cleanup.

Proposed Data Flow

graph LR
  Main[main.ts] --> Session[sessionStore.bootstrap]
  Router[router guard] --> Session
  Login[login.vue] --> SessionLogin[sessionStore.login]
  Session --> Auth[authStore identity]
  Session --> Gateway[gatewayStore websocket]
  Session --> Stores[business stores]
  Logout[logout action] --> SessionLogout[sessionStore.logout]
  SessionLogout --> Gateway
  SessionLogout --> Stores
  SessionLogout --> Auth

Proposed Changes

  • Add frontend/src/stores/session.ts:
    • State: isBootstrapping: false, isReady: false.
    • bootstrap():
      • return early if isReady or isBootstrapping;
      • call authStore.fetchCurrentUser();
      • if authStore.isAuthenticated, call startAuthenticatedSession();
      • set isReady = true in finally, and clear isBootstrapping.
    • startAuthenticatedSession():
      • call gatewayStore.connect() idempotently;
      • call loadInitialData().
    • stopAuthenticatedSession():
      • call gatewayStore.disconnect();
      • call reset() on server, category, channel, and message stores.
    • loadInitialData():
      • run serverStore.fetchServers(), categoryStore.fetchCategories(), and channelStore.fetchChannels() in Promise.all.
    • login(username: string, password: string):
      • delegate to authStore.login(...);
      • call startAuthenticatedSession().
    • logout():
      • call stopAuthenticatedSession();
      • call authStore.logout();
      • preserve isReady = true for the current public session state.
  • Refactor frontend/src/stores/auth.ts:
    • Remove isInitialized in favor of sessionStore.isReady.
    • Rename initialize() to fetchCurrentUser().
    • Add login(username: string, password: string) using api.post('/auth/login', { username, password }), throwing usable errors for 401 and non-OK responses, then fetchCurrentUser().
    • Make logout() call POST /auth/logout and always clear this.user = null in finally.
  • Refactor frontend/src/stores/app.ts:
    • Remove imports of auth/gateway/business stores and the initialize() orchestration.
    • Keep baseurl: 'http://localhost:8080' unless a later cleanup moves it to config.
  • Refactor frontend/src/stores/gateway.ts:
    • Keep WebSocket-only responsibilities: connect, disconnect, send, handleMessage, status tracking.
    • Make connect() return early for connecting and connected.
    • Remove window.dispatchEvent('gateway:connected').
    • Add an internal intentional-close guard or equivalent so disconnect() does not trigger scheduleReconnect().
  • Add reset actions:
    • server.ts: servers = [].
    • category.ts: categories = [].
    • channel.ts: channels = [], loading = false, error = null.
    • message.ts: messages = [].
  • Update startup/routing/UI:
    • frontend/src/main.ts: import useSessionStore, call await sessionStore.bootstrap(), then mount.
    • frontend/src/router/index.ts: import useSessionStore; replace authStore.isInitialized/authStore.initialize() with waiting for sessionStore.isReady or await sessionStore.bootstrap(); keep auth/admin decisions in terms of authStore.isAuthenticated and authStore.isAdmin.
    • frontend/src/pages/auth/login.vue: remove direct useApi() and useAuthStore() login flow; call sessionStore.login(username.value, password.value) and redirect to /.
    • Search for any direct logout usages; replace them with sessionStore.logout() and route to /auth/login where present.

Backend Security Check

  • Confirm src/routes/gateway/handlers.rs keeps CurrentUser(user): CurrentUser in the WebSocket handler.
  • Confirm global middleware order in src/http/server.rs continues to inject RequestContext and optional auth before /ws/gateway handlers run.
  • If testing shows unauthenticated WebSocket upgrade succeeds, explicitly layer middleware::require_auth onto ws_routes in src/routes/mod.rs or equivalent.

Testing

Validation Approach

  • Run frontend type checking/build validation using the existing frontend scripts where available (npm run type-check, and build if needed).
  • Run targeted manual browser/network validation for bootstrap, login, logout, and routing flows.
  • Run or compile backend checks only if backend route/middleware code changes are required.

Key Scenarios

  • Unauthenticated visitor: GET /api/auth/me is attempted, no /ws/gateway connection is opened, no private data endpoints are loaded, and protected routes redirect to /auth/login.
  • Authenticated page load: GET /api/auth/me succeeds, one /ws/gateway connection opens, initial servers/categories/channels loads once, and protected routes render.
  • Login: login page calls sessionStore.login(), user becomes authenticated, gateway connects, initial data loads, and the user is redirected to /.
  • Logout: gateway closes without reconnecting, business stores are empty, authStore.user is null, and the user is redirected to a public route.
  • Double bootstrap / navigation: repeated guard invocations or main startup do not create duplicate gateway connections or duplicate initial data loads.

Regression Checks

  • gateway:connected and similar global event usages are absent from frontend/src after refactor.
  • authStore.initialize and authStore.isInitialized references are absent after migration.
  • Router admin checks continue to use authStore.isAdmin after session readiness.

Delivery Steps

✓ Step 1: Introduce session lifecycle store

A new sessionStore owns bootstrap, authenticated startup, initial data loading, and teardown.

  • Add frontend/src/stores/session.ts as a Pinia store named session.
  • Implement isBootstrapping and isReady state with idempotent bootstrap() behavior.
  • Wire bootstrap() to authStore.fetchCurrentUser() and conditionally startAuthenticatedSession().
  • Implement startAuthenticatedSession(), stopAuthenticatedSession(), loadInitialData(), login(...), and logout() using existing auth, gateway, and business stores.
  • Ensure initial private data loading uses Promise.all for servers, categories, and channels.

✓ Step 2: Separate auth, gateway, and app store responsibilities

authStore, gatewayStore, and appStore each have a single responsibility and no longer perform cross-store orchestration.

  • Update frontend/src/stores/auth.ts by replacing initialize()/isInitialized with fetchCurrentUser() and identity-only state/getters.
  • Add authStore.login(username, password) that posts to /auth/login, handles 401/non-OK errors, then fetches the current user because the backend login response returns a token payload.
  • Make authStore.logout() call /auth/logout and clear user in finally.
  • Update frontend/src/stores/gateway.ts so connect() is idempotent for both connecting and connected states.
  • Remove window.dispatchEvent('gateway:connected') and any event-driven loading from the gateway store.
  • Make disconnect() intentionally close and clear the socket without scheduling reconnect.
  • Simplify frontend/src/stores/app.ts to retain only global non-session state such as baseurl.

✓ Step 3: Add private-data reset support

Business stores can be reliably cleared during logout and user switching.

  • Add reset() to frontend/src/stores/server.ts to clear servers.
  • Add reset() to frontend/src/stores/category.ts to clear categories.
  • Add reset() to frontend/src/stores/channel.ts to clear channels, loading, and error.
  • Add reset() to frontend/src/stores/message.ts to clear messages.
  • Use these reset actions from sessionStore.stopAuthenticatedSession().

✓ Step 4: Route app startup and auth UI through sessionStore

Application bootstrap, route guards, login, and logout entry points use sessionStore as the orchestration boundary.

  • Update frontend/src/main.ts to create the app, register plugins, call await useSessionStore().bootstrap(), then mount.
  • Update frontend/src/router/index.ts to wait for or trigger sessionStore.bootstrap() before auth/admin decisions.
  • Keep route authorization checks based on authStore.isAuthenticated and authStore.isAdmin after session readiness.
  • Update frontend/src/pages/auth/login.vue to call sessionStore.login(username.value, password.value) instead of manually posting with useApi() and calling authStore.initialize().
  • Search for direct logout usages and route them through sessionStore.logout() with redirect to /auth/login where UI logout actions exist.

✓ Step 5: Verify security and regressions

The refactor is validated against frontend lifecycle behavior and backend WebSocket authentication guarantees.

  • Confirm no gateway:connected, window.addEventListener, or window.dispatchEvent orchestration remains in frontend/src.
  • Confirm no references to authStore.initialize or authStore.isInitialized remain.
  • Run frontend type checking/build validation using existing scripts.
  • Manually validate unauthenticated load, authenticated load, login, logout, and double-bootstrap scenarios via browser/network behavior.
  • Verify /ws/gateway still rejects unauthenticated users through CurrentUser; if not, add explicit auth protection to the backend WebSocket route.