目标:一个 HomeDash 容器取代 navi + portal-nginx + portal-auth。 - Route 扩展为"反代 + 导航"一条记录:kind(proxy/portal/auth)、streaming、 insecureTls、bypassPaths、injectBeian,以及 navEnabled/navSection/navName/ navDescription/navUrl/navIcon/navAutoIcon - 保存路由时**同时**写 gateway.yml 与 conf.yml 的导航项(gateway_nav.go): 同 host 接管而不是重复、未勾选导航的会被清理、无关手工项与系统「历史访问」分组 一律不动;两步任一步失败都回滚(避免后台显示与导航页各说各话) - 未知域名与上游故障改用**内置自定义 404/500 页面**(portal/ 目录), 不再是一行纯文本;HTTP→HTTPS 跳转段用 RedirectPort 复刻原 3081 设计 - 内置统一登录(auth.go):HMAC 签名 cookie、恒定时间比较、登录跳转白名单校验, portal-auth 容器可以退场 - 自动抓图标对托管导航项同样生效(history-server 识别 naviGatewayAutoIcon) - 后台卡片补齐"加入导航/分组/名称/描述/图标/自动抓图标/服务类型"字段 - 内部入口端口 8080 → 18080:为一体化 host 模式让开宿主机 blog(:8080) - 修掉上一轮编辑写坏的 deploy/zd.sh 一行(会让脚本直接失败) 测试:新增自定义错误页、portal 类型、导航字段校验、导航同步(接管/清理/保留)、 分组不存在必须报错等用例;admin-server 与 history-server 全绿。
141 lines
3.5 KiB
Go
141 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
gatewayManagedKey = "naviGateway"
|
|
gatewayHostKey = "naviGatewayHost"
|
|
gatewayAutoIconKey = "naviGatewayAutoIcon"
|
|
)
|
|
|
|
func normalizedHost(raw string) string {
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.ToLower(u.Hostname())
|
|
}
|
|
|
|
func navURLForRoute(r Route) string {
|
|
if r.NavURL != "" {
|
|
return strings.TrimSpace(r.NavURL)
|
|
}
|
|
return "https://" + strings.TrimSpace(r.Host)
|
|
}
|
|
|
|
// validateGatewayNavigation ensures every selected Dashy group exists before either
|
|
// gateway.yml or conf.yml is changed.
|
|
func validateGatewayNavigation(gw *GatewayConfig, dashy *Config) error {
|
|
sections := make(map[string]bool, len(dashy.Sections))
|
|
for _, s := range dashy.Sections {
|
|
sections[strings.TrimSpace(s.Name)] = true
|
|
}
|
|
for _, r := range gw.Routes {
|
|
if !r.NavEnabled {
|
|
continue
|
|
}
|
|
if !sections[strings.TrimSpace(r.NavSection)] {
|
|
return fmt.Errorf("%s: 导航分组 %q 不存在", r.Host, r.NavSection)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// syncGatewayNavigation makes gateway.yml the source of truth for proxy-managed
|
|
// Dashy items. Manually-created items that are unrelated to a managed host remain
|
|
// untouched. Existing entries for a newly-managed host are adopted instead of
|
|
// duplicated.
|
|
func syncGatewayNavigation(gw *GatewayConfig) error {
|
|
dashy, err := loadConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := validateGatewayNavigation(gw, dashy); err != nil {
|
|
return err
|
|
}
|
|
|
|
managedHosts := make(map[string]bool)
|
|
for _, r := range gw.Routes {
|
|
if r.NavEnabled {
|
|
managedHosts[strings.ToLower(strings.TrimSpace(r.Host))] = true
|
|
}
|
|
}
|
|
|
|
// Remove stale proxy-managed items and adopt existing items for hosts that are
|
|
// now managed by the gateway. The system-owned history group is never touched.
|
|
for si := range dashy.Sections {
|
|
if isHistorySection(dashy.Sections[si]) {
|
|
continue
|
|
}
|
|
kept := dashy.Sections[si].Items[:0]
|
|
for _, item := range dashy.Sections[si].Items {
|
|
itemHost := normalizedHost(item.URL)
|
|
wasManaged, _ := item.Extra[gatewayManagedKey].(bool)
|
|
if wasManaged || managedHosts[itemHost] {
|
|
continue
|
|
}
|
|
kept = append(kept, item)
|
|
}
|
|
dashy.Sections[si].Items = kept
|
|
}
|
|
|
|
for _, r := range gw.Routes {
|
|
if !r.NavEnabled {
|
|
continue
|
|
}
|
|
idx := -1
|
|
for i := range dashy.Sections {
|
|
if strings.TrimSpace(dashy.Sections[i].Name) == strings.TrimSpace(r.NavSection) {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
if idx < 0 {
|
|
return fmt.Errorf("%s: 导航分组 %q 不存在", r.Host, r.NavSection)
|
|
}
|
|
icon := strings.TrimSpace(r.NavIcon)
|
|
if icon == "" {
|
|
icon = "fas fa-globe"
|
|
}
|
|
item := NavItem{
|
|
Title: strings.TrimSpace(r.NavName),
|
|
Description: strings.TrimSpace(r.NavDescription),
|
|
Icon: icon,
|
|
URL: navURLForRoute(r),
|
|
Extra: map[string]any{
|
|
gatewayManagedKey: true,
|
|
gatewayHostKey: strings.ToLower(strings.TrimSpace(r.Host)),
|
|
gatewayAutoIconKey: r.NavAutoIcon,
|
|
},
|
|
}
|
|
dashy.Sections[idx].Items = append(dashy.Sections[idx].Items, item)
|
|
}
|
|
if err := saveConfig(dashy); err != nil {
|
|
return err
|
|
}
|
|
triggerHistoryRebuild()
|
|
return nil
|
|
}
|
|
|
|
func triggerHistoryRebuild() {
|
|
req, err := http.NewRequest(http.MethodPost, historyBaseURL+"/rebuild", bytes.NewReader(nil))
|
|
if err != nil {
|
|
return
|
|
}
|
|
if adminToken != "" {
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
}
|
|
client := &http.Client{Timeout: 3 * time.Second}
|
|
res, err := client.Do(req)
|
|
if err == nil {
|
|
_ = res.Body.Close()
|
|
}
|
|
}
|