fix: harden import/export flow
This commit is contained in:
@@ -21,8 +21,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DataPayload struct {
|
type DataPayload struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type,omitempty"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version,omitempty"`
|
||||||
ExportedAt string `json:"exported_at"`
|
ExportedAt string `json:"exported_at"`
|
||||||
Proxies []DataProxy `json:"proxies"`
|
Proxies []DataProxy `json:"proxies"`
|
||||||
Accounts []DataAccount `json:"accounts"`
|
Accounts []DataAccount `json:"accounts"`
|
||||||
@@ -160,8 +160,6 @@ func (h *AccountHandler) ExportData(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
payload := DataPayload{
|
payload := DataPayload{
|
||||||
Type: dataType,
|
|
||||||
Version: dataVersion,
|
|
||||||
ExportedAt: time.Now().UTC().Format(time.RFC3339),
|
ExportedAt: time.Now().UTC().Format(time.RFC3339),
|
||||||
Proxies: dataProxies,
|
Proxies: dataProxies,
|
||||||
Accounts: dataAccounts,
|
Accounts: dataAccounts,
|
||||||
@@ -218,9 +216,17 @@ func (h *AccountHandler) ImportData(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
normalizedStatus := normalizeProxyStatus(item.Status)
|
||||||
if existingID, ok := proxyKeyToID[key]; ok {
|
if existingID, ok := proxyKeyToID[key]; ok {
|
||||||
proxyKeyToID[key] = existingID
|
proxyKeyToID[key] = existingID
|
||||||
result.ProxyReused++
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,9 +251,9 @@ func (h *AccountHandler) ImportData(c *gin.Context) {
|
|||||||
proxyKeyToID[key] = created.ID
|
proxyKeyToID[key] = created.ID
|
||||||
result.ProxyCreated++
|
result.ProxyCreated++
|
||||||
|
|
||||||
if item.Status != "" && item.Status != created.Status {
|
if normalizedStatus != "" && normalizedStatus != created.Status {
|
||||||
_, _ = h.adminService.UpdateProxy(c.Request.Context(), created.ID, &service.UpdateProxyInput{
|
_, _ = 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 {
|
func validateDataHeader(payload DataPayload) error {
|
||||||
if payload.Type == "" {
|
if payload.Type != "" && payload.Type != dataType && payload.Type != legacyDataType {
|
||||||
return errors.New("data type is required")
|
|
||||||
}
|
|
||||||
if payload.Type != dataType && payload.Type != legacyDataType {
|
|
||||||
return fmt.Errorf("unsupported data type: %s", payload.Type)
|
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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,9 +502,8 @@ func validateDataProxy(item DataProxy) error {
|
|||||||
return fmt.Errorf("proxy protocol is invalid: %s", item.Protocol)
|
return fmt.Errorf("proxy protocol is invalid: %s", item.Protocol)
|
||||||
}
|
}
|
||||||
if item.Status != "" {
|
if item.Status != "" {
|
||||||
switch item.Status {
|
normalizedStatus := normalizeProxyStatus(item.Status)
|
||||||
case service.StatusActive, service.StatusDisabled, "inactive":
|
if normalizedStatus != service.StatusActive && normalizedStatus != "inactive" {
|
||||||
default:
|
|
||||||
return fmt.Errorf("proxy status is invalid: %s", item.Status)
|
return fmt.Errorf("proxy status is invalid: %s", item.Status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -538,3 +546,17 @@ func defaultProxyName(name string) string {
|
|||||||
}
|
}
|
||||||
return name
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -120,7 +120,8 @@ func TestExportDataIncludesSecrets(t *testing.T) {
|
|||||||
var resp dataResponse
|
var resp dataResponse
|
||||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||||
require.Equal(t, 0, resp.Code)
|
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.Proxies, 1)
|
||||||
require.Equal(t, "pass", resp.Data.Proxies[0].Password)
|
require.Equal(t, "pass", resp.Data.Proxies[0].Password)
|
||||||
require.Len(t, resp.Data.Accounts, 1)
|
require.Len(t, resp.Data.Accounts, 1)
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ func (h *ProxyHandler) ExportData(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
payload := DataPayload{
|
payload := DataPayload{
|
||||||
Type: dataType,
|
|
||||||
Version: dataVersion,
|
|
||||||
ExportedAt: time.Now().UTC().Format(time.RFC3339),
|
ExportedAt: time.Now().UTC().Format(time.RFC3339),
|
||||||
Proxies: dataProxies,
|
Proxies: dataProxies,
|
||||||
Accounts: []DataAccount{},
|
Accounts: []DataAccount{},
|
||||||
@@ -123,10 +121,11 @@ func (h *ProxyHandler) ImportData(c *gin.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalizedStatus := normalizeProxyStatus(item.Status)
|
||||||
if existing, ok := proxyByKey[key]; ok {
|
if existing, ok := proxyByKey[key]; ok {
|
||||||
result.ProxyReused++
|
result.ProxyReused++
|
||||||
if item.Status != "" && item.Status != existing.Status {
|
if normalizedStatus != "" && normalizedStatus != existing.Status {
|
||||||
if _, err := h.adminService.UpdateProxy(ctx, existing.ID, &service.UpdateProxyInput{Status: item.Status}); err != nil {
|
if _, err := h.adminService.UpdateProxy(ctx, existing.ID, &service.UpdateProxyInput{Status: normalizedStatus}); err != nil {
|
||||||
result.Errors = append(result.Errors, DataImportError{
|
result.Errors = append(result.Errors, DataImportError{
|
||||||
Kind: "proxy",
|
Kind: "proxy",
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
@@ -160,8 +159,8 @@ func (h *ProxyHandler) ImportData(c *gin.Context) {
|
|||||||
result.ProxyCreated++
|
result.ProxyCreated++
|
||||||
proxyByKey[key] = *created
|
proxyByKey[key] = *created
|
||||||
|
|
||||||
if item.Status != "" && item.Status != created.Status {
|
if normalizedStatus != "" && normalizedStatus != created.Status {
|
||||||
if _, err := h.adminService.UpdateProxy(ctx, created.ID, &service.UpdateProxyInput{Status: item.Status}); err != nil {
|
if _, err := h.adminService.UpdateProxy(ctx, created.ID, &service.UpdateProxyInput{Status: normalizedStatus}); err != nil {
|
||||||
result.Errors = append(result.Errors, DataImportError{
|
result.Errors = append(result.Errors, DataImportError{
|
||||||
Kind: "proxy",
|
Kind: "proxy",
|
||||||
Name: item.Name,
|
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 {
|
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) {
|
func (h *ProxyHandler) getProxiesByIDs(ctx context.Context, ids []int64) ([]service.Proxy, error) {
|
||||||
out := make([]service.Proxy, 0, len(ids))
|
if len(ids) == 0 {
|
||||||
for _, id := range ids {
|
return []service.Proxy{}, nil
|
||||||
proxy, err := h.adminService.GetProxy(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if proxy == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, *proxy)
|
|
||||||
}
|
}
|
||||||
return out, nil
|
return h.adminService.GetProxiesByIDs(ctx, ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseProxyIDs(c *gin.Context) ([]int64, error) {
|
func parseProxyIDs(c *gin.Context) ([]int64, error) {
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ func TestProxyExportDataRespectsFilters(t *testing.T) {
|
|||||||
var resp proxyDataResponse
|
var resp proxyDataResponse
|
||||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||||
require.Equal(t, 0, resp.Code)
|
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.Proxies, 1)
|
||||||
require.Len(t, resp.Data.Accounts, 0)
|
require.Len(t, resp.Data.Accounts, 0)
|
||||||
require.Equal(t, "https", resp.Data.Proxies[0].Protocol)
|
require.Equal(t, "https", resp.Data.Proxies[0].Protocol)
|
||||||
@@ -156,6 +157,7 @@ func TestProxyImportDataReusesAndTriggersLatencyProbe(t *testing.T) {
|
|||||||
"status": "active",
|
"status": "active",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"accounts": []map[string]any{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +183,6 @@ func TestProxyImportDataReusesAndTriggersLatencyProbe(t *testing.T) {
|
|||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
adminSvc.mu.Lock()
|
adminSvc.mu.Lock()
|
||||||
defer adminSvc.mu.Unlock()
|
defer adminSvc.mu.Unlock()
|
||||||
return len(adminSvc.testedProxyIDs) == 2
|
return len(adminSvc.testedProxyIDs) == 1
|
||||||
}, time.Second, 10*time.Millisecond)
|
}, time.Second, 10*time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -728,8 +728,8 @@ export interface UpdateProxyRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminDataPayload {
|
export interface AdminDataPayload {
|
||||||
type: string
|
type?: string
|
||||||
version: number
|
version?: number
|
||||||
exported_at: string
|
exported_at: string
|
||||||
proxies: AdminDataProxy[]
|
proxies: AdminDataProxy[]
|
||||||
accounts: AdminDataAccount[]
|
accounts: AdminDataAccount[]
|
||||||
|
|||||||
Reference in New Issue
Block a user