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:
eezd committed 2026-10-13 13:32:55 +08:00
commit aabc81ba5b
88 files changed
+12603

No files matched your search

+58
View File
@@ -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)