feat(auth): matrice permessi 3 ruoli + landingFor

This commit is contained in:
2026-07-05 21:44:30 +02:00
parent 5cea895d51
commit e87a464090
2 changed files with 69 additions and 27 deletions
+20 -4
View File
@@ -5,7 +5,7 @@ import { randomBytes } from 'node:crypto';
export const SESSION_COOKIE = 'session'; export const SESSION_COOKIE = 'session';
const SESSION_DAYS = 7; const SESSION_DAYS = 7;
export type Role = 'admin' | 'editor'; export type Role = 'admin' | 'superuser' | 'user';
export function hashPassword(plain: string): string { export function hashPassword(plain: string): string {
return bcrypt.hashSync(plain, 12); 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); db.prepare('DELETE FROM sessions WHERE token = ?').run(token);
} }
// Percorsi admin consentiti anche al ruolo editor. // Autorizzazione per prefisso di rotta. L'admin passa sempre; per gli altri, la prima regola
const EDITOR_ALLOWED = [/^\/admin\/content(\/|$)/, /^\/api\/admin\/content(\/|$)/, /^\/admin\/logout$/]; // 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 { export function canAccessAdminPath(role: string, pathname: string): boolean {
if (role === 'admin') return true; 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';
} }
+49 -23
View File
@@ -1,37 +1,63 @@
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
import type Database from 'better-sqlite3'; import type Database from 'better-sqlite3';
import { createDb } from '../src/lib/db'; 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; let db: Database.Database;
beforeEach(() => { db = createDb(':memory:'); }); beforeEach(() => { db = createDb(':memory:'); });
describe('ruoli', () => { describe('ruoli', () => {
it('createUser accetta ruolo editor e getSessionUser lo ritorna', () => { it('createUser accetta i tre ruoli e getSessionUser li ritorna', () => {
createUser(db, 'cliente', 'segreta123', 'editor'); createUser(db, 'u', 'segreta123', 'user');
const token = login(db, 'cliente', 'segreta123')!; createUser(db, 's', 'segreta123', 'superuser');
expect(getSessionUser(db, token)).toMatchObject({ username: 'cliente', role: 'editor' }); 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', () => { it('createUser senza ruolo crea admin', () => {
createUser(db, 'adriano', 'segreta123'); createUser(db, 'adriano', 'segreta123');
const token = login(db, 'adriano', 'segreta123')!; expect(getSessionUser(db, login(db, 'adriano', 'segreta123')!)).toMatchObject({ role: 'admin' });
expect(getSessionUser(db, token)).toMatchObject({ role: 'admin' }); });
}); });
it('admin accede a tutto, editor solo a contenuti e logout', () => { describe('canAccessAdminPath', () => {
expect(canAccessAdminPath('admin', '/admin')).toBe(true); it('admin accede a tutto', () => {
expect(canAccessAdminPath('admin', '/api/admin/posts')).toBe(true); for (const p of ['/admin', '/admin/users', '/admin/content', '/api/admin/posts', '/api/admin/users/1'])
expect(canAccessAdminPath('editor', '/admin/content')).toBe(true); expect(canAccessAdminPath('admin', p)).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); it('superuser: contenuti + blog sì, utenti no', () => {
expect(canAccessAdminPath('editor', '/admin/new')).toBe(false); expect(canAccessAdminPath('superuser', '/admin/content')).toBe(true);
expect(canAccessAdminPath('editor', '/api/admin/posts')).toBe(false); expect(canAccessAdminPath('superuser', '/admin/content/')).toBe(true);
expect(canAccessAdminPath('editor', '/api/admin/upload')).toBe(false); expect(canAccessAdminPath('superuser', '/api/admin/content/home.hero.title')).toBe(true);
expect(canAccessAdminPath('editor', '/admin/contentious')).toBe(false); expect(canAccessAdminPath('superuser', '/admin')).toBe(true);
expect(canAccessAdminPath('editor', '/admin/content-x')).toBe(false); expect(canAccessAdminPath('superuser', '/admin/new')).toBe(true);
expect(canAccessAdminPath('editor', '/admin/content')).toBe(true); expect(canAccessAdminPath('superuser', '/api/admin/posts')).toBe(true);
expect(canAccessAdminPath('editor', '/admin/content/')).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');
}); });
}); });