Portage mobile (Android) via Capacitor : authentification par jeton, coquille native, passe responsive #39
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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/webest « prêt à être embarqué par Capacitor plus tard », etAppLayouta déjà un vrai traitement mobile pour la sidebar(
@media (max-width: 640px),AppLayout.scss:363-404— devient une barrehorizontale 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://localhostouhttps://localhostselon la plateforme) — le "site" (au sens du cookieSameSite, 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=Laxne 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
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/signupetPOST /auth/loginrenvoient aussi le JWT dans le corps JSON de laréponse (en plus de continuer à poser le cookie, inchangé pour le
navigateur).
POST /auth/logoutreste tel quel (efface le cookie) — unclient 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.tokenVersionexiste 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 lirereq.cookies?.[env.AUTH_COOKIE_NAME](ligne 46, inchangé), accepte aussiun en-tête
Authorization: Bearer <token>en repli quand le cookie estabsent. Les deux mécanismes coexistent, aucun des deux n'est retiré.
apps/web/src/api/client.ts—ApiClientgagne une petiteabstraction de stockage de jeton (
get/set/clear), avec deuximplémentations : un no-op pour le navigateur (le cookie continue de tout
faire), et une version Capacitor (
@capacitor/preferences) utiliséeuniquement quand
Capacitor.isNativePlatform()est vrai._requestajoute l'en-tête
Authorizationquand un jeton existe ;credentials: "include"reste posé dans tous les cas (inoffensif côté natif, rien àenvoyer).
AuthContext.tsxstocke le jeton reçu au login/signup etl'efface au logout.
Phase 2 — Coquille Capacitor (Android)
@capacitor/core,@capacitor/android,@capacitor/preferencesà
apps/web;capacitor.config.ts(appId,appName,webDir: "dist") ;npx cap add android(génèreapps/web/android/,projet natif — Android Studio + SDK, buildable depuis Windows).
dur (pas de "même origine" possible pour un bundle packagé dans l'appli)
— un mode de build/
.envdédié pour ce cas, distinct du build web normalqui peut rester en URL relative.
main.tsxutiliseBrowserRouteraujourd'hui — dans laWebView locale de Capacitor, sans serveur pour retomber sur
index.htmlsur une route profonde, ça casserait au premier lien direct/refresh.
Recommandé : choisir le routeur selon
Capacitor.isNativePlatform()—HashRouteruniquement pour le build natif (zéro config nativesupplémentaire, l'URL affichée n'a de toute façon aucune importance sans
barre d'adresse visible),
BrowserRouterinchangé pour le web.tokens de thème existants,
_theme.scss), permissionINTERNETdumanifeste 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 horizontalscroll 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
.scssdans toutapps/web/srcont un@mediaaujourd'hui(
_theme.scsspour le dark mode,AppLayout.scsspour la sidebar,recipes.scss— voir point 2 ci-dessous) — la quasi-totalité du contenudes pages reste à traiter.
Pages prioritaires (par ordre d'impact, la page d'accueil d'abord) :
PlanningPage(planning-page.scss) — grille 7 jours × 5 repas en<table>, illisible sur un écran de téléphone en l'état, aucuntraitement 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).
RecipesPage/RecipeTable— la vue maître-détail a déjà unempilement vertical sous 1024px (
recipes.scss:169-183, table puispanneau 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.
RecipePickerDialog,TechStepCorrectionPopover,Dialog.tsx) — vérifier tailles de cible tactile, largeurs en%/vwplutôt que pixels fixes.
ShoppingListPage,CookingSessionPage— à revoir une fois lestrois premières traitées, moins prioritaires (pages secondaires).
Fichiers à toucher (représentatifs, pas exhaustifs)
apps/api/src/modules/auth/auth.routes.ts,require-auth.tsapps/web/src/api/client.ts,features/auth/AuthContext.tsxapps/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.scssde page listéesen Phase 3
Tests
requireAuthaccepte un en-têteAuthorizationvalide en l'absence de cookie ;
login/signuprenvoient bien le jetondans le corps de la réponse.
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).
Android réel) — rien de tout ça n'est mockable en Cypress.
Hors scope pour cette itération
fois l'infrastructure de build disponible.
notifications push, etc.) — cette itération porte l'expérience web
existante dans une coquille native, elle n'ajoute aucune capacité
nouvelle.
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.
tokenVersionau logout) —n'existe pas non plus pour le cookie aujourd'hui, pas une régression
introduite par ce ticket.
🤖 Filed by Claude Code
kyuno referenced this issue2026-09-06 02:49:13 +02:00
Les 3 phases sont mergées :
AuthResponse(=SafeUserProfile+token) renvoyé aussi dans le corps par/auth/loginet/auth/signup;requireAuthaccepteAuthorization: Bearerquand le cookie est absent ;TokenStore(no-op navigateur) côté web. Purement additif, le web est inchangé..app-content32→16 px, grille planning sur son propre scroll horizontal,✕de suppression visible au tactile, panneaux de dialogue sous 640 px).@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.tsxHashRouteren natif /BrowserRoutersur 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 androidpour générerapps/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 + permissionINTERNETdu manifeste se font dans ce projet natif généré..env.capacitordoit recevoir l'URL HTTPS publique de l'API.Ticket laissé ouvert pour cette étape.
🤖 via Claude Code
Fermé par #63 (
feat/capacitor-android-native, mergé dansmainàdcfb86d), qui génère et versionne le projet natifapps/web/android/(npx cap add android) : thème natif aligné sur la palette « Mise en Place », icône adaptative + splash clair/sombre depuisapps/web/assets/logo.svg, section README « Application mobile (Android) ».Récap des 4 PR du portage :
capacitor.config.ts,HashRouternatif, token store (phase 2)Reste manuel, hors périmètre CI : ouvrir
apps/web/androiddans Android Studio, laisser installer le SDK Platform 36, builder l'APK et le tester sur émulateur/appareil. Avantpnpm build:capacitor:cp apps/web/.env.capacitor.example apps/web/.env.capacitorpuis renseignerVITE_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