feat: 交付官方 Docker 运行时,外观改背景图并下线自定义 CSS/JS
站点/后台分轨背景与用户列表排序一并落地;生产 CORS 改走 SITE_URL,健康检查带版本号。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,12 +12,14 @@ import (
|
||||
|
||||
// Config 应用全局配置
|
||||
type Config struct {
|
||||
WorkPath string
|
||||
Port int
|
||||
DataDir string
|
||||
JWTSecret string
|
||||
DBDSN string
|
||||
DevMode bool
|
||||
WorkPath string
|
||||
Port int
|
||||
DataDir string
|
||||
JWTSecret string
|
||||
DBDSN string
|
||||
DevMode bool
|
||||
SiteURL string // 对外站点 origin,如 https://bbs.example.com;生产 CORS 用
|
||||
CORSOrigins []string // 额外允许的 CORS origin(逗号分隔环境变量)
|
||||
}
|
||||
|
||||
// Parse 解析配置:环境变量 > app.ini > 默认值。
|
||||
@@ -84,6 +86,19 @@ func Parse() (*Config, error) {
|
||||
if v := os.Getenv("DEV_MODE"); v != "" {
|
||||
cfg.DevMode = strings.EqualFold(v, "true") || v == "1"
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("SITE_URL")); v != "" {
|
||||
cfg.SiteURL = strings.TrimRight(v, "/")
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("CORS_ORIGINS")); v != "" {
|
||||
cfg.CORSOrigins = splitCSVOrigins(v)
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("DATA_DIR")); v != "" {
|
||||
abs, err := filepath.Abs(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析 DATA_DIR 失败: %w", err)
|
||||
}
|
||||
cfg.DataDir = abs
|
||||
}
|
||||
|
||||
if err := rejectStrayDataDir(cfg.DataDir); err != nil {
|
||||
return nil, err
|
||||
@@ -108,6 +123,40 @@ func Parse() (*Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// AllowOrigin 供 CORS 中间件判断浏览器 Origin。
|
||||
// 开发态只放行 localhost / 127.0.0.1 的 3000 端口(与 next dev 一致);
|
||||
// 生产态放行 SITE_URL 与 CORS_ORIGINS。同源反代下浏览器不依赖 CORS,此列表作兜底。
|
||||
func (c *Config) AllowOrigin(origin string) bool {
|
||||
if origin == "" {
|
||||
return false
|
||||
}
|
||||
origin = strings.TrimRight(origin, "/")
|
||||
if c.DevMode {
|
||||
return origin == "http://localhost:3000" || origin == "http://127.0.0.1:3000"
|
||||
}
|
||||
if c.SiteURL != "" && origin == c.SiteURL {
|
||||
return true
|
||||
}
|
||||
for _, o := range c.CORSOrigins {
|
||||
if origin == o {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func splitCSVOrigins(v string) []string {
|
||||
parts := strings.Split(v, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
s := strings.TrimRight(strings.TrimSpace(p), "/")
|
||||
if s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// resolveWorkPath 定位后端根目录(app.ini 与 data/ 所在处)。
|
||||
// 优先级:JIANG13_WORK_PATH > 从 cwd 向上找 app.ini > 源码树 backend/ > cwd。
|
||||
func resolveWorkPath() (string, error) {
|
||||
|
||||
@@ -88,3 +88,54 @@ func TestRejectStrayDataDir(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowOriginDevAndProd(t *testing.T) {
|
||||
dev := &Config{DevMode: true}
|
||||
if !dev.AllowOrigin("http://localhost:3000") || !dev.AllowOrigin("http://127.0.0.1:3000") {
|
||||
t.Fatal("开发态应放行 next dev Origin")
|
||||
}
|
||||
if dev.AllowOrigin("https://bbs.example.com") {
|
||||
t.Fatal("开发态不应放行生产域名")
|
||||
}
|
||||
|
||||
prod := &Config{
|
||||
DevMode: false,
|
||||
SiteURL: "https://bbs.example.com",
|
||||
CORSOrigins: []string{"https://mirror.example.com"},
|
||||
}
|
||||
if !prod.AllowOrigin("https://bbs.example.com/") {
|
||||
t.Fatal("生产态应放行 SITE_URL")
|
||||
}
|
||||
if !prod.AllowOrigin("https://mirror.example.com") {
|
||||
t.Fatal("生产态应放行 CORS_ORIGINS")
|
||||
}
|
||||
if prod.AllowOrigin("http://localhost:3000") {
|
||||
t.Fatal("生产态不应放行 localhost")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSiteURLAndDataDir(t *testing.T) {
|
||||
work := t.TempDir()
|
||||
data := t.TempDir()
|
||||
t.Setenv("JIANG13_WORK_PATH", work)
|
||||
t.Setenv("DEV_MODE", "false")
|
||||
t.Setenv("SITE_URL", "https://bbs.example.com/")
|
||||
t.Setenv("CORS_ORIGINS", " https://a.example.com ,https://b.example.com/ ")
|
||||
t.Setenv("DATA_DIR", data)
|
||||
t.Setenv("JWT_SECRET", "test-secret-not-for-prod")
|
||||
|
||||
cfg, err := Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.DevMode {
|
||||
t.Fatal("DEV_MODE=false 应关闭开发态")
|
||||
}
|
||||
if cfg.SiteURL != "https://bbs.example.com" {
|
||||
t.Fatalf("SITE_URL 未去尾斜杠: %q", cfg.SiteURL)
|
||||
}
|
||||
if len(cfg.CORSOrigins) != 2 || cfg.CORSOrigins[0] != "https://a.example.com" || cfg.CORSOrigins[1] != "https://b.example.com" {
|
||||
t.Fatalf("CORS_ORIGINS 解析错误: %#v", cfg.CORSOrigins)
|
||||
}
|
||||
assertPath(t, cfg.DataDir, data)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user