feat(delivery): sync mysql service ledger and credentials

This commit is contained in:
mac
2026-07-30 16:09:13 +08:00
parent f41abfd609
commit 1aa984c7e5
15 changed files with 1322 additions and 262 deletions
+169
View File
@@ -2,15 +2,20 @@ package handler
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/1024XEngineer/xinfra/server/internal/config"
"github.com/1024XEngineer/xinfra/server/internal/service"
gmsm2 "github.com/tjfoc/gmsm/sm2"
gmx509 "github.com/tjfoc/gmsm/x509"
"github.com/gin-gonic/gin"
)
@@ -38,6 +43,22 @@ func (h *CloudDMHandler) Login(c *gin.Context) {
return
}
if claims.IsAdmin {
targetURL, err := h.adminLogin(c, targetURL)
if err != nil {
h.writeAudit(c, claims.UserID, claims.Username, "deny", err.Error())
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
}
h.writeAudit(c, claims.UserID, claims.Username, "allow", "platform admin uses clouddm admin account")
if strings.Contains(c.GetHeader("Accept"), "application/json") {
c.JSON(http.StatusOK, gin.H{"target_url": targetURL})
return
}
c.Redirect(http.StatusFound, targetURL)
return
}
jumpURL, err := h.loginJumpURL(targetURL)
if err != nil {
h.writeAudit(c, claims.UserID, claims.Username, "deny", err.Error())
@@ -97,6 +118,154 @@ func (h *CloudDMHandler) loginJumpURL(targetURL string) (string, error) {
return result.Data, nil
}
func (h *CloudDMHandler) adminLogin(c *gin.Context, targetURL string) (string, error) {
username := strings.TrimSpace(h.cfg.CloudDMAdminUsername)
password := h.cfg.CloudDMAdminPassword
if username == "" || password == "" {
return "", fmt.Errorf("clouddm admin account is not configured")
}
publicKey, err := h.fetchPublicKey(targetURL)
if err != nil {
return "", err
}
encryptedPassword, err := encryptCloudDMPassword(publicKey, password)
if err != nil {
return "", err
}
loginURL := strings.TrimSpace(h.cfg.CloudDMLoginURL)
if loginURL == "" {
loginURL = strings.TrimRight(targetURL, "/") + "/login"
}
body, _ := json.Marshal(gin.H{
"accountType": "SUB_ACCOUNT",
"loginType": "PASSWORD",
"account": username,
"password": encryptedPassword,
})
req, err := http.NewRequest(http.MethodPost, loginURL, bytes.NewReader(body))
if err != nil {
return "", err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 8192))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("clouddm admin login failed: status %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
}
var result struct {
Success bool `json:"success"`
Data struct {
NeedMore bool `json:"needMore"`
NeedMfa bool `json:"needMfa"`
} `json:"data"`
Msg string `json:"msg"`
MsgContent string `json:"msgContent"`
}
if err := json.Unmarshal(raw, &result); err != nil {
return "", err
}
if !result.Success || result.Data.NeedMore || result.Data.NeedMfa {
reason := strings.TrimSpace(result.MsgContent)
if reason == "" {
reason = strings.TrimSpace(result.Msg)
}
if reason == "" {
reason = "admin login did not complete"
}
return "", fmt.Errorf("clouddm admin login failed: %s", reason)
}
for _, cookie := range resp.Header.Values("Set-Cookie") {
c.Writer.Header().Add("Set-Cookie", cookie)
}
return h.publicSQLURL(), nil
}
func (h *CloudDMHandler) fetchPublicKey(targetURL string) (string, error) {
requestURL := strings.TrimRight(targetURL, "/") + "/api/entry/dmGlobalSettings"
req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader([]byte("{}")))
if err != nil {
return "", err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 8192))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("clouddm dmGlobalSettings failed: status %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
}
var result struct {
Success bool `json:"success"`
Data struct {
PublicKey string `json:"publicKey"`
} `json:"data"`
Msg string `json:"msg"`
MsgContent string `json:"msgContent"`
}
if err := json.Unmarshal(raw, &result); err != nil {
return "", err
}
if !result.Success || strings.TrimSpace(result.Data.PublicKey) == "" {
reason := strings.TrimSpace(result.MsgContent)
if reason == "" {
reason = strings.TrimSpace(result.Msg)
}
if reason == "" {
reason = "empty public key"
}
return "", fmt.Errorf("clouddm dmGlobalSettings failed: %s", reason)
}
return result.Data.PublicKey, nil
}
func encryptCloudDMPassword(publicKey, password string) (string, error) {
pub, err := gmx509.ReadPublicKeyFromHex(strings.TrimSpace(publicKey))
if err != nil {
return "", err
}
ciphertext, err := gmsm2.Encrypt(pub, []byte(password), rand.Reader, gmsm2.C1C3C2)
if err != nil {
return "", err
}
return hex.EncodeToString(ciphertext), nil
}
func (h *CloudDMHandler) publicSQLURL() string {
base := strings.TrimSpace(h.cfg.CloudDMPublicURL)
if base == "" {
if redirectURI, err := url.Parse(strings.TrimSpace(h.cfg.CloudDMRedirectURI)); err == nil && redirectURI.Scheme != "" && redirectURI.Host != "" {
base = redirectURI.Scheme + "://" + redirectURI.Host
}
}
if base == "" {
base = strings.TrimSpace(h.cfg.CloudDMTargetURL)
}
base = strings.TrimRight(base, "/")
if strings.Contains(base, "#") {
return base
}
return base + "/#/sql"
}
func (h *CloudDMHandler) writeAudit(c *gin.Context, userID uint64, username, decision, reason string) {
h.audit.Write(service.AuditEntry{
ActorUserID: userID,
+23
View File
@@ -163,6 +163,29 @@ func (h *DeliveryHandler) MySQLServiceLedger(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"items": items})
}
func (h *DeliveryHandler) SyncMySQLServiceLedger(c *gin.Context) {
claims, ok := CurrentClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
return
}
businessLineID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || businessLineID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid business line id"})
return
}
items, err := h.service.SyncMySQLInstanceStatuses(c.Request.Context(), claims.UserID, claims.IsAdmin, businessLineID)
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"items": items})
}
func (h *DeliveryHandler) RevealCredentials(c *gin.Context) {
claims, ok := CurrentClaims(c)
if !ok {