All checks were successful
La session expire déjà (JWT 7 j), mais le client web n'y réagissait pas :
seul le `GET /auth/me` du montage de `AuthProvider` attrapait un 401 ; un
401 de n'importe quel autre endpoint ne faisait que remonter une erreur
locale, l'UI connectée restant affichée jusqu'au prochain rechargement
complet. Sur l'app native (rarement redémarrée, token persisté) la session
paraissait ne jamais finir.
- `client.ts` : sur un `401 NOT_AUTHENTICATED` pour toute requête autre que
le probe `GET /auth/me`, `ApiClient._request` purge le token store et
appelle un handler `onSessionExpired` (enregistré via
`setSessionExpiredHandler`). L'`ApiError` continue d'être levée comme
avant.
- `AuthContext.tsx` : `AuthProvider` enregistre ce handler au montage —
`setUser(null)` + `navigate(/login, { replace: true })`, gardé par un
`userRef` pour être un no-op si déjà déconnecté. `ApiClient` restant
agnostique du framework, c'est bien lui qui délègue la navigation/état.
Pas de changement API : le back renvoie déjà 401 sur JWT expiré
(`require-auth.ts`), couvert par sa suite Mocha.
Tests : `apps/web/cypress/e2e/session-expiry.cy.ts` — un 401 en cours de
session redirige vers `/login` et vide l'état (un retour sur une route
gardée re-redirige) ; le 401 du `/auth/me` initial d'un visiteur non
connecté ne déclenche pas ce chemin (la landing reste affichée). Suite e2e
existante (auth / layout / account / recipes / onboarding / preferences)
revérifiée sans régression.
Closes #108
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
66 lines
2.6 KiB
TypeScript
66 lines
2.6 KiB
TypeScript
// Mocks the API via cy.intercept — this job doesn't run a live backend (see
|
|
// .github/workflows/ci.yml). apps/api's own Mocha suite covers the real
|
|
// 401 behavior (an expired/invalid JWT -> `require-auth` -> 401
|
|
// NOT_AUTHENTICATED); this only covers the *web client's* reaction to it.
|
|
|
|
import { ErrorCode } from "@batch-cooking/shared";
|
|
|
|
const authenticatedProfile = {
|
|
id: 1,
|
|
firstName: "Alice",
|
|
lastName: "Martin",
|
|
email: "alice@example.com",
|
|
tokenVersion: 0,
|
|
houseId: 1,
|
|
dietId: null,
|
|
};
|
|
|
|
// Issue #108 — a session that expired (or was invalidated) server-side
|
|
// mid-use only took effect on the next full reload: any other request just
|
|
// surfaced a local error while the authenticated UI stayed put. `ApiClient`
|
|
// now reacts to a `401 NOT_AUTHENTICATED` on any request other than the
|
|
// initial `/auth/me` probe by dropping the session and bouncing to
|
|
// `/login`.
|
|
describe("Session expiry", () => {
|
|
it("bounces to /login when a request 401s mid-session", () => {
|
|
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile });
|
|
// Logged in, planning loads fine.
|
|
cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }).as("getPlanning");
|
|
|
|
cy.visit("/");
|
|
cy.wait("@getPlanning");
|
|
cy.contains("h1", "Planning de la semaine").should("be.visible");
|
|
|
|
// The session dies server-side: every endpoint now 401s, including the
|
|
// `/auth/me` probe a fresh load would make.
|
|
const unauthenticated = {
|
|
statusCode: 401,
|
|
body: { code: ErrorCode.NOT_AUTHENTICATED, message: "not authenticated" },
|
|
};
|
|
cy.intercept("GET", "**/auth/me", unauthenticated);
|
|
cy.intercept("GET", /\/planning\?/, unauthenticated);
|
|
|
|
// The next planning fetch (a week-nav click) 401s -> bounced to /login.
|
|
cy.get(".week-nav__arrow").last().click();
|
|
cy.url().should("include", "/login");
|
|
|
|
// ...and it's a real logout: reloading a guarded route redirects again
|
|
// rather than showing stale authed content.
|
|
cy.visit("/recettes");
|
|
cy.url().should("include", "/login");
|
|
});
|
|
|
|
it("does not bounce a logged-out visitor — the initial /auth/me 401 is the normal state", () => {
|
|
cy.intercept("GET", "**/auth/me", {
|
|
statusCode: 401,
|
|
body: { code: ErrorCode.NOT_AUTHENTICATED, message: "not authenticated" },
|
|
});
|
|
|
|
cy.visit("/");
|
|
// `/` for a logged-out visitor is the public landing page (issue #41),
|
|
// not a redirect to /login — the `/auth/me` probe 401 must not trip the
|
|
// session-expiry handler.
|
|
cy.contains("a", "Se connecter").should("be.visible");
|
|
cy.url().should("not.include", "/login");
|
|
});
|
|
});
|