mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-30 00:21:34 +00:00
fix(gateway,frontend): 修复鉴权绕过与前端支付/会话缺陷
后端: - Gemini /v1beta 鉴权中间件补齐主中间件的授权校验: API Key 的 IP 白/黑名单、 专属分组授权、运行时过期/配额二次检查, 修复经 Gemini 端点绕过 IP ACL、 越权访问专属分组、以及状态未刷新时的配额/有效期绕过窗口。 - 粘性会话等待计划分支改走 newSelectionResult 以 hydrate 账号凭证, 修复调度 快照中账号凭证被剥离导致等待路径转发鉴权失败。 - SSE 流式转发客户端断开时不再 break 跳过当前事件 usage 合并, 修复少计费。 - Forward 对 nil gin.Context 的防御补齐; 上游错误体读取失败时记录日志避免静默。 前端: - logout 将本地会话清理移入 finally, 服务端吊销失败也保证本地登出。 - Stripe 弹窗轮询改用正确的 auth_token 键并加防重入; 收到 INIT 后清除兜底 超时定时器, onUnmounted 清理 message 监听器。 - token 刷新请求补充 30s 超时, 避免挂起导致请求队列与 loading 永久卡死。 - 路由守卫在公共设置未加载时先 await fetchPublicSettings, 避免 payment/ risk_control 被误判为未启用而错误拦截。 - 支付状态轮询回调补充防重入与终态守卫。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
Clawd
co-authored by
Cursor
parent
6f43986c37
commit
29a5fcd25e
@@ -132,16 +132,26 @@ async function renderQR() {
|
||||
}
|
||||
}
|
||||
|
||||
let pollInFlight = false
|
||||
async function pollStatus() {
|
||||
if (!orderId.value) return
|
||||
const order = await paymentStore.pollOrderStatus(orderId.value)
|
||||
if (!order) return
|
||||
if (order.status === 'COMPLETED' || order.status === 'PAID') {
|
||||
cleanup()
|
||||
router.push({ path: '/payment/result', query: { order_id: String(orderId.value), status: 'success' } })
|
||||
} else if (order.status === 'EXPIRED' || order.status === 'CANCELLED' || order.status === 'FAILED') {
|
||||
cleanup()
|
||||
expired.value = true
|
||||
// 防重入:接口响应慢于 3 秒轮询间隔时避免并发重叠请求与重复跳转。
|
||||
if (pollInFlight) return
|
||||
pollInFlight = true
|
||||
try {
|
||||
const order = await paymentStore.pollOrderStatus(orderId.value)
|
||||
if (!order) return
|
||||
// 定时器已被 cleanup 清除时不再执行终态跳转(响应可能在 cleanup 后才回来)。
|
||||
if (!pollTimer) return
|
||||
if (order.status === 'COMPLETED' || order.status === 'PAID') {
|
||||
cleanup()
|
||||
router.push({ path: '/payment/result', query: { order_id: String(orderId.value), status: 'success' } })
|
||||
} else if (order.status === 'EXPIRED' || order.status === 'CANCELLED' || order.status === 'FAILED') {
|
||||
cleanup()
|
||||
expired.value = true
|
||||
}
|
||||
} finally {
|
||||
pollInFlight = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,23 +84,38 @@ const success = ref(false)
|
||||
const hint = ref(t('payment.stripePopup.redirecting'))
|
||||
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
let initTimeoutTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let messageHandler: ((event: MessageEvent) => void) | null = null
|
||||
|
||||
function closeWindow() { window.close() }
|
||||
|
||||
function clearInitTimeout() {
|
||||
if (initTimeoutTimer) {
|
||||
clearTimeout(initTimeoutTimer)
|
||||
initTimeoutTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const handler = (event: MessageEvent) => {
|
||||
messageHandler = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return
|
||||
if (event.data?.type !== 'STRIPE_POPUP_INIT') return
|
||||
window.removeEventListener('message', handler)
|
||||
// INIT 已到达,取消兜底超时,避免长时间的扫码支付被误判为超时。
|
||||
clearInitTimeout()
|
||||
if (messageHandler) {
|
||||
window.removeEventListener('message', messageHandler)
|
||||
messageHandler = null
|
||||
}
|
||||
initStripe(event.data.clientSecret, event.data.publishableKey)
|
||||
}
|
||||
window.addEventListener('message', handler)
|
||||
window.addEventListener('message', messageHandler)
|
||||
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({ type: 'STRIPE_POPUP_READY' }, window.location.origin)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// 仅兜底“父窗口始终未发 STRIPE_POPUP_INIT”的场景。
|
||||
initTimeoutTimer = setTimeout(() => {
|
||||
if (!error.value && !success.value) {
|
||||
error.value = t('payment.stripePopup.timeout')
|
||||
}
|
||||
@@ -108,7 +123,12 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pollTimer) clearInterval(pollTimer)
|
||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
|
||||
clearInitTimeout()
|
||||
if (messageHandler) {
|
||||
window.removeEventListener('message', messageHandler)
|
||||
messageHandler = null
|
||||
}
|
||||
})
|
||||
|
||||
async function initStripe(clientSecret: string, publishableKey: string) {
|
||||
@@ -149,10 +169,15 @@ async function initStripe(clientSecret: string, publishableKey: string) {
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
let inFlight = false
|
||||
pollTimer = setInterval(async () => {
|
||||
// 防重入:接口响应慢于轮询间隔时避免并发重叠请求。
|
||||
if (inFlight) return
|
||||
inFlight = true
|
||||
try {
|
||||
const token = document.cookie.split('; ').find(c => c.startsWith('token='))?.split('=')[1]
|
||||
|| localStorage.getItem('token') || ''
|
||||
// access token 存储在 localStorage 的 'auth_token' 键下(见 api/client.ts),
|
||||
// 之前误读 'token' 导致轮询请求不带认证、永远 401,支付成功无法被检测到。
|
||||
const token = localStorage.getItem('auth_token') || ''
|
||||
const res = await fetch(buildApiUrl(`/payment/orders/${orderId}`), {
|
||||
headers: token ? { Authorization: 'Bearer ' + token } : {},
|
||||
credentials: 'include',
|
||||
@@ -165,7 +190,9 @@ function startPolling() {
|
||||
success.value = true
|
||||
setTimeout(closeWindow, 2000)
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch { /* ignore */ } finally {
|
||||
inFlight = false
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user