Init
This commit is contained in:
@@ -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 `<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
|
||||||
|
```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.
|
||||||
+13
-8
@@ -1,13 +1,18 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link href="/favicon.ico" rel="icon">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
<title>Welcome to Vuetify 4</title>
|
<title>Welcome to Vuetify 4</title>
|
||||||
</head>
|
<style>
|
||||||
<body>
|
body {
|
||||||
<div id="app"></div>
|
background-color: black;
|
||||||
<script type="module" src="/src/main.ts"></script>
|
}
|
||||||
</body>
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="/src/main.ts" type="module"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ export function useApi() {
|
|||||||
// headers.set('Authorization', `Bearer ${authStore.token}`)
|
// headers.set('Authorization', `Bearer ${authStore.token}`)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const config = {
|
const config: RequestInit = {
|
||||||
...options,
|
...options,
|
||||||
headers,
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
body: options.body && typeof options.body === 'object'
|
body: options.body && typeof options.body === 'object'
|
||||||
? JSON.stringify(options.body)
|
? JSON.stringify(options.body)
|
||||||
: options.body
|
: options.body
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import App from './App.vue'
|
|||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
import 'unfonts.css'
|
import 'unfonts.css'
|
||||||
import {useAppStore} from "@/stores/app.ts";
|
import {useSessionStore} from "@/stores/session.ts";
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
registerPlugins(app)
|
registerPlugins(app)
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const sessionStore = useSessionStore()
|
||||||
appStore.initialize()
|
await sessionStore.bootstrap()
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {ref} from 'vue'
|
import {ref} from 'vue'
|
||||||
import {useAuthStore} from '@/stores/auth'
|
|
||||||
import {useRouter} from 'vue-router'
|
import {useRouter} from 'vue-router'
|
||||||
import {useApi} from "@/composables/useApi.ts";
|
import {useSessionStore} from "@/stores/session.ts";
|
||||||
|
|
||||||
|
|
||||||
const api = useApi()
|
const sessionStore = useSessionStore()
|
||||||
const authStore = useAuthStore()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const username = ref('')
|
const username = ref('')
|
||||||
@@ -23,26 +21,10 @@ async function handleLogin() {
|
|||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.post('/auth/login', {
|
await sessionStore.login(username.value, password.value)
|
||||||
username: username.value,
|
|
||||||
password: password.value
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.status === 401) {
|
|
||||||
error.value = "Identifiants incorrects"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
error.value = "Une erreur est survenue lors de la connexion"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
await authStore.setToken(data.token)
|
|
||||||
await router.push('/')
|
await router.push('/')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = 'Impossible de contacter le serveur'
|
error.value = err instanceof Error ? err.message : 'Impossible de contacter le serveur'
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import {createRouter, createWebHistory} from 'vue-router'
|
import {createRouter, createWebHistory} from 'vue-router'
|
||||||
|
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
import {useSessionStore} from '@/stores/session'
|
||||||
import AuthLayout from "@/layouts/AuthLayout.vue";
|
import AuthLayout from "@/layouts/AuthLayout.vue";
|
||||||
import AppLayout from "@/layouts/AppLayout.vue";
|
import AppLayout from "@/layouts/AppLayout.vue";
|
||||||
import AdminLayout from "@/layouts/AdminLayout.vue";
|
import AdminLayout from "@/layouts/AdminLayout.vue";
|
||||||
@@ -80,9 +81,10 @@ const router = createRouter({
|
|||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
const sessionStore = useSessionStore()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
if (!authStore.isInitialized) {
|
if (!sessionStore.isReady) {
|
||||||
await authStore.initialize()
|
await sessionStore.bootstrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicPages = ['login', 'join']
|
const publicPages = ['login', 'join']
|
||||||
|
|||||||
@@ -1,49 +1,7 @@
|
|||||||
// Utilities
|
|
||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
import {useAuthStore} from "@/stores/auth.ts";
|
|
||||||
import {useGatewayStore} from "@/stores/gateway.ts";
|
|
||||||
import {useServerStore} from "@/stores/server.ts";
|
|
||||||
import {useCategoryStore} from "@/stores/category.ts";
|
|
||||||
import {useChannelStore} from "@/stores/channel.ts";
|
|
||||||
|
|
||||||
export const useAppStore = defineStore('app', {
|
export const useAppStore = defineStore('app', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
baseurl: 'http://localhost:8080',
|
baseurl: 'http://localhost:8080',
|
||||||
}),
|
}),
|
||||||
actions: {
|
|
||||||
async initialize() {
|
|
||||||
const authStore = useAuthStore()
|
|
||||||
const gatewayStore = useGatewayStore()
|
|
||||||
const serverStore = useServerStore()
|
|
||||||
const categoryStore = useCategoryStore()
|
|
||||||
const channelStore = useChannelStore()
|
|
||||||
|
|
||||||
// Définition du trigger de chargement des données de l'application
|
|
||||||
const loadAppData = async () => {
|
|
||||||
try {
|
|
||||||
await Promise.all([
|
|
||||||
serverStore.fetchServers(),
|
|
||||||
categoryStore.fetchCategories(),
|
|
||||||
channelStore.fetchChannels()
|
|
||||||
])
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Erreur lors du chargement des données initiales :", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// On s'abonne à l'événement de connexion de la gateway
|
|
||||||
window.addEventListener('gateway:connected', loadAppData)
|
|
||||||
|
|
||||||
// On connecte la gateway (qui déclenchera 'gateway:connected')
|
|
||||||
await gatewayStore.connect()
|
|
||||||
|
|
||||||
// Initialisation de l'authentification (qui lancera la connexion à la gateway)
|
|
||||||
// await authStore.initialize()
|
|
||||||
|
|
||||||
// Sécurité : si la gateway s'est déjà connectée entre-temps
|
|
||||||
if (gatewayStore.status === 'connected') {
|
|
||||||
await loadAppData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
+27
-72
@@ -12,11 +12,7 @@ export interface User {
|
|||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
// Plus de token stocké dans le localStorage !
|
|
||||||
user: null as User | null,
|
user: null as User | null,
|
||||||
isInitialized: false,
|
|
||||||
// Nous ne gardons ce token en mémoire que pour la connexion au WebSocket
|
|
||||||
// gatewayToken: null as string | null,
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
@@ -26,86 +22,45 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
async initialize() {
|
async fetchCurrentUser() {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/auth/me')
|
const response = await api.get('/auth/me')
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
this.user = data.user
|
this.user = data.user
|
||||||
|
} else {
|
||||||
|
this.user = null
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Auth initialization failed", e)
|
console.error("Auth initialization failed", e)
|
||||||
// this.logout()
|
this.user = null
|
||||||
} finally {
|
|
||||||
this.isInitialized = true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
logout() {
|
async login(username: string, password: string) {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
this.user = null
|
const response = await api.post('/auth/login', {username, password})
|
||||||
api.post('/auth/logout')
|
|
||||||
// Note : si vous implémentez une route /auth/logout côté Axum,
|
if (!response.ok) {
|
||||||
// elle devra nettoyer le cookie en renvoyant un Set-Cookie expiré.
|
if (response.status === 401) {
|
||||||
}
|
throw new Error('Identifiants invalides')
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = await response.json().catch(() => null)
|
||||||
|
throw new Error(error?.message || 'Erreur de connexion')
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.fetchCurrentUser()
|
||||||
|
},
|
||||||
|
|
||||||
|
async logout() {
|
||||||
|
const api = useApi()
|
||||||
|
try {
|
||||||
|
await api.post('/auth/logout')
|
||||||
|
} finally {
|
||||||
|
this.user = null
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
////// Version with token + local storage
|
|
||||||
// export const useAuthStore = defineStore('auth', {
|
|
||||||
// state: () => ({
|
|
||||||
// token: localStorage.getItem('token') || null as string | null,
|
|
||||||
// user: null as User | null,
|
|
||||||
// isInitialized: false,
|
|
||||||
// }),
|
|
||||||
//
|
|
||||||
// getters: {
|
|
||||||
// isAuthenticated: (state) => !!state.token && !!state.user,
|
|
||||||
// isAdmin: (state) => state.user?.is_superuser || false,
|
|
||||||
// currentUser: (state) => state.user,
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// actions: {
|
|
||||||
// async initialize() {
|
|
||||||
// if (!this.token) {
|
|
||||||
// this.isInitialized = true
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const api = useApi()
|
|
||||||
// try {
|
|
||||||
// const response = await api.get('/auth/me')
|
|
||||||
// if (response.ok) {
|
|
||||||
// const data = await response.json()
|
|
||||||
// this.user = data.user
|
|
||||||
//
|
|
||||||
// // Initialisation du gateway
|
|
||||||
// const gatewayStore = useGatewayStore()
|
|
||||||
// await gatewayStore.connect()
|
|
||||||
// } else {
|
|
||||||
// this.logout()
|
|
||||||
// }
|
|
||||||
// } catch (e) {
|
|
||||||
// console.error("Auth initialization failed", e)
|
|
||||||
// this.logout()
|
|
||||||
// } finally {
|
|
||||||
// this.isInitialized = true
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// setToken(token: string) {
|
|
||||||
// this.token = token
|
|
||||||
// localStorage.setItem('token', token)
|
|
||||||
// // On déclenche la récupération des infos utilisateur immédiatement
|
|
||||||
// return this.initialize()
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// logout() {
|
|
||||||
// this.token = null
|
|
||||||
// this.user = null
|
|
||||||
// localStorage.removeItem('token')
|
|
||||||
// // On ne redirige pas ici pour laisser le router ou le composant décider
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
@@ -10,6 +10,9 @@ export const useCategoryStore = defineStore("category", {
|
|||||||
let api = useApi();
|
let api = useApi();
|
||||||
let response = await api.get("/categories");
|
let response = await api.get("/categories");
|
||||||
this.categories = await response.json();
|
this.categories = await response.json();
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.categories = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -55,6 +55,11 @@ export const useChannelStore = defineStore('channel', {
|
|||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.channels = [];
|
||||||
|
this.loading = false;
|
||||||
|
this.error = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -8,29 +8,27 @@ export const useGatewayStore = defineStore('gateway', {
|
|||||||
socket: null as WebSocket | null,
|
socket: null as WebSocket | null,
|
||||||
status: 'disconnected' as GatewayStatus,
|
status: 'disconnected' as GatewayStatus,
|
||||||
reconnectAttempts: 0,
|
reconnectAttempts: 0,
|
||||||
|
shouldReconnect: false,
|
||||||
|
reconnectTimer: null as number | null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
async connect() {
|
async connect() {
|
||||||
if (this.socket && this.status === 'connected') {
|
if (this.status === 'connecting' || this.status === 'connected') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
// const authStore = useAuthStore()
|
|
||||||
|
|
||||||
this.status = 'connecting'
|
this.status = 'connecting'
|
||||||
// version token
|
this.shouldReconnect = true
|
||||||
// const token = authStore.token
|
if (this.reconnectTimer) {
|
||||||
// if (!token) {
|
window.clearTimeout(this.reconnectTimer)
|
||||||
// this.status = 'error'
|
this.reconnectTimer = null
|
||||||
// return
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
const apiUri = appStore.baseurl ? new URL(appStore.baseurl) : new URL(window.location.href)
|
const apiUri = appStore.baseurl ? new URL(appStore.baseurl) : new URL(window.location.href)
|
||||||
const wsProtocol = apiUri.protocol === 'https:' ? 'wss:' : 'ws:'
|
const wsProtocol = apiUri.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
// version token
|
|
||||||
// const wsUrl = `${wsProtocol}//${apiUri.host}/ws/gateway?token=${encodeURIComponent(token)}`
|
|
||||||
const wsUrl = `${wsProtocol}//${apiUri.host}/ws/gateway`
|
const wsUrl = `${wsProtocol}//${apiUri.host}/ws/gateway`
|
||||||
const socket = new WebSocket(wsUrl)
|
const socket = new WebSocket(wsUrl)
|
||||||
|
|
||||||
@@ -38,14 +36,16 @@ export const useGatewayStore = defineStore('gateway', {
|
|||||||
this.status = 'connected'
|
this.status = 'connected'
|
||||||
this.reconnectAttempts = 0
|
this.reconnectAttempts = 0
|
||||||
|
|
||||||
// Émission d'un événement global indiquant que la connexion est établie et prête
|
|
||||||
window.dispatchEvent(new CustomEvent('gateway:connected'))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.onclose = () => {
|
socket.onclose = () => {
|
||||||
this.status = 'disconnected'
|
this.status = 'disconnected'
|
||||||
this.socket = null
|
if (this.socket === socket) {
|
||||||
this.scheduleReconnect()
|
this.socket = null
|
||||||
|
}
|
||||||
|
if (this.shouldReconnect) {
|
||||||
|
this.scheduleReconnect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.onerror = () => {
|
socket.onerror = () => {
|
||||||
@@ -60,9 +60,15 @@ export const useGatewayStore = defineStore('gateway', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async disconnect() {
|
async disconnect() {
|
||||||
|
this.shouldReconnect = false
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
window.clearTimeout(this.reconnectTimer)
|
||||||
|
this.reconnectTimer = null
|
||||||
|
}
|
||||||
this.socket?.close()
|
this.socket?.close()
|
||||||
this.socket = null
|
this.socket = null
|
||||||
this.status = 'disconnected'
|
this.status = 'disconnected'
|
||||||
|
this.reconnectAttempts = 0
|
||||||
},
|
},
|
||||||
|
|
||||||
async send(payload: object) {
|
async send(payload: object) {
|
||||||
@@ -81,7 +87,8 @@ export const useGatewayStore = defineStore('gateway', {
|
|||||||
const delay = Math.min(1000 * 2 ** this.reconnectAttempts, 30000)
|
const delay = Math.min(1000 * 2 ** this.reconnectAttempts, 30000)
|
||||||
this.reconnectAttempts += 1
|
this.reconnectAttempts += 1
|
||||||
|
|
||||||
window.setTimeout(() => {
|
this.reconnectTimer = window.setTimeout(() => {
|
||||||
|
this.reconnectTimer = null
|
||||||
this.connect()
|
this.connect()
|
||||||
}, delay)
|
}, delay)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export const useMessageStore = defineStore("message", {
|
|||||||
let api = useApi();
|
let api = useApi();
|
||||||
let response = await api.get("/api/messages");
|
let response = await api.get("/api/messages");
|
||||||
this.messages = await response.json();
|
this.messages = await response.json();
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.messages = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import {defineStore} from "pinia";
|
import {defineStore} from "pinia";
|
||||||
import {useApi} from "@/composables/useApi.ts";
|
import {useApi} from "@/composables/useApi.ts";
|
||||||
|
|
||||||
|
interface Server {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
export const useServerStore = defineStore("server", {
|
export const useServerStore = defineStore("server", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
servers: []
|
servers: [] as Server[]
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async fetchServers() {
|
async fetchServers() {
|
||||||
let api = useApi();
|
let api = useApi();
|
||||||
const response = await api.get("/servers");
|
const response = await api.get("/servers");
|
||||||
this.servers = await response.json();
|
this.servers = await response.json();
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.servers = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import {defineStore} from 'pinia'
|
||||||
|
import {useAuthStore} from '@/stores/auth.ts'
|
||||||
|
import {useGatewayStore} from '@/stores/gateway.ts'
|
||||||
|
import {useServerStore} from '@/stores/server.ts'
|
||||||
|
import {useCategoryStore} from '@/stores/category.ts'
|
||||||
|
import {useChannelStore} from '@/stores/channel.ts'
|
||||||
|
import {useMessageStore} from '@/stores/message.ts'
|
||||||
|
|
||||||
|
let bootstrapPromise: Promise<void> | null = null
|
||||||
|
|
||||||
|
export const useSessionStore = defineStore('session', {
|
||||||
|
state: () => ({
|
||||||
|
isBootstrapping: false,
|
||||||
|
isReady: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async bootstrap() {
|
||||||
|
if (this.isReady) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bootstrapPromise) {
|
||||||
|
return bootstrapPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
bootstrapPromise = (async () => {
|
||||||
|
this.isBootstrapping = true
|
||||||
|
try {
|
||||||
|
await authStore.fetchCurrentUser()
|
||||||
|
|
||||||
|
if (authStore.isAuthenticated) {
|
||||||
|
await this.startAuthenticatedSession()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isReady = true
|
||||||
|
this.isBootstrapping = false
|
||||||
|
bootstrapPromise = null
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return bootstrapPromise
|
||||||
|
},
|
||||||
|
|
||||||
|
async startAuthenticatedSession() {
|
||||||
|
const gatewayStore = useGatewayStore()
|
||||||
|
|
||||||
|
await gatewayStore.connect()
|
||||||
|
await this.loadInitialData()
|
||||||
|
},
|
||||||
|
|
||||||
|
async stopAuthenticatedSession() {
|
||||||
|
const gatewayStore = useGatewayStore()
|
||||||
|
const serverStore = useServerStore()
|
||||||
|
const categoryStore = useCategoryStore()
|
||||||
|
const channelStore = useChannelStore()
|
||||||
|
const messageStore = useMessageStore()
|
||||||
|
|
||||||
|
await gatewayStore.disconnect()
|
||||||
|
serverStore.reset()
|
||||||
|
categoryStore.reset()
|
||||||
|
channelStore.reset()
|
||||||
|
messageStore.reset()
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadInitialData() {
|
||||||
|
const serverStore = useServerStore()
|
||||||
|
const categoryStore = useCategoryStore()
|
||||||
|
const channelStore = useChannelStore()
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
serverStore.fetchServers(),
|
||||||
|
categoryStore.fetchCategories(),
|
||||||
|
channelStore.fetchChannels(),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
|
||||||
|
async login(username: string, password: string) {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
await authStore.login(username, password)
|
||||||
|
await this.startAuthenticatedSession()
|
||||||
|
this.isReady = true
|
||||||
|
},
|
||||||
|
|
||||||
|
async logout() {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
await this.stopAuthenticatedSession()
|
||||||
|
await authStore.logout()
|
||||||
|
this.isReady = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user