feat: ruoli admin/editor in auth e create-user
This commit is contained in:
+11
-6
@@ -3,9 +3,13 @@ import bcrypt from 'bcryptjs';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
|
|
||||||
const [, , username, password] = process.argv;
|
const [, , username, password, role = 'admin'] = process.argv;
|
||||||
if (!username || !password) {
|
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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const path = process.env.DB_PATH ?? 'data/insanitylab.db';
|
const path = process.env.DB_PATH ?? 'data/insanitylab.db';
|
||||||
@@ -14,10 +18,11 @@ const db = new Database(path);
|
|||||||
db.exec(`CREATE TABLE IF NOT EXISTS users (
|
db.exec(`CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
username TEXT NOT NULL UNIQUE,
|
username TEXT NOT NULL UNIQUE,
|
||||||
password_hash TEXT NOT NULL
|
password_hash TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL DEFAULT 'admin'
|
||||||
);`);
|
);`);
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT INTO users (username, password_hash) VALUES (?, ?)
|
`INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)
|
||||||
ON CONFLICT(username) DO UPDATE SET password_hash = excluded.password_hash`
|
ON CONFLICT(username) DO UPDATE SET password_hash = excluded.password_hash, role = excluded.role`
|
||||||
).run(username, bcrypt.hashSync(password, 12));
|
).run(username, bcrypt.hashSync(password, 12), role);
|
||||||
console.log(`Utente "${username}" creato/aggiornato in ${path}`);
|
console.log(`Utente "${username}" creato/aggiornato in ${path}`);
|
||||||
|
|||||||
Vendored
+1
-1
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
declare namespace App {
|
declare namespace App {
|
||||||
interface Locals {
|
interface Locals {
|
||||||
user?: { id: number; username: string };
|
user?: { id: number; username: string; role: string };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-7
@@ -5,6 +5,8 @@ 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 function hashPassword(plain: string): string {
|
export function hashPassword(plain: string): string {
|
||||||
return bcrypt.hashSync(plain, 12);
|
return bcrypt.hashSync(plain, 12);
|
||||||
}
|
}
|
||||||
@@ -13,9 +15,9 @@ export function verifyPassword(plain: string, hash: string): boolean {
|
|||||||
return bcrypt.compareSync(plain, hash);
|
return bcrypt.compareSync(plain, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createUser(db: Database.Database, username: string, password: string): number {
|
export function createUser(db: Database.Database, username: string, password: string, role: Role = 'admin'): number {
|
||||||
const res = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)')
|
const res = db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)')
|
||||||
.run(username, hashPassword(password));
|
.run(username, hashPassword(password), role);
|
||||||
return Number(res.lastInsertRowid);
|
return Number(res.lastInsertRowid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,19 +32,27 @@ export function login(db: Database.Database, username: string, password: string)
|
|||||||
return token;
|
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(
|
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 = ?`
|
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) return null;
|
||||||
if (row.expires_at <= new Date().toISOString().slice(0, 19).replace('T', ' ')) {
|
if (row.expires_at <= new Date().toISOString().slice(0, 19).replace('T', ' ')) {
|
||||||
db.prepare('DELETE FROM sessions WHERE token = ?').run(token);
|
db.prepare('DELETE FROM sessions WHERE token = ?').run(token);
|
||||||
return null;
|
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 {
|
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.
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user