From 37047919abef5ec425882dbd9b449fd9165c0a87 Mon Sep 17 00:00:00 2001 From: LLLLLLiulei <1065070665@qq.com> Date: Thu, 5 Feb 2026 18:59:30 +0800 Subject: [PATCH] fix: harden import/export flow --- .../internal/handler/admin/account_data.go | 50 +++++++++++++------ .../admin/account_data_handler_test.go | 3 +- backend/internal/handler/admin/proxy_data.go | 27 ++++------ .../handler/admin/proxy_data_handler_test.go | 6 ++- frontend/src/types/index.ts | 4 +- 5 files changed, 53 insertions(+), 37 deletions(-) diff --git a/backend/internal/handler/admin/account_data.go b/backend/internal/handler/admin/account_data.go index 86f4cbae..7bd05074 100644 --- a/backend/internal/handler/admin/account_data.go +++ b/backend/internal/handler/admin/account_data.go @@ -21,8 +21,8 @@ const ( ) type DataPayload struct { - Type string `json:"type"` - Version int `json:"version"` + Type string `json:"type,omitempty"` + Version int `json:"version,omitempty"` ExportedAt string `json:"exported_at"` Proxies []DataProxy `json:"proxies"` Accounts []DataAccount `json:"accounts"` @@ -160,8 +160,6 @@ func (h *AccountHandler) ExportData(c *gin.Context) { } payload := DataPayload{ - Type: dataType, - Version: dataVersion, ExportedAt: time.Now().UTC().Format(time.RFC3339), Proxies: dataProxies, Accounts: dataAccounts, @@ -218,9 +216,17 @@ func (h *AccountHandler) ImportData(c *gin.Context) { }) continue } + normalizedStatus := normalizeProxyStatus(item.Status) if existingID, ok := proxyKeyToID[key]; ok { proxyKeyToID[key] = existingID result.ProxyReused++ + if normalizedStatus != "" { + if proxy, err := h.adminService.GetProxy(c.Request.Context(), existingID); err == nil && proxy != nil && proxy.Status != normalizedStatus { + _, _ = h.adminService.UpdateProxy(c.Request.Context(), existingID, &service.UpdateProxyInput{ + Status: normalizedStatus, + }) + } + } continue } @@ -245,9 +251,9 @@ func (h *AccountHandler) ImportData(c *gin.Context) { proxyKeyToID[key] = created.ID result.ProxyCreated++ - if item.Status != "" && item.Status != created.Status { + if normalizedStatus != "" && normalizedStatus != created.Status { _, _ = h.adminService.UpdateProxy(c.Request.Context(), created.ID, &service.UpdateProxyInput{ - Status: item.Status, + Status: normalizedStatus, }) } } @@ -465,15 +471,18 @@ func parseIncludeProxies(c *gin.Context) (bool, error) { } func validateDataHeader(payload DataPayload) error { - if payload.Type == "" { - return errors.New("data type is required") - } - if payload.Type != dataType && payload.Type != legacyDataType { + if payload.Type != "" && payload.Type != dataType && payload.Type != legacyDataType { return fmt.Errorf("unsupported data type: %s", payload.Type) } - if payload.Version != dataVersion { + if payload.Version != 0 && payload.Version != dataVersion { return fmt.Errorf("unsupported data version: %d", payload.Version) } + if payload.Proxies == nil { + return errors.New("proxies is required") + } + if payload.Accounts == nil { + return errors.New("accounts is required") + } return nil } @@ -493,9 +502,8 @@ func validateDataProxy(item DataProxy) error { return fmt.Errorf("proxy protocol is invalid: %s", item.Protocol) } if item.Status != "" { - switch item.Status { - case service.StatusActive, service.StatusDisabled, "inactive": - default: + normalizedStatus := normalizeProxyStatus(item.Status) + if normalizedStatus != service.StatusActive && normalizedStatus != "inactive" { return fmt.Errorf("proxy status is invalid: %s", item.Status) } } @@ -538,3 +546,17 @@ func defaultProxyName(name string) string { } return name } + +func normalizeProxyStatus(status string) string { + normalized := strings.TrimSpace(strings.ToLower(status)) + switch normalized { + case "": + return "" + case service.StatusActive: + return service.StatusActive + case "inactive", service.StatusDisabled: + return "inactive" + default: + return normalized + } +} diff --git a/backend/internal/handler/admin/account_data_handler_test.go b/backend/internal/handler/admin/account_data_handler_test.go index a96e27c0..c8b04c2a 100644 --- a/backend/internal/handler/admin/account_data_handler_test.go +++ b/backend/internal/handler/admin/account_data_handler_test.go @@ -120,7 +120,8 @@ func TestExportDataIncludesSecrets(t *testing.T) { var resp dataResponse require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) require.Equal(t, 0, resp.Code) - require.Equal(t, dataType, resp.Data.Type) + require.Empty(t, resp.Data.Type) + require.Equal(t, 0, resp.Data.Version) require.Len(t, resp.Data.Proxies, 1) require.Equal(t, "pass", resp.Data.Proxies[0].Password) require.Len(t, resp.Data.Accounts, 1) diff --git a/backend/internal/handler/admin/proxy_data.go b/backend/internal/handler/admin/proxy_data.go index bc2a76ab..72ecd6c1 100644 --- a/backend/internal/handler/admin/proxy_data.go +++ b/backend/internal/handler/admin/proxy_data.go @@ -61,8 +61,6 @@ func (h *ProxyHandler) ExportData(c *gin.Context) { } payload := DataPayload{ - Type: dataType, - Version: dataVersion, ExportedAt: time.Now().UTC().Format(time.RFC3339), Proxies: dataProxies, Accounts: []DataAccount{}, @@ -123,10 +121,11 @@ func (h *ProxyHandler) ImportData(c *gin.Context) { continue } + normalizedStatus := normalizeProxyStatus(item.Status) if existing, ok := proxyByKey[key]; ok { result.ProxyReused++ - if item.Status != "" && item.Status != existing.Status { - if _, err := h.adminService.UpdateProxy(ctx, existing.ID, &service.UpdateProxyInput{Status: item.Status}); err != nil { + if normalizedStatus != "" && normalizedStatus != existing.Status { + if _, err := h.adminService.UpdateProxy(ctx, existing.ID, &service.UpdateProxyInput{Status: normalizedStatus}); err != nil { result.Errors = append(result.Errors, DataImportError{ Kind: "proxy", Name: item.Name, @@ -160,8 +159,8 @@ func (h *ProxyHandler) ImportData(c *gin.Context) { result.ProxyCreated++ proxyByKey[key] = *created - if item.Status != "" && item.Status != created.Status { - if _, err := h.adminService.UpdateProxy(ctx, created.ID, &service.UpdateProxyInput{Status: item.Status}); err != nil { + if normalizedStatus != "" && normalizedStatus != created.Status { + if _, err := h.adminService.UpdateProxy(ctx, created.ID, &service.UpdateProxyInput{Status: normalizedStatus}); err != nil { result.Errors = append(result.Errors, DataImportError{ Kind: "proxy", Name: item.Name, @@ -170,7 +169,7 @@ func (h *ProxyHandler) ImportData(c *gin.Context) { }) } } - latencyProbeIDs = append(latencyProbeIDs, created.ID) + // CreateProxy already triggers a latency probe, avoid double probing here. } if len(latencyProbeIDs) > 0 { @@ -186,18 +185,10 @@ func (h *ProxyHandler) ImportData(c *gin.Context) { } func (h *ProxyHandler) getProxiesByIDs(ctx context.Context, ids []int64) ([]service.Proxy, error) { - out := make([]service.Proxy, 0, len(ids)) - for _, id := range ids { - proxy, err := h.adminService.GetProxy(ctx, id) - if err != nil { - return nil, err - } - if proxy == nil { - continue - } - out = append(out, *proxy) + if len(ids) == 0 { + return []service.Proxy{}, nil } - return out, nil + return h.adminService.GetProxiesByIDs(ctx, ids) } func parseProxyIDs(c *gin.Context) ([]int64, error) { diff --git a/backend/internal/handler/admin/proxy_data_handler_test.go b/backend/internal/handler/admin/proxy_data_handler_test.go index 545b24a3..803f9b61 100644 --- a/backend/internal/handler/admin/proxy_data_handler_test.go +++ b/backend/internal/handler/admin/proxy_data_handler_test.go @@ -69,7 +69,8 @@ func TestProxyExportDataRespectsFilters(t *testing.T) { var resp proxyDataResponse require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) require.Equal(t, 0, resp.Code) - require.Equal(t, dataType, resp.Data.Type) + require.Empty(t, resp.Data.Type) + require.Equal(t, 0, resp.Data.Version) require.Len(t, resp.Data.Proxies, 1) require.Len(t, resp.Data.Accounts, 0) require.Equal(t, "https", resp.Data.Proxies[0].Protocol) @@ -156,6 +157,7 @@ func TestProxyImportDataReusesAndTriggersLatencyProbe(t *testing.T) { "status": "active", }, }, + "accounts": []map[string]any{}, }, } @@ -181,6 +183,6 @@ func TestProxyImportDataReusesAndTriggersLatencyProbe(t *testing.T) { require.Eventually(t, func() bool { adminSvc.mu.Lock() defer adminSvc.mu.Unlock() - return len(adminSvc.testedProxyIDs) == 2 + return len(adminSvc.testedProxyIDs) == 1 }, time.Second, 10*time.Millisecond) } diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index bf4a1fbc..82d7939b 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -728,8 +728,8 @@ export interface UpdateProxyRequest { } export interface AdminDataPayload { - type: string - version: number + type?: string + version?: number exported_at: string proxies: AdminDataProxy[] accounts: AdminDataAccount[]