App Nuxt 3 + Nuxt UI qui lit les projets du CMS Grav et compose des portfolios A3 paysage exportés en PDF (gabarit book_v3) : composeur WYSIWYG, templates cover/toc/projet/grille/page libre, pipeline Playwright + Ghostscript, gel du contenu à la génération, partage public opt-in. Dépôt autonome : Dockerfile (base Playwright + Ghostscript + ffmpeg + rsync) inclus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
49 lines
1.7 KiB
TypeScript
49 lines
1.7 KiB
TypeScript
// Session « mot de passe partagé » (spec §12) : cookie HMAC signé, sans stockage serveur.
|
|
import { createHmac, timingSafeEqual } from 'node:crypto'
|
|
import type { H3Event } from 'h3'
|
|
|
|
const COOKIE_NAME = 'fl_session'
|
|
const SESSION_TTL_MS = 1000 * 60 * 60 * 24 * 30 // 30 jours
|
|
|
|
function sign(secret: string, payload: string): string {
|
|
return createHmac('sha256', secret).update(payload).digest('base64url')
|
|
}
|
|
|
|
export function createSessionToken(secret: string): string {
|
|
const exp = String(Date.now() + SESSION_TTL_MS)
|
|
return `${exp}.${sign(secret, exp)}`
|
|
}
|
|
|
|
export function isValidSessionToken(secret: string, token: string | undefined): boolean {
|
|
if (!token) return false
|
|
const [exp, sig] = token.split('.')
|
|
if (!exp || !sig) return false
|
|
if (Number(exp) < Date.now()) return false
|
|
const expected = sign(secret, exp)
|
|
const a = Buffer.from(sig)
|
|
const b = Buffer.from(expected)
|
|
return a.length === b.length && timingSafeEqual(a, b)
|
|
}
|
|
|
|
export function setSessionCookie(event: H3Event): void {
|
|
const { sessionSecret } = useRuntimeConfig(event)
|
|
setCookie(event, COOKIE_NAME, createSessionToken(sessionSecret), {
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
path: '/',
|
|
maxAge: SESSION_TTL_MS / 1000,
|
|
})
|
|
}
|
|
|
|
export function clearSessionCookie(event: H3Event): void {
|
|
deleteCookie(event, COOKIE_NAME, { path: '/' })
|
|
}
|
|
|
|
export function isAuthenticated(event: H3Event): boolean {
|
|
const config = useRuntimeConfig(event)
|
|
// Jeton interne : utilisé par le rendu print (Playwright) en phase 2.
|
|
const headerToken = getHeader(event, 'x-internal-token') ?? getQuery(event)['internal-token']
|
|
if (headerToken && headerToken === config.internalToken) return true
|
|
return isValidSessionToken(config.sessionSecret, getCookie(event, COOKIE_NAME))
|
|
}
|