feat: DSH编排基建首版(server/tests 88用例/P2插件/8020端口/PORT-NOTE/MOBILE_APP)
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user