registri dei piani in git (AI-OS#309)
Refs gitea:Adriano/AI-OS#310 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
# Review package: 5cea895..e87a464
|
||||
|
||||
## Commits
|
||||
e87a464 feat(auth): matrice permessi 3 ruoli + landingFor
|
||||
|
||||
## Files changed
|
||||
src/lib/auth.ts | 24 +++++++++++++++----
|
||||
tests/roles.test.ts | 68 ++++++++++++++++++++++++++++++++++++-----------------
|
||||
2 files changed, 67 insertions(+), 25 deletions(-)
|
||||
|
||||
## Diff
|
||||
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
|
||||
@@ -1,18 +1,18 @@
|
||||
import type Database from 'better-sqlite3';
|
||||
import bcrypt from 'bcryptjs';
|
||||
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);
|
||||
}
|
||||
|
||||
export function verifyPassword(plain: string, hash: string): boolean {
|
||||
return bcrypt.compareSync(plain, hash);
|
||||
}
|
||||
|
||||
export function createUser(db: Database.Database, username: string, password: string, role: Role = 'admin'): number {
|
||||
@@ -42,17 +42,33 @@ export function getSessionUser(db: Database.Database, token: string): { id: numb
|
||||
db.prepare('DELETE FROM sessions WHERE token = ?').run(token);
|
||||
return null;
|
||||
}
|
||||
return { id: row.id, username: row.username, role: row.role };
|
||||
}
|
||||
|
||||
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' });
|
||||
+ 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
|
||||
+ });
|
||||
+});
|
||||
|
||||
- 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);
|
||||
+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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user