É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érialiseurstring) ;user— clérdt_auth_user(sérialiseurobject, JSON). C'est laUserResourcedeGET /user:id,name,email,status,is_traveller,is_super_adminetorganizations(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érialiseurstring) : 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, ouorganization(porteorgId/orgName/orgType/rolebrut de l'API) ;deriveHats(user)— casquettes disponibles, dans l'ordre de priorité par défaut : super-admin, puis organisations (appartenancesactiveseulement), puis voyageur.hats[0]est donc le défaut ;resolveActiveHat(hats, key)— la casquette de clékey, sinon le défaut, sinonnull(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 viagetCurrentUser(GET /user), puis mémorise jeton + utilisateur (donc les persiste) ;refreshUser()— rafraîchit l'utilisateur depuisGET /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 parlogout()et le 401 derefreshUser().
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 :
syncTokenToClient()— la couche API a perdu son jeton en mémoire après un rechargement, le store l'a restauré depuislocalStorage; on le réinjecte ;- si
isAuthenticated,await refreshUser()— rafraîchit l'utilisateur (et donc ses accès) depuisGET /user, ou vide la session sur 401 ; await router.isReady()puisapp.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).

