From e87a464090d8ec3ed5fc1a93524384addcb31ce5 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Sun, 5 Jul 2026 21:44:30 +0200 Subject: [PATCH] feat(auth): matrice permessi 3 ruoli + landingFor --- src/lib/auth.ts | 24 ++++++++++++--- tests/roles.test.ts | 72 ++++++++++++++++++++++++++++++--------------- 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 2dea325..cd10d1e 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -5,7 +5,7 @@ import { randomBytes } from 'node:crypto'; export const SESSION_COOKIE = 'session'; const SESSION_DAYS = 7; -export type Role = 'admin' | 'editor'; +export type Role = 'admin' | 'superuser' | 'user'; export function hashPassword(plain: string): string { return bcrypt.hashSync(plain, 12); @@ -49,10 +49,26 @@ export function logout(db: Database.Database, token: string): void { db.prepare('DELETE FROM sessions WHERE token = ?').run(token); } -// Percorsi admin consentiti anche al ruolo editor. -const EDITOR_ALLOWED = [/^\/admin\/content(\/|$)/, /^\/api\/admin\/content(\/|$)/, /^\/admin\/logout$/]; +// Autorizzazione per prefisso di rotta. L'admin passa sempre; per gli altri, la prima regola +// che matcha decide; se nessuna regola matcha la rotta è "blog/upload/logout" → consentita a +// qualsiasi loggato (il middleware protegge solo /admin e /api/admin, quindi qui arrivano solo +// utenti già autenticati). +const RULES: [RegExp, Role[]][] = [ + [/^\/admin\/users(\/|$)/, ['admin']], + [/^\/api\/admin\/users(\/|$)/, ['admin']], + [/^\/admin\/content(\/|$)/, ['admin', 'superuser']], + [/^\/api\/admin\/content(\/|$)/, ['admin', 'superuser']], +]; export function canAccessAdminPath(role: string, pathname: string): boolean { if (role === 'admin') return true; - return EDITOR_ALLOWED.some((re) => re.test(pathname)); + for (const [re, roles] of RULES) { + if (re.test(pathname)) return roles.includes(role as Role); + } + return true; // blog, upload, logout, showtags: tutti i loggati +} + +export function landingFor(role: string): string { + if (role === 'superuser') return '/admin/content'; + return '/admin'; } diff --git a/tests/roles.test.ts b/tests/roles.test.ts index af5c1d9..c14de01 100644 --- a/tests/roles.test.ts +++ b/tests/roles.test.ts @@ -1,37 +1,63 @@ import { describe, it, expect, beforeEach } from 'vitest'; import type Database from 'better-sqlite3'; import { createDb } from '../src/lib/db'; -import { createUser, login, getSessionUser, canAccessAdminPath } from '../src/lib/auth'; +import { createUser, login, getSessionUser, canAccessAdminPath, landingFor } from '../src/lib/auth'; let db: Database.Database; beforeEach(() => { db = createDb(':memory:'); }); describe('ruoli', () => { - it('createUser accetta ruolo editor e getSessionUser lo ritorna', () => { - createUser(db, 'cliente', 'segreta123', 'editor'); - const token = login(db, 'cliente', 'segreta123')!; - expect(getSessionUser(db, token)).toMatchObject({ username: 'cliente', role: 'editor' }); + it('createUser accetta i tre ruoli e getSessionUser li ritorna', () => { + createUser(db, 'u', 'segreta123', 'user'); + createUser(db, 's', 'segreta123', 'superuser'); + expect(getSessionUser(db, login(db, 'u', 'segreta123')!)).toMatchObject({ role: 'user' }); + expect(getSessionUser(db, login(db, 's', 'segreta123')!)).toMatchObject({ role: 'superuser' }); }); it('createUser senza ruolo crea admin', () => { createUser(db, 'adriano', 'segreta123'); - const token = login(db, 'adriano', 'segreta123')!; - expect(getSessionUser(db, token)).toMatchObject({ role: 'admin' }); - }); - - it('admin accede a tutto, editor solo a contenuti e logout', () => { - expect(canAccessAdminPath('admin', '/admin')).toBe(true); - expect(canAccessAdminPath('admin', '/api/admin/posts')).toBe(true); - expect(canAccessAdminPath('editor', '/admin/content')).toBe(true); - expect(canAccessAdminPath('editor', '/api/admin/content/home.hero.title')).toBe(true); - expect(canAccessAdminPath('editor', '/admin/logout')).toBe(true); - expect(canAccessAdminPath('editor', '/admin')).toBe(false); - expect(canAccessAdminPath('editor', '/admin/new')).toBe(false); - expect(canAccessAdminPath('editor', '/api/admin/posts')).toBe(false); - expect(canAccessAdminPath('editor', '/api/admin/upload')).toBe(false); - expect(canAccessAdminPath('editor', '/admin/contentious')).toBe(false); - expect(canAccessAdminPath('editor', '/admin/content-x')).toBe(false); - expect(canAccessAdminPath('editor', '/admin/content')).toBe(true); - expect(canAccessAdminPath('editor', '/admin/content/')).toBe(true); + expect(getSessionUser(db, login(db, 'adriano', 'segreta123')!)).toMatchObject({ role: 'admin' }); + }); +}); + +describe('canAccessAdminPath', () => { + it('admin accede a tutto', () => { + for (const p of ['/admin', '/admin/users', '/admin/content', '/api/admin/posts', '/api/admin/users/1']) + expect(canAccessAdminPath('admin', p)).toBe(true); + }); + + it('superuser: contenuti + blog sì, utenti no', () => { + expect(canAccessAdminPath('superuser', '/admin/content')).toBe(true); + expect(canAccessAdminPath('superuser', '/admin/content/')).toBe(true); + expect(canAccessAdminPath('superuser', '/api/admin/content/home.hero.title')).toBe(true); + expect(canAccessAdminPath('superuser', '/admin')).toBe(true); + expect(canAccessAdminPath('superuser', '/admin/new')).toBe(true); + expect(canAccessAdminPath('superuser', '/api/admin/posts')).toBe(true); + expect(canAccessAdminPath('superuser', '/admin/users')).toBe(false); + expect(canAccessAdminPath('superuser', '/api/admin/users/1')).toBe(false); + }); + + it('user: blog sì, contenuti e utenti no', () => { + expect(canAccessAdminPath('user', '/admin')).toBe(true); + expect(canAccessAdminPath('user', '/admin/new')).toBe(true); + expect(canAccessAdminPath('user', '/api/admin/posts')).toBe(true); + expect(canAccessAdminPath('user', '/api/admin/upload')).toBe(true); + expect(canAccessAdminPath('user', '/admin/logout')).toBe(true); + expect(canAccessAdminPath('user', '/admin/content')).toBe(false); + expect(canAccessAdminPath('user', '/api/admin/content/x')).toBe(false); + expect(canAccessAdminPath('user', '/admin/users')).toBe(false); + }); + + it('la matrice non confonde prefissi simili', () => { + expect(canAccessAdminPath('user', '/admin/contentious')).toBe(true); // NON è /admin/content + expect(canAccessAdminPath('superuser', '/admin/users-x')).toBe(true); // NON è /admin/users + }); +}); + +describe('landingFor', () => { + it('instrada ogni ruolo alla sua home', () => { + expect(landingFor('superuser')).toBe('/admin/content'); + expect(landingFor('user')).toBe('/admin'); + expect(landingFor('admin')).toBe('/admin'); }); });