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:
1 parent
d5b8b8116c
commit
16f1f7d65f
26 files changed
+916
-963
No files matched your search
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sql-platform/internal/handler/resp"
|
||||
"sql-platform/internal/middleware"
|
||||
"sql-platform/internal/model"
|
||||
|
||||
@@ -22,7 +23,7 @@ import (
|
||||
func Login(c *gin.Context) {
|
||||
var req model.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "username and password required"})
|
||||
resp.BadRequest(c, "username and password required")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -34,7 +35,7 @@ func Login(c *gin.Context) {
|
||||
maxAttempts := getSettingInt("login_max_attempts", 5)
|
||||
lockMin := getSettingInt("login_lockout_minutes", 30)
|
||||
remaining := int(user.LockedUntil.Sub(time.Now()).Minutes()) + 1
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{"detail": fmt.Sprintf("账号已锁定,连续 %d 次登录失败,请 %d 分钟后重试", maxAttempts, lockMin), "retry_after_minutes": remaining})
|
||||
resp.TooManyRequests(c, fmt.Sprintf("账号已锁定,连续 %d 次登录失败,请 %d 分钟后重试", maxAttempts, lockMin), remaining)
|
||||
return
|
||||
}
|
||||
// Lock expired → reset
|
||||
@@ -47,7 +48,7 @@ func Login(c *gin.Context) {
|
||||
if err == nil {
|
||||
// ── 本地用户存在 ──
|
||||
if user.IsActive != nil && !*user.IsActive {
|
||||
c.JSON(http.StatusForbidden, gin.H{"detail": "Account is disabled"})
|
||||
resp.Forbidden(c, "Account is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -60,7 +61,7 @@ func Login(c *gin.Context) {
|
||||
if ldapErr != nil || ldapInfo == nil {
|
||||
// Both local and LDAP failed → record failed attempt
|
||||
handleFailedLogin(&user)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"detail": "Invalid username or password"})
|
||||
resp.Unauthorized(c, "Invalid username or password")
|
||||
return
|
||||
}
|
||||
// LDAP OK — update local user info
|
||||
@@ -81,7 +82,7 @@ func Login(c *gin.Context) {
|
||||
// ── 本地不存在该用户,尝试 LDAP 认证 ──
|
||||
_, ldapInfo, ldapErr := tryLDAPAuth(req.Username, req.Password)
|
||||
if ldapErr != nil || ldapInfo == nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"detail": "Invalid username or password"})
|
||||
resp.Unauthorized(c, "Invalid username or password")
|
||||
return
|
||||
}
|
||||
// LDAP 认证成功,自动创建或恢复本地用户
|
||||
@@ -102,7 +103,7 @@ func Login(c *gin.Context) {
|
||||
// FirstOrCreate: 不存在则创建,已存在则获取(避免 uniqueIndex 冲突)
|
||||
result := model.DB.Where("username = ?", ldapInfo.Username).FirstOrCreate(&newUser)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Failed to create LDAP user: " + result.Error.Error()})
|
||||
resp.InternalErr(c, result.Error)
|
||||
return
|
||||
}
|
||||
// 更新已有用户的 display_name / email(LDAP 信息可能变更)
|
||||
@@ -119,7 +120,7 @@ func Login(c *gin.Context) {
|
||||
}
|
||||
user = newUser
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Database error"})
|
||||
resp.InternalMsg(c, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -129,7 +130,7 @@ func Login(c *gin.Context) {
|
||||
user.LockedUntil = nil
|
||||
token, err := middleware.GenerateToken(&user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Failed to generate token"})
|
||||
resp.InternalMsg(c, "Failed to generate token")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -137,13 +138,53 @@ func Login(c *gin.Context) {
|
||||
user.LastLogin = &now
|
||||
model.DB.Save(&user)
|
||||
|
||||
// 签发 refresh token(失败不阻塞登录,仅记录)
|
||||
refreshToken, refreshErr := middleware.GenerateRefreshToken(user.ID)
|
||||
if refreshErr != nil {
|
||||
fmt.Printf("generate refresh token failed: %v\n", refreshErr)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.LoginResponse{
|
||||
AccessToken: token,
|
||||
TokenType: "bearer",
|
||||
User: user,
|
||||
AccessToken: token,
|
||||
RefreshToken: refreshToken,
|
||||
TokenType: "bearer",
|
||||
User: user,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/auth/refresh — 用 refresh_token 轮换新的 token 对
|
||||
func RefreshToken(c *gin.Context) {
|
||||
var req struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.BadRequest(c, "refresh_token required")
|
||||
return
|
||||
}
|
||||
user, newRefresh, err := middleware.ConsumeRefreshToken(req.RefreshToken)
|
||||
if err != nil {
|
||||
resp.Fail(c, http.StatusUnauthorized, "登录已过期,请重新登录")
|
||||
return
|
||||
}
|
||||
access, err := middleware.GenerateToken(user)
|
||||
if err != nil {
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"access_token": access,
|
||||
"refresh_token": newRefresh,
|
||||
"token_type": "bearer",
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/auth/logout — 撤销当前用户全部 refresh token
|
||||
func Logout(c *gin.Context) {
|
||||
userID, _, _ := middleware.GetCurrentUser(c)
|
||||
middleware.RevokeUserRefreshTokens(userID)
|
||||
resp.Message(c, "Logged out")
|
||||
}
|
||||
|
||||
// LDAPAuthInfo holds extracted user info from LDAP
|
||||
type LDAPAuthInfo struct {
|
||||
Username string
|
||||
@@ -258,7 +299,7 @@ func GetMe(c *gin.Context) {
|
||||
userID, _, _ := middleware.GetCurrentUser(c)
|
||||
var user model.User
|
||||
if err := model.DB.First(&user, userID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "User not found"})
|
||||
resp.NotFound(c, "User not found")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, user)
|
||||
@@ -282,13 +323,13 @@ func CreateUser(c *gin.Context) {
|
||||
Role string `json:"role" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Failed to hash password"})
|
||||
resp.InternalMsg(c, "Failed to hash password")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -305,9 +346,9 @@ func CreateUser(c *gin.Context) {
|
||||
|
||||
if err := model.DB.Create(&user).Error; err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE") || strings.Contains(err.Error(), "duplicate") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Username already exists"})
|
||||
resp.BadRequest(c, "Username already exists")
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
||||
resp.InternalErr(c, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -320,13 +361,13 @@ func UpdateUser(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var user model.User
|
||||
if err := model.DB.First(&user, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "User not found"})
|
||||
resp.NotFound(c, "User not found")
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -337,7 +378,7 @@ func UpdateUser(c *gin.Context) {
|
||||
delete(input, "password")
|
||||
|
||||
if err := model.DB.Model(&user).Updates(input).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -350,11 +391,11 @@ func DeleteUser(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
// Protect admin (id=1) and dba (id=2) from being deleted
|
||||
if id == "1" || id == "2" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"detail": "Cannot delete system reserved users"})
|
||||
resp.Forbidden(c, "Cannot delete system reserved users")
|
||||
return
|
||||
}
|
||||
if err := model.DB.Delete(&model.User{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "User deleted"})
|
||||
|
||||
Reference in new issue
Block a user