feat(config,gateway,web): 路径式安全入口 ADMIN_PATH + 数据目录 platform 哨兵(裁决 1.6/16.4.7/12.4.2 + 1.5/12.4.3 核对)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 1.6 + 16.4.7:安全入口=路径式 `/<ADMIN_PATH>`(env `ADMIN_PATH`,默认 `admin`;`xxx/admin` 亦可,
  非法值回退 `/admin`)。命中前缀 → 送注入了 `<base href="/">` + `window.__CYRENE_ADMIN_ENTRY__` 的面板入口页
  (深链直达);**形如面板入口但前缀不符 → 404**(不回落用户端聊天壳);`/api/*`、`/ws`、webhook 先放行
  ⇒ **API 不藏**。前端路由改**路径式**(`router.ts` 只读注入,无硬编兜底;移除 hash 入口)
- 1.5 / 12.4.3 核对:暴露面默认 `0.0.0.0` 与裁决一致,无需改代码(仅补注释)
- 12.4.2:`DATA_DIR` 支持 `platform` 哨兵(大小写不敏感)→ 平台标准目录;默认 `./data` 与显式空值行为不变
  (显式空值保持原兜底,避免正在运行实例突然换库)
- 复核:build/vet 绿 + config/gateway 包测试绿 + linux/darwin 交叉编译绿;新增 7 条用例,
  并用真实构建产物(`webui.Assets()`)跑通「浏览器直开 /admin 能启动面板」(`<base>` 必须在资源引用之前)
- 变异 2 条(关掉错前缀 404 / 前端默认前缀兜底)已还原
- ⚠️ 运行时序:前端已生效而后端 hatch 未生效期间**面板打不开** ⇒ 需紧接一次「build 到位 + restart」
This commit is contained in:
AskaEth committed 2026-10-04 14:00:25 +08:00
1 parent 9253fef92a
commit a398dc9b92
7 files changed
+481 -33

No files matched your search

@@ -0,0 +1,72 @@
package config
import (
"strings"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/platform"
)
// TestLoad_DataDirPlatformSentinel 覆盖数据目录的两种取法(design/13 §2,
// 裁决 12.4.2 + 15.4.1):未配置走默认 `./data`;显式哨兵 `platform` 才走
// 平台标准数据目录;显式路径原样保留。
func TestLoad_DataDirPlatformSentinel(t *testing.T) {
t.Setenv("JWT_SECRET", strings.Repeat("s", 40))
unsetEnvForTest(t, "DATA_DIR")
cfg, err := Load("")
if err != nil {
t.Fatalf("Load() error: %v", err)
}
if cfg.DataDir != "./data" {
t.Errorf("DataDir = %q, want ./data (unset keeps the historic default)", cfg.DataDir)
}
t.Setenv("DATA_DIR", "platform")
cfg, err = Load("")
if err != nil {
t.Fatalf("Load() with sentinel error: %v", err)
}
if want := platform.DefaultDataDir(); cfg.DataDir != want {
t.Errorf("DataDir = %q, want the platform standard dir %q", cfg.DataDir, want)
}
// 显式路径(含显式 ./data)不被改写。
for _, p := range []string{"./data", "/var/lib/cyrene"} {
t.Setenv("DATA_DIR", p)
cfg, err = Load("")
if err != nil {
t.Fatalf("Load() with DATA_DIR=%q error: %v", p, err)
}
if cfg.DataDir != p {
t.Errorf("DataDir = %q, want %q verbatim", cfg.DataDir, p)
}
}
}
// TestAdminPathPrefix 覆盖安全入口前缀的归一化(裁决 1.6 / 16.4.7,
// design/07 §0.2):默认 `admin` → `/admin`,自定义 `xxx/admin` →
// `/xxx/admin`(即 `host/xxx/admin` 形态),非法值一律挂回默认。
func TestAdminPathPrefix(t *testing.T) {
want := map[string]string{
"": "/admin",
"admin": "/admin",
"/admin": "/admin",
"/admin/": "/admin",
"xxx/admin": "/xxx/admin",
"/xxx/admin/": "/xxx/admin",
" a / b ": "/a/b",
"../etc": "/admin",
"a/../b": "/admin",
"a//b": "/admin",
"a?b": "/admin",
"a#b": "/admin",
" ": "/admin",
}
for in, exp := range want {
cfg := ServerConfig{AdminPath: in}
if got := cfg.AdminPathPrefix(); got != exp {
t.Errorf("AdminPath %q → %q, want %q", in, got, exp)
}
}
}