mirror of
https://github.com/veops/oneterm.git
synced 2026-09-02 22:56:21 +08:00
refactor(backend): simplify asset partial update implementation
This commit is contained in:
@@ -19,10 +19,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
kFmtAssetIds = "assetIds-%d"
|
|
||||||
kAuthorizationIds = "authorizationIds"
|
kAuthorizationIds = "authorizationIds"
|
||||||
kNodeIds = "nodeIds"
|
|
||||||
kAccountIds = "accountIds"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -126,7 +123,7 @@ func (c *Controller) GetAssets(ctx *gin.Context) {
|
|||||||
if info {
|
if info {
|
||||||
db = db.Select("id", "parent_id", "name", "ip", "protocols",
|
db = db.Select("id", "parent_id", "name", "ip", "protocols",
|
||||||
"connectable", "authorization", "resource_id", "access_time_control",
|
"connectable", "authorization", "resource_id", "access_time_control",
|
||||||
"asset_command_control", "web_config", "gateway_id")
|
"asset_command_control", "web_config", "gateway_id", "ci_id", "ci_type_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
doGet(ctx, false, db, config.RESOURCE_ASSET, assetPostHooks...)
|
doGet(ctx, false, db, config.RESOURCE_ASSET, assetPostHooks...)
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
@@ -245,10 +248,26 @@ func doUpdate[T model.Model](ctx *gin.Context, needAcl bool, md T, resourceType
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read request body for partial update field detection
|
||||||
|
bodyBytes, err := io.ReadAll(ctx.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Restore body for binding
|
||||||
|
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||||
|
|
||||||
|
// Parse body to extract field keys for partial update
|
||||||
|
var bodyFields map[string]any
|
||||||
|
if len(bodyBytes) > 0 {
|
||||||
|
json.Unmarshal(bodyBytes, &bodyFields)
|
||||||
|
}
|
||||||
|
|
||||||
if err = ctx.ShouldBindBodyWithJSON(md); err != nil {
|
if err = ctx.ShouldBindBodyWithJSON(md); err != nil {
|
||||||
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
md.SetUpdaterId(currentUser.Uid)
|
md.SetUpdaterId(currentUser.Uid)
|
||||||
|
|
||||||
for _, hook := range preHooks {
|
for _, hook := range preHooks {
|
||||||
@@ -289,27 +308,55 @@ func doUpdate[T model.Model](ctx *gin.Context, needAcl bool, md T, resourceType
|
|||||||
|
|
||||||
if err = baseService.ExecuteInTransaction(ctx, func(tx *gorm.DB) (err error) {
|
if err = baseService.ExecuteInTransaction(ctx, func(tx *gorm.DB) (err error) {
|
||||||
omits := []string{"resource_id", "created_at", "deleted_at"}
|
omits := []string{"resource_id", "created_at", "deleted_at"}
|
||||||
selects := []string{"*"}
|
var selects []string
|
||||||
switch t := any(md).(type) {
|
|
||||||
case *model.Asset:
|
// Build dynamic selects based on fields present in request body
|
||||||
if err = service.DefaultAuthService.HandleAuthorization(ctx, tx, model.ACTION_UPDATE, t, nil); err != nil {
|
if len(bodyFields) > 0 {
|
||||||
handleRemoteErr(ctx, err)
|
// Define allowed fields per model type
|
||||||
return
|
var allowedFields []string
|
||||||
|
|
||||||
|
switch t := any(md).(type) {
|
||||||
|
case *model.Asset:
|
||||||
|
if err = service.DefaultAuthService.HandleAuthorization(ctx, tx, model.ACTION_UPDATE, t, nil); err != nil {
|
||||||
|
handleRemoteErr(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Define allowed fields based on auth type
|
||||||
|
if cast.ToBool(ctx.Value("isAuthWithKey")) {
|
||||||
|
// API Key auth: Only sync-related fields
|
||||||
|
allowedFields = []string{"ip", "protocols", "authorization", "parent_id", "comment", "name", "ci_id", "ci_type_id"}
|
||||||
|
} else {
|
||||||
|
// Normal auth: All asset fields
|
||||||
|
allowedFields = []string{"name", "ip", "gateway_id", "protocols", "authorization", "parent_id",
|
||||||
|
"comment", "connectable", "access_time_control", "asset_command_control", "web_config", "ci_id", "ci_type_id"}
|
||||||
|
}
|
||||||
|
case *model.Node:
|
||||||
|
if err = handleNodeAuthorization(ctx, tx, model.ACTION_UPDATE, t); err != nil {
|
||||||
|
handleRemoteErr(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case *model.Account:
|
||||||
|
// For accounts, allow selective field updates for sensitive data
|
||||||
|
allowedFields = []string{"name", "account", "account_type", "password", "pk", "phrase"}
|
||||||
}
|
}
|
||||||
if cast.ToBool(ctx.Value("isAuthWithKey")) {
|
|
||||||
selects = []string{"ip", "protocols", "authorization"}
|
// Build selects list based on which fields are present in body
|
||||||
}
|
if len(allowedFields) > 0 {
|
||||||
case *model.Node:
|
for _, field := range allowedFields {
|
||||||
if err = handleNodeAuthorization(ctx, tx, model.ACTION_UPDATE, t); err != nil {
|
if _, present := bodyFields[field]; present {
|
||||||
handleRemoteErr(ctx, err)
|
selects = append(selects, field)
|
||||||
return
|
}
|
||||||
}
|
}
|
||||||
case *model.Account:
|
// Always update these fields
|
||||||
if cast.ToBool(ctx.Value("isAuthWithKey")) {
|
selects = append(selects, "updated_at", "updater_id")
|
||||||
selects = []string{"account", "password", "phrase", "pk", "account_type"}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no fields specified or no allowed fields matched, update all fields
|
||||||
|
if len(selects) == 0 {
|
||||||
|
selects = []string{"*"}
|
||||||
|
}
|
||||||
|
|
||||||
if err = tx.Select(selects).Omit(omits...).Save(md).Error; err != nil {
|
if err = tx.Select(selects).Omit(omits...).Save(md).Error; err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ type Asset struct {
|
|||||||
ParentId int `json:"parent_id" gorm:"column:parent_id"`
|
ParentId int `json:"parent_id" gorm:"column:parent_id"`
|
||||||
Ip string `json:"ip" gorm:"column:ip"`
|
Ip string `json:"ip" gorm:"column:ip"`
|
||||||
Protocols Slice[string] `json:"protocols" gorm:"column:protocols;type:text"`
|
Protocols Slice[string] `json:"protocols" gorm:"column:protocols;type:text"`
|
||||||
|
CIId int `json:"ci_id" gorm:"column:ci_id;index"`
|
||||||
|
CITypeId int `json:"ci_type_id" gorm:"column:ci_type_id"`
|
||||||
GatewayId int `json:"gateway_id" gorm:"column:gateway_id"`
|
GatewayId int `json:"gateway_id" gorm:"column:gateway_id"`
|
||||||
Authorization AuthorizationMap `json:"authorization" gorm:"column:authorization;type:text"`
|
Authorization AuthorizationMap `json:"authorization" gorm:"column:authorization;type:text"`
|
||||||
AccessAuth AccessAuth `json:"access_auth" gorm:"embedded;column:access_auth"` // Deprecated: Use V2 fields below
|
AccessAuth AccessAuth `json:"access_auth" gorm:"embedded;column:access_auth"` // Deprecated: Use V2 fields below
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ func (r *assetRepository) BuildQuery(ctx *gin.Context) (*gorm.DB, error) {
|
|||||||
func(s string, _ int) int { return cast.ToInt(s) }))
|
func(s string, _ int) int { return cast.ToInt(s) }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by CMDB CI ID
|
||||||
|
db = dbpkg.FilterEqual(ctx, db, "ci_id")
|
||||||
|
|
||||||
|
// Filter by CMDB CI Type ID
|
||||||
|
db = dbpkg.FilterEqual(ctx, db, "ci_type_id")
|
||||||
|
|
||||||
// Sort by name
|
// Sort by name
|
||||||
db = db.Order("name")
|
db = db.Order("name")
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/veops/oneterm/internal/model"
|
"github.com/veops/oneterm/internal/model"
|
||||||
"github.com/veops/oneterm/internal/repository"
|
"github.com/veops/oneterm/internal/repository"
|
||||||
"github.com/veops/oneterm/internal/schedule"
|
"github.com/veops/oneterm/internal/schedule"
|
||||||
|
"github.com/veops/oneterm/internal/sshsrv/icons"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,7 +34,22 @@ func (s *AssetService) GetById(ctx context.Context, id int) (*model.Asset, error
|
|||||||
// PreprocessAssetData preprocesses asset data before saving
|
// PreprocessAssetData preprocesses asset data before saving
|
||||||
func (s *AssetService) PreprocessAssetData(asset *model.Asset) {
|
func (s *AssetService) PreprocessAssetData(asset *model.Asset) {
|
||||||
asset.Ip = strings.TrimSpace(asset.Ip)
|
asset.Ip = strings.TrimSpace(asset.Ip)
|
||||||
asset.Protocols = lo.Map(asset.Protocols, func(s string, _ int) string { return strings.TrimSpace(s) })
|
|
||||||
|
// Normalize protocols: add default ports if not specified
|
||||||
|
asset.Protocols = lo.Map(asset.Protocols, func(p string, _ int) string {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
// If protocol already has port (contains ':'), keep as-is
|
||||||
|
if strings.Contains(p, ":") {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
// Otherwise, add default port
|
||||||
|
if defaultPort := icons.GetDefaultPort(p); defaultPort != "" {
|
||||||
|
return p + ":" + defaultPort
|
||||||
|
}
|
||||||
|
// If no default port available, keep as-is
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
|
||||||
if asset.Authorization == nil {
|
if asset.Authorization == nil {
|
||||||
asset.Authorization = make(model.AuthorizationMap)
|
asset.Authorization = make(model.AuthorizationMap)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ func GetDefaultPort(protocol string) string {
|
|||||||
switch protocol {
|
switch protocol {
|
||||||
case "ssh":
|
case "ssh":
|
||||||
return "22"
|
return "22"
|
||||||
|
case "rdp":
|
||||||
|
return "3389"
|
||||||
|
case "vnc":
|
||||||
|
return "5900"
|
||||||
case "mysql":
|
case "mysql":
|
||||||
return "3306"
|
return "3306"
|
||||||
case "redis":
|
case "redis":
|
||||||
|
|||||||
Reference in New Issue
Block a user