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

+62 -38
View File
@@ -8,6 +8,7 @@ import (
"strings"
"time"
"sql-platform/internal/handler/resp"
"sql-platform/internal/model"
"github.com/gin-gonic/gin"
@@ -46,7 +47,7 @@ func GetLDAPConfig(c *gin.Context) {
func UpdateLDAPConfig(c *gin.Context) {
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
resp.BadRequest(c, err.Error())
return
}
@@ -84,11 +85,23 @@ func UpdateLDAPConfig(c *gin.Context) {
if len(configs) == 0 {
// Create new config
cfg := model.LDAPConfig{
LDAPHost: getStr("ldap_host"),
AdminDN: getStr("admin_dn"),
BaseDN: getStr("base_dn"),
SearchFilter: func() *string { s := getStr("search_filter"); if s != "" { return &s }; return nil }(),
UserMapping: func() *string { s := userMappingStr; if s != "" { return &s }; return nil }(),
LDAPHost: getStr("ldap_host"),
AdminDN: getStr("admin_dn"),
BaseDN: getStr("base_dn"),
SearchFilter: func() *string {
s := getStr("search_filter")
if s != "" {
return &s
}
return nil
}(),
UserMapping: func() *string {
s := userMappingStr
if s != "" {
return &s
}
return nil
}(),
}
if port := getInt("ldap_port"); port > 0 {
cfg.LDAPPort = &port
@@ -149,13 +162,13 @@ func TestLDAPConnection(c *gin.Context) {
TestPassword string `json:"test_password"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
resp.BadRequest(c, err.Error())
return
}
var cfg model.LDAPConfig
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "LDAP 配置不存在,请先保存配置"})
resp.NotFound(c, "LDAP 配置不存在,请先保存配置")
return
}
if cfg.LDAPHost == "" {
@@ -342,17 +355,17 @@ func ListLDAPUsers(c *gin.Context) {
}
type LdapUser struct {
DN string `json:"dn"`
UID string `json:"uid"`
CN string `json:"cn"`
DN string `json:"dn"`
UID string `json:"uid"`
CN string `json:"cn"`
Mail string `json:"mail"`
}
var users []LdapUser
for _, e := range sr.Entries {
users = append(users, LdapUser{
DN: e.DN,
UID: e.GetAttributeValue("uid"),
CN: e.GetAttributeValue("cn"),
DN: e.DN,
UID: e.GetAttributeValue("uid"),
CN: e.GetAttributeValue("cn"),
Mail: e.GetAttributeValue("mail"),
})
}
@@ -367,26 +380,26 @@ func CreateLDAPOU(c *gin.Context) {
Description string `json:"description"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.OuName == "" {
c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 ou_name 参数"})
resp.BadRequest(c, "缺少 ou_name 参数")
return
}
var cfg model.LDAPConfig
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 配置不存在"})
resp.BadRequest(c, "LDAP 配置不存在")
return
}
conn, err := dialLDAP(&cfg)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 连接失败: " + err.Error()})
resp.BadRequest(c, "LDAP 连接失败: "+err.Error())
return
}
defer conn.Close()
adminPw, _ := decryptPassword(cfg.AdminPassword)
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 管理员绑定失败"})
resp.BadRequest(c, "LDAP 管理员绑定失败")
return
}
@@ -399,7 +412,7 @@ func CreateLDAPOU(c *gin.Context) {
}
if err := conn.Add(addReq); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "创建 OU 失败: " + err.Error()})
resp.BadRequest(c, "创建 OU 失败: "+err.Error())
return
}
@@ -416,13 +429,13 @@ func CreateLDAPUser(c *gin.Context) {
OuDn string `json:"ou_dn"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.Username == "" || req.Password == "" {
c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 username 或 password 参数"})
resp.BadRequest(c, "缺少 username 或 password 参数")
return
}
var cfg model.LDAPConfig
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 配置不存在"})
resp.BadRequest(c, "LDAP 配置不存在")
return
}
@@ -434,14 +447,14 @@ func CreateLDAPUser(c *gin.Context) {
conn, err := dialLDAP(&cfg)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 连接失败: " + err.Error()})
resp.BadRequest(c, "LDAP 连接失败: "+err.Error())
return
}
defer conn.Close()
adminPw, _ := decryptPassword(cfg.AdminPassword)
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 管理员绑定失败"})
resp.BadRequest(c, "LDAP 管理员绑定失败")
return
}
@@ -475,7 +488,7 @@ func CreateLDAPUser(c *gin.Context) {
}
if err := conn.Add(addReq); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "创建用户失败: " + err.Error()})
resp.BadRequest(c, "创建用户失败: "+err.Error())
return
}
@@ -496,20 +509,20 @@ func BatchImportLDAP(c *gin.Context) {
var cfg model.LDAPConfig
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 配置不存在"})
resp.BadRequest(c, "LDAP 配置不存在")
return
}
conn, err := dialLDAP(&cfg)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 连接失败: " + err.Error()})
resp.BadRequest(c, "LDAP 连接失败: "+err.Error())
return
}
defer conn.Close()
adminPw, _ := decryptPassword(cfg.AdminPassword)
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 管理员绑定失败"})
resp.BadRequest(c, "LDAP 管理员绑定失败")
return
}
@@ -534,7 +547,7 @@ func BatchImportLDAP(c *gin.Context) {
[]string{"uid", "cn", "mail"}, nil,
))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 搜索失败: " + err.Error()})
resp.BadRequest(c, "LDAP 搜索失败: "+err.Error())
return
}
@@ -551,9 +564,14 @@ func BatchImportLDAP(c *gin.Context) {
if len(req.UserIDs) > 0 {
found := false
for _, id := range req.UserIDs {
if id == uid { found = true; break }
if id == uid {
found = true
break
}
}
if !found {
continue
}
if !found { continue }
}
cn := entry.GetAttributeValue("cn")
@@ -564,8 +582,12 @@ func BatchImportLDAP(c *gin.Context) {
if err == nil {
// Update existing user
updates := map[string]interface{}{}
if cn != "" && cn != existing.DisplayName { updates["display_name"] = cn }
if mail != "" && (existing.Email == nil || *existing.Email != mail) { updates["email"] = mail }
if cn != "" && cn != existing.DisplayName {
updates["display_name"] = cn
}
if mail != "" && (existing.Email == nil || *existing.Email != mail) {
updates["email"] = mail
}
if len(updates) > 0 {
model.DB.Model(&existing).Updates(updates)
}
@@ -582,7 +604,9 @@ func BatchImportLDAP(c *gin.Context) {
IsActive: &isActive,
HashedPassword: "-",
}
if mail != "" { newUser.Email = &mail }
if mail != "" {
newUser.Email = &mail
}
if err := model.DB.Create(&newUser).Error; err != nil {
results = append(results, map[string]string{"uid": uid, "status": "error: " + err.Error()})
skipped++
@@ -606,31 +630,31 @@ func DeleteLDAPUser(c *gin.Context) {
DN string `json:"dn" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 dn 参数"})
resp.BadRequest(c, "缺少 dn 参数")
return
}
var cfg model.LDAPConfig
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 配置不存在"})
resp.BadRequest(c, "LDAP 配置不存在")
return
}
conn, err := dialLDAP(&cfg)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 连接失败: " + err.Error()})
resp.BadRequest(c, "LDAP 连接失败: "+err.Error())
return
}
defer conn.Close()
adminPw, _ := decryptPassword(cfg.AdminPassword)
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 管理员绑定失败"})
resp.BadRequest(c, "LDAP 管理员绑定失败")
return
}
if err := conn.Del(ldap.NewDelRequest(req.DN, nil)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "删除用户失败: " + err.Error()})
resp.BadRequest(c, "删除用户失败: "+err.Error())
return
}