"""Token issuing, request authentication, and per-request context.""" from __future__ import annotations import secrets from fastapi import HTTPException, Request from .config import Config from .db import connect from .util import now_iso, sha256_text def hash_token(raw: str) -> str: return sha256_text(raw) def issue_token(conn, user_id: int, name: str) -> str: raw = "dsh_" + secrets.token_hex(32) conn.execute( "INSERT INTO tokens (token_hash, user_id, name, created_at) VALUES (%s, %s, %s, %s)", (hash_token(raw), user_id, name, now_iso()), ) conn.commit() return raw def ensure_bootstrap_admin(cfg: Config) -> None: """Idempotently materialize the admin user + token from env on startup.""" if not cfg.bootstrap_admin_token: return conn = connect(cfg) try: row = conn.execute( "SELECT id FROM users WHERE username = %s", (cfg.admin_username,) ).fetchone() if row is None: cur = conn.execute( "INSERT INTO users (username, role, created_at) VALUES (%s, 'admin', %s) RETURNING id", (cfg.admin_username, now_iso()), ) user_id = cur.fetchone()["id"] else: user_id = row["id"] conn.execute( """INSERT INTO tokens (token_hash, user_id, name, created_at) VALUES (%s, %s, 'bootstrap-admin', %s) ON CONFLICT (token_hash) DO NOTHING""", (hash_token(cfg.bootstrap_admin_token), user_id, now_iso()), ) conn.commit() finally: conn.close() def authenticate(conn, authorization: str | None) -> dict: if not authorization or not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="missing bearer token") raw = authorization[len("Bearer "):].strip() row = conn.execute( """SELECT t.token_hash, t.revoked, u.id AS user_id, u.username, u.role FROM tokens t JOIN users u ON u.id = t.user_id WHERE t.token_hash = %s""", (hash_token(raw),), ).fetchone() if row is None or row["revoked"]: raise HTTPException(status_code=401, detail="invalid or revoked token") conn.execute( "UPDATE tokens SET last_used_at = %s WHERE token_hash = %s", (now_iso(), row["token_hash"]), ) conn.commit() return {"id": row["user_id"], "username": row["username"], "role": row["role"]} class Ctx: """Per-request context: config + DB connection + lazily authenticated user.""" def __init__(self, cfg: Config, conn): self.cfg = cfg self.conn = conn self._user: dict | None = None def require_user(self, request: Request) -> dict: if self._user is None: self._user = authenticate(self.conn, request.headers.get("authorization")) return self._user def require_admin(self, request: Request) -> dict: user = self.require_user(request) if user["role"] != "admin": raise HTTPException(status_code=403, detail="admin role required") return user