Portage mobile (Android) via Capacitor : authentification par jeton, coquille native, passe responsive #39

Closed
opened 2026-09-05 16:30:22 +02:00 by kyuno · 2 comments
Owner

Contexte

Porter l'application sur mobile (téléphone/tablette) via Capacitor. Ce n'est
pas un projet totalement neuf pour ce repo : le README racine dit déjà
qu'apps/web est « prêt à être embarqué par Capacitor plus tard », et
AppLayout a déjà un vrai traitement mobile pour la sidebar
(@media (max-width: 640px), AppLayout.scss:363-404 — devient une barre
horizontale avec défilement plutôt que de se comprimer) posé tôt exactement
en anticipation de ce portage (voir specs/frontend-architecture.md:161-163).
Ce ticket part de cette base plutôt que de l'ignorer.

Le vrai obstacle technique, vérifié dans le code plutôt que supposé :
aujourd'hui, front et API sont servis depuis la même origine en
production (une seule image Docker, un seul port — voir le README,
section Déploiement) : c'est précisément pourquoi le cookie de session
actuel (sameSite: "lax", auth.routes.ts:19-27) fonctionne sans souci.
Une appli Capacitor packagée, elle, exécute le bundle front dans une WebView
avec sa propre origine interne (capacitor://localhost ou
https://localhost selon la plateforme) — le "site" (au sens du cookie
SameSite, basé sur le domaine, pas le port) n'est alors plus jamais le
même
que celui de l'API en production, quel que soit l'environnement. Un
cookie SameSite=Lax ne part tout simplement pas dans cette situation :
sans changement, l'authentification casserait silencieusement dans l'appli
native (connexion "réussie" en apparence, mais la session ne suit jamais).

Décisions validées avec l'utilisateur

Question Décision
Accès réseau à l'API depuis le téléphone Déjà exposée en HTTPS public — rien à prévoir côté infrastructure dans ce ticket
Authentification Jeton Bearer, stocké via le stockage sécurisé de Capacitor — plutôt que d'adapter les cookies (SameSite=None), pour éviter les blocages/incohérences documentées de Safari/WebKit (ITP) sur les cookies en WebView
Plateformes pour cette itération Android d'abord — buildable depuis Windows ; iOS (nécessite Xcode/Mac) reporté
Périmètre UI Inclut une vraie passe responsive, pas seulement la coquille + l'auth

Phase 1 — Authentification par jeton Bearer

Purement additif côté navigateur (rien ne change pour le web existant, qui
continue d'utiliser le cookie) — testable et mergeable indépendamment de
tout le reste, avant même d'ajouter Capacitor :

  • apps/api/src/modules/auth/auth.routes.ts — POST /auth/signup et
    POST /auth/login renvoient aussi le JWT dans le corps JSON de la
    réponse (en plus de continuer à poser le cookie, inchangé pour le
    navigateur). POST /auth/logout reste tel quel (efface le cookie) — un
    client natif ignore ce cookie de toute façon, et la révocation
    server-side du jeton n'existe pas plus aujourd'hui pour le cookie
    (UserProfile.tokenVersion existe mais rien ne l'incrémente encore,
    README ligne 249-252) : pas de régression de posture de sécurité, juste
    la même limite déjà acceptée aujourd'hui.
  • apps/api/src/middlewares/require-auth.ts — en plus de lire
    req.cookies?.[env.AUTH_COOKIE_NAME] (ligne 46, inchangé), accepte aussi
    un en-tête Authorization: Bearer <token> en repli quand le cookie est
    absent. Les deux mécanismes coexistent, aucun des deux n'est retiré.
  • apps/web/src/api/client.ts — ApiClient gagne une petite
    abstraction de stockage de jeton (get/set/clear), avec deux
    implémentations : un no-op pour le navigateur (le cookie continue de tout
    faire), et une version Capacitor (@capacitor/preferences) utilisée
    uniquement quand Capacitor.isNativePlatform() est vrai. _request
    ajoute l'en-tête Authorization quand un jeton existe ; credentials: "include" reste posé dans tous les cas (inoffensif côté natif, rien à
    envoyer). AuthContext.tsx stocke le jeton reçu au login/signup et
    l'efface au logout.

Phase 2 — Coquille Capacitor (Android)

  • Ajouter @capacitor/core, @capacitor/android, @capacitor/preferences
    à apps/web ; capacitor.config.ts (appId, appName,
    webDir: "dist") ; npx cap add android (génère apps/web/android/,
    projet natif — Android Studio + SDK, buildable depuis Windows).
  • URL de l'API : le build Capacitor a besoin de l'URL HTTPS publique en
    dur (pas de "même origine" possible pour un bundle packagé dans l'appli)
    — un mode de build/.env dédié pour ce cas, distinct du build web normal
    qui peut rester en URL relative.
  • Routeur : main.tsx utilise BrowserRouter aujourd'hui — dans la
    WebView locale de Capacitor, sans serveur pour retomber sur index.html
    sur une route profonde, ça casserait au premier lien direct/refresh.
    Recommandé : choisir le routeur selon Capacitor.isNativePlatform() —
    HashRouter uniquement pour le build natif (zéro config native
    supplémentaire, l'URL affichée n'a de toute façon aucune importance sans
    barre d'adresse visible), BrowserRouter inchangé pour le web.
  • Icônes d'appli, splash screen, couleur de la status bar (alignée sur les
    tokens de thème existants, _theme.scss), permission INTERNET du
    manifeste Android.
  • Vérification : build + lancement sur un émulateur/appareil Android,
    connexion → planning affiché → au moins un appel API authentifié réussit
    via le jeton.

Phase 3 — Passe responsive

Principe directeur : réutiliser la philosophie déjà choisie pour la
sidebar (AppLayout.scss's propre commentaire : "falls back to horizontal
scroll rather than shrinking if it still doesn't fit"
) plutôt
qu'inventer un nouveau patron à chaque page — défilement horizontal plutôt
que compression illisible, cohérent avec ce qui existe déjà. Seuls 3
fichiers .scss dans tout apps/web/src ont un @media aujourd'hui
(_theme.scss pour le dark mode, AppLayout.scss pour la sidebar,
recipes.scss — voir point 2 ci-dessous) — la quasi-totalité du contenu
des pages reste à traiter.

Pages prioritaires (par ordre d'impact, la page d'accueil d'abord) :

  1. PlanningPage (planning-page.scss) — grille 7 jours × 5 repas en
    <table>, illisible sur un écran de téléphone en l'état, aucun
    traitement mobile existant. Même principe que la sidebar : défilement
    horizontal de la grille plutôt qu'une refonte en vue "un jour à la fois"
    pour cette itération (plus rapide à livrer, cohérent visuellement).
  2. RecipesPage/RecipeTable — la vue maître-détail a déjà un
    empilement vertical sous 1024px (recipes.scss:169-183, table puis
    panneau de détail au lieu de côte à côte) — pas à construire, mais à
    vérifier/affiner à la largeur d'un téléphone spécifiquement (bien plus
    étroit que 1024px) : tailles de cible tactile, hauteur des lignes,
    comportement du panneau de détail en plein écran.
  3. Dialogues (RecipePickerDialog, TechStepCorrectionPopover,
    Dialog.tsx) — vérifier tailles de cible tactile, largeurs en %/vw
    plutôt que pixels fixes.
  4. ShoppingListPage, CookingSessionPage — à revoir une fois les
    trois premières traitées, moins prioritaires (pages secondaires).

Fichiers à toucher (représentatifs, pas exhaustifs)

  • apps/api/src/modules/auth/auth.routes.ts, require-auth.ts
  • apps/web/src/api/client.ts, features/auth/AuthContext.tsx
  • apps/web/src/main.tsx (choix du routeur)
  • apps/web/capacitor.config.ts (nouveau), apps/web/android/ (généré)
  • apps/web/package.json (nouvelles dépendances Capacitor)
  • apps/web/src/pages/planning/planning-page.scss,
    features/recipes/recipes.scss, et les autres .scss de page listées
    en Phase 3

Tests

  • Backend (Mocha) : requireAuth accepte un en-tête Authorization
    valide en l'absence de cookie ; login/signup renvoient bien le jeton
    dans le corps de la réponse.
  • Frontend (Cypress) : le mécanisme de stockage de jeton reste
    entièrement non-natif dans l'environnement de test (Cypress tourne dans
    un vrai navigateur, jamais dans Capacitor) — un test dédié au choix
    cookie/jeton n'apporterait rien ; se concentrer sur la non-régression du
    flux auth existant (déjà couvert).
  • Vérification manuelle obligatoire pour la Phase 2 (émulateur/appareil
    Android réel) — rien de tout ça n'est mockable en Cypress.

Hors scope pour cette itération

  • iOS — nécessite Xcode/macOS, reporté à une itération suivante une
    fois l'infrastructure de build disponible.
  • Fonctionnalités natives (appareil photo pour les photos de recette,
    notifications push, etc.) — cette itération porte l'expérience web
    existante dans une coquille native, elle n'ajoute aucune capacité
    nouvelle.
  • Mode hors-ligne / synchronisation locale — l'appli continue de
    nécessiter une connexion réseau à l'API, exactement comme aujourd'hui ;
    un vrai mode déconnecté serait un chantier à part entière, à
    réévaluer séparément si le besoin se confirme à l'usage.
  • Révocation serveur des jetons (bump de tokenVersion au logout) —
    n'existe pas non plus pour le cookie aujourd'hui, pas une régression
    introduite par ce ticket.

🤖 Filed by Claude Code

## Contexte Porter l'application sur mobile (téléphone/tablette) via Capacitor. Ce n'est pas un projet totalement neuf pour ce repo : le README racine dit déjà qu'`apps/web` est « prêt à être embarqué par Capacitor plus tard », et `AppLayout` a déjà un vrai traitement mobile pour la sidebar (`@media (max-width: 640px)`, `AppLayout.scss:363-404` — devient une barre horizontale avec défilement plutôt que de se comprimer) posé tôt exactement en anticipation de ce portage (voir `specs/frontend-architecture.md:161-163`). Ce ticket part de cette base plutôt que de l'ignorer. **Le vrai obstacle technique**, vérifié dans le code plutôt que supposé : aujourd'hui, front et API sont servis **depuis la même origine** en production (une seule image Docker, un seul port — voir le README, section Déploiement) : c'est précisément pourquoi le cookie de session actuel (`sameSite: "lax"`, `auth.routes.ts:19-27`) fonctionne sans souci. Une appli Capacitor packagée, elle, exécute le bundle front dans une WebView avec sa **propre origine interne** (`capacitor://localhost` ou `https://localhost` selon la plateforme) — le "site" (au sens du cookie SameSite, basé sur le domaine, pas le port) n'est alors **plus jamais le même** que celui de l'API en production, quel que soit l'environnement. Un cookie `SameSite=Lax` ne part tout simplement pas dans cette situation : sans changement, l'authentification casserait silencieusement dans l'appli native (connexion "réussie" en apparence, mais la session ne suit jamais). ## Décisions validées avec l'utilisateur | Question | Décision | |---|---| | Accès réseau à l'API depuis le téléphone | **Déjà exposée en HTTPS public** — rien à prévoir côté infrastructure dans ce ticket | | Authentification | **Jeton Bearer**, stocké via le stockage sécurisé de Capacitor — plutôt que d'adapter les cookies (SameSite=None), pour éviter les blocages/incohérences documentées de Safari/WebKit (ITP) sur les cookies en WebView | | Plateformes pour cette itération | **Android d'abord** — buildable depuis Windows ; iOS (nécessite Xcode/Mac) reporté | | Périmètre UI | **Inclut une vraie passe responsive**, pas seulement la coquille + l'auth | ## Phase 1 — Authentification par jeton Bearer Purement additif côté navigateur (rien ne change pour le web existant, qui continue d'utiliser le cookie) — testable et mergeable indépendamment de tout le reste, avant même d'ajouter Capacitor : - **`apps/api/src/modules/auth/auth.routes.ts`** — `POST /auth/signup` et `POST /auth/login` renvoient **aussi** le JWT dans le corps JSON de la réponse (en plus de continuer à poser le cookie, inchangé pour le navigateur). `POST /auth/logout` reste tel quel (efface le cookie) — un client natif ignore ce cookie de toute façon, et la révocation server-side du jeton n'existe pas plus aujourd'hui pour le cookie (`UserProfile.tokenVersion` existe mais rien ne l'incrémente encore, README ligne 249-252) : pas de régression de posture de sécurité, juste la même limite déjà acceptée aujourd'hui. - **`apps/api/src/middlewares/require-auth.ts`** — en plus de lire `req.cookies?.[env.AUTH_COOKIE_NAME]` (ligne 46, inchangé), accepte aussi un en-tête `Authorization: Bearer <token>` en repli quand le cookie est absent. Les deux mécanismes coexistent, aucun des deux n'est retiré. - **`apps/web/src/api/client.ts`** — `ApiClient` gagne une petite abstraction de stockage de jeton (`get`/`set`/`clear`), avec deux implémentations : un no-op pour le navigateur (le cookie continue de tout faire), et une version Capacitor (`@capacitor/preferences`) utilisée uniquement quand `Capacitor.isNativePlatform()` est vrai. `_request` ajoute l'en-tête `Authorization` quand un jeton existe ; `credentials: "include"` reste posé dans tous les cas (inoffensif côté natif, rien à envoyer). `AuthContext.tsx` stocke le jeton reçu au login/signup et l'efface au logout. ## Phase 2 — Coquille Capacitor (Android) - Ajouter `@capacitor/core`, `@capacitor/android`, `@capacitor/preferences` à `apps/web` ; `capacitor.config.ts` (`appId`, `appName`, `webDir: "dist"`) ; `npx cap add android` (génère `apps/web/android/`, projet natif — Android Studio + SDK, buildable depuis Windows). - **URL de l'API** : le build Capacitor a besoin de l'URL HTTPS publique en dur (pas de "même origine" possible pour un bundle packagé dans l'appli) — un mode de build/`.env` dédié pour ce cas, distinct du build web normal qui peut rester en URL relative. - **Routeur** : `main.tsx` utilise `BrowserRouter` aujourd'hui — dans la WebView locale de Capacitor, sans serveur pour retomber sur `index.html` sur une route profonde, ça casserait au premier lien direct/refresh. Recommandé : choisir le routeur selon `Capacitor.isNativePlatform()` — `HashRouter` uniquement pour le build natif (zéro config native supplémentaire, l'URL affichée n'a de toute façon aucune importance sans barre d'adresse visible), `BrowserRouter` inchangé pour le web. - Icônes d'appli, splash screen, couleur de la status bar (alignée sur les tokens de thème existants, `_theme.scss`), permission `INTERNET` du manifeste Android. - Vérification : build + lancement sur un émulateur/appareil Android, connexion → planning affiché → au moins un appel API authentifié réussit via le jeton. ## Phase 3 — Passe responsive **Principe directeur** : réutiliser la philosophie déjà choisie pour la sidebar (`AppLayout.scss`'s propre commentaire : *"falls back to horizontal scroll rather than shrinking if it still doesn't fit"*) plutôt qu'inventer un nouveau patron à chaque page — défilement horizontal plutôt que compression illisible, cohérent avec ce qui existe déjà. Seuls 3 fichiers `.scss` dans tout `apps/web/src` ont un `@media` aujourd'hui (`_theme.scss` pour le dark mode, `AppLayout.scss` pour la sidebar, `recipes.scss` — voir point 2 ci-dessous) — la quasi-totalité du contenu des pages reste à traiter. Pages prioritaires (par ordre d'impact, la page d'accueil d'abord) : 1. **`PlanningPage`** (`planning-page.scss`) — grille 7 jours × 5 repas en `<table>`, illisible sur un écran de téléphone en l'état, aucun traitement mobile existant. Même principe que la sidebar : défilement horizontal de la grille plutôt qu'une refonte en vue "un jour à la fois" pour cette itération (plus rapide à livrer, cohérent visuellement). 2. **`RecipesPage`/`RecipeTable`** — la vue maître-détail a déjà un empilement vertical sous 1024px (`recipes.scss:169-183`, table puis panneau de détail au lieu de côte à côte) — pas à construire, mais à vérifier/affiner à la largeur d'un téléphone spécifiquement (bien plus étroit que 1024px) : tailles de cible tactile, hauteur des lignes, comportement du panneau de détail en plein écran. 3. **Dialogues** (`RecipePickerDialog`, `TechStepCorrectionPopover`, `Dialog.tsx`) — vérifier tailles de cible tactile, largeurs en `%`/`vw` plutôt que pixels fixes. 4. **`ShoppingListPage`**, **`CookingSessionPage`** — à revoir une fois les trois premières traitées, moins prioritaires (pages secondaires). ## Fichiers à toucher (représentatifs, pas exhaustifs) - `apps/api/src/modules/auth/auth.routes.ts`, `require-auth.ts` - `apps/web/src/api/client.ts`, `features/auth/AuthContext.tsx` - `apps/web/src/main.tsx` (choix du routeur) - `apps/web/capacitor.config.ts` (nouveau), `apps/web/android/` (généré) - `apps/web/package.json` (nouvelles dépendances Capacitor) - `apps/web/src/pages/planning/planning-page.scss`, `features/recipes/recipes.scss`, et les autres `.scss` de page listées en Phase 3 ## Tests - **Backend (Mocha)** : `requireAuth` accepte un en-tête `Authorization` valide en l'absence de cookie ; `login`/`signup` renvoient bien le jeton dans le corps de la réponse. - **Frontend (Cypress)** : le mécanisme de stockage de jeton reste entièrement non-natif dans l'environnement de test (Cypress tourne dans un vrai navigateur, jamais dans Capacitor) — un test dédié au choix cookie/jeton n'apporterait rien ; se concentrer sur la non-régression du flux auth existant (déjà couvert). - **Vérification manuelle obligatoire** pour la Phase 2 (émulateur/appareil Android réel) — rien de tout ça n'est mockable en Cypress. ## Hors scope pour cette itération - **iOS** — nécessite Xcode/macOS, reporté à une itération suivante une fois l'infrastructure de build disponible. - **Fonctionnalités natives** (appareil photo pour les photos de recette, notifications push, etc.) — cette itération porte l'expérience web existante dans une coquille native, elle n'ajoute aucune capacité nouvelle. - **Mode hors-ligne / synchronisation locale** — l'appli continue de nécessiter une connexion réseau à l'API, exactement comme aujourd'hui ; un vrai mode déconnecté serait un chantier à part entière, à réévaluer séparément si le besoin se confirme à l'usage. - **Révocation serveur des jetons** (bump de `tokenVersion` au logout) — n'existe pas non plus pour le cookie aujourd'hui, pas une régression introduite par ce ticket. --- 🤖 Filed by [Claude Code](https://claude.com/claude-code)
kyuno added the
enhancement
planned
labels 2026-09-05 16:30:22 +02:00
kyuno added the
Low
label 2026-09-05 18:52:39 +02:00
Author
Owner

Les 3 phases sont mergées :

  • Phase 1 — #54 : jeton Bearer en repli du cookie de session. AuthResponse (= SafeUserProfile + token) renvoyé aussi dans le corps par /auth/login et /auth/signup ; requireAuth accepte Authorization: Bearer quand le cookie est absent ; TokenStore (no-op navigateur) côté web. Purement additif, le web est inchangé.
  • Phase 3 — #55 : passe responsive téléphone (.app-content 32→16 px, grille planning sur son propre scroll horizontal, ✕ de suppression visible au tactile, panneaux de dialogue sous 640 px).
  • Phase 2 — #56 : plomberie Capacitor — dépendances @capacitor/{core,android,preferences,cli}, capacitor.config.ts, capacitor-token-store.ts (@capacitor/preferences, importé dynamiquement seulement en natif → jamais dans le bundle web), main.tsx HashRouter en natif / BrowserRouter sur le web, build:capacitor + .env.capacitor.example.

Reste l'étape manuelle (impossible sans Android Studio ici, comme prévu par le ticket) : npx cap add android pour générer apps/web/android/, build de l'APK, lancement sur émulateur/appareil, puis la vérif « connexion → planning → un appel API authentifié réussit via le jeton ». Icônes/splash/status bar + permission INTERNET du manifeste se font dans ce projet natif généré. .env.capacitor doit recevoir l'URL HTTPS publique de l'API.

Ticket laissé ouvert pour cette étape.

🤖 via Claude Code

Les 3 phases sont mergées : - **Phase 1 — #54** : jeton Bearer en repli du cookie de session. `AuthResponse` (= `SafeUserProfile` + `token`) renvoyé aussi dans le corps par `/auth/login` et `/auth/signup` ; `requireAuth` accepte `Authorization: Bearer` quand le cookie est absent ; `TokenStore` (no-op navigateur) côté web. Purement additif, le web est inchangé. - **Phase 3 — #55** : passe responsive téléphone (`.app-content` 32→16 px, grille planning sur son propre scroll horizontal, `✕` de suppression visible au tactile, panneaux de dialogue sous 640 px). - **Phase 2 — #56** : plomberie Capacitor — dépendances `@capacitor/{core,android,preferences,cli}`, `capacitor.config.ts`, `capacitor-token-store.ts` (`@capacitor/preferences`, importé dynamiquement seulement en natif → jamais dans le bundle web), `main.tsx` `HashRouter` en natif / `BrowserRouter` sur le web, `build:capacitor` + `.env.capacitor.example`. **Reste l'étape manuelle** (impossible sans Android Studio ici, comme prévu par le ticket) : `npx cap add android` pour générer `apps/web/android/`, build de l'APK, lancement sur émulateur/appareil, puis la vérif « connexion → planning → un appel API authentifié réussit via le jeton ». Icônes/splash/status bar + permission `INTERNET` du manifeste se font dans ce projet natif généré. `.env.capacitor` doit recevoir l'URL HTTPS publique de l'API. Ticket laissé ouvert pour cette étape. 🤖 via [Claude Code](https://claude.com/claude-code)
kyuno closed this issue 2026-09-06 10:09:15 +02:00
Author
Owner

Fermé par #63 (feat/capacitor-android-native, mergé dans main à dcfb86d), qui génère et versionne le projet natif apps/web/android/ (npx cap add android) : thème natif aligné sur la palette « Mise en Place », icône adaptative + splash clair/sombre depuis apps/web/assets/logo.svg, section README « Application mobile (Android) ».

Récap des 4 PR du portage :

  • #54 — jeton Bearer en repli du cookie de session (phase 1)
  • #55 — passe responsive téléphone (phase 3)
  • #56 — plomberie web Capacitor : deps, capacitor.config.ts, HashRouter natif, token store (phase 2)
  • #63 — projet natif Android + thème + icônes/splash

Reste manuel, hors périmètre CI : ouvrir apps/web/android dans Android Studio, laisser installer le SDK Platform 36, builder l'APK et le tester sur émulateur/appareil. Avant pnpm build:capacitor : cp apps/web/.env.capacitor.example apps/web/.env.capacitor puis renseigner VITE_API_URL (URL HTTPS publique de l'API). La CI self-hosted ne build pas Android — voir #64 pour l'ajout d'un pipeline APK.

🤖 via Claude Code

Fermé par #63 (`feat/capacitor-android-native`, mergé dans `main` à `dcfb86d`), qui génère et versionne le projet natif `apps/web/android/` (`npx cap add android`) : thème natif aligné sur la palette « Mise en Place », icône adaptative + splash clair/sombre depuis `apps/web/assets/logo.svg`, section README « Application mobile (Android) ». Récap des 4 PR du portage : - #54 — jeton Bearer en repli du cookie de session (phase 1) - #55 — passe responsive téléphone (phase 3) - #56 — plomberie web Capacitor : deps, `capacitor.config.ts`, `HashRouter` natif, token store (phase 2) - #63 — projet natif Android + thème + icônes/splash **Reste manuel, hors périmètre CI** : ouvrir `apps/web/android` dans Android Studio, laisser installer le *SDK Platform 36*, builder l'APK et le tester sur émulateur/appareil. Avant `pnpm build:capacitor` : `cp apps/web/.env.capacitor.example apps/web/.env.capacitor` puis renseigner `VITE_API_URL` (URL HTTPS publique de l'API). La CI self-hosted ne build pas Android — voir #64 pour l'ajout d'un pipeline APK. 🤖 via [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
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#39
No description provided.