"""PostgreSQL-backed user store for authentication. Manages a `users` table with hashed passwords and roles. Provides lookup by username for the login flow. Table DDL is auto-applied on first connection. """ from __future__ import annotations from dataclasses import dataclass from typing import Optional import psycopg2 import psycopg2.extras from loguru import logger from passlib.context import CryptContext from app.config.settings import settings # bcrypt context — work factor 12 is a good production default. _PWD_CTX = CryptContext(schemes=["bcrypt"], deprecated="auto") # DDL executed once to ensure the table exists. _CREATE_TABLE_SQL = """ CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username VARCHAR(100) UNIQUE NOT NULL, hashed_pw TEXT NOT NULL, role VARCHAR(50) NOT NULL DEFAULT 'readonly', is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); """ @dataclass class UserRecord: """A single row from the users table.""" id: str username: str hashed_pw: str role: str is_active: bool class PostgresUserStore: """Read and verify users stored in the PostgreSQL users table. The connection is opened on first use and shared for the lifetime of the singleton instance wired by bootstrap. """ def __init__(self) -> None: """Initialise the store and ensure the users table exists.""" self._conn = psycopg2.connect( host=settings.postgres_host, port=settings.postgres_port, user=settings.postgres_user, password=settings.postgres_password, dbname=settings.postgres_db, cursor_factory=psycopg2.extras.RealDictCursor, ) self._conn.autocommit = True self._ensure_table() def _ensure_table(self) -> None: """Create the users table if it does not already exist.""" with self._conn.cursor() as cur: # Enable pgcrypto so gen_random_uuid() is available for UUID primary keys. try: cur.execute("CREATE EXTENSION IF NOT EXISTS pgcrypto;") except Exception: self._conn.rollback() cur.execute(_CREATE_TABLE_SQL) def get_by_username(self, username: str) -> Optional[UserRecord]: """Return a UserRecord for the given username, or None if not found.""" with self._conn.cursor() as cur: cur.execute( "SELECT id, username, hashed_pw, role, is_active " "FROM users WHERE username = %s", (username,), ) row = cur.fetchone() if row is None: return None return UserRecord( id=str(row["id"]), username=row["username"], hashed_pw=row["hashed_pw"], role=row["role"], is_active=row["is_active"], ) def verify_password(self, plain: str, hashed: str) -> bool: """Return True if `plain` matches the stored bcrypt hash.""" return _PWD_CTX.verify(plain, hashed) def authenticate(self, username: str, password: str) -> Optional[UserRecord]: """Return the UserRecord if credentials are valid, else None.""" user = self.get_by_username(username) if user is None or not user.is_active: return None if not self.verify_password(password, user.hashed_pw): return None return user @staticmethod def hash_password(plain: str) -> str: """Hash a plain-text password with bcrypt.""" return _PWD_CTX.hash(plain)