2026-07-17 15:02:18 +08:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
2026-07-29 16:36:27 +08:00
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
2026-07-17 15:02:18 +08:00
|
|
|
"errors"
|
2026-07-29 16:36:27 +08:00
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2026-07-17 15:02:18 +08:00
|
|
|
"net/http"
|
2026-07-29 16:36:27 +08:00
|
|
|
"net/url"
|
2026-07-17 15:02:18 +08:00
|
|
|
"strconv"
|
2026-07-17 19:26:40 +08:00
|
|
|
"strings"
|
2026-07-17 15:02:18 +08:00
|
|
|
"time"
|
|
|
|
|
|
2026-07-29 16:36:27 +08:00
|
|
|
"github.com/1024XEngineer/xinfra/server/internal/config"
|
2026-07-17 15:02:18 +08:00
|
|
|
"github.com/1024XEngineer/xinfra/server/internal/model"
|
2026-07-17 19:26:40 +08:00
|
|
|
"github.com/1024XEngineer/xinfra/server/internal/service"
|
2026-07-17 15:02:18 +08:00
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type BusinessLineHandler struct {
|
2026-07-29 16:36:27 +08:00
|
|
|
cfg config.Config
|
|
|
|
|
db *gorm.DB
|
|
|
|
|
wayne *service.WayneRoleBindingService
|
|
|
|
|
httpClient *http.Client
|
2026-07-17 15:02:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type BusinessLineWithPermission struct {
|
|
|
|
|
ID uint64 `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
CreatedAt string `json:"created_at"`
|
|
|
|
|
UpdatedAt string `json:"updated_at"`
|
|
|
|
|
Permission int `json:"permission"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type GrantBusinessLinePermissionRequest struct {
|
|
|
|
|
BusinessLineID uint64 `json:"business_line_id" binding:"required"`
|
|
|
|
|
TargetUserID uint64 `json:"target_user_id" binding:"required"`
|
|
|
|
|
TargetBusinessLineID uint64 `json:"target_business_line_id" binding:"required"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type BusinessLinePayload struct {
|
|
|
|
|
Name string `json:"name" binding:"required"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type WayneNamespaceBindingPayload struct {
|
|
|
|
|
Namespaces []WayneNamespaceBindingItem `json:"namespaces"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type WayneNamespaceBindingItem struct {
|
|
|
|
|
ID uint64 `json:"id" binding:"required"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
KubeNamespace string `json:"kubeNamespace"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-29 16:36:27 +08:00
|
|
|
type SinaOrganizationBindingPayload struct {
|
|
|
|
|
Organizations []SinaOrganizationBindingItem `json:"organizations"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type SinaOrganizationBindingItem struct {
|
|
|
|
|
ID string `json:"id" binding:"required"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewBusinessLineHandler(cfg config.Config, db *gorm.DB, wayne *service.WayneRoleBindingService) *BusinessLineHandler {
|
|
|
|
|
return &BusinessLineHandler{
|
|
|
|
|
cfg: cfg,
|
|
|
|
|
db: db,
|
|
|
|
|
wayne: wayne,
|
|
|
|
|
httpClient: &http.Client{
|
|
|
|
|
Timeout: 10 * time.Second,
|
|
|
|
|
},
|
|
|
|
|
}
|
2026-07-17 15:02:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *BusinessLineHandler) ListCurrentUserBusinessLines(c *gin.Context) {
|
|
|
|
|
claims, ok := CurrentClaims(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if claims.IsAdmin {
|
|
|
|
|
var rows []model.BusinessLine
|
|
|
|
|
if err := h.db.Order("id ASC").Find(&rows).Error; err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items := make([]BusinessLineWithPermission, 0, len(rows))
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
items = append(items, BusinessLineWithPermission{
|
|
|
|
|
ID: row.ID,
|
|
|
|
|
Name: row.Name,
|
|
|
|
|
CreatedAt: row.CreatedAt.Format(time.RFC3339),
|
|
|
|
|
UpdatedAt: row.UpdatedAt.Format(time.RFC3339),
|
|
|
|
|
Permission: 0,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var rows []struct {
|
|
|
|
|
ID uint64
|
|
|
|
|
Name string
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
UpdatedAt time.Time
|
|
|
|
|
Permission int
|
|
|
|
|
}
|
|
|
|
|
if err := h.db.
|
|
|
|
|
Table("business_line_users").
|
|
|
|
|
Select("business_lines.id, business_lines.name, business_lines.created_at, business_lines.updated_at, business_line_users.permission").
|
|
|
|
|
Joins("JOIN business_lines ON business_lines.id = business_line_users.business_line_id").
|
|
|
|
|
Where("business_line_users.user_id = ?", claims.UserID).
|
|
|
|
|
Order("business_lines.id ASC").
|
|
|
|
|
Scan(&rows).Error; err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items := make([]BusinessLineWithPermission, 0, len(rows))
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
items = append(items, BusinessLineWithPermission{
|
|
|
|
|
ID: row.ID,
|
|
|
|
|
Name: row.Name,
|
|
|
|
|
CreatedAt: row.CreatedAt.Format(time.RFC3339),
|
|
|
|
|
UpdatedAt: row.UpdatedAt.Format(time.RFC3339),
|
|
|
|
|
Permission: row.Permission,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *BusinessLineHandler) ListAll(c *gin.Context) {
|
|
|
|
|
if !requirePlatformAdmin(c) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var rows []model.BusinessLine
|
|
|
|
|
if err := h.db.Order("id ASC").Find(&rows).Error; err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items := make([]gin.H, 0, len(rows))
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
items = append(items, gin.H{
|
|
|
|
|
"id": row.ID,
|
|
|
|
|
"name": row.Name,
|
|
|
|
|
"created_at": row.CreatedAt.Format(time.RFC3339),
|
|
|
|
|
"updated_at": row.UpdatedAt.Format(time.RFC3339),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *BusinessLineHandler) Create(c *gin.Context) {
|
|
|
|
|
if !requirePlatformAdmin(c) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req BusinessLinePayload
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-22 14:59:54 +08:00
|
|
|
req.Name = strings.TrimSpace(req.Name)
|
|
|
|
|
if req.Name == "" {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "business line name is required"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var existing model.BusinessLine
|
|
|
|
|
if err := h.db.Where("name = ?", req.Name).First(&existing).Error; err == nil {
|
|
|
|
|
c.JSON(http.StatusConflict, gin.H{"error": "business line already exists"})
|
|
|
|
|
return
|
|
|
|
|
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if h.wayne == nil {
|
|
|
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "wayne native api is not configured"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
namespace, err := h.wayne.EnsureNamespace(c.Request.Context(), req.Name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeWayneRoleBindingError(c, nil, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if namespace == nil || namespace.ID == 0 {
|
|
|
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": "wayne namespace response is invalid"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(namespace.Name) == "" {
|
|
|
|
|
namespace.Name = req.Name
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(namespace.KubeNamespace) == "" {
|
|
|
|
|
namespace.KubeNamespace = req.Name
|
|
|
|
|
}
|
2026-07-17 15:02:18 +08:00
|
|
|
|
|
|
|
|
item := model.BusinessLine{Name: req.Name}
|
2026-07-22 14:59:54 +08:00
|
|
|
if err := h.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
if err := tx.Create(&item).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return tx.Create(&model.BusinessLineWayneNamespace{
|
|
|
|
|
BusinessLineID: item.ID,
|
|
|
|
|
WayneNamespaceID: namespace.ID,
|
|
|
|
|
WayneNamespaceName: namespace.Name,
|
|
|
|
|
KubeNamespace: namespace.KubeNamespace,
|
|
|
|
|
}).Error
|
|
|
|
|
}); err != nil {
|
2026-07-17 15:02:18 +08:00
|
|
|
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeBusinessLine(c, item)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *BusinessLineHandler) Update(c *gin.Context) {
|
|
|
|
|
if !requirePlatformAdmin(c) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req BusinessLinePayload
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var item model.BusinessLine
|
|
|
|
|
if err := h.db.First(&item, "id = ?", c.Param("id")).Error; err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := h.db.Model(&item).Update("name", req.Name).Error; err != nil {
|
|
|
|
|
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
item.Name = req.Name
|
|
|
|
|
writeBusinessLine(c, item)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *BusinessLineHandler) Delete(c *gin.Context) {
|
|
|
|
|
if !requirePlatformAdmin(c) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var item model.BusinessLine
|
|
|
|
|
if err := h.db.First(&item, "id = ?", c.Param("id")).Error; err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := h.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
if err := tx.Where("business_line_id = ?", item.ID).Delete(&model.BusinessLineUser{}).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err := tx.Where("business_line_id = ?", item.ID).Delete(&model.BusinessLineWayneNamespace{}).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return tx.Delete(&item).Error
|
|
|
|
|
}); err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *BusinessLineHandler) GrantPermission(c *gin.Context) {
|
|
|
|
|
claims, ok := CurrentClaims(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req GrantBusinessLinePermissionRequest
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !claims.IsAdmin {
|
|
|
|
|
var currentBinding model.BusinessLineUser
|
|
|
|
|
if err := h.db.Where("business_line_id = ? AND user_id = ? AND permission = ?", req.BusinessLineID, claims.UserID, 0).
|
|
|
|
|
First(¤tBinding).Error; err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "current user is not platform admin or business line admin"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var businessLine model.BusinessLine
|
|
|
|
|
if err := h.db.First(&businessLine, "id = ?", req.BusinessLineID).Error; err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var targetUser model.User
|
|
|
|
|
if err := h.db.Where("id = ? AND deleted_at IS NULL", req.TargetUserID).First(&targetUser).Error; err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "target user not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var targetBusinessLine model.BusinessLine
|
|
|
|
|
if err := h.db.First(&targetBusinessLine, "id = ?", req.TargetBusinessLineID).Error; err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "target business line not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var binding model.BusinessLineUser
|
2026-07-17 19:26:40 +08:00
|
|
|
created := false
|
2026-07-17 15:02:18 +08:00
|
|
|
err := h.db.Where("business_line_id = ? AND user_id = ?", req.TargetBusinessLineID, req.TargetUserID).First(&binding).Error
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
binding = model.BusinessLineUser{
|
|
|
|
|
BusinessLineID: req.TargetBusinessLineID,
|
|
|
|
|
UserID: req.TargetUserID,
|
2026-07-21 17:05:48 +08:00
|
|
|
Permission: 1,
|
2026-07-17 15:02:18 +08:00
|
|
|
}
|
|
|
|
|
if err := h.db.Create(&binding).Error; err != nil {
|
|
|
|
|
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-17 19:26:40 +08:00
|
|
|
created = true
|
2026-07-17 15:02:18 +08:00
|
|
|
} else if err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
2026-07-17 19:26:40 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-17 15:02:18 +08:00
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
"id": binding.ID,
|
|
|
|
|
"business_line_id": binding.BusinessLineID,
|
|
|
|
|
"user_id": binding.UserID,
|
|
|
|
|
"permission": binding.Permission,
|
|
|
|
|
"created_at": binding.CreatedAt.Format(time.RFC3339),
|
|
|
|
|
"updated_at": binding.UpdatedAt.Format(time.RFC3339),
|
2026-07-21 17:05:48 +08:00
|
|
|
"created": created,
|
2026-07-17 15:02:18 +08:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *BusinessLineHandler) ListWayneNamespaces(c *gin.Context) {
|
|
|
|
|
businessLineID, ok := parseBusinessLineID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !h.canManageBusinessLine(c, businessLineID) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var rows []model.BusinessLineWayneNamespace
|
|
|
|
|
if err := h.db.Where("business_line_id = ?", businessLineID).Order("wayne_namespace_id ASC").Find(&rows).Error; err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items := make([]gin.H, 0, len(rows))
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
items = append(items, gin.H{
|
|
|
|
|
"id": row.WayneNamespaceID,
|
|
|
|
|
"name": row.WayneNamespaceName,
|
|
|
|
|
"kubeNamespace": row.KubeNamespace,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *BusinessLineHandler) ReplaceWayneNamespaces(c *gin.Context) {
|
|
|
|
|
businessLineID, ok := parseBusinessLineID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !h.canManageBusinessLine(c, businessLineID) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req WayneNamespaceBindingPayload
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := h.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
if err := tx.Where("business_line_id = ?", businessLineID).Delete(&model.BusinessLineWayneNamespace{}).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
for _, item := range req.Namespaces {
|
|
|
|
|
row := model.BusinessLineWayneNamespace{
|
|
|
|
|
BusinessLineID: businessLineID,
|
|
|
|
|
WayneNamespaceID: item.ID,
|
|
|
|
|
WayneNamespaceName: item.Name,
|
|
|
|
|
KubeNamespace: item.KubeNamespace,
|
|
|
|
|
}
|
|
|
|
|
if err := tx.Create(&row).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}); err != nil {
|
|
|
|
|
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-29 16:36:27 +08:00
|
|
|
func (h *BusinessLineHandler) ListSinaOrganizations(c *gin.Context) {
|
|
|
|
|
businessLineID, ok := parseBusinessLineID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !h.canManageBusinessLine(c, businessLineID) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
token, err := h.loginSina(c.Request.Context())
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
items, err := h.fetchSinaOrganizations(c.Request.Context(), token, c.Query("keyword"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *BusinessLineHandler) ListSinaOrganizationMappings(c *gin.Context) {
|
|
|
|
|
businessLineID, ok := parseBusinessLineID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !h.canManageBusinessLine(c, businessLineID) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var rows []model.BusinessLineSinaOrganization
|
|
|
|
|
if err := h.db.Where("business_line_id = ?", businessLineID).Order("sina_organization_name ASC").Find(&rows).Error; err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
items := make([]gin.H, 0, len(rows))
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
items = append(items, gin.H{
|
|
|
|
|
"id": row.SinaOrganizationID,
|
|
|
|
|
"name": row.SinaOrganizationName,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *BusinessLineHandler) ReplaceSinaOrganizationMappings(c *gin.Context) {
|
|
|
|
|
businessLineID, ok := parseBusinessLineID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !h.canManageBusinessLine(c, businessLineID) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req SinaOrganizationBindingPayload
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := h.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
if err := tx.Where("business_line_id = ?", businessLineID).Delete(&model.BusinessLineSinaOrganization{}).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
seen := map[string]struct{}{}
|
|
|
|
|
for _, item := range req.Organizations {
|
|
|
|
|
id := strings.TrimSpace(item.ID)
|
|
|
|
|
if id == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, ok := seen[id]; ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[id] = struct{}{}
|
|
|
|
|
row := model.BusinessLineSinaOrganization{
|
|
|
|
|
BusinessLineID: businessLineID,
|
|
|
|
|
SinaOrganizationID: id,
|
|
|
|
|
SinaOrganizationName: strings.TrimSpace(item.Name),
|
|
|
|
|
}
|
|
|
|
|
if err := tx.Create(&row).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}); err != nil {
|
|
|
|
|
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type sinaBusinessLoginResp struct {
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
Result map[string]interface{} `json:"result"`
|
|
|
|
|
Data map[string]interface{} `json:"data"`
|
|
|
|
|
Token string `json:"token"`
|
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type sinaOrganizationListResp struct {
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
Result struct {
|
|
|
|
|
Items []map[string]interface{} `json:"items"`
|
|
|
|
|
Count int64 `json:"count"`
|
|
|
|
|
} `json:"result"`
|
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *BusinessLineHandler) loginSina(ctx context.Context) (string, error) {
|
|
|
|
|
username := strings.TrimSpace(h.cfg.SINAUsername)
|
|
|
|
|
password := strings.TrimSpace(h.cfg.SINAPassword)
|
|
|
|
|
if username == "" || password == "" {
|
|
|
|
|
return "", errors.New("SINA_USERNAME or SINA_PASSWORD is not configured")
|
|
|
|
|
}
|
|
|
|
|
payload, err := json.Marshal(map[string]string{
|
|
|
|
|
"username": username,
|
|
|
|
|
"password": password,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.cfg.SINABaseURL+"/sinai/v1/login", bytes.NewReader(payload))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
req.Header.Set("content-type", "application/json")
|
|
|
|
|
|
|
|
|
|
resp, err := h.httpClient.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
|
|
|
return "", fmt.Errorf("sina login failed: status=%d body=%s", resp.StatusCode, string(body))
|
|
|
|
|
}
|
|
|
|
|
var parsed sinaBusinessLoginResp
|
|
|
|
|
if err := json.Unmarshal(body, &parsed); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if !parsed.Success {
|
|
|
|
|
return "", fmt.Errorf("sina login failed: %s", parsed.Message)
|
|
|
|
|
}
|
|
|
|
|
token := firstNonEmptyString(
|
|
|
|
|
parsed.Token,
|
|
|
|
|
sinaStringValue(parsed.Result["token"]),
|
|
|
|
|
sinaStringValue(parsed.Result["access_token"]),
|
|
|
|
|
sinaStringValue(parsed.Data["token"]),
|
|
|
|
|
sinaStringValue(parsed.Data["access_token"]),
|
|
|
|
|
)
|
|
|
|
|
if token == "" {
|
|
|
|
|
return "", errors.New("sina login response missing token")
|
|
|
|
|
}
|
|
|
|
|
return token, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *BusinessLineHandler) fetchSinaOrganizations(ctx context.Context, token string, keyword string) ([]SinaOrganizationBindingItem, error) {
|
|
|
|
|
const size = 100
|
|
|
|
|
page := 1
|
|
|
|
|
items := make([]SinaOrganizationBindingItem, 0)
|
|
|
|
|
for {
|
|
|
|
|
values := url.Values{}
|
|
|
|
|
values.Set("ciClsName", "zion_organization")
|
|
|
|
|
values.Set("keyword", keyword)
|
|
|
|
|
values.Set("page", strconv.Itoa(page))
|
|
|
|
|
values.Set("size", strconv.Itoa(size))
|
|
|
|
|
values.Set("isAccurate", "false")
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, h.cfg.SINABaseURL+"/sinai/v1/ci?"+values.Encode(), nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
req.Header.Set("Authorization", token)
|
|
|
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
|
|
|
|
|
|
resp, err := h.httpClient.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
body, readErr := io.ReadAll(resp.Body)
|
|
|
|
|
_ = resp.Body.Close()
|
|
|
|
|
if readErr != nil {
|
|
|
|
|
return nil, readErr
|
|
|
|
|
}
|
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
|
|
|
return nil, fmt.Errorf("sina organization list failed: status=%d body=%s", resp.StatusCode, string(body))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var parsed sinaOrganizationListResp
|
|
|
|
|
if err := json.Unmarshal(body, &parsed); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if !parsed.Success {
|
|
|
|
|
return nil, fmt.Errorf("sina organization list failed: %s", parsed.Message)
|
|
|
|
|
}
|
|
|
|
|
for _, row := range parsed.Result.Items {
|
|
|
|
|
id := firstNonEmptyString(sinaStringValue(row["id"]), sinaStringValue(row["ciId"]))
|
|
|
|
|
name := firstNonEmptyString(
|
|
|
|
|
sinaStringValue(row["name"]),
|
|
|
|
|
sinaStringValue(row["org_name"]),
|
|
|
|
|
sinaStringValue(row["title"]),
|
|
|
|
|
id,
|
|
|
|
|
)
|
|
|
|
|
if id == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
items = append(items, SinaOrganizationBindingItem{ID: id, Name: name})
|
|
|
|
|
}
|
|
|
|
|
if len(parsed.Result.Items) < size || int64(len(items)) >= parsed.Result.Count {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
page++
|
|
|
|
|
}
|
|
|
|
|
return items, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sinaStringValue(value interface{}) string {
|
|
|
|
|
switch v := value.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
return strings.TrimSpace(v)
|
|
|
|
|
case fmt.Stringer:
|
|
|
|
|
return strings.TrimSpace(v.String())
|
|
|
|
|
case nil:
|
|
|
|
|
return ""
|
|
|
|
|
default:
|
|
|
|
|
return strings.TrimSpace(fmt.Sprint(v))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func firstNonEmptyString(values ...string) string {
|
|
|
|
|
for _, value := range values {
|
|
|
|
|
if strings.TrimSpace(value) != "" {
|
|
|
|
|
return strings.TrimSpace(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 15:02:18 +08:00
|
|
|
func (h *BusinessLineHandler) canManageBusinessLine(c *gin.Context, businessLineID uint64) bool {
|
|
|
|
|
claims, ok := CurrentClaims(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var businessLine model.BusinessLine
|
|
|
|
|
if err := h.db.First(&businessLine, "id = ?", businessLineID).Error; err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if claims.IsAdmin {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var binding model.BusinessLineUser
|
|
|
|
|
if err := h.db.Where("business_line_id = ? AND user_id = ? AND permission = ?", businessLineID, claims.UserID, 0).
|
|
|
|
|
First(&binding).Error; err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "current user is not platform admin or business line admin"})
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 19:26:40 +08:00
|
|
|
func (h *BusinessLineHandler) initializeWayneVisitorForBusinessLine(c *gin.Context, businessLineID uint64, targetUsername string, operatorEmail string, skipWaynePermissionCheck bool) ([]gin.H, bool) {
|
|
|
|
|
var namespaces []model.BusinessLineWayneNamespace
|
|
|
|
|
if err := h.db.Where("business_line_id = ?", businessLineID).Order("wayne_namespace_id ASC").Find(&namespaces).Error; err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
if len(namespaces) == 0 {
|
|
|
|
|
return []gin.H{}, true
|
|
|
|
|
}
|
|
|
|
|
if h.wayne == nil {
|
|
|
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "wayne internal role binding api is not configured"})
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
groupIDs, err := h.wayne.NamespaceVisitorGroupIDs(c.Request.Context())
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeWayneRoleBindingError(c, nil, err)
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
replace := true
|
|
|
|
|
req := service.WayneRoleBindingRequest{
|
|
|
|
|
GroupIDs: groupIDs,
|
|
|
|
|
Replace: &replace,
|
|
|
|
|
RequestID: "business-line-user-init-" + strconv.FormatInt(time.Now().UnixNano(), 10),
|
|
|
|
|
Reason: "初始化业务线 Wayne 访客角色",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items := make([]gin.H, 0, len(namespaces))
|
|
|
|
|
for _, namespace := range namespaces {
|
|
|
|
|
if !skipWaynePermissionCheck {
|
|
|
|
|
permissions, err := h.wayne.NamespaceOperatorPermissionsParsed(c.Request.Context(), namespace.WayneNamespaceID, operatorEmail)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeWayneRoleBindingError(c, nil, err)
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
if !permissions.Create && !permissions.Update {
|
|
|
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "current user does not have Wayne namespace role create or update permission"})
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
result, err := h.wayne.BindNamespace(c.Request.Context(), namespace.WayneNamespaceID, strings.TrimSpace(targetUsername), operatorEmail, req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeWayneRoleBindingError(c, result, err)
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
items = append(items, gin.H{
|
|
|
|
|
"namespace_id": namespace.WayneNamespaceID,
|
|
|
|
|
"namespace_name": namespace.WayneNamespaceName,
|
|
|
|
|
"group_ids": groupIDs,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return items, true
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 15:02:18 +08:00
|
|
|
func parseBusinessLineID(c *gin.Context) (uint64, bool) {
|
|
|
|
|
value, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
|
|
|
if err != nil || value == 0 {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid business line id"})
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
return value, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func requirePlatformAdmin(c *gin.Context) bool {
|
|
|
|
|
claims, ok := CurrentClaims(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if !claims.IsAdmin {
|
|
|
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "platform admin required"})
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeBusinessLine(c *gin.Context, item model.BusinessLine) {
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
"id": item.ID,
|
|
|
|
|
"name": item.Name,
|
|
|
|
|
"created_at": item.CreatedAt.Format(time.RFC3339),
|
|
|
|
|
"updated_at": item.UpdatedAt.Format(time.RFC3339),
|
|
|
|
|
})
|
|
|
|
|
}
|