115 lines
4.6 KiB
TypeScript
115 lines
4.6 KiB
TypeScript
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' | '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 {
|
|
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; role: string } | null {
|
|
const row = db.prepare(
|
|
`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; 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, role: row.role };
|
|
}
|
|
|
|
export function logout(db: Database.Database, token: string): void {
|
|
db.prepare('DELETE FROM sessions WHERE token = ?').run(token);
|
|
}
|
|
|
|
// 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;
|
|
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 getUsernameById(db: Database.Database, id: number): string | null {
|
|
const row = db.prepare('SELECT username FROM users WHERE id = ?').get(id) as { username: string } | undefined;
|
|
return row?.username ?? null;
|
|
}
|
|
|
|
export function listUsers(db: Database.Database): { id: number; username: string; role: string }[] {
|
|
return db.prepare('SELECT id, username, role FROM users ORDER BY username').all() as
|
|
{ id: number; username: string; role: string }[];
|
|
}
|
|
|
|
export function countAdmins(db: Database.Database): number {
|
|
return (db.prepare("SELECT COUNT(*) AS n FROM users WHERE role = 'admin'").get() as { n: number }).n;
|
|
}
|
|
|
|
export function getUserById(db: Database.Database, id: number): { id: number; username: string; role: string } | null {
|
|
const row = db.prepare('SELECT id, username, role FROM users WHERE id = ?').get(id) as
|
|
{ id: number; username: string; role: string } | undefined;
|
|
return row ?? null;
|
|
}
|
|
|
|
export function updateUserRole(db: Database.Database, id: number, role: Role): void {
|
|
db.prepare('UPDATE users SET role = ? WHERE id = ?').run(role, id);
|
|
}
|
|
|
|
export function updateUserPassword(db: Database.Database, id: number, hash: string): void {
|
|
db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(hash, id);
|
|
}
|
|
|
|
export function deleteUser(db: Database.Database, id: number): void {
|
|
db.prepare('DELETE FROM users WHERE id = ?').run(id);
|
|
}
|
|
|
|
export function randomPassword(): string {
|
|
return randomBytes(9).toString('base64url'); // ~12 caratteri
|
|
}
|
|
|
|
export function isRole(v: unknown): v is Role {
|
|
return v === 'admin' || v === 'superuser' || v === 'user';
|
|
}
|
|
|
|
export function landingFor(role: string): string {
|
|
if (role === 'superuser') return '/admin/content';
|
|
return '/admin';
|
|
}
|