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
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
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 });
|
||||
};
|
||||
Reference in New Issue
Block a user