mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
feat(payment): order safety protection for provider/plan CRUD
Backend (matching sub2apipay behavior): - UpdateProviderInstance: block credential changes (key/secret/password fields) when instance has PENDING/PAID/RECHARGING orders (409 Conflict) - DeleteProviderInstance: block deletion when pending orders exist (409) - DeletePlan: block deletion when pending orders exist (409) - Non-credential changes (name/enabled/sort) still allowed with pending orders Frontend: - Filter empty config values before sending (avoid overwriting existing credentials when editing a provider without re-entering all secrets)
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/ent/paymentorder"
|
||||
"github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance"
|
||||
"github.com/Wei-Shaw/sub2api/ent/subscriptionplan"
|
||||
"github.com/Wei-Shaw/sub2api/internal/payment"
|
||||
@@ -243,6 +244,45 @@ func (s *PaymentConfigService) ListProviderInstances(ctx context.Context) ([]*db
|
||||
return s.entClient.PaymentProviderInstance.Query().Order(paymentproviderinstance.BySortOrder()).All(ctx)
|
||||
}
|
||||
|
||||
// pendingOrderStatuses are order statuses considered "in progress" —
|
||||
// modifying provider credentials or deleting a provider/plan is blocked
|
||||
// while orders in these states exist.
|
||||
var pendingOrderStatuses = []string{
|
||||
payment.OrderStatusPending,
|
||||
payment.OrderStatusPaid,
|
||||
payment.OrderStatusRecharging,
|
||||
}
|
||||
|
||||
// sensitiveConfigPatterns are substrings that identify credential fields.
|
||||
// Changes to these fields are blocked when the provider has pending orders.
|
||||
var sensitiveConfigPatterns = []string{"key", "pkey", "secret", "private", "password"}
|
||||
|
||||
func isSensitiveConfigField(fieldName string) bool {
|
||||
lower := strings.ToLower(fieldName)
|
||||
for _, p := range sensitiveConfigPatterns {
|
||||
if strings.Contains(lower, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *PaymentConfigService) countPendingOrders(ctx context.Context, providerInstanceID int64) (int, error) {
|
||||
return s.entClient.PaymentOrder.Query().
|
||||
Where(
|
||||
paymentorder.ProviderInstanceIDEQ(strconv.FormatInt(providerInstanceID, 10)),
|
||||
paymentorder.StatusIn(pendingOrderStatuses...),
|
||||
).Count(ctx)
|
||||
}
|
||||
|
||||
func (s *PaymentConfigService) countPendingOrdersByPlan(ctx context.Context, planID int64) (int, error) {
|
||||
return s.entClient.PaymentOrder.Query().
|
||||
Where(
|
||||
paymentorder.PlanIDEQ(planID),
|
||||
paymentorder.StatusIn(pendingOrderStatuses...),
|
||||
).Count(ctx)
|
||||
}
|
||||
|
||||
func (s *PaymentConfigService) CreateProviderInstance(ctx context.Context, req CreateProviderInstanceRequest) (*dbent.PaymentProviderInstance, error) {
|
||||
enc, err := s.encryptConfig(req.Config)
|
||||
if err != nil {
|
||||
@@ -256,6 +296,27 @@ func (s *PaymentConfigService) CreateProviderInstance(ctx context.Context, req C
|
||||
}
|
||||
|
||||
func (s *PaymentConfigService) UpdateProviderInstance(ctx context.Context, id int64, req UpdateProviderInstanceRequest) (*dbent.PaymentProviderInstance, error) {
|
||||
// Check credential change safety when config is being modified
|
||||
if req.Config != nil {
|
||||
hasSensitive := false
|
||||
for k := range req.Config {
|
||||
if isSensitiveConfigField(k) && req.Config[k] != "" {
|
||||
hasSensitive = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasSensitive {
|
||||
count, err := s.countPendingOrders(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check pending orders: %w", err)
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, infraerrors.Conflict("PENDING_ORDERS",
|
||||
fmt.Sprintf("this instance has %d in-progress orders; changing credentials may break payment callbacks — wait for orders to complete or disable the instance first", count))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
u := s.entClient.PaymentProviderInstance.UpdateOneID(id)
|
||||
if req.Name != nil {
|
||||
u.SetName(*req.Name)
|
||||
@@ -286,6 +347,14 @@ func (s *PaymentConfigService) UpdateProviderInstance(ctx context.Context, id in
|
||||
}
|
||||
|
||||
func (s *PaymentConfigService) DeleteProviderInstance(ctx context.Context, id int64) error {
|
||||
count, err := s.countPendingOrders(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check pending orders: %w", err)
|
||||
}
|
||||
if count > 0 {
|
||||
return infraerrors.Conflict("PENDING_ORDERS",
|
||||
fmt.Sprintf("this instance has %d in-progress orders and cannot be deleted — wait for orders to complete or disable the instance first", count))
|
||||
}
|
||||
return s.entClient.PaymentProviderInstance.DeleteOneID(id).Exec(ctx)
|
||||
}
|
||||
|
||||
@@ -367,6 +436,14 @@ func (s *PaymentConfigService) UpdatePlan(ctx context.Context, id int64, req Upd
|
||||
}
|
||||
|
||||
func (s *PaymentConfigService) DeletePlan(ctx context.Context, id int64) error {
|
||||
count, err := s.countPendingOrdersByPlan(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check pending orders: %w", err)
|
||||
}
|
||||
if count > 0 {
|
||||
return infraerrors.Conflict("PENDING_ORDERS",
|
||||
fmt.Sprintf("this plan has %d in-progress orders and cannot be deleted — wait for orders to complete first", count))
|
||||
}
|
||||
return s.entClient.SubscriptionPlan.DeleteOneID(id).Exec(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -3070,6 +3070,11 @@ function openEditProvider(provider: ProviderInstance) {
|
||||
async function handleSaveProvider() {
|
||||
providerSaving.value = true
|
||||
try {
|
||||
// Filter out empty config values to avoid overwriting existing credentials
|
||||
const filteredConfig: Record<string, string> = {}
|
||||
for (const [k, v] of Object.entries(providerConfig)) {
|
||||
if (v && v.trim()) filteredConfig[k] = v
|
||||
}
|
||||
// Build camelCase payload matching backend struct
|
||||
const payload: any = {
|
||||
providerKey: providerForm.provider_key,
|
||||
@@ -3077,7 +3082,7 @@ async function handleSaveProvider() {
|
||||
supportedTypes: providerForm.supported_types,
|
||||
enabled: providerForm.enabled,
|
||||
refundEnabled: providerForm.refund_enabled,
|
||||
config: { ...providerConfig },
|
||||
config: filteredConfig,
|
||||
}
|
||||
if (editingProvider.value) { await adminAPI.payment.updateProvider(editingProvider.value.id, payload) }
|
||||
else { await adminAPI.payment.createProvider(payload) }
|
||||
|
||||
Reference in New Issue
Block a user