feat: DSH编排基建首版(server/tests 88用例/P2插件/8020端口/PORT-NOTE/MOBILE_APP)
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"""Token lifecycle: redeem an invite, list, self-issue, revoke."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..auth import Ctx, issue_token
|
||||
from ..db import record_audit
|
||||
from ..deps import get_ctx
|
||||
from ..ratelimit import SlidingWindow, limit_invite_attempts
|
||||
from ..util import USERNAME_RE, now_iso
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class TokenRequest(BaseModel):
|
||||
username: str
|
||||
invite_code: str | None = None
|
||||
token_name: str = "cli"
|
||||
|
||||
|
||||
class SelfTokenRequest(BaseModel):
|
||||
token_name: str = "cli"
|
||||
|
||||
|
||||
def _limiter(request: Request) -> SlidingWindow:
|
||||
return request.app.state.invite_limiter
|
||||
|
||||
|
||||
@router.post("/v1/tokens")
|
||||
def create_token(body: TokenRequest, request: Request, ctx: Ctx = Depends(get_ctx)):
|
||||
"""Exchange a team invite code for a personal token (new users only).
|
||||
|
||||
Unauthenticated, so it is the one endpoint worth brute-forcing: rate limited
|
||||
per peer address before the invite code is compared.
|
||||
"""
|
||||
limiter = _limiter(request)
|
||||
bucket = limit_invite_attempts(request, limiter)
|
||||
cfg = ctx.cfg
|
||||
if not USERNAME_RE.match(body.username or ""):
|
||||
raise HTTPException(400, "invalid username")
|
||||
if cfg.invite_code is None or body.invite_code != cfg.invite_code:
|
||||
raise HTTPException(403, "invalid invite code")
|
||||
row = ctx.conn.execute("SELECT id FROM users WHERE username = %s", (body.username,)).fetchone()
|
||||
if row is not None:
|
||||
raise HTTPException(409, "user already exists; ask an admin for a new token")
|
||||
cur = ctx.conn.execute(
|
||||
"INSERT INTO users (username, role, created_at) VALUES (%s, 'member', %s) RETURNING id",
|
||||
(body.username, now_iso()),
|
||||
)
|
||||
user_id = cur.fetchone()["id"]
|
||||
raw = issue_token(ctx.conn, user_id, body.token_name)
|
||||
record_audit(ctx.conn, user_id, "user.create", {"username": body.username, "via": "invite"})
|
||||
record_audit(ctx.conn, user_id, "token.create", {"name": body.token_name})
|
||||
limiter.clear(bucket)
|
||||
return {
|
||||
"token": raw,
|
||||
"user": {"id": user_id, "username": body.username, "role": "member"},
|
||||
}
|
||||
|
||||
|
||||
@router.post("/v1/tokens/self")
|
||||
def create_self_token(body: SelfTokenRequest, request: Request, ctx: Ctx = Depends(get_ctx)):
|
||||
"""Issue an additional token for the calling user (re-login helper)."""
|
||||
user = ctx.require_user(request)
|
||||
raw = issue_token(ctx.conn, user["id"], body.token_name)
|
||||
record_audit(ctx.conn, user["id"], "token.create", {"name": body.token_name, "via": "self"})
|
||||
return {"token": raw}
|
||||
|
||||
|
||||
@router.get("/v1/tokens")
|
||||
def list_tokens(request: Request, ctx: Ctx = Depends(get_ctx)):
|
||||
user = ctx.require_user(request)
|
||||
rows = ctx.conn.execute(
|
||||
"""SELECT token_hash, name, created_at, last_used_at FROM tokens
|
||||
WHERE user_id = %s AND revoked = FALSE ORDER BY created_at DESC""",
|
||||
(user["id"],),
|
||||
).fetchall()
|
||||
return {"tokens": [dict(r) for r in rows]}
|
||||
|
||||
|
||||
@router.delete("/v1/tokens/{token_hash}")
|
||||
def revoke_token(token_hash: str, request: Request, ctx: Ctx = Depends(get_ctx)):
|
||||
user = ctx.require_user(request)
|
||||
row = ctx.conn.execute(
|
||||
"SELECT token_hash, user_id FROM tokens WHERE token_hash = %s", (token_hash,)
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(404, "token not found")
|
||||
if user["role"] != "admin" and row["user_id"] != user["id"]:
|
||||
raise HTTPException(403, "cannot revoke another user's token")
|
||||
ctx.conn.execute("UPDATE tokens SET revoked = TRUE WHERE token_hash = %s", (token_hash,))
|
||||
ctx.conn.commit()
|
||||
record_audit(ctx.conn, user["id"], "token.revoke", {"token_hash": token_hash[:12] + "…"})
|
||||
return {"revoked": True}
|
||||
Reference in New Issue
Block a user