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

+41 -40
View File
@@ -5,6 +5,7 @@ import (
"net/http"
"reflect"
"sql-platform/internal/handler/resp"
"sql-platform/internal/model"
"bytes"
@@ -28,7 +29,7 @@ func DetailListDatabases(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
}
@@ -39,14 +40,14 @@ func DetailListDatabases(c *gin.Context) {
db, err := getDBConnection(&instance, "")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
resp.InternalErr(c, err)
return
}
defer db.Close()
rows, err := db.Query("SHOW DATABASES")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer rows.Close()
@@ -72,7 +73,7 @@ func DetailListTables(c *gin.Context) {
dbName := c.Query("database")
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
}
@@ -87,20 +88,20 @@ func DetailListTables(c *gin.Context) {
}
}
if dbName == "" {
c.JSON(http.StatusBadRequest, gin.H{"detail": "database parameter required"})
resp.BadRequest(c, "database parameter required")
return
}
db, err := getDBConnection(&instance, dbName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
resp.InternalErr(c, err)
return
}
defer db.Close()
rows, err := db.Query("SHOW TABLE STATUS")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer rows.Close()
@@ -139,7 +140,7 @@ func DetailDbSchema(c *gin.Context) {
dbName := c.Query("database")
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
}
@@ -154,13 +155,13 @@ func DetailDbSchema(c *gin.Context) {
}
}
if dbName == "" {
c.JSON(http.StatusBadRequest, gin.H{"detail": "database parameter required"})
resp.BadRequest(c, "database parameter required")
return
}
db, err := getDBConnection(&instance, dbName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
resp.InternalErr(c, err)
return
}
defer db.Close()
@@ -168,7 +169,7 @@ func DetailDbSchema(c *gin.Context) {
// Get all tables and their columns
tableRows, err := db.Query("SHOW TABLES")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer tableRows.Close()
@@ -207,7 +208,7 @@ func DetailTableDetail(c *gin.Context) {
tableName := c.Query("table")
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
}
@@ -217,13 +218,13 @@ func DetailTableDetail(c *gin.Context) {
}
}
if dbName == "" || tableName == "" {
c.JSON(http.StatusBadRequest, gin.H{"detail": "database and table parameters required"})
resp.BadRequest(c, "database and table parameters required")
return
}
db, err := getDBConnection(&instance, dbName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
resp.InternalErr(c, err)
return
}
defer db.Close()
@@ -235,7 +236,7 @@ func DetailTableDetail(c *gin.Context) {
// Get columns
colRows, err := db.Query("SHOW FULL COLUMNS FROM `" + tableName + "`")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer colRows.Close()
@@ -306,7 +307,7 @@ func DetailProcesslist(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
}
@@ -321,14 +322,14 @@ func DetailProcesslist(c *gin.Context) {
}
db, err := getDBConnection(&instance, dbName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
resp.InternalErr(c, err)
return
}
defer db.Close()
rows, err := db.Query("SHOW PROCESSLIST")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer rows.Close()
@@ -365,7 +366,7 @@ func DetailStatus(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
}
@@ -383,14 +384,14 @@ func DetailStatus(c *gin.Context) {
}
db, err := getDBConnection(&instance, dbName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
resp.InternalErr(c, err)
return
}
defer db.Close()
rows, err := db.Query("SHOW GLOBAL STATUS")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
resp.InternalErr(c, err)
return
}
defer rows.Close()
@@ -429,7 +430,7 @@ func DetailKillProcess(c *gin.Context) {
pid := c.Param("pid")
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
}
@@ -439,14 +440,14 @@ func DetailKillProcess(c *gin.Context) {
}
db, err := getDBConnection(&instance, dbName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
resp.InternalErr(c, err)
return
}
defer db.Close()
_, err = db.Exec("KILL " + pid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Kill failed: %v", err)})
resp.InternalErr(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Process " + pid + " killed"})
@@ -457,7 +458,7 @@ func DetailListUsers(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
}
@@ -472,7 +473,7 @@ func DetailListUsers(c *gin.Context) {
}
db, err := getDBConnection(&instance, dbName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
resp.InternalErr(c, err)
return
}
defer db.Close()
@@ -514,11 +515,11 @@ func ExecuteQuery(c *gin.Context) {
Offset int `json:"offset"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
resp.BadRequest(c, err.Error())
return
}
if input.InstanceID == 0 || input.SQL == "" {
c.JSON(http.StatusBadRequest, gin.H{"detail": "instance_id and sql are required"})
resp.BadRequest(c, "instance_id and sql are required")
return
}
// Read query limits from platform settings
@@ -538,7 +539,7 @@ func ExecuteQuery(c *gin.Context) {
var instance model.Instance
if err := model.DB.First(&instance, input.InstanceID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"})
resp.NotFound(c, "Instance not found")
return
}
@@ -607,7 +608,7 @@ func ExecuteQuery(c *gin.Context) {
c.JSON(http.StatusOK, result)
default:
c.JSON(http.StatusBadRequest, gin.H{"detail": "Unsupported database type: " + instance.DBType})
resp.BadRequest(c, "Unsupported database type: "+instance.DBType)
}
}
@@ -1247,13 +1248,13 @@ func executeMongoQuery(instance *model.Instance, dbName string, queryStr string,
if pipeline != nil {
// Aggregation — add $facet for count + data
facetStage := bson.D{
{"$facet", bson.D{
{"data", bson.A{
bson.D{{"$skip", offset}},
bson.D{{"$limit", limit}},
{Key: "$facet", Value: bson.D{
{Key: "data", Value: bson.A{
bson.D{{Key: "$skip", Value: offset}},
bson.D{{Key: "$limit", Value: limit}},
}},
{"count", bson.A{
bson.D{{"$count", "total"}},
{Key: "count", Value: bson.A{
bson.D{{Key: "$count", Value: "total"}},
}},
}},
}
@@ -1404,11 +1405,11 @@ func ExportQueryCSV(c *gin.Context) {
Limit int `json:"limit"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
resp.BadRequest(c, err.Error())
return
}
if input.InstanceID == 0 || input.SQL == "" {
c.JSON(http.StatusBadRequest, gin.H{"detail": "instance_id and sql are required"})
resp.BadRequest(c, "instance_id and sql are required")
return
}
if input.Limit <= 0 {
@@ -1420,7 +1421,7 @@ func ExportQueryCSV(c *gin.Context) {
var instance model.Instance
if err := model.DB.First(&instance, input.InstanceID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"})
resp.NotFound(c, "Instance not found")
return
}
@@ -1438,7 +1439,7 @@ func ExportQueryCSV(c *gin.Context) {
case "mongodb", "mongo":
exportMongoCSV(c, &instance, input.Database, input.SQL, input.Limit)
default:
c.JSON(http.StatusBadRequest, gin.H{"detail": "CSV export not supported for " + instance.DBType})
resp.BadRequest(c, "CSV export not supported for "+instance.DBType)
}
}