Files
opsxsql/backend/internal/handler/instance_extra.go
T
Logan.Li 16f1f7d65f 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
2026-10-26 17:21:00 +08:00

129 lines
3.4 KiB
Go

package handler
import (
"net/http"
"sql-platform/internal/handler/resp"
"sql-platform/internal/middleware"
"sql-platform/internal/model"
"github.com/gin-gonic/gin"
)
// GET /api/v1/instances/accessible?access_type=readonly|write
func AccessibleInstances(c *gin.Context) {
userID, _, role := middleware.GetCurrentUser(c)
accessType := c.DefaultQuery("access_type", "readonly")
var instances []model.Instance
if role == "admin" || role == "dba" {
// admin/dba see all instances, filter by access_mode if needed
query := model.DB.Order("id")
if accessType == "readonly" {
query = query.Where("access_mode IN ? OR access_mode IS NULL", []string{"readwrite", "readonly"})
} else {
query = query.Where("access_mode IN ? OR access_mode IS NULL", []string{"readwrite", "write"})
}
query.Find(&instances)
} else {
// developer/auditor: only instances in their groups
var groupIDs []int
model.DB.Model(&model.UserGroup{}).Where("user_id = ?", userID).Pluck("group_id", &groupIDs)
if len(groupIDs) == 0 {
c.JSON(http.StatusOK, []model.Instance{})
return
}
var instanceIDs []int
// Query for the requested access_type OR readwrite (readwrite grants both)
model.DB.Model(&model.InstanceGroupMember{}).
Where("group_id IN ? AND (access_type = ? OR access_type = ?)", groupIDs, accessType, "readwrite").
Pluck("instance_id", &instanceIDs)
if len(instanceIDs) == 0 {
c.JSON(http.StatusOK, []model.Instance{})
return
}
model.DB.Where("id IN ?", instanceIDs).Order("id").Find(&instances)
}
// mask passwords
for i := range instances {
instances[i].Password = "******"
}
c.JSON(http.StatusOK, instances)
}
// GET /api/v1/instances/:id/databases
func ListDatabases(c *gin.Context) {
id := c.Param("id")
var instance model.Instance
if err := model.DB.First(&instance, id).Error; err != nil {
resp.NotFound(c, "Instance not found")
return
}
databases, err := listDatabases(&instance)
if err != nil {
c.JSON(http.StatusOK, []string{})
return
}
if databases == nil {
databases = []string{}
}
c.JSON(http.StatusOK, databases)
}
// GET /api/v1/instances/:id/databases/:db/tables
func ListTables(c *gin.Context) {
id := c.Param("id")
dbName := c.Param("db")
var instance model.Instance
if err := model.DB.First(&instance, id).Error; err != nil {
resp.NotFound(c, "Instance not found")
return
}
tables, err := listTables(&instance, dbName)
if err != nil {
c.JSON(http.StatusOK, []interface{}{})
return
}
if tables == nil {
tables = []map[string]interface{}{}
}
c.JSON(http.StatusOK, tables)
}
// POST /api/v1/instances/test-connection (new instance, not saved yet)
func TestNewConnection(c *gin.Context) {
var req struct {
DBType string `json:"db_type"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Database string `json:"database"`
}
if err := c.ShouldBindJSON(&req); err != nil {
resp.BadRequest(c, "Invalid request")
return
}
instance := &model.Instance{
DBType: req.DBType,
Host: req.Host,
Port: req.Port,
Username: req.Username,
Password: req.Password,
}
if req.Database != "" {
instance.Database = &req.Database
}
result := testDBConnectionFull(instance)
c.JSON(http.StatusOK, gin.H{
"success": result.Success,
"message": result.Message,
"version": result.Version,
"cluster_mode": result.ClusterMode,
})
}