package main import ( "encoding/json" "html" "net/http" "os" "path/filepath" "strings" ) // ── 认证页 / 门户页的可配置渲染 ── // // 原先这两页的文案写死在代码与镜像里(登录页标题、门户页标题/副标题/页脚…), // 改一次就得改代码重新部署。现在统一从 conf.yml 的 pageInfo 读取, // 缺省时回落到当前默认文案(即"不配置也照旧")。 // // 覆盖优先级(门户页): // 1. /portal/index.html —— 完全自定义整页(放进去即生效) // 2. 内置模板 + pageInfo 字段替换 // // 门户页的静态资源(/css/portal.css、favicon)仍由 nginx 提供, // 因此自定义整页时把它们放在 /portal/ 下即可覆盖。 const ( defaultPortalTitle = "JianF Git - 个人开发环境" defaultPortalHero = "代码、博客与灵感之站" defaultPortalHeroSub = "JianF Git · 代码托管 · 技术博客 · 私有服务入口" defaultAuthTitle = "统一门户" defaultAuthSubtitle = "登录后访问受保护的内部服务" defaultAuthLogo = "JF" ) // pageStr 读取 pageInfo 中的字符串,缺失时用兜底值。 func pageStr(info map[string]any, key, fallback string) string { if v := strings.TrimSpace(mapString(info, key)); v != "" { return v } return fallback } // portalOverridePath 数据卷里的整页覆盖文件。 func portalOverridePath() string { return filepath.Join(filepath.Dir(confFile), "portal", "index.html") } // renderPortalPage 渲染门户首页:优先数据卷覆盖,其次内置模板 + pageInfo 替换。 func renderPortalPage() []byte { if b, err := os.ReadFile(portalOverridePath()); err == nil && len(strings.TrimSpace(string(b))) > 0 { return b } builtin, err := os.ReadFile("/app/portal/index.html") if err != nil { return []byte("Portal

Portal

") } cfg, _ := loadConfig() info := map[string]any{} if cfg != nil { info = cfg.PageInfo } rep := strings.NewReplacer( "", html.EscapeString(pageStr(info, "portalTitle", defaultPortalTitle)), "", html.EscapeString(pageStr(info, "heroTitle", defaultPortalHero)), "", html.EscapeString(pageStr(info, "heroSubtitle", defaultPortalHeroSub)), "", beianFooter(mapString(info, "icp"), mapString(info, "gongan")), ) out := rep.Replace(string(builtin)) // 服务卡片区可覆盖:数据卷 portal/cards.html 存在时替换标记之间的内容, // 这样改门户页的服务链接不必重新构建镜像(仓库也不再预置任何真实域名)。 out = applyCardsOverride(out) // 未做占位替换的旧模板(例如自备的 index.html)原样返回 return []byte(out) } // cardsOverridePath 数据卷里的服务卡片片段。 func cardsOverridePath() string { return filepath.Join(filepath.Dir(confFile), "portal", "cards.html") } const ( cardsStartMarker = "" cardsEndMarker = "" ) // applyCardsOverride 用数据卷片段替换标记之间的卡片内容;无片段或标记缺失则原样返回。 func applyCardsOverride(page string) string { raw, err := os.ReadFile(cardsOverridePath()) if err != nil || len(strings.TrimSpace(string(raw))) == 0 { return page } i := strings.Index(page, cardsStartMarker) j := strings.Index(page, cardsEndMarker) if i < 0 || j < i { return page } return page[:i+len(cardsStartMarker)] + "\n" + string(raw) + "\n" + page[j:] } // handlePortalCards 读写服务卡片片段(后台「站点与外观」用)。 func handlePortalCards(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: raw, err := os.ReadFile(cardsOverridePath()) if err != nil { jsonResp(w, map[string]any{"html": ""}) return } jsonResp(w, map[string]any{"html": string(raw)}) case http.MethodPost: if !requireAdminToken(w, r) { return } var body struct { HTML string `json:"html"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { jsonErr(w, 400, err.Error()) return } path := cardsOverridePath() if strings.TrimSpace(body.HTML) == "" { _ = os.Remove(path) // 清空 = 回到内置卡片 jsonResp(w, map[string]any{"status": "reset"}) return } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { jsonErr(w, 500, err.Error()) return } if err := os.WriteFile(path, []byte(body.HTML), 0o644); err != nil { jsonErr(w, 500, err.Error()) return } jsonResp(w, map[string]any{"status": "saved"}) default: jsonErr(w, 405, "方法不允许") } } // handlePortalPage 供 nginx 的 kind=portal 路由反代使用。 func handlePortalPage(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") _, _ = w.Write(renderPortalPage()) } // authPageInfo 返回登录页所需字段(供 auth.go 渲染)。 func authPageInfo() (title, subtitle, logo, icp, gongan string) { cfg, err := loadConfig() info := map[string]any{} if err == nil && cfg != nil { info = cfg.PageInfo } return pageStr(info, "authTitle", defaultAuthTitle), pageStr(info, "authSubtitle", defaultAuthSubtitle), pageStr(info, "authLogo", defaultAuthLogo), mapString(info, "icp"), mapString(info, "gongan") }