e2bd1d95ed
Audit di sicurezza su tutta l'app. Report in .planning/SECURITY-SCAN.md (codice), .planning/SECURITY-AUDIT-INFRA.md (dipendenze/segreti/deploy) e piano in .planning/SECURITY-REMEDIATION-PLAN.md. CRITICO — l'autorizzazione admin era un unico punto di rottura: nessuna delle 21 pagine /admin controllava la sessione e admin/layout.tsx renderizzava comunque i figli quando mancava. L'unico guard era proxy.ts, su un Next.js affetto da GHSA-6gpp-xcg3-4w24 (proxy bypass). Ora il layout è un secondo gate indipendente; proxy.ts marca il path con un token derivato da NEXTAUTH_SECRET, così il gate non è aggirabile forgiando header e fallisce chiuso se il proxy non gira. ALTO — gli slug cliente avevano 4 caratteri casuali da Math.random() (~20 bit, 1.7M tentativi) e risolvono prima del token: ora 12 caratteri via nanoid (CSPRNG, ~62 bit). Aggiunto rate limit al ramo /client/, che ne era privo. ALTO — src/lib/quote-actions.ts esponeva due server action pubbliche senza autenticazione, una delle quali scriveva su DB. Codice morto, zero chiamanti: rimosso. MEDIO — i quattro dangerouslySetInnerHTML nelle sezioni proposta rendevano output AI come HTML grezzo su pagina pubblica, alimentato da transcript di terzi. Sostituiti con RichText (whitelist di emphasis, nessun HTML al DOM). I transcript ora sono recintati in tag che il system prompt dichiara essere dati, non istruzioni. Inoltre: next 16.2.6 -> 16.2.12 e next-auth 4.24.14 -> 4.24.15 (chiude 9 CVE Next piu GHSA-xmf8-cvqr-rfgj su getToken, raggiungibile dal proxy); HSTS e CSP; potatura della Map di rate-limit.ts, che cresceva senza limite; espunta la password Postgres di produzione dai due 07-01-SUMMARY.md. Verificato: tsc pulito, build OK, smoke test su login/redirect/header forgiati. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
37 lines
1.4 KiB
TypeScript
37 lines
1.4 KiB
TypeScript
import type { SolutionNode } from "@/lib/proposal/schema";
|
||
import { RichText } from "@/components/public/proposal/RichText";
|
||
|
||
type Props = { solution: SolutionNode };
|
||
|
||
export function SolutionNodeSection({ solution }: Props) {
|
||
return (
|
||
<div className="h-full flex flex-col justify-center px-16 py-24 space-y-8 max-w-4xl">
|
||
<div>
|
||
<p className="text-xs font-mono tracking-widest text-primary uppercase">
|
||
{solution.subtitle}
|
||
</p>
|
||
<h2 className="text-5xl font-light text-foreground mt-4 leading-tight">
|
||
{solution.title}
|
||
</h2>
|
||
</div>
|
||
|
||
<div className="border border-primary/20 bg-primary/5 rounded-xl p-6 space-y-2">
|
||
<p className="text-xs font-mono tracking-widest text-primary uppercase">La trasformazione</p>
|
||
<p className="text-sm text-foreground leading-relaxed">{solution.transformation}</p>
|
||
</div>
|
||
|
||
<div className="border border-border rounded-xl p-6 space-y-3">
|
||
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Attraverso cosa</p>
|
||
<ul className="space-y-2">
|
||
{solution.throughWhat.map((item, i) => (
|
||
<li key={i} className="flex items-start gap-2 text-sm text-foreground">
|
||
<span className="text-primary mt-0.5">›</span>
|
||
<RichText>{item}</RichText>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|