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

+35 -34
View File
@@ -8,6 +8,7 @@ import (
"strings"
"time"
"sql-platform/internal/handler/resp"
"sql-platform/internal/model"
"github.com/gin-gonic/gin"
@@ -384,13 +385,13 @@ func RedisInfo(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
}
client, err := getRedisClient(&instance, 0)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer client.Close()
@@ -398,7 +399,7 @@ func RedisInfo(c *gin.Context) {
ctx := context.Background()
info, err := client.Info(ctx).Result()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
@@ -460,13 +461,13 @@ func RedisDbsize(c *gin.Context) {
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
}
client, err := getRedisClient(&instance, db)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer client.Close()
@@ -474,7 +475,7 @@ func RedisDbsize(c *gin.Context) {
ctx := context.Background()
dbSize, err := client.DBSize(ctx).Result()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"db_size": dbSize})
@@ -510,13 +511,13 @@ func RedisScanKeys(c *gin.Context) {
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
}
client, err := getRedisClient(&instance, db)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer client.Close()
@@ -549,7 +550,7 @@ func RedisScanKeys(c *gin.Context) {
allKeys, err := client.ScanAllKeys(ctx, pattern, count)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
@@ -586,13 +587,13 @@ func RedisGetKeyDetail(c *gin.Context) {
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
}
client, err := getRedisClient(&instance, db)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer client.Close()
@@ -636,13 +637,13 @@ func RedisSlowlog(c *gin.Context) {
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
}
client, err := getRedisClient(&instance, 0)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer client.Close()
@@ -650,7 +651,7 @@ func RedisSlowlog(c *gin.Context) {
ctx := context.Background()
logs, err := client.SlowLogGet(ctx, int64(count)).Result()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
@@ -675,13 +676,13 @@ func RedisClients(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
}
client, err := getRedisClient(&instance, 0)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer client.Close()
@@ -689,7 +690,7 @@ func RedisClients(c *gin.Context) {
ctx := context.Background()
clientList, err := client.ClientList(ctx).Result()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
@@ -729,19 +730,19 @@ func RedisExecuteCommand(c *gin.Context) {
Args []string `json:"args"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
resp.BadRequest(c, err.Error())
return
}
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
}
client, err := getRedisClient(&instance, db)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer client.Close()
@@ -760,7 +761,7 @@ func RedisExecuteCommand(c *gin.Context) {
}
if !readOnly[cmd] {
c.JSON(http.StatusForbidden, gin.H{"detail": "Only read-only commands allowed via query interface"})
resp.Forbidden(c, "Only read-only commands allowed via query interface")
return
}
@@ -803,19 +804,19 @@ func RedisCreateKey(c *gin.Context) {
TTL int `json:"ttl"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
resp.BadRequest(c, err.Error())
return
}
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
}
client, err := getRedisClient(&instance, db)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer client.Close()
@@ -823,7 +824,7 @@ func RedisCreateKey(c *gin.Context) {
ctx := context.Background()
if err := writeRedisKeyValue(ctx, client, input.Key, input.Type, input.Value); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
resp.BadRequest(c, err.Error())
return
}
@@ -850,30 +851,30 @@ func RedisUpdateKey(c *gin.Context) {
TTL int `json:"ttl"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
resp.BadRequest(c, err.Error())
return
}
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
}
client, err := getRedisClient(&instance, db)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer client.Close()
ctx := context.Background()
if err := client.Del(ctx, key).Err(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
if err := writeRedisKeyValue(ctx, client, key, input.Type, input.Value); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
resp.BadRequest(c, err.Error())
return
}
if input.TTL > 0 {
@@ -895,13 +896,13 @@ func RedisDeleteKey(c *gin.Context) {
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
}
client, err := getRedisClient(&instance, db)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer client.Close()
@@ -909,7 +910,7 @@ func RedisDeleteKey(c *gin.Context) {
ctx := context.Background()
deleted, err := client.Del(ctx, key).Result()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Key deleted", "deleted": deleted})
@@ -920,7 +921,7 @@ func CloneWorkflow(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var wf model.WorkflowDefinition
if err := model.DB.First(&wf, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Workflow not found"})
resp.NotFound(c, "Workflow not found")
return
}
// clone