# -*- coding: utf-8 -*- """Autenticazione utenti famiglia — pattern Loogle Casa.""" import base64 import datetime import hashlib import hmac import os import secrets import time from typing import Optional from fastapi import HTTPException, Request from .db import get_conn SESSION_DAYS = 30 PBKDF2_ITER = 240_000 MAX_ATTEMPTS = 8 WINDOW_S = 600 _attempts: dict = {} FAMILY_USERS = ( ("daniele", True), ("lucia", False), ("davide", False), ("luca", False), ) DEFAULT_SCOPES = ( "context:read context:write knowledge:read knowledge:write gitea:read gitea:write" ) def hash_password(password: str) -> str: salt = os.urandom(16) dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, PBKDF2_ITER) return "pbkdf2$%d$%s$%s" % ( PBKDF2_ITER, base64.b64encode(salt).decode(), base64.b64encode(dk).decode(), ) def verify_password(password: str, stored: str) -> bool: try: _, iters, salt_b64, dk_b64 = stored.split("$") salt = base64.b64decode(salt_b64) expected = base64.b64decode(dk_b64) dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, int(iters)) return hmac.compare_digest(dk, expected) except Exception: return False def ensure_family_users() -> None: conn = get_conn() for username, is_admin in FAMILY_USERS: row = conn.execute("SELECT id FROM users WHERE username=?", (username,)).fetchone() if row: continue conn.execute( "INSERT INTO users(username,password_hash,is_admin,must_change_password)" " VALUES (?,?,?,1)", (username, hash_password(username), 1 if is_admin else 0), ) conn.commit() def throttle(ip: str) -> None: now = time.time() hist = [t for t in _attempts.get(ip, []) if now - t < WINDOW_S] _attempts[ip] = hist if len(hist) >= MAX_ATTEMPTS: raise HTTPException(429, "Troppi tentativi: riprova tra qualche minuto") def record_attempt(ip: str) -> None: _attempts.setdefault(ip, []).append(time.time()) def authenticate(username: str, password: str) -> Optional[dict]: conn = get_conn() row = conn.execute("SELECT * FROM users WHERE username=?", (username.strip(),)).fetchone() if not row or not verify_password(password, row["password_hash"]): return None return dict(row) def get_user_by_id(user_id: int) -> Optional[dict]: row = get_conn().execute("SELECT * FROM users WHERE id=?", (user_id,)).fetchone() return dict(row) if row else None def get_user_by_username(username: str) -> Optional[dict]: row = get_conn().execute("SELECT * FROM users WHERE username=?", (username,)).fetchone() return dict(row) if row else None def change_password(user_id: int, old_password: str, new_password: str) -> bool: row = get_conn().execute("SELECT password_hash FROM users WHERE id=?", (user_id,)).fetchone() if not row or not verify_password(old_password, row["password_hash"]): return False conn = get_conn() conn.execute( "UPDATE users SET password_hash=?, must_change_password=0 WHERE id=?", (hash_password(new_password), user_id), ) conn.commit() return True def current_user_from_cookie(request: Request) -> dict: token = request.cookies.get("mcp_session", "") if not token: raise HTTPException(401, "Non autenticato") row = get_conn().execute( "SELECT u.id,u.username,u.is_admin,u.must_change_password" " FROM sessions s JOIN users u ON u.id=s.user_id" " WHERE s.token=? AND s.expires_at > datetime('now')", (token,), ).fetchone() if not row: raise HTTPException(401, "Sessione scaduta") return dict(row) def require_admin(request: Request) -> dict: user = current_user_from_cookie(request) if not user["is_admin"]: raise HTTPException(403, "Riservato all'amministratore") return user def ensure_sessions_table() -> None: get_conn().execute( """ CREATE TABLE IF NOT EXISTS sessions ( token TEXT PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT (datetime('now')), expires_at TEXT NOT NULL ) """ ) get_conn().commit()