Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a59b4e0c0e | |||
| c8a6b318cb | |||
| 62b19b21d9 | |||
| 89d2186e69 | |||
| 15dbe6a397 | |||
| a7b81e2ff4 | |||
| 18b0877026 |
@@ -11,7 +11,15 @@ export default defineConfig({
|
||||
// il login smette di funzionare, e l'errore sembra "credenziali sbagliate".
|
||||
// Stessa configurazione del sito, con l'host di quest'app.
|
||||
security: {
|
||||
allowedDomains: [{ hostname: 'apps.insanitylab.it', protocol: 'https' }],
|
||||
allowedDomains: [
|
||||
// L'host definitivo, quando il record A su Aruba ci sara'.
|
||||
{ hostname: 'apps.insanitylab.it', protocol: 'https' },
|
||||
// ⚠️ L'host in uso oggi (03/09/2026): il dominio del cliente resta su Aruba e le aree
|
||||
// riservate stanno su un sottodominio nostro. Finche' e' in uso deve stare qui:
|
||||
// senza, dietro Traefik il controllo CSRF di Astro respinge ogni POST con 403 e il
|
||||
// login sembra rifiutare le credenziali.
|
||||
{ hostname: 'piattaforme.tielogic.xyz', protocol: 'https' },
|
||||
],
|
||||
},
|
||||
|
||||
adapter: node({ mode: 'standalone' }),
|
||||
|
||||
@@ -23,7 +23,13 @@ services:
|
||||
- /opt/docker/insanitylab-platforms/data:/app/data
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.insanitylab-platforms.rule=Host(`apps.insanitylab.it`)
|
||||
# L'host di casa, creato da Adriano il 03/09/2026 sulla zona tielogic.xyz (Hostinger).
|
||||
# ⚠️ Per un'ora circa le piattaforme avevano preso in prestito `insanitylab.tielogic.xyz`:
|
||||
# quel prestito e' finito, e poi Adriano ha tolto del tutto quel record dal DNS.
|
||||
# Quando il record A `apps` esistera' su Aruba, qui si aggiunge
|
||||
# `|| Host(`apps.insanitylab.it`)` e si tengono doppi per un po', invece di spostare
|
||||
# di netto un indirizzo che nel frattempo qualcuno ha salvato.
|
||||
- traefik.http.routers.insanitylab-platforms.rule=Host(`piattaforme.tielogic.xyz`)
|
||||
- traefik.http.routers.insanitylab-platforms.tls=true
|
||||
- traefik.http.routers.insanitylab-platforms.entrypoints=websecure
|
||||
- traefik.http.routers.insanitylab-platforms.tls.certresolver=mytlschallenge
|
||||
|
||||
Generated
+20
@@ -26,6 +26,7 @@
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"marked": "^18.0.11",
|
||||
"tsx": "^4.23.13",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.9"
|
||||
}
|
||||
@@ -6245,6 +6246,25 @@
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.23.13",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz",
|
||||
"integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
"start": "node ./dist/server/entry.mjs",
|
||||
"test": "vitest run",
|
||||
"create-user": "node scripts/create-user.mjs",
|
||||
"sync-campus": "node scripts/sync-campus.mjs"
|
||||
"sync-campus": "node scripts/sync-campus.mjs",
|
||||
"seed-demo": "tsx scripts/seed-demo.ts",
|
||||
"crea-fascicolo": "tsx scripts/crea-fascicolo.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "11.0.1",
|
||||
@@ -31,6 +33,7 @@
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"marked": "^18.0.11",
|
||||
"tsx": "^4.23.13",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.9"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* crea-fascicolo.ts — apre il fascicolo Longevity di una persona che ha già un account.
|
||||
*
|
||||
* A COSA SERVE: il referto si aggancia all'utente, non al ruolo (`codicePerUtenteLoggato`),
|
||||
* quindi chiunque abbia un account — cliente, trainer o admin — può avere il proprio, ma
|
||||
* solo se esiste la riga che lo lega al fascicolo. Finché non c'è il gestionale trainer,
|
||||
* questo è l'unico modo di crearla senza inventarsi dati come fa seed-demo.
|
||||
*
|
||||
* COME SI USA:
|
||||
* npm run crea-fascicolo -- --utente nicola --nome Nicola --cognome Antonelli --sesso M
|
||||
* opzionali: --nascita 1980-01-31 --email x@y.it --telefono 333...
|
||||
* percorsi: --utenti-db / --longevity-db / --identity-db, oppure DB_PATH /
|
||||
* LONGEVITY_DB_PATH / IDENTITY_DB_PATH (gli stessi nomi del container).
|
||||
*
|
||||
* ⚠️ NON inventa niente. `--sesso` è obbligatorio perché entra nel calcolo dei punteggi
|
||||
* (le curve dei test fisici sono distinte per sesso), e la data di nascita si può omettere
|
||||
* ma allora l'età della sessione resta vuota: meglio un campo vuoto che una data
|
||||
* plausibile e falsa.
|
||||
* ⚠️ Si ferma se quell'utente ha già un fascicolo: due fascicoli per la stessa persona
|
||||
* significano due storie parallele, e nessuna delle due è quella giusta.
|
||||
* ⚠️ Il fascicolo nasce VUOTO: il referto dirà «non hai ancora compilato il questionario»
|
||||
* finché la persona non lo compila da sé, che è il percorso vero.
|
||||
*
|
||||
* DIPENDE DA: tsx (devDependency).
|
||||
*/
|
||||
import { createLongevityDb, createIdentityDb } from '../src/lib/longevity/db';
|
||||
import { creaCliente, codicePerUtente } from '../src/lib/longevity/anagrafica';
|
||||
import { createDb } from '../src/lib/db';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const opt = (nome: string, def?: string): string | undefined => {
|
||||
const i = argv.indexOf(nome);
|
||||
return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : def;
|
||||
};
|
||||
|
||||
const utente = opt('--utente');
|
||||
const nome = opt('--nome');
|
||||
const cognome = opt('--cognome');
|
||||
const sesso = opt('--sesso');
|
||||
|
||||
if (!utente || !nome || !cognome || (sesso !== 'M' && sesso !== 'F')) {
|
||||
console.error('Uso: npm run crea-fascicolo -- --utente <username> --nome <N> --cognome <C> --sesso <M|F>');
|
||||
console.error(' opzionali: --nascita AAAA-MM-GG --email <mail> --telefono <n>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const utentiDb = createDb(opt('--utenti-db', process.env.DB_PATH ?? 'data/piattaforme.db'));
|
||||
const longevity = createLongevityDb(opt('--longevity-db', process.env.LONGEVITY_DB_PATH ?? 'data/longevity.db'));
|
||||
const identity = createIdentityDb(opt('--identity-db', process.env.IDENTITY_DB_PATH ?? 'data/identity.db'));
|
||||
|
||||
const riga = utentiDb.prepare('SELECT id, role FROM users WHERE username = ?').get(utente) as
|
||||
{ id: number; role: string } | undefined;
|
||||
if (!riga) {
|
||||
console.error(`L'utente "${utente}" non esiste: crealo prima con npm run create-user.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const esistente = codicePerUtente(identity, riga.id);
|
||||
if (esistente) {
|
||||
console.error(`"${utente}" ha già il fascicolo ${esistente}: non ne apro un secondo.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const code = creaCliente(identity, longevity, {
|
||||
nome, cognome, sesso,
|
||||
data_nascita: opt('--nascita'),
|
||||
email: opt('--email'),
|
||||
telefono: opt('--telefono'),
|
||||
user_id: riga.id,
|
||||
});
|
||||
|
||||
console.log(`fascicolo ${code} aperto per ${nome} ${cognome}`);
|
||||
console.log(` utente ${utente} (id ${riga.id}, ruolo ${riga.role})`);
|
||||
console.log(` nascita ${opt('--nascita') ?? '— non dichiarata: l\'età delle sessioni resterà vuota'}`);
|
||||
console.log('\nÈ vuoto: da /longevity/questionario la persona lo compila e il referto compare.');
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* seed-demo.ts — un cliente dimostrativo con un referto vero da mostrare.
|
||||
*
|
||||
* COSA FA, in quest'ordine:
|
||||
* 1. apre i tre database (li crea se mancano; il registro dei test si semina da sé,
|
||||
* vedi src/lib/longevity/db.ts);
|
||||
* 2. crea un utente con ruolo `cliente` in piattaforme.db;
|
||||
* 3. crea il suo fascicolo — la riga con il nome in identity.db, quella senza nome in
|
||||
* longevity.db — passando da `creaCliente`, l'unico varco fra le due;
|
||||
* 4. compila il questionario per lui con risposte plausibili, attraverso `salvaDalForm`:
|
||||
* cioè lo stesso percorso della pagina vera, consenso compreso. Da lì i punteggi
|
||||
* vengono calcolati e congelati, ed è quello che il referto legge.
|
||||
*
|
||||
* COME SI USA:
|
||||
* npm run seed-demo # utente "demo-cliente", password generata
|
||||
* npm run seed-demo -- --utente nicola-demo --password sceltaDaTe
|
||||
* npm run seed-demo -- --data 2026-09-03 # data della compilazione (default: oggi)
|
||||
* percorsi: --utenti-db / --longevity-db / --identity-db, oppure le variabili
|
||||
* DB_PATH / LONGEVITY_DB_PATH / IDENTITY_DB_PATH (stessi nomi del container).
|
||||
*
|
||||
* ⚠️ NON è idempotente per scelta: se l'utente esiste già lo dice e si ferma, senza
|
||||
* toccare niente. Rifare la demo su dati esistenti significherebbe o duplicare un
|
||||
* fascicolo o cancellarne uno, e nessuna delle due è una cosa che uno script deve
|
||||
* decidere da solo.
|
||||
*
|
||||
* ⚠️ I dati sono INVENTATI e vanno detti tali a chi guarda: è una persona che non esiste.
|
||||
* Il referto che ne esce non è un esempio clinico, è un esempio di interfaccia.
|
||||
*
|
||||
* ⚠️ Cosa mostrerà davvero il radar: il registro contiene oggi SOLO le 20 domande del
|
||||
* questionario — i test fisici (handgrip, VO2max, plank, composizione corporea...) non
|
||||
* hanno ancora una riga, arrivano col gestionale trainer. Quindi escono con un valore gli
|
||||
* assi che il questionario copre almeno al 40%, e gli altri restano dichiarati
|
||||
* insufficienti con l'elenco di cosa manca. Non è un difetto della demo: è lo stato del
|
||||
* prodotto, ed è anche la regola di Nicola che si vede all'opera (annullare invece di
|
||||
* stimare).
|
||||
*
|
||||
* DIPENDE DA: tsx (devDependency) — il codice del prodotto è TypeScript e questo script
|
||||
* lo usa davvero invece di riscriverne una copia in SQL.
|
||||
*/
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { createLongevityDb, createIdentityDb } from '../src/lib/longevity/db';
|
||||
import { creaCliente, etaAllaData } from '../src/lib/longevity/anagrafica';
|
||||
import { salvaDalForm, campiDelQuestionario } from '../src/lib/longevity/vista';
|
||||
import { createDb } from '../src/lib/db';
|
||||
import { createUser } from '../src/lib/auth';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const opt = (nome: string, def: string): string => {
|
||||
const i = argv.indexOf(nome);
|
||||
return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : def;
|
||||
};
|
||||
|
||||
const utente = opt('--utente', 'demo-cliente');
|
||||
const password = opt('--password', randomBytes(9).toString('base64url'));
|
||||
const nome = opt('--nome', 'Giulia');
|
||||
const cognome = opt('--cognome', 'Esempio');
|
||||
const nascita = opt('--nascita', '1990-05-14');
|
||||
const data = opt('--data', new Date().toISOString().slice(0, 10));
|
||||
|
||||
const utentiDb = createDb(opt('--utenti-db', process.env.DB_PATH ?? 'data/piattaforme.db'));
|
||||
const longevity = createLongevityDb(opt('--longevity-db', process.env.LONGEVITY_DB_PATH ?? 'data/longevity.db'));
|
||||
const identity = createIdentityDb(opt('--identity-db', process.env.IDENTITY_DB_PATH ?? 'data/identity.db'));
|
||||
|
||||
const esistente = utentiDb.prepare('SELECT id FROM users WHERE username = ?').get(utente) as { id: number } | undefined;
|
||||
if (esistente) {
|
||||
console.error(`L'utente "${utente}" esiste già (id ${esistente.id}): non tocco niente.`);
|
||||
console.error('Usa --utente <altro-nome> per una demo nuova.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Le risposte della persona inventata: una in salute ma non perfetta. Un questionario
|
||||
* con tutti i valori al massimo darebbe un referto tutto verde, che è il modo migliore
|
||||
* per non far vedere niente — né le curve, né la soglia del 40%, né cosa manca.
|
||||
*/
|
||||
const RISPOSTE: Record<string, number> = {
|
||||
// sonno
|
||||
q_ore_sonno: 7, q_min_addorm: 20, q_risvegli: 1, q_riposato: 7,
|
||||
q_sonnolenza_diurna: 2, q_caffeina: 3,
|
||||
// energia e stress
|
||||
q_energia_media: 7, q_calo_pomeridiano: 4, q_tensione: 3, q_sopraffatto: 4,
|
||||
q_esaurimento: 1, q_controllo: 7, q_sicurezza_gestione: 8, q_pensieri_lavoro: 2,
|
||||
// stile di vita
|
||||
q_attivita: 3, q_alimentazione: 7, q_alcol_life: 4, q_sigarette: 0,
|
||||
q_luce: 2, q_schermi: 30,
|
||||
};
|
||||
|
||||
// Il registro è la fonte: se una domanda è stata aggiunta o rinominata, questo script deve
|
||||
// dirlo invece di salvare un questionario a metà senza che si veda.
|
||||
const campi = campiDelQuestionario(longevity, data);
|
||||
const mancanti = campi.filter((c) => !(c.id in RISPOSTE)).map((c) => c.id);
|
||||
const inventate = Object.keys(RISPOSTE).filter((id) => !campi.some((c) => c.id === id));
|
||||
if (mancanti.length || inventate.length) {
|
||||
if (mancanti.length) console.error(`⚠️ domande del registro senza risposta qui: ${mancanti.join(', ')}`);
|
||||
if (inventate.length) console.error(`⚠️ risposte che il registro non conosce: ${inventate.join(', ')}`);
|
||||
console.error('Allinea RISPOSTE al registro prima di rifare la demo.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const userId = createUser(utentiDb, utente, password, 'cliente');
|
||||
const code = creaCliente(identity, longevity, {
|
||||
nome, cognome, sesso: 'F', data_nascita: nascita, user_id: userId,
|
||||
});
|
||||
|
||||
const esito = salvaDalForm(longevity, {
|
||||
client_code: code,
|
||||
data,
|
||||
eta: etaAllaData(nascita, data),
|
||||
risposte: RISPOSTE,
|
||||
consensoSanitario: 'sì',
|
||||
});
|
||||
|
||||
if (!esito.ok) {
|
||||
console.error(`Il questionario non è stato salvato: ${esito.errore}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`utente ${utente} (ruolo cliente, id ${userId})`);
|
||||
console.log(`password ${password}`);
|
||||
console.log(`fascicolo ${code} — ${nome} ${cognome}, ${etaAllaData(nascita, data)} anni`);
|
||||
console.log(`sessione ${esito.sessioneId}, questionario del ${data}, ${campi.length} risposte`);
|
||||
console.log('\nEntra da /login e atterri sul referto: /longevity/io');
|
||||
@@ -61,9 +61,15 @@ export function logout(db: Database.Database, token: string): void {
|
||||
// /api/admin: anche /campus, /piattaforme e /longevity) vive in isProtectedPath, più sotto:
|
||||
// qui arrivano solo utenti già autenticati su una di quelle rotte.
|
||||
const RULES: [RegExp, Role[]][] = [
|
||||
// Le due piattaforme storiche: il Biohacking Campus (contenuti) e Stress Index.
|
||||
// ⚠️ L'INDICE /piattaforme e' la casa comune: lo apre chiunque sia loggato, perche' e'
|
||||
// la pagina su cui si atterra dopo il login (deciso da Adriano il 03/09/2026). Mostra a
|
||||
// ognuno solo le sezioni che puo' aprire davvero — il filtro e' in data/piattaforme.
|
||||
// Stress Index sta SOTTO quel percorso ma resta riservato, quindi la sua regola viene
|
||||
// prima: vince la prima che combacia, e la piu' specifica deve poter parlare per prima.
|
||||
[/^\/piattaforme\/stress-index(\/|$)/, ['admin', 'piattaforme']],
|
||||
[/^\/piattaforme\/?$/, [...ROLES]],
|
||||
// Il Biohacking Campus (contenuti del corso).
|
||||
[/^\/campus(\/|$)/, ['admin', 'piattaforme']],
|
||||
[/^\/piattaforme(\/|$)/, ['admin', 'piattaforme']],
|
||||
// Longevity: il cliente vede solo il proprio spazio, il gestionale è del trainer.
|
||||
[/^\/longevity\/gestionale(\/|$)/, ['admin', 'trainer']],
|
||||
[/^\/api\/longevity\/gestionale(\/|$)/, ['admin', 'trainer']],
|
||||
@@ -130,12 +136,18 @@ export function isRole(v: unknown): v is Role {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dove atterra chi ha appena fatto il login. ⚠️ `/admin` non esiste in quest'app: il
|
||||
* fondo della scala è l'indice delle piattaforme, non un pannello.
|
||||
* Dove atterra chi ha appena fatto il login: **sempre l'indice delle piattaforme**, per
|
||||
* tutti i ruoli (deciso da Adriano il 03/09/2026 — si entra e si sceglie dove andare,
|
||||
* anche quando la scelta e' una sola).
|
||||
*
|
||||
* ⚠️ Questa funzione e' anche il "torna a casa" del middleware quando un ruolo apre una
|
||||
* sezione che non gli spetta. Quindi la sua destinazione DEVE essere aperta a ogni ruolo,
|
||||
* o si forma un ciclo di redirect: il middleware nega la pagina, rimanda a landingFor, e
|
||||
* landingFor rimanda dove il middleware nega di nuovo. E' esattamente il guasto che il
|
||||
* sito ha rischiato lo stesso giorno con gli account emigrati. Un test lo presidia
|
||||
* girando su tutti i ruoli.
|
||||
*/
|
||||
export function landingFor(role: string): string {
|
||||
if (role === 'cliente') return '/longevity/io';
|
||||
if (role === 'trainer') return '/longevity/gestionale';
|
||||
export function landingFor(_role: string): string {
|
||||
return '/piattaforme';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { dirname } from 'node:path';
|
||||
import { seedRegistro, seedPesi, MODEL_VERSION } from './registro';
|
||||
|
||||
/**
|
||||
* Sotto questa copertura di peso un elemento calcolato e' 'insufficiente' e non ha valore.
|
||||
@@ -127,8 +128,28 @@ function apri(p: string, schema: string): Database.Database {
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ Il registro dei test e i pesi si seminano QUI, all'apertura del database.
|
||||
*
|
||||
* Fino al 03/09/2026 `seedRegistro` e `seedPesi` esistevano, erano giusti, ed erano
|
||||
* chiamati **solo dai test**: su un database nuovo il registro restava vuoto, quindi
|
||||
* `campiDelQuestionario` non trovava niente e la pagina del questionario si apriva
|
||||
* **senza domande**. Non era un difetto della demo: era il primo avvio in produzione.
|
||||
* È lo stesso difetto già visto una volta in questo ramo — «una funzione corretta che
|
||||
* nessuno chiama» — e la correzione è la stessa: agganciarla dove non si può dimenticare.
|
||||
*
|
||||
* Entrambe le semine sono `INSERT OR REPLACE`, quindi rilanciarle è a vuoto; e il
|
||||
* registro è **dato di prodotto** (le domande e i pesi decisi con Nicola), non dato di
|
||||
* un cliente: riallinearlo a ogni avvio è voluto, così una domanda modificata in
|
||||
* `registro.ts` entra col rilascio invece che con una migrazione a mano.
|
||||
* ⚠️ I punteggi già emessi non cambiano: sono congelati in `score` e il referto legge
|
||||
* quelli, mai un ricalcolo (vedi `refertoDi`).
|
||||
*/
|
||||
export function createLongevityDb(path?: string): Database.Database {
|
||||
return apri(path ?? process.env.LONGEVITY_DB_PATH ?? 'data/longevity.db', SCHEMA_LONGEVITY);
|
||||
const db = apri(path ?? process.env.LONGEVITY_DB_PATH ?? 'data/longevity.db', SCHEMA_LONGEVITY);
|
||||
seedRegistro(db);
|
||||
seedPesi(db, MODEL_VERSION);
|
||||
return db;
|
||||
}
|
||||
|
||||
export function createIdentityDb(path?: string): Database.Database {
|
||||
|
||||
@@ -34,13 +34,20 @@ describe('utenti e sessioni di quest\'app', () => {
|
||||
for (const finto of ['campus', 'superuser', 'user', 'Admin', '', 'cliente ']) expect(isRole(finto)).toBe(false);
|
||||
});
|
||||
|
||||
it('landingFor manda ognuno a casa propria, e nessuno su /admin', () => {
|
||||
expect(landingFor('cliente')).toBe('/longevity/io');
|
||||
expect(landingFor('trainer')).toBe('/longevity/gestionale');
|
||||
expect(landingFor('piattaforme')).toBe('/piattaforme');
|
||||
expect(landingFor('admin')).toBe('/piattaforme');
|
||||
it('dopo il login si atterra sulla scelta della piattaforma, qualunque sia il ruolo', () => {
|
||||
// Deciso da Adriano il 03/09/2026: si entra e si sceglie, anche quando la scelta e' una
|
||||
// sola. Prima ogni ruolo saltava direttamente nella sua sezione.
|
||||
for (const ruolo of ROLES) expect(landingFor(ruolo)).toBe('/piattaforme');
|
||||
for (const ruolo of ROLES) expect(landingFor(ruolo)).not.toMatch(/^\/admin/);
|
||||
});
|
||||
|
||||
it('⚠️ la pagina di atterraggio è aperta a TUTTI i ruoli, o è un ciclo di redirect', () => {
|
||||
// landingFor e' anche il "torna a casa" del middleware quando un ruolo apre una sezione
|
||||
// che non gli spetta: se la sua destinazione fosse a sua volta negata, il middleware
|
||||
// rimanderebbe li' all'infinito. E' il guasto che il sito ha rischiato lo stesso giorno
|
||||
// con gli account emigrati, in un punto diverso del codice.
|
||||
for (const ruolo of ROLES) expect(canAccessPath(ruolo, landingFor(ruolo))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canAccessPath — le tre sezioni', () => {
|
||||
@@ -65,6 +72,8 @@ describe('canAccessPath — le tre sezioni', () => {
|
||||
expect(canAccessPath('cliente', '/api/longevity/gestionale/misure')).toBe(false);
|
||||
expect(canAccessPath('cliente', '/campus')).toBe(false);
|
||||
expect(canAccessPath('cliente', '/piattaforme/stress-index')).toBe(false);
|
||||
// ...ma l'indice sì: è la pagina di scelta, ed è la sua casa dopo il login.
|
||||
expect(canAccessPath('cliente', '/piattaforme')).toBe(true);
|
||||
});
|
||||
|
||||
it('trainer: entrambe le parti di Longevity, niente Campus né Stress Index', () => {
|
||||
@@ -73,6 +82,7 @@ describe('canAccessPath — le tre sezioni', () => {
|
||||
expect(canAccessPath('trainer', '/api/longevity/gestionale/misure')).toBe(true);
|
||||
expect(canAccessPath('trainer', '/campus')).toBe(false);
|
||||
expect(canAccessPath('trainer', '/piattaforme/stress-index')).toBe(false);
|
||||
expect(canAccessPath('trainer', '/piattaforme')).toBe(true);
|
||||
});
|
||||
|
||||
it('admin passa sempre', () => {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Il registro dei test è dato di prodotto, non dato di un cliente: un database nuovo deve
|
||||
// nascere con dentro le domande e i pesi. Fino al 03/09/2026 seedRegistro e seedPesi erano
|
||||
// chiamati SOLO dai test, quindi ogni suite passava e il primo avvio vero apriva un
|
||||
// questionario senza domande. Questo test guarda il database come lo trova l'applicazione.
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createLongevityDb } from '../../src/lib/longevity/db';
|
||||
import { campiDelQuestionario } from '../../src/lib/longevity/vista';
|
||||
import { MODEL_VERSION, seedRegistro, seedPesi } from '../../src/lib/longevity/registro';
|
||||
|
||||
const OGGI = new Date().toISOString().slice(0, 10);
|
||||
|
||||
describe('un database longevity appena creato', () => {
|
||||
it('ha il registro dei test già seminato, non vuoto', () => {
|
||||
const db = createLongevityDb(':memory:');
|
||||
const n = (db.prepare('SELECT COUNT(*) AS n FROM registro_test').get() as { n: number }).n;
|
||||
expect(n).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('mostra le domande del questionario senza che nessuno semini a mano', () => {
|
||||
const db = createLongevityDb(':memory:');
|
||||
const campi = campiDelQuestionario(db, OGGI);
|
||||
expect(campi.length).toBeGreaterThan(0);
|
||||
// I tre sotto-domini che il questionario alimenta oggi: se un giorno il registro
|
||||
// perdesse una famiglia di domande, il referto continuerebbe a "funzionare" con un
|
||||
// asse in meno e nessuno se ne accorgerebbe.
|
||||
const sottoDomini = new Set(campi.map((c) => c.sottoDominio));
|
||||
expect(sottoDomini).toContain('questionario_sonno');
|
||||
expect(sottoDomini).toContain('questionario_energia_stress');
|
||||
expect(sottoDomini).toContain('questionario_lifestyle');
|
||||
});
|
||||
|
||||
it('ha i pesi del modello corrente, per assi e macro', () => {
|
||||
const db = createLongevityDb(':memory:');
|
||||
const perLivello = db.prepare(
|
||||
'SELECT livello, COUNT(*) AS n FROM pesi WHERE model_version = ? GROUP BY livello'
|
||||
).all(MODEL_VERSION) as { livello: string; n: number }[];
|
||||
const mappa = Object.fromEntries(perLivello.map((r) => [r.livello, r.n]));
|
||||
expect(mappa.asse).toBeGreaterThan(0);
|
||||
expect(mappa.macro).toBeGreaterThan(0);
|
||||
expect(mappa.fitness_age).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('rilanciare la creazione non duplica niente (INSERT OR REPLACE)', () => {
|
||||
const db = createLongevityDb(':memory:');
|
||||
const prima = (db.prepare('SELECT COUNT(*) AS n FROM registro_test').get() as { n: number }).n;
|
||||
// Stessa semina, stesso database: è ciò che accade a ogni riavvio del container.
|
||||
seedRegistro(db); seedPesi(db, MODEL_VERSION);
|
||||
const dopo = (db.prepare('SELECT COUNT(*) AS n FROM registro_test').get() as { n: number }).n;
|
||||
expect(dopo).toBe(prima);
|
||||
});
|
||||
});
|
||||
@@ -32,7 +32,11 @@ services:
|
||||
- traefik.http.routers.insanitylab.tls.certresolver=mytlschallenge
|
||||
- traefik.http.services.insanitylab.loadbalancer.server.port=4321
|
||||
# Router redirect: www e vecchio sottodominio tielogic.xyz → 301 apex
|
||||
- traefik.http.routers.insanitylab-redir.rule=Host(`www.insanitylab.it`) || Host(`insanitylab.tielogic.xyz`)
|
||||
# ⚠️ `insanitylab.tielogic.xyz` e' stato tolto dal DNS il 03/09/2026. Nominare in una
|
||||
# regola un host che non risolve non e' inerte: il router chiede un certificato per i
|
||||
# nomi della sua regola, e la sfida ACME fallisce a ogni tentativo — cio' che Let's
|
||||
# Encrypt limita. Resta il solo redirect di `www`.
|
||||
- traefik.http.routers.insanitylab-redir.rule=Host(`www.insanitylab.it`)
|
||||
- traefik.http.routers.insanitylab-redir.tls=true
|
||||
- traefik.http.routers.insanitylab-redir.entrypoints=websecure
|
||||
- traefik.http.routers.insanitylab-redir.tls.certresolver=mytlschallenge
|
||||
|
||||
@@ -12,7 +12,10 @@ const path = Astro.url.pathname;
|
||||
// ⚠️ Le aree riservate (Campus, Stress Index, Longevity) dal 03/09/2026 NON sono in questo
|
||||
// sito: vivono su apps.insanitylab.it, con utenti e sessioni propri. Qui resta un link che
|
||||
// esce, non un menu — questo sito non sa più chi possa aprirle, ed è la separazione voluta.
|
||||
const AREA_RISERVATA = 'https://apps.insanitylab.it';
|
||||
// ⚠️ L'indirizzo vero, oggi: apps.insanitylab.it NON esiste (manca il record su Aruba) e
|
||||
// puntarci scriverebbe nel menu del sito un link morto. Quando quel record ci sara', qui
|
||||
// si cambia una riga — ed e' l'unico posto del sito che nomina l'area riservata.
|
||||
const AREA_RISERVATA = 'https://piattaforme.tielogic.xyz';
|
||||
const areaHref = user ? landingFor(user.role) : '/admin';
|
||||
---
|
||||
<header class="hdr" id="site-header">
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Il sito nomina l'area riservata in UN posto solo (l'header). Dopo la separazione quel
|
||||
// link esce dal dominio, quindi non lo protegge piu' nessuna rotta interna: se punta a un
|
||||
// host che non esiste, il menu del sito porta a un errore di DNS e nessun test se ne
|
||||
// accorge. Qui si verifica che sia l'host in uso, non quello previsto per il futuro.
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const header = readFileSync(join(process.cwd(), 'src/components/Header.astro'), 'utf8');
|
||||
|
||||
describe("il link all'area riservata", () => {
|
||||
it('punta a un host che esiste davvero oggi', () => {
|
||||
expect(header).toContain('https://piattaforme.tielogic.xyz');
|
||||
// apps.insanitylab.it e' l'indirizzo definitivo, ma il record A su Aruba non c'e'
|
||||
// ancora: finche' non c'e', qui sarebbe un link morto dentro al menu del sito.
|
||||
expect(header).not.toContain('https://apps.insanitylab.it');
|
||||
});
|
||||
|
||||
it('non prova piu a costruire un menu delle piattaforme dentro al sito', () => {
|
||||
// Le tre piattaforme non vivono piu' qui: un menu che le elenca sarebbe un elenco di
|
||||
// rotte inesistenti, servite dal sito come 404.
|
||||
expect(header).not.toContain("data/piattaforme");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user