feat(backend): unified response helpers, refresh token, security hardening

- new resp package (OK/Page/Fail/BadRequest/.../InternalErr): unified error
  envelope {detail, code?}, sanitized 500s (full error logged server-side,
  generic message to client - stops DB error leakage), pagination envelope
  {items,total,page,page_size}; migrate 200+ inline c.JSON error sites
- refresh token: 7d rotation tokens (SHA-256 hashed at rest), public
  POST /auth/refresh with single-use rotation, POST /auth/logout revokes
  all user refresh tokens (endpoint previously missing - frontend 404)
- security: gh-ost command line password masked in task status payload;
  Instance.MarshalJSON guarantees encrypted passwords never leave the API
  (removes reliance on per-handler manual masking); production (DEBUG=false)
  with default JWT secret now auto-generates and persists a random secret
- safe.Go recover wrapper for background goroutines (instance probe,
  gh-ost async)
- delete dead service layer (zero callers)
- fix pre-existing go vet unkeyed bson.E literals
This commit is contained in:
Logan.Li committed 2026-10-26 17:21:00 +08:00
1 parent d5b8b8116c
commit 16f1f7d65f
26 files changed
+916 -963

No files matched your search

+51 -7
View File
@@ -1,19 +1,27 @@
package config
import (
"crypto/rand"
"encoding/hex"
"log"
"os"
"strconv"
"sql-platform/internal/model"
"github.com/joho/godotenv"
)
// DefaultJWTSecret 仅用于本地 DEBUG;生产模式(DEBUG=false)会被 EnsureJWTSecret 替换为随机密钥
const DefaultJWTSecret = "sql-platform-secret-key-change-in-production"
type Config struct {
Port string
DBPath string
JWTSecret string
JWTExpire int // hours
GhOstPath string
Debug bool
Port string
DBPath string
JWTSecret string
JWTExpire int // hours
GhOstPath string
Debug bool
}
var C Config
@@ -24,13 +32,49 @@ func Init() {
C = Config{
Port: getEnv("PORT", "9000"),
DBPath: getEnv("DB_PATH", "../backend/sqlite3.db"),
JWTSecret: getEnv("JWT_SECRET", "sql-platform-secret-key-change-in-production"),
JWTSecret: getEnv("JWT_SECRET", DefaultJWTSecret),
JWTExpire: getEnvInt("JWT_EXPIRE_HOURS", 24),
GhOstPath: getEnv("GH_OST_PATH", "gh-ost"),
Debug: getEnvBool("DEBUG", true),
}
}
// EnsureJWTSecret 治理默认 JWT 密钥:
// - 显式配置了 JWT_SECRET(非默认值)→ 直接使用
// - DEBUG 模式使用默认密钥 → 打印告警(开发便利优先)
// - 生产模式使用默认密钥 → 自动生成随机密钥并持久化到 system_settings(重启复用;
// 首次切换会使存量会话失效一次)
//
// 必须在 model.DB 初始化并完成 AutoMigrate 之后调用。
func EnsureJWTSecret() {
if C.JWTSecret != DefaultJWTSecret {
return
}
if C.Debug {
log.Println("[WARN] 正在使用默认 JWT 密钥(DEBUG 模式);生产环境请设置 JWT_SECRET 环境变量")
return
}
var s model.SystemSetting
if err := model.DB.Where("`key` = ?", "jwt_secret").Take(&s).Error; err == nil && s.Value != "" {
C.JWTSecret = s.Value
return
}
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
log.Fatalf("生成随机 JWT 密钥失败: %v", err)
}
secret := hex.EncodeToString(buf)
if err := model.DB.Where(model.SystemSetting{Key: "jwt_secret"}).
Assign(model.SystemSetting{Value: secret, ValueType: "string", Group: "security", Label: "JWT Secret"}).
FirstOrCreate(&s).Error; err != nil {
log.Fatalf("持久化 JWT 密钥失败: %v", err)
}
C.JWTSecret = secret
log.Println("[INFO] 已自动生成随机 JWT 密钥并持久化(未显式配置 JWT_SECRET 的生产部署,存量会话将失效一次)")
}
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v