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 (
|
||||
"time"
|
||||
|
||||
"sql-platform/internal/ghost"
|
||||
"sql-platform/internal/handler/resp"
|
||||
"sql-platform/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -37,18 +38,18 @@ func getGhostPath() string {
|
||||
// getGhostConfig returns all ghost-related settings as a map for API responses
|
||||
func getGhostConfig() gin.H {
|
||||
return gin.H{
|
||||
"enabled": isGhostEnabled(),
|
||||
"default_mode": getSetting("ghost_default_mode", "dry_run"),
|
||||
"osc_priority": getSetting("ghost_osc_priority", "native_first"),
|
||||
"env_mode": getSetting("ghost_env_mode", "development"),
|
||||
"cloud_type": getSetting("ghost_cloud_type", "self_hosted"),
|
||||
"auto_trigger_rows": getSettingInt("ghost_auto_trigger_rows", 10),
|
||||
"auto_trigger_ddl": getSetting("ghost_auto_trigger_ddl", "ADD COLUMN,MODIFY COLUMN,ADD INDEX"),
|
||||
"chunk_size": getSettingInt("ghost_default_chunk_size", 1000),
|
||||
"max_load": getSetting("ghost_max_load", "Threads_running=25"),
|
||||
"critical_load": getSetting("ghost_critical_load", "Threads_running=50"),
|
||||
"allow_on_master": getSetting("ghost_allow_on_master", "true") == "true",
|
||||
"max_lag_millis": getSettingInt("ghost_max_lag_millis", 1500),
|
||||
"enabled": isGhostEnabled(),
|
||||
"default_mode": getSetting("ghost_default_mode", "dry_run"),
|
||||
"osc_priority": getSetting("ghost_osc_priority", "native_first"),
|
||||
"env_mode": getSetting("ghost_env_mode", "development"),
|
||||
"cloud_type": getSetting("ghost_cloud_type", "self_hosted"),
|
||||
"auto_trigger_rows": getSettingInt("ghost_auto_trigger_rows", 10),
|
||||
"auto_trigger_ddl": getSetting("ghost_auto_trigger_ddl", "ADD COLUMN,MODIFY COLUMN,ADD INDEX"),
|
||||
"chunk_size": getSettingInt("ghost_default_chunk_size", 1000),
|
||||
"max_load": getSetting("ghost_max_load", "Threads_running=25"),
|
||||
"critical_load": getSetting("ghost_critical_load", "Threads_running=50"),
|
||||
"allow_on_master": getSetting("ghost_allow_on_master", "true") == "true",
|
||||
"max_lag_millis": getSettingInt("ghost_max_lag_millis", 1500),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,10 +65,10 @@ func getSettingInt(key string, fallback int) int {
|
||||
|
||||
// DDLAnalysis contains the analysis result of an ALTER statement
|
||||
type DDLAnalysis struct {
|
||||
DDLTypes []string // extracted DDL operation types
|
||||
NeedsOSC bool // whether OSC tool is recommended
|
||||
Reason string // explanation for the recommendation
|
||||
RecommendedTool string // "native", "gh-ost", "pt-osc"
|
||||
DDLTypes []string // extracted DDL operation types
|
||||
NeedsOSC bool // whether OSC tool is recommended
|
||||
Reason string // explanation for the recommendation
|
||||
RecommendedTool string // "native", "gh-ost", "pt-osc"
|
||||
}
|
||||
|
||||
// analyzeDDL inspects the ALTER statement to determine DDL types and OSC need
|
||||
@@ -81,7 +82,7 @@ func analyzeDDL(alterStmt string) DDLAnalysis {
|
||||
"ADD INDEX": "ADD INDEX", "DROP INDEX": "DROP INDEX",
|
||||
"ADD UNIQUE": "ADD INDEX", "ADD PRIMARY KEY": "ADD INDEX",
|
||||
"DROP PRIMARY KEY": "DROP INDEX",
|
||||
"RENAME COLUMN": "CHANGE COLUMN", "ALGORITHM": "ALGORITHM",
|
||||
"RENAME COLUMN": "CHANGE COLUMN", "ALGORITHM": "ALGORITHM",
|
||||
}
|
||||
for pattern, ddlType := range ddls {
|
||||
if strings.Contains(upper, pattern) {
|
||||
@@ -195,7 +196,7 @@ func GhOstAnalyzeDDL(c *gin.Context) {
|
||||
Alter string `json:"alter"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -234,16 +235,16 @@ func GhOstAnalyzeDDL(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"enabled": isGhostEnabled(),
|
||||
"ddl_types": analysis.DDLTypes,
|
||||
"needs_osc": analysis.NeedsOSC,
|
||||
"recommended_tool": analysis.RecommendedTool,
|
||||
"reason": analysis.Reason,
|
||||
"table_rows": rowCount,
|
||||
"mysql_version": mysqlVersion,
|
||||
"supports_instant": instantDDL,
|
||||
"cloud_type": cloudType,
|
||||
"config": getGhostConfig(),
|
||||
"enabled": isGhostEnabled(),
|
||||
"ddl_types": analysis.DDLTypes,
|
||||
"needs_osc": analysis.NeedsOSC,
|
||||
"recommended_tool": analysis.RecommendedTool,
|
||||
"reason": analysis.Reason,
|
||||
"table_rows": rowCount,
|
||||
"mysql_version": mysqlVersion,
|
||||
"supports_instant": instantDDL,
|
||||
"cloud_type": cloudType,
|
||||
"config": getGhostConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -274,7 +275,7 @@ func getTableRowCount(instanceID int, database, table string) int {
|
||||
// POST /api/v1/ghost/dry-run
|
||||
func GhOstDryRun(c *gin.Context) {
|
||||
if !isGhostEnabled() {
|
||||
c.JSON(http.StatusForbidden, gin.H{"detail": "gh-ost 功能已禁用,请在系统设置中开启"})
|
||||
resp.Forbidden(c, "gh-ost 功能已禁用,请在系统设置中开启")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -285,13 +286,13 @@ func GhOstDryRun(c *gin.Context) {
|
||||
Alter string `json:"alter"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
task, err := buildGhOstTask(req.InstanceID, req.Database, req.Table, req.Alter)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
task.DryRun = true
|
||||
@@ -309,7 +310,7 @@ func GhOstDryRun(c *gin.Context) {
|
||||
// POST /api/v1/ghost/execute
|
||||
func GhOstExecute(c *gin.Context) {
|
||||
if !isGhostEnabled() {
|
||||
c.JSON(http.StatusForbidden, gin.H{"detail": "gh-ost 功能已禁用,请在系统设置中开启"})
|
||||
resp.Forbidden(c, "gh-ost 功能已禁用,请在系统设置中开启")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -321,13 +322,13 @@ func GhOstExecute(c *gin.Context) {
|
||||
Force bool `json:"force"` // skip auto-trigger check
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
task, err := buildGhOstTask(req.InstanceID, req.Database, req.Table, req.Alter)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
task.DryRun = false
|
||||
@@ -408,7 +409,7 @@ func GhOstStatus(c *gin.Context) {
|
||||
taskID := c.Param("taskId")
|
||||
status, result := ghost.Manager.GetTaskStatus(taskID)
|
||||
if status == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "Task not found"})
|
||||
resp.NotFound(c, "Task not found")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -445,7 +446,7 @@ func ListAnnouncements(c *gin.Context) {
|
||||
// GET /api/v1/query-histories
|
||||
func ListQueryHistories(c *gin.Context) {
|
||||
_, _, _ = c.GetInt("user_id"), c.GetString("username"), c.GetString("role")
|
||||
|
||||
|
||||
var histories []model.QueryHistory
|
||||
model.DB.Order("id DESC").Limit(100).Find(&histories)
|
||||
c.JSON(http.StatusOK, histories)
|
||||
@@ -471,7 +472,7 @@ func UpdateSettings(c *gin.Context) {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&items); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
for _, item := range items {
|
||||
@@ -650,7 +651,7 @@ func SeedAuditRules(c *gin.Context) {
|
||||
dbType := c.Param("dbType")
|
||||
rules, ok := defaultAuditRules[dbType]
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("不支持的数据库类型: %s", dbType)})
|
||||
resp.BadRequest(c, fmt.Sprintf("不支持的数据库类型: %s", dbType))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -679,7 +680,7 @@ func SeedAuditRules(c *gin.Context) {
|
||||
Value: &value,
|
||||
}
|
||||
if err := model.DB.Create(&rule).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("创建规则失败: %s", err.Error())})
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
count++
|
||||
@@ -692,12 +693,12 @@ func SeedAuditRules(c *gin.Context) {
|
||||
func ToggleAuditRule(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "无效的规则 ID"})
|
||||
resp.BadRequest(c, "无效的规则 ID")
|
||||
return
|
||||
}
|
||||
var rule model.AuditRule
|
||||
if err := model.DB.First(&rule, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "规则不存在"})
|
||||
resp.NotFound(c, "规则不存在")
|
||||
return
|
||||
}
|
||||
newEnabled := !*rule.IsEnabled
|
||||
@@ -709,12 +710,12 @@ func ToggleAuditRule(c *gin.Context) {
|
||||
func UpdateAuditRule(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "无效的规则 ID"})
|
||||
resp.BadRequest(c, "无效的规则 ID")
|
||||
return
|
||||
}
|
||||
var rule model.AuditRule
|
||||
if err := model.DB.First(&rule, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "规则不存在"})
|
||||
resp.NotFound(c, "规则不存在")
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
@@ -724,7 +725,7 @@ func UpdateAuditRule(c *gin.Context) {
|
||||
Severity *string `json:"severity"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
updates := map[string]interface{}{}
|
||||
|
||||
Reference in new issue
Block a user