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
25 files changed
+566
-613
No files matched your search
@@ -58,11 +58,15 @@ func main() {
|
|||||||
&model.ScheduledTask{},
|
&model.ScheduledTask{},
|
||||||
&model.ScheduledTaskLog{},
|
&model.ScheduledTaskLog{},
|
||||||
&model.UserGroup{},
|
&model.UserGroup{},
|
||||||
|
&model.RefreshToken{},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Seed default data (idempotent): users, workflow, settings
|
// Seed default data (idempotent): users, workflow, settings
|
||||||
seedAll(db)
|
seedAll(db)
|
||||||
|
|
||||||
|
// 治理默认 JWT 密钥(生产模式自动生成并持久化)
|
||||||
|
config.EnsureJWTSecret()
|
||||||
|
|
||||||
// Setup Gin
|
// Setup Gin
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
r.Use(middleware.CORSMiddleware())
|
r.Use(middleware.CORSMiddleware())
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultJWTSecret 仅用于本地 DEBUG;生产模式(DEBUG=false)会被 EnsureJWTSecret 替换为随机密钥
|
||||||
|
const DefaultJWTSecret = "sql-platform-secret-key-change-in-production"
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port string
|
Port string
|
||||||
DBPath string
|
DBPath string
|
||||||
@@ -24,13 +32,49 @@ func Init() {
|
|||||||
C = Config{
|
C = Config{
|
||||||
Port: getEnv("PORT", "9000"),
|
Port: getEnv("PORT", "9000"),
|
||||||
DBPath: getEnv("DB_PATH", "../backend/sqlite3.db"),
|
DBPath: getEnv("DB_PATH", "../backend/sqlite3.db"),
|
||||||
JWTSecret: getEnv("JWT_SECRET", "sql-platform-secret-key-change-in-production"),
|
JWTSecret: getEnv("JWT_SECRET", DefaultJWTSecret),
|
||||||
JWTExpire: getEnvInt("JWT_EXPIRE_HOURS", 24),
|
JWTExpire: getEnvInt("JWT_EXPIRE_HOURS", 24),
|
||||||
GhOstPath: getEnv("GH_OST_PATH", "gh-ost"),
|
GhOstPath: getEnv("GH_OST_PATH", "gh-ost"),
|
||||||
Debug: getEnvBool("DEBUG", true),
|
Debug: getEnvBool("DEBUG", true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnsureJWTSecret 治理默认 JWT 密钥:
|
||||||
|
// - 显式配置了 JWT_SECRET(非默认值)→ 直接使用
|
||||||
|
// - DEBUG 模式使用默认密钥 → 打印告警(开发便利优先)
|
||||||
|
// - 生产模式使用默认密钥 → 自动生成随机密钥并持久化到 system_settings(重启复用;
|
||||||
|
// 首次切换会使存量会话失效一次)
|
||||||
|
//
|
||||||
|
// 必须在 model.DB 初始化并完成 AutoMigrate 之后调用。
|
||||||
|
func EnsureJWTSecret() {
|
||||||
|
if C.JWTSecret != DefaultJWTSecret {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if C.Debug {
|
||||||
|
log.Println("[WARN] 正在使用默认 JWT 密钥(DEBUG 模式);生产环境请设置 JWT_SECRET 环境变量")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var s model.SystemSetting
|
||||||
|
if err := model.DB.Where("`key` = ?", "jwt_secret").Take(&s).Error; err == nil && s.Value != "" {
|
||||||
|
C.JWTSecret = s.Value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(buf); err != nil {
|
||||||
|
log.Fatalf("生成随机 JWT 密钥失败: %v", err)
|
||||||
|
}
|
||||||
|
secret := hex.EncodeToString(buf)
|
||||||
|
if err := model.DB.Where(model.SystemSetting{Key: "jwt_secret"}).
|
||||||
|
Assign(model.SystemSetting{Value: secret, ValueType: "string", Group: "security", Label: "JWT Secret"}).
|
||||||
|
FirstOrCreate(&s).Error; err != nil {
|
||||||
|
log.Fatalf("持久化 JWT 密钥失败: %v", err)
|
||||||
|
}
|
||||||
|
C.JWTSecret = secret
|
||||||
|
log.Println("[INFO] 已自动生成随机 JWT 密钥并持久化(未显式配置 JWT_SECRET 的生产部署,存量会话将失效一次)")
|
||||||
|
}
|
||||||
|
|
||||||
func getEnv(key, fallback string) string {
|
func getEnv(key, fallback string) string {
|
||||||
if v := os.Getenv(key); v != "" {
|
if v := os.Getenv(key); v != "" {
|
||||||
return v
|
return v
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"sql-platform/internal/safe"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GhOstTask defines a gh-ost execution task
|
// GhOstTask defines a gh-ost execution task
|
||||||
@@ -121,7 +124,7 @@ func RunGhOst(ctx context.Context, task GhOstTask, ghOstPath string) (*GhOstResu
|
|||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
|
||||||
result := &GhOstResult{
|
result := &GhOstResult{
|
||||||
Command: "gh-ost " + strings.Join(args, " "),
|
Command: "gh-ost " + maskSecrets(strings.Join(args, " ")),
|
||||||
Duration: time.Since(start).String(),
|
Duration: time.Since(start).String(),
|
||||||
Output: stdout.String(),
|
Output: stdout.String(),
|
||||||
}
|
}
|
||||||
@@ -145,7 +148,7 @@ func RunGhOstAsync(task GhOstTask, ghOstPath string, cb ProgressCallback) string
|
|||||||
Manager.tasks[taskID] = &GhOstResult{TaskID: taskID}
|
Manager.tasks[taskID] = &GhOstResult{TaskID: taskID}
|
||||||
Manager.mu.Unlock()
|
Manager.mu.Unlock()
|
||||||
|
|
||||||
go func() {
|
run := func() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -165,7 +168,7 @@ func RunGhOstAsync(task GhOstTask, ghOstPath string, cb ProgressCallback) string
|
|||||||
|
|
||||||
result := &GhOstResult{
|
result := &GhOstResult{
|
||||||
TaskID: taskID,
|
TaskID: taskID,
|
||||||
Command: "gh-ost " + strings.Join(args, " "),
|
Command: "gh-ost " + maskSecrets(strings.Join(args, " ")),
|
||||||
Duration: time.Since(start).String(),
|
Duration: time.Since(start).String(),
|
||||||
Output: stdout.String(),
|
Output: stdout.String(),
|
||||||
Success: err == nil,
|
Success: err == nil,
|
||||||
@@ -183,11 +186,17 @@ func RunGhOstAsync(task GhOstTask, ghOstPath string, cb ProgressCallback) string
|
|||||||
if cb != nil {
|
if cb != nil {
|
||||||
cb(taskID, result.Output)
|
cb(taskID, result.Output)
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
safe.Go("ghost:"+taskID, run)
|
||||||
|
|
||||||
return taskID
|
return taskID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maskSecrets 将命令行中的密码参数值替换为 ******,防止经任务状态接口回传前端
|
||||||
|
func maskSecrets(cmdline string) string {
|
||||||
|
return regexp.MustCompile(`(--password=)([^ ]+)`).ReplaceAllString(cmdline, `$1******`)
|
||||||
|
}
|
||||||
|
|
||||||
// GetTaskStatus returns the status of an async task
|
// GetTaskStatus returns the status of an async task
|
||||||
func (m *TaskManager) GetTaskStatus(taskID string) (string, *GhOstResult) {
|
func (m *TaskManager) GetTaskStatus(taskID string) (string, *GhOstResult) {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"sql-platform/internal/handler/resp"
|
||||||
"sql-platform/internal/middleware"
|
"sql-platform/internal/middleware"
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ import (
|
|||||||
func Login(c *gin.Context) {
|
func Login(c *gin.Context) {
|
||||||
var req model.LoginRequest
|
var req model.LoginRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ func Login(c *gin.Context) {
|
|||||||
maxAttempts := getSettingInt("login_max_attempts", 5)
|
maxAttempts := getSettingInt("login_max_attempts", 5)
|
||||||
lockMin := getSettingInt("login_lockout_minutes", 30)
|
lockMin := getSettingInt("login_lockout_minutes", 30)
|
||||||
remaining := int(user.LockedUntil.Sub(time.Now()).Minutes()) + 1
|
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
|
return
|
||||||
}
|
}
|
||||||
// Lock expired → reset
|
// Lock expired → reset
|
||||||
@@ -47,7 +48,7 @@ func Login(c *gin.Context) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
// ── 本地用户存在 ──
|
// ── 本地用户存在 ──
|
||||||
if user.IsActive != nil && !*user.IsActive {
|
if user.IsActive != nil && !*user.IsActive {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"detail": "Account is disabled"})
|
resp.Forbidden(c, "Account is disabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ func Login(c *gin.Context) {
|
|||||||
if ldapErr != nil || ldapInfo == nil {
|
if ldapErr != nil || ldapInfo == nil {
|
||||||
// Both local and LDAP failed → record failed attempt
|
// Both local and LDAP failed → record failed attempt
|
||||||
handleFailedLogin(&user)
|
handleFailedLogin(&user)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"detail": "Invalid username or password"})
|
resp.Unauthorized(c, "Invalid username or password")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// LDAP OK — update local user info
|
// LDAP OK — update local user info
|
||||||
@@ -81,7 +82,7 @@ func Login(c *gin.Context) {
|
|||||||
// ── 本地不存在该用户,尝试 LDAP 认证 ──
|
// ── 本地不存在该用户,尝试 LDAP 认证 ──
|
||||||
_, ldapInfo, ldapErr := tryLDAPAuth(req.Username, req.Password)
|
_, ldapInfo, ldapErr := tryLDAPAuth(req.Username, req.Password)
|
||||||
if ldapErr != nil || ldapInfo == nil {
|
if ldapErr != nil || ldapInfo == nil {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"detail": "Invalid username or password"})
|
resp.Unauthorized(c, "Invalid username or password")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// LDAP 认证成功,自动创建或恢复本地用户
|
// LDAP 认证成功,自动创建或恢复本地用户
|
||||||
@@ -102,7 +103,7 @@ func Login(c *gin.Context) {
|
|||||||
// FirstOrCreate: 不存在则创建,已存在则获取(避免 uniqueIndex 冲突)
|
// FirstOrCreate: 不存在则创建,已存在则获取(避免 uniqueIndex 冲突)
|
||||||
result := model.DB.Where("username = ?", ldapInfo.Username).FirstOrCreate(&newUser)
|
result := model.DB.Where("username = ?", ldapInfo.Username).FirstOrCreate(&newUser)
|
||||||
if result.Error != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
// 更新已有用户的 display_name / email(LDAP 信息可能变更)
|
// 更新已有用户的 display_name / email(LDAP 信息可能变更)
|
||||||
@@ -119,7 +120,7 @@ func Login(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
user = newUser
|
user = newUser
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Database error"})
|
resp.InternalMsg(c, "Database error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ func Login(c *gin.Context) {
|
|||||||
user.LockedUntil = nil
|
user.LockedUntil = nil
|
||||||
token, err := middleware.GenerateToken(&user)
|
token, err := middleware.GenerateToken(&user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Failed to generate token"})
|
resp.InternalMsg(c, "Failed to generate token")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,13 +138,53 @@ func Login(c *gin.Context) {
|
|||||||
user.LastLogin = &now
|
user.LastLogin = &now
|
||||||
model.DB.Save(&user)
|
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{
|
c.JSON(http.StatusOK, model.LoginResponse{
|
||||||
AccessToken: token,
|
AccessToken: token,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
TokenType: "bearer",
|
TokenType: "bearer",
|
||||||
User: user,
|
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
|
// LDAPAuthInfo holds extracted user info from LDAP
|
||||||
type LDAPAuthInfo struct {
|
type LDAPAuthInfo struct {
|
||||||
Username string
|
Username string
|
||||||
@@ -258,7 +299,7 @@ func GetMe(c *gin.Context) {
|
|||||||
userID, _, _ := middleware.GetCurrentUser(c)
|
userID, _, _ := middleware.GetCurrentUser(c)
|
||||||
var user model.User
|
var user model.User
|
||||||
if err := model.DB.First(&user, userID).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, user)
|
c.JSON(http.StatusOK, user)
|
||||||
@@ -282,13 +323,13 @@ func CreateUser(c *gin.Context) {
|
|||||||
Role string `json:"role" binding:"required"`
|
Role string `json:"role" binding:"required"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hashed, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
|
hashed, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Failed to hash password"})
|
resp.InternalMsg(c, "Failed to hash password")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,9 +346,9 @@ func CreateUser(c *gin.Context) {
|
|||||||
|
|
||||||
if err := model.DB.Create(&user).Error; err != nil {
|
if err := model.DB.Create(&user).Error; err != nil {
|
||||||
if strings.Contains(err.Error(), "UNIQUE") || strings.Contains(err.Error(), "duplicate") {
|
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 {
|
} else {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -320,13 +361,13 @@ func UpdateUser(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var user model.User
|
var user model.User
|
||||||
if err := model.DB.First(&user, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var input map[string]interface{}
|
var input map[string]interface{}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +378,7 @@ func UpdateUser(c *gin.Context) {
|
|||||||
delete(input, "password")
|
delete(input, "password")
|
||||||
|
|
||||||
if err := model.DB.Model(&user).Updates(input).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,11 +391,11 @@ func DeleteUser(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
// Protect admin (id=1) and dba (id=2) from being deleted
|
// Protect admin (id=1) and dba (id=2) from being deleted
|
||||||
if id == "1" || id == "2" {
|
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
|
return
|
||||||
}
|
}
|
||||||
if err := model.DB.Delete(&model.User{}, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "User deleted"})
|
c.JSON(http.StatusOK, gin.H{"message": "User deleted"})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sql-platform/internal/ghost"
|
"sql-platform/internal/ghost"
|
||||||
|
"sql-platform/internal/handler/resp"
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -195,7 +196,7 @@ func GhOstAnalyzeDDL(c *gin.Context) {
|
|||||||
Alter string `json:"alter"`
|
Alter string `json:"alter"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +275,7 @@ func getTableRowCount(instanceID int, database, table string) int {
|
|||||||
// POST /api/v1/ghost/dry-run
|
// POST /api/v1/ghost/dry-run
|
||||||
func GhOstDryRun(c *gin.Context) {
|
func GhOstDryRun(c *gin.Context) {
|
||||||
if !isGhostEnabled() {
|
if !isGhostEnabled() {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"detail": "gh-ost 功能已禁用,请在系统设置中开启"})
|
resp.Forbidden(c, "gh-ost 功能已禁用,请在系统设置中开启")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,13 +286,13 @@ func GhOstDryRun(c *gin.Context) {
|
|||||||
Alter string `json:"alter"`
|
Alter string `json:"alter"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
task, err := buildGhOstTask(req.InstanceID, req.Database, req.Table, req.Alter)
|
task, err := buildGhOstTask(req.InstanceID, req.Database, req.Table, req.Alter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
task.DryRun = true
|
task.DryRun = true
|
||||||
@@ -309,7 +310,7 @@ func GhOstDryRun(c *gin.Context) {
|
|||||||
// POST /api/v1/ghost/execute
|
// POST /api/v1/ghost/execute
|
||||||
func GhOstExecute(c *gin.Context) {
|
func GhOstExecute(c *gin.Context) {
|
||||||
if !isGhostEnabled() {
|
if !isGhostEnabled() {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"detail": "gh-ost 功能已禁用,请在系统设置中开启"})
|
resp.Forbidden(c, "gh-ost 功能已禁用,请在系统设置中开启")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,13 +322,13 @@ func GhOstExecute(c *gin.Context) {
|
|||||||
Force bool `json:"force"` // skip auto-trigger check
|
Force bool `json:"force"` // skip auto-trigger check
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
task, err := buildGhOstTask(req.InstanceID, req.Database, req.Table, req.Alter)
|
task, err := buildGhOstTask(req.InstanceID, req.Database, req.Table, req.Alter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
task.DryRun = false
|
task.DryRun = false
|
||||||
@@ -408,7 +409,7 @@ func GhOstStatus(c *gin.Context) {
|
|||||||
taskID := c.Param("taskId")
|
taskID := c.Param("taskId")
|
||||||
status, result := ghost.Manager.GetTaskStatus(taskID)
|
status, result := ghost.Manager.GetTaskStatus(taskID)
|
||||||
if status == "" {
|
if status == "" {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"detail": "Task not found"})
|
resp.NotFound(c, "Task not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@@ -471,7 +472,7 @@ func UpdateSettings(c *gin.Context) {
|
|||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&items); err != nil {
|
if err := c.ShouldBindJSON(&items); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
@@ -650,7 +651,7 @@ func SeedAuditRules(c *gin.Context) {
|
|||||||
dbType := c.Param("dbType")
|
dbType := c.Param("dbType")
|
||||||
rules, ok := defaultAuditRules[dbType]
|
rules, ok := defaultAuditRules[dbType]
|
||||||
if !ok {
|
if !ok {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("不支持的数据库类型: %s", dbType)})
|
resp.BadRequest(c, fmt.Sprintf("不支持的数据库类型: %s", dbType))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,7 +680,7 @@ func SeedAuditRules(c *gin.Context) {
|
|||||||
Value: &value,
|
Value: &value,
|
||||||
}
|
}
|
||||||
if err := model.DB.Create(&rule).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
count++
|
count++
|
||||||
@@ -692,12 +693,12 @@ func SeedAuditRules(c *gin.Context) {
|
|||||||
func ToggleAuditRule(c *gin.Context) {
|
func ToggleAuditRule(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "无效的规则 ID"})
|
resp.BadRequest(c, "无效的规则 ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var rule model.AuditRule
|
var rule model.AuditRule
|
||||||
if err := model.DB.First(&rule, id).Error; err != nil {
|
if err := model.DB.First(&rule, id).Error; err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"detail": "规则不存在"})
|
resp.NotFound(c, "规则不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
newEnabled := !*rule.IsEnabled
|
newEnabled := !*rule.IsEnabled
|
||||||
@@ -709,12 +710,12 @@ func ToggleAuditRule(c *gin.Context) {
|
|||||||
func UpdateAuditRule(c *gin.Context) {
|
func UpdateAuditRule(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "无效的规则 ID"})
|
resp.BadRequest(c, "无效的规则 ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var rule model.AuditRule
|
var rule model.AuditRule
|
||||||
if err := model.DB.First(&rule, id).Error; err != nil {
|
if err := model.DB.First(&rule, id).Error; err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"detail": "规则不存在"})
|
resp.NotFound(c, "规则不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body struct {
|
var body struct {
|
||||||
@@ -724,7 +725,7 @@ func UpdateAuditRule(c *gin.Context) {
|
|||||||
Severity *string `json:"severity"`
|
Severity *string `json:"severity"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&body); err != nil {
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
updates := map[string]interface{}{}
|
updates := map[string]interface{}{}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"sql-platform/internal/handler/resp"
|
||||||
"sql-platform/internal/middleware"
|
"sql-platform/internal/middleware"
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
"sql-platform/internal/safe"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -35,14 +37,11 @@ func ListInstances(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := query.Find(&instances).Error; err != nil {
|
if err := query.Find(&instances).Error; err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// mask passwords in response
|
// 密码出站遮蔽由 Instance.MarshalJSON 统一收口
|
||||||
for i := range instances {
|
|
||||||
instances[i].Password = ""
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, instances)
|
c.JSON(http.StatusOK, instances)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,10 +50,9 @@ func GetInstance(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
instance.Password = "******"
|
|
||||||
c.JSON(http.StatusOK, instance)
|
c.JSON(http.StatusOK, instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +60,7 @@ func GetInstance(c *gin.Context) {
|
|||||||
func CreateInstance(c *gin.Context) {
|
func CreateInstance(c *gin.Context) {
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := c.ShouldBindJSON(&instance); err != nil {
|
if err := c.ShouldBindJSON(&instance); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Encrypt password before storing
|
// Encrypt password before storing
|
||||||
@@ -74,11 +72,11 @@ func CreateInstance(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := model.DB.Create(&instance).Error; err != nil {
|
if err := model.DB.Create(&instance).Error; err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Auto-detect version, cluster_mode and status after creation
|
// Auto-detect version, cluster_mode and status after creation
|
||||||
go func() {
|
probe := func() {
|
||||||
result := testDBConnectionFull(&instance)
|
result := testDBConnectionFull(&instance)
|
||||||
updates := map[string]interface{}{}
|
updates := map[string]interface{}{}
|
||||||
if result.Success {
|
if result.Success {
|
||||||
@@ -95,8 +93,8 @@ func CreateInstance(c *gin.Context) {
|
|||||||
if len(updates) > 0 {
|
if len(updates) > 0 {
|
||||||
model.DB.Model(&model.Instance{}).Where("id = ?", instance.ID).Updates(updates)
|
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)
|
c.JSON(http.StatusCreated, instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,13 +103,13 @@ func UpdateInstance(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var input map[string]interface{}
|
var input map[string]interface{}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,12 +128,11 @@ func UpdateInstance(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := model.DB.Model(&instance).Updates(input).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
model.DB.First(&instance, id)
|
model.DB.First(&instance, id)
|
||||||
instance.Password = "******"
|
|
||||||
c.JSON(http.StatusOK, instance)
|
c.JSON(http.StatusOK, instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +140,7 @@ func UpdateInstance(c *gin.Context) {
|
|||||||
func DeleteInstance(c *gin.Context) {
|
func DeleteInstance(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
if err := model.DB.Delete(&model.Instance{}, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Instance deleted"})
|
c.JSON(http.StatusOK, gin.H{"message": "Instance deleted"})
|
||||||
@@ -154,7 +151,7 @@ func TestConnection(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +206,7 @@ func InstanceMetrics(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"sql-platform/internal/handler/resp"
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -28,7 +29,7 @@ func DetailListDatabases(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,14 +40,14 @@ func DetailListDatabases(c *gin.Context) {
|
|||||||
|
|
||||||
db, err := getDBConnection(&instance, "")
|
db, err := getDBConnection(&instance, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
rows, err := db.Query("SHOW DATABASES")
|
rows, err := db.Query("SHOW DATABASES")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
@@ -72,7 +73,7 @@ func DetailListTables(c *gin.Context) {
|
|||||||
dbName := c.Query("database")
|
dbName := c.Query("database")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,20 +88,20 @@ func DetailListTables(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if dbName == "" {
|
if dbName == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "database parameter required"})
|
resp.BadRequest(c, "database parameter required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := getDBConnection(&instance, dbName)
|
db, err := getDBConnection(&instance, dbName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
rows, err := db.Query("SHOW TABLE STATUS")
|
rows, err := db.Query("SHOW TABLE STATUS")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
@@ -139,7 +140,7 @@ func DetailDbSchema(c *gin.Context) {
|
|||||||
dbName := c.Query("database")
|
dbName := c.Query("database")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,13 +155,13 @@ func DetailDbSchema(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if dbName == "" {
|
if dbName == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "database parameter required"})
|
resp.BadRequest(c, "database parameter required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := getDBConnection(&instance, dbName)
|
db, err := getDBConnection(&instance, dbName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
@@ -168,7 +169,7 @@ func DetailDbSchema(c *gin.Context) {
|
|||||||
// Get all tables and their columns
|
// Get all tables and their columns
|
||||||
tableRows, err := db.Query("SHOW TABLES")
|
tableRows, err := db.Query("SHOW TABLES")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer tableRows.Close()
|
defer tableRows.Close()
|
||||||
@@ -207,7 +208,7 @@ func DetailTableDetail(c *gin.Context) {
|
|||||||
tableName := c.Query("table")
|
tableName := c.Query("table")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,13 +218,13 @@ func DetailTableDetail(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if dbName == "" || tableName == "" {
|
if dbName == "" || tableName == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "database and table parameters required"})
|
resp.BadRequest(c, "database and table parameters required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := getDBConnection(&instance, dbName)
|
db, err := getDBConnection(&instance, dbName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
@@ -235,7 +236,7 @@ func DetailTableDetail(c *gin.Context) {
|
|||||||
// Get columns
|
// Get columns
|
||||||
colRows, err := db.Query("SHOW FULL COLUMNS FROM `" + tableName + "`")
|
colRows, err := db.Query("SHOW FULL COLUMNS FROM `" + tableName + "`")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer colRows.Close()
|
defer colRows.Close()
|
||||||
@@ -306,7 +307,7 @@ func DetailProcesslist(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,14 +322,14 @@ func DetailProcesslist(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
db, err := getDBConnection(&instance, dbName)
|
db, err := getDBConnection(&instance, dbName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
rows, err := db.Query("SHOW PROCESSLIST")
|
rows, err := db.Query("SHOW PROCESSLIST")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
@@ -365,7 +366,7 @@ func DetailStatus(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,14 +384,14 @@ func DetailStatus(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
db, err := getDBConnection(&instance, dbName)
|
db, err := getDBConnection(&instance, dbName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
rows, err := db.Query("SHOW GLOBAL STATUS")
|
rows, err := db.Query("SHOW GLOBAL STATUS")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
@@ -429,7 +430,7 @@ func DetailKillProcess(c *gin.Context) {
|
|||||||
pid := c.Param("pid")
|
pid := c.Param("pid")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,14 +440,14 @@ func DetailKillProcess(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
db, err := getDBConnection(&instance, dbName)
|
db, err := getDBConnection(&instance, dbName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
_, err = db.Exec("KILL " + pid)
|
_, err = db.Exec("KILL " + pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Kill failed: %v", err)})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Process " + pid + " killed"})
|
c.JSON(http.StatusOK, gin.H{"message": "Process " + pid + " killed"})
|
||||||
@@ -457,7 +458,7 @@ func DetailListUsers(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,7 +473,7 @@ func DetailListUsers(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
db, err := getDBConnection(&instance, dbName)
|
db, err := getDBConnection(&instance, dbName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
@@ -514,11 +515,11 @@ func ExecuteQuery(c *gin.Context) {
|
|||||||
Offset int `json:"offset"`
|
Offset int `json:"offset"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if input.InstanceID == 0 || input.SQL == "" {
|
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
|
return
|
||||||
}
|
}
|
||||||
// Read query limits from platform settings
|
// Read query limits from platform settings
|
||||||
@@ -538,7 +539,7 @@ func ExecuteQuery(c *gin.Context) {
|
|||||||
|
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, input.InstanceID).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,7 +608,7 @@ func ExecuteQuery(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, result)
|
c.JSON(http.StatusOK, result)
|
||||||
|
|
||||||
default:
|
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 {
|
if pipeline != nil {
|
||||||
// Aggregation — add $facet for count + data
|
// Aggregation — add $facet for count + data
|
||||||
facetStage := bson.D{
|
facetStage := bson.D{
|
||||||
{"$facet", bson.D{
|
{Key: "$facet", Value: bson.D{
|
||||||
{"data", bson.A{
|
{Key: "data", Value: bson.A{
|
||||||
bson.D{{"$skip", offset}},
|
bson.D{{Key: "$skip", Value: offset}},
|
||||||
bson.D{{"$limit", limit}},
|
bson.D{{Key: "$limit", Value: limit}},
|
||||||
}},
|
}},
|
||||||
{"count", bson.A{
|
{Key: "count", Value: bson.A{
|
||||||
bson.D{{"$count", "total"}},
|
bson.D{{Key: "$count", Value: "total"}},
|
||||||
}},
|
}},
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
@@ -1404,11 +1405,11 @@ func ExportQueryCSV(c *gin.Context) {
|
|||||||
Limit int `json:"limit"`
|
Limit int `json:"limit"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if input.InstanceID == 0 || input.SQL == "" {
|
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
|
return
|
||||||
}
|
}
|
||||||
if input.Limit <= 0 {
|
if input.Limit <= 0 {
|
||||||
@@ -1420,7 +1421,7 @@ func ExportQueryCSV(c *gin.Context) {
|
|||||||
|
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, input.InstanceID).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1438,7 +1439,7 @@ func ExportQueryCSV(c *gin.Context) {
|
|||||||
case "mongodb", "mongo":
|
case "mongodb", "mongo":
|
||||||
exportMongoCSV(c, &instance, input.Database, input.SQL, input.Limit)
|
exportMongoCSV(c, &instance, input.Database, input.SQL, input.Limit)
|
||||||
default:
|
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 (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"sql-platform/internal/handler/resp"
|
||||||
"sql-platform/internal/middleware"
|
"sql-platform/internal/middleware"
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ func ListDatabases(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
databases, err := listDatabases(&instance)
|
databases, err := listDatabases(&instance)
|
||||||
@@ -77,7 +78,7 @@ func ListTables(c *gin.Context) {
|
|||||||
dbName := c.Param("db")
|
dbName := c.Param("db")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
tables, err := listTables(&instance, dbName)
|
tables, err := listTables(&instance, dbName)
|
||||||
@@ -102,7 +103,7 @@ func TestNewConnection(c *gin.Context) {
|
|||||||
Database string `json:"database"`
|
Database string `json:"database"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Invalid request"})
|
resp.BadRequest(c, "Invalid request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"sql-platform/internal/handler/resp"
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -17,7 +18,7 @@ func GetInstanceGroup(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var group model.InstanceGroup
|
var group model.InstanceGroup
|
||||||
if err := model.DB.First(&group, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, enrichGroup(group))
|
c.JSON(http.StatusOK, enrichGroup(group))
|
||||||
@@ -34,7 +35,7 @@ func CreateInstanceGroup(c *gin.Context) {
|
|||||||
WriteInstanceIDs []int `json:"write_instance_ids"`
|
WriteInstanceIDs []int `json:"write_instance_ids"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ func CreateInstanceGroup(c *gin.Context) {
|
|||||||
IsActive: &isActive,
|
IsActive: &isActive,
|
||||||
}
|
}
|
||||||
if err := model.DB.Create(&group).Error; err != nil {
|
if err := model.DB.Create(&group).Error; err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ func UpdateInstanceGroup(c *gin.Context) {
|
|||||||
gid, _ := strconv.Atoi(id)
|
gid, _ := strconv.Atoi(id)
|
||||||
var group model.InstanceGroup
|
var group model.InstanceGroup
|
||||||
if err := model.DB.First(&group, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ func UpdateInstanceGroup(c *gin.Context) {
|
|||||||
WriteInstanceIDs *[]int `json:"write_instance_ids"`
|
WriteInstanceIDs *[]int `json:"write_instance_ids"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ func UpdateInstanceGroup(c *gin.Context) {
|
|||||||
|
|
||||||
if len(updates) > 0 {
|
if len(updates) > 0 {
|
||||||
if err := model.DB.Model(&group).Updates(updates).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +176,7 @@ func UpdateInstanceGroup(c *gin.Context) {
|
|||||||
func DeleteInstanceGroup(c *gin.Context) {
|
func DeleteInstanceGroup(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
if err := model.DB.Delete(&model.InstanceGroup{}, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
// also clean up members
|
// also clean up members
|
||||||
@@ -191,7 +192,7 @@ func AssignGroupInstances(c *gin.Context) {
|
|||||||
AccessType string `json:"access_type"`
|
AccessType string `json:"access_type"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if input.AccessType == "" {
|
if input.AccessType == "" {
|
||||||
@@ -240,7 +241,7 @@ func AssignGroupUsers(c *gin.Context) {
|
|||||||
Permissions []string `json:"permissions"`
|
Permissions []string `json:"permissions"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +279,7 @@ func UpdateUserGroups(c *gin.Context) {
|
|||||||
GroupIDs []int `json:"group_ids"`
|
GroupIDs []int `json:"group_ids"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"sql-platform/internal/handler/resp"
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -46,7 +47,7 @@ func GetLDAPConfig(c *gin.Context) {
|
|||||||
func UpdateLDAPConfig(c *gin.Context) {
|
func UpdateLDAPConfig(c *gin.Context) {
|
||||||
var input map[string]interface{}
|
var input map[string]interface{}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,8 +88,20 @@ func UpdateLDAPConfig(c *gin.Context) {
|
|||||||
LDAPHost: getStr("ldap_host"),
|
LDAPHost: getStr("ldap_host"),
|
||||||
AdminDN: getStr("admin_dn"),
|
AdminDN: getStr("admin_dn"),
|
||||||
BaseDN: getStr("base_dn"),
|
BaseDN: getStr("base_dn"),
|
||||||
SearchFilter: func() *string { s := getStr("search_filter"); if s != "" { return &s }; return nil }(),
|
SearchFilter: func() *string {
|
||||||
UserMapping: func() *string { s := userMappingStr; if s != "" { return &s }; return nil }(),
|
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 {
|
if port := getInt("ldap_port"); port > 0 {
|
||||||
cfg.LDAPPort = &port
|
cfg.LDAPPort = &port
|
||||||
@@ -149,13 +162,13 @@ func TestLDAPConnection(c *gin.Context) {
|
|||||||
TestPassword string `json:"test_password"`
|
TestPassword string `json:"test_password"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg model.LDAPConfig
|
var cfg model.LDAPConfig
|
||||||
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"detail": "LDAP 配置不存在,请先保存配置"})
|
resp.NotFound(c, "LDAP 配置不存在,请先保存配置")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cfg.LDAPHost == "" {
|
if cfg.LDAPHost == "" {
|
||||||
@@ -367,26 +380,26 @@ func CreateLDAPOU(c *gin.Context) {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil || req.OuName == "" {
|
if err := c.ShouldBindJSON(&req); err != nil || req.OuName == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 ou_name 参数"})
|
resp.BadRequest(c, "缺少 ou_name 参数")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg model.LDAPConfig
|
var cfg model.LDAPConfig
|
||||||
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 配置不存在"})
|
resp.BadRequest(c, "LDAP 配置不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := dialLDAP(&cfg)
|
conn, err := dialLDAP(&cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 连接失败: " + err.Error()})
|
resp.BadRequest(c, "LDAP 连接失败: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
adminPw, _ := decryptPassword(cfg.AdminPassword)
|
adminPw, _ := decryptPassword(cfg.AdminPassword)
|
||||||
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
|
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 管理员绑定失败"})
|
resp.BadRequest(c, "LDAP 管理员绑定失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,7 +412,7 @@ func CreateLDAPOU(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := conn.Add(addReq); err != nil {
|
if err := conn.Add(addReq); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "创建 OU 失败: " + err.Error()})
|
resp.BadRequest(c, "创建 OU 失败: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,13 +429,13 @@ func CreateLDAPUser(c *gin.Context) {
|
|||||||
OuDn string `json:"ou_dn"`
|
OuDn string `json:"ou_dn"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil || req.Username == "" || req.Password == "" {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg model.LDAPConfig
|
var cfg model.LDAPConfig
|
||||||
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 配置不存在"})
|
resp.BadRequest(c, "LDAP 配置不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,14 +447,14 @@ func CreateLDAPUser(c *gin.Context) {
|
|||||||
|
|
||||||
conn, err := dialLDAP(&cfg)
|
conn, err := dialLDAP(&cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 连接失败: " + err.Error()})
|
resp.BadRequest(c, "LDAP 连接失败: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
adminPw, _ := decryptPassword(cfg.AdminPassword)
|
adminPw, _ := decryptPassword(cfg.AdminPassword)
|
||||||
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
|
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 管理员绑定失败"})
|
resp.BadRequest(c, "LDAP 管理员绑定失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +488,7 @@ func CreateLDAPUser(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := conn.Add(addReq); err != nil {
|
if err := conn.Add(addReq); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "创建用户失败: " + err.Error()})
|
resp.BadRequest(c, "创建用户失败: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,20 +509,20 @@ func BatchImportLDAP(c *gin.Context) {
|
|||||||
|
|
||||||
var cfg model.LDAPConfig
|
var cfg model.LDAPConfig
|
||||||
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 配置不存在"})
|
resp.BadRequest(c, "LDAP 配置不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := dialLDAP(&cfg)
|
conn, err := dialLDAP(&cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 连接失败: " + err.Error()})
|
resp.BadRequest(c, "LDAP 连接失败: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
adminPw, _ := decryptPassword(cfg.AdminPassword)
|
adminPw, _ := decryptPassword(cfg.AdminPassword)
|
||||||
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
|
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 管理员绑定失败"})
|
resp.BadRequest(c, "LDAP 管理员绑定失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,7 +547,7 @@ func BatchImportLDAP(c *gin.Context) {
|
|||||||
[]string{"uid", "cn", "mail"}, nil,
|
[]string{"uid", "cn", "mail"}, nil,
|
||||||
))
|
))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 搜索失败: " + err.Error()})
|
resp.BadRequest(c, "LDAP 搜索失败: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,9 +564,14 @@ func BatchImportLDAP(c *gin.Context) {
|
|||||||
if len(req.UserIDs) > 0 {
|
if len(req.UserIDs) > 0 {
|
||||||
found := false
|
found := false
|
||||||
for _, id := range req.UserIDs {
|
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")
|
cn := entry.GetAttributeValue("cn")
|
||||||
@@ -564,8 +582,12 @@ func BatchImportLDAP(c *gin.Context) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
// Update existing user
|
// Update existing user
|
||||||
updates := map[string]interface{}{}
|
updates := map[string]interface{}{}
|
||||||
if cn != "" && cn != existing.DisplayName { updates["display_name"] = cn }
|
if cn != "" && cn != existing.DisplayName {
|
||||||
if mail != "" && (existing.Email == nil || *existing.Email != mail) { updates["email"] = mail }
|
updates["display_name"] = cn
|
||||||
|
}
|
||||||
|
if mail != "" && (existing.Email == nil || *existing.Email != mail) {
|
||||||
|
updates["email"] = mail
|
||||||
|
}
|
||||||
if len(updates) > 0 {
|
if len(updates) > 0 {
|
||||||
model.DB.Model(&existing).Updates(updates)
|
model.DB.Model(&existing).Updates(updates)
|
||||||
}
|
}
|
||||||
@@ -582,7 +604,9 @@ func BatchImportLDAP(c *gin.Context) {
|
|||||||
IsActive: &isActive,
|
IsActive: &isActive,
|
||||||
HashedPassword: "-",
|
HashedPassword: "-",
|
||||||
}
|
}
|
||||||
if mail != "" { newUser.Email = &mail }
|
if mail != "" {
|
||||||
|
newUser.Email = &mail
|
||||||
|
}
|
||||||
if err := model.DB.Create(&newUser).Error; err != nil {
|
if err := model.DB.Create(&newUser).Error; err != nil {
|
||||||
results = append(results, map[string]string{"uid": uid, "status": "error: " + err.Error()})
|
results = append(results, map[string]string{"uid": uid, "status": "error: " + err.Error()})
|
||||||
skipped++
|
skipped++
|
||||||
@@ -606,31 +630,31 @@ func DeleteLDAPUser(c *gin.Context) {
|
|||||||
DN string `json:"dn" binding:"required"`
|
DN string `json:"dn" binding:"required"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 dn 参数"})
|
resp.BadRequest(c, "缺少 dn 参数")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg model.LDAPConfig
|
var cfg model.LDAPConfig
|
||||||
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 配置不存在"})
|
resp.BadRequest(c, "LDAP 配置不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := dialLDAP(&cfg)
|
conn, err := dialLDAP(&cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 连接失败: " + err.Error()})
|
resp.BadRequest(c, "LDAP 连接失败: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
adminPw, _ := decryptPassword(cfg.AdminPassword)
|
adminPw, _ := decryptPassword(cfg.AdminPassword)
|
||||||
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
|
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 管理员绑定失败"})
|
resp.BadRequest(c, "LDAP 管理员绑定失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := conn.Del(ldap.NewDelRequest(req.DN, nil)); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"sql-platform/internal/handler/resp"
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -63,11 +64,11 @@ func CreateNotification(c *gin.Context) {
|
|||||||
IsEnabled bool `json:"is_enabled"`
|
IsEnabled bool `json:"is_enabled"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "参数错误"})
|
resp.BadRequest(c, "参数错误")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if input.Name == "" {
|
if input.Name == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "名称不能为空"})
|
resp.BadRequest(c, "名称不能为空")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cfgBytes, _ := json.Marshal(input.Config)
|
cfgBytes, _ := json.Marshal(input.Config)
|
||||||
@@ -79,7 +80,7 @@ func CreateNotification(c *gin.Context) {
|
|||||||
IsEnabled: &enabled,
|
IsEnabled: &enabled,
|
||||||
}
|
}
|
||||||
if err := model.DB.Create(&ch).Error; err != nil {
|
if err := model.DB.Create(&ch).Error; err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "创建失败"})
|
resp.InternalMsg(c, "创建失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, toChannelResponse(ch))
|
c.JSON(http.StatusOK, toChannelResponse(ch))
|
||||||
@@ -90,12 +91,12 @@ func UpdateNotification(c *gin.Context) {
|
|||||||
id, _ := strconv.Atoi(c.Param("id"))
|
id, _ := strconv.Atoi(c.Param("id"))
|
||||||
var ch model.NotificationChannel
|
var ch model.NotificationChannel
|
||||||
if err := model.DB.First(&ch, id).Error; err != nil {
|
if err := model.DB.First(&ch, id).Error; err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"detail": "通知渠道不存在"})
|
resp.NotFound(c, "通知渠道不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var input map[string]interface{}
|
var input map[string]interface{}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "参数错误"})
|
resp.BadRequest(c, "参数错误")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if name, ok := input["name"].(string); ok {
|
if name, ok := input["name"].(string); ok {
|
||||||
@@ -127,7 +128,7 @@ func TestNotificationSend(c *gin.Context) {
|
|||||||
id, _ := strconv.Atoi(c.Param("id"))
|
id, _ := strconv.Atoi(c.Param("id"))
|
||||||
var ch model.NotificationChannel
|
var ch model.NotificationChannel
|
||||||
if err := model.DB.First(&ch, id).Error; err != nil {
|
if err := model.DB.First(&ch, id).Error; err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"detail": "通知渠道不存在"})
|
resp.NotFound(c, "通知渠道不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +136,7 @@ func TestNotificationSend(c *gin.Context) {
|
|||||||
_ = json.Unmarshal([]byte(ch.Config), &cfg)
|
_ = json.Unmarshal([]byte(ch.Config), &cfg)
|
||||||
webhookURL, _ := cfg["webhook_url"].(string)
|
webhookURL, _ := cfg["webhook_url"].(string)
|
||||||
if webhookURL == "" {
|
if webhookURL == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Webhook URL 未配置"})
|
resp.BadRequest(c, "Webhook URL 未配置")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,12 +151,12 @@ func TestNotificationSend(c *gin.Context) {
|
|||||||
secret, _ := cfg["secret"].(string)
|
secret, _ := cfg["secret"].(string)
|
||||||
sendErr = sendDingtalkWebhook(webhookURL, secret, title, body)
|
sendErr = sendDingtalkWebhook(webhookURL, secret, title, body)
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "不支持的通知类型: " + ch.ChannelType})
|
resp.BadRequest(c, "不支持的通知类型: "+ch.ChannelType)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if sendErr != nil {
|
if sendErr != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "发送失败: " + sendErr.Error()})
|
resp.InternalErr(c, sendErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "测试消息已发送"})
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "测试消息已发送"})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sql-platform/internal/handler/resp"
|
||||||
"sql-platform/internal/middleware"
|
"sql-platform/internal/middleware"
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ func GetUserPreferences(c *gin.Context) {
|
|||||||
userID, _, _ := middleware.GetCurrentUser(c)
|
userID, _, _ := middleware.GetCurrentUser(c)
|
||||||
var user model.User
|
var user model.User
|
||||||
if err := model.DB.First(&user, userID).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ func UpdateUserPreferences(c *gin.Context) {
|
|||||||
|
|
||||||
var prefs map[string]string
|
var prefs map[string]string
|
||||||
if err := c.ShouldBindJSON(&prefs); err != nil {
|
if err := c.ShouldBindJSON(&prefs); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ func UpdateUserPreferences(c *gin.Context) {
|
|||||||
|
|
||||||
result := model.DB.Model(&model.User{}).Where("id = ?", userID).Update("preferences", str)
|
result := model.DB.Model(&model.User{}).Where("id = ?", userID).Update("preferences", str)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": result.Error.Error()})
|
resp.InternalErr(c, result.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"sql-platform/internal/handler/resp"
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -384,13 +385,13 @@ func RedisInfo(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := getRedisClient(&instance, 0)
|
client, err := getRedisClient(&instance, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
@@ -398,7 +399,7 @@ func RedisInfo(c *gin.Context) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
info, err := client.Info(ctx).Result()
|
info, err := client.Info(ctx).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,13 +461,13 @@ func RedisDbsize(c *gin.Context) {
|
|||||||
|
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := getRedisClient(&instance, db)
|
client, err := getRedisClient(&instance, db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
@@ -474,7 +475,7 @@ func RedisDbsize(c *gin.Context) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
dbSize, err := client.DBSize(ctx).Result()
|
dbSize, err := client.DBSize(ctx).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"db_size": dbSize})
|
c.JSON(http.StatusOK, gin.H{"db_size": dbSize})
|
||||||
@@ -510,13 +511,13 @@ func RedisScanKeys(c *gin.Context) {
|
|||||||
|
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := getRedisClient(&instance, db)
|
client, err := getRedisClient(&instance, db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
@@ -549,7 +550,7 @@ func RedisScanKeys(c *gin.Context) {
|
|||||||
|
|
||||||
allKeys, err := client.ScanAllKeys(ctx, pattern, count)
|
allKeys, err := client.ScanAllKeys(ctx, pattern, count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,13 +587,13 @@ func RedisGetKeyDetail(c *gin.Context) {
|
|||||||
|
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := getRedisClient(&instance, db)
|
client, err := getRedisClient(&instance, db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
@@ -636,13 +637,13 @@ func RedisSlowlog(c *gin.Context) {
|
|||||||
|
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := getRedisClient(&instance, 0)
|
client, err := getRedisClient(&instance, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
@@ -650,7 +651,7 @@ func RedisSlowlog(c *gin.Context) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
logs, err := client.SlowLogGet(ctx, int64(count)).Result()
|
logs, err := client.SlowLogGet(ctx, int64(count)).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,13 +676,13 @@ func RedisClients(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := getRedisClient(&instance, 0)
|
client, err := getRedisClient(&instance, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
@@ -689,7 +690,7 @@ func RedisClients(c *gin.Context) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
clientList, err := client.ClientList(ctx).Result()
|
clientList, err := client.ClientList(ctx).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -729,19 +730,19 @@ func RedisExecuteCommand(c *gin.Context) {
|
|||||||
Args []string `json:"args"`
|
Args []string `json:"args"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := getRedisClient(&instance, db)
|
client, err := getRedisClient(&instance, db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
@@ -760,7 +761,7 @@ func RedisExecuteCommand(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !readOnly[cmd] {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,19 +804,19 @@ func RedisCreateKey(c *gin.Context) {
|
|||||||
TTL int `json:"ttl"`
|
TTL int `json:"ttl"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := getRedisClient(&instance, db)
|
client, err := getRedisClient(&instance, db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
@@ -823,7 +824,7 @@ func RedisCreateKey(c *gin.Context) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
if err := writeRedisKeyValue(ctx, client, input.Key, input.Type, input.Value); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -850,30 +851,30 @@ func RedisUpdateKey(c *gin.Context) {
|
|||||||
TTL int `json:"ttl"`
|
TTL int `json:"ttl"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := getRedisClient(&instance, db)
|
client, err := getRedisClient(&instance, db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := client.Del(ctx, key).Err(); err != nil {
|
if err := client.Del(ctx, key).Err(); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := writeRedisKeyValue(ctx, client, key, input.Type, input.Value); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
if input.TTL > 0 {
|
if input.TTL > 0 {
|
||||||
@@ -895,13 +896,13 @@ func RedisDeleteKey(c *gin.Context) {
|
|||||||
|
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := getRedisClient(&instance, db)
|
client, err := getRedisClient(&instance, db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
@@ -909,7 +910,7 @@ func RedisDeleteKey(c *gin.Context) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
deleted, err := client.Del(ctx, key).Result()
|
deleted, err := client.Del(ctx, key).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Key deleted", "deleted": deleted})
|
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"))
|
id, _ := strconv.Atoi(c.Param("id"))
|
||||||
var wf model.WorkflowDefinition
|
var wf model.WorkflowDefinition
|
||||||
if err := model.DB.First(&wf, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
// clone
|
// 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"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"sql-platform/internal/handler/resp"
|
||||||
"sql-platform/internal/middleware"
|
"sql-platform/internal/middleware"
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
@@ -71,19 +72,19 @@ func CancelScheduledTask(c *gin.Context) {
|
|||||||
|
|
||||||
var ticket model.Ticket
|
var ticket model.Ticket
|
||||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only the applicant or admin/dba can cancel
|
// Only the applicant or admin/dba can cancel
|
||||||
if ticket.ApplicantID != userID && role != "admin" && role != "dba" {
|
if ticket.ApplicantID != userID && role != "admin" && role != "dba" {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"detail": "Permission denied"})
|
resp.Forbidden(c, "Permission denied")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow cancellation of scheduled tickets
|
// Only allow cancellation of scheduled tickets
|
||||||
if !ticket.ScheduledAt.Valid {
|
if !ticket.ScheduledAt.Valid {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Ticket is not scheduled"})
|
resp.BadRequest(c, "Ticket is not scheduled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"sql-platform/internal/handler/resp"
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -20,7 +21,7 @@ func AuditTicketSQL(c *gin.Context) {
|
|||||||
Database string `json:"database"`
|
Database string `json:"database"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +185,7 @@ func SQLFormat(c *gin.Context) {
|
|||||||
SQL string `json:"sql" binding:"required"`
|
SQL string `json:"sql" binding:"required"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +201,7 @@ func SQLAnalyze(c *gin.Context) {
|
|||||||
DBType string `json:"db_type"`
|
DBType string `json:"db_type"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sql-platform/internal/ghost"
|
"sql-platform/internal/ghost"
|
||||||
|
"sql-platform/internal/handler/resp"
|
||||||
"sql-platform/internal/middleware"
|
"sql-platform/internal/middleware"
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
@@ -91,8 +92,12 @@ func ListTickets(c *gin.Context) {
|
|||||||
status := c.Query("status")
|
status := c.Query("status")
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
limit, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
limit, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||||
if page < 1 { page = 1 }
|
if page < 1 {
|
||||||
if limit < 1 || limit > 100 { limit = 20 }
|
page = 1
|
||||||
|
}
|
||||||
|
if limit < 1 || limit > 100 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
|
||||||
query := model.DB.Model(&model.Ticket{}).Order("id DESC")
|
query := model.DB.Model(&model.Ticket{}).Order("id DESC")
|
||||||
|
|
||||||
@@ -162,7 +167,7 @@ func GetTicket(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var ticket model.Ticket
|
var ticket model.Ticket
|
||||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +228,7 @@ func CreateTicket(c *gin.Context) {
|
|||||||
|
|
||||||
// auditor cannot create tickets
|
// auditor cannot create tickets
|
||||||
if userRole == "auditor" {
|
if userRole == "auditor" {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"detail": "权限不足:审计员不能提交工单"})
|
resp.Forbidden(c, "权限不足:审计员不能提交工单")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +247,7 @@ func CreateTicket(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +257,7 @@ func CreateTicket(c *gin.Context) {
|
|||||||
readOnlyPrefixes := []string{"SELECT", "SHOW", "DESC", "DESCRIBE", "EXPLAIN"}
|
readOnlyPrefixes := []string{"SELECT", "SHOW", "DESC", "DESCRIBE", "EXPLAIN"}
|
||||||
for _, prefix := range readOnlyPrefixes {
|
for _, prefix := range readOnlyPrefixes {
|
||||||
if strings.HasPrefix(upperSQL, prefix) {
|
if strings.HasPrefix(upperSQL, prefix) {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "数据变更工单不支持 SELECT/SHOW/DESC 等只读语句,请使用查询数据功能"})
|
resp.BadRequest(c, "数据变更工单不支持 SELECT/SHOW/DESC 等只读语句,请使用查询数据功能")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,7 +272,7 @@ func CreateTicket(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
for _, ro := range redisReadOnlyCmds {
|
for _, ro := range redisReadOnlyCmds {
|
||||||
if cmdUpper == ro {
|
if cmdUpper == ro {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("数据变更工单不支持只读命令 %s,请使用查询功能", cmdUpper)})
|
resp.BadRequest(c, fmt.Sprintf("数据变更工单不支持只读命令 %s,请使用查询功能", cmdUpper))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,7 +384,7 @@ func CreateTicket(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := model.DB.Create(&ticket).Error; err != nil {
|
if err := model.DB.Create(&ticket).Error; err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,13 +481,13 @@ func ApproveTicket(c *gin.Context) {
|
|||||||
|
|
||||||
// Role check: admin/dba/developer can approve
|
// Role check: admin/dba/developer can approve
|
||||||
if userRole != "admin" && userRole != "dba" && userRole != "developer" {
|
if userRole != "admin" && userRole != "dba" && userRole != "developer" {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"detail": "权限不足:审核操作需要管理员、DBA 或开发者角色"})
|
resp.Forbidden(c, "权限不足:审核操作需要管理员、DBA 或开发者角色")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ticket model.Ticket
|
var ticket model.Ticket
|
||||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,7 +496,7 @@ func ApproveTicket(c *gin.Context) {
|
|||||||
Comment *string `json:"comment"`
|
Comment *string `json:"comment"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,7 +506,7 @@ func ApproveTicket(c *gin.Context) {
|
|||||||
var approval model.TicketApproval
|
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 {
|
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
|
// Already approved or no pending approval for this user
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "没有待处理的审批记录(你可能已经审批过了)"})
|
resp.BadRequest(c, "没有待处理的审批记录(你可能已经审批过了)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,19 +544,19 @@ func ExecuteTicket(c *gin.Context) {
|
|||||||
// Role check: only admin/dba can execute
|
// Role check: only admin/dba can execute
|
||||||
_, _, userRole := middleware.GetCurrentUser(c)
|
_, _, userRole := middleware.GetCurrentUser(c)
|
||||||
if userRole != "admin" && userRole != "dba" {
|
if userRole != "admin" && userRole != "dba" {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"detail": "权限不足:执行操作需要管理员或 DBA 角色"})
|
resp.Forbidden(c, "权限不足:执行操作需要管理员或 DBA 角色")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ticket model.Ticket
|
var ticket model.Ticket
|
||||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check ticket status — allow approved OR reviewing (will auto-approve execute step)
|
// Check ticket status — allow approved OR reviewing (will auto-approve execute step)
|
||||||
if ticket.Status != nil && *ticket.Status != "approved" && *ticket.Status != "reviewing" {
|
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
|
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).
|
Where("ticket_id = ? AND step_type = 'execute' AND (approver_id = ? OR approver_id IS NULL)", id, userID).
|
||||||
Count(&executorCheck)
|
Count(&executorCheck)
|
||||||
if executorCheck == 0 {
|
if executorCheck == 0 {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"detail": "你不是此工单的执行人"})
|
resp.Forbidden(c, "你不是此工单的执行人")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,7 +592,7 @@ func ExecuteTicket(c *gin.Context) {
|
|||||||
Where("ticket_id = ? AND step_type = 'audit' AND action IS NULL", id).
|
Where("ticket_id = ? AND step_type = 'audit' AND action IS NULL", id).
|
||||||
Count(&pendingAudit)
|
Count(&pendingAudit)
|
||||||
if pendingAudit > 0 {
|
if pendingAudit > 0 {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "还有未完成的审核步骤"})
|
resp.BadRequest(c, "还有未完成的审核步骤")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,14 +602,14 @@ func ExecuteTicket(c *gin.Context) {
|
|||||||
Where("ticket_id = ? AND step_type = 'audit' AND action = 'reject'", id).
|
Where("ticket_id = ? AND step_type = 'audit' AND action = 'reject'", id).
|
||||||
Count(&rejectedAudit)
|
Count(&rejectedAudit)
|
||||||
if rejectedAudit > 0 {
|
if rejectedAudit > 0 {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "审核已被拒绝,无法执行"})
|
resp.BadRequest(c, "审核已被拒绝,无法执行")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get instance info
|
// Get instance info
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, ticket.InstanceID).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,7 +859,7 @@ func ExecuteTicket(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
executionDone:
|
executionDone:
|
||||||
// Update final status
|
// Update final status
|
||||||
if execErr != "" {
|
if execErr != "" {
|
||||||
failStatus := "failed"
|
failStatus := "failed"
|
||||||
@@ -930,7 +935,6 @@ func parseAlterTable(sql string) (string, string, error) {
|
|||||||
return tableName, alterClause, nil
|
return tableName, alterClause, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// POST /api/v1/tickets/:id/cancel
|
// POST /api/v1/tickets/:id/cancel
|
||||||
func CancelTicket(c *gin.Context) {
|
func CancelTicket(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
@@ -938,7 +942,7 @@ func CancelTicket(c *gin.Context) {
|
|||||||
|
|
||||||
var ticket model.Ticket
|
var ticket model.Ticket
|
||||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -946,13 +950,13 @@ func CancelTicket(c *gin.Context) {
|
|||||||
if ticket.ApplicantID != userID {
|
if ticket.ApplicantID != userID {
|
||||||
_, _, role := middleware.GetCurrentUser(c)
|
_, _, role := middleware.GetCurrentUser(c)
|
||||||
if role != "admin" {
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ticket.Status != nil && (*ticket.Status != "pending" && *ticket.Status != "reviewing" && *ticket.Status != "approved") {
|
if ticket.Status != nil && (*ticket.Status != "pending" && *ticket.Status != "reviewing" && *ticket.Status != "approved") {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "只能撤销待审核、审核中或已通过(待执行)的工单"})
|
resp.BadRequest(c, "只能撤销待审核、审核中或已通过(待执行)的工单")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -971,30 +975,30 @@ func RollbackTicket(c *gin.Context) {
|
|||||||
|
|
||||||
var ticket model.Ticket
|
var ticket model.Ticket
|
||||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ticket.RollbackSQL == nil || *ticket.RollbackSQL == "" {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get instance
|
// Get instance
|
||||||
var instance model.Instance
|
var instance model.Instance
|
||||||
if err := model.DB.First(&instance, ticket.InstanceID).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ticket.DBType == "redis" {
|
if ticket.DBType == "redis" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Redis rollback not supported"})
|
resp.BadRequest(c, "Redis rollback not supported")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := getDBConnection(&instance, ticket.TargetDatabase)
|
db, err := getDBConnection(&instance, ticket.TargetDatabase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
@@ -1037,7 +1041,7 @@ func GetTicketTimeline(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var ticket model.Ticket
|
var ticket model.Ticket
|
||||||
if err := model.DB.First(&ticket, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1095,7 +1099,12 @@ func GetTicketTimeline(c *gin.Context) {
|
|||||||
ID: 99,
|
ID: 99,
|
||||||
Action: execAction,
|
Action: execAction,
|
||||||
Timestamp: ticket.ExecuteTime.Format(time.RFC3339),
|
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 ""
|
||||||
|
}(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1108,7 +1117,7 @@ func GetTicketApprovals(c *gin.Context) {
|
|||||||
|
|
||||||
var approvals []model.TicketApproval
|
var approvals []model.TicketApproval
|
||||||
if err := model.DB.Where("ticket_id = ?", id).Order("step_number").Find(&approvals).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"sql-platform/internal/handler/resp"
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -15,7 +16,7 @@ func GetWorkflow(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var wf model.WorkflowDefinition
|
var wf model.WorkflowDefinition
|
||||||
if err := model.DB.First(&wf, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
var steps []model.WorkflowStep
|
var steps []model.WorkflowStep
|
||||||
@@ -55,7 +56,7 @@ func CreateWorkflow(c *gin.Context) {
|
|||||||
} `json:"steps"`
|
} `json:"steps"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ func CreateWorkflow(c *gin.Context) {
|
|||||||
IsDefault: input.IsDefault,
|
IsDefault: input.IsDefault,
|
||||||
}
|
}
|
||||||
if err := model.DB.Create(&wf).Error; err != nil {
|
if err := model.DB.Create(&wf).Error; err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
resp.InternalErr(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,13 +100,13 @@ func UpdateWorkflow(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var wf model.WorkflowDefinition
|
var wf model.WorkflowDefinition
|
||||||
if err := model.DB.First(&wf, id).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var input map[string]interface{}
|
var input map[string]interface{}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +172,7 @@ func ResetPassword(c *gin.Context) {
|
|||||||
NewPassword string `json:"new_password" binding:"required"`
|
NewPassword string `json:"new_password" binding:"required"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _ = id, input.NewPassword
|
_, _ = id, input.NewPassword
|
||||||
@@ -193,7 +194,7 @@ func BatchImportUsers(c *gin.Context) {
|
|||||||
SkipExisting bool `json:"skip_existing"`
|
SkipExisting bool `json:"skip_existing"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +277,7 @@ func BatchImportLdapUsers(c *gin.Context) {
|
|||||||
} `json:"users"`
|
} `json:"users"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,21 +360,21 @@ func ResetUserPassword(c *gin.Context) {
|
|||||||
NewPassword string `json:"new_password" binding:"required"`
|
NewPassword string `json:"new_password" binding:"required"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
resp.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id, err := strconv.Atoi(userId)
|
id, err := strconv.Atoi(userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Invalid user ID"})
|
resp.BadRequest(c, "Invalid user ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(input.NewPassword), bcrypt.DefaultCost)
|
hash, err := bcrypt.GenerateFromPassword([]byte(input.NewPassword), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Hash failed"})
|
resp.InternalMsg(c, "Hash failed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := model.DB.Model(&model.User{}).Where("id = ?", id).Update("hashed_password", string(hash)).Error; err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Password reset successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": "Password reset successfully"})
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -13,6 +16,71 @@ import (
|
|||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RefreshTokenTTL refresh token 有效期;access token 有效期仍走 session_timeout_hours
|
||||||
|
const RefreshTokenTTL = 7 * 24 * time.Hour
|
||||||
|
|
||||||
|
func hashToken(token string) string {
|
||||||
|
sum := sha256.Sum256([]byte(token))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateRefreshToken 签发 refresh token(明文只返回给客户端,库里只存 SHA-256 摘要)
|
||||||
|
func GenerateRefreshToken(userID int) (string, error) {
|
||||||
|
buf := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(buf); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
token := hex.EncodeToString(buf)
|
||||||
|
rt := model.RefreshToken{
|
||||||
|
UserID: userID,
|
||||||
|
TokenHash: hashToken(token),
|
||||||
|
ExpiresAt: time.Now().Add(RefreshTokenTTL),
|
||||||
|
}
|
||||||
|
if err := model.DB.Create(&rt).Error; err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// 顺手清理该用户已过期/已撤销的记录
|
||||||
|
model.DB.Where("user_id = ? AND (expires_at < ? OR revoked_at IS NOT NULL)", userID, time.Now()).Delete(&model.RefreshToken{})
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConsumeRefreshToken 校验并轮换 refresh token(旧 token 撤销、签发新对),返回所属用户与新 refresh token。
|
||||||
|
func ConsumeRefreshToken(tokenStr string) (*model.User, string, error) {
|
||||||
|
var rt model.RefreshToken
|
||||||
|
if err := model.DB.Where("token_hash = ?", hashToken(tokenStr)).First(&rt).Error; err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if rt.RevokedAt != nil || rt.ExpiresAt.Before(time.Now()) {
|
||||||
|
return nil, "", ErrTokenRevoked
|
||||||
|
}
|
||||||
|
|
||||||
|
var user model.User
|
||||||
|
if err := model.DB.First(&user, rt.UserID).Error; err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if user.IsActive != nil && !*user.IsActive {
|
||||||
|
return nil, "", ErrTokenRevoked
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
rt.RevokedAt = &now
|
||||||
|
model.DB.Save(&rt)
|
||||||
|
|
||||||
|
newToken, err := GenerateRefreshToken(rt.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
return &user, newToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeUserRefreshTokens 撤销用户全部 refresh token(登出/改密时调用)
|
||||||
|
func RevokeUserRefreshTokens(userID int) {
|
||||||
|
now := time.Now()
|
||||||
|
model.DB.Model(&model.RefreshToken{}).
|
||||||
|
Where("user_id = ? AND revoked_at IS NULL", userID).
|
||||||
|
Update("revoked_at", now)
|
||||||
|
}
|
||||||
|
|
||||||
// getSessionTimeoutHours reads session_timeout_hours from system_settings,
|
// getSessionTimeoutHours reads session_timeout_hours from system_settings,
|
||||||
// falls back to config/env, then default 24h.
|
// falls back to config/env, then default 24h.
|
||||||
func getSessionTimeoutHours() int {
|
func getSessionTimeoutHours() int {
|
||||||
@@ -35,6 +103,13 @@ type Claims struct {
|
|||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrTokenRevoked refresh token 已撤销/过期
|
||||||
|
var ErrTokenRevoked = errTokenRevoked{}
|
||||||
|
|
||||||
|
type errTokenRevoked struct{}
|
||||||
|
|
||||||
|
func (errTokenRevoked) Error() string { return "refresh token revoked or expired" }
|
||||||
|
|
||||||
func GenerateToken(user *model.User) (string, error) {
|
func GenerateToken(user *model.User) (string, error) {
|
||||||
claims := Claims{
|
claims := Claims{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -57,6 +58,15 @@ type Instance struct {
|
|||||||
|
|
||||||
func (Instance) TableName() string { return "instances" }
|
func (Instance) TableName() string { return "instances" }
|
||||||
|
|
||||||
|
// MarshalJSON 保证实例密文密码在任何响应中都不出站,
|
||||||
|
// 消除各 handler 手动遮蔽遗漏的隐患(入站绑定不受影响)。
|
||||||
|
func (i Instance) MarshalJSON() ([]byte, error) {
|
||||||
|
type instanceAlias Instance
|
||||||
|
a := instanceAlias(i)
|
||||||
|
a.Password = ""
|
||||||
|
return json.Marshal(a)
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Ticket ====================
|
// ==================== Ticket ====================
|
||||||
|
|
||||||
type Ticket struct {
|
type Ticket struct {
|
||||||
@@ -494,6 +504,20 @@ type UserGroup struct {
|
|||||||
|
|
||||||
func (UserGroup) TableName() string { return "user_groups" }
|
func (UserGroup) TableName() string { return "user_groups" }
|
||||||
|
|
||||||
|
// ==================== RefreshToken ====================
|
||||||
|
|
||||||
|
// RefreshToken 持久化 refresh token 的 SHA-256 摘要(明文不落库),支持过期与撤销。
|
||||||
|
type RefreshToken struct {
|
||||||
|
ID int `gorm:"primaryKey" json:"id"`
|
||||||
|
UserID int `gorm:"index;not null" json:"user_id"`
|
||||||
|
TokenHash string `gorm:"size:64;uniqueIndex;not null" json:"-"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
RevokedAt *time.Time `json:"revoked_at"`
|
||||||
|
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RefreshToken) TableName() string { return "refresh_tokens" }
|
||||||
|
|
||||||
// ==================== DTO ====================
|
// ==================== DTO ====================
|
||||||
|
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
@@ -503,6 +527,7 @@ type LoginRequest struct {
|
|||||||
|
|
||||||
type LoginResponse struct {
|
type LoginResponse struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
TokenType string `json:"token_type"`
|
TokenType string `json:"token_type"`
|
||||||
User User `json:"user"`
|
User User `json:"user"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Package safe 提供带 recover 的后台 goroutine 包装:
|
||||||
|
// 裸 go func() 一旦 panic 会击穿整个进程,后台任务必须经由 safe.Go 启动。
|
||||||
|
package safe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"runtime/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Go 启动带 recover 的后台任务;panic 只记录堆栈,不影响进程。
|
||||||
|
func Go(name string, fn func()) {
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("[safe:%s] panic recovered: %v\n%s", name, r, debug.Stack())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
fn()
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"sql-platform/internal/middleware"
|
|
||||||
"sql-platform/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func stringPtr(value string) *string {
|
|
||||||
return &value
|
|
||||||
}
|
|
||||||
|
|
||||||
func boolPtr(value bool) *bool {
|
|
||||||
return &value
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthService struct {
|
|
||||||
DB *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAuthService(db *gorm.DB) *AuthService {
|
|
||||||
return &AuthService{DB: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login authenticates a user and returns a JWT token
|
|
||||||
func (s *AuthService) Login(username, password string) (*model.User, string, error) {
|
|
||||||
var user model.User
|
|
||||||
if err := s.DB.Where("username = ?", username).First(&user).Error; err != nil {
|
|
||||||
return nil, "", errors.New("invalid username or password")
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.IsActive != nil && !*user.IsActive {
|
|
||||||
return nil, "", errors.New("account is disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(password)); err != nil {
|
|
||||||
return nil, "", errors.New("invalid username or password")
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := middleware.GenerateToken(&user)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", errors.New("failed to generate token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update last login
|
|
||||||
now := time.Now()
|
|
||||||
user.LastLogin = &now
|
|
||||||
s.DB.Model(&user).Update("last_login", now)
|
|
||||||
|
|
||||||
return &user, token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register creates a new user
|
|
||||||
func (s *AuthService) Register(username, displayName, email, phone, password, role string) (*model.User, error) {
|
|
||||||
// Check if username already exists
|
|
||||||
var count int64
|
|
||||||
s.DB.Model(&model.User{}).Where("username = ?", username).Count(&count)
|
|
||||||
if count > 0 {
|
|
||||||
return nil, errors.New("username already exists")
|
|
||||||
}
|
|
||||||
|
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("failed to hash password")
|
|
||||||
}
|
|
||||||
|
|
||||||
if role == "" {
|
|
||||||
role = "developer"
|
|
||||||
}
|
|
||||||
|
|
||||||
user := model.User{
|
|
||||||
Username: username,
|
|
||||||
DisplayName: displayName,
|
|
||||||
Email: stringPtr(email),
|
|
||||||
Phone: stringPtr(phone),
|
|
||||||
HashedPassword: string(hashedPassword),
|
|
||||||
Role: role,
|
|
||||||
IsActive: boolPtr(true),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.DB.Create(&user).Error; err != nil {
|
|
||||||
return nil, errors.New("failed to create user")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByID retrieves a user by ID
|
|
||||||
func (s *AuthService) GetUserByID(id uint) (*model.User, error) {
|
|
||||||
var user model.User
|
|
||||||
if err := s.DB.First(&user, id).Error; err != nil {
|
|
||||||
return nil, errors.New("user not found")
|
|
||||||
}
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListUsers returns all users
|
|
||||||
func (s *AuthService) ListUsers(page, pageSize int) ([]model.User, int64, error) {
|
|
||||||
var users []model.User
|
|
||||||
var total int64
|
|
||||||
|
|
||||||
db := s.DB.Model(&model.User{})
|
|
||||||
db.Count(&total)
|
|
||||||
|
|
||||||
offset := (page - 1) * pageSize
|
|
||||||
if err := db.Offset(offset).Limit(pageSize).Find(&users).Error; err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return users, total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateUser updates a user's info
|
|
||||||
func (s *AuthService) UpdateUser(id uint, updates map[string]interface{}) error {
|
|
||||||
return s.DB.Model(&model.User{}).Where("id = ?", id).Updates(updates).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteUser soft-deletes a user
|
|
||||||
func (s *AuthService) DeleteUser(id uint) error {
|
|
||||||
return s.DB.Delete(&model.User{}, id).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChangePassword changes a user's password
|
|
||||||
func (s *AuthService) ChangePassword(id uint, oldPassword, newPassword string) error {
|
|
||||||
var user model.User
|
|
||||||
if err := s.DB.First(&user, id).Error; err != nil {
|
|
||||||
return errors.New("user not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(oldPassword)); err != nil {
|
|
||||||
return errors.New("incorrect current password")
|
|
||||||
}
|
|
||||||
|
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("failed to hash password")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.DB.Model(&user).Update("hashed_password", string(hashedPassword)).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetPassword resets a user's password (admin only)
|
|
||||||
func (s *AuthService) ResetPassword(id uint, newPassword string) error {
|
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("failed to hash password")
|
|
||||||
}
|
|
||||||
return s.DB.Model(&model.User{}).Where("id = ?", id).Update("hashed_password", string(hashedPassword)).Error
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"sql-platform/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type InstanceService struct {
|
|
||||||
DB *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInstanceService(db *gorm.DB) *InstanceService {
|
|
||||||
return &InstanceService{DB: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateInstance creates a new database instance
|
|
||||||
func (s *InstanceService) CreateInstance(instance *model.Instance) error {
|
|
||||||
return s.DB.Create(instance).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInstanceByID retrieves an instance by ID
|
|
||||||
func (s *InstanceService) GetInstanceByID(id uint) (*model.Instance, error) {
|
|
||||||
var instance model.Instance
|
|
||||||
if err := s.DB.First(&instance, id).Error; err != nil {
|
|
||||||
return nil, errors.New("instance not found")
|
|
||||||
}
|
|
||||||
return &instance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListInstances returns paginated instances
|
|
||||||
func (s *InstanceService) ListInstances(page, pageSize int, dbType, env string) ([]model.Instance, int64, error) {
|
|
||||||
var instances []model.Instance
|
|
||||||
var total int64
|
|
||||||
|
|
||||||
db := s.DB.Model(&model.Instance{})
|
|
||||||
if dbType != "" {
|
|
||||||
db = db.Where("db_type = ?", dbType)
|
|
||||||
}
|
|
||||||
if env != "" {
|
|
||||||
db = db.Where("env = ?", env)
|
|
||||||
}
|
|
||||||
|
|
||||||
db.Count(&total)
|
|
||||||
offset := (page - 1) * pageSize
|
|
||||||
if err := db.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&instances).Error; err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return instances, total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateInstance updates an instance
|
|
||||||
func (s *InstanceService) UpdateInstance(id uint, updates map[string]interface{}) error {
|
|
||||||
result := s.DB.Model(&model.Instance{}).Where("id = ?", id).Updates(updates)
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return errors.New("instance not found")
|
|
||||||
}
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteInstance soft-deletes an instance
|
|
||||||
func (s *InstanceService) DeleteInstance(id uint) error {
|
|
||||||
return s.DB.Delete(&model.Instance{}, id).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestConnection tests the database connection
|
|
||||||
func (s *InstanceService) TestConnection(instance *model.Instance) error {
|
|
||||||
// TODO: Implement actual connection test based on db_type
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListInstancesByIDs returns instances matching the given IDs
|
|
||||||
func (s *InstanceService) ListInstancesByIDs(ids []uint) ([]model.Instance, error) {
|
|
||||||
var instances []model.Instance
|
|
||||||
if err := s.DB.Where("id IN ?", ids).Find(&instances).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return instances, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInstanceGroups returns all instance groups
|
|
||||||
func (s *InstanceService) GetInstanceGroups() ([]model.InstanceGroup, error) {
|
|
||||||
var groups []model.InstanceGroup
|
|
||||||
err := s.DB.Find(&groups).Error
|
|
||||||
return groups, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateInstanceGroup creates a new instance group
|
|
||||||
func (s *InstanceService) CreateInstanceGroup(group *model.InstanceGroup) error {
|
|
||||||
return s.DB.Create(group).Error
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"sql-platform/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TicketService struct {
|
|
||||||
DB *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTicketService(db *gorm.DB) *TicketService {
|
|
||||||
return &TicketService{DB: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateTicket creates a new ticket
|
|
||||||
func (s *TicketService) CreateTicket(ticket *model.Ticket) error {
|
|
||||||
if ticket.TicketNo == nil || *ticket.TicketNo == "" {
|
|
||||||
ticketNo := s.generateTicketNo()
|
|
||||||
ticket.TicketNo = &ticketNo
|
|
||||||
}
|
|
||||||
if ticket.CreateTime.IsZero() {
|
|
||||||
ticket.CreateTime = time.Now()
|
|
||||||
}
|
|
||||||
if ticket.Status == nil || *ticket.Status == "" {
|
|
||||||
status := "pending"
|
|
||||||
ticket.Status = &status
|
|
||||||
}
|
|
||||||
return s.DB.Create(ticket).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTicketByID retrieves a ticket by ID
|
|
||||||
func (s *TicketService) GetTicketByID(id uint) (*model.Ticket, error) {
|
|
||||||
var ticket model.Ticket
|
|
||||||
if err := s.DB.First(&ticket, id).Error; err != nil {
|
|
||||||
return nil, errors.New("ticket not found")
|
|
||||||
}
|
|
||||||
return &ticket, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListTickets returns paginated tickets with filters
|
|
||||||
func (s *TicketService) ListTickets(page, pageSize int, status, priority string, applicantID uint) ([]model.Ticket, int64, error) {
|
|
||||||
var tickets []model.Ticket
|
|
||||||
var total int64
|
|
||||||
|
|
||||||
db := s.DB.Model(&model.Ticket{})
|
|
||||||
if status != "" {
|
|
||||||
db = db.Where("status = ?", status)
|
|
||||||
}
|
|
||||||
if priority != "" {
|
|
||||||
db = db.Where("priority = ?", priority)
|
|
||||||
}
|
|
||||||
if applicantID > 0 {
|
|
||||||
db = db.Where("applicant_id = ?", applicantID)
|
|
||||||
}
|
|
||||||
|
|
||||||
db.Count(&total)
|
|
||||||
offset := (page - 1) * pageSize
|
|
||||||
if err := db.Offset(offset).Limit(pageSize).Order("create_time DESC").Find(&tickets).Error; err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tickets, total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateTicketStatus updates a ticket's status
|
|
||||||
func (s *TicketService) UpdateTicketStatus(id uint, status string, updaterID uint) error {
|
|
||||||
updates := map[string]interface{}{
|
|
||||||
"status": status,
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
switch status {
|
|
||||||
case "approved":
|
|
||||||
updates["approver_id"] = updaterID
|
|
||||||
updates["review_time"] = now
|
|
||||||
case "executing", "completed", "failed":
|
|
||||||
updates["execute_time"] = now
|
|
||||||
}
|
|
||||||
|
|
||||||
result := s.DB.Model(&model.Ticket{}).Where("id = ?", id).Updates(updates)
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return errors.New("ticket not found")
|
|
||||||
}
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApproveTicket approves a ticket
|
|
||||||
func (s *TicketService) ApproveTicket(id, approverID uint, comment string) error {
|
|
||||||
return s.UpdateTicketStatus(id, "approved", approverID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RejectTicket rejects a ticket
|
|
||||||
func (s *TicketService) RejectTicket(id, approverID uint, comment string) error {
|
|
||||||
return s.UpdateTicketStatus(id, "rejected", approverID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTicketSQLItems returns SQL items for a ticket
|
|
||||||
func (s *TicketService) GetTicketSQLItems(ticketID uint) ([]model.TicketSQLItem, error) {
|
|
||||||
var items []model.TicketSQLItem
|
|
||||||
err := s.DB.Where("ticket_id = ?", ticketID).Order("seq ASC").Find(&items).Error
|
|
||||||
return items, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateTicketSQLItem creates a SQL item for a ticket
|
|
||||||
func (s *TicketService) CreateTicketSQLItem(item *model.TicketSQLItem) error {
|
|
||||||
return s.DB.Create(item).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTicketApprovals returns approval records for a ticket
|
|
||||||
func (s *TicketService) GetTicketApprovals(ticketID uint) ([]model.TicketApproval, error) {
|
|
||||||
var approvals []model.TicketApproval
|
|
||||||
err := s.DB.Where("ticket_id = ?", ticketID).Order("step_number ASC").Find(&approvals).Error
|
|
||||||
return approvals, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteTicket soft-deletes a ticket
|
|
||||||
func (s *TicketService) DeleteTicket(id uint) error {
|
|
||||||
return s.DB.Delete(&model.Ticket{}, id).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateTicketNo generates a unique ticket number
|
|
||||||
func (s *TicketService) generateTicketNo() string {
|
|
||||||
now := time.Now()
|
|
||||||
var count int64
|
|
||||||
s.DB.Model(&model.Ticket{}).Where("DATE(create_time) = DATE(?)", now).Count(&count)
|
|
||||||
return fmt.Sprintf("TK-%s-%04d", now.Format("20060102"), count+1)
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@ func Setup(r *gin.Engine) {
|
|||||||
|
|
||||||
// Public routes
|
// Public routes
|
||||||
api.POST("/auth/login", handler.Login)
|
api.POST("/auth/login", handler.Login)
|
||||||
|
api.POST("/auth/refresh", handler.RefreshToken)
|
||||||
api.GET("/public/login-style", handler.GetPublicLoginStyle)
|
api.GET("/public/login-style", handler.GetPublicLoginStyle)
|
||||||
|
|
||||||
// Authenticated routes
|
// Authenticated routes
|
||||||
@@ -20,6 +21,7 @@ func Setup(r *gin.Engine) {
|
|||||||
{
|
{
|
||||||
// Auth
|
// Auth
|
||||||
auth.GET("/auth/me", handler.GetMe)
|
auth.GET("/auth/me", handler.GetMe)
|
||||||
|
auth.POST("/auth/logout", handler.Logout)
|
||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
auth.GET("/dashboard/stats", handler.DashboardStats)
|
auth.GET("/dashboard/stats", handler.DashboardStats)
|
||||||
|
|||||||
Reference in new issue
Block a user