From ccfea3d3cf59fdb905b4c83efb8519de60737671 Mon Sep 17 00:00:00 2001 From: Nell Date: Thu, 2 Jul 2026 00:20:42 +0200 Subject: [PATCH] Init --- .../plans/session-store-frontend-refactor.md | 183 ++++++++++++++++++ frontend/index.html | 21 +- frontend/src/composables/useApi.ts | 3 +- frontend/src/main.ts | 6 +- frontend/src/pages/auth/login.vue | 26 +-- frontend/src/router/index.ts | 6 +- frontend/src/stores/app.ts | 42 ---- frontend/src/stores/auth.ts | 101 +++------- frontend/src/stores/category.ts | 3 + frontend/src/stores/channel.ts | 5 + frontend/src/stores/gateway.ts | 37 ++-- frontend/src/stores/message.ts | 3 + frontend/src/stores/server.ts | 9 +- frontend/src/stores/session.ts | 96 +++++++++ 14 files changed, 374 insertions(+), 167 deletions(-) create mode 100644 .junie/plans/session-store-frontend-refactor.md create mode 100644 frontend/src/stores/session.ts diff --git a/.junie/plans/session-store-frontend-refactor.md b/.junie/plans/session-store-frontend-refactor.md new file mode 100644 index 0000000..31f66da --- /dev/null +++ b/.junie/plans/session-store-frontend-refactor.md @@ -0,0 +1,183 @@ +--- +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 `` 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 +```mermaid +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. \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index aaf3eec..c36ed6e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,13 +1,18 @@ - + - - + + Welcome to Vuetify 4 - - -
- - + + + +
+ + diff --git a/frontend/src/composables/useApi.ts b/frontend/src/composables/useApi.ts index 8eaebc9..a40fcb5 100644 --- a/frontend/src/composables/useApi.ts +++ b/frontend/src/composables/useApi.ts @@ -18,9 +18,10 @@ export function useApi() { // headers.set('Authorization', `Bearer ${authStore.token}`) // } - const config = { + const config: RequestInit = { ...options, headers, + credentials: 'include', body: options.body && typeof options.body === 'object' ? JSON.stringify(options.body) : options.body diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 58f7606..8298f39 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -15,13 +15,13 @@ import App from './App.vue' // Styles import 'unfonts.css' -import {useAppStore} from "@/stores/app.ts"; +import {useSessionStore} from "@/stores/session.ts"; const app = createApp(App) registerPlugins(app) -const appStore = useAppStore() -appStore.initialize() +const sessionStore = useSessionStore() +await sessionStore.bootstrap() app.mount('#app') diff --git a/frontend/src/pages/auth/login.vue b/frontend/src/pages/auth/login.vue index cb997ab..8f38c5d 100644 --- a/frontend/src/pages/auth/login.vue +++ b/frontend/src/pages/auth/login.vue @@ -1,12 +1,10 @@