Files
InsanityLab-web/apps/platforms/src/pages/api/longevity/questionario.ts
T
AdrianoDev 6301f521fd Separazione: le aree riservate diventano un'app propria (apps/platforms)
Campus, Stress Index e Longevity escono dal sito e diventano
apps.insanitylab.it, con utenti, sessioni, login e database propri.
Deciso da Adriano il 03/09/2026: una repo sola, due applicazioni
autosufficienti, cosi' che ognuna resti estraibile.

Misura che ha guidato il taglio: le tre aree condividevano col sito
SOLO l'identita' - ~170 righe fra auth.ts, middleware.ts e safe-next.ts.
Layout, librerie e dati erano gia' disgiunti.

Lato piattaforme:
- ruoli piattaforme/cliente/trainer/admin, senza quelli redazionali
- canAccessPath con default CHIUSO (nel sito era aperto: li' le rotte non
  elencate erano di chiunque fosse loggato, qui ogni sezione e' di qualcuno)
- tutta l'app protetta, si elencano le eccezioni invece dei prefissi protetti
- /api risponde 401/403, non un redirect verso una pagina di login
- tre database distinti: utenti, dati clinici senza nomi, mappatura coi nomi

Lato sito:
- ROLES tornano i tre redazionali; via le regole e il login pubblico
- il login del pannello RESPINGE un account emigrato invece di aprirgli una
  sessione: senza, landingFor lo mandava su /admin, il middleware glielo
  negava, e si formava un ciclo di redirect che sembra un guasto del sito
- l'header non mostra piu' un menu delle piattaforme: un link che esce

Verificato: sito 20 file / 146 test, piattaforme 26 file / 197 test, entrambi
i build passano, e l'app girata davvero in locale - il ruolo piattaforme entra
in Campus e viene rimbalzato da Longevity, il cliente il contrario.

Non ancora in produzione: manca il record DNS di apps.insanitylab.it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 11:21:12 +00:00

95 lines
4.4 KiB
TypeScript

import type { APIRoute } from 'astro';
import { getLongevityDb } from '../../../lib/longevity/db';
import { codicePerUtenteLoggato } from '../../../lib/longevity/anagrafica';
import { salvaDalForm } from '../../../lib/longevity/vista';
import { CAMPI_LIBERI } from '../../../lib/longevity/questionario';
export const prerender = false;
const json = (status: number, body: object) =>
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
/**
* Il corpo legittimo della richiesta: SOLO i campi che un cliente può davvero mandare.
* `client_code` non è un campo di questo tipo — non per una lista di nomi vietati (una
* lista così invecchia appena il file rinomina la variabile, vedi il commento nel test),
* ma perché il tipo non lo prevede: chi scrivesse domani `corpo.client_code` non
* supererebbe `tsc --noEmit`, il campo non esiste. Il codice cliente resta SEMPRE quello
* della sessione (codicePerUtenteLoggato, spec §8), mai un campo letto da qui.
*/
type CorpoQuestionario = {
risposte: Record<string, number>;
liberi: Record<string, string>;
consensoSanitario: string;
eta?: number;
};
/** Estrae SOLO i campi di CorpoQuestionario da un JSON grezzo; `null` se il corpo non è
* nemmeno un oggetto, o se manca `risposte`. Ogni altra chiave del corpo (incluso un
* eventuale `client_code`) viene semplicemente ignorata: non esiste modo di farla arrivare
* al chiamante, perché il tipo di ritorno non la contiene. */
function corpoDaJson(raw: unknown): CorpoQuestionario | null {
if (typeof raw !== 'object' || raw === null) return null;
const grezzo = raw as Record<string, unknown>;
const risposteIn = grezzo.risposte;
if (typeof risposteIn !== 'object' || risposteIn === null) return null;
const risposte: Record<string, number> = {};
for (const [id, v] of Object.entries(risposteIn as Record<string, unknown>)) {
if (typeof v === 'number' && Number.isFinite(v)) risposte[id] = v;
}
// Solo i campi liberi dichiarati: un corpo con chiavi arbitrarie non ne inventa di nuove.
const liberiIn = grezzo.liberi;
const liberi: Record<string, string> = {};
if (typeof liberiIn === 'object' && liberiIn !== null) {
for (const campo of CAMPI_LIBERI) {
const v = (liberiIn as Record<string, unknown>)[campo];
if (typeof v === 'string') liberi[campo] = v;
}
}
const consensoSanitario = typeof grezzo.consensoSanitario === 'string' ? grezzo.consensoSanitario : '';
const eta = typeof grezzo.eta === 'number' && Number.isFinite(grezzo.eta) ? grezzo.eta : undefined;
return { risposte, liberi, consensoSanitario, eta };
}
export const POST: APIRoute = async ({ request, locals }) => {
// Il codice cliente si ricava SEMPRE dalla sessione dell'utente loggato (spec §8), mai dal
// corpo della richiesta: la risoluzione passa da anagrafica.ts (l'unico modulo autorizzato
// a toccare quella connessione). `corpo`, qui sotto, non ha nemmeno un campo da cui
// prenderlo per errore.
const codice = codicePerUtenteLoggato(locals.user!.id);
if (!codice) return json(403, { error: 'Nessun fascicolo cliente associato a questo utente.' });
let raw: unknown;
try { raw = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
if (typeof raw !== 'object' || raw === null) return json(400, { error: 'Dati non validi.' });
const corpo = corpoDaJson(raw);
if (!corpo) return json(400, { error: 'Risposte mancanti.' });
const { risposte, liberi, consensoSanitario, eta } = corpo;
// La data della compilazione è quella del server, non quella (manomettibile) del client.
const oggi = new Date().toISOString().slice(0, 10);
// salvaDalForm delega a salvaCompilazione, che lancia se una risposta porta un test_id
// sconosciuto al registro (client disallineato, non serve malizia): un'eccezione qui
// non deve diventare un 500 generico, come in src/pages/api/contact.ts per l'invio email.
let esito;
try {
esito = salvaDalForm(getLongevityDb(), {
client_code: codice, data: oggi, eta, risposte, consensoSanitario, liberi,
});
} catch (err) {
console.error('Salvataggio questionario fallito:', err);
return json(400, {
error: 'Impossibile salvare: alcune risposte non sono riconosciute. Ricarica la pagina e riprova.',
});
}
if (!esito.ok) return json(422, { error: esito.errore });
return json(201, { sessioneId: esito.sessioneId });
};