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,177 @@
|
||||
# Review package: d3b5e67..HEAD
|
||||
|
||||
## Commits
|
||||
6caba6f feat: autenticazione a sessioni, middleware admin e script create-user
|
||||
|
||||
## Files changed
|
||||
scripts/create-user.mjs | 23 +++++++++++++++++++++++
|
||||
src/lib/auth.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
src/middleware.ts | 24 ++++++++++++++++++++++++
|
||||
tests/auth.test.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++
|
||||
4 files changed, 140 insertions(+)
|
||||
|
||||
## Diff
|
||||
diff --git a/scripts/create-user.mjs b/scripts/create-user.mjs
|
||||
new file mode 100644
|
||||
index 0000000..5bd0e67
|
||||
--- /dev/null
|
||||
+++ b/scripts/create-user.mjs
|
||||
@@ -0,0 +1,23 @@
|
||||
+import Database from 'better-sqlite3';
|
||||
+import bcrypt from 'bcryptjs';
|
||||
+import { mkdirSync } from 'node:fs';
|
||||
+import { dirname } from 'node:path';
|
||||
+
|
||||
+const [, , username, password] = process.argv;
|
||||
+if (!username || !password) {
|
||||
+ console.error('Uso: npm run create-user -- <username> <password>');
|
||||
+ 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
|
||||
+);`);
|
||||
+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));
|
||||
+console.log(`Utente "${username}" creato/aggiornato in ${path}`);
|
||||
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
|
||||
new file mode 100644
|
||||
index 0000000..1946702
|
||||
--- /dev/null
|
||||
+++ b/src/lib/auth.ts
|
||||
@@ -0,0 +1,48 @@
|
||||
+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 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));
|
||||
+ 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 {
|
||||
+ const row = db.prepare(
|
||||
+ `SELECT s.token, s.expires_at, u.id, u.username
|
||||
+ 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;
|
||||
+ 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 };
|
||||
+}
|
||||
+
|
||||
+export function logout(db: Database.Database, token: string): void {
|
||||
+ db.prepare('DELETE FROM sessions WHERE token = ?').run(token);
|
||||
+}
|
||||
diff --git a/src/middleware.ts b/src/middleware.ts
|
||||
new file mode 100644
|
||||
index 0000000..fa8dcfd
|
||||
--- /dev/null
|
||||
+++ b/src/middleware.ts
|
||||
@@ -0,0 +1,24 @@
|
||||
+import { defineMiddleware } from 'astro:middleware';
|
||||
+import { getDb } from './lib/db';
|
||||
+import { getSessionUser, SESSION_COOKIE } from './lib/auth';
|
||||
+
|
||||
+export const onRequest = defineMiddleware((context, next) => {
|
||||
+ const { pathname } = context.url;
|
||||
+ const isProtected =
|
||||
+ (pathname.startsWith('/admin') && pathname !== '/admin/login') ||
|
||||
+ pathname.startsWith('/api/admin');
|
||||
+ if (!isProtected) return next();
|
||||
+
|
||||
+ const token = context.cookies.get(SESSION_COOKIE)?.value;
|
||||
+ const user = token ? getSessionUser(getDb(), token) : null;
|
||||
+ if (!user) {
|
||||
+ if (pathname.startsWith('/api/')) {
|
||||
+ return new Response(JSON.stringify({ error: 'Non autorizzato' }), {
|
||||
+ status: 401, headers: { 'Content-Type': 'application/json' },
|
||||
+ });
|
||||
+ }
|
||||
+ return context.redirect('/admin/login');
|
||||
+ }
|
||||
+ context.locals.user = user;
|
||||
+ return next();
|
||||
+});
|
||||
diff --git a/tests/auth.test.ts b/tests/auth.test.ts
|
||||
new file mode 100644
|
||||
index 0000000..5eb2459
|
||||
--- /dev/null
|
||||
+++ b/tests/auth.test.ts
|
||||
@@ -0,0 +1,45 @@
|
||||
+import { describe, it, expect, beforeEach } from 'vitest';
|
||||
+import type Database from 'better-sqlite3';
|
||||
+import { createDb } from '../src/lib/db';
|
||||
+import { hashPassword, verifyPassword, createUser, login, getSessionUser, logout } from '../src/lib/auth';
|
||||
+
|
||||
+let db: Database.Database;
|
||||
+beforeEach(() => { db = createDb(':memory:'); });
|
||||
+
|
||||
+describe('auth', () => {
|
||||
+ it('hash e verifica password', () => {
|
||||
+ const h = hashPassword('segreta123');
|
||||
+ expect(h).not.toBe('segreta123');
|
||||
+ expect(verifyPassword('segreta123', h)).toBe(true);
|
||||
+ expect(verifyPassword('sbagliata', h)).toBe(false);
|
||||
+ });
|
||||
+
|
||||
+ it('login corretto crea sessione recuperabile', () => {
|
||||
+ createUser(db, 'adriano', 'segreta123');
|
||||
+ const token = login(db, 'adriano', 'segreta123');
|
||||
+ expect(token).toBeTruthy();
|
||||
+ const user = getSessionUser(db, token!);
|
||||
+ expect(user).toMatchObject({ username: 'adriano' });
|
||||
+ });
|
||||
+
|
||||
+ it('login errato ritorna null', () => {
|
||||
+ createUser(db, 'adriano', 'segreta123');
|
||||
+ expect(login(db, 'adriano', 'sbagliata')).toBeNull();
|
||||
+ expect(login(db, 'inesistente', 'x')).toBeNull();
|
||||
+ });
|
||||
+
|
||||
+ it('sessione scaduta viene rifiutata ed eliminata', () => {
|
||||
+ createUser(db, 'adriano', 'segreta123');
|
||||
+ const token = login(db, 'adriano', 'segreta123')!;
|
||||
+ db.prepare("UPDATE sessions SET expires_at = datetime('now', '-1 day')").run();
|
||||
+ expect(getSessionUser(db, token)).toBeNull();
|
||||
+ expect(db.prepare('SELECT COUNT(*) AS n FROM sessions').get()).toMatchObject({ n: 0 });
|
||||
+ });
|
||||
+
|
||||
+ it('logout elimina la sessione', () => {
|
||||
+ createUser(db, 'adriano', 'segreta123');
|
||||
+ const token = login(db, 'adriano', 'segreta123')!;
|
||||
+ logout(db, token);
|
||||
+ expect(getSessionUser(db, token)).toBeNull();
|
||||
+ });
|
||||
+});
|
||||
Reference in New Issue
Block a user