Initial AlphaPulse implementation: news crawler and Binance klines
- Backend (FastAPI, SQLAlchemy 2, Alembic): single-admin cookie auth, news/crawler/market APIs with SSE and WebSocket streaming - Crawler (Scrapy + Scrapy-Redis): wscn_news, wscn_live, cls_depth, cls_telegraph spiders, idempotent upserts with revisions, raw response archiving, per-source subprocess supervision, restart and backoff - Market worker: full-history Binance spot/USD-M kline backfill, realtime WS stream, hourly and on-reconnect catch-up - Frontend (Vue 3, Element Plus, UnoCSS, KLineChart): news feed, crawler console with live logs, Binance-style chart page - Deploy: docker compose, nginx site, migrate and daily backup scripts
This commit is contained in:
commit
aabc81ba5b
88 files changed
+12603
No files matched your search
@@ -0,0 +1,58 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from alphapulse.core.auth import COOKIE_NAME, create_token, require_admin, verify_password
|
||||
from alphapulse.core.config import get_settings
|
||||
from alphapulse.core.redis import KEY_PREFIX, redis
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
MAX_FAILURES = 5
|
||||
LOCK_SECONDS = 15 * 60
|
||||
|
||||
|
||||
class LoginIn(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class MeOut(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(body: LoginIn, request: Request, response: Response) -> MeOut:
|
||||
settings = get_settings()
|
||||
if not settings.admin_password_hash:
|
||||
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, "管理员密码未配置")
|
||||
fail_key = f"{KEY_PREFIX}login:fail:{request.client.host}"
|
||||
if int(await redis.get(fail_key) or 0) >= MAX_FAILURES:
|
||||
raise HTTPException(status.HTTP_429_TOO_MANY_REQUESTS, "失败次数过多,请 15 分钟后再试")
|
||||
|
||||
if body.username != settings.admin_username or not verify_password(body.password, settings.admin_password_hash):
|
||||
async with redis.pipeline() as pipe:
|
||||
await pipe.incr(fail_key).expire(fail_key, LOCK_SECONDS).execute()
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "用户名或密码错误")
|
||||
|
||||
await redis.delete(fail_key)
|
||||
response.set_cookie(
|
||||
COOKIE_NAME,
|
||||
create_token(body.username),
|
||||
max_age=settings.jwt_expire_days * 86400,
|
||||
httponly=True,
|
||||
secure=True,
|
||||
samesite="strict",
|
||||
)
|
||||
return MeOut(username=body.username)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(response: Response) -> None:
|
||||
response.delete_cookie(COOKIE_NAME, httponly=True, secure=True, samesite="strict")
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
async def me(username: Annotated[str, Depends(require_admin)]) -> MeOut:
|
||||
return MeOut(username=username)
|
||||
Reference in new issue
Block a user