# Review package: d4cf3598059fb57d39289d363218202f4921ed28..HEAD ## Commits e2bbc90 fix(create-user): ruoli aggiornati a superuser/user (gap dal piano) be77285 test(smoke): login pubblico, badge, protezione /admin/users c36ad4c feat(users): gestione utenti /admin/users con guardie self/ultimo-admin 0f2b2ba feat(blog): firma autore sotto il titolo, fallback Staff InsanityLab 2bf6b3d feat(admin): nav per 3 ruoli, blog filtrato per autore + colonna autore 22d72be feat(inline): badge con guid + modal di modifica contenuti in pagina 8423716 feat(showtags): endpoint cookie + toggle nel menu utente 71cd162 feat(header): identità utente SSR, link Accedi / menu utente 6b85872 feat(login): pagina pubblica /login con next sicuro 5ce10da feat(content): guid in resolve/componenti + GET /api/admin/content/[tag] c3267c9 feat(posts-api): assegna autore in POST, ownership su PUT/DELETE 6d1e260 feat(posts): author_id nel repository + listByAuthor + helper utenti db92fc9 feat(middleware): autorizzazione a matrice + redirect per ruolo e87a464 feat(auth): matrice permessi 3 ruoli + landingFor 5cea895 feat(db): migrazioni ruoli editor->superuser, guid contenuti, posts.author_id ## Files changed scripts/create-user.mjs | 4 +- scripts/smoke.sh | 20 +++ src/components/Header.astro | 47 +++++++ src/components/content/T.astro | 4 +- src/components/content/TImg.astro | 10 +- src/layouts/Admin.astro | 13 +- src/layouts/Base.astro | 169 +++++++++++++++++++---- src/lib/auth.ts | 64 ++++++++- src/lib/content.ts | 21 +-- src/lib/db.ts | 27 +++- src/lib/posts.ts | 19 ++- src/lib/safe-next.ts | 7 + src/middleware.ts | 4 +- src/pages/admin/index.astro | 11 +- src/pages/admin/users.astro | 95 +++++++++++++ src/pages/api/admin/content/[tag].ts | 15 ++ src/pages/api/admin/posts/[id].ts | 16 ++- src/pages/api/admin/posts/index.ts | 4 +- src/pages/api/admin/showtags.ts | 18 +++ src/pages/api/admin/users/[id]/index.ts | 19 +++ src/pages/api/admin/users/[id]/reset-password.ts | 16 +++ src/pages/api/admin/users/[id]/role.ts | 21 +++ src/pages/api/admin/users/index.ts | 26 ++++ src/pages/blog/[slug].astro | 5 +- src/pages/login.astro | 53 +++++++ tests/content-api.test.ts | 13 ++ tests/content-db.test.ts | 29 ++++ tests/content.test.ts | 8 +- tests/posts-ownership.test.ts | 21 +++ tests/posts.test.ts | 27 +++- tests/roles.test.ts | 68 ++++++--- tests/safe-next.test.ts | 18 +++ tests/users.test.ts | 49 +++++++ 33 files changed, 847 insertions(+), 94 deletions(-) ## Diff diff --git a/scripts/create-user.mjs b/scripts/create-user.mjs index 8568b66..4690813 100644 --- a/scripts/create-user.mjs +++ b/scripts/create-user.mjs @@ -1,22 +1,22 @@ import Database from 'better-sqlite3'; import bcrypt from 'bcryptjs'; import { mkdirSync } from 'node:fs'; import { dirname } from 'node:path'; const [, , username, password, role = 'admin'] = process.argv; if (!username || !password) { console.error('Uso: npm run create-user -- [role]'); process.exit(1); } -if (!['admin', 'editor'].includes(role)) { - console.error('Ruolo non valido. Ammessi: admin, editor'); +if (!['admin', 'superuser', 'user'].includes(role)) { + console.error('Ruolo non valido. Ammessi: admin, superuser, user'); process.exit(1); } const path = process.env.DB_PATH ?? 'data/insanitylab.db'; mkdirSync(dirname(path), { recursive: true }); const db = new Database(path); db.exec(`CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'admin' diff --git a/scripts/smoke.sh b/scripts/smoke.sh index 226f29b..7db9da2 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -21,11 +21,31 @@ echo "$code /sitemap.xml" [ "$code" = "200" ] || fail=1 locs=$(curl -s http://localhost:4399/sitemap.xml | grep -c '') echo "$locs in sitemap (atteso >= 12)" [ "$locs" -ge 12 ] || fail=1 tags=$(curl -s http://localhost:4399/ | grep -c 'data-tag=') echo "$tags righe con data-tag= in home (atteso >= 1)" [ "$tags" -ge 1 ] || fail=1 badges=$(curl -s 'http://localhost:4399/?annotate=1' | grep -c 'tag-badge' || true) echo "$badges tag-badge da anonimo con ?annotate=1 (atteso 0)" [ "$badges" = "0" ] || fail=1 + +# /login pubblico raggiungibile +code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:4399/login) +echo "$code /login" +[ "$code" = "200" ] || fail=1 + +# Header mostra "Accedi" da anonimo +curl -s http://localhost:4399/ | grep -q 'Accedi' || { echo "FAIL: link Accedi assente"; fail=1; } + +# Nessun badge tag-badge da anonimo sulla home semplice +badges2=$(curl -s http://localhost:4399/ | grep -c 'tag-badge' || true) +echo "$badges2 tag-badge da anonimo in home semplice (atteso 0)" +[ "$badges2" = "0" ] || fail=1 + +# Rotta gestione utenti protetta (redirect al login da anonimo) +code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:4399/admin/users) +echo "$code /admin/users (atteso 302)" +[ "$code" = "302" ] || fail=1 + +echo "smoke ruoli/login/inline OK" exit $fail diff --git a/src/components/Header.astro b/src/components/Header.astro index 33a0b4c..bfcbcdc 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -1,23 +1,47 @@ --- import { site } from '../data/site'; import { t, contentImageUrl } from '../lib/content'; +interface Props { + user: { username: string; role: string } | null; + showTags: boolean; + canTags: boolean; +} +const { user, canTags, showTags } = Astro.props; const path = Astro.url.pathname; +const loginHref = `/login?next=${encodeURIComponent(path)}`; +const areaHref = user?.role === 'superuser' ? '/admin/content' : '/admin'; --- diff --git a/src/components/content/T.astro b/src/components/content/T.astro index 41cefd3..b11d23e 100644 --- a/src/components/content/T.astro +++ b/src/components/content/T.astro @@ -1,11 +1,11 @@ --- import { resolveContent } from '../../lib/content'; interface Props { tag: string; as?: string; class?: string } const { tag, as = 'span', class: cls } = Astro.props; const Tag = as; const c = resolveContent(tag); const classes = [cls, c.classes].filter(Boolean).join(' ') || undefined; --- {c.type === 'html' - ? - : {c.value}} + ? + : {c.value}} diff --git a/src/components/content/TImg.astro b/src/components/content/TImg.astro index 3fcb915..b1f6ac3 100644 --- a/src/components/content/TImg.astro +++ b/src/components/content/TImg.astro @@ -1,14 +1,16 @@ --- import { Image } from 'astro:assets'; import type { ImageMetadata } from 'astro'; -import { getImageOverride } from '../../lib/content'; +import { resolveContent } from '../../lib/content'; interface Props { tag: string; src: ImageMetadata; alt: string; class?: string; loading?: 'lazy' | 'eager'; widths?: number[]; sizes?: string; height?: number; } const { tag, src, alt, class: cls, ...rest } = Astro.props; -const override = getImageOverride(tag); +const c = resolveContent(tag); +const override = c.type === 'image' && c.value ? `/uploads/${c.value}` : null; +const guid = c.guid || undefined; --- {override - ? {alt} - : {alt}} + ? {alt} + : {alt}} diff --git a/src/layouts/Admin.astro b/src/layouts/Admin.astro index e61622f..ab98680 100644 --- a/src/layouts/Admin.astro +++ b/src/layouts/Admin.astro @@ -24,17 +24,26 @@ const user = Astro.locals.user; .pill--draft { background: #f3d9a4; } .form-msg { font-size: .9rem; margin-top: 8px; } .form-msg--ok { color: #3c7a3c; } .form-msg--err { color: #b03030; }
InsanityLab · Admin - {user && user.role === 'editor' && <>{`Ciao, ${user.username}`} Contenuti Esci} - {user && user.role !== 'editor' && <>{`Ciao, ${user.username}`} Articoli Contenuti Nuovo Esci Vedi sito ↗} + {user && ( + <> + {`Ciao, ${user.username}`} + Articoli + {(user.role === 'superuser' || user.role === 'admin') && Contenuti} + {user.role === 'admin' && Utenti} + Nuovo + Esci + Vedi sito ↗ + + )}
diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 365aabf..4e4263d 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -7,44 +7,43 @@ import '@fontsource/open-sans/400.css'; import '@fontsource/open-sans/600.css'; import '../styles/global.css'; import Header from '../components/Header.astro'; import Footer from '../components/Footer.astro'; import { getDb } from '../lib/db'; import { getSessionUser, SESSION_COOKIE } from '../lib/auth'; interface Props { title: string; description?: string } const { title, description = 'InsanityLab — Performance. Balance. Longevity. Allenamento personalizzato, piccoli gruppi e benessere a Bologna.' } = Astro.props; -// Overlay annotate: badge coi tag contenuto, solo per utenti loggati con ?annotate=1. -let annotate = false; -if (Astro.url.searchParams.get('annotate') === '1') { - const tok = Astro.cookies.get(SESSION_COOKIE)?.value; - annotate = Boolean(tok && getSessionUser(getDb(), tok)); -} +const tok = Astro.cookies.get(SESSION_COOKIE)?.value; +const user = tok ? getSessionUser(getDb(), tok) : null; +const canTags = !!user && (user.role === 'superuser' || user.role === 'admin'); +const wantTags = Astro.cookies.get('showtags')?.value === '1' || Astro.url.searchParams.get('annotate') === '1'; +const showTags = canTags && wantTags; --- {title} — InsanityLab -
+