feat(auth): jeton Bearer en repli du cookie de session (portage mobile #39, phase 1) #54

Merged
kyuno merged 2 commits from feat/mobile-bearer-auth into main 2026-09-06 03:23:57 +02:00
Owner

Première phase du portage mobile #39. Purement additive côté navigateur : le cookie session httpOnly reste le mécanisme principal et rien ne change pour le web existant. Mergeable seule, avant même d'introduire Capacitor.

Pourquoi

Front et API sont servis depuis la même origine en prod, d'où le cookie SameSite=Lax qui suffit aujourd'hui. Une WebView Capacitor fait tourner le bundle sous capacitor://localhost / https://localhost — un site différent (SameSite est basé sur le domaine) de l'API, quel que soit l'hébergement : le cookie ne part jamais. Sans repli, l'auth casserait silencieusement dans l'appli native.

Contenu

Backend

  • packages/shared : nouveau type AuthResponse = SafeUserProfile + token. Documenté : le JWT est aussi renvoyé dans le corps, identique à la valeur du cookie, pour les clients dont l'origine ne peut pas recevoir ce cookie.
  • auth.routes.ts : POST /auth/login et POST /auth/signup renvoient { ...profile, token } (le res.cookie(...) est inchangé). Additif : tous les champs de profil existants restent à leur place.
  • require-auth.ts : nouvel helper readBearerToken() ; lecture du cookie en priorité, repli sur Authorization: Bearer <jwt> quand le cookie est absent. Le chemin cookie est inchangé au octet près — quand un cookie est présent, le ternaire le renvoie sans jamais toucher l'en-tête.

Frontend (apps/web)

  • api/token-store.ts (nouveau) : interface TokenStore (get/set/clear) + noopTokenStore, l'implémentation navigateur — un vrai no-op, le cookie fait tout.
  • api/client.ts : activeTokenStore (module-level, défaut = no-op) + export setTokenStore(), le point d'injection qu'utilisera la phase 2 (@capacitor/preferences quand Capacitor.isNativePlatform()). _request ajoute l'en-tête Authorization uniquement si un jeton existe ; login/signup le stockent, logout/deleteAccount l'effacent.
  • AuthContext.tsx non modifié — délibéré. Le cycle de vie du jeton est entièrement encapsulé dans le client (mieux que le découpage suggéré par le ticket : aucun risque qu'un futur appelant de apiClient.login oublie de stocker le jeton, et le jeton ne transite jamais par l'état React).

Hors de cette PR (phases suivantes de #39)

  • L'implémentation @capacitor/preferences de TokenStore + son branchement via setTokenStore() — arrive avec les dépendances Capacitor en phase 2.
  • La coquille Android, le switch de routeur, la passe responsive (phases 2 et 3).

Tests

  • Mocha (apps/api/test/auth.test.ts, +5 cas) : login/signup exposent le jeton dans le corps (et il est bien égal à la valeur du cookie) ; requireAuth authentifie une route protégée via Authorization: Bearer sans cookie ; rejette un jeton Bearer malformé (401 NOT_AUTHENTICATED) ; ignore un schéma non-Bearer.
  • pnpm build + pnpm lint verts localement. Pas de base Postgres locale sur ce poste → les tests Mocha DB s'exécutent en CI.
  • Front : non-régression du flux auth existant, déjà couverte par auth.feature ; le mécanisme reste 100 % no-op hors natif (un test Cypress dédié n'apporterait rien, cf. ticket).

Avancement de #39 — l'issue reste ouverte (phases 2 et 3 à suivre).

🤖 Generated with Claude Code

Première phase du [portage mobile #39](https://git.dev.kyuno.fr/kyuno/batchCooking/issues/39). **Purement additive côté navigateur** : le cookie `session` httpOnly reste le mécanisme principal et rien ne change pour le web existant. Mergeable seule, avant même d'introduire Capacitor. ## Pourquoi Front et API sont servis depuis la **même origine** en prod, d'où le cookie `SameSite=Lax` qui suffit aujourd'hui. Une WebView Capacitor fait tourner le bundle sous `capacitor://localhost` / `https://localhost` — un *site* différent (SameSite est basé sur le domaine) de l'API, quel que soit l'hébergement : le cookie ne part jamais. Sans repli, l'auth casserait silencieusement dans l'appli native. ## Contenu **Backend** - `packages/shared` : nouveau type `AuthResponse` = `SafeUserProfile` + `token`. Documenté : le JWT est *aussi* renvoyé dans le corps, identique à la valeur du cookie, pour les clients dont l'origine ne peut pas recevoir ce cookie. - `auth.routes.ts` : `POST /auth/login` et `POST /auth/signup` renvoient `{ ...profile, token }` (le `res.cookie(...)` est inchangé). Additif : tous les champs de profil existants restent à leur place. - `require-auth.ts` : nouvel helper `readBearerToken()` ; lecture du cookie **en priorité**, repli sur `Authorization: Bearer <jwt>` quand le cookie est absent. Le chemin cookie est inchangé au octet près — quand un cookie est présent, le ternaire le renvoie sans jamais toucher l'en-tête. **Frontend** (`apps/web`) - `api/token-store.ts` (nouveau) : interface `TokenStore` (`get`/`set`/`clear`) + `noopTokenStore`, l'implémentation navigateur — un vrai no-op, le cookie fait tout. - `api/client.ts` : `activeTokenStore` (module-level, défaut = no-op) + export `setTokenStore()`, le point d'injection qu'utilisera la phase 2 (`@capacitor/preferences` quand `Capacitor.isNativePlatform()`). `_request` ajoute l'en-tête `Authorization` **uniquement si** un jeton existe ; `login`/`signup` le stockent, `logout`/`deleteAccount` l'effacent. - **`AuthContext.tsx` non modifié** — délibéré. Le cycle de vie du jeton est entièrement encapsulé dans le client (mieux que le découpage suggéré par le ticket : aucun risque qu'un futur appelant de `apiClient.login` oublie de stocker le jeton, et le jeton ne transite jamais par l'état React). ## Hors de cette PR (phases suivantes de #39) - L'implémentation `@capacitor/preferences` de `TokenStore` + son branchement via `setTokenStore()` — arrive avec les dépendances Capacitor en phase 2. - La coquille Android, le switch de routeur, la passe responsive (phases 2 et 3). ## Tests - **Mocha** (`apps/api/test/auth.test.ts`, +5 cas) : `login`/`signup` exposent le jeton dans le corps (et il est bien égal à la valeur du cookie) ; `requireAuth` authentifie une route protégée via `Authorization: Bearer` **sans cookie** ; rejette un jeton Bearer malformé (401 `NOT_AUTHENTICATED`) ; ignore un schéma non-`Bearer`. - `pnpm build` + `pnpm lint` verts localement. Pas de base Postgres locale sur ce poste → les tests Mocha DB s'exécutent en CI. - Front : non-régression du flux auth existant, déjà couverte par `auth.feature` ; le mécanisme reste 100 % no-op hors natif (un test Cypress dédié n'apporterait rien, cf. ticket). Avancement de #39 — l'issue reste ouverte (phases 2 et 3 à suivre). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
kyuno added 1 commit 2026-09-06 02:49:14 +02:00
feat(auth): accepte un jeton Bearer en repli du cookie de session
Some checks failed
CI / lint (push) Successful in 2m6s
CI / build (push) Successful in 4m0s
CI / intent-service-test (push) Successful in 6m47s
CI / test (push) Failing after 8m58s
CI / e2e (push) Successful in 9m28s
1d8c001035
Phase 1 du portage mobile (#39) : purement additif côté navigateur, qui
continue d'utiliser le cookie `session` httpOnly inchangé.

- `POST /auth/login` et `POST /auth/signup` renvoient aussi le JWT dans le
  corps de la réponse (`AuthResponse` = `SafeUserProfile` + `token`), en
  plus de poser le cookie. Un client dont l'origine ne peut pas recevoir
  un cookie `SameSite=Lax` (WebView Capacitor) le stocke et le renvoie.
- `requireAuth` lit le cookie en priorité, puis se rabat sur un en-tête
  `Authorization: Bearer <jwt>` quand le cookie est absent. Les deux
  mécanismes coexistent, aucun n'est retiré.
- `apps/web` : `ApiClient` gagne une abstraction `TokenStore`
  (`get`/`set`/`clear`) ; l'implémentation par défaut est un no-op
  navigateur (le cookie fait tout). `_request` ajoute l'en-tête
  `Authorization` quand un jeton existe ; `login`/`signup` le stockent,
  `logout`/`deleteAccount` l'effacent. `setTokenStore` est le point
  d'injection de la version Capacitor (`@capacitor/preferences`), branchée
  en phase 2 quand la dépendance sera ajoutée.

Tests Mocha : `requireAuth` authentifie via l'en-tête `Authorization` sans
cookie, rejette un jeton Bearer invalide ou un schéma non-Bearer ;
`login`/`signup` exposent bien le jeton dans le corps.

Refs #39
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
kyuno added 1 commit 2026-09-06 03:07:24 +02:00
chore: relance la CI (job test cassé par une course d'infra avec la run de main)
All checks were successful
CI / lint (push) Successful in 2m42s
CI / build (push) Successful in 2m45s
CI / intent-service-test (push) Successful in 4m22s
CI / e2e (push) Successful in 8m26s
CI / test (push) Successful in 15m35s
5d7e4d6f6a
La run #104 a échoué au `beforeAll` racine (`seedReferenceData`, P2002 sur
`key`) parce que la run `main` #103 exécutait son propre job `test` au même
moment sur le conteneur Postgres `--name postgres` partagé. Runner idle,
aucune run concurrente cette fois. Code inchangé — build+lint verts en
local.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
kyuno merged commit b0ac42441f into main 2026-09-06 03:23:57 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: kyuno/batchCooking#54
No description provided.