Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/app/server/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -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,
)
})
28 changes: 28 additions & 0 deletions apps/app/server/utils/auth-context.ts
Original file line number Diff line number Diff line change
@@ -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<SessionLike | null>

/**
* 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<string | undefined> {
const session = await getSession({ headers })
return session?.user?.id ?? undefined
}
37 changes: 37 additions & 0 deletions apps/app/test/auth-context.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})