Files
eth_hedge_sim/backend/app/api/auth_routes.py
T
dekun 16efa44ffb Hide manual trade by default, block open while running, auto-refresh auth token.
Also fix flat-side reconcile to check both long and short residuals; document in 更新说明.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 22:43:38 +08:00

142 lines
4.4 KiB
Python

from __future__ import annotations
import hmac
import time
from collections import defaultdict
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel, Field
from ..config import Settings, get_settings
from ..credentials import get_credentials, update_credentials, upsert_env_file
from .auth import LoginRequest, LoginResponse, issue_token, require_user
router = APIRouter(prefix="/api/auth", tags=["auth"])
_login_hits: dict[str, list[float]] = defaultdict(list)
class ChangeCredentialsRequest(BaseModel):
current_password: str = Field(min_length=1)
new_username: str = Field(min_length=1, max_length=64)
new_password: str = Field(min_length=8, max_length=128)
def _client_ip(request: Request) -> str:
xff = request.headers.get("x-forwarded-for") or ""
if xff.strip():
return xff.split(",")[0].strip()
if request.client:
return request.client.host or "unknown"
return "unknown"
def _rate_limit_login(ip: str, settings: Settings) -> None:
now = time.time()
window = float(settings.login_window_sec)
max_n = int(settings.login_max_attempts)
hits = [t for t in _login_hits[ip] if now - t < window]
_login_hits[ip] = hits
if len(hits) >= max_n:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail=f"登录过于频繁,请 {int(window)} 秒后再试",
)
@router.post("/login", response_model=LoginResponse)
async def login(
body: LoginRequest,
request: Request,
settings: Annotated[Settings, Depends(get_settings)],
) -> LoginResponse:
ip = _client_ip(request)
_rate_limit_login(ip, settings)
user, pwd = get_credentials()
user_ok = hmac.compare_digest(
body.username.encode("utf-8"), user.encode("utf-8")
)
pwd_ok = hmac.compare_digest(
body.password.encode("utf-8"), pwd.encode("utf-8")
)
if not (user_ok and pwd_ok):
_login_hits[ip].append(time.time())
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误"
)
_login_hits.pop(ip, None)
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,
}
@router.post("/refresh", response_model=LoginResponse)
async def refresh_token(
username: Annotated[str, Depends(require_user)],
settings: Annotated[Settings, Depends(get_settings)],
) -> LoginResponse:
"""用仍有效的 Bearer 换发新 HMAC token(自动轮换,无需重登)。"""
token, ttl = issue_token(username, settings)
return LoginResponse(
token=token,
username=username,
expires_in=ttl,
env_name=settings.env_name,
mode=settings.mode,
)
@router.post("/change-credentials", response_model=LoginResponse)
async def change_credentials(
body: ChangeCredentialsRequest,
username: Annotated[str, Depends(require_user)],
settings: Annotated[Settings, Depends(get_settings)],
) -> LoginResponse:
_cur_user, cur_pwd = get_credentials()
if not hmac.compare_digest(
body.current_password.encode("utf-8"), cur_pwd.encode("utf-8")
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="当前密码不正确"
)
try:
update_credentials(
new_username=body.new_username, new_password=body.new_password
)
# 作废旧 token
new_ver = int(settings.auth_token_version) + 1
upsert_env_file("AUTH_TOKEN_VERSION", str(new_ver))
get_settings.cache_clear()
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)
) from e
settings2 = get_settings()
token, ttl = issue_token(body.new_username.strip(), settings2)
return LoginResponse(
token=token,
username=body.new_username.strip(),
expires_in=ttl,
env_name=settings2.env_name,
mode=settings2.mode,
)