import jwt from "@fastify/jwt" import type { UserRole } from "@iti/shared" import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify" import fp from "fastify-plugin" import { AppError } from "../errors.js" /** Payload dell'access token: solo id e ruolo (niente dati personali). */ export type AccessTokenPayload = { sub: string; role: UserRole } declare module "@fastify/jwt" { interface FastifyJWT { payload: AccessTokenPayload user: AccessTokenPayload } } declare module "fastify" { interface FastifyInstance { /** onRequest/preHandler: richiede un access token valido (Authorization: Bearer ...). */ authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise /** * onRequest/preHandler per le route pubbliche che cambiano con l'utente (shop): senza header * Authorization la richiesta è anonima; con un token non valido o scaduto → 401 (il frontend * fa il refresh invece di ricevere silenziosamente i dati da anonimo). */ optionalAuthenticate: (request: FastifyRequest, reply: FastifyReply) => Promise /** onRequest/preHandler: autentica e richiede uno dei ruoli indicati (403 altrimenti). */ requireRole: (...roles: UserRole[]) => (request: FastifyRequest, reply: FastifyReply) => Promise /** Firma un access token per l'utente (durata ACCESS_TOKEN_TTL_SECONDS). */ signAccessToken: (payload: AccessTokenPayload) => string } } /** * Access token JWT HS256 di breve durata, restituito nel body e tenuto in memoria dal frontend. * Il ruolo nel token vale fino alla scadenza: un cambio di ruolo (es. installatore approvato) * si riflette al refresh successivo, che rilegge l'utente dal DB. */ export default fp( async function authPlugin(app: FastifyInstance) { await app.register(jwt, { secret: app.config.JWT_SECRET, sign: { algorithm: "HS256", expiresIn: app.config.ACCESS_TOKEN_TTL_SECONDS }, verify: { algorithms: ["HS256"] }, messages: { noAuthorizationInHeaderMessage: "Autenticazione richiesta", authorizationTokenExpiredMessage: "Sessione scaduta", authorizationTokenInvalid: "Token non valido", authorizationTokenUntrusted: "Token non valido", authorizationTokenUnsigned: "Token non valido", badRequestErrorMessage: "Formato del token non valido", badCookieRequestErrorMessage: "Formato del token non valido", }, }) app.decorate("signAccessToken", (payload: AccessTokenPayload) => app.jwt.sign(payload)) app.decorate("authenticate", async (request: FastifyRequest) => { try { await request.jwtVerify() } catch (err) { const message = err instanceof Error ? err.message : "Autenticazione richiesta" throw new AppError(401, "UNAUTHORIZED", message) } }) app.decorate("optionalAuthenticate", async (request: FastifyRequest, reply: FastifyReply) => { if (request.headers.authorization) await app.authenticate(request, reply) }) app.decorate("requireRole", (...roles: UserRole[]) => { return async (request: FastifyRequest, reply: FastifyReply) => { if (!request.user) await app.authenticate(request, reply) if (!roles.includes(request.user.role)) { throw new AppError(403, "FORBIDDEN", "Permessi insufficienti") } } }) }, { name: "auth", dependencies: ["config"] }, )