Files
Logan.Li 16f1f7d65f 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
2026-10-26 17:21:00 +08:00

86 lines
2.2 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"sql-platform/internal/handler/resp"
"sql-platform/internal/middleware"
"sql-platform/internal/model"
"github.com/gin-gonic/gin"
)
// GET /api/v1/user/preferences
func GetUserPreferences(c *gin.Context) {
userID, _, _ := middleware.GetCurrentUser(c)
var user model.User
if err := model.DB.First(&user, userID).Error; err != nil {
resp.NotFound(c, "User not found")
return
}
prefs := map[string]string{}
if user.Preferences != nil && *user.Preferences != "" {
json.Unmarshal([]byte(*user.Preferences), &prefs)
}
c.JSON(http.StatusOK, prefs)
}
// PUT /api/v1/user/preferences
func UpdateUserPreferences(c *gin.Context) {
userID, _, _ := middleware.GetCurrentUser(c)
var prefs map[string]string
if err := c.ShouldBindJSON(&prefs); err != nil {
resp.BadRequest(c, err.Error())
return
}
data, _ := json.Marshal(prefs)
str := string(data)
result := model.DB.Model(&model.User{}).Where("id = ?", userID).Update("preferences", str)
if result.Error != nil {
resp.InternalErr(c, result.Error)
return
}
// Sync login_style and theme to SystemSetting so public endpoint picks them up
syncToSystemSetting := func(key, value string) {
var s model.SystemSetting
if model.DB.Where("`key` = ?", key).First(&s).Error == nil {
model.DB.Model(&s).Update("value", value)
} else {
model.DB.Create(&model.SystemSetting{
Key: key, Value: value, ValueType: "string",
Group: "appearance", Label: key, SortOrder: 0,
})
}
}
if v, ok := prefs["login_style"]; ok {
syncToSystemSetting("login_style", v)
}
if v, ok := prefs["theme"]; ok {
syncToSystemSetting("default_theme", v)
}
c.JSON(http.StatusOK, gin.H{"message": "Preferences updated", "preferences": prefs})
}
// GET /api/v1/public/login-style (no auth required)
func GetPublicLoginStyle(c *gin.Context) {
style := "split"
theme := "system"
var s model.SystemSetting
if model.DB.Where("`key` = ?", "login_style").Take(&s).Error == nil && s.Value != "" {
style = s.Value
}
s = model.SystemSetting{} // reset to avoid GORM reusing primary key
if model.DB.Where("`key` = ?", "default_theme").Take(&s).Error == nil && s.Value != "" {
theme = s.Value
}
c.JSON(http.StatusOK, gin.H{"login_style": style, "theme": theme})
}