fix(payment): return decrypted non-sensitive config on provider list

- Backend: add ListProviderInstancesWithConfig that decrypts config
  and masks sensitive fields (keys, secrets) as "••••••••"
- Frontend: pre-fill non-sensitive config values when editing provider
  (PID, API base URL, notify URL, return URL, channel IDs etc.)
- Sensitive fields left empty for user to re-enter if needed
This commit is contained in:
erio
2026-04-06 22:54:17 +08:00
parent 57ec01eb66
commit eff86c9a72
4 changed files with 69 additions and 1 deletions
@@ -221,7 +221,7 @@ func (h *PaymentHandler) DeletePlan(c *gin.Context) {
// ListProviders returns all payment provider instances.
// GET /api/v1/admin/payment/providers
func (h *PaymentHandler) ListProviders(c *gin.Context) {
providers, err := h.configService.ListProviderInstances(c.Request.Context())
providers, err := h.configService.ListProviderInstancesWithConfig(c.Request.Context())
if err != nil {
response.ErrorFrom(c, err)
return
@@ -244,6 +244,64 @@ func (s *PaymentConfigService) ListProviderInstances(ctx context.Context) ([]*db
return s.entClient.PaymentProviderInstance.Query().Order(paymentproviderinstance.BySortOrder()).All(ctx)
}
// ProviderInstanceResponse is the API response for a provider instance.
// Config contains non-sensitive fields in cleartext; sensitive fields are masked.
type ProviderInstanceResponse struct {
ID int64 `json:"id"`
ProviderKey string `json:"provider_key"`
Name string `json:"name"`
Config map[string]string `json:"config"`
SupportedTypes string `json:"supported_types"`
Limits string `json:"limits"`
Enabled bool `json:"enabled"`
RefundEnabled bool `json:"refund_enabled"`
SortOrder int `json:"sort_order"`
}
// ListProviderInstancesWithConfig returns provider instances with decrypted
// non-sensitive config fields and masked sensitive fields.
func (s *PaymentConfigService) ListProviderInstancesWithConfig(ctx context.Context) ([]ProviderInstanceResponse, error) {
instances, err := s.entClient.PaymentProviderInstance.Query().
Order(paymentproviderinstance.BySortOrder()).All(ctx)
if err != nil {
return nil, err
}
result := make([]ProviderInstanceResponse, 0, len(instances))
for _, inst := range instances {
resp := ProviderInstanceResponse{
ID: int64(inst.ID), ProviderKey: inst.ProviderKey, Name: inst.Name,
SupportedTypes: inst.SupportedTypes, Limits: inst.Limits,
Enabled: inst.Enabled, RefundEnabled: inst.RefundEnabled, SortOrder: inst.SortOrder,
}
resp.Config = s.decryptAndMaskConfig(inst.Config)
result = append(result, resp)
}
return result, nil
}
func (s *PaymentConfigService) decryptAndMaskConfig(encrypted string) map[string]string {
if encrypted == "" {
return nil
}
decrypted, err := payment.Decrypt(encrypted, s.encryptionKey)
if err != nil {
return nil
}
var raw map[string]string
if err := json.Unmarshal([]byte(decrypted), &raw); err != nil {
return nil
}
masked := make(map[string]string, len(raw))
for k, v := range raw {
if isSensitiveConfigField(k) && v != "" {
masked[k] = "••••••••"
} else {
masked[k] = v
}
}
return masked
}
// pendingOrderStatuses are order statuses considered "in progress" —
// modifying provider credentials or deleting a provider/plan is blocked
// while orders in these states exist.
@@ -349,6 +349,15 @@ function loadProvider(provider: ProviderInstance) {
form.enabled = provider.enabled
form.refund_enabled = provider.refund_enabled
clearConfig()
// Pre-fill config from API response (non-sensitive in cleartext, sensitive masked)
if (provider.config) {
for (const [k, v] of Object.entries(provider.config)) {
// Skip masked sensitive values — leave field empty so user can re-enter
if (v !== '••••••••') {
config[k] = v
}
}
}
// Parse existing limits
if (provider.limits) {
try {
+1
View File
@@ -103,6 +103,7 @@ export interface ProviderInstance {
id: number
provider_key: string
name: string
config: Record<string, string>
supported_types: string
enabled: boolean
refund_enabled: boolean