feat(delivery): refine base service integrations
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1024XEngineer/xinfra/server/internal/config"
|
||||
"github.com/1024XEngineer/xinfra/server/internal/model"
|
||||
"github.com/1024XEngineer/xinfra/server/internal/service"
|
||||
|
||||
@@ -15,8 +22,10 @@ import (
|
||||
)
|
||||
|
||||
type BusinessLineHandler struct {
|
||||
db *gorm.DB
|
||||
wayne *service.WayneRoleBindingService
|
||||
cfg config.Config
|
||||
db *gorm.DB
|
||||
wayne *service.WayneRoleBindingService
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type BusinessLineWithPermission struct {
|
||||
@@ -47,8 +56,24 @@ type WayneNamespaceBindingItem struct {
|
||||
KubeNamespace string `json:"kubeNamespace"`
|
||||
}
|
||||
|
||||
func NewBusinessLineHandler(db *gorm.DB, wayne *service.WayneRoleBindingService) *BusinessLineHandler {
|
||||
return &BusinessLineHandler{db: db, wayne: wayne}
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) ListCurrentUserBusinessLines(c *gin.Context) {
|
||||
@@ -405,6 +430,250 @@ func (h *BusinessLineHandler) ReplaceWayneNamespaces(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
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 ""
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) canManageBusinessLine(c *gin.Context, businessLineID uint64) bool {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
|
||||
@@ -171,7 +171,7 @@ func (h *DeliveryHandler) RevealCredentials(c *gin.Context) {
|
||||
}
|
||||
items, err := h.service.RevealDeploymentCredentials(c.Request.Context(), c.Param("id"), claims.UserID, claims.IsAdmin)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "credentials not found or already viewed"})
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "credentials not found or already claimed"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
@@ -357,7 +357,7 @@ func (h *DeliveryHandler) TargetHostMountPaths(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid target_id"})
|
||||
return
|
||||
}
|
||||
items, err := h.service.ListHostMountPaths(c.Request.Context(), targetID, c.Param("host"))
|
||||
items, err := h.service.ListHostMountPaths(c.Request.Context(), targetID, c.Param("host"), c.Query("prefix"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
@@ -18,7 +18,17 @@ func NewMachineHandler(machines *service.MachineService) *MachineHandler {
|
||||
}
|
||||
|
||||
func (h *MachineHandler) Overview(c *gin.Context) {
|
||||
overview, err := h.machines.Overview(c.Request.Context())
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return
|
||||
}
|
||||
businessLineID, ok := queryUint(c, "business_line_id")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "business_line_id is required"})
|
||||
return
|
||||
}
|
||||
overview, err := h.machines.Overview(c.Request.Context(), claims.UserID, claims.IsAdmin, businessLineID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -27,18 +37,28 @@ func (h *MachineHandler) Overview(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *MachineHandler) List(c *gin.Context) {
|
||||
resources, err := h.machines.List(c.Request.Context(), service.MachineListQuery{
|
||||
Page: queryInt(c, "page", 1),
|
||||
Size: queryInt(c, "size", 20),
|
||||
Hostname: c.Query("hostname"),
|
||||
AssetNumber: c.Query("assetNumber"),
|
||||
Type: c.Query("type"),
|
||||
Location: c.Query("location"),
|
||||
IP: c.Query("ip"),
|
||||
Spec: c.Query("spec"),
|
||||
BusinessLine: c.Query("businessLine"),
|
||||
Source: c.Query("source"),
|
||||
Status: c.Query("status"),
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return
|
||||
}
|
||||
businessLineID, ok := queryUint(c, "business_line_id")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "business_line_id is required"})
|
||||
return
|
||||
}
|
||||
resources, err := h.machines.List(c.Request.Context(), claims.UserID, claims.IsAdmin, service.MachineListQuery{
|
||||
BusinessLineID: businessLineID,
|
||||
Page: queryInt(c, "page", 1),
|
||||
Size: queryInt(c, "size", 20),
|
||||
Hostname: c.Query("hostname"),
|
||||
AssetNumber: c.Query("assetNumber"),
|
||||
Type: c.Query("type"),
|
||||
Location: c.Query("location"),
|
||||
IP: c.Query("ip"),
|
||||
Spec: c.Query("spec"),
|
||||
Source: c.Query("source"),
|
||||
Status: c.Query("status"),
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
@@ -67,3 +87,15 @@ func queryInt(c *gin.Context, key string, fallback int) int {
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func queryUint(c *gin.Context, key string) (uint64, bool) {
|
||||
value := c.Query(key)
|
||||
if value == "" {
|
||||
return 0, false
|
||||
}
|
||||
n, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil || n == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return n, true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user