重置项目
This commit is contained in:
+73
-32
@@ -11,8 +11,7 @@ from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app import __version__
|
||||
from app import auth as auth_module
|
||||
from app.api import analysis, auth, backtest, data, ext_data, financials, indices, intraday, kline, monitor_rules, alerts, overview, pipeline, screener, settings as settings_api, signals, strategy, watchlist
|
||||
from app.api import analysis, auth as auth_api, backtest, data, ext_data, financials, indices, intraday, kline, market_recap, monitor_rules, alerts, overview, pipeline, rps, screener, settings as settings_api, signals, stock_analysis, strategy, watchlist
|
||||
from app.api.routes import router as core_router
|
||||
from app.config import settings
|
||||
from app.jobs import daily_pipeline
|
||||
@@ -31,10 +30,18 @@ logger = logging.getLogger(__name__)
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
logger.info(
|
||||
"Stock Panel v%s starting (mode=%s)",
|
||||
"TickFlow Stock Panel v%s starting (mode=%s)",
|
||||
__version__, tf_client.current_mode(),
|
||||
)
|
||||
|
||||
# 首次启动: 若配置了 AUTH_PASSWORD 环境变量且未设过密码, 用它初始化。
|
||||
# 公网部署免 SSH 端口转发; 已设过密码则不覆盖 (改密码走 UI)。
|
||||
try:
|
||||
from app.services import auth as auth_service
|
||||
auth_service.bootstrap_from_env()
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("auth bootstrap failed: %s", e)
|
||||
|
||||
# 数据层
|
||||
store = DataStore()
|
||||
repo = KlineRepository(store)
|
||||
@@ -91,7 +98,16 @@ async def lifespan(app: FastAPI):
|
||||
pull_scheduler.refresh(store.data_dir)
|
||||
app.state.pull_scheduler = pull_scheduler
|
||||
|
||||
# 财务数据独立调度 (需 Expert 套餐)
|
||||
# 内置扩展表 (概念/行业): 只创建 config (含拉取配置), 不自动拉数据
|
||||
# 数据获取由用户在概念/行业页点「获取数据」手动触发 (POST /api/ext-data/presets/{id}/fetch)
|
||||
try:
|
||||
from app.services.ext_presets import ensure_builtin_presets
|
||||
await ensure_builtin_presets(store.data_dir)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("内置扩展表初始化失败 (不影响启动): %s", e)
|
||||
|
||||
# 财务数据 (需 Expert 套餐): 仅初始化调度器供 /api/financials/sync/* 手动同步,
|
||||
# 不启动自动调度——用户在「财务分析」页点「同步」手动拉取。
|
||||
from app.services.financial_sync import financial_scheduler
|
||||
financial_scheduler.start(store.data_dir, capset)
|
||||
app.state.financial_scheduler = financial_scheduler
|
||||
@@ -122,6 +138,9 @@ async def lifespan(app: FastAPI):
|
||||
monitor_engine = MonitorRuleEngine()
|
||||
monitor_engine.set_strategy_engine(strategy_engine)
|
||||
monitor_engine.set_data_dir(store.data_dir)
|
||||
# 复用 ScreenerService 的历史窗口加载器 (三级缓存, 启动预计算命中 ~0ms),
|
||||
# 让声明 filter_history 的策略 (如反包) 也能在实时监控里跑选股 → 盘中触发通知。
|
||||
monitor_engine.set_history_loader(_screener_svc._load_enriched_history)
|
||||
|
||||
# 自动迁移: 把旧 strategy_monitor_ids 同步为 type=strategy 规则 (统一到监控页)
|
||||
try:
|
||||
@@ -162,9 +181,9 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Stock Panel",
|
||||
title="TickFlow Stock Panel",
|
||||
version=__version__,
|
||||
description="A 股选股 + 监控 + 回测面板",
|
||||
description="A 股选股 + 回测面板 — TickFlow 适配",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
@@ -180,37 +199,54 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
|
||||
# 访问门控中间件:仅对 /api/* 路径校验,静态资源和前端路由放行,
|
||||
# 由前端 AccessGuard 控制 UI 展示。
|
||||
# ================================================================
|
||||
# 访问认证中间件
|
||||
# ================================================================
|
||||
# 拦截所有 /api/ 请求, 三种状态:
|
||||
# 1. 未设密码 + 本机/内网 → 放行(让本机用户访问面板 + 调 /api/auth/setup 设密码)
|
||||
# 2. 未设密码 + 公网 → 拒绝(403, 防裸奔也防抢占; 引导本机设密码)
|
||||
# 3. 已设密码 → 检查 session, 无效则 401(前端跳登录)
|
||||
# 白名单: /api/auth/* (设密码/登录本身)、/health 等探活。
|
||||
_AUTH_WHITELIST_PREFIX = ("/api/auth/",)
|
||||
_AUTH_WHITELIST_EXACT = ("/health", "/api/health", "/openapi.json", "/docs", "/redoc")
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def access_uuid_middleware(request: Request, call_next):
|
||||
if auth_module.access_control_enabled():
|
||||
path = request.url.path
|
||||
# 白名单直接放行
|
||||
if not auth_module.is_public_path(path):
|
||||
token = auth_module.get_access_token_from_request(request)
|
||||
role = auth_module.validate_access_token(token)
|
||||
# 管理员接口需 admin 角色
|
||||
if auth_module.is_admin_path(path):
|
||||
if role != auth_module.AuthRole.ADMIN:
|
||||
return JSONResponse(
|
||||
status_code=403 if role else 401,
|
||||
content={"detail": "需要管理员权限"},
|
||||
)
|
||||
# 其它 API 调用需任意有效角色
|
||||
elif path.startswith("/api/"):
|
||||
if role is None:
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content={"detail": "访问令牌无效或已过期,请先验证"},
|
||||
)
|
||||
return await call_next(request)
|
||||
async def auth_middleware(request: Request, call_next):
|
||||
path = request.url.path
|
||||
# 仅 /api/ 走认证; 静态资源(前端页面/assets)放行, 由前端处理跳转
|
||||
if not path.startswith("/api/"):
|
||||
return await call_next(request)
|
||||
# 白名单放行(设密码/登录/探活本身不拦)
|
||||
if path.startswith(_AUTH_WHITELIST_PREFIX) or path in _AUTH_WHITELIST_EXACT:
|
||||
return await call_next(request)
|
||||
|
||||
from app.services import auth as auth_service
|
||||
# 情况 1+2: 未设密码
|
||||
if not auth_service.is_configured():
|
||||
# 本机/内网 → 放行(服务器主人可访问, 并去 /login 设密码)
|
||||
if auth_api._is_local_network(auth_api._client_ip(request)):
|
||||
return await call_next(request)
|
||||
# 公网 → 拒绝。不裸奔, 也不给公网设密码的机会(防抢占)
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
content={
|
||||
"detail": "面板尚未初始化访问密码,请通过 SSH/本机浏览器访问以设置密码",
|
||||
"code": "NOT_INITIALIZED",
|
||||
},
|
||||
)
|
||||
|
||||
# 情况 3: 已设密码, 检查会话
|
||||
token = request.cookies.get(auth_api.COOKIE_NAME)
|
||||
if token and auth_service.is_valid_session(token):
|
||||
return await call_next(request)
|
||||
# 未登录: 401(前端跳登录页)
|
||||
return JSONResponse(status_code=401, content={"detail": "未登录或会话已过期"})
|
||||
|
||||
|
||||
# 路由
|
||||
app.include_router(core_router)
|
||||
app.include_router(auth.router)
|
||||
app.include_router(auth.admin_router)
|
||||
app.include_router(auth_api.router)
|
||||
app.include_router(kline.router)
|
||||
app.include_router(watchlist.router)
|
||||
app.include_router(screener.router)
|
||||
@@ -223,16 +259,21 @@ app.include_router(pipeline.router)
|
||||
app.include_router(data.router)
|
||||
app.include_router(ext_data.router)
|
||||
app.include_router(financials.router)
|
||||
app.include_router(stock_analysis.router)
|
||||
app.include_router(market_recap.router)
|
||||
app.include_router(settings_api.router)
|
||||
app.include_router(strategy.router)
|
||||
app.include_router(signals.router)
|
||||
app.include_router(monitor_rules.router)
|
||||
app.include_router(alerts.router)
|
||||
app.include_router(rps.router)
|
||||
|
||||
|
||||
# 能力门控异常 → 403(而非默认 500)
|
||||
# 业务代码用 capset.require(Cap.X) 断言能力,缺失时抛 CapabilityDenied;
|
||||
# 若不注册 handler 会冒泡成 500 Internal Server Error,对前端不友好且语义错误。
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from app.tickflow.capabilities import CapabilityDenied
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user