feat: K8s集群拓扑图接入真实后端数据 — 分层可视化Cluster→Ingress→Service→Deployment→Node + http.ts类型修复
This commit is contained in:
1 parent
4b094f4645
commit
9f65faff6f
17 files changed
+3021
-1205
No files matched your search
@@ -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: "删除成功"})
|
||||
}
|
||||
@@ -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(®ions)
|
||||
|
||||
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(®ion).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(®ion, 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(®ion).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: "删除成功"})
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ResourceModel 资源模型 (CMDB 模型定义)
|
||||
type ResourceModel struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Name string `gorm:"size:64;not null" json:"name"`
|
||||
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
|
||||
Description string `gorm:"size:500" json:"description"`
|
||||
Type string `gorm:"size:32;default:normal" json:"type"` // normal, parent
|
||||
Attributes string `gorm:"type:text" json:"attributes"` // JSON 数组
|
||||
Icon string `gorm:"size:64" json:"icon"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
func (ResourceModel) TableName() string {
|
||||
return "resource_models"
|
||||
}
|
||||
@@ -88,5 +88,6 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
&models.NavigationLink{},
|
||||
&models.AuditLog{},
|
||||
&models.LDAPConfig{},
|
||||
&models.ResourceModel{},
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user