Générateur de portfolios PDF Figures Libres (app Nuxt autonome)
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>
This commit is contained in:
48
server/lib/session.ts
Normal file
48
server/lib/session.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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))
|
||||
}
|
||||
Reference in New Issue
Block a user