Files
HomeDash/admin-server/gateway_import_test.go
HomeDash Bot 53beff646a feat(admin): 现有配置导入 + 添加表单补齐认证/导航开关 + 统一登录账号设置 + 设置分组
针对反馈的五个问题:

1. 后台"没有映射":新增 /admin/api/gateway/import 与界面「从现有 nginx 配置导入」,
   把老统一入口的 nginx 配置贴进来即可解析出全部「域名 → 后端」初稿
   (含 requireLogin / 免认证路径 / SSE 流式 / 备案注入 / 自签证书 / auth 与 portal 类型)。
   解析只读不落盘,确认后才保存;导入默认不接管导航,避免一导入就改掉手工导航。
2. 添加转发时没有导航选项:添加表单补齐「同时加入 Dashy 导航 / 分组 / 名称 / 描述 /
   图标 / 自动抓取」。
3. 添加转发时没有 portal 保护选项:添加表单补齐「启用 portal 统一登录保护」,
   并补上 WebSocket 透传与上传上限。
4. 设置里无法配置 portal 账号:新增统一登录账号卡片,账号存数据卷
   (portal-auth.json,只存加盐哈希),优先级 数据卷 > 环境变量,
   改完立即生效、无需重新部署;会话校验按当前用户名,改账号后旧会话失效。
5. 设置项太乱:按用途分组(站点与外观 / 导航内容 / 历史记录 / 转发与统一登录 / 高级),
   外观卡片并入站点组。

另修两个本轮引入的缺陷:
- 容器假 unhealthy:内部端口改 18080 后,基础镜像的 HEALTHCHECK 仍指向 8080;
  部署脚本生成的 Dockerfile 现在显式覆盖为 :18080/healthz。
- kind=auth 不依赖 portal-auth 容器,指向内置登录服务;避免生成空 proxy_pass。

测试:nginx 配置解析(含兜底段忽略、多域名段、类型判定、默认不接管导航)、
垃圾输入报错、账号存储(不明文、可校验、改名后旧口令失效)、auth 类型生成。
2026-09-11 22:00:09 +08:00

148 lines
4.1 KiB
Go

package main
import (
"os"
"strings"
"testing"
)
const sampleConf = `map $host $backend {
hostnames;
default "";
gitea.example.com http://127.0.0.1:3000;
login.example.com http://127.0.0.1:4001;
dashy.example.com http://127.0.0.1:3003;
modulerouter.example.com http://10.0.0.11:8081;
}
server {
listen 3080 ssl;
server_name example.com www.example.com;
root /usr/share/nginx/html;
error_page 404 /404.html;
}
server {
listen 3080 ssl;
server_name gitea.example.com;
location / {
sub_filter '</footer>' 'x';
proxy_pass $backend; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
proxy_ssl_verify off;
}
}
server {
listen 3080 ssl;
server_name login.example.com;
location / { proxy_pass $backend; }
}
server {
listen 3080 ssl;
server_name modulerouter.example.com dify.example.com;
location / {
postpone_output 0; proxy_buffering off;
proxy_pass $backend;
}
}
server {
listen 3080 ssl;
server_name dashy.example.com;
auth_request /_auth;
error_page 401 = @login;
location @login { return 302 https://login.example.com/login; }
location /search { auth_request off; proxy_pass $backend; }
location ~ ^/(api|autocompleter|static)/ { auth_request off; proxy_pass $backend; }
location / { proxy_pass $backend; }
}
server {
listen 3080 ssl default_server;
server_name _;
root /usr/share/nginx/html;
location / { return 404; }
}
`
func TestParseNginxConf(t *testing.T) {
routes, err := parseNginxConf(sampleConf)
if err != nil {
t.Fatalf("解析失败: %v", err)
}
byHost := map[string]Route{}
for _, r := range routes {
byHost[r.Host] = r
}
if _, ok := byHost["_"]; ok {
t.Fatalf("不应把默认段当成服务")
}
d, ok := byHost["dashy.example.com"]
if !ok || !d.RequireLogin || d.Upstream != "http://127.0.0.1:3003" {
t.Fatalf("dashy 解析不对: %+v", d)
}
if len(d.BypassPaths) == 0 {
t.Fatalf("dashy 应解析出免认证路径: %+v", d.BypassPaths)
}
g := byHost["gitea.example.com"]
if g.RequireLogin || !g.InjectBeian || !g.InsecureTLS || !g.Websocket {
t.Fatalf("gitea 解析不对: %+v", g)
}
m := byHost["modulerouter.example.com"]
if m.RequireLogin || !m.Streaming {
t.Fatalf("modulerouter 应解析为免认证 + 流式: %+v", m)
}
if _, ok := byHost["dify.example.com"]; !ok {
t.Fatalf("同一段里多个域名都要生成路由")
}
if l := byHost["login.example.com"]; l.Kind != "auth" || l.Upstream != "" {
t.Fatalf("login 应为内置 auth: %+v", l)
}
if p := byHost["example.com"]; p.Kind != "portal" {
t.Fatalf("静态门户段应为 portal: %+v", p)
}
for _, r := range routes {
if r.NavEnabled {
t.Fatalf("导入不应默认接管导航: %+v", r)
}
}
}
func TestParseNginxConfRejectsGarbage(t *testing.T) {
if _, err := parseNginxConf(" "); err == nil {
t.Fatalf("空内容应报错")
}
if _, err := parseNginxConf("hello world"); err == nil {
t.Fatalf("没有任何 server_name 应报错")
}
}
func TestPortalAuthStore(t *testing.T) {
dir := t.TempDir()
oldConf := confFile
confFile = dir + "/conf.yml"
defer func() { confFile = oldConf }()
if err := savePortalAuthFile("jianf", "s3cret-pass"); err != nil {
t.Fatalf("保存失败: %v", err)
}
raw, err := os.ReadFile(portalAuthPath())
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(raw), "s3cret-pass") {
t.Fatalf("不得写入明文口令: %s", raw)
}
if !checkPortalCredentials("jianf", "s3cret-pass") {
t.Fatalf("正确口令应通过")
}
if checkPortalCredentials("jianf", "wrong") || checkPortalCredentials("other", "s3cret-pass") {
t.Fatalf("错误口令/用户名不得通过")
}
if err := savePortalAuthFile("admin2", "another-pass"); err != nil {
t.Fatal(err)
}
if checkPortalCredentials("jianf", "s3cret-pass") || !checkPortalCredentials("admin2", "another-pass") {
t.Fatalf("改账号后校验未按新值")
}
user, source := portalAuthConfigured()
if user != "admin2" || !strings.Contains(source, "数据卷") {
t.Fatalf("配置来源不对: %s %s", user, source)
}
}