Initial eth_hedge_sim: P0 market, auth UI, one-click deploy.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .auth_routes import router as auth_router
|
||||
from .market import router as market_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(auth_router)
|
||||
router.include_router(market_router)
|
||||
@@ -0,0 +1,85 @@
|
||||
"""简单 HMAC Token 鉴权(无 JWT 依赖)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ..config import Settings, get_settings
|
||||
|
||||
_bearer = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str = Field(min_length=1)
|
||||
password: str = Field(min_length=1)
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
token: str
|
||||
username: str
|
||||
expires_in: int
|
||||
env_name: str
|
||||
mode: str
|
||||
|
||||
|
||||
def _b64url(data: bytes) -> str:
|
||||
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
|
||||
|
||||
|
||||
def _b64url_decode(s: str) -> bytes:
|
||||
pad = "=" * (-len(s) % 4)
|
||||
return base64.urlsafe_b64decode(s + pad)
|
||||
|
||||
|
||||
def issue_token(username: str, settings: Settings) -> tuple[str, int]:
|
||||
exp = int(time.time()) + int(settings.auth_token_ttl_sec)
|
||||
payload = {"u": username, "exp": exp}
|
||||
raw = _b64url(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
|
||||
sig = hmac.new(
|
||||
settings.auth_secret.encode("utf-8"),
|
||||
raw.encode("ascii"),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
return f"{raw}.{sig}", settings.auth_token_ttl_sec
|
||||
|
||||
|
||||
def verify_token(token: str, settings: Settings) -> str:
|
||||
try:
|
||||
raw, sig = token.rsplit(".", 1)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token") from e
|
||||
expect = hmac.new(
|
||||
settings.auth_secret.encode("utf-8"),
|
||||
raw.encode("ascii"),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
if not hmac.compare_digest(expect, sig):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token")
|
||||
try:
|
||||
payload = json.loads(_b64url_decode(raw))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token") from e
|
||||
if int(payload.get("exp") or 0) < int(time.time()):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="token expired")
|
||||
username = str(payload.get("u") or "")
|
||||
if not username:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token")
|
||||
return username
|
||||
|
||||
|
||||
def require_user(
|
||||
creds: Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer)],
|
||||
settings: Annotated[Settings, Depends(get_settings)],
|
||||
) -> str:
|
||||
if creds is None or not creds.credentials:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="login required")
|
||||
return verify_token(creds.credentials, settings)
|
||||
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from ..config import Settings, get_settings
|
||||
from .auth import LoginRequest, LoginResponse, issue_token, require_user
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
async def login(body: LoginRequest, settings: Annotated[Settings, Depends(get_settings)]) -> LoginResponse:
|
||||
user_ok = body.username == settings.auth_username
|
||||
pass_ok = body.password == settings.auth_password
|
||||
if not (user_ok and pass_ok):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误")
|
||||
token, ttl = issue_token(body.username, settings)
|
||||
return LoginResponse(
|
||||
token=token,
|
||||
username=body.username,
|
||||
expires_in=ttl,
|
||||
env_name=settings.env_name,
|
||||
mode=settings.mode,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
async def me(username: Annotated[str, Depends(require_user)], settings: Annotated[Settings, Depends(get_settings)]) -> dict:
|
||||
return {
|
||||
"username": username,
|
||||
"env_name": settings.env_name,
|
||||
"mode": settings.mode,
|
||||
"sim": settings.is_sim,
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from ..market import get_gateway
|
||||
from .auth import require_user
|
||||
|
||||
router = APIRouter(prefix="/api/market", tags=["market"])
|
||||
|
||||
|
||||
@router.get("/snapshot")
|
||||
async def market_snapshot(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
gw = get_gateway()
|
||||
snap = gw.snapshot_dict()
|
||||
if snap.get("pair") is None:
|
||||
raise HTTPException(status_code=503, detail="market not aligned yet")
|
||||
return snap
|
||||
|
||||
|
||||
@router.post("/realign")
|
||||
async def market_realign(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
"""手动重对齐次日到期 ATM 合约(运维/调试用)。"""
|
||||
gw = get_gateway()
|
||||
try:
|
||||
pair = await gw.realign_async()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=502, detail=str(e)) from e
|
||||
return {"ok": True, "pair": pair.to_dict() if pair else None, "snapshot": gw.snapshot_dict()}
|
||||
Reference in New Issue
Block a user