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"})
|
||||
|
||||
@@ -32,19 +32,19 @@ func DashboardStats(c *gin.Context) {
|
||||
model.DB.Model(&model.Ticket{}).Select("db_type as db_type, count(*) as count").Group("db_type").Scan(&typeDist)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"ticket_total": ticketTotal,
|
||||
"ticket_pending": ticketPending,
|
||||
"ticket_month": ticketTotal, // 简化
|
||||
"ticket_executing": ticketExecuting,
|
||||
"instance_total": instanceTotal,
|
||||
"instance_online": instanceOnline,
|
||||
"user_total": userTotal,
|
||||
"queries_today": queriesToday,
|
||||
"ticket_total": ticketTotal,
|
||||
"ticket_pending": ticketPending,
|
||||
"ticket_month": ticketTotal, // 简化
|
||||
"ticket_executing": ticketExecuting,
|
||||
"instance_total": instanceTotal,
|
||||
"instance_online": instanceOnline,
|
||||
"user_total": userTotal,
|
||||
"queries_today": queriesToday,
|
||||
"type_distribution": typeDist,
|
||||
"query_stats": gin.H{
|
||||
"total": queriesToday,
|
||||
"avg_time_ms": 0,
|
||||
"slow_count": 0,
|
||||
"total": queriesToday,
|
||||
"avg_time_ms": 0,
|
||||
"slow_count": 0,
|
||||
"type_distribution": typeDist,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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{}{}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"sql-platform/internal/handler/resp"
|
||||
"sql-platform/internal/middleware"
|
||||
"sql-platform/internal/model"
|
||||
|
||||
@@ -57,7 +58,7 @@ func ListDatabases(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
|
||||
}
|
||||
databases, err := listDatabases(&instance)
|
||||
@@ -77,7 +78,7 @@ func ListTables(c *gin.Context) {
|
||||
dbName := c.Param("db")
|
||||
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
|
||||
}
|
||||
tables, err := listTables(&instance, dbName)
|
||||
@@ -102,7 +103,7 @@ func TestNewConnection(c *gin.Context) {
|
||||
Database string `json:"database"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Invalid request"})
|
||||
resp.BadRequest(c, "Invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"sql-platform/internal/handler/resp"
|
||||
"sql-platform/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -17,7 +18,7 @@ func GetInstanceGroup(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var group model.InstanceGroup
|
||||
if err := model.DB.First(&group, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "Instance group not found"})
|
||||
resp.NotFound(c, "Instance group not found")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, enrichGroup(group))
|
||||
@@ -34,7 +35,7 @@ func CreateInstanceGroup(c *gin.Context) {
|
||||
WriteInstanceIDs []int `json:"write_instance_ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -58,7 +59,7 @@ func CreateInstanceGroup(c *gin.Context) {
|
||||
IsActive: &isActive,
|
||||
}
|
||||
if err := model.DB.Create(&group).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -83,7 +84,7 @@ func UpdateInstanceGroup(c *gin.Context) {
|
||||
gid, _ := strconv.Atoi(id)
|
||||
var group model.InstanceGroup
|
||||
if err := model.DB.First(&group, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "Instance group not found"})
|
||||
resp.NotFound(c, "Instance group not found")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,7 +101,7 @@ func UpdateInstanceGroup(c *gin.Context) {
|
||||
WriteInstanceIDs *[]int `json:"write_instance_ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -132,7 +133,7 @@ func UpdateInstanceGroup(c *gin.Context) {
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := model.DB.Model(&group).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -175,7 +176,7 @@ func UpdateInstanceGroup(c *gin.Context) {
|
||||
func DeleteInstanceGroup(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := model.DB.Delete(&model.InstanceGroup{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
// also clean up members
|
||||
@@ -191,7 +192,7 @@ func AssignGroupInstances(c *gin.Context) {
|
||||
AccessType string `json:"access_type"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
if input.AccessType == "" {
|
||||
@@ -240,7 +241,7 @@ func AssignGroupUsers(c *gin.Context) {
|
||||
Permissions []string `json:"permissions"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -278,7 +279,7 @@ func UpdateUserGroups(c *gin.Context) {
|
||||
GroupIDs []int `json:"group_ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sql-platform/internal/handler/resp"
|
||||
"sql-platform/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -46,7 +47,7 @@ func GetLDAPConfig(c *gin.Context) {
|
||||
func UpdateLDAPConfig(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -84,11 +85,23 @@ func UpdateLDAPConfig(c *gin.Context) {
|
||||
if len(configs) == 0 {
|
||||
// Create new config
|
||||
cfg := model.LDAPConfig{
|
||||
LDAPHost: getStr("ldap_host"),
|
||||
AdminDN: getStr("admin_dn"),
|
||||
BaseDN: getStr("base_dn"),
|
||||
SearchFilter: func() *string { s := getStr("search_filter"); if s != "" { return &s }; return nil }(),
|
||||
UserMapping: func() *string { s := userMappingStr; if s != "" { return &s }; return nil }(),
|
||||
LDAPHost: getStr("ldap_host"),
|
||||
AdminDN: getStr("admin_dn"),
|
||||
BaseDN: getStr("base_dn"),
|
||||
SearchFilter: func() *string {
|
||||
s := getStr("search_filter")
|
||||
if s != "" {
|
||||
return &s
|
||||
}
|
||||
return nil
|
||||
}(),
|
||||
UserMapping: func() *string {
|
||||
s := userMappingStr
|
||||
if s != "" {
|
||||
return &s
|
||||
}
|
||||
return nil
|
||||
}(),
|
||||
}
|
||||
if port := getInt("ldap_port"); port > 0 {
|
||||
cfg.LDAPPort = &port
|
||||
@@ -149,13 +162,13 @@ func TestLDAPConnection(c *gin.Context) {
|
||||
TestPassword string `json:"test_password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var cfg model.LDAPConfig
|
||||
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "LDAP 配置不存在,请先保存配置"})
|
||||
resp.NotFound(c, "LDAP 配置不存在,请先保存配置")
|
||||
return
|
||||
}
|
||||
if cfg.LDAPHost == "" {
|
||||
@@ -342,17 +355,17 @@ func ListLDAPUsers(c *gin.Context) {
|
||||
}
|
||||
|
||||
type LdapUser struct {
|
||||
DN string `json:"dn"`
|
||||
UID string `json:"uid"`
|
||||
CN string `json:"cn"`
|
||||
DN string `json:"dn"`
|
||||
UID string `json:"uid"`
|
||||
CN string `json:"cn"`
|
||||
Mail string `json:"mail"`
|
||||
}
|
||||
var users []LdapUser
|
||||
for _, e := range sr.Entries {
|
||||
users = append(users, LdapUser{
|
||||
DN: e.DN,
|
||||
UID: e.GetAttributeValue("uid"),
|
||||
CN: e.GetAttributeValue("cn"),
|
||||
DN: e.DN,
|
||||
UID: e.GetAttributeValue("uid"),
|
||||
CN: e.GetAttributeValue("cn"),
|
||||
Mail: e.GetAttributeValue("mail"),
|
||||
})
|
||||
}
|
||||
@@ -367,26 +380,26 @@ func CreateLDAPOU(c *gin.Context) {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.OuName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 ou_name 参数"})
|
||||
resp.BadRequest(c, "缺少 ou_name 参数")
|
||||
return
|
||||
}
|
||||
|
||||
var cfg model.LDAPConfig
|
||||
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 配置不存在"})
|
||||
resp.BadRequest(c, "LDAP 配置不存在")
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := dialLDAP(&cfg)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 连接失败: " + err.Error()})
|
||||
resp.BadRequest(c, "LDAP 连接失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
adminPw, _ := decryptPassword(cfg.AdminPassword)
|
||||
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 管理员绑定失败"})
|
||||
resp.BadRequest(c, "LDAP 管理员绑定失败")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -399,7 +412,7 @@ func CreateLDAPOU(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := conn.Add(addReq); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "创建 OU 失败: " + err.Error()})
|
||||
resp.BadRequest(c, "创建 OU 失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -416,13 +429,13 @@ func CreateLDAPUser(c *gin.Context) {
|
||||
OuDn string `json:"ou_dn"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.Username == "" || req.Password == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 username 或 password 参数"})
|
||||
resp.BadRequest(c, "缺少 username 或 password 参数")
|
||||
return
|
||||
}
|
||||
|
||||
var cfg model.LDAPConfig
|
||||
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 配置不存在"})
|
||||
resp.BadRequest(c, "LDAP 配置不存在")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -434,14 +447,14 @@ func CreateLDAPUser(c *gin.Context) {
|
||||
|
||||
conn, err := dialLDAP(&cfg)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 连接失败: " + err.Error()})
|
||||
resp.BadRequest(c, "LDAP 连接失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
adminPw, _ := decryptPassword(cfg.AdminPassword)
|
||||
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 管理员绑定失败"})
|
||||
resp.BadRequest(c, "LDAP 管理员绑定失败")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -475,7 +488,7 @@ func CreateLDAPUser(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := conn.Add(addReq); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "创建用户失败: " + err.Error()})
|
||||
resp.BadRequest(c, "创建用户失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -496,20 +509,20 @@ func BatchImportLDAP(c *gin.Context) {
|
||||
|
||||
var cfg model.LDAPConfig
|
||||
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 配置不存在"})
|
||||
resp.BadRequest(c, "LDAP 配置不存在")
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := dialLDAP(&cfg)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 连接失败: " + err.Error()})
|
||||
resp.BadRequest(c, "LDAP 连接失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
adminPw, _ := decryptPassword(cfg.AdminPassword)
|
||||
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 管理员绑定失败"})
|
||||
resp.BadRequest(c, "LDAP 管理员绑定失败")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -534,7 +547,7 @@ func BatchImportLDAP(c *gin.Context) {
|
||||
[]string{"uid", "cn", "mail"}, nil,
|
||||
))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 搜索失败: " + err.Error()})
|
||||
resp.BadRequest(c, "LDAP 搜索失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -551,9 +564,14 @@ func BatchImportLDAP(c *gin.Context) {
|
||||
if len(req.UserIDs) > 0 {
|
||||
found := false
|
||||
for _, id := range req.UserIDs {
|
||||
if id == uid { found = true; break }
|
||||
if id == uid {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
if !found { continue }
|
||||
}
|
||||
|
||||
cn := entry.GetAttributeValue("cn")
|
||||
@@ -564,8 +582,12 @@ func BatchImportLDAP(c *gin.Context) {
|
||||
if err == nil {
|
||||
// Update existing user
|
||||
updates := map[string]interface{}{}
|
||||
if cn != "" && cn != existing.DisplayName { updates["display_name"] = cn }
|
||||
if mail != "" && (existing.Email == nil || *existing.Email != mail) { updates["email"] = mail }
|
||||
if cn != "" && cn != existing.DisplayName {
|
||||
updates["display_name"] = cn
|
||||
}
|
||||
if mail != "" && (existing.Email == nil || *existing.Email != mail) {
|
||||
updates["email"] = mail
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
model.DB.Model(&existing).Updates(updates)
|
||||
}
|
||||
@@ -582,7 +604,9 @@ func BatchImportLDAP(c *gin.Context) {
|
||||
IsActive: &isActive,
|
||||
HashedPassword: "-",
|
||||
}
|
||||
if mail != "" { newUser.Email = &mail }
|
||||
if mail != "" {
|
||||
newUser.Email = &mail
|
||||
}
|
||||
if err := model.DB.Create(&newUser).Error; err != nil {
|
||||
results = append(results, map[string]string{"uid": uid, "status": "error: " + err.Error()})
|
||||
skipped++
|
||||
@@ -606,31 +630,31 @@ func DeleteLDAPUser(c *gin.Context) {
|
||||
DN string `json:"dn" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 dn 参数"})
|
||||
resp.BadRequest(c, "缺少 dn 参数")
|
||||
return
|
||||
}
|
||||
|
||||
var cfg model.LDAPConfig
|
||||
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 配置不存在"})
|
||||
resp.BadRequest(c, "LDAP 配置不存在")
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := dialLDAP(&cfg)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 连接失败: " + err.Error()})
|
||||
resp.BadRequest(c, "LDAP 连接失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
adminPw, _ := decryptPassword(cfg.AdminPassword)
|
||||
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 管理员绑定失败"})
|
||||
resp.BadRequest(c, "LDAP 管理员绑定失败")
|
||||
return
|
||||
}
|
||||
|
||||
if err := conn.Del(ldap.NewDelRequest(req.DN, nil)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "删除用户失败: " + err.Error()})
|
||||
resp.BadRequest(c, "删除用户失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"sql-platform/internal/handler/resp"
|
||||
"sql-platform/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -63,11 +64,11 @@ func CreateNotification(c *gin.Context) {
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "参数错误"})
|
||||
resp.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
if input.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "名称不能为空"})
|
||||
resp.BadRequest(c, "名称不能为空")
|
||||
return
|
||||
}
|
||||
cfgBytes, _ := json.Marshal(input.Config)
|
||||
@@ -79,7 +80,7 @@ func CreateNotification(c *gin.Context) {
|
||||
IsEnabled: &enabled,
|
||||
}
|
||||
if err := model.DB.Create(&ch).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "创建失败"})
|
||||
resp.InternalMsg(c, "创建失败")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, toChannelResponse(ch))
|
||||
@@ -90,12 +91,12 @@ func UpdateNotification(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var ch model.NotificationChannel
|
||||
if err := model.DB.First(&ch, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "通知渠道不存在"})
|
||||
resp.NotFound(c, "通知渠道不存在")
|
||||
return
|
||||
}
|
||||
var input map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "参数错误"})
|
||||
resp.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
if name, ok := input["name"].(string); ok {
|
||||
@@ -127,7 +128,7 @@ func TestNotificationSend(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var ch model.NotificationChannel
|
||||
if err := model.DB.First(&ch, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "通知渠道不存在"})
|
||||
resp.NotFound(c, "通知渠道不存在")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -135,7 +136,7 @@ func TestNotificationSend(c *gin.Context) {
|
||||
_ = json.Unmarshal([]byte(ch.Config), &cfg)
|
||||
webhookURL, _ := cfg["webhook_url"].(string)
|
||||
if webhookURL == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Webhook URL 未配置"})
|
||||
resp.BadRequest(c, "Webhook URL 未配置")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -150,12 +151,12 @@ func TestNotificationSend(c *gin.Context) {
|
||||
secret, _ := cfg["secret"].(string)
|
||||
sendErr = sendDingtalkWebhook(webhookURL, secret, title, body)
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "不支持的通知类型: " + ch.ChannelType})
|
||||
resp.BadRequest(c, "不支持的通知类型: "+ch.ChannelType)
|
||||
return
|
||||
}
|
||||
|
||||
if sendErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "发送失败: " + sendErr.Error()})
|
||||
resp.InternalErr(c, sendErr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "测试消息已发送"})
|
||||
|
||||
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sql-platform/internal/handler/resp"
|
||||
"sql-platform/internal/middleware"
|
||||
"sql-platform/internal/model"
|
||||
|
||||
@@ -14,7 +15,7 @@ func GetUserPreferences(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
|
||||
}
|
||||
|
||||
@@ -31,7 +32,7 @@ func UpdateUserPreferences(c *gin.Context) {
|
||||
|
||||
var prefs map[string]string
|
||||
if err := c.ShouldBindJSON(&prefs); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -40,7 +41,7 @@ func UpdateUserPreferences(c *gin.Context) {
|
||||
|
||||
result := model.DB.Model(&model.User{}).Where("id = ?", userID).Update("preferences", str)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": result.Error.Error()})
|
||||
resp.InternalErr(c, result.Error)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Package resp 统一 API 响应助手(渐进式契约统一):
|
||||
// - 成功:直接返回业务数据;分页统一 {items, total, page, page_size} 信封
|
||||
// - 失败:HTTP 状态码语义保持不变,错误体 {detail, code?}(兼容既有前端与测试)
|
||||
// - 内部错误:服务端记录完整 error,客户端只收通用文案,杜绝 DB 错误原文泄漏
|
||||
//
|
||||
// 新代码禁止直接 c.JSON 错误响应;存量调用点在迭代中逐步迁移到本包。
|
||||
package resp
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// OK 200:直接返回业务数据
|
||||
func OK(c *gin.Context, data any) {
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// Created 201:资源创建成功
|
||||
func Created(c *gin.Context, data any) {
|
||||
c.JSON(http.StatusCreated, data)
|
||||
}
|
||||
|
||||
// Page 200:统一分页信封 {items, total, page, page_size}
|
||||
func Page(c *gin.Context, items any, total int64, page, pageSize int) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// Message 200:仅含 {message} 的轻量确认(删除/测试类端点)
|
||||
func Message(c *gin.Context, msg string) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": msg})
|
||||
}
|
||||
|
||||
// Fail 通用失败:{detail}
|
||||
func Fail(c *gin.Context, status int, detail string) {
|
||||
c.JSON(status, gin.H{"detail": detail})
|
||||
}
|
||||
|
||||
// FailCode 失败并携带机器可读错误码:{detail, code}
|
||||
func FailCode(c *gin.Context, status int, code, detail string) {
|
||||
c.JSON(status, gin.H{"detail": detail, "code": code})
|
||||
}
|
||||
|
||||
func BadRequest(c *gin.Context, detail string) { Fail(c, http.StatusBadRequest, detail) }
|
||||
func Unauthorized(c *gin.Context, detail string) { Fail(c, http.StatusUnauthorized, detail) }
|
||||
func Forbidden(c *gin.Context, detail string) { Fail(c, http.StatusForbidden, detail) }
|
||||
func NotFound(c *gin.Context, detail string) { Fail(c, http.StatusNotFound, detail) }
|
||||
|
||||
// InternalErr 500:服务端 log 完整错误,客户端只收通用文案(防 DB/内部错误原文泄漏)
|
||||
func InternalErr(c *gin.Context, err error) {
|
||||
log.Printf("[resp] internal error on %s %s: %v", c.Request.Method, c.Request.URL.Path, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "服务器内部错误,请稍后重试", "code": "internal"})
|
||||
}
|
||||
|
||||
// InternalMsg 500:无底层 error、需要给用户可读文案时使用
|
||||
func InternalMsg(c *gin.Context, userMsg string) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": userMsg})
|
||||
}
|
||||
|
||||
// TooManyRequests 429:携带 retry_after_minutes
|
||||
func TooManyRequests(c *gin.Context, detail string, retryAfterMinutes int) {
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{"detail": detail, "retry_after_minutes": retryAfterMinutes})
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"sql-platform/internal/handler/resp"
|
||||
"sql-platform/internal/middleware"
|
||||
"sql-platform/internal/model"
|
||||
|
||||
@@ -28,7 +29,7 @@ func ListScheduledTasks(c *gin.Context) {
|
||||
"applicant_id = ? OR id IN (SELECT ticket_id FROM ticket_approvals WHERE approver_id = ?)",
|
||||
userID, userID,
|
||||
)
|
||||
// admin sees everything
|
||||
// admin sees everything
|
||||
}
|
||||
|
||||
var total int64
|
||||
@@ -40,8 +41,8 @@ func ListScheduledTasks(c *gin.Context) {
|
||||
// Enrich with instance_name and instance_db_type
|
||||
type ScheduledTicketItem struct {
|
||||
model.Ticket
|
||||
InstanceName string `json:"instance_name"`
|
||||
InstanceDBType string `json:"instance_db_type"`
|
||||
InstanceName string `json:"instance_name"`
|
||||
InstanceDBType string `json:"instance_db_type"`
|
||||
}
|
||||
|
||||
items := make([]ScheduledTicketItem, 0, len(tickets))
|
||||
@@ -71,19 +72,19 @@ func CancelScheduledTask(c *gin.Context) {
|
||||
|
||||
var ticket model.Ticket
|
||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "Ticket not found"})
|
||||
resp.NotFound(c, "Ticket not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Only the applicant or admin/dba can cancel
|
||||
if ticket.ApplicantID != userID && role != "admin" && role != "dba" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"detail": "Permission denied"})
|
||||
resp.Forbidden(c, "Permission denied")
|
||||
return
|
||||
}
|
||||
|
||||
// Only allow cancellation of scheduled tickets
|
||||
if !ticket.ScheduledAt.Valid {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Ticket is not scheduled"})
|
||||
resp.BadRequest(c, "Ticket is not scheduled")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"sql-platform/internal/handler/resp"
|
||||
"sql-platform/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -20,7 +21,7 @@ func AuditTicketSQL(c *gin.Context) {
|
||||
Database string `json:"database"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -184,7 +185,7 @@ func SQLFormat(c *gin.Context) {
|
||||
SQL string `json:"sql" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -200,7 +201,7 @@ func SQLAnalyze(c *gin.Context) {
|
||||
DBType string `json:"db_type"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+139
-130
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"sql-platform/internal/ghost"
|
||||
"sql-platform/internal/handler/resp"
|
||||
"sql-platform/internal/middleware"
|
||||
"sql-platform/internal/model"
|
||||
|
||||
@@ -21,7 +22,7 @@ import (
|
||||
// SqlExecItem records the execution result of a single SQL statement
|
||||
type SqlExecItem struct {
|
||||
SQL string `json:"sql"`
|
||||
Status string `json:"status"` // success / fail
|
||||
Status string `json:"status"` // success / fail
|
||||
AffectedRows int64 `json:"affected_rows"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Error string `json:"error,omitempty"`
|
||||
@@ -39,15 +40,15 @@ type SqlExecResult struct {
|
||||
// GET /api/v1/tickets/statistics
|
||||
func TicketStatistics(c *gin.Context) {
|
||||
userID, _, role := middleware.GetCurrentUser(c)
|
||||
|
||||
|
||||
type statusCount struct {
|
||||
Status string `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
var counts []statusCount
|
||||
|
||||
|
||||
query := model.DB.Model(&model.Ticket{}).Select("status, count(*) as count")
|
||||
|
||||
|
||||
// Filter by user role
|
||||
switch role {
|
||||
case "developer":
|
||||
@@ -58,11 +59,11 @@ func TicketStatistics(c *gin.Context) {
|
||||
"applicant_id = ? OR id IN (SELECT ticket_id FROM ticket_approvals WHERE approver_id = ?)",
|
||||
userID, userID,
|
||||
)
|
||||
// admin sees everything
|
||||
// admin sees everything
|
||||
}
|
||||
|
||||
|
||||
query.Group("status").Scan(&counts)
|
||||
|
||||
|
||||
stats := model.TicketStatistics{}
|
||||
for _, c := range counts {
|
||||
switch c.Status {
|
||||
@@ -81,7 +82,7 @@ func TicketStatistics(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
stats.Total = stats.Pending + stats.Reviewing + stats.Executing + stats.Completed + stats.Failed + stats.Rejected
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
|
||||
@@ -91,17 +92,21 @@ func ListTickets(c *gin.Context) {
|
||||
status := c.Query("status")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
if page < 1 { page = 1 }
|
||||
if limit < 1 || limit > 100 { limit = 20 }
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
query := model.DB.Model(&model.Ticket{}).Order("id DESC")
|
||||
|
||||
|
||||
// keyword search
|
||||
keyword := c.Query("keyword")
|
||||
if keyword != "" {
|
||||
query = query.Where("title LIKE ? OR ticket_no LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
|
||||
// Filter by user role
|
||||
switch role {
|
||||
case "admin":
|
||||
@@ -112,24 +117,24 @@ func ListTickets(c *gin.Context) {
|
||||
userID, userID,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if status != "" && status != "all" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
|
||||
var tickets []model.Ticket
|
||||
query.Offset((page - 1) * limit).Limit(limit).Find(&tickets)
|
||||
|
||||
// Enrich tickets with has_audit_step info
|
||||
type TicketWithMeta struct {
|
||||
model.Ticket
|
||||
HasAuditStep bool `json:"has_audit_step"`
|
||||
HasExecuteStep bool `json:"has_execute_step"`
|
||||
IsExecutor bool `json:"is_executor"`
|
||||
IsApprover bool `json:"is_approver"`
|
||||
HasAuditStep bool `json:"has_audit_step"`
|
||||
HasExecuteStep bool `json:"has_execute_step"`
|
||||
IsExecutor bool `json:"is_executor"`
|
||||
IsApprover bool `json:"is_approver"`
|
||||
}
|
||||
var result []TicketWithMeta
|
||||
for _, t := range tickets {
|
||||
@@ -150,9 +155,9 @@ func ListTickets(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"items": result,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"items": result,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": limit,
|
||||
})
|
||||
}
|
||||
@@ -162,10 +167,10 @@ func GetTicket(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var ticket model.Ticket
|
||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "Ticket not found"})
|
||||
resp.NotFound(c, "Ticket not found")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Get approvals — enrich with user display names
|
||||
var approvals []model.TicketApproval
|
||||
model.DB.Where("ticket_id = ?", id).Order("step_number, id").Find(&approvals)
|
||||
@@ -191,11 +196,11 @@ func GetTicket(c *gin.Context) {
|
||||
}
|
||||
approvalsResult = append(approvalsResult, item)
|
||||
}
|
||||
|
||||
|
||||
// Get SQL items
|
||||
var sqlItems []model.TicketSQLItem
|
||||
model.DB.Where("ticket_id = ?", id).Order("seq").Find(&sqlItems)
|
||||
|
||||
|
||||
// Flatten ticket fields into response + add nested data
|
||||
result := gin.H{}
|
||||
// Marshal ticket to JSON then unmarshal to map for flattening
|
||||
@@ -203,7 +208,7 @@ func GetTicket(c *gin.Context) {
|
||||
json.Unmarshal(ticketBytes, &result)
|
||||
result["approvals"] = approvalsResult
|
||||
result["sql_items"] = sqlItems
|
||||
|
||||
|
||||
// Add applicant_name and instance_name for frontend
|
||||
var applicant model.User
|
||||
if err := model.DB.First(&applicant, ticket.ApplicantID).Error; err == nil {
|
||||
@@ -213,7 +218,7 @@ func GetTicket(c *gin.Context) {
|
||||
if err := model.DB.First(&inst, ticket.InstanceID).Error; err == nil {
|
||||
result["instance_name"] = inst.Name
|
||||
}
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
@@ -223,7 +228,7 @@ func CreateTicket(c *gin.Context) {
|
||||
|
||||
// auditor cannot create tickets
|
||||
if userRole == "auditor" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"detail": "权限不足:审计员不能提交工单"})
|
||||
resp.Forbidden(c, "权限不足:审计员不能提交工单")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -240,9 +245,9 @@ func CreateTicket(c *gin.Context) {
|
||||
RiskLevel *string `json:"risk_level"`
|
||||
ExecStrategy *string `json:"exec_strategy"`
|
||||
}
|
||||
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -252,7 +257,7 @@ func CreateTicket(c *gin.Context) {
|
||||
readOnlyPrefixes := []string{"SELECT", "SHOW", "DESC", "DESCRIBE", "EXPLAIN"}
|
||||
for _, prefix := range readOnlyPrefixes {
|
||||
if strings.HasPrefix(upperSQL, prefix) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "数据变更工单不支持 SELECT/SHOW/DESC 等只读语句,请使用查询数据功能"})
|
||||
resp.BadRequest(c, "数据变更工单不支持 SELECT/SHOW/DESC 等只读语句,请使用查询数据功能")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -267,7 +272,7 @@ func CreateTicket(c *gin.Context) {
|
||||
}
|
||||
for _, ro := range redisReadOnlyCmds {
|
||||
if cmdUpper == ro {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("数据变更工单不支持只读命令 %s,请使用查询功能", cmdUpper)})
|
||||
resp.BadRequest(c, fmt.Sprintf("数据变更工单不支持只读命令 %s,请使用查询功能", cmdUpper))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -341,12 +346,12 @@ func CreateTicket(c *gin.Context) {
|
||||
input.WorkflowID = inst.WorkflowID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
status := "pending"
|
||||
priority := "medium"
|
||||
riskLevel := "low"
|
||||
execStrategy := "stop_on_error"
|
||||
|
||||
|
||||
if input.Priority != nil {
|
||||
priority = *input.Priority
|
||||
}
|
||||
@@ -356,11 +361,11 @@ func CreateTicket(c *gin.Context) {
|
||||
if input.ExecStrategy != nil {
|
||||
execStrategy = *input.ExecStrategy
|
||||
}
|
||||
|
||||
|
||||
// Generate ticket number
|
||||
now := time.Now()
|
||||
ticketNo := now.Format("20060102") + "-" + fmt.Sprintf("%04d", time.Now().UnixNano()%10000)
|
||||
|
||||
|
||||
ticket := model.Ticket{
|
||||
Title: input.Title,
|
||||
DBType: input.DBType,
|
||||
@@ -377,12 +382,12 @@ func CreateTicket(c *gin.Context) {
|
||||
ExecStrategy: &execStrategy,
|
||||
TicketNo: &ticketNo,
|
||||
}
|
||||
|
||||
|
||||
if err := model.DB.Create(&ticket).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Create approval records based on workflow or default to admin
|
||||
hasAuditStep := false
|
||||
if input.WorkflowID != nil && *input.WorkflowID > 0 {
|
||||
@@ -408,11 +413,11 @@ func CreateTicket(c *gin.Context) {
|
||||
stepName := ws.Name
|
||||
stepType := ws.StepType
|
||||
approval := model.TicketApproval{
|
||||
TicketID: ticket.ID,
|
||||
StepNumber: ws.StepNumber,
|
||||
StepName: &stepName,
|
||||
StepType: &stepType,
|
||||
ApproverID: &uidCopy,
|
||||
TicketID: ticket.ID,
|
||||
StepNumber: ws.StepNumber,
|
||||
StepName: &stepName,
|
||||
StepType: &stepType,
|
||||
ApproverID: &uidCopy,
|
||||
}
|
||||
model.DB.Create(&approval)
|
||||
}
|
||||
@@ -476,13 +481,13 @@ func ApproveTicket(c *gin.Context) {
|
||||
|
||||
// Role check: admin/dba/developer can approve
|
||||
if userRole != "admin" && userRole != "dba" && userRole != "developer" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"detail": "权限不足:审核操作需要管理员、DBA 或开发者角色"})
|
||||
resp.Forbidden(c, "权限不足:审核操作需要管理员、DBA 或开发者角色")
|
||||
return
|
||||
}
|
||||
|
||||
var ticket model.Ticket
|
||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "Ticket not found"})
|
||||
resp.NotFound(c, "Ticket not found")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -491,7 +496,7 @@ func ApproveTicket(c *gin.Context) {
|
||||
Comment *string `json:"comment"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -501,7 +506,7 @@ func ApproveTicket(c *gin.Context) {
|
||||
var approval model.TicketApproval
|
||||
if err := model.DB.Where("ticket_id = ? AND approver_id = ? AND action IS NULL AND step_type != 'execute'", id, userID).First(&approval).Error; err != nil {
|
||||
// Already approved or no pending approval for this user
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "没有待处理的审批记录(你可能已经审批过了)"})
|
||||
resp.BadRequest(c, "没有待处理的审批记录(你可能已经审批过了)")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -510,7 +515,7 @@ func ApproveTicket(c *gin.Context) {
|
||||
approval.ActualApproverID = &userID
|
||||
approval.CompletedAt = &now
|
||||
model.DB.Save(&approval)
|
||||
|
||||
|
||||
// Update ticket status
|
||||
newStatus := "approved"
|
||||
if input.Action == "reject" {
|
||||
@@ -524,11 +529,11 @@ func ApproveTicket(c *gin.Context) {
|
||||
}
|
||||
// If all audit steps done → "approved" (ready for execution)
|
||||
}
|
||||
|
||||
|
||||
ticket.Status = &newStatus
|
||||
ticket.ReviewTime = &now
|
||||
model.DB.Save(&ticket)
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ticket": ticket, "approval": approval})
|
||||
}
|
||||
|
||||
@@ -539,19 +544,19 @@ func ExecuteTicket(c *gin.Context) {
|
||||
// Role check: only admin/dba can execute
|
||||
_, _, userRole := middleware.GetCurrentUser(c)
|
||||
if userRole != "admin" && userRole != "dba" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"detail": "权限不足:执行操作需要管理员或 DBA 角色"})
|
||||
resp.Forbidden(c, "权限不足:执行操作需要管理员或 DBA 角色")
|
||||
return
|
||||
}
|
||||
|
||||
var ticket model.Ticket
|
||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "Ticket not found"})
|
||||
resp.NotFound(c, "Ticket not found")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Check ticket status — allow approved OR reviewing (will auto-approve execute step)
|
||||
if ticket.Status != nil && *ticket.Status != "approved" && *ticket.Status != "reviewing" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Ticket must be approved before execution"})
|
||||
resp.BadRequest(c, "Ticket must be approved before execution")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -564,7 +569,7 @@ func ExecuteTicket(c *gin.Context) {
|
||||
Where("ticket_id = ? AND step_type = 'execute' AND (approver_id = ? OR approver_id IS NULL)", id, userID).
|
||||
Count(&executorCheck)
|
||||
if executorCheck == 0 {
|
||||
c.JSON(http.StatusForbidden, gin.H{"detail": "你不是此工单的执行人"})
|
||||
resp.Forbidden(c, "你不是此工单的执行人")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -587,7 +592,7 @@ func ExecuteTicket(c *gin.Context) {
|
||||
Where("ticket_id = ? AND step_type = 'audit' AND action IS NULL", id).
|
||||
Count(&pendingAudit)
|
||||
if pendingAudit > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "还有未完成的审核步骤"})
|
||||
resp.BadRequest(c, "还有未完成的审核步骤")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -597,27 +602,27 @@ func ExecuteTicket(c *gin.Context) {
|
||||
Where("ticket_id = ? AND step_type = 'audit' AND action = 'reject'", id).
|
||||
Count(&rejectedAudit)
|
||||
if rejectedAudit > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "审核已被拒绝,无法执行"})
|
||||
resp.BadRequest(c, "审核已被拒绝,无法执行")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Get instance info
|
||||
var instance model.Instance
|
||||
if err := model.DB.First(&instance, ticket.InstanceID).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Instance not found"})
|
||||
resp.BadRequest(c, "Instance not found")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
status := "executing"
|
||||
ticket.Status = &status
|
||||
now := time.Now()
|
||||
ticket.ExecuteTime = &now
|
||||
model.DB.Save(&ticket)
|
||||
|
||||
|
||||
// Execute SQL based on db_type
|
||||
var execResult string
|
||||
var execErr string
|
||||
|
||||
|
||||
if ticket.DBType == "redis" {
|
||||
// Execute Redis command (write operations allowed in ticket workflow)
|
||||
redisDB := 0
|
||||
@@ -716,31 +721,31 @@ func ExecuteTicket(c *gin.Context) {
|
||||
execTimeout := getSettingInt("executor_timeout", 300)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(execTimeout)*time.Second)
|
||||
defer cancel()
|
||||
execStart := time.Now()
|
||||
result, ghErr := ghost.RunGhOst(ctx, *ghostTask, getGhostPath())
|
||||
stmtMs := time.Since(execStart).Milliseconds()
|
||||
ghRes := SqlExecResult{
|
||||
Status: "success",
|
||||
TotalMs: stmtMs,
|
||||
StartTime: execStart.UTC().Format(time.RFC3339),
|
||||
EndTime: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
if ghErr != nil {
|
||||
ghRes.Status = "failed"
|
||||
ghRes.Items = []SqlExecItem{{
|
||||
SQL: ticket.SQLContent, Status: "fail", DurationMs: stmtMs,
|
||||
Error: fmt.Sprintf("%s\n%s", result.Error, ghErr.Error()),
|
||||
}}
|
||||
execErr = ghErr.Error()
|
||||
} else {
|
||||
ghRes.Items = []SqlExecItem{{
|
||||
SQL: ticket.SQLContent, Status: "success", DurationMs: stmtMs,
|
||||
Error: result.Output,
|
||||
}}
|
||||
}
|
||||
if jsonBytes, jsonErr := json.Marshal(ghRes); jsonErr == nil {
|
||||
execResult = string(jsonBytes)
|
||||
}
|
||||
execStart := time.Now()
|
||||
result, ghErr := ghost.RunGhOst(ctx, *ghostTask, getGhostPath())
|
||||
stmtMs := time.Since(execStart).Milliseconds()
|
||||
ghRes := SqlExecResult{
|
||||
Status: "success",
|
||||
TotalMs: stmtMs,
|
||||
StartTime: execStart.UTC().Format(time.RFC3339),
|
||||
EndTime: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
if ghErr != nil {
|
||||
ghRes.Status = "failed"
|
||||
ghRes.Items = []SqlExecItem{{
|
||||
SQL: ticket.SQLContent, Status: "fail", DurationMs: stmtMs,
|
||||
Error: fmt.Sprintf("%s\n%s", result.Error, ghErr.Error()),
|
||||
}}
|
||||
execErr = ghErr.Error()
|
||||
} else {
|
||||
ghRes.Items = []SqlExecItem{{
|
||||
SQL: ticket.SQLContent, Status: "success", DurationMs: stmtMs,
|
||||
Error: result.Output,
|
||||
}}
|
||||
}
|
||||
if jsonBytes, jsonErr := json.Marshal(ghRes); jsonErr == nil {
|
||||
execResult = string(jsonBytes)
|
||||
}
|
||||
// Skip normal execution path
|
||||
goto executionDone
|
||||
}
|
||||
@@ -803,7 +808,7 @@ func ExecuteTicket(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// Split multi-statement by semicolons — wrap in transaction for atomic rollback
|
||||
// Split multi-statement by semicolons — wrap in transaction for atomic rollback
|
||||
execStart := time.Now()
|
||||
statements := splitSQL(ticket.SQLContent)
|
||||
execRes := SqlExecResult{Status: "success", StartTime: execStart.UTC().Format(time.RFC3339)}
|
||||
@@ -853,8 +858,8 @@ func ExecuteTicket(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executionDone:
|
||||
|
||||
executionDone:
|
||||
// Update final status
|
||||
if execErr != "" {
|
||||
failStatus := "failed"
|
||||
@@ -866,7 +871,7 @@ func ExecuteTicket(c *gin.Context) {
|
||||
ticket.ExecuteResult = &execResult
|
||||
}
|
||||
model.DB.Save(&ticket)
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Execution completed", "ticket": ticket})
|
||||
}
|
||||
|
||||
@@ -930,75 +935,74 @@ func parseAlterTable(sql string) (string, string, error) {
|
||||
return tableName, alterClause, nil
|
||||
}
|
||||
|
||||
|
||||
// POST /api/v1/tickets/:id/cancel
|
||||
func CancelTicket(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
userID, _, _ := middleware.GetCurrentUser(c)
|
||||
|
||||
|
||||
var ticket model.Ticket
|
||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "Ticket not found"})
|
||||
resp.NotFound(c, "Ticket not found")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Only applicant or admin can cancel
|
||||
if ticket.ApplicantID != userID {
|
||||
_, _, role := middleware.GetCurrentUser(c)
|
||||
if role != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"detail": "Only applicant or admin can cancel"})
|
||||
resp.Forbidden(c, "Only applicant or admin can cancel")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ticket.Status != nil && (*ticket.Status != "pending" && *ticket.Status != "reviewing" && *ticket.Status != "approved") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "只能撤销待审核、审核中或已通过(待执行)的工单"})
|
||||
resp.BadRequest(c, "只能撤销待审核、审核中或已通过(待执行)的工单")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
status := "cancelled"
|
||||
ticket.Status = &status
|
||||
now := time.Now()
|
||||
ticket.ReviewTime = &now
|
||||
model.DB.Save(&ticket)
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Ticket cancelled", "ticket": ticket})
|
||||
}
|
||||
|
||||
// POST /api/v1/tickets/:id/rollback
|
||||
func RollbackTicket(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
|
||||
var ticket model.Ticket
|
||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "Ticket not found"})
|
||||
resp.NotFound(c, "Ticket not found")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if ticket.RollbackSQL == nil || *ticket.RollbackSQL == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "No rollback SQL available"})
|
||||
resp.BadRequest(c, "No rollback SQL available")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Get instance
|
||||
var instance model.Instance
|
||||
if err := model.DB.First(&instance, ticket.InstanceID).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Instance not found"})
|
||||
resp.BadRequest(c, "Instance not found")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if ticket.DBType == "redis" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Redis rollback not supported"})
|
||||
resp.BadRequest(c, "Redis rollback not supported")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
db, err := getDBConnection(&instance, ticket.TargetDatabase)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
|
||||
// Execute rollback SQL
|
||||
statements := splitSQL(*ticket.RollbackSQL)
|
||||
var results []string
|
||||
@@ -1017,18 +1021,18 @@ func RollbackTicket(c *gin.Context) {
|
||||
results = append(results, fmt.Sprintf("[OK] %s", truncateStr(stmt, 50)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if execErr != "" {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Rollback failed", "error": execErr, "results": results})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
status := "rollback"
|
||||
ticket.Status = &status
|
||||
execResult := strings.Join(results, "\n")
|
||||
ticket.ExecuteResult = &execResult
|
||||
model.DB.Save(&ticket)
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Rollback completed", "results": results})
|
||||
}
|
||||
|
||||
@@ -1037,10 +1041,10 @@ func GetTicketTimeline(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var ticket model.Ticket
|
||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"detail": "Ticket not found"})
|
||||
resp.NotFound(c, "Ticket not found")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
type TimelineEntry struct {
|
||||
ID int `json:"id"`
|
||||
Action string `json:"action"`
|
||||
@@ -1049,9 +1053,9 @@ func GetTicketTimeline(c *gin.Context) {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
|
||||
var timeline []TimelineEntry
|
||||
|
||||
|
||||
// 1. Created event
|
||||
timeline = append(timeline, TimelineEntry{
|
||||
ID: 1,
|
||||
@@ -1060,7 +1064,7 @@ func GetTicketTimeline(c *gin.Context) {
|
||||
Timestamp: ticket.CreateTime.Format(time.RFC3339),
|
||||
Status: "pending",
|
||||
})
|
||||
|
||||
|
||||
// 2. Approval events from ticket_approvals
|
||||
var approvals []model.TicketApproval
|
||||
model.DB.Where("ticket_id = ?", id).Order("step_number").Find(&approvals)
|
||||
@@ -1082,7 +1086,7 @@ func GetTicketTimeline(c *gin.Context) {
|
||||
Timestamp: ts,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 3. Executed event
|
||||
if ticket.ExecuteTime != nil {
|
||||
execAction := "executed"
|
||||
@@ -1095,36 +1099,41 @@ func GetTicketTimeline(c *gin.Context) {
|
||||
ID: 99,
|
||||
Action: execAction,
|
||||
Timestamp: ticket.ExecuteTime.Format(time.RFC3339),
|
||||
Status: func() string { if ticket.Status != nil { return *ticket.Status }; return "" }(),
|
||||
Status: func() string {
|
||||
if ticket.Status != nil {
|
||||
return *ticket.Status
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"timeline": timeline})
|
||||
}
|
||||
|
||||
// GET /api/v1/tickets/:id/approvals
|
||||
func GetTicketApprovals(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
|
||||
var approvals []model.TicketApproval
|
||||
if err := model.DB.Where("ticket_id = ?", id).Order("step_number").Find(&approvals).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Enrich with approver names
|
||||
type ApprovalResponse struct {
|
||||
model.TicketApproval
|
||||
ApproverName string `json:"approver_name"`
|
||||
}
|
||||
|
||||
|
||||
var result []ApprovalResponse
|
||||
for _, a := range approvals {
|
||||
ar := ApprovalResponse{TicketApproval: a}
|
||||
ar.ApproverName = getUserName(a.ApproverID)
|
||||
result = append(result, ar)
|
||||
}
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"sql-platform/internal/handler/resp"
|
||||
"sql-platform/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -15,7 +16,7 @@ func GetWorkflow(c *gin.Context) {
|
||||
id := 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
|
||||
}
|
||||
var steps []model.WorkflowStep
|
||||
@@ -55,7 +56,7 @@ func CreateWorkflow(c *gin.Context) {
|
||||
} `json:"steps"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ func CreateWorkflow(c *gin.Context) {
|
||||
IsDefault: input.IsDefault,
|
||||
}
|
||||
if err := model.DB.Create(&wf).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
||||
resp.InternalErr(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -99,13 +100,13 @@ func UpdateWorkflow(c *gin.Context) {
|
||||
id := 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -171,7 +172,7 @@ func ResetPassword(c *gin.Context) {
|
||||
NewPassword string `json:"new_password" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
_, _ = id, input.NewPassword
|
||||
@@ -182,18 +183,18 @@ func ResetPassword(c *gin.Context) {
|
||||
// POST /api/v1/users/batch-import
|
||||
func BatchImportUsers(c *gin.Context) {
|
||||
var input struct {
|
||||
Users []struct {
|
||||
Users []struct {
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Password string `json:"password"`
|
||||
} `json:"users"`
|
||||
ImportType string `json:"import_type"`
|
||||
SkipExisting bool `json:"skip_existing"`
|
||||
ImportType string `json:"import_type"`
|
||||
SkipExisting bool `json:"skip_existing"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -267,16 +268,16 @@ func BatchImportUsers(c *gin.Context) {
|
||||
func BatchImportLdapUsers(c *gin.Context) {
|
||||
var input struct {
|
||||
Users []struct {
|
||||
DN string `json:"dn"`
|
||||
CN string `json:"cn"`
|
||||
Nickname string `json:"nickname"`
|
||||
UID string `json:"uid"`
|
||||
Username string `json:"username"`
|
||||
Mail string `json:"mail"`
|
||||
DN string `json:"dn"`
|
||||
CN string `json:"cn"`
|
||||
Nickname string `json:"nickname"`
|
||||
UID string `json:"uid"`
|
||||
Username string `json:"username"`
|
||||
Mail string `json:"mail"`
|
||||
} `json:"users"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -359,21 +360,21 @@ func ResetUserPassword(c *gin.Context) {
|
||||
NewPassword string `json:"new_password" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
resp.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
id, err := strconv.Atoi(userId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Invalid user ID"})
|
||||
resp.BadRequest(c, "Invalid user ID")
|
||||
return
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(input.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Hash failed"})
|
||||
resp.InternalMsg(c, "Hash failed")
|
||||
return
|
||||
}
|
||||
if err := model.DB.Model(&model.User{}).Where("id = ?", id).Update("hashed_password", string(hash)).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Update failed"})
|
||||
resp.InternalMsg(c, "Update failed")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Password reset successfully"})
|
||||
|
||||
Reference in new issue
Block a user