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:
2026-10-06 10:36:26 +02:00
parent 9126af28ce
commit b913a2bf90
94 changed files with 40294 additions and 0 deletions
@@ -0,0 +1,179 @@
# Review package: c7d9d4b..274a6c7
## Commits
274a6c7 fix: regex percorsi editor senza bypass prefisso
77041f9 feat: ruoli admin/editor in auth e create-user
## Files changed
scripts/create-user.mjs | 17 +++++++++++------
src/env.d.ts | 2 +-
src/lib/auth.ts | 24 +++++++++++++++++-------
tests/roles.test.ts | 37 +++++++++++++++++++++++++++++++++++++
4 files changed, 66 insertions(+), 14 deletions(-)
## Diff
diff --git a/scripts/create-user.mjs b/scripts/create-user.mjs
index 5bd0e67..8568b66 100644
--- a/scripts/create-user.mjs
+++ b/scripts/create-user.mjs
@@ -1,23 +1,28 @@
import Database from 'better-sqlite3';
import bcrypt from 'bcryptjs';
import { mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
-const [, , username, password] = process.argv;
+const [, , username, password, role = 'admin'] = process.argv;
if (!username || !password) {
- console.error('Uso: npm run create-user -- <username> <password>');
+ console.error('Uso: npm run create-user -- <username> <password> [role]');
+ process.exit(1);
+}
+if (!['admin', 'editor'].includes(role)) {
+ console.error('Ruolo non valido. Ammessi: admin, editor');
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
+ password_hash TEXT NOT NULL,
+ role TEXT NOT NULL DEFAULT 'admin'
);`);
db.prepare(
- `INSERT INTO users (username, password_hash) VALUES (?, ?)
- ON CONFLICT(username) DO UPDATE SET password_hash = excluded.password_hash`
-).run(username, bcrypt.hashSync(password, 12));
+ `INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)
+ ON CONFLICT(username) DO UPDATE SET password_hash = excluded.password_hash, role = excluded.role`
+).run(username, bcrypt.hashSync(password, 12), role);
console.log(`Utente "${username}" creato/aggiornato in ${path}`);
diff --git a/src/env.d.ts b/src/env.d.ts
index 81f5f4a..6b05ca0 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -1,7 +1,7 @@
/// <reference types="astro/client" />
declare namespace App {
interface Locals {
- user?: { id: number; username: string };
+ user?: { id: number; username: string; role: string };
}
}
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index 1946702..2dea325 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -1,48 +1,58 @@
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 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): number {
- const res = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)')
- .run(username, hashPassword(password));
+export function createUser(db: Database.Database, username: string, password: string, role: Role = 'admin'): number {
+ const res = db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)')
+ .run(username, hashPassword(password), role);
return Number(res.lastInsertRowid);
}
export function login(db: Database.Database, username: string, password: string): string | null {
const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username) as
| { id: number; password_hash: string } | undefined;
if (!user || !verifyPassword(password, user.password_hash)) return null;
const token = randomBytes(32).toString('hex');
db.prepare(
`INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, datetime('now', '+${SESSION_DAYS} days'))`
).run(token, user.id);
return token;
}
-export function getSessionUser(db: Database.Database, token: string): { id: number; username: string } | null {
+export function getSessionUser(db: Database.Database, token: string): { id: number; username: string; role: string } | null {
const row = db.prepare(
- `SELECT s.token, s.expires_at, u.id, u.username
+ `SELECT s.token, s.expires_at, u.id, u.username, u.role
FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ?`
- ).get(token) as { token: string; expires_at: string; id: number; username: string } | undefined;
+ ).get(token) as { token: string; expires_at: string; id: number; username: string; role: string } | undefined;
if (!row) return null;
if (row.expires_at <= new Date().toISOString().slice(0, 19).replace('T', ' ')) {
db.prepare('DELETE FROM sessions WHERE token = ?').run(token);
return null;
}
- return { id: row.id, username: row.username };
+ 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$/];
+
+export function canAccessAdminPath(role: string, pathname: string): boolean {
+ if (role === 'admin') return true;
+ return EDITOR_ALLOWED.some((re) => re.test(pathname));
+}
diff --git a/tests/roles.test.ts b/tests/roles.test.ts
new file mode 100644
index 0000000..af5c1d9
--- /dev/null
+++ b/tests/roles.test.ts
@@ -0,0 +1,37 @@
+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';
+
+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 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);
+ });
+});