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

+16 -19
View File
@@ -5,8 +5,10 @@ import (
"strconv"
"strings"
"sql-platform/internal/handler/resp"
"sql-platform/internal/middleware"
"sql-platform/internal/model"
"sql-platform/internal/safe"
"github.com/gin-gonic/gin"
)
@@ -35,14 +37,11 @@ func ListInstances(c *gin.Context) {
}
if err := query.Find(&instances).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
// mask passwords in response
for i := range instances {
instances[i].Password = ""
}
// 密码出站遮蔽由 Instance.MarshalJSON 统一收口
c.JSON(http.StatusOK, instances)
}
@@ -51,10 +50,9 @@ func GetInstance(c *gin.Context) {
id := c.Param("id")
var instance model.Instance
if err := model.DB.First(&instance, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"})
resp.NotFound(c, "Instance not found")
return
}
instance.Password = "******"
c.JSON(http.StatusOK, instance)
}
@@ -62,7 +60,7 @@ func GetInstance(c *gin.Context) {
func CreateInstance(c *gin.Context) {
var instance model.Instance
if err := c.ShouldBindJSON(&instance); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
resp.BadRequest(c, err.Error())
return
}
// Encrypt password before storing
@@ -74,11 +72,11 @@ func CreateInstance(c *gin.Context) {
}
}
if err := model.DB.Create(&instance).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
// Auto-detect version, cluster_mode and status after creation
go func() {
probe := func() {
result := testDBConnectionFull(&instance)
updates := map[string]interface{}{}
if result.Success {
@@ -95,8 +93,8 @@ func CreateInstance(c *gin.Context) {
if len(updates) > 0 {
model.DB.Model(&model.Instance{}).Where("id = ?", instance.ID).Updates(updates)
}
}()
instance.Password = "******"
}
safe.Go("instance-probe:"+strconv.Itoa(instance.ID), probe)
c.JSON(http.StatusCreated, instance)
}
@@ -105,13 +103,13 @@ func UpdateInstance(c *gin.Context) {
id := c.Param("id")
var instance model.Instance
if err := model.DB.First(&instance, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"})
resp.NotFound(c, "Instance 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
}
@@ -130,12 +128,11 @@ func UpdateInstance(c *gin.Context) {
}
if err := model.DB.Model(&instance).Updates(input).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
model.DB.First(&instance, id)
instance.Password = "******"
c.JSON(http.StatusOK, instance)
}
@@ -143,7 +140,7 @@ func UpdateInstance(c *gin.Context) {
func DeleteInstance(c *gin.Context) {
id := c.Param("id")
if err := model.DB.Delete(&model.Instance{}, 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": "Instance deleted"})
@@ -154,7 +151,7 @@ func TestConnection(c *gin.Context) {
id := c.Param("id")
var instance model.Instance
if err := model.DB.First(&instance, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"})
resp.NotFound(c, "Instance not found")
return
}
@@ -209,7 +206,7 @@ func InstanceMetrics(c *gin.Context) {
id := c.Param("id")
var instance model.Instance
if err := model.DB.First(&instance, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"})
resp.NotFound(c, "Instance not found")
return
}