完善站点运营设置:分路由管理、登录可见评论、邮件模板内联与关闭注册 SSR。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,19 +12,41 @@ import (
|
||||
|
||||
// Config 应用全局配置
|
||||
type Config struct {
|
||||
WorkPath string
|
||||
Port int
|
||||
DataDir string
|
||||
JWTSecret string
|
||||
DBDSN string
|
||||
DevMode bool
|
||||
SiteURL string // 对外站点 origin,如 https://bbs.example.com;生产 CORS 用
|
||||
CORSOrigins []string // 额外允许的 CORS origin(逗号分隔环境变量)
|
||||
WorkPath string
|
||||
Port int
|
||||
DataDir string
|
||||
JWTSecret string
|
||||
SettingsMasterKey string // 32 字节标准 Base64;加密邮件/存储凭据。留空则禁止保存凭据
|
||||
DBDSN string
|
||||
DevMode bool
|
||||
SiteURL string // 对外站点 origin,如 https://bbs.example.com;生产 CORS 用
|
||||
CORSOrigins []string // 额外允许的 CORS origin(逗号分隔环境变量)
|
||||
}
|
||||
|
||||
// appIniKey 描述 app.ini 中应由启动补全的键(已有值不改写)。
|
||||
type appIniKey struct {
|
||||
section string
|
||||
key string
|
||||
value string // 缺项时写入的缺省值(空串表示键存在但待填写)
|
||||
comment string // 写在键上方的注释(不含前导分号外的格式由 ini 库处理)
|
||||
}
|
||||
|
||||
// appIniSchema 是本地配置清单;启动时把缺失键补进现有 app.ini,避免升级后站长看不到新字段。
|
||||
var appIniSchema = []appIniKey{
|
||||
{section: "server", key: "HTTP_PORT", value: "3001"},
|
||||
{section: "database", key: "DSN", value: "postgres://postgres:postgres@localhost:5432/jiang13?sslmode=disable", comment: "PostgreSQL 连接串(部署时请修改账号密码)"},
|
||||
{section: "security", key: "JWT_SECRET", value: "", comment: "留空则自动生成并持久化到 data/.jwt_secret;生产环境务必显式指定强随机值"},
|
||||
{section: "security", key: "SETTINGS_MASTER_KEY", value: "", comment: "32 字节随机值的标准 Base64(openssl rand -base64 32)。留空则不能保存邮件/S3 凭据。\n环境变量 SETTINGS_MASTER_KEY 优先于此处。丢失后已加密凭据无法解密,不要提交真实值。"},
|
||||
{section: "paths", key: "DATA", value: "data"},
|
||||
{section: "app", key: "DEV_MODE", value: "true"},
|
||||
{section: "app", key: "SITE_URL", value: "", comment: "对外正式 origin,无尾斜杠。账号邮件链接与 Next 的 SITE_URL 必须一致。\n本地可填 http://localhost:3000;生产填 https://bbs.example.com。环境变量 SITE_URL 优先。"},
|
||||
{section: "app", key: "CORS_ORIGINS", value: "", comment: "额外 CORS origin,逗号分隔。环境变量 CORS_ORIGINS 优先。同源反代通常可留空。"},
|
||||
}
|
||||
|
||||
// Parse 解析配置:环境变量 > app.ini > 默认值。
|
||||
// 工作目录不跟 os.Getwd() 走:从 cwd 向上查找 backend 根(含 app.ini 或源码标记),
|
||||
// 避免在仓库根 / cmd/jiang13 启动时把上传文件写进另一套 data/。
|
||||
// 若 app.ini 缺少 schema 中的键,会就地补全缺省行(不覆盖已有值)。
|
||||
func Parse() (*Config, error) {
|
||||
workPath, err := resolveWorkPath()
|
||||
if err != nil {
|
||||
@@ -40,35 +62,13 @@ func Parse() (*Config, error) {
|
||||
DevMode: true,
|
||||
}
|
||||
|
||||
// 尝试加载 app.ini
|
||||
iniPath := filepath.Join(workPath, "app.ini")
|
||||
if _, err := os.Stat(iniPath); err == nil {
|
||||
f, err := ini.Load(iniPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载 app.ini 失败: %w", err)
|
||||
}
|
||||
if s := f.Section("server"); s.HasKey("HTTP_PORT") {
|
||||
if v, err := s.Key("HTTP_PORT").Int(); err == nil {
|
||||
cfg.Port = v
|
||||
}
|
||||
}
|
||||
if s := f.Section("database"); s.HasKey("DSN") {
|
||||
cfg.DBDSN = s.Key("DSN").String()
|
||||
}
|
||||
if s := f.Section("security"); s.HasKey("JWT_SECRET") {
|
||||
cfg.JWTSecret = s.Key("JWT_SECRET").String()
|
||||
}
|
||||
if s := f.Section("paths"); s.HasKey("DATA") {
|
||||
dataRel := s.Key("DATA").String()
|
||||
if !filepath.IsAbs(dataRel) {
|
||||
cfg.DataDir = filepath.Join(workPath, dataRel)
|
||||
} else {
|
||||
cfg.DataDir = dataRel
|
||||
}
|
||||
}
|
||||
if s := f.Section("app"); s.HasKey("DEV_MODE") {
|
||||
cfg.DevMode = s.Key("DEV_MODE").MustBool(true)
|
||||
}
|
||||
f, err := loadAndEnsureAppIni(iniPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f != nil {
|
||||
applyIni(cfg, f)
|
||||
}
|
||||
|
||||
// 环境变量覆盖
|
||||
@@ -83,6 +83,9 @@ func Parse() (*Config, error) {
|
||||
if v := os.Getenv("JWT_SECRET"); v != "" {
|
||||
cfg.JWTSecret = v
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("SETTINGS_MASTER_KEY")); v != "" {
|
||||
cfg.SettingsMasterKey = v
|
||||
}
|
||||
if v := os.Getenv("DEV_MODE"); v != "" {
|
||||
cfg.DevMode = strings.EqualFold(v, "true") || v == "1"
|
||||
}
|
||||
@@ -123,6 +126,90 @@ func Parse() (*Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func applyIni(cfg *Config, f *ini.File) {
|
||||
if s := f.Section("server"); s.HasKey("HTTP_PORT") {
|
||||
if v, err := s.Key("HTTP_PORT").Int(); err == nil {
|
||||
cfg.Port = v
|
||||
}
|
||||
}
|
||||
if s := f.Section("database"); s.HasKey("DSN") {
|
||||
cfg.DBDSN = s.Key("DSN").String()
|
||||
}
|
||||
if s := f.Section("security"); s.HasKey("JWT_SECRET") {
|
||||
cfg.JWTSecret = s.Key("JWT_SECRET").String()
|
||||
}
|
||||
if s := f.Section("security"); s.HasKey("SETTINGS_MASTER_KEY") {
|
||||
cfg.SettingsMasterKey = strings.TrimSpace(s.Key("SETTINGS_MASTER_KEY").String())
|
||||
}
|
||||
if s := f.Section("paths"); s.HasKey("DATA") {
|
||||
dataRel := s.Key("DATA").String()
|
||||
if !filepath.IsAbs(dataRel) {
|
||||
cfg.DataDir = filepath.Join(cfg.WorkPath, dataRel)
|
||||
} else {
|
||||
cfg.DataDir = dataRel
|
||||
}
|
||||
}
|
||||
if s := f.Section("app"); s.HasKey("DEV_MODE") {
|
||||
cfg.DevMode = s.Key("DEV_MODE").MustBool(true)
|
||||
}
|
||||
if s := f.Section("app"); s.HasKey("SITE_URL") {
|
||||
if v := strings.TrimSpace(s.Key("SITE_URL").String()); v != "" {
|
||||
cfg.SiteURL = strings.TrimRight(v, "/")
|
||||
}
|
||||
}
|
||||
if s := f.Section("app"); s.HasKey("CORS_ORIGINS") {
|
||||
if v := strings.TrimSpace(s.Key("CORS_ORIGINS").String()); v != "" {
|
||||
cfg.CORSOrigins = splitCSVOrigins(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loadAndEnsureAppIni 加载 app.ini;不存在则按 schema 新建;已存在则只补缺失键。
|
||||
func loadAndEnsureAppIni(path string) (*ini.File, error) {
|
||||
_, statErr := os.Stat(path)
|
||||
missingFile := os.IsNotExist(statErr)
|
||||
if statErr != nil && !missingFile {
|
||||
return nil, fmt.Errorf("读取 app.ini 失败: %w", statErr)
|
||||
}
|
||||
|
||||
var f *ini.File
|
||||
var err error
|
||||
if missingFile {
|
||||
f = ini.Empty()
|
||||
} else {
|
||||
f, err = ini.Load(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载 app.ini 失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
changed := missingFile
|
||||
for _, spec := range appIniSchema {
|
||||
sec := f.Section(spec.section)
|
||||
if !sec.HasKey(spec.key) {
|
||||
val := spec.value
|
||||
// 开发态缺 SITE_URL 时写入本机前端 origin,生产(DEV_MODE=false)只留空键提示填写
|
||||
if spec.section == "app" && spec.key == "SITE_URL" && val == "" && sec.Key("DEV_MODE").MustBool(true) {
|
||||
val = "http://localhost:3000"
|
||||
}
|
||||
k, kerr := sec.NewKey(spec.key, val)
|
||||
if kerr != nil {
|
||||
return nil, fmt.Errorf("补全 app.ini %s.%s 失败: %w", spec.section, spec.key, kerr)
|
||||
}
|
||||
if spec.comment != "" {
|
||||
k.Comment = spec.comment
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
if err := f.SaveTo(path); err != nil {
|
||||
return nil, fmt.Errorf("写回 app.ini 失败: %w", err)
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// AllowOrigin 供 CORS 中间件判断浏览器 Origin。
|
||||
// 开发态只放行 localhost / 127.0.0.1 的 3000 端口(与 next dev 一致);
|
||||
// 生产态放行 SITE_URL 与 CORS_ORIGINS。同源反代下浏览器不依赖 CORS,此列表作兜底。
|
||||
|
||||
Reference in New Issue
Block a user