Merge branch 'main' into rebuild/auth-identity-foundation
This commit is contained in:
@@ -284,6 +284,16 @@ const hasError = computed(() => {
|
||||
return props.account.status === 'error'
|
||||
})
|
||||
|
||||
const isQuotaExceeded = computed(() => {
|
||||
const exceeded = (used?: number | null, limit?: number | null) =>
|
||||
typeof limit === 'number' && limit > 0 && typeof used === 'number' && used >= limit
|
||||
return (
|
||||
exceeded(props.account.quota_used, props.account.quota_limit) ||
|
||||
exceeded(props.account.quota_daily_used, props.account.quota_daily_limit) ||
|
||||
exceeded(props.account.quota_weekly_used, props.account.quota_weekly_limit)
|
||||
)
|
||||
})
|
||||
|
||||
// Computed: countdown text for rate limit (429)
|
||||
const rateLimitCountdown = computed(() => {
|
||||
return formatCountdown(props.account.rate_limit_reset_at)
|
||||
@@ -307,19 +317,16 @@ const statusClass = computed(() => {
|
||||
if (isTempUnschedulable.value) {
|
||||
return 'badge-warning'
|
||||
}
|
||||
if (props.account.status !== 'active') {
|
||||
return props.account.status === 'error' ? 'badge-danger' : 'badge-gray'
|
||||
}
|
||||
if (isQuotaExceeded.value) {
|
||||
return 'badge-warning'
|
||||
}
|
||||
if (!props.account.schedulable) {
|
||||
return 'badge-gray'
|
||||
}
|
||||
switch (props.account.status) {
|
||||
case 'active':
|
||||
return 'badge-success'
|
||||
case 'inactive':
|
||||
return 'badge-gray'
|
||||
case 'error':
|
||||
return 'badge-danger'
|
||||
default:
|
||||
return 'badge-gray'
|
||||
}
|
||||
return 'badge-success'
|
||||
})
|
||||
|
||||
// Computed: status text
|
||||
@@ -330,6 +337,12 @@ const statusText = computed(() => {
|
||||
if (isTempUnschedulable.value) {
|
||||
return t('admin.accounts.status.tempUnschedulable')
|
||||
}
|
||||
if (props.account.status !== 'active') {
|
||||
return t(`admin.accounts.status.${props.account.status}`)
|
||||
}
|
||||
if (isQuotaExceeded.value) {
|
||||
return t('admin.accounts.status.quotaExceeded')
|
||||
}
|
||||
if (!props.account.schedulable) {
|
||||
return t('admin.accounts.status.paused')
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -617,66 +617,6 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
|
||||
}
|
||||
}
|
||||
const openaiModels = {
|
||||
'gpt-5-codex': {
|
||||
name: 'GPT-5 Codex',
|
||||
limit: {
|
||||
context: 400000,
|
||||
output: 128000
|
||||
},
|
||||
options: {
|
||||
store: false
|
||||
},
|
||||
variants: {
|
||||
low: {},
|
||||
medium: {},
|
||||
high: {}
|
||||
}
|
||||
},
|
||||
'gpt-5.1-codex': {
|
||||
name: 'GPT-5.1 Codex',
|
||||
limit: {
|
||||
context: 400000,
|
||||
output: 128000
|
||||
},
|
||||
options: {
|
||||
store: false
|
||||
},
|
||||
variants: {
|
||||
low: {},
|
||||
medium: {},
|
||||
high: {}
|
||||
}
|
||||
},
|
||||
'gpt-5.1-codex-max': {
|
||||
name: 'GPT-5.1 Codex Max',
|
||||
limit: {
|
||||
context: 400000,
|
||||
output: 128000
|
||||
},
|
||||
options: {
|
||||
store: false
|
||||
},
|
||||
variants: {
|
||||
low: {},
|
||||
medium: {},
|
||||
high: {}
|
||||
}
|
||||
},
|
||||
'gpt-5.1-codex-mini': {
|
||||
name: 'GPT-5.1 Codex Mini',
|
||||
limit: {
|
||||
context: 400000,
|
||||
output: 128000
|
||||
},
|
||||
options: {
|
||||
store: false
|
||||
},
|
||||
variants: {
|
||||
low: {},
|
||||
medium: {},
|
||||
high: {}
|
||||
}
|
||||
},
|
||||
'gpt-5.2': {
|
||||
name: 'GPT-5.2',
|
||||
limit: {
|
||||
@@ -725,22 +665,6 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
|
||||
xhigh: {}
|
||||
}
|
||||
},
|
||||
'gpt-5.4-nano': {
|
||||
name: 'GPT-5.4 Nano',
|
||||
limit: {
|
||||
context: 400000,
|
||||
output: 128000
|
||||
},
|
||||
options: {
|
||||
store: false
|
||||
},
|
||||
variants: {
|
||||
low: {},
|
||||
medium: {},
|
||||
high: {},
|
||||
xhigh: {}
|
||||
}
|
||||
},
|
||||
'gpt-5.3-codex-spark': {
|
||||
name: 'GPT-5.3 Codex Spark',
|
||||
limit: {
|
||||
@@ -773,22 +697,6 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
|
||||
xhigh: {}
|
||||
}
|
||||
},
|
||||
'gpt-5.2-codex': {
|
||||
name: 'GPT-5.2 Codex',
|
||||
limit: {
|
||||
context: 400000,
|
||||
output: 128000
|
||||
},
|
||||
options: {
|
||||
store: false
|
||||
},
|
||||
variants: {
|
||||
low: {},
|
||||
medium: {},
|
||||
high: {},
|
||||
xhigh: {}
|
||||
}
|
||||
},
|
||||
'codex-mini-latest': {
|
||||
name: 'Codex Mini',
|
||||
limit: {
|
||||
|
||||
@@ -17,7 +17,7 @@ vi.mock('@/composables/useClipboard', () => ({
|
||||
import UseKeyModal from '../UseKeyModal.vue'
|
||||
|
||||
describe('UseKeyModal', () => {
|
||||
it('renders updated GPT-5.4 mini/nano names in OpenCode config', async () => {
|
||||
it('renders GPT-5.4 mini entry in OpenCode config', async () => {
|
||||
const wrapper = mount(UseKeyModal, {
|
||||
props: {
|
||||
show: true,
|
||||
@@ -48,6 +48,6 @@ describe('UseKeyModal', () => {
|
||||
const codeBlock = wrapper.find('pre code')
|
||||
expect(codeBlock.exists()).toBe(true)
|
||||
expect(codeBlock.text()).toContain('"name": "GPT-5.4 Mini"')
|
||||
expect(codeBlock.text()).toContain('"name": "GPT-5.4 Nano"')
|
||||
expect(codeBlock.text()).not.toContain('"name": "GPT-5.4 Nano"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -78,8 +78,8 @@ import Icon from '@/components/icons/Icon.vue'
|
||||
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 { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ async function handleCancel() {
|
||||
cleanup()
|
||||
emit('close')
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
|
||||
@@ -124,8 +124,8 @@ import { useI18n } from 'vue-i18n'
|
||||
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 { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import QRCode from 'qrcode'
|
||||
@@ -200,7 +200,7 @@ function isSuccessStatus(status: string | null | undefined): boolean {
|
||||
|
||||
function reopenPopup() {
|
||||
if (props.payUrl) {
|
||||
const win = window.open(props.payUrl, 'paymentPopup', POPUP_WINDOW_FEATURES)
|
||||
const win = window.open(props.payUrl, 'paymentPopup', getPaymentPopupFeatures())
|
||||
if (!win || win.closed) {
|
||||
window.location.href = props.payUrl
|
||||
}
|
||||
@@ -257,7 +257,7 @@ async function handleCancel() {
|
||||
cleanup()
|
||||
setOutcome('cancelled')
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
|
||||
@@ -67,10 +67,10 @@
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { extractI18nErrorMessage } 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'
|
||||
|
||||
@@ -132,7 +132,7 @@ onMounted(async () => {
|
||||
selectedType.value = event.value.type
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
initError.value = extractApiErrorMessage(err, t('payment.stripeLoadFailed'))
|
||||
initError.value = extractI18nErrorMessage(err, t, 'payment.errors', t('payment.stripeLoadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -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
|
||||
@@ -186,7 +186,7 @@ async function handlePay() {
|
||||
emit('success')
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
error.value = extractApiErrorMessage(err, t('payment.result.failed'))
|
||||
error.value = extractI18nErrorMessage(err, t, 'payment.errors', t('payment.result.failed'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -199,7 +199,7 @@ async function handleCancel() {
|
||||
await paymentAPI.cancelOrder(props.orderId)
|
||||
emit('back')
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
/** 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
|
||||
|
||||
/** 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'
|
||||
/** 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> = {
|
||||
@@ -87,9 +100,9 @@ export const PROVIDER_CONFIG_FIELDS: Record<string, ConfigFieldDef[]> = {
|
||||
{ key: 'mchId', label: '', sensitive: false },
|
||||
{ key: 'privateKey', label: '', sensitive: true },
|
||||
{ key: 'apiV3Key', label: '', sensitive: true },
|
||||
{ key: 'certSerial', label: '', sensitive: false },
|
||||
{ key: 'publicKey', label: '', sensitive: true },
|
||||
{ key: 'publicKeyId', label: '', sensitive: false },
|
||||
{ key: 'certSerial', label: '', sensitive: false },
|
||||
{ key: 'h5AppName', label: '', sensitive: false, optional: true },
|
||||
{ key: 'h5AppUrl', label: '', sensitive: false, optional: true },
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user