精简 app.ini:仅保留端口/数据目录/JWT,业务项改由管理后台配置。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -174,21 +174,15 @@ cp app.ini.example /opt/jiang13/app.ini
|
||||
```ini
|
||||
[server]
|
||||
HTTP_PORT = 3000
|
||||
ROOT_URL = https://bbs.iioio.com
|
||||
|
||||
[paths]
|
||||
DATA = data
|
||||
|
||||
[security]
|
||||
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` > 内置默认值。
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
| 前台 SPA(React) | ✅ | 浏览、发帖、回复、管理操作已统一在 SPA 内 |
|
||||
| 管理后台 | ✅ | React 后台 `/admin/*`,与前台风格一致 |
|
||||
| 评论系统 | ✅ | 换行显示已修复 |
|
||||
| OIDC Provider | ✅ | 可供 Gitea 等站点 SSO(`ROOT_URL` + `[oauth]`) |
|
||||
| OIDC Provider | ✅ | 可供 Gitea 等站点 SSO(管理后台配置 ROOT_URL 与 OAuth 应用) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
; 姜十三论坛 Jiang13 Forum — 配置文件示例(风格类似 Gitea app.ini)
|
||||
; 复制为程序工作目录下的 app.ini 后修改。也可直接启动程序,首次会自动生成。
|
||||
; 修改后重启进程/服务生效。命令行 --port / --data 等优先级更高。
|
||||
; OIDC / 邮件 / Gitea 同步 / 对象存储等请在管理后台「系统设置」配置。
|
||||
|
||||
[server]
|
||||
HTTP_PORT = 3000
|
||||
; 对外公网根地址(无尾斜杠);也可在管理后台 OIDC 设置中填写
|
||||
; 例:https://bbs.iioio.com
|
||||
ROOT_URL = https://bbs.iioio.com
|
||||
|
||||
[paths]
|
||||
; 相对路径相对于工作目录(默认可执行文件所在目录)
|
||||
@@ -15,36 +13,3 @@ DATA = data
|
||||
[security]
|
||||
; 留空则自动生成并持久化到 data/.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
|
||||
|
||||
138
config/config.go
138
config/config.go
@@ -8,24 +8,12 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StorageTypeLocal / StorageTypeS3 上传存储后端
|
||||
// StorageTypeLocal / StorageTypeS3 上传存储后端(管理后台运行时配置)
|
||||
const (
|
||||
StorageTypeLocal = "local"
|
||||
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-style(MinIO 等通常为 true;AWS 官方多为 false)
|
||||
}
|
||||
|
||||
// Config 应用全局配置:默认读工作目录下 app.ini,命令行可覆盖
|
||||
type Config struct {
|
||||
// 工作目录(默认可执行文件所在目录)
|
||||
@@ -34,23 +22,10 @@ type Config struct {
|
||||
ConfigFile string
|
||||
// 监听端口
|
||||
Port int
|
||||
// 对外公网根地址(无尾斜杠),OIDC Issuer 使用
|
||||
RootURL string
|
||||
// 数据目录:SQLite、上传、日志(绝对路径)
|
||||
DataDir string
|
||||
// JWT 签名密钥
|
||||
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
|
||||
// 系统服务控制动作:install|uninstall|start|stop|restart|status,空表示正常运行
|
||||
@@ -113,55 +88,24 @@ func Parse() (*Config, error) {
|
||||
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{
|
||||
WorkPath: workPath,
|
||||
ConfigFile: configFile,
|
||||
Port: port,
|
||||
RootURL: normalizeRootURL(fileCfg.RootURL),
|
||||
DataDir: absData,
|
||||
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,
|
||||
},
|
||||
WorkPath: workPath,
|
||||
ConfigFile: configFile,
|
||||
Port: port,
|
||||
DataDir: absData,
|
||||
JWTSecret: jwtSecret,
|
||||
LogFile: filepath.Join(absData, "jiang13.log"),
|
||||
ServiceAction: action,
|
||||
}
|
||||
|
||||
// [storage] 仅作首次种子;运行时以管理后台为准,此处不强制校验 S3 完整性
|
||||
|
||||
needDirs := action == "" || action == "install"
|
||||
if needDirs {
|
||||
// 首次启动自动生成 app.ini,便于像 Gitea 一样改文件而不记一长串参数
|
||||
if !configExists {
|
||||
dataRel := resolveDataRelForINI(workPath, absData)
|
||||
if err := writeAppINI(configFile, fileSettings{
|
||||
Port: port,
|
||||
DataRel: dataRel,
|
||||
StorageType: StorageTypeLocal,
|
||||
S3ForcePathStyle: true,
|
||||
S3Region: "us-east-1",
|
||||
Port: port,
|
||||
DataRel: dataRel,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("生成默认配置文件失败: %w", err)
|
||||
}
|
||||
@@ -174,25 +118,9 @@ func Parse() (*Config, error) {
|
||||
iniJWT = jwtSecret
|
||||
}
|
||||
if err := writeAppINI(configFile, fileSettings{
|
||||
Port: port,
|
||||
DataRel: dataRel,
|
||||
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,
|
||||
Port: port,
|
||||
DataRel: dataRel,
|
||||
JWTSecret: iniJWT,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("更新配置文件失败: %w", err)
|
||||
}
|
||||
@@ -294,50 +222,6 @@ func (c *Config) FilterWordsPath() string {
|
||||
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 {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, n)
|
||||
|
||||
135
config/ini.go
135
config/ini.go
@@ -18,34 +18,15 @@ const (
|
||||
|
||||
// fileSettings 从 app.ini 读出的原始值(尚未解析为绝对路径)
|
||||
type fileSettings struct {
|
||||
Port int
|
||||
DataRel 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
|
||||
Port int
|
||||
DataRel string
|
||||
JWTSecret string
|
||||
}
|
||||
|
||||
func defaultFileSettings() fileSettings {
|
||||
return fileSettings{
|
||||
Port: defaultPort,
|
||||
DataRel: defaultDataRel,
|
||||
StorageType: StorageTypeLocal,
|
||||
S3Region: "us-east-1",
|
||||
S3ForcePathStyle: true,
|
||||
Port: defaultPort,
|
||||
DataRel: defaultDataRel,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +50,6 @@ func loadAppINI(path string) (fileSettings, error) {
|
||||
}
|
||||
out.Port = p
|
||||
}
|
||||
out.RootURL = strings.TrimSpace(sec.Key("ROOT_URL").String())
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -121,18 +71,10 @@ func writeAppINI(path string, s fileSettings) error {
|
||||
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
|
||||
b.WriteString("; 姜十三论坛 Jiang13 Forum — 配置文件(风格类似 Gitea app.ini)\n")
|
||||
b.WriteString("; 修改后重启进程/服务生效。命令行参数优先级高于本文件。\n")
|
||||
b.WriteString("; OIDC / 邮件 / Gitea 同步 / 对象存储等请在管理后台「系统设置」配置。\n")
|
||||
b.WriteString(";\n")
|
||||
b.WriteString("; 默认位置:程序工作目录下的 app.ini\n")
|
||||
b.WriteString("; 可用 --config / --work-path 覆盖。\n")
|
||||
@@ -140,11 +82,6 @@ func writeAppINI(path string, s fileSettings) error {
|
||||
b.WriteString("[server]\n")
|
||||
b.WriteString("HTTP_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("[paths]\n")
|
||||
b.WriteString("; 相对路径相对于工作目录(默认可执行文件所在目录)\n")
|
||||
@@ -155,66 +92,6 @@ func writeAppINI(path string, s fileSettings) error {
|
||||
b.WriteString("; 留空则自动生成并持久化到 data/.jwt_secret(勿把生产密钥提交到仓库)\n")
|
||||
b.WriteString("JWT_SECRET = ")
|
||||
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 等多为 true;AWS S3 官方多为 false\n")
|
||||
b.WriteString("FORCE_PATH_STYLE = ")
|
||||
b.WriteString(strconv.FormatBool(s.S3ForcePathStyle))
|
||||
b.WriteString("\n")
|
||||
|
||||
return os.WriteFile(path, []byte(b.String()), 0644)
|
||||
|
||||
@@ -95,7 +95,7 @@ Feed 可按 **最新发帖 / 最新回复 / 热门讨论** 切换。浅色与暗
|
||||
|
||||
- **单二进制**:静态资源已内嵌,不必再配 Nginx 专门反代前端文件;
|
||||
- **零外部数据库**:SQLite 落在数据目录,备份就是拷贝文件;
|
||||
- **app.ini 配置**:风格类似 Gitea,端口、数据目录、站点根地址一眼能改;
|
||||
- **app.ini 配置**:风格类似 Gitea,端口与数据目录一眼能改;业务项在管理后台配置;
|
||||
- **系统服务**:内置 Linux systemd / Windows Service 安装与启停;
|
||||
- **跨平台**:Windows / Linux / macOS 均可编译与运行。
|
||||
|
||||
|
||||
@@ -409,12 +409,7 @@ func (h *Handlers) userPageMeta(base, siteName, defaultImage string, user *model
|
||||
}
|
||||
|
||||
func (h *Handlers) publicBaseURL(c *gin.Context) string {
|
||||
cfgRoot := ""
|
||||
if h.Cfg != nil {
|
||||
cfgRoot = h.Cfg.RootURL
|
||||
}
|
||||
origin := requestOrigin(c)
|
||||
return h.Settings.SitePublicBaseURL(cfgRoot, origin)
|
||||
return h.Settings.SitePublicBaseURL(requestOrigin(c))
|
||||
}
|
||||
|
||||
func requestOrigin(c *gin.Context) string {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"git.iioio.com/freefire/jiang13-forum/config"
|
||||
@@ -31,24 +30,6 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
filter.LoadFromFile(cfg.FilterWordsPath())
|
||||
|
||||
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,避免刷新时先闪默认文案
|
||||
embed_static.SetSPADocumentTitle(func() string {
|
||||
return settingsSvc.SiteBranding().DocumentTitle()
|
||||
|
||||
@@ -23,12 +23,9 @@ type SitemapURL struct {
|
||||
}
|
||||
|
||||
// SitePublicBaseURL 公开站点根地址(无尾斜杠)
|
||||
// 优先 OIDC / 配置中的 RootURL,否则根据请求 Host 推断
|
||||
func (s *ForumSettingsService) SitePublicBaseURL(cfgRoot, requestOrigin string) string {
|
||||
// 优先管理后台 OIDC 中的 ROOT_URL,否则根据请求 Host 推断
|
||||
func (s *ForumSettingsService) SitePublicBaseURL(requestOrigin string) string {
|
||||
root := normalizeRootURL(s.getString(SettingOIDCRootURL, ""))
|
||||
if root == "" {
|
||||
root = normalizeRootURL(cfgRoot)
|
||||
}
|
||||
if root == "" {
|
||||
root = normalizeRootURL(requestOrigin)
|
||||
}
|
||||
|
||||
@@ -716,33 +716,6 @@ func (s *ForumSettingsService) UpdateOIDCConfig(in OIDCConfig) error {
|
||||
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 明文,供服务内部使用)
|
||||
func (s *ForumSettingsService) GiteaSyncConfig() GiteaSyncConfig {
|
||||
interval, _ := strconv.Atoi(s.getString(SettingGiteaSyncIntervalMin, "60"))
|
||||
@@ -811,26 +784,6 @@ func (s *ForumSettingsService) UpdateGiteaSyncConfig(in GiteaSyncConfig) error {
|
||||
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 读取上传存储配置(含密钥明文,供内部使用)
|
||||
func (s *ForumSettingsService) StorageConfig() StorageConfig {
|
||||
secret := s.getString(SettingStorageSecretKey, "")
|
||||
@@ -934,59 +887,6 @@ func (s *ForumSettingsService) UpdateStorageConfig(in StorageConfig) error {
|
||||
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 读取站点品牌配置
|
||||
func (s *ForumSettingsService) SiteBranding() SiteBranding {
|
||||
name := strings.TrimSpace(s.getString(SettingSiteName, siteBrandingDefaults[SettingSiteName]))
|
||||
|
||||
Reference in New Issue
Block a user