Unverified Commit 51af8df3 authored by Wesley Liddick's avatar Wesley Liddick Committed by GitHub
Browse files

Merge pull request #1731 from touwaeriol/fix/rate-billing-autofill-response-limit

fix: subscription billing, alipay redirect + H5, payment secrets, 128MB response limit
parents 6c73b621 235f7108
......@@ -52,6 +52,10 @@
v-model="editApiKey"
type="password"
class="input font-mono"
autocomplete="new-password"
data-1p-ignore
data-lpignore="true"
data-bwignore="true"
:placeholder="
account.platform === 'openai'
? 'sk-proj-...'
......
......@@ -166,7 +166,7 @@
<input
type="number"
step="0.001"
min="0"
min="0.001"
autocomplete="off"
:value="entry.rate_multiplier"
class="hide-spinner w-20 rounded border border-gray-200 bg-white px-2 py-1 text-center text-sm font-medium transition-colors focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500/20 dark:border-dark-500 dark:bg-dark-700 dark:focus:border-primary-500"
......
......@@ -81,7 +81,7 @@
<input
type="number"
step="0.001"
min="0"
min="0.001"
:value="config.customRate ?? ''"
@input="updateCustomRate(config.groupId, ($event.target as HTMLInputElement).value)"
:placeholder="String(config.defaultRate)"
......@@ -139,7 +139,7 @@
<input
type="number"
step="0.001"
min="0"
min="0.001"
:value="config.customRate ?? ''"
@input="updateCustomRate(config.groupId, ($event.target as HTMLInputElement).value)"
:placeholder="String(config.defaultRate)"
......
......@@ -88,13 +88,24 @@
v-model="config[field.key]"
rows="3"
class="input font-mono text-xs"
autocomplete="new-password"
data-1p-ignore
data-lpignore="true"
data-bwignore="true"
spellcheck="false"
:placeholder="editing ? t('admin.accounts.leaveEmptyToKeep') : ''"
/>
<div v-else-if="field.sensitive" class="relative">
<input
:type="visibleFields[field.key] ? 'text' : 'password'"
v-model="config[field.key]"
class="input pr-10"
:placeholder="field.defaultValue || ''"
autocomplete="new-password"
data-1p-ignore
data-lpignore="true"
data-bwignore="true"
spellcheck="false"
:placeholder="editing ? t('admin.accounts.leaveEmptyToKeep') : (field.defaultValue || '')"
/>
<button
type="button"
......@@ -398,9 +409,12 @@ function handleSave() {
emitValidationError(t('admin.settings.payment.validationNameRequired'))
return
}
// Validate required config fields — all non-optional fields must be filled
// Validate required config fields — all non-optional fields must be filled.
// In edit mode, sensitive fields may be left blank to preserve the stored
// value (backend merges blanks by preserving the existing secret).
for (const f of PROVIDER_CONFIG_FIELDS[form.provider_key] || []) {
if (f.optional) continue
if (props.editing && f.sensitive) continue
const val = (config[f.key] || '').trim()
if (!val) {
const label = f.label || t(`admin.settings.payment.field_${f.key}`)
......@@ -412,8 +426,6 @@ function handleSave() {
const filteredConfig: Record<string, string> = {}
for (const [k, v] of Object.entries(config)) {
if (!v || !v.trim()) continue
// Skip masked values — backend keeps existing credentials
if (v === '••••••••') continue
filteredConfig[k] = v
}
......@@ -470,7 +482,8 @@ function loadProvider(provider: ProviderInstance) {
form.refund_enabled = provider.refund_enabled
form.allow_user_refund = provider.allow_user_refund
clearConfig()
// Pre-fill config from API response (non-sensitive in cleartext, sensitive masked as ••••••••)
// Pre-fill config from API response. Backend omits sensitive fields entirely,
// so those inputs stay blank — submitting blank preserves the stored secret.
if (provider.config) {
for (const [k, v] of Object.entries(provider.config)) {
// Skip notifyUrl/returnUrl — they are derived from callbackBaseUrl
......
......@@ -79,7 +79,7 @@ import { usePaymentStore } from '@/stores/payment'
import { useAppStore } from '@/stores'
import { paymentAPI } from '@/api/payment'
import { extractApiErrorMessage } from '@/utils/apiError'
import { POPUP_WINDOW_FEATURES } from '@/components/payment/providerConfig'
import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
import type { PaymentOrder } from '@/types/payment'
import QRCode from 'qrcode'
import alipayIcon from '@/assets/icons/alipay.svg'
......@@ -147,7 +147,7 @@ function getLogoForType(): string | null {
function reopenPopup() {
if (props.payUrl) {
window.open(props.payUrl, 'paymentPopup', POPUP_WINDOW_FEATURES)
window.open(props.payUrl, 'paymentPopup', getPaymentPopupFeatures())
}
}
......
......@@ -125,7 +125,7 @@ import { usePaymentStore } from '@/stores/payment'
import { useAppStore } from '@/stores'
import { paymentAPI } from '@/api/payment'
import { extractApiErrorMessage } from '@/utils/apiError'
import { POPUP_WINDOW_FEATURES } from '@/components/payment/providerConfig'
import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
import type { PaymentOrder } from '@/types/payment'
import Icon from '@/components/icons/Icon.vue'
import QRCode from 'qrcode'
......@@ -194,7 +194,7 @@ const countdownDisplay = computed(() => {
function reopenPopup() {
if (props.payUrl) {
window.open(props.payUrl, 'paymentPopup', POPUP_WINDOW_FEATURES)
window.open(props.payUrl, 'paymentPopup', getPaymentPopupFeatures())
}
}
......
......@@ -70,7 +70,7 @@ import { useRouter } from 'vue-router'
import { extractApiErrorMessage } from '@/utils/apiError'
import { paymentAPI } from '@/api/payment'
import { useAppStore } from '@/stores'
import { STRIPE_POPUP_WINDOW_FEATURES } from '@/components/payment/providerConfig'
import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
import type { Stripe, StripeElements } from '@stripe/stripe-js'
import Icon from '@/components/icons/Icon.vue'
......@@ -151,7 +151,7 @@ async function handlePay() {
amount: String(props.payAmount),
},
}).href
const popup = window.open(popupUrl, 'paymentPopup', STRIPE_POPUP_WINDOW_FEATURES)
const popup = window.open(popupUrl, 'paymentPopup', getPaymentPopupFeatures())
const onReady = (event: MessageEvent) => {
if (event.source !== popup || event.data?.type !== 'STRIPE_POPUP_READY') return
......
......@@ -43,11 +43,24 @@ export const METHOD_ORDER = ['alipay', 'alipay_direct', 'wxpay', 'wxpay_direct',
export const PAYMENT_MODE_QRCODE = 'qrcode'
export const PAYMENT_MODE_POPUP = 'popup'
/** Window features for payment popup windows */
export const POPUP_WINDOW_FEATURES = 'width=1000,height=750,left=100,top=80,scrollbars=yes,resizable=yes'
/** Wider popup for Stripe redirect methods (Alipay checkout page needs ~1200px) */
export const STRIPE_POPUP_WINDOW_FEATURES = 'width=1250,height=780,left=80,top=60,scrollbars=yes,resizable=yes'
/** Preferred popup size for payment gateways. Alipay's standard checkout
* (QR + account login panel) needs ~1200×900 to render without any scrolling. */
const PAYMENT_POPUP_PREFERRED_WIDTH = 1250
const PAYMENT_POPUP_PREFERRED_HEIGHT = 900
/** Build a window.open features string sized to fit within the current screen
* while preferring the above dimensions. Centers the popup on the available
* work area so nothing is clipped on smaller laptop displays. */
export function getPaymentPopupFeatures(): string {
const screen = typeof window !== 'undefined' ? window.screen : null
const availW = screen?.availWidth ?? PAYMENT_POPUP_PREFERRED_WIDTH
const availH = screen?.availHeight ?? PAYMENT_POPUP_PREFERRED_HEIGHT
const width = Math.min(PAYMENT_POPUP_PREFERRED_WIDTH, availW - 40)
const height = Math.min(PAYMENT_POPUP_PREFERRED_HEIGHT, availH - 40)
const left = Math.max(0, Math.floor((availW - width) / 2))
const top = Math.max(0, Math.floor((availH - height) / 2))
return `width=${width},height=${height},left=${left},top=${top},scrollbars=yes,resizable=yes`
}
/** Webhook paths for each provider (relative to origin). */
export const WEBHOOK_PATHS: Record<string, string> = {
......
......@@ -154,6 +154,7 @@ export interface CreateOrderRequest {
payment_type: string
order_type: string
plan_id?: number
is_mobile?: boolean
}
export interface CreateOrderResult {
......
......@@ -277,7 +277,7 @@ import type { SubscriptionPlan, CheckoutInfoResponse, OrderType } from '@/types/
import AppLayout from '@/components/layout/AppLayout.vue'
import AmountInput from '@/components/payment/AmountInput.vue'
import PaymentMethodSelector from '@/components/payment/PaymentMethodSelector.vue'
import { METHOD_ORDER, POPUP_WINDOW_FEATURES } from '@/components/payment/providerConfig'
import { METHOD_ORDER, getPaymentPopupFeatures } from '@/components/payment/providerConfig'
import { platformAccentBarClass, platformBadgeLightClass, platformBadgeClass, platformTextClass, platformLabel } from '@/utils/platformColors'
import SubscriptionPlanCard from '@/components/payment/SubscriptionPlanCard.vue'
import PaymentStatusPanel from '@/components/payment/PaymentStatusPanel.vue'
......@@ -551,9 +551,10 @@ async function createOrder(orderAmount: number, orderType: OrderType, planId?: n
payment_type: selectedMethod.value,
order_type: orderType,
plan_id: planId,
is_mobile: isMobileDevice(),
})
const openWindow = (url: string) => {
const win = window.open(url, 'paymentPopup', POPUP_WINDOW_FEATURES)
const win = window.open(url, 'paymentPopup', getPaymentPopupFeatures())
if (!win || win.closed) {
window.location.href = url
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment