Skip to content

État applicatif (Pinia)

L'état partagé du frontend vit dans des stores Pinia (src/stores/). Pinia est enregistré dans src/main.ts (createApp(App).use(createPinia())).

src/stores/auth.ts — session

Source de vérité de la session authentifiée : l'utilisateur connecté et son jeton d'accès. Store en syntaxe setup (defineStore("auth", () => …)).

État & persistance

L'état est persisté dans localStorage pour survivre à un rechargement de page, via useStorage de @vueuse/core (pas de dépendance supplémentaire — le paquet est déjà présent) :

  • token — clé rdt_auth_token (sérialiseur string) ;
  • user — clé rdt_auth_user (sérialiseur object, JSON). C'est la UserResource de GET /user : id, name, email, status, is_traveller, is_super_admin et organizations (id, name, typereseller|region, membership_status, role). Elle décrit les « casquettes » de l'utilisateur — un indice d'UI, pas une frontière de sécurité ;
  • activeHatKey — clé rdt_active_hat (sérialiseur string) : la casquette choisie, persistée pour survivre au rechargement (voir Casquettes ci-dessous).

Les sérialiseurs sont explicites : avec une valeur par défaut null, l'inférence de useStorage serait ambiguë. useStorage lit la valeur au montage du store, la réécrit à chaque changement, et supprime la clé quand la valeur repasse à null.

Getters dérivés : isAuthenticated (vrai dès qu'un jeton est présent) et isSuperAdmin (vrai si user.is_super_admin) — ce dernier sert à la redirection d'accueil par rôle (voir Routage & gardes).

Casquettes (rôles) — src/stores/hats.ts

La logique pure des casquettes vit dans src/stores/hats.ts (à l'image de router/guards.ts) ; le store l'expose en getters/action :

  • Hat — union discriminée : super-admin, traveller, ou organization (porte orgId / orgName / orgType / role brut de l'API) ;
  • deriveHats(user) — casquettes disponibles, dans l'ordre de priorité par défaut : super-admin, puis organisations (appartenances active seulement), puis voyageur. hats[0] est donc le défaut ;
  • resolveActiveHat(hats, key) — la casquette de clé key, sinon le défaut, sinon null (une clé périmée retombe proprement sur le défaut) ;
  • hatRoleLabelKey(hat) — clé i18n du libellé de rôle (roles.*), utilisée par le sélecteur de casquette (components/auth/RoleSwitcher.vue).

Côté store : getter availableHats (= deriveHats(user)), getter currentHat (= resolveActiveHat(availableHats, activeHatKey)), action setActiveHat(key) (n'accepte qu'une clé d'une casquette disponible, sinon ignore).

Cycle de vie de activeHatKey : remise à null (→ défaut) à la connexion (nouvelle session), effacée par clearSession(), conservée par refreshUser() (même session → on garde le choix de l'utilisateur au rechargement).

Ces casquettes alimenteront les gardes par rôle (étape 3) et le sélecteur de rôle de la barre latérale (étape 4) ; aucune UI ne les consomme encore.

Actions

  • login(credentials) — délègue à src/api/auth.login (qui joint déjà le jeton au client HTTP), récupère ensuite l'utilisateur via getCurrentUser (GET /user), puis mémorise jeton + utilisateur (donc les persiste) ;
  • refreshUser() — rafraîchit l'utilisateur depuis GET /user. Un 401 (jeton expiré/invalide) vide la session ; une erreur non-401 (réseau) est tolérée et conserve l'utilisateur persisté (on ne déconnecte pas un utilisateur simplement hors-ligne) ;
  • logout() — délègue à src/api/auth.logout (révocation côté serveur + effacement du jeton HTTP) puis vide l'état local, même si l'appel échoue (jamais de session fantôme) ;
  • setActiveHat(key) — choisit la casquette active (voir Casquettes ci-dessus) ;
  • syncTokenToClient() — réinjecte le jeton persisté dans le client HTTP ;
  • clearSession() (privée) — vide jeton + utilisateur + casquette (local et client HTTP) ; partagée par logout() et le 401 de refreshUser().

Démarrage de l'application

src/main.ts démarre de façon asynchrone et bloque le montage le temps de préparer la session, pour que le premier rendu et les gardes de route partent d'un état à jour :

  1. syncTokenToClient() — la couche API a perdu son jeton en mémoire après un rechargement, le store l'a restauré depuis localStorage ; on le réinjecte ;
  2. si isAuthenticated, await refreshUser() — rafraîchit l'utilisateur (et donc ses accès) depuis GET /user, ou vide la session sur 401 ;
  3. await router.isReady() puis app.mount("#app").

Répartition des responsabilités

  • Store = état de session persistant (source de vérité, localStorage).
  • Couche API = transport HTTP (jeton en mémoire joint aux requêtes).

src/views/Login.vue passe par le store (auth.login(...)), jamais par la couche API directement, afin que la session soit mémorisée et persistée.

Tests

src/stores/auth.spec.ts mocke les couches @/api/auth et @/api/client, repart d'un localStorage vierge et d'un Pinia neuf à chaque cas (setActivePinia), et couvre connexion, déconnexion (succès et échec), refreshUser (succès, 401 qui vide la session, erreur réseau tolérée), les casquettes (dérivation, sélection et persistance, cycle de vie), réhydratation depuis localStorage et syncTokenToClient. src/stores/hats.spec.ts couvre la logique pure (deriveHats / hatKey / resolveActiveHat).

Contributors

No contributors

Changelog

No recent changes