From 73b84155bce620e8387a4e4828d545fd39906b33 Mon Sep 17 00:00:00 2001 From: tbeaumont79 Date: Tue, 9 Jun 2026 12:47:05 +0200 Subject: [PATCH] =?UTF-8?q?feat(auth):=20middleware=20Nitro=20serveur=20?= =?UTF-8?q?=E2=80=94=20pose=20event.context.userId=20(THI-134)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aucun middleware serveur ne résolvait la session : requireUserId(event) renvoyait 401 pour toutes les routes authentifiées (metering, export-pdf, génération). Ajoute apps/app/server/middleware/auth.ts qui résout la session Better Auth (auth.api.getSession) et pose event.context.userId, sans lever d'erreur si absente — chaque route décide via requireUserId. Logique de résolution extraite en util pur testable (resolveUserId) suivant la convention DI du repo (cf. health.ts). 5 tests unitaires verts. Rattaché à l'auth (THI-131). Débloque le metering (THI-133/THI-126). Co-Authored-By: Paperclip --- apps/app/server/middleware/auth.ts | 31 ++++++++++++++++++++++ apps/app/server/utils/auth-context.ts | 28 ++++++++++++++++++++ apps/app/test/auth-context.spec.ts | 37 +++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 apps/app/server/middleware/auth.ts create mode 100644 apps/app/server/utils/auth-context.ts create mode 100644 apps/app/test/auth-context.spec.ts diff --git a/apps/app/server/middleware/auth.ts b/apps/app/server/middleware/auth.ts new file mode 100644 index 0000000..2e462da --- /dev/null +++ b/apps/app/server/middleware/auth.ts @@ -0,0 +1,31 @@ +/** + * Middleware Nitro serveur — résout la session Better Auth et renseigne + * `event.context.userId` pour toutes les requêtes API. + * + * Volontairement non bloquant : si la session est absente, on laisse + * `userId` indéfini plutôt que de lever une 401. Chaque route choisit son + * exigence d'authentification via `requireUserId(event)` (server/utils/session.ts, + * lot metering THI-126). Ce middleware est le *producteur* de `event.context.userId` ; + * `requireUserId` en est le *consommateur*. + * + * Rattaché à l'auth (THI-131). Débloque le metering (THI-133/THI-126), dont + * `GET /api/usage/current` qui renvoyait 401 faute de middleware serveur. + */ +import { auth } from '../utils/auth' +import { resolveUserId } from '../utils/auth-context' + +// Le champ de contexte est aussi déclaré côté lecteur dans server/utils/session.ts. +// Les déclarations d'interface identiques fusionnent : aucune collision au merge. +declare module 'h3' { + interface H3EventContext { + /** Id de l'utilisateur authentifié (renseigné par ce middleware). */ + userId?: string + } +} + +export default defineEventHandler(async (event) => { + event.context.userId = await resolveUserId( + (args) => auth.api.getSession(args), + event.headers, + ) +}) diff --git a/apps/app/server/utils/auth-context.ts b/apps/app/server/utils/auth-context.ts new file mode 100644 index 0000000..85d279a --- /dev/null +++ b/apps/app/server/utils/auth-context.ts @@ -0,0 +1,28 @@ +/** + * Résolution de l'id utilisateur à partir d'une session Better Auth. + * + * Logique pure et injectable (on passe le résolveur de session en paramètre) + * pour rester testable sans charger l'instance `auth` ni `better-auth`. + * Le middleware Nitro (`server/middleware/auth.ts`) câble ici la vraie session. + */ + +/** Forme minimale d'une session Better Auth dont on a besoin ici. */ +export interface SessionLike { + user?: { id?: string | null } | null +} + +/** Récupère une session à partir des en-têtes de la requête (ou null si absente). */ +export type SessionResolver = (args: { headers: Headers }) => Promise + +/** + * Résout l'id de l'utilisateur authentifié, ou `undefined` si la session est + * absente/anonyme. Ne lève jamais d'erreur d'authentification : chaque route + * décide de l'exigence via `requireUserId(event)`. + */ +export async function resolveUserId( + getSession: SessionResolver, + headers: Headers, +): Promise { + const session = await getSession({ headers }) + return session?.user?.id ?? undefined +} diff --git a/apps/app/test/auth-context.spec.ts b/apps/app/test/auth-context.spec.ts new file mode 100644 index 0000000..87636c6 --- /dev/null +++ b/apps/app/test/auth-context.spec.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from 'vitest' +import { resolveUserId, type SessionResolver } from '../server/utils/auth-context' + +const headers = new Headers() + +describe('resolveUserId', () => { + it("renvoie l'id quand une session authentifiée existe", async () => { + const getSession: SessionResolver = async () => ({ user: { id: 'usr_123' } }) + expect(await resolveUserId(getSession, headers)).toBe('usr_123') + }) + + it('renvoie undefined (sans lever) quand il n’y a pas de session', async () => { + const getSession: SessionResolver = async () => null + expect(await resolveUserId(getSession, headers)).toBeUndefined() + }) + + it('renvoie undefined quand la session n’a pas d’utilisateur', async () => { + const getSession: SessionResolver = async () => ({ user: null }) + expect(await resolveUserId(getSession, headers)).toBeUndefined() + }) + + it('renvoie undefined quand l’id utilisateur est nul', async () => { + const getSession: SessionResolver = async () => ({ user: { id: null } }) + expect(await resolveUserId(getSession, headers)).toBeUndefined() + }) + + it('transmet bien les en-têtes de la requête au résolveur de session', async () => { + const h = new Headers({ cookie: 'better-auth.session_token=abc' }) + let received: Headers | undefined + const getSession: SessionResolver = async ({ headers }) => { + received = headers + return { user: { id: 'usr_x' } } + } + await resolveUserId(getSession, h) + expect(received).toBe(h) + }) +})