fix(txn+security): 仓储改 flush + 启动校验/兜底 + job.error 脱敏 + SSE 异常硬化
P0-1 SqlCredentialStore/save_draft 由自提交改 flush,端点/服务统一 commit (新增 CredentialStore.commit() 统一提交点;token 刷新落库显式提交); 补多凭据一请求中途失败整体回滚集成测试。 P0-2 启动校验 _fernet(enc_key) 快速失败 + catch-all Exception → ErrorEnvelope; credential_enc_key 改 SecretStr。 P0-3 run_job 异常分类:AppError 存 code+message,其余存通用文案不泄 str(exc)。 P0-4 评审/正文 SSE 失败先发 error 事件,尾部 commit 包 try/except。 P1-4 max_version 加 FOR UPDATE 行锁消除 TOCTOU。 P1-5 scan_overdue 谓词下推 + 批量 UPDATE RETURNING。 P1-10 移除 OAuth user_code 日志。 P2 provider_deps 改调网关 build_adapter;accept_service Committable Protocol; CORS 白名单收窄;request_id 安全字符集白名单;stdlib 日志接管;读端点 404 校验; httpx timeout;测试用合法 Fernet key;类型化响应模型(JobResponse/DimensionEntry/ ReviewConflictView/selling_points)+路由 ErrorEnvelope responses(供 codegen)。
This commit is contained in:
@@ -11,10 +11,10 @@ from fastapi.responses import JSONResponse
|
||||
from ww_config import get_settings
|
||||
from ww_core.domain.job_repo import SqlJobRepo
|
||||
from ww_db import get_sessionmaker
|
||||
from ww_shared import AppError, ErrorBody, ErrorEnvelope
|
||||
from ww_shared import AppError, ErrorBody, ErrorCode, ErrorEnvelope
|
||||
|
||||
from ww_api.logging_config import configure_logging, get_logger
|
||||
from ww_api.middleware import request_id_middleware
|
||||
from ww_api.middleware import REQUEST_ID_HEADER, request_id_middleware
|
||||
from ww_api.routers import (
|
||||
foreshadow,
|
||||
generation,
|
||||
@@ -27,6 +27,7 @@ from ww_api.routers import (
|
||||
settings_providers,
|
||||
style,
|
||||
)
|
||||
from ww_api.security.credentials import _fernet
|
||||
from ww_api.services.project_deps import seed_stub_user
|
||||
|
||||
configure_logging()
|
||||
@@ -35,6 +36,9 @@ log = get_logger("ww.api")
|
||||
|
||||
@asynccontextmanager
|
||||
async def _lifespan(app: FastAPI) -> AsyncIterator[None]:
|
||||
# 加密 key 启动校验:缺失/非法则**快速失败**(不带病启动接流量)。`_fernet` 缺/非法抛
|
||||
# CredentialKeyError;此处让其冒泡使进程启动失败(P0-2),而非首次凭据读写才 500。
|
||||
_fernet(get_settings().credential_enc_key.get_secret_value())
|
||||
# 幂等 seed 单用户 stub——所有 owner_id FK 依赖它(见 memory/gotchas)。
|
||||
async with get_sessionmaker()() as session:
|
||||
await seed_stub_user(session)
|
||||
@@ -53,12 +57,18 @@ def create_app() -> FastAPI:
|
||||
# (RSC 服务端取数同源、无需 CORS,故此前同进程测试从未暴露此缺口)。原型单用户、
|
||||
# 本地开发:放行 localhost:3000;可经 env `CORS_ORIGINS`(逗号分隔)覆盖。
|
||||
settings = get_settings()
|
||||
# `allow_credentials=True` 与通配 origin `"*"` 并存会被浏览器拒(且语义危险)——启动断言
|
||||
# cors_origins 不含通配,强制显式白名单(P2 收窄)。
|
||||
assert "*" not in settings.cors_origins, (
|
||||
"cors_origins 不能含通配 '*'(与 allow_credentials=True 不兼容);请配置显式来源白名单"
|
||||
)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
# 收窄方法/头白名单(避免 `*` 放行任意方法/头,P2)。
|
||||
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
||||
allow_headers=["content-type", "authorization", REQUEST_ID_HEADER],
|
||||
)
|
||||
app.middleware("http")(request_id_middleware)
|
||||
|
||||
@@ -76,6 +86,26 @@ def create_app() -> FastAPI:
|
||||
)
|
||||
return JSONResponse(status_code=exc.http_status, content=envelope.model_dump())
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def _unhandled_error_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||
# 兜底:任何未被 AppError 捕获的异常 → 统一 INTERNAL 信封(不回显原始异常给客户端),
|
||||
# 但服务端记完整上下文 + request_id 便于端到端排查(P0-2)。
|
||||
request_id = getattr(request.state, "request_id", None)
|
||||
log.error(
|
||||
"unhandled_exception",
|
||||
request_id=request_id,
|
||||
error_type=type(exc).__name__,
|
||||
exc_info=exc,
|
||||
)
|
||||
envelope = ErrorEnvelope(
|
||||
error=ErrorBody(
|
||||
code=ErrorCode.INTERNAL,
|
||||
message="服务器内部错误",
|
||||
request_id=request_id,
|
||||
)
|
||||
)
|
||||
return JSONResponse(status_code=500, content=envelope.model_dump())
|
||||
|
||||
app.include_router(health.router)
|
||||
app.include_router(jobs.router)
|
||||
app.include_router(projects.router)
|
||||
|
||||
Reference in New Issue
Block a user