from typing import Any from fastapi import HTTPException, Request, status from passlib.context import CryptContext from app.db.database import get_connection from app.db.migrations import run_migrations pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def _table_columns(con, table_name: str) -> set[str]: rows = con.execute(f"PRAGMA table_info({table_name})").fetchall() return {row["name"] for row in rows} def _user_select_columns(con) -> str: columns = _table_columns(con, "users") enabled_select = "COALESCE(is_enabled, 1) AS is_enabled" if "is_enabled" in columns else "1 AS is_enabled" return f"id, username, display_name, email, password_hash, role, is_active, {enabled_select}, created_at, updated_at" def mark_last_login(user_id: int) -> None: con = get_connection() columns = _table_columns(con, "users") if "last_login_at" in columns: con.execute( """ UPDATE users SET last_login_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ? """, (user_id,), ) con.commit() con.close() def hash_password(password: str) -> str: return pwd_context.hash(password) def verify_password(plain_password: str, password_hash: str) -> bool: if not password_hash: return False return pwd_context.verify(plain_password, password_hash) def get_user_by_username(username: str) -> dict[str, Any] | None: run_migrations() con = get_connection() row = con.execute( f""" SELECT {_user_select_columns(con)} FROM users WHERE username = ? """, (username,), ).fetchone() con.close() return dict(row) if row else None def find_or_create_oauth_user(username: str, display_name: str, email: str) -> dict[str, Any]: username = username.strip() display_name = (display_name or username).strip() or username email = (email or "").strip() run_migrations() con = get_connection() row = con.execute( f""" SELECT {_user_select_columns(con)} FROM users WHERE username = ? """, (username,), ).fetchone() if row: con.execute( """ UPDATE users SET display_name = ?, email = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? """, (display_name, email, row["id"]), ) else: con.execute( """ INSERT INTO users ( username, display_name, email, password_hash, role, is_active, created_at, updated_at ) VALUES (?, ?, ?, '', 'developer', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) """, (username, display_name, email), ) con.commit() user_id = row["id"] if row else con.execute("SELECT last_insert_rowid() AS id").fetchone()["id"] user = con.execute( f""" SELECT {_user_select_columns(con)} FROM users WHERE id = ? """, (user_id,), ).fetchone() con.close() return dict(user) def get_user_by_id(user_id: int) -> dict[str, Any] | None: run_migrations() con = get_connection() row = con.execute( f""" SELECT {_user_select_columns(con)} FROM users WHERE id = ? """, (user_id,), ).fetchone() con.close() return dict(row) if row else None def authenticate_user(username: str, password: str) -> dict[str, Any] | None: user = get_user_by_username(username.strip()) if not user or not user.get("is_active") or not user.get("is_enabled", 1): return None if not verify_password(password, user.get("password_hash") or ""): return None mark_last_login(int(user["id"])) return user def current_user(request: Request) -> dict[str, Any] | None: user_id = request.session.get("user_id") if not user_id: return None try: user_id = int(user_id) except (TypeError, ValueError): request.session.pop("user_id", None) return None user = get_user_by_id(user_id) if not user or not user.get("is_active") or not user.get("is_enabled", 1): request.session.pop("user_id", None) return None return user def require_user(request: Request) -> dict[str, Any]: user = current_user(request) if user: return user raise HTTPException( status_code=status.HTTP_303_SEE_OTHER, headers={"Location": "/portal/login"}, )