chore(config): align server environment templates

This commit is contained in:
2026-10-27 17:17:08 +08:00
parent af1dc11681
commit 2865811f1b
4 changed files with 105 additions and 6 deletions
+19 -5
View File
@@ -1,6 +1,7 @@
# TJWater Server 环境变量模板
# NextTJWater Server 环境变量模板
# 复制为 .env 后按启用的功能填写;不要提交 .env 或任何真实凭据。
# 生产配置由部署主机的受控 backend.env 注入,不要把完整配置保存为仓库密钥。
# 生产配置由部署主机的受控 next-backend.env 注入,不随镜像或 CI/CD 同步本地 .env。
# 进程环境变量优先;修改 Compose env_file 后需重新创建容器。
# 基础运行
PROJECT_NAME="TJWater Server"
@@ -69,9 +70,11 @@ KEYCLOAK_ALGORITHM="RS256"
KEYCLOAK_AUDIENCE="account"
KEYCLOAK_ACCESS_TOKEN_MAX_AGE_SECONDS=900
# 工作台事件与跨域
# 认证接口必须使用明确来源,逗号分隔;不要配置为 *。
CORS_ALLOWED_ORIGINS="http://127.0.0.1:5173,http://localhost:5173,http://127.0.0.1:15173,http://localhost:15173"
# 浏览器跨域访问(主 API)
# 必须填写明确来源(协议、域名或 IP、端口),英文逗号分隔;不允许 * 或网段正则。
CORS_ALLOWED_ORIGINS=http://127.0.0.1:5173,http://localhost:5173,http://127.0.0.1:15173,http://localhost:15173
# 工作台事件
WORKBENCH_EVENTS_ENABLED=true
WORKBENCH_EVENT_RETENTION_DAYS=7
@@ -116,6 +119,17 @@ OPERATIONAL_ACTION_PERIPHERAL_URL=""
OPERATIONAL_ACTION_COMMAND_GATEWAY_URL=""
OPERATIONAL_ACTION_TIMEOUT_SECONDS=10
# 外围样板(挂载于 /peripheral;故障注入默认关闭)
# 独立启动时用于调用主 API 验证身份;容器内按网络填写主服务地址。
PERIPHERAL_SAMPLE_NEXT_SERVER_URL="http://127.0.0.1:8000"
# 以下两个默认库选择只供种子、导出等脚本使用,HTTP 请求仍按已授权项目路由。
PERIPHERAL_SAMPLE_DB_NAME="tjwater_v2"
PERIPHERAL_SAMPLE_PROJECT_CODE="tjwater_v2"
# 子应用独立运行时的浏览器来源列表,使用 JSON 格式;挂载时还受主 API CORS 配置控制。
PERIPHERAL_SAMPLE_CORS_ORIGINS='["http://127.0.0.1:5173","http://localhost:5173"]'
PERIPHERAL_SAMPLE_ENABLE_FAULTS=false
PERIPHERAL_SAMPLE_TIMEOUT_SECONDS=5
# MCP 查询服务;OAuth 和所有项目业务库就绪前保持关闭。
TJWATER_MCP_ENABLED=false
TJWATER_MCP_ISSUER_URI=""
+12
View File
@@ -70,6 +70,18 @@ conda activate server
基础数据库变量用于启动和项目供应。运行时的项目请求仍以元数据库中的 `biz_data` 和 `iot_data` 路由为准,不会因为存在默认数据库变量而回退到固定项目。
CORS 通过后端 `.env` 或部署进程环境变量中的 `CORS_ALLOWED_ORIGINS` 配置,进程环境变量优先。
必须使用英文逗号分隔的明确页面来源(协议、域名或 IP、端口,不带路径),不能填写 `*`。
未设置时默认只允许本机 `5173`、`15173` 开发来源。正式部署按实际浏览器地址填写,例如:
```dotenv
CORS_ALLOWED_ORIGINS=https://next-demo.waternetwork.cn,http://192.168.1.133,https://192.168.1.133,http://192.168.1.110,https://192.168.1.110
```
CORS 匹配浏览器页面来源,不是 Nginx 连接 IP 或域名解析结果;非默认端口须逐项列出。
保留跨域凭据支持及 `Authorization`、`X-Project-Id` 请求头,接口认证和权限校验继续生效。
修改后需重启进程;使用 Compose `env_file` 注入时需重新创建容器才能读取新值。
## 常用命令
外围接入样板作为 `/peripheral` 子应用随主 API 进程启动,复用 PostgreSQL 且不自动建表;授权边界和契约见 [外围样板说明](docs/peripheral-sample.md)。
+1 -1
View File
@@ -138,7 +138,7 @@ conda run -n server python scripts/cleanup_workbench_events.py \
禁止省略 `--project-id` 执行清理。数据库维护脚本同样遵循先在 `tjwater_v2` 验证、再逐库扩展的规则。
浏览器使用 `GET /api/v1/workbench/sync-snapshot` 建立原子快照,并以该游标连接 `GET /api/v1/workbench/events`。游标早于保留窗口、超前或对应事件已全部清理时返回 `reset_required`,客户端必须重建快照。SSE 需要 Bearer 与 `X-Project-Id`,且 `CORS_ALLOWED_ORIGINS` 必须配置为明确来源。
浏览器使用 `GET /api/v1/workbench/sync-snapshot` 建立原子快照,并以该游标连接 `GET /api/v1/workbench/events`。游标早于保留窗口、超前或对应事件已全部清理时返回 `reset_required`,客户端必须重建快照。SSE 需要 Bearer 与 `X-Project-Id`,且 `CORS_ALLOWED_ORIGINS` 必须配置为明确来源,不允许 `*`。配置说明见 [README](../README.md#环境配置)。
## 外围样板工单同步
+73
View File
@@ -0,0 +1,73 @@
import pytest
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.testclient import TestClient
from app.core.config import Settings
ALLOWED_ORIGINS = [
"https://next-demo.waternetwork.cn",
"http://192.168.1.133",
"https://192.168.1.133",
"http://192.168.1.110",
"https://192.168.1.110",
]
@pytest.mark.parametrize("origins", ["*", " *, http://localhost:5173", "http://localhost:5173,*"])
def test_wildcard_origins_are_rejected(origins: str):
with pytest.raises(ValueError, match="must list explicit origins"):
Settings(_env_file=None, CORS_ALLOWED_ORIGINS=origins).cors_allowed_origins
@pytest.mark.parametrize(
"origin",
ALLOWED_ORIGINS + [
"http://192.168.1.114",
"http://192.168.1.133:5173",
"https://demo.waternetwork.cn",
"http://192.168.1.133.evil.example",
"null",
],
)
def test_explicit_origin_preflight(origin: str):
settings = Settings(_env_file=None, CORS_ALLOWED_ORIGINS=", ".join(ALLOWED_ORIGINS))
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
with TestClient(app) as client:
response = client.options(
"/api/v1/projects",
headers={
"Origin": origin,
"Access-Control-Request-Method": "GET",
"Access-Control-Request-Headers": "authorization,x-project-id",
},
)
allowed = origin in ALLOWED_ORIGINS
assert response.status_code == (200 if allowed else 400)
assert response.headers.get("access-control-allow-origin") == (origin if allowed else None)
if allowed:
assert response.headers["access-control-allow-credentials"] == "true"
assert "authorization" in response.headers["access-control-allow-headers"]
def test_default_origins_are_explicit_local_sources(monkeypatch):
monkeypatch.delenv("CORS_ALLOWED_ORIGINS", raising=False)
settings = Settings(_env_file=None)
assert settings.cors_allowed_origins == [
"http://127.0.0.1:5173", "http://localhost:5173",
"http://127.0.0.1:15173", "http://localhost:15173",
]
def test_environment_controls_origins(monkeypatch):
monkeypatch.setenv("CORS_ALLOWED_ORIGINS", "http://192.168.1.133,https://192.168.1.110")
settings = Settings(_env_file=None)
assert settings.cors_allowed_origins == ["http://192.168.1.133", "https://192.168.1.110"]