14 KiB
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.tswithisBootstrapping,isReady, and lifecycle actions. - Refactor
authStoreto manage identity only. - Refactor
appStoreto keep only global non-user app state such asbaseurl. - Refactor
gatewayStoreto manage WebSocket connection state only and remove globalwindoworchestration events. - Add
reset()actions toserver,category,channel, andmessagestores. - 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/gatewayis found not to reject unauthenticated users.
Acceptance Criteria
authStoreno longer knows about gateway, bootstrap, or business stores.gatewayStoreno longer dispatches or relies ongateway:connected/gateway:disconnectedbrowser events.appStore.initialize()no longer orchestrates auth/gateway/data loading, and app startup usessessionStore.bootstrap()instead.- No WebSocket
/ws/gatewayconnection is attempted for unauthenticated users. - On login, user identity is loaded, gateway connects once, and
server,category, andchannelinitial data are loaded once. - On logout, gateway disconnects, private business stores reset, and
authStore.userbecomesnull. - Protected/admin routing still redirects correctly based on
authStore.isAuthenticatedandauthStore.isAdminafter session readiness.
Technical Design
Current Implementation
frontend/src/stores/app.tscurrently importsauthStore,gatewayStore, and business stores, definesloadAppData()insideinitialize(), registerswindow.addEventListener('gateway:connected', loadAppData), callsgatewayStore.connect()unconditionally, and loadsservers/categories/channelsfrom the event or connected status.frontend/src/stores/auth.tsexposesuser,isInitialized,initialize(), andlogout().initialize()callsGET /auth/me; login is currently performed directly infrontend/src/pages/auth/login.vue.frontend/src/stores/gateway.tsbuildsws://.../ws/gatewayfromappStore.baseurl, setsstatus, dispatcheswindow.dispatchEvent(new CustomEvent('gateway:connected')), and schedules reconnect on every close.frontend/src/main.tscurrently callsappStore.initialize()beforeapp.mount('#app'), without awaiting it.frontend/src/router/index.tscurrently callsauthStore.initialize()insidebeforeEachwhen!authStore.isInitialized.- Backend gateway route
src/routes/gateway/handlers.rsrequiresCurrentUserinws_handler, andCurrentUserrejects unauthenticated requests. Howeversrc/routes/mod.rsnests/wsroutes outsidesecure_routes; protection therefore depends on global auth/context middleware insrc/http/server.rs, which is currently applied globally.
Key Decisions
- Use a dedicated
sessionStoreorchestration layer. It will be the only place that sequencesauthStore.fetchCurrentUser(),gatewayStore.connect(), business data loading, and cleanup. - Use bootstrap-before-mount as the primary startup model. Existing
App.vuehas only<router-view/>and no loader UI, somain.tswillawait sessionStore.bootstrap()beforeapp.mount('#app'). Router guards will still be safe/idempotent by calling or awaitingsessionStore.bootstrap()only when needed. - Keep
appStoreforbaseurlonly.gatewayStorestill needs a base URL source for direct WebSocket URL construction, whileuseApi.tscurrently uses Vite proxy path/api. - Make
authStore.login()match the existing backend contract.POST /api/auth/loginreturnsLoginResponse { token }, not a user payload, soauthStore.login(username, password)will post credentials and then callfetchCurrentUser()to populatethis.userfromGET /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
isReadyorisBootstrapping; - call
authStore.fetchCurrentUser(); - if
authStore.isAuthenticated, callstartAuthenticatedSession(); - set
isReady = trueinfinally, and clearisBootstrapping.
- return early if
startAuthenticatedSession():- call
gatewayStore.connect()idempotently; - call
loadInitialData().
- call
stopAuthenticatedSession():- call
gatewayStore.disconnect(); - call
reset()onserver,category,channel, andmessagestores.
- call
loadInitialData():- run
serverStore.fetchServers(),categoryStore.fetchCategories(), andchannelStore.fetchChannels()inPromise.all.
- run
login(username: string, password: string):- delegate to
authStore.login(...); - call
startAuthenticatedSession().
- delegate to
logout():- call
stopAuthenticatedSession(); - call
authStore.logout(); - preserve
isReady = truefor the current public session state.
- call
- State:
- Refactor
frontend/src/stores/auth.ts:- Remove
isInitializedin favor ofsessionStore.isReady. - Rename
initialize()tofetchCurrentUser(). - Add
login(username: string, password: string)usingapi.post('/auth/login', { username, password }), throwing usable errors for401and non-OK responses, thenfetchCurrentUser(). - Make
logout()callPOST /auth/logoutand always clearthis.user = nullinfinally.
- Remove
- 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.
- Remove imports of auth/gateway/business stores and the
- Refactor
frontend/src/stores/gateway.ts:- Keep WebSocket-only responsibilities:
connect,disconnect,send,handleMessage, status tracking. - Make
connect()return early forconnectingandconnected. - Remove
window.dispatchEvent('gateway:connected'). - Add an internal intentional-close guard or equivalent so
disconnect()does not triggerscheduleReconnect().
- Keep WebSocket-only responsibilities:
- 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: importuseSessionStore, callawait sessionStore.bootstrap(), then mount.frontend/src/router/index.ts: importuseSessionStore; replaceauthStore.isInitialized/authStore.initialize()with waiting forsessionStore.isReadyorawait sessionStore.bootstrap(); keep auth/admin decisions in terms ofauthStore.isAuthenticatedandauthStore.isAdmin.frontend/src/pages/auth/login.vue: remove directuseApi()anduseAuthStore()login flow; callsessionStore.login(username.value, password.value)and redirect to/.- Search for any direct logout usages; replace them with
sessionStore.logout()and route to/auth/loginwhere present.
Backend Security Check
- Confirm
src/routes/gateway/handlers.rskeepsCurrentUser(user): CurrentUserin the WebSocket handler. - Confirm global middleware order in
src/http/server.rscontinues to injectRequestContextand optional auth before/ws/gatewayhandlers run. - If testing shows unauthenticated WebSocket upgrade succeeds, explicitly layer
middleware::require_authontows_routesinsrc/routes/mod.rsor 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/meis attempted, no/ws/gatewayconnection is opened, no private data endpoints are loaded, and protected routes redirect to/auth/login. - Authenticated page load:
GET /api/auth/mesucceeds, one/ws/gatewayconnection opens, initialservers/categories/channelsloads 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.userisnull, 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:connectedand similar global event usages are absent fromfrontend/srcafter refactor.authStore.initializeandauthStore.isInitializedreferences are absent after migration.- Router admin checks continue to use
authStore.isAdminafter 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.tsas a Pinia store namedsession. - Implement
isBootstrappingandisReadystate with idempotentbootstrap()behavior. - Wire
bootstrap()toauthStore.fetchCurrentUser()and conditionallystartAuthenticatedSession(). - Implement
startAuthenticatedSession(),stopAuthenticatedSession(),loadInitialData(),login(...), andlogout()using existing auth, gateway, and business stores. - Ensure initial private data loading uses
Promise.allfor 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.tsby replacinginitialize()/isInitializedwithfetchCurrentUser()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/logoutand clearuserinfinally. - Update
frontend/src/stores/gateway.tssoconnect()is idempotent for bothconnectingandconnectedstates. - 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.tsto retain only global non-session state such asbaseurl.
✓ Step 3: Add private-data reset support
Business stores can be reliably cleared during logout and user switching.
- Add
reset()tofrontend/src/stores/server.tsto clearservers. - Add
reset()tofrontend/src/stores/category.tsto clearcategories. - Add
reset()tofrontend/src/stores/channel.tsto clearchannels,loading, anderror. - Add
reset()tofrontend/src/stores/message.tsto clearmessages. - 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.tsto create the app, register plugins, callawait useSessionStore().bootstrap(), then mount. - Update
frontend/src/router/index.tsto wait for or triggersessionStore.bootstrap()before auth/admin decisions. - Keep route authorization checks based on
authStore.isAuthenticatedandauthStore.isAdminafter session readiness. - Update
frontend/src/pages/auth/login.vueto callsessionStore.login(username.value, password.value)instead of manually posting withuseApi()and callingauthStore.initialize(). - Search for direct logout usages and route them through
sessionStore.logout()with redirect to/auth/loginwhere 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, orwindow.dispatchEventorchestration remains infrontend/src. - Confirm no references to
authStore.initializeorauthStore.isInitializedremain. - 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/gatewaystill rejects unauthenticated users throughCurrentUser; if not, add explicit auth protection to the backend WebSocket route.