Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务: - 安全审计全部修复(docs/security-audit-2026-09-05.md): bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、 全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、 管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、 会话默认 7 天、限流缓存故障降级、robots.txt 端点等 - 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固 - Redis 支持库号(FCB_REDIS_DB / redis://…/db URL) - 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、 提码 5 位起、chunk 32MiB 上限、admin 审计动作等) 验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
173 lines
5.1 KiB
Go
173 lines
5.1 KiB
Go
// Package settings 提供数据库 settings KV 的运行时读写:
|
||
// env(FCB_*)提供基线,DB KV 覆盖可变项;管理端修改后立即生效。
|
||
package settings
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"log"
|
||
"sync"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
|
||
"filecodebox/internal/config"
|
||
"filecodebox/internal/model"
|
||
)
|
||
|
||
// settingsKey 数据库中的配置键(对齐参考实现)。
|
||
const settingsKey = "settings"
|
||
|
||
// Manager 设置管理器:线程安全,缓存 KV 覆盖到内存。
|
||
type Manager struct {
|
||
db *gorm.DB
|
||
cfg *config.Config
|
||
|
||
mu sync.RWMutex
|
||
secret string // jwt_secret(频繁使用,单独缓存)
|
||
initPwd string // admin_token 哈希(频繁使用,单独缓存)
|
||
}
|
||
|
||
// NewManager 构造设置管理器并加载 DB KV。
|
||
// ensure 默认配置行(首次启动时写入 settings 键)。
|
||
func NewManager(ctx context.Context, db *gorm.DB, cfg *config.Config) (*Manager, error) {
|
||
m := &Manager{db: db, cfg: cfg}
|
||
if err := m.ensureSettingsRow(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := m.Reload(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
// ensureSettingsRow 首次启动时把默认安全配置写入 KV(对齐 ensure_settings_row)。
|
||
func (m *Manager) ensureSettingsRow(ctx context.Context) error {
|
||
var row model.KeyValue
|
||
err := m.db.WithContext(ctx).Where(model.KeyValue{Key: settingsKey}).First(&row).Error
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
// 双方言:必须用 errors.Is 判定(sqlite 驱动错误链与字符串消息与 postgres 不同)
|
||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return err
|
||
}
|
||
// 不存在:写入初始配置(不含 admin_token/jwt_secret,保持未初始化状态)
|
||
initial := map[string]any{}
|
||
raw, _ := json.Marshal(initial)
|
||
row = model.KeyValue{Key: settingsKey, Value: strPtr(string(raw))}
|
||
if err := m.db.WithContext(ctx).Create(&row).Error; err != nil {
|
||
return err
|
||
}
|
||
log.Println("[settings] 系统尚未初始化,请在浏览器中打开站点并完成管理员密码设置")
|
||
return nil
|
||
}
|
||
|
||
// Reload 从数据库加载 settings KV 并覆盖到运行时配置。
|
||
func (m *Manager) Reload(ctx context.Context) error {
|
||
var row model.KeyValue
|
||
err := m.db.WithContext(ctx).Where(model.KeyValue{Key: settingsKey}).First(&row).Error
|
||
if err != nil {
|
||
// 行不存在时保持现有覆盖(双方言:errors.Is 判定)
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil
|
||
}
|
||
return err
|
||
}
|
||
kv := map[string]any{}
|
||
if row.Value != nil && *row.Value != "" {
|
||
if err := json.Unmarshal([]byte(*row.Value), &kv); err != nil {
|
||
log.Printf("[settings] settings KV 解析失败: %v", err)
|
||
}
|
||
}
|
||
// 内部键不允许通过 KV 覆盖(_ 开头)
|
||
safe := map[string]any{}
|
||
for k, v := range kv {
|
||
if len(k) > 0 && k[0] == '_' {
|
||
continue
|
||
}
|
||
safe[k] = v
|
||
}
|
||
m.mu.Lock()
|
||
m.cfg.ApplyKV(safe)
|
||
m.secret, _ = safe["jwt_secret"].(string)
|
||
m.initPwd, _ = safe["admin_token"].(string)
|
||
m.mu.Unlock()
|
||
return nil
|
||
}
|
||
|
||
// Get 返回当前配置(只读使用;不要修改返回值)。
|
||
func (m *Manager) Get() *config.Config { return m.cfg }
|
||
|
||
// SecretProvider 返回 jwt_secret 读取函数(JWT 中间件用)。
|
||
func (m *Manager) SecretProvider() func() string {
|
||
return func() string {
|
||
m.mu.RLock()
|
||
defer m.mu.RUnlock()
|
||
return m.secret
|
||
}
|
||
}
|
||
|
||
// IsInitialized 系统是否已完成初始化(管理员密码已设置且非默认密码)。
|
||
func (m *Manager) IsInitialized() bool {
|
||
m.mu.RLock()
|
||
defer m.mu.RUnlock()
|
||
if m.initPwd == "" {
|
||
return false
|
||
}
|
||
// 旧版默认密码视为未初始化(对齐 LEGACY_DEFAULT_ADMIN_TOKEN 检查)
|
||
return !verifyLegacyDefault(m.initPwd)
|
||
}
|
||
|
||
// legacyDefaultToken 参考实现的旧默认管理员密码。
|
||
const legacyDefaultToken = "FileCodeBox2023"
|
||
|
||
// verifyLegacyDefault 检查哈希是否对应旧默认密码。
|
||
func verifyLegacyDefault(hashed string) bool {
|
||
if hashed == "" {
|
||
return false
|
||
}
|
||
return VerifyPassword(legacyDefaultToken, hashed)
|
||
}
|
||
|
||
// UpdateKV 合并更新 settings KV(管理端保存配置)。
|
||
func (m *Manager) UpdateKV(ctx context.Context, patch map[string]any) error {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
// 读现有值
|
||
var row model.KeyValue
|
||
err := m.db.WithContext(ctx).Where(model.KeyValue{Key: settingsKey}).First(&row).Error
|
||
kv := map[string]any{}
|
||
if err == nil && row.Value != nil {
|
||
_ = json.Unmarshal([]byte(*row.Value), &kv)
|
||
}
|
||
for k, v := range patch {
|
||
if len(k) > 0 && k[0] == '_' {
|
||
continue
|
||
}
|
||
kv[k] = v
|
||
}
|
||
raw, err := json.Marshal(kv)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err == nil && row.ID > 0 {
|
||
row.Value = strPtr(string(raw))
|
||
return m.db.WithContext(ctx).Model(&row).Update("value", row.Value).Error
|
||
}
|
||
row = model.KeyValue{Key: settingsKey, Value: strPtr(string(raw))}
|
||
return m.db.WithContext(ctx).Create(&row).Error
|
||
}
|
||
|
||
// SystemStart 记录系统启动时间(对齐 sys_start 键)。
|
||
func (m *Manager) SystemStart(ctx context.Context) {
|
||
now := time.Now().UnixMilli()
|
||
raw, _ := json.Marshal(now)
|
||
_ = m.db.WithContext(ctx).Where(model.KeyValue{Key: "sys_start"}).
|
||
Assign(model.KeyValue{Value: strPtr(string(raw))}).
|
||
FirstOrCreate(&model.KeyValue{Key: "sys_start"}).Error
|
||
}
|
||
|
||
func strPtr(s string) *string { return &s }
|