fix: audit platform contracts and relation identities
This commit is contained in:
@@ -54,7 +54,7 @@ func CreateGasAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, account)
|
||||
infra.Response.Success(ctx, resourceResponse(account))
|
||||
}
|
||||
|
||||
func UpdateGasAccount(ctx *gin.Context) {
|
||||
@@ -95,7 +95,7 @@ func CreateDeliveryAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, account)
|
||||
infra.Response.Success(ctx, resourceResponse(account))
|
||||
}
|
||||
|
||||
func UpdateDeliveryAccount(ctx *gin.Context) {
|
||||
|
||||
@@ -47,7 +47,7 @@ func listPage[T any](ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": list})
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": publicResourceResponse(list)})
|
||||
}
|
||||
|
||||
func getByIdentity[T any](ctx *gin.Context) {
|
||||
@@ -56,7 +56,7 @@ func getByIdentity[T any](ctx *gin.Context) {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, data)
|
||||
infra.Response.Success(ctx, publicResourceResponse(data))
|
||||
}
|
||||
|
||||
func updateAllowedByIdentity(ctx *gin.Context, model any, values map[string]any, allowedFields []string) {
|
||||
|
||||
@@ -261,6 +261,29 @@ func TestResourceResponseDoesNotExposeAutoIncrementRelationIDs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListGasAccountProjectsGasBasicIdentityAndNeverReturnsRelationID(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "gas_account"`)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "gas_account" ORDER BY created_at desc LIMIT $1`)).
|
||||
WithArgs(20).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "gas_basic_id", "username", "display_name", "password_hash", "role_code"}).
|
||||
AddRow(uint64(9), "account-a", nil, nil, "enabled", 1, uint64(7), "operator", "Operator", "hash", "admin"))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "identity" FROM "gas_basic" WHERE id = $1 ORDER BY "gas_basic"."id" LIMIT $2`)).
|
||||
WithArgs(uint64(7), 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"identity"}).AddRow("gas-a"))
|
||||
|
||||
ctx, recorder := updateContext(http.MethodGet, "/gas/gas_account", "", nil)
|
||||
ListGasAccount(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, 0)
|
||||
body := recorder.Body.String()
|
||||
if !strings.Contains(body, `"gas_basic_identity":"gas-a"`) || strings.Contains(body, `"gas_basic_id"`) || strings.Contains(body, `"id":`) {
|
||||
t.Fatalf("account list did not return the public relation shape: %s", body)
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestGetDeliveryTrackOrdersAndMasksPointsWithoutPreciseLocationScope(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
now := time.Now().UTC()
|
||||
|
||||
@@ -35,7 +35,7 @@ func CreateStaffCredential(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, credential)
|
||||
infra.Response.Success(ctx, resourceResponse(credential))
|
||||
}
|
||||
func UpdateStaffCredential(ctx *gin.Context) {
|
||||
var request staffCredentialRequest
|
||||
@@ -77,7 +77,7 @@ func CreateUserAddress(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, address)
|
||||
infra.Response.Success(ctx, resourceResponse(address))
|
||||
}
|
||||
func UpdateUserAddress(ctx *gin.Context) {
|
||||
var request userAddressRequest
|
||||
@@ -133,7 +133,7 @@ func CreateUserServiceRelation(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, relation)
|
||||
infra.Response.Success(ctx, resourceResponse(relation))
|
||||
}
|
||||
func UpdateUserServiceRelation(ctx *gin.Context) {
|
||||
var request userServiceRelationRequest
|
||||
|
||||
@@ -102,7 +102,7 @@ func listResource(ctx *gin.Context, model any) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": resourceResponse(list.Elem().Interface())})
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": publicResourceResponse(list.Elem().Interface())})
|
||||
}
|
||||
|
||||
func getResource(ctx *gin.Context, model any) {
|
||||
@@ -111,7 +111,7 @@ func getResource(ctx *gin.Context, model any) {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, resourceResponse(data.Interface()))
|
||||
infra.Response.Success(ctx, publicResourceResponse(data.Interface()))
|
||||
}
|
||||
|
||||
func createResource(ctx *gin.Context, model any, allowedFields []string, relations []ResourceRelation) {
|
||||
@@ -230,6 +230,113 @@ func resourceResponse(value any) any {
|
||||
return stripInternalIDs(decoded)
|
||||
}
|
||||
|
||||
// publicResourceResponse additionally resolves persisted relation keys into
|
||||
// their public identities. It is used by list/detail endpoints so an edit form
|
||||
// can round-trip the relation without ever receiving a surrogate database ID.
|
||||
func publicResourceResponse(value any) any {
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return value
|
||||
}
|
||||
var decoded any
|
||||
if err := json.Unmarshal(encoded, &decoded); err != nil {
|
||||
return value
|
||||
}
|
||||
return projectRelationIdentities(decoded)
|
||||
}
|
||||
|
||||
var relationIdentityModels = map[string]any{
|
||||
"gas_basic_id": &models.GasBasic{},
|
||||
"gas_station_id": &models.GasBasic{},
|
||||
"delivery_basic_id": &models.DeliveryBasic{},
|
||||
"delivery_point_id": &models.DeliveryBasic{},
|
||||
"user_account_id": &models.UserAccount{},
|
||||
"staff_account_id": &models.StaffAccount{},
|
||||
"smart_cylinder_valve_id": &models.DevSmartCylinderValve{},
|
||||
"ec_category_id": &models.EcCategory{},
|
||||
"ec_product_id": &models.EcProduct{},
|
||||
"ec_order_id": &models.EcOrder{},
|
||||
"delivery_task_id": &models.DeliveryTask{},
|
||||
"delivery_track_id": &models.DeliveryTrack{},
|
||||
"platform_role_id": &models.PlatformRole{},
|
||||
"platform_menu_id": &models.PlatformMenu{},
|
||||
"report_id": &models.Report{},
|
||||
"wallet_id": &models.Wallet{},
|
||||
}
|
||||
|
||||
var relationIdentityKeys = map[string]string{
|
||||
"gas_station_id": "gas_basic_identity",
|
||||
"delivery_point_id": "delivery_basic_identity",
|
||||
}
|
||||
|
||||
func projectRelationIdentities(value any) any {
|
||||
switch data := value.(type) {
|
||||
case map[string]any:
|
||||
for key, item := range data {
|
||||
if key == "id" {
|
||||
delete(data, key)
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(key, "_id") {
|
||||
identityKey := strings.TrimSuffix(key, "_id") + "_identity"
|
||||
if alias := relationIdentityKeys[key]; alias != "" {
|
||||
identityKey = alias
|
||||
}
|
||||
model := relationIdentityModels[key]
|
||||
if key == "subject_id" {
|
||||
model = settlementSubjectModel(data["subject_type"])
|
||||
identityKey = "subject_identity"
|
||||
}
|
||||
if model != nil {
|
||||
data[identityKey] = relationIdentity(item, model)
|
||||
}
|
||||
delete(data, key)
|
||||
continue
|
||||
}
|
||||
data[key] = projectRelationIdentities(item)
|
||||
}
|
||||
case []any:
|
||||
for index := range data {
|
||||
data[index] = projectRelationIdentities(data[index])
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func relationIdentity(value any, model any) string {
|
||||
var id uint64
|
||||
switch raw := value.(type) {
|
||||
case float64:
|
||||
id = uint64(raw)
|
||||
case uint64:
|
||||
id = raw
|
||||
case int:
|
||||
id = uint64(raw)
|
||||
}
|
||||
if id == 0 {
|
||||
return ""
|
||||
}
|
||||
var related struct{ Identity string }
|
||||
if err := impl.DBService.Model(model).Select("identity").Where("id = ?", id).First(&related).Error; err != nil {
|
||||
return ""
|
||||
}
|
||||
return related.Identity
|
||||
}
|
||||
|
||||
func settlementSubjectModel(value any) any {
|
||||
subjectType, _ := value.(string)
|
||||
switch subjectType {
|
||||
case "gas", "gas_basic":
|
||||
return &models.GasBasic{}
|
||||
case "delivery", "delivery_basic":
|
||||
return &models.DeliveryBasic{}
|
||||
case "staff", "staff_account":
|
||||
return &models.StaffAccount{}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func stripInternalIDs(value any) any {
|
||||
switch data := value.(type) {
|
||||
case map[string]any:
|
||||
@@ -311,7 +418,7 @@ func GetEcOrder(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, resourceResponse(gin.H{"order": order, "items": items}))
|
||||
infra.Response.Success(ctx, publicResourceResponse(gin.H{"order": order, "items": items}))
|
||||
}
|
||||
|
||||
// GetDeliveryTrack returns time-ordered points. Precise coordinates are only
|
||||
@@ -337,7 +444,7 @@ func GetDeliveryTrack(ctx *gin.Context) {
|
||||
points[index].Latitude = ""
|
||||
}
|
||||
}
|
||||
infra.Response.Success(ctx, resourceResponse(gin.H{"track": track, "points": points}))
|
||||
infra.Response.Success(ctx, publicResourceResponse(gin.H{"track": track, "points": points}))
|
||||
}
|
||||
|
||||
type ecCategoryView struct {
|
||||
|
||||
Reference in New Issue
Block a user