feat: K8s集群拓扑图接入真实后端数据 — 分层可视化Cluster→Ingress→Service→Deployment→Node + http.ts类型修复

This commit is contained in:
attacker committed 2026-06-14 20:12:42 +08:00
1 parent 4b094f4645
commit 9f65faff6f
17 files changed
+3021 -1205

No files matched your search

+263 -17
View File
@@ -149,7 +149,7 @@ func (h *LDAPHandler) GetLDAPConfig(c *gin.Context) {
cfg := configs[0]
// mask password
cfg.AdminPassword = "******"
c.JSON(http.StatusOK, cfg)
c.JSON(http.StatusOK, models.Response{Code: 200, Data: cfg})
}
// UpdateLDAPConfig 更新LDAP配置
@@ -312,11 +312,11 @@ func (h *LDAPHandler) TestLDAPConnection(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": false, "message": fmt.Sprintf("密码验证失败: %s", err)})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": fmt.Sprintf("认证成功!用户DN: %s", userDN)})
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{"success": true, "message": fmt.Sprintf("认证成功!用户DN: %s", userDN)}})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "LDAP连接成功,管理员认证通过"})
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{"success": true, "message": "LDAP连接成功,管理员认证通过"}})
}
// ListLDAPUsers 列出LDAP用户
@@ -386,7 +386,7 @@ func (h *LDAPHandler) ListLDAPUsers(c *gin.Context) {
Mail: e.GetAttributeValue("mail"),
})
}
c.JSON(http.StatusOK, users)
c.JSON(http.StatusOK, models.Response{Code: 200, Data: users})
}
// BatchImportLDAP 批量导入LDAP用户
@@ -394,9 +394,12 @@ func (h *LDAPHandler) ListLDAPUsers(c *gin.Context) {
func (h *LDAPHandler) BatchImportLDAP(c *gin.Context) {
var req struct {
Users []struct {
UID string `json:"uid"`
CN string `json:"cn"`
Mail string `json:"mail"`
UID string `json:"uid"`
CN string `json:"cn"`
Mail string `json:"mail"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Email string `json:"email"`
} `json:"users"`
}
if err := c.ShouldBindJSON(&req); err != nil {
@@ -407,23 +410,34 @@ func (h *LDAPHandler) BatchImportLDAP(c *gin.Context) {
imported := 0
skipped := 0
for _, lu := range req.Users {
if lu.UID == "" {
uid := lu.UID
if uid == "" {
uid = lu.Username
}
cn := lu.CN
if cn == "" {
cn = lu.Nickname
}
mail := lu.Mail
if mail == "" {
mail = lu.Email
}
if uid == "" {
skipped++
continue
}
// Check if already exists
var existing models.User
if err := h.db.Where("username = ?", lu.UID).First(&existing).Error; err == nil {
if err := h.db.Where("username = ?", uid).First(&existing).Error; err == nil {
// Already exists, update info
updates := map[string]interface{}{
"auth_source": "ldap",
"is_active": true,
}
if lu.CN != "" {
updates["real_name"] = lu.CN
if cn != "" {
updates["real_name"] = cn
}
if lu.Mail != "" {
updates["email"] = lu.Mail
if mail != "" {
updates["email"] = mail
}
h.db.Model(&existing).Updates(updates)
imported++
@@ -432,10 +446,10 @@ func (h *LDAPHandler) BatchImportLDAP(c *gin.Context) {
// Create new LDAP user
user := models.User{
Username: lu.UID,
Username: uid,
Password: "-", // LDAP users don't have local password
RealName: lu.CN,
Email: lu.Mail,
RealName: cn,
Email: mail,
Status: 1,
AuthSource: "ldap",
}
@@ -455,3 +469,235 @@ func (h *LDAPHandler) BatchImportLDAP(c *gin.Context) {
Data: gin.H{"imported": imported, "skipped": skipped},
})
}
// ListLDAPOUs 列出 LDAP 组织单元
// POST /api/v1/admin/ldap-config/ou-list
func (h *LDAPHandler) ListLDAPOUs(c *gin.Context) {
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
cfg := configs[0]
port := 389
if cfg.LDAPPort != nil {
port = *cfg.LDAPPort
}
conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port))
if err != nil {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
searchReq := ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
"(objectClass=organizationalUnit)",
[]string{"ou", "description"},
nil,
)
sr, err := conn.Search(searchReq)
if err != nil {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
type OU struct {
DN string `json:"dn"`
Name string `json:"name"`
Description string `json:"description"`
}
var ous []OU
for _, e := range sr.Entries {
ous = append(ous, OU{
DN: e.DN,
Name: e.GetAttributeValue("ou"),
Description: e.GetAttributeValue("description"),
})
}
c.JSON(http.StatusOK, models.Response{Code: 200, Data: ous})
}
// CreateLDAPOU 创建组织单元
// POST /api/v1/admin/ldap-config/create-ou
func (h *LDAPHandler) CreateLDAPOU(c *gin.Context) {
var req struct {
OUName string `json:"ou_name"`
ParentDN string `json:"parent_dn"`
Description string `json:"description"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
return
}
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "LDAP未配置"})
return
}
cfg := configs[0]
port := 389
if cfg.LDAPPort != nil {
port = *cfg.LDAPPort
}
if req.ParentDN == "" {
req.ParentDN = cfg.BaseDN
}
ouDN := fmt.Sprintf("ou=%s,%s", ldap.EscapeFilter(req.OUName), req.ParentDN)
conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port))
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "管理员认证失败: " + err.Error()})
return
}
addReq := ldap.NewAddRequest(ouDN, nil)
addReq.Attribute("objectClass", []string{"organizationalUnit", "top"})
addReq.Attribute("ou", []string{req.OUName})
if req.Description != "" {
addReq.Attribute("description", []string{req.Description})
}
if err := conn.Add(addReq); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建OU失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "OU创建成功", Data: gin.H{"dn": ouDN}})
}
// CreateLDAPUser 在LDAP中创建用户
// POST /api/v1/admin/ldap-config/create-user
func (h *LDAPHandler) CreateLDAPUser(c *gin.Context) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
DisplayName string `json:"display_name"`
Email string `json:"email"`
OUDN string `json:"ou_dn"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
return
}
if req.Username == "" || req.Password == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "用户名和密码不能为空"})
return
}
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "LDAP未配置"})
return
}
cfg := configs[0]
port := 389
if cfg.LDAPPort != nil {
port = *cfg.LDAPPort
}
if req.OUDN == "" {
req.OUDN = cfg.BaseDN
}
userDN := fmt.Sprintf("uid=%s,%s", ldap.EscapeFilter(req.Username), req.OUDN)
// Parse user mapping
mapping := map[string]string{"username": "uid", "email": "mail", "display_name": "cn"}
if cfg.UserMapping != nil {
_ = json.Unmarshal([]byte(*cfg.UserMapping), &mapping)
}
uidAttr := mapping["username"]
if uidAttr == "" {
uidAttr = "uid"
}
mailAttr := mapping["email"]
if mailAttr == "" {
mailAttr = "mail"
}
conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port))
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "管理员认证失败: " + err.Error()})
return
}
addReq := ldap.NewAddRequest(userDN, nil)
addReq.Attribute("objectClass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"})
addReq.Attribute(uidAttr, []string{req.Username})
addReq.Attribute("cn", []string{req.DisplayName})
addReq.Attribute("sn", []string{req.DisplayName})
if req.Email != "" {
addReq.Attribute(mailAttr, []string{req.Email})
}
addReq.Attribute("userPassword", []string{req.Password})
if err := conn.Add(addReq); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建用户失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "用户创建成功", Data: gin.H{"dn": userDN}})
}
// DeleteLDAPUser 删除LDAP条目(用户或OU)
// POST /api/v1/admin/ldap-config/delete-user
func (h *LDAPHandler) DeleteLDAPUser(c *gin.Context) {
var req struct {
DN string `json:"dn"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.DN == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "DN不能为空"})
return
}
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "LDAP未配置"})
return
}
cfg := configs[0]
port := 389
if cfg.LDAPPort != nil {
port = *cfg.LDAPPort
}
conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port))
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "管理员认证失败: " + err.Error()})
return
}
delReq := ldap.NewDelRequest(req.DN, nil)
if err := conn.Del(delReq); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
}
@@ -0,0 +1,134 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// ResourceModelHandler 资源模型处理器
type ResourceModelHandler struct {
db *gorm.DB
}
func NewResourceModelHandler(db *gorm.DB) *ResourceModelHandler {
return &ResourceModelHandler{db: db}
}
// resourceModelRequest 资源模型创建/更新请求
type resourceModelRequest struct {
Name string `json:"name"`
Code string `json:"code"`
Description string `json:"description"`
Type string `json:"type"`
Icon string `json:"icon"`
Attributes []map[string]interface{} `json:"attributes"`
}
// ListResourceModels 获取资源模型列表
func (h *ResourceModelHandler) ListResourceModels(c *gin.Context) {
var models_ []models.ResourceModel
h.db.Order("id ASC").Find(&models_)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: models_,
Total: int64(len(models_)),
},
})
}
// CreateResourceModel 创建资源模型
func (h *ResourceModelHandler) CreateResourceModel(c *gin.Context) {
var req resourceModelRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Code) == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "模型名称和编码不能为空"})
return
}
modelType := strings.TrimSpace(req.Type)
if modelType == "" {
modelType = "normal"
}
var attrsJSON string
if len(req.Attributes) > 0 {
b, _ := json.Marshal(req.Attributes)
attrsJSON = string(b)
}
rm := models.ResourceModel{
Name: strings.TrimSpace(req.Name),
Code: strings.TrimSpace(req.Code),
Description: req.Description,
Type: modelType,
Attributes: attrsJSON,
Icon: req.Icon,
}
if err := h.db.Create(&rm).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败,编码可能已存在"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: rm})
}
// UpdateResourceModel 更新资源模型
func (h *ResourceModelHandler) UpdateResourceModel(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req resourceModelRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
var rm models.ResourceModel
if err := h.db.First(&rm, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "资源模型不存在"})
return
}
if strings.TrimSpace(req.Name) != "" {
rm.Name = strings.TrimSpace(req.Name)
}
if strings.TrimSpace(req.Code) != "" {
rm.Code = strings.TrimSpace(req.Code)
}
if strings.TrimSpace(req.Type) != "" {
rm.Type = strings.TrimSpace(req.Type)
}
rm.Description = req.Description
rm.Icon = req.Icon
if len(req.Attributes) > 0 {
b, _ := json.Marshal(req.Attributes)
rm.Attributes = string(b)
}
if err := h.db.Save(&rm).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: rm})
}
// DeleteResourceModel 删除资源模型
func (h *ResourceModelHandler) DeleteResourceModel(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.ResourceModel{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
}
+234
View File
@@ -775,3 +775,237 @@ func (h *DomainHandler) UpdateDomain(c *gin.Context) {
Data: domain,
})
}
// ==================== 云平台(Platform / Cloud)====================
// PlatformHandler 云平台处理器
type PlatformHandler struct {
db *gorm.DB
}
func NewPlatformHandler(db *gorm.DB) *PlatformHandler {
return &PlatformHandler{db: db}
}
// platformRequest 云平台创建/更新请求
type platformRequest struct {
Name string `json:"name"`
Code string `json:"code"`
Type string `json:"type"`
Status string `json:"status"`
Website string `json:"website"`
Description string `json:"description"`
}
// ListPlatforms 获取云平台列表
func (h *PlatformHandler) ListPlatforms(c *gin.Context) {
var platforms []models.Cloud
h.db.Find(&platforms)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: platforms,
Total: int64(len(platforms)),
},
})
}
// CreatePlatform 创建云平台
func (h *PlatformHandler) CreatePlatform(c *gin.Context) {
var req platformRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Code) == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "平台名称和编码不能为空"})
return
}
cloudType := strings.TrimSpace(req.Type)
if cloudType == "" {
cloudType = "public"
}
status := strings.TrimSpace(req.Status)
if status == "" {
status = "active"
}
platform := models.Cloud{
Name: strings.TrimSpace(req.Name),
Code: strings.TrimSpace(req.Code),
Type: cloudType,
Status: status,
Website: req.Website,
Description: req.Description,
}
if err := h.db.Create(&platform).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败,编码可能已存在"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: platform})
}
// UpdatePlatform 更新云平台
func (h *PlatformHandler) UpdatePlatform(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req platformRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
var platform models.Cloud
if err := h.db.First(&platform, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "云平台不存在"})
return
}
if strings.TrimSpace(req.Name) != "" {
platform.Name = strings.TrimSpace(req.Name)
}
if strings.TrimSpace(req.Code) != "" {
platform.Code = strings.TrimSpace(req.Code)
}
if strings.TrimSpace(req.Type) != "" {
platform.Type = strings.TrimSpace(req.Type)
}
if strings.TrimSpace(req.Status) != "" {
platform.Status = strings.TrimSpace(req.Status)
}
platform.Website = req.Website
platform.Description = req.Description
if err := h.db.Save(&platform).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: platform})
}
// DeletePlatform 删除云平台
func (h *PlatformHandler) DeletePlatform(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.Cloud{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
}
// ==================== 区域管理(Region)====================
// RegionHandler 区域处理器
type RegionHandler struct {
db *gorm.DB
}
func NewRegionHandler(db *gorm.DB) *RegionHandler {
return &RegionHandler{db: db}
}
// regionRequest 区域创建/更新请求
type regionRequest struct {
Name string `json:"name"`
Code string `json:"code"`
Provider string `json:"provider"`
Location string `json:"location"`
Status string `json:"status"`
Endpoint string `json:"endpoint"`
}
// ListRegions 获取区域列表
func (h *RegionHandler) ListRegions(c *gin.Context) {
var regions []models.Region
h.db.Find(&regions)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: regions,
Total: int64(len(regions)),
},
})
}
// CreateRegion 创建区域
func (h *RegionHandler) CreateRegion(c *gin.Context) {
var req regionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Code) == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "区域名称和编码不能为空"})
return
}
status := strings.TrimSpace(req.Status)
if status == "" {
status = "active"
}
region := models.Region{
Name: strings.TrimSpace(req.Name),
Code: strings.TrimSpace(req.Code),
Provider: strings.TrimSpace(req.Provider),
Location: req.Location,
Status: status,
Endpoint: req.Endpoint,
}
if err := h.db.Create(&region).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败,编码可能已存在"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: region})
}
// UpdateRegion 更新区域
func (h *RegionHandler) UpdateRegion(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req regionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
var region models.Region
if err := h.db.First(&region, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "区域不存在"})
return
}
if strings.TrimSpace(req.Name) != "" {
region.Name = strings.TrimSpace(req.Name)
}
if strings.TrimSpace(req.Code) != "" {
region.Code = strings.TrimSpace(req.Code)
}
if strings.TrimSpace(req.Provider) != "" {
region.Provider = strings.TrimSpace(req.Provider)
}
region.Location = req.Location
if strings.TrimSpace(req.Status) != "" {
region.Status = strings.TrimSpace(req.Status)
}
region.Endpoint = req.Endpoint
if err := h.db.Save(&region).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: region})
}
// DeleteRegion 删除区域
func (h *RegionHandler) DeleteRegion(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.Region{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
}
+105
View File
@@ -1,8 +1,10 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
"strings"
"ezops/internal/api/middleware"
"ezops/internal/config"
@@ -597,3 +599,106 @@ func (h *UserHandler) DeleteUser(c *gin.Context) {
Message: "删除成功",
})
}
// BatchImportUsers 批量导入用户
// POST /api/v1/admin/users/batch-import
func (h *UserHandler) BatchImportUsers(c *gin.Context) {
var req struct {
Users []struct {
Username string `json:"username"`
Nickname string `json:"nickname"`
Email string `json:"email"`
Phone string `json:"phone"`
Password string `json:"password"`
Role string `json:"role"`
} `json:"users"`
SkipExisting bool `json:"skip_existing"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
imported := 0
skipped := 0
var errors []string
for i, u := range req.Users {
if u.Username == "" {
errors = append(errors, fmt.Sprintf("第%d行: 用户名为空", i+1))
continue
}
// Check if exists
var existing models.User
if err := h.db.Where("username = ?", u.Username).First(&existing).Error; err == nil {
if req.SkipExisting {
skipped++
continue
}
errors = append(errors, fmt.Sprintf("第%d行: 用户 %s 已存在", i+1, u.Username))
skipped++
continue
}
// Default password
pwd := u.Password
if pwd == "" {
pwd = "admin123"
}
hashed, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
if err != nil {
errors = append(errors, fmt.Sprintf("第%d行: 密码加密失败", i+1))
continue
}
user := models.User{
Username: u.Username,
Password: string(hashed),
Email: u.Email,
Phone: u.Phone,
RealName: u.Nickname,
Status: 1,
AuthSource: "local",
}
if err := h.db.Create(&user).Error; err != nil {
errors = append(errors, fmt.Sprintf("第%d行: 创建用户 %s 失败", i+1, u.Username))
continue
}
imported++
}
msg := fmt.Sprintf("导入完成: %d 成功, %d 跳过", imported, skipped)
if len(errors) > 0 {
msg += fmt.Sprintf(", %d 个错误", len(errors))
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: msg,
Data: gin.H{
"imported": imported,
"skipped": skipped,
"errors": errors,
},
})
}
// ImportTemplate 下载用户导入模板
// GET /api/v1/admin/users/import-template
func (h *UserHandler) ImportTemplate(c *gin.Context) {
// UTF-8 BOM for Excel compatibility
bom := "\xEF\xBB\xBF"
csv := bom + "用户名,昵称,邮箱,手机号,角色,密码\n"
csv += "zhangsan,张三,zhangsan@example.com,13800138000,developer,123456\n"
csv += "lisi,李四,lisi@example.com,13800138001,developer,123456\n"
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=users_import_template.csv")
c.String(http.StatusOK, csv)
}
// ensure strings import is used
var _ = strings.TrimSpace
+6
View File
@@ -41,6 +41,12 @@ func GenerateToken(user *models.User, cfg *config.JWTConfig) (string, error) {
func JWTAuth(cfg *config.JWTConfig) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
// Also allow token via query param for file downloads
if authHeader == "" {
if token := c.Query("token"); token != "" {
authHeader = "Bearer " + token
}
}
if authHeader == "" {
c.JSON(http.StatusUnauthorized, models.Response{
Code: 401,
+36
View File
@@ -50,6 +50,8 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
admin.POST("/users", authHandler.CreateUser)
admin.PUT("/users/:id", authHandler.UpdateUser)
admin.DELETE("/users/:id", authHandler.DeleteUser)
admin.POST("/users/batch-import", authHandler.BatchImportUsers)
admin.GET("/users/import-template", authHandler.ImportTemplate)
}
// LDAP 配置路由
@@ -62,6 +64,10 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
ldapGroup.POST("/test", ldapHandler.TestLDAPConnection)
ldapGroup.POST("/users", ldapHandler.ListLDAPUsers)
ldapGroup.POST("/import", ldapHandler.BatchImportLDAP)
ldapGroup.POST("/ou-list", ldapHandler.ListLDAPOUs)
ldapGroup.POST("/create-ou", ldapHandler.CreateLDAPOU)
ldapGroup.POST("/create-user", ldapHandler.CreateLDAPUser)
ldapGroup.POST("/delete-user", ldapHandler.DeleteLDAPUser)
}
// 仪表盘路由
@@ -107,6 +113,36 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
domains.DELETE("/:id", domainHandler.DeleteDomain)
}
// 云平台路由
platformHandler := handlers.NewPlatformHandler(db)
platforms := authorized.Group("/platforms")
{
platforms.GET("", platformHandler.ListPlatforms)
platforms.POST("", platformHandler.CreatePlatform)
platforms.PUT("/:id", platformHandler.UpdatePlatform)
platforms.DELETE("/:id", platformHandler.DeletePlatform)
}
// 区域路由
regionHandler := handlers.NewRegionHandler(db)
regions := authorized.Group("/regions")
{
regions.GET("", regionHandler.ListRegions)
regions.POST("", regionHandler.CreateRegion)
regions.PUT("/:id", regionHandler.UpdateRegion)
regions.DELETE("/:id", regionHandler.DeleteRegion)
}
// 资源模型路由
resourceModelHandler := handlers.NewResourceModelHandler(db)
resourceModels := authorized.Group("/resource-models")
{
resourceModels.GET("", resourceModelHandler.ListResourceModels)
resourceModels.POST("", resourceModelHandler.CreateResourceModel)
resourceModels.PUT("/:id", resourceModelHandler.UpdateResourceModel)
resourceModels.DELETE("/:id", resourceModelHandler.DeleteResourceModel)
}
// K8s集群路由
k8sHandler := handlers.NewK8sHandler(db)
clusters := authorized.Group("/k8s-clusters")