精简 app.ini:仅保留端口/数据目录/JWT,业务项改由管理后台配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 16:58:30 +08:00
parent 48db333272
commit 81f5d451bc
10 changed files with 24 additions and 431 deletions

View File

@@ -174,21 +174,15 @@ cp app.ini.example /opt/jiang13/app.ini
```ini ```ini
[server] [server]
HTTP_PORT = 3000 HTTP_PORT = 3000
ROOT_URL = https://bbs.iioio.com
[paths] [paths]
DATA = data DATA = data
[security] [security]
JWT_SECRET = JWT_SECRET =
[oauth]
CLIENT_ID = gitea
CLIENT_SECRET =
REDIRECT_URIS = https://git.iioio.com/user/oauth2/jiang13/callback
``` ```
完整示例见仓库根目录 [`app.ini.example`](app.ini.example)。`ROOT_URL``[oauth]` 可作首次种子;日常请在管理后台「系统设置 → OIDC / SSO」配置(保存即生效)。 完整示例见仓库根目录 [`app.ini.example`](app.ini.example)。OIDC、邮件、Gitea 同步、对象存储等请在管理后台「系统设置」配置(保存即生效)。
**优先级:** 命令行显式参数 > `app.ini` > 内置默认值。 **优先级:** 命令行显式参数 > `app.ini` > 内置默认值。

View File

@@ -14,7 +14,7 @@
| 前台 SPAReact | ✅ | 浏览、发帖、回复、管理操作已统一在 SPA 内 | | 前台 SPAReact | ✅ | 浏览、发帖、回复、管理操作已统一在 SPA 内 |
| 管理后台 | ✅ | React 后台 `/admin/*`,与前台风格一致 | | 管理后台 | ✅ | React 后台 `/admin/*`,与前台风格一致 |
| 评论系统 | ✅ | 换行显示已修复 | | 评论系统 | ✅ | 换行显示已修复 |
| OIDC Provider | ✅ | 可供 Gitea 等站点 SSO`ROOT_URL` + `[oauth]` | | OIDC Provider | ✅ | 可供 Gitea 等站点 SSO管理后台配置 ROOT_URL 与 OAuth 应用 |
--- ---

View File

@@ -1,12 +1,10 @@
; 姜十三论坛 Jiang13 Forum — 配置文件示例(风格类似 Gitea app.ini ; 姜十三论坛 Jiang13 Forum — 配置文件示例(风格类似 Gitea app.ini
; 复制为程序工作目录下的 app.ini 后修改。也可直接启动程序,首次会自动生成。 ; 复制为程序工作目录下的 app.ini 后修改。也可直接启动程序,首次会自动生成。
; 修改后重启进程/服务生效。命令行 --port / --data 等优先级更高。 ; 修改后重启进程/服务生效。命令行 --port / --data 等优先级更高。
; OIDC / 邮件 / Gitea 同步 / 对象存储等请在管理后台「系统设置」配置。
[server] [server]
HTTP_PORT = 3000 HTTP_PORT = 3000
; 对外公网根地址(无尾斜杠);也可在管理后台 OIDC 设置中填写
; 例https://bbs.iioio.com
ROOT_URL = https://bbs.iioio.com
[paths] [paths]
; 相对路径相对于工作目录(默认可执行文件所在目录) ; 相对路径相对于工作目录(默认可执行文件所在目录)
@@ -15,36 +13,3 @@ DATA = data
[security] [security]
; 留空则自动生成并持久化到 data/.jwt_secret勿把生产密钥提交到仓库 ; 留空则自动生成并持久化到 data/.jwt_secret勿把生产密钥提交到仓库
JWT_SECRET = JWT_SECRET =
[oauth]
; 可选:启动时若管理后台尚未配置,会用此处种子写入 oauth_clients 一次
; 日常请优先在管理后台「系统设置 → OIDC / SSO」修改保存即生效
; Gitea 认证源名称须与回调路径一致,例如名称 jiang13 对应:
; https://git.iioio.com/user/oauth2/jiang13/callback
CLIENT_ID = gitea
CLIENT_SECRET = 请替换为足够长的随机字符串
; 多个回调用逗号分隔
REDIRECT_URIS = https://git.iioio.com/user/oauth2/jiang13/callback
[gitea]
; 可选:同步会员公开仓库到侧栏 /projects优先在管理后台配置
; BASE_URL 例https://git.iioio.com
BASE_URL =
; 只读 Access Token需能列出用户公开仓库
TOKEN =
; 首次种子时若 BASE_URL+TOKEN 齐全且此项为 true则写入并启用同步
SYNC_ENABLED = false
[storage]
; 可选种子:仅当管理后台尚未配置时写入一次;日常请在「系统设置 → 对象存储」修改(保存即生效)
; TYPE = local | s3
TYPE = local
; 以下仅 TYPE = s3 时作为种子MinIO / 七牛 / 又拍 / 阿里云 OSS 等)
ENDPOINT =
REGION = us-east-1
BUCKET =
ACCESS_KEY =
SECRET_KEY =
PUBLIC_BASE_URL =
PREFIX =
FORCE_PATH_STYLE = true

View File

@@ -8,24 +8,12 @@ import (
"strings" "strings"
) )
// StorageTypeLocal / StorageTypeS3 上传存储后端 // StorageTypeLocal / StorageTypeS3 上传存储后端(管理后台运行时配置)
const ( const (
StorageTypeLocal = "local" StorageTypeLocal = "local"
StorageTypeS3 = "s3" StorageTypeS3 = "s3"
) )
// S3Config S3 兼容对象存储MinIO / 七牛 / 又拍 / 阿里云 OSS 等)
type S3Config struct {
Endpoint string // 例https://s3.example.com 或 s3.example.com:9000
Region string
Bucket string
AccessKey string
SecretKey string
PublicBaseURL string // 公开访问根 URL无尾斜杠上传后返回此前缀下的绝对地址
Prefix string // 对象 key 前缀(可选,如 forum/
ForcePathStyle bool // path-styleMinIO 等通常为 trueAWS 官方多为 false
}
// Config 应用全局配置:默认读工作目录下 app.ini命令行可覆盖 // Config 应用全局配置:默认读工作目录下 app.ini命令行可覆盖
type Config struct { type Config struct {
// 工作目录(默认可执行文件所在目录) // 工作目录(默认可执行文件所在目录)
@@ -34,23 +22,10 @@ type Config struct {
ConfigFile string ConfigFile string
// 监听端口 // 监听端口
Port int Port int
// 对外公网根地址无尾斜杠OIDC Issuer 使用
RootURL string
// 数据目录SQLite、上传、日志绝对路径 // 数据目录SQLite、上传、日志绝对路径
DataDir string DataDir string
// JWT 签名密钥 // JWT 签名密钥
JWTSecret string JWTSecret string
// OIDC 客户端P0写死在 app.ini供 Gitea 对接)
OAuthClientID string
OAuthClientSecret string
OAuthRedirectURIs []string
// Gitea API 同步种子(可选,运行时以管理后台为准)
GiteaBaseURL string
GiteaToken string
GiteaSyncEnabled bool
// 上传存储local默认或 s3
StorageType string
S3 S3Config
// 日志文件路径 // 日志文件路径
LogFile string LogFile string
// 系统服务控制动作install|uninstall|start|stop|restart|status空表示正常运行 // 系统服务控制动作install|uninstall|start|stop|restart|status空表示正常运行
@@ -113,44 +88,16 @@ func Parse() (*Config, error) {
jwtSecret = fileCfg.JWTSecret jwtSecret = fileCfg.JWTSecret
} }
storageType := strings.ToLower(strings.TrimSpace(fileCfg.StorageType))
if storageType == "" {
storageType = StorageTypeLocal
}
if storageType != StorageTypeLocal && storageType != StorageTypeS3 {
return nil, fmt.Errorf("storage.TYPE 无效: %q可选 local / s3", fileCfg.StorageType)
}
cfg := &Config{ cfg := &Config{
WorkPath: workPath, WorkPath: workPath,
ConfigFile: configFile, ConfigFile: configFile,
Port: port, Port: port,
RootURL: normalizeRootURL(fileCfg.RootURL),
DataDir: absData, DataDir: absData,
JWTSecret: jwtSecret, JWTSecret: jwtSecret,
OAuthClientID: fileCfg.OAuthClientID,
OAuthClientSecret: fileCfg.OAuthClientSecret,
OAuthRedirectURIs: splitCSV(fileCfg.OAuthRedirectURIs),
GiteaBaseURL: normalizeRootURL(fileCfg.GiteaBaseURL),
GiteaToken: fileCfg.GiteaToken,
GiteaSyncEnabled: fileCfg.GiteaSyncEnabled,
StorageType: storageType,
S3: S3Config{
Endpoint: strings.TrimSpace(fileCfg.S3Endpoint),
Region: strings.TrimSpace(fileCfg.S3Region),
Bucket: strings.TrimSpace(fileCfg.S3Bucket),
AccessKey: strings.TrimSpace(fileCfg.S3AccessKey),
SecretKey: strings.TrimSpace(fileCfg.S3SecretKey),
PublicBaseURL: normalizeRootURL(fileCfg.S3PublicBaseURL),
Prefix: normalizeStoragePrefix(fileCfg.S3Prefix),
ForcePathStyle: fileCfg.S3ForcePathStyle,
},
LogFile: filepath.Join(absData, "jiang13.log"), LogFile: filepath.Join(absData, "jiang13.log"),
ServiceAction: action, ServiceAction: action,
} }
// [storage] 仅作首次种子;运行时以管理后台为准,此处不强制校验 S3 完整性
needDirs := action == "" || action == "install" needDirs := action == "" || action == "install"
if needDirs { if needDirs {
// 首次启动自动生成 app.ini便于像 Gitea 一样改文件而不记一长串参数 // 首次启动自动生成 app.ini便于像 Gitea 一样改文件而不记一长串参数
@@ -159,9 +106,6 @@ func Parse() (*Config, error) {
if err := writeAppINI(configFile, fileSettings{ if err := writeAppINI(configFile, fileSettings{
Port: port, Port: port,
DataRel: dataRel, DataRel: dataRel,
StorageType: StorageTypeLocal,
S3ForcePathStyle: true,
S3Region: "us-east-1",
}); err != nil { }); err != nil {
return nil, fmt.Errorf("生成默认配置文件失败: %w", err) return nil, fmt.Errorf("生成默认配置文件失败: %w", err)
} }
@@ -177,22 +121,6 @@ func Parse() (*Config, error) {
Port: port, Port: port,
DataRel: dataRel, DataRel: dataRel,
JWTSecret: iniJWT, JWTSecret: iniJWT,
RootURL: fileCfg.RootURL,
OAuthClientID: fileCfg.OAuthClientID,
OAuthClientSecret: fileCfg.OAuthClientSecret,
OAuthRedirectURIs: fileCfg.OAuthRedirectURIs,
GiteaBaseURL: fileCfg.GiteaBaseURL,
GiteaToken: fileCfg.GiteaToken,
GiteaSyncEnabled: fileCfg.GiteaSyncEnabled,
StorageType: fileCfg.StorageType,
S3Endpoint: fileCfg.S3Endpoint,
S3Region: fileCfg.S3Region,
S3Bucket: fileCfg.S3Bucket,
S3AccessKey: fileCfg.S3AccessKey,
S3SecretKey: fileCfg.S3SecretKey,
S3PublicBaseURL: fileCfg.S3PublicBaseURL,
S3Prefix: fileCfg.S3Prefix,
S3ForcePathStyle: fileCfg.S3ForcePathStyle,
}); err != nil { }); err != nil {
return nil, fmt.Errorf("更新配置文件失败: %w", err) return nil, fmt.Errorf("更新配置文件失败: %w", err)
} }
@@ -294,50 +222,6 @@ func (c *Config) FilterWordsPath() string {
return filepath.Join(c.DataDir, "filter_words.txt") return filepath.Join(c.DataDir, "filter_words.txt")
} }
func normalizeRootURL(raw string) string {
u := strings.TrimSpace(raw)
u = strings.TrimRight(u, "/")
return u
}
// normalizeStoragePrefix 规范化对象 key 前缀:去首尾空白与首斜杠,非空时保证尾斜杠
func normalizeStoragePrefix(raw string) string {
p := strings.TrimSpace(raw)
p = strings.TrimPrefix(p, "/")
if p == "" {
return ""
}
return strings.TrimSuffix(p, "/") + "/"
}
func (s S3Config) validate() error {
if s.Endpoint == "" {
return fmt.Errorf("storage.TYPE=s3 时必须配置 ENDPOINT")
}
if s.Bucket == "" {
return fmt.Errorf("storage.TYPE=s3 时必须配置 BUCKET")
}
if s.AccessKey == "" || s.SecretKey == "" {
return fmt.Errorf("storage.TYPE=s3 时必须配置 ACCESS_KEY 与 SECRET_KEY")
}
if s.PublicBaseURL == "" {
return fmt.Errorf("storage.TYPE=s3 时必须配置 PUBLIC_BASE_URL公开访问根地址")
}
return nil
}
func splitCSV(raw string) []string {
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}
func generateRandomSecret(n int) string { func generateRandomSecret(n int) string {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, n) b := make([]byte, n)

View File

@@ -21,31 +21,12 @@ type fileSettings struct {
Port int Port int
DataRel string DataRel string
JWTSecret string JWTSecret string
RootURL string
OAuthClientID string
OAuthClientSecret string
OAuthRedirectURIs string
GiteaBaseURL string
GiteaToken string
GiteaSyncEnabled bool
StorageType string
S3Endpoint string
S3Region string
S3Bucket string
S3AccessKey string
S3SecretKey string
S3PublicBaseURL string
S3Prefix string
S3ForcePathStyle bool
} }
func defaultFileSettings() fileSettings { func defaultFileSettings() fileSettings {
return fileSettings{ return fileSettings{
Port: defaultPort, Port: defaultPort,
DataRel: defaultDataRel, DataRel: defaultDataRel,
StorageType: StorageTypeLocal,
S3Region: "us-east-1",
S3ForcePathStyle: true,
} }
} }
@@ -69,7 +50,6 @@ func loadAppINI(path string) (fileSettings, error) {
} }
out.Port = p out.Port = p
} }
out.RootURL = strings.TrimSpace(sec.Key("ROOT_URL").String())
} }
if sec, err := cfg.GetSection("paths"); err == nil { if sec, err := cfg.GetSection("paths"); err == nil {
@@ -82,36 +62,6 @@ func loadAppINI(path string) (fileSettings, error) {
out.JWTSecret = strings.TrimSpace(sec.Key("JWT_SECRET").String()) out.JWTSecret = strings.TrimSpace(sec.Key("JWT_SECRET").String())
} }
if sec, err := cfg.GetSection("oauth"); err == nil {
out.OAuthClientID = strings.TrimSpace(sec.Key("CLIENT_ID").String())
out.OAuthClientSecret = strings.TrimSpace(sec.Key("CLIENT_SECRET").String())
out.OAuthRedirectURIs = strings.TrimSpace(sec.Key("REDIRECT_URIS").String())
}
if sec, err := cfg.GetSection("gitea"); err == nil {
out.GiteaBaseURL = strings.TrimSpace(sec.Key("BASE_URL").String())
out.GiteaToken = strings.TrimSpace(sec.Key("TOKEN").String())
out.GiteaSyncEnabled = sec.Key("SYNC_ENABLED").MustBool(false)
}
if sec, err := cfg.GetSection("storage"); err == nil {
if v := strings.TrimSpace(sec.Key("TYPE").String()); v != "" {
out.StorageType = v
}
out.S3Endpoint = strings.TrimSpace(sec.Key("ENDPOINT").String())
if v := strings.TrimSpace(sec.Key("REGION").String()); v != "" {
out.S3Region = v
}
out.S3Bucket = strings.TrimSpace(sec.Key("BUCKET").String())
out.S3AccessKey = strings.TrimSpace(sec.Key("ACCESS_KEY").String())
out.S3SecretKey = strings.TrimSpace(sec.Key("SECRET_KEY").String())
out.S3PublicBaseURL = strings.TrimSpace(sec.Key("PUBLIC_BASE_URL").String())
out.S3Prefix = strings.TrimSpace(sec.Key("PREFIX").String())
if sec.HasKey("FORCE_PATH_STYLE") {
out.S3ForcePathStyle = sec.Key("FORCE_PATH_STYLE").MustBool(true)
}
}
return out, nil return out, nil
} }
@@ -121,18 +71,10 @@ func writeAppINI(path string, s fileSettings) error {
return err return err
} }
storageType := strings.TrimSpace(s.StorageType)
if storageType == "" {
storageType = StorageTypeLocal
}
region := strings.TrimSpace(s.S3Region)
if region == "" {
region = "us-east-1"
}
var b strings.Builder var b strings.Builder
b.WriteString("; 姜十三论坛 Jiang13 Forum — 配置文件(风格类似 Gitea app.ini\n") b.WriteString("; 姜十三论坛 Jiang13 Forum — 配置文件(风格类似 Gitea app.ini\n")
b.WriteString("; 修改后重启进程/服务生效。命令行参数优先级高于本文件。\n") b.WriteString("; 修改后重启进程/服务生效。命令行参数优先级高于本文件。\n")
b.WriteString("; OIDC / 邮件 / Gitea 同步 / 对象存储等请在管理后台「系统设置」配置。\n")
b.WriteString(";\n") b.WriteString(";\n")
b.WriteString("; 默认位置:程序工作目录下的 app.ini\n") b.WriteString("; 默认位置:程序工作目录下的 app.ini\n")
b.WriteString("; 可用 --config / --work-path 覆盖。\n") b.WriteString("; 可用 --config / --work-path 覆盖。\n")
@@ -140,11 +82,6 @@ func writeAppINI(path string, s fileSettings) error {
b.WriteString("[server]\n") b.WriteString("[server]\n")
b.WriteString("HTTP_PORT = ") b.WriteString("HTTP_PORT = ")
b.WriteString(strconv.Itoa(s.Port)) b.WriteString(strconv.Itoa(s.Port))
b.WriteString("\n")
b.WriteString("; 对外公网根地址无尾斜杠OIDC Issuer / Discovery 依赖此项\n")
b.WriteString("; 例https://bbs.iioio.com\n")
b.WriteString("ROOT_URL = ")
b.WriteString(s.RootURL)
b.WriteString("\n\n") b.WriteString("\n\n")
b.WriteString("[paths]\n") b.WriteString("[paths]\n")
b.WriteString("; 相对路径相对于工作目录(默认可执行文件所在目录)\n") b.WriteString("; 相对路径相对于工作目录(默认可执行文件所在目录)\n")
@@ -155,66 +92,6 @@ func writeAppINI(path string, s fileSettings) error {
b.WriteString("; 留空则自动生成并持久化到 data/.jwt_secret勿把生产密钥提交到仓库\n") b.WriteString("; 留空则自动生成并持久化到 data/.jwt_secret勿把生产密钥提交到仓库\n")
b.WriteString("JWT_SECRET = ") b.WriteString("JWT_SECRET = ")
b.WriteString(s.JWTSecret) b.WriteString(s.JWTSecret)
b.WriteString("\n\n")
b.WriteString("[oauth]\n")
b.WriteString("; 作为 OIDC Provider 时,给 Gitea 等客户端使用的凭据P0 写死在配置)\n")
b.WriteString("; Gitea 认证源名称需与回调路径一致,例如名称 jiang13 对应:\n")
b.WriteString("; https://git.iioio.com/user/oauth2/jiang13/callback\n")
b.WriteString("CLIENT_ID = ")
b.WriteString(s.OAuthClientID)
b.WriteString("\n")
b.WriteString("CLIENT_SECRET = ")
b.WriteString(s.OAuthClientSecret)
b.WriteString("\n")
b.WriteString("; 多个回调用逗号分隔\n")
b.WriteString("REDIRECT_URIS = ")
b.WriteString(s.OAuthRedirectURIs)
b.WriteString("\n\n")
b.WriteString("[gitea]\n")
b.WriteString("; 可选:同步会员公开仓库到侧栏 /projects\n")
b.WriteString("BASE_URL = ")
b.WriteString(s.GiteaBaseURL)
b.WriteString("\n")
b.WriteString("TOKEN = ")
b.WriteString(s.GiteaToken)
b.WriteString("\n")
b.WriteString("SYNC_ENABLED = ")
b.WriteString(strconv.FormatBool(s.GiteaSyncEnabled))
b.WriteString("\n\n")
b.WriteString("[storage]\n")
b.WriteString("; 可选种子:日常请在管理后台「系统设置 → 对象存储」配置(保存即生效)\n")
b.WriteString("TYPE = ")
b.WriteString(storageType)
b.WriteString("\n")
b.WriteString("; 以下仅作首次种子MinIO / 七牛 / 又拍 / 阿里云 OSS 等)\n")
b.WriteString("; ENDPOINT = https://s3.example.com\n")
b.WriteString("ENDPOINT = ")
b.WriteString(s.S3Endpoint)
b.WriteString("\n")
b.WriteString("REGION = ")
b.WriteString(region)
b.WriteString("\n")
b.WriteString("BUCKET = ")
b.WriteString(s.S3Bucket)
b.WriteString("\n")
b.WriteString("ACCESS_KEY = ")
b.WriteString(s.S3AccessKey)
b.WriteString("\n")
b.WriteString("SECRET_KEY = ")
b.WriteString(s.S3SecretKey)
b.WriteString("\n")
b.WriteString("; 公开访问根 URL无尾斜杠上传后返回 PUBLIC_BASE_URL/avatars/xxx.jpg\n")
b.WriteString("; PUBLIC_BASE_URL = https://cdn.example.com/forum\n")
b.WriteString("PUBLIC_BASE_URL = ")
b.WriteString(s.S3PublicBaseURL)
b.WriteString("\n")
b.WriteString("; 对象 key 前缀(可选),如 forum/\n")
b.WriteString("PREFIX = ")
b.WriteString(s.S3Prefix)
b.WriteString("\n")
b.WriteString("; MinIO 等多为 trueAWS S3 官方多为 false\n")
b.WriteString("FORCE_PATH_STYLE = ")
b.WriteString(strconv.FormatBool(s.S3ForcePathStyle))
b.WriteString("\n") b.WriteString("\n")
return os.WriteFile(path, []byte(b.String()), 0644) return os.WriteFile(path, []byte(b.String()), 0644)

View File

@@ -95,7 +95,7 @@ Feed 可按 **最新发帖 / 最新回复 / 热门讨论** 切换。浅色与暗
- **单二进制**:静态资源已内嵌,不必再配 Nginx 专门反代前端文件; - **单二进制**:静态资源已内嵌,不必再配 Nginx 专门反代前端文件;
- **零外部数据库**SQLite 落在数据目录,备份就是拷贝文件; - **零外部数据库**SQLite 落在数据目录,备份就是拷贝文件;
- **app.ini 配置**:风格类似 Gitea端口数据目录、站点根地址一眼能改; - **app.ini 配置**:风格类似 Gitea端口数据目录一眼能改;业务项在管理后台配置
- **系统服务**:内置 Linux systemd / Windows Service 安装与启停; - **系统服务**:内置 Linux systemd / Windows Service 安装与启停;
- **跨平台**Windows / Linux / macOS 均可编译与运行。 - **跨平台**Windows / Linux / macOS 均可编译与运行。

View File

@@ -409,12 +409,7 @@ func (h *Handlers) userPageMeta(base, siteName, defaultImage string, user *model
} }
func (h *Handlers) publicBaseURL(c *gin.Context) string { func (h *Handlers) publicBaseURL(c *gin.Context) string {
cfgRoot := "" return h.Settings.SitePublicBaseURL(requestOrigin(c))
if h.Cfg != nil {
cfgRoot = h.Cfg.RootURL
}
origin := requestOrigin(c)
return h.Settings.SitePublicBaseURL(cfgRoot, origin)
} }
func requestOrigin(c *gin.Context) string { func requestOrigin(c *gin.Context) string {

View File

@@ -6,7 +6,6 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"git.iioio.com/freefire/jiang13-forum/config" "git.iioio.com/freefire/jiang13-forum/config"
@@ -31,24 +30,6 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
filter.LoadFromFile(cfg.FilterWordsPath()) filter.LoadFromFile(cfg.FilterWordsPath())
settingsSvc := service.NewForumSettingsService() settingsSvc := service.NewForumSettingsService()
settingsSvc.SeedOIDCFromINI(
cfg.RootURL,
cfg.OAuthClientID,
cfg.OAuthClientSecret,
strings.Join(cfg.OAuthRedirectURIs, ","),
)
settingsSvc.SeedGiteaFromINI(cfg.GiteaBaseURL, cfg.GiteaToken, cfg.GiteaSyncEnabled)
settingsSvc.SeedStorageFromINI(
cfg.StorageType,
cfg.S3.Endpoint,
cfg.S3.Region,
cfg.S3.Bucket,
cfg.S3.AccessKey,
cfg.S3.SecretKey,
cfg.S3.PublicBaseURL,
cfg.S3.Prefix,
cfg.S3.ForcePathStyle,
)
// SPA 入口 HTML 注入标题与品牌 JSON避免刷新时先闪默认文案 // SPA 入口 HTML 注入标题与品牌 JSON避免刷新时先闪默认文案
embed_static.SetSPADocumentTitle(func() string { embed_static.SetSPADocumentTitle(func() string {
return settingsSvc.SiteBranding().DocumentTitle() return settingsSvc.SiteBranding().DocumentTitle()

View File

@@ -23,12 +23,9 @@ type SitemapURL struct {
} }
// SitePublicBaseURL 公开站点根地址(无尾斜杠) // SitePublicBaseURL 公开站点根地址(无尾斜杠)
// 优先 OIDC / 配置中的 RootURL否则根据请求 Host 推断 // 优先管理后台 OIDC 中的 ROOT_URL否则根据请求 Host 推断
func (s *ForumSettingsService) SitePublicBaseURL(cfgRoot, requestOrigin string) string { func (s *ForumSettingsService) SitePublicBaseURL(requestOrigin string) string {
root := normalizeRootURL(s.getString(SettingOIDCRootURL, "")) root := normalizeRootURL(s.getString(SettingOIDCRootURL, ""))
if root == "" {
root = normalizeRootURL(cfgRoot)
}
if root == "" { if root == "" {
root = normalizeRootURL(requestOrigin) root = normalizeRootURL(requestOrigin)
} }

View File

@@ -716,33 +716,6 @@ func (s *ForumSettingsService) UpdateOIDCConfig(in OIDCConfig) error {
return nil return nil
} }
// SeedOIDCFromINI 若库中尚未配置,则用 app.ini 种子一次(直写 oauth_clients
func (s *ForumSettingsService) SeedOIDCFromINI(rootURL, clientID, clientSecret, redirectURIsCSV string) {
rootURL = normalizeRootURL(rootURL)
clientID = strings.TrimSpace(clientID)
clientSecret = strings.TrimSpace(clientSecret)
uris := normalizeRedirectURIs(redirectURIsCSV)
if rootURL == "" && clientID == "" && clientSecret == "" && uris == "" {
return
}
if s.getString(SettingOIDCRootURL, "") == "" && rootURL != "" {
_ = s.setString(SettingOIDCRootURL, rootURL)
}
if CountEnabledOAuthClients() == 0 && clientID != "" && clientSecret != "" && uris != "" {
_, _ = s.CreateOAuthClient(OAuthClientInput{
ClientID: clientID,
Name: "Gitea",
RedirectURIs: uris,
ClientSecret: clientSecret,
})
}
if s.getString(SettingOIDCEnabled, "0") == "0" &&
s.getString(SettingOIDCRootURL, "") != "" &&
CountEnabledOAuthClients() > 0 {
_ = s.setString(SettingOIDCEnabled, "1")
}
}
// GiteaSyncConfig 读取 Gitea 同步配置(含 Token 明文,供服务内部使用) // GiteaSyncConfig 读取 Gitea 同步配置(含 Token 明文,供服务内部使用)
func (s *ForumSettingsService) GiteaSyncConfig() GiteaSyncConfig { func (s *ForumSettingsService) GiteaSyncConfig() GiteaSyncConfig {
interval, _ := strconv.Atoi(s.getString(SettingGiteaSyncIntervalMin, "60")) interval, _ := strconv.Atoi(s.getString(SettingGiteaSyncIntervalMin, "60"))
@@ -811,26 +784,6 @@ func (s *ForumSettingsService) UpdateGiteaSyncConfig(in GiteaSyncConfig) error {
return nil return nil
} }
// SeedGiteaFromINI 若库中尚未配置,则用 app.ini 种子一次
func (s *ForumSettingsService) SeedGiteaFromINI(baseURL, token string, enabled bool) {
baseURL = normalizeRootURL(baseURL)
token = strings.TrimSpace(token)
if baseURL == "" && token == "" && !enabled {
return
}
if s.getString(SettingGiteaBaseURL, "") == "" && baseURL != "" {
_ = s.setString(SettingGiteaBaseURL, baseURL)
}
if s.getString(SettingGiteaToken, "") == "" && token != "" {
_ = s.setString(SettingGiteaToken, token)
}
if enabled && s.getString(SettingGiteaSyncEnabled, "0") == "0" &&
s.getString(SettingGiteaBaseURL, "") != "" &&
s.getString(SettingGiteaToken, "") != "" {
_ = s.setString(SettingGiteaSyncEnabled, "1")
}
}
// StorageConfig 读取上传存储配置(含密钥明文,供内部使用) // StorageConfig 读取上传存储配置(含密钥明文,供内部使用)
func (s *ForumSettingsService) StorageConfig() StorageConfig { func (s *ForumSettingsService) StorageConfig() StorageConfig {
secret := s.getString(SettingStorageSecretKey, "") secret := s.getString(SettingStorageSecretKey, "")
@@ -934,59 +887,6 @@ func (s *ForumSettingsService) UpdateStorageConfig(in StorageConfig) error {
return nil return nil
} }
// SeedStorageFromINI 若库中尚未配置 S3则用 app.ini 种子一次
func (s *ForumSettingsService) SeedStorageFromINI(storageType, endpoint, region, bucket, accessKey, secretKey, publicBaseURL, prefix string, forcePathStyle bool) {
storageType = normalizeStorageType(storageType)
endpoint = strings.TrimSpace(endpoint)
region = strings.TrimSpace(region)
bucket = strings.TrimSpace(bucket)
accessKey = strings.TrimSpace(accessKey)
secretKey = strings.TrimSpace(secretKey)
publicBaseURL = normalizeRootURL(publicBaseURL)
prefix = normalizeObjectPrefix(prefix)
if storageType == "local" && endpoint == "" && bucket == "" && accessKey == "" && secretKey == "" && publicBaseURL == "" {
return
}
if s.getString(SettingStorageEndpoint, "") == "" && endpoint != "" {
_ = s.setString(SettingStorageEndpoint, endpoint)
}
if s.getString(SettingStorageRegion, "") == "" && region != "" {
_ = s.setString(SettingStorageRegion, region)
}
if s.getString(SettingStorageBucket, "") == "" && bucket != "" {
_ = s.setString(SettingStorageBucket, bucket)
}
if s.getString(SettingStorageAccessKey, "") == "" && accessKey != "" {
_ = s.setString(SettingStorageAccessKey, accessKey)
}
if s.getString(SettingStorageSecretKey, "") == "" && secretKey != "" {
_ = s.setString(SettingStorageSecretKey, secretKey)
}
if s.getString(SettingStoragePublicBaseURL, "") == "" && publicBaseURL != "" {
_ = s.setString(SettingStoragePublicBaseURL, publicBaseURL)
}
if s.getString(SettingStoragePrefix, "") == "" && prefix != "" {
_ = s.setString(SettingStoragePrefix, prefix)
}
// 仅当库中仍为默认 local 且 INI 明确为 s3、且关键字段齐全时切到 s3
if storageType == "s3" &&
normalizeStorageType(s.getString(SettingStorageType, "local")) == "local" &&
s.getString(SettingStorageEndpoint, "") != "" &&
s.getString(SettingStorageBucket, "") != "" &&
s.getString(SettingStorageAccessKey, "") != "" &&
s.getString(SettingStorageSecretKey, "") != "" &&
s.getString(SettingStoragePublicBaseURL, "") != "" {
_ = s.setString(SettingStorageType, "s3")
if forcePathStyle {
_ = s.setString(SettingStorageForcePathStyle, "1")
} else {
_ = s.setString(SettingStorageForcePathStyle, "0")
}
}
}
// SiteBranding 读取站点品牌配置 // SiteBranding 读取站点品牌配置
func (s *ForumSettingsService) SiteBranding() SiteBranding { func (s *ForumSettingsService) SiteBranding() SiteBranding {
name := strings.TrimSpace(s.getString(SettingSiteName, siteBrandingDefaults[SettingSiteName])) name := strings.TrimSpace(s.getString(SettingSiteName, siteBrandingDefaults[SettingSiteName]))