feat(auth): 内测登录改为显式开关,并支持后端单端口托管前端产物
This commit is contained in:
+13
-2
@@ -57,6 +57,9 @@ class Settings(BaseSettings):
|
||||
sms_throttle_seconds: int = 60
|
||||
sms_dev_code: str = "123456"
|
||||
sms_provider: Literal["dev", "aliyun", "tencent"] = "dev"
|
||||
# 内测开关:生产默认禁止固定验证码。确实要在内网试用时显式打开,
|
||||
# 启动会打 WARNING(见 main.py),其余生产校验照旧——绝不能配合公网暴露。
|
||||
internal_test_login: bool = False
|
||||
|
||||
# ── 幂等 ──────────────────────────────────────────────
|
||||
idempotency_enabled: bool = True
|
||||
@@ -100,6 +103,11 @@ class Settings(BaseSettings):
|
||||
embedding_dim: int = 1024
|
||||
embedding_model: str = "BAAI/bge-m3"
|
||||
|
||||
# ── 前端产物 ──────────────────────────────────────────
|
||||
# 生产没有 nginx 时由后端直接托管前端构建产物(同源,/api 无跨域)。
|
||||
# 指向 web/dist 的绝对路径;留空则只提供 API。
|
||||
web_dist_dir: str | None = None
|
||||
|
||||
# ── 构建标记(部署校验用)──────────────────────────────
|
||||
build_commit: str = "unknown"
|
||||
build_time: str = "unknown"
|
||||
@@ -121,8 +129,11 @@ class Settings(BaseSettings):
|
||||
problems.append("JWT_SECRET 仍是开发默认值或长度不足 32")
|
||||
if "dadao_dev_pw" in self.database_url:
|
||||
problems.append("DATABASE_URL 仍在用开发默认口令")
|
||||
if self.sms_provider == "dev":
|
||||
problems.append("生产环境不允许 SMS_PROVIDER=dev(任意验证码可登录)")
|
||||
if self.sms_provider == "dev" and not self.internal_test_login:
|
||||
problems.append(
|
||||
"生产环境不允许 SMS_PROVIDER=dev(任意验证码可登录);"
|
||||
"内网试用确需固定验证码时显式设置 INTERNAL_TEST_LOGIN=true"
|
||||
)
|
||||
if problems:
|
||||
raise RuntimeError("配置校验失败:" + ";".join(problems))
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
@@ -11,6 +12,9 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy import text
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.responses import Response
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from starlette.types import Scope
|
||||
|
||||
from .api import attachments, auth, items, knowledge, tickets, workspaces
|
||||
from .config import get_settings
|
||||
@@ -27,6 +31,11 @@ logger = get_logger("app")
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
settings.validate_runtime()
|
||||
if settings.env == "prod" and settings.internal_test_login:
|
||||
logger.warning(
|
||||
"内测登录已开启:任意手机号可用固定验证码登录,只能在内网访问,切勿暴露公网",
|
||||
extra={"extra_fields": {"sms_provider": settings.sms_provider}},
|
||||
)
|
||||
logger.info(
|
||||
"服务启动",
|
||||
extra={"extra_fields": {"env": settings.env, "commit": settings.build_commit}},
|
||||
@@ -138,3 +147,22 @@ async def version() -> dict:
|
||||
"api_prefix": settings.api_prefix,
|
||||
"db_role": role,
|
||||
}
|
||||
|
||||
|
||||
# ── 前端产物:单端口部署(没有 nginx 时也让一个端口提供完整站点)────────
|
||||
# 顺序很重要:必须挂在所有 API 路由之后,否则挂载 "/" 会吞掉 /api、/health、/version。
|
||||
class _SPAStaticFiles(StaticFiles):
|
||||
"""静态命中就返回,未命中回退 index.html——前端路由(如 /w/<id>)交给浏览器处理。"""
|
||||
|
||||
async def get_response(self, path: str, scope: Scope) -> Response:
|
||||
try:
|
||||
return await super().get_response(path, scope)
|
||||
except StarletteHTTPException as exc:
|
||||
if exc.status_code == 404:
|
||||
return await super().get_response("index.html", scope)
|
||||
raise
|
||||
|
||||
|
||||
_web_dist = Path(settings.web_dist_dir) if settings.web_dist_dir else None
|
||||
if _web_dist is not None and (_web_dist / "index.html").is_file():
|
||||
app.mount("/", _SPAStaticFiles(directory=str(_web_dist), html=True), name="web")
|
||||
|
||||
Reference in New Issue
Block a user