Merge branch 'release/test' of http://8.163.30.128:10082/html/client-web into release/test
This commit is contained in:
@@ -11,6 +11,19 @@ server {
|
||||
return 200 "ok\n";
|
||||
}
|
||||
|
||||
# SPA 入口每次重新校验,避免发布后浏览器继续运行旧资源清单。
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires "0" always;
|
||||
}
|
||||
|
||||
# Vite 产物文件名带内容哈希,可安全长期缓存。
|
||||
location /assets/ {
|
||||
try_files $uri =404;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
}
|
||||
|
||||
# 前端统一请求 /api,保持原始路径转发给 Gateway。
|
||||
location /api/ {
|
||||
client_max_body_size 55m;
|
||||
@@ -36,6 +49,7 @@ server {
|
||||
|
||||
# 支持 Vue Router history 路由直接刷新。
|
||||
location / {
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
|
||||
+31
-11
@@ -1,32 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
|
||||
let readyInterval: number | null = null
|
||||
let expectedAdminOrigin = ''
|
||||
let impersonationExchangePending = false
|
||||
|
||||
function configuredImpersonationOrigin() {
|
||||
const queryOrigin = new URLSearchParams(window.location.search).get('impersonationOrigin')
|
||||
if (queryOrigin) {
|
||||
try { return new URL(queryOrigin).origin } catch { return '' }
|
||||
}
|
||||
return String(import.meta.env.VITE_ADMIN_WEB_ORIGIN || '').trim() || window.location.origin
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const configuredAdminOrigin = String(import.meta.env.VITE_ADMIN_WEB_ORIGIN || '').trim()
|
||||
const targetOrigin = configuredAdminOrigin || window.location.origin
|
||||
const announceReady = () => window.opener?.postMessage({ source: 'chongchong-client', type: 'impersonation-ready' }, targetOrigin)
|
||||
expectedAdminOrigin = configuredImpersonationOrigin()
|
||||
if (!window.opener || !expectedAdminOrigin) return
|
||||
window.addEventListener('message', acceptImpersonationTicket)
|
||||
const announceReady = () => window.opener?.postMessage({ source: 'chongchong-client', type: 'impersonation-ready' }, expectedAdminOrigin)
|
||||
announceReady()
|
||||
readyInterval = window.setInterval(announceReady, 300)
|
||||
window.setTimeout(() => {
|
||||
if (readyInterval !== null) window.clearInterval(readyInterval)
|
||||
readyInterval = null
|
||||
}, 10000)
|
||||
window.addEventListener('message', stopReadyAnnouncement)
|
||||
})
|
||||
|
||||
function stopReadyAnnouncement(event: MessageEvent<unknown>) {
|
||||
const configuredAdminOrigin = String(import.meta.env.VITE_ADMIN_WEB_ORIGIN || '').trim()
|
||||
const expectedAdminOrigin = configuredAdminOrigin || window.location.origin
|
||||
const message = event.data as { source?: string; type?: string } | null
|
||||
if (event.source !== window.opener || event.origin !== expectedAdminOrigin || message?.source !== 'chongchong-admin' || message.type !== 'impersonation-ticket' || readyInterval === null) return
|
||||
window.clearInterval(readyInterval)
|
||||
async function acceptImpersonationTicket(event: MessageEvent<unknown>) {
|
||||
const message = event.data as { source?: string; type?: string; code?: string } | null
|
||||
if (event.source !== window.opener || event.origin !== expectedAdminOrigin || message?.source !== 'chongchong-admin' || message.type !== 'impersonation-ticket' || !message.code || impersonationExchangePending) return
|
||||
impersonationExchangePending = true
|
||||
if (readyInterval !== null) window.clearInterval(readyInterval)
|
||||
readyInterval = null
|
||||
const auth = useAuthStore()
|
||||
try {
|
||||
await auth.acceptImpersonationCode(message.code)
|
||||
window.opener?.postMessage({ source: 'chongchong-client', type: 'impersonation-complete' }, expectedAdminOrigin)
|
||||
window.location.replace('/')
|
||||
} catch {
|
||||
window.opener?.postMessage({ source: 'chongchong-client', type: 'impersonation-failed' }, expectedAdminOrigin)
|
||||
window.location.replace('/login?impersonation=failed')
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (readyInterval !== null) window.clearInterval(readyInterval)
|
||||
window.removeEventListener('message', stopReadyAnnouncement)
|
||||
window.removeEventListener('message', acceptImpersonationTicket)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ test('allows 50 MiB uploads with multipart overhead through both API proxies', (
|
||||
assert.equal(config.match(/client_max_body_size 55m;/g)?.length, 2)
|
||||
})
|
||||
|
||||
test('does not cache the SPA entry while keeping hashed assets immutable', () => {
|
||||
assert.match(config, /location = \/index\.html[\s\S]*no-store, no-cache, must-revalidate/)
|
||||
assert.match(config, /location \/assets\/[\s\S]*max-age=31536000, immutable/)
|
||||
assert.match(config, /location \/ \{[\s\S]*no-store, no-cache, must-revalidate[\s\S]*try_files \$uri \$uri\/ \/index\.html/)
|
||||
})
|
||||
|
||||
test('routes domestic and overseas browser APIs to their test gateways without duplicate api prefixes', () => {
|
||||
assert.match(testCompose, /API_UPSTREAM: "http:\/\/test-api-cn\.buggoglobal\.com"/)
|
||||
assert.match(testCompose, /OVERSEAS_API_UPSTREAM: "http:\/\/test-api-global\.buggoglobal\.com"/)
|
||||
|
||||
@@ -223,15 +223,6 @@ export const useAuthStore = defineStore('client-auth', () => {
|
||||
}
|
||||
|
||||
window.addEventListener('cc-client-session-expired', clearSession)
|
||||
window.addEventListener('message', (event: MessageEvent<unknown>) => {
|
||||
const configuredAdminOrigin = String(import.meta.env.VITE_ADMIN_WEB_ORIGIN || '').trim()
|
||||
const expectedAdminOrigin = configuredAdminOrigin || window.location.origin
|
||||
const message = event.data as { source?: string; type?: string; code?: string } | null
|
||||
if (event.source !== window.opener || event.origin !== expectedAdminOrigin || message?.source !== 'chongchong-admin' || message.type !== 'impersonation-ticket' || !message.code) return
|
||||
void acceptImpersonationCode(message.code)
|
||||
.then(() => window.location.replace('/'))
|
||||
.catch(() => window.location.replace('/login?impersonation=failed'))
|
||||
})
|
||||
window.addEventListener('storage', (event) => {
|
||||
if (event.key === 'cc-client-access-token' || event.key === 'cc-client-refresh-token') applyStoredSession()
|
||||
})
|
||||
|
||||
@@ -13,6 +13,8 @@ const httpSource = readFileSync(new URL('../../../api/http.ts', import.meta.url)
|
||||
const sessionSource = readFileSync(new URL('../../../api/session.ts', import.meta.url), 'utf8')
|
||||
const authApiSource = readFileSync(new URL('../api/auth.ts', import.meta.url), 'utf8')
|
||||
const mainSource = readFileSync(new URL('../../../main.ts', import.meta.url), 'utf8')
|
||||
const appSource = readFileSync(new URL('../../../App.vue', import.meta.url), 'utf8')
|
||||
const routerSource = readFileSync(new URL('../../../router/index.ts', import.meta.url), 'utf8')
|
||||
const stylesSource = readFileSync(new URL('../../../styles/index.css', import.meta.url), 'utf8')
|
||||
|
||||
test('uses one prototype-inspired responsive shell for login and registration', () => {
|
||||
@@ -105,7 +107,12 @@ test('uses real auth endpoints and single-flight refresh handling', () => {
|
||||
|
||||
test('handles impersonation exchange as public authentication and exposes failures', () => {
|
||||
assert.match(httpSource, /publicAuthPaths = \[[^\]]*'\/auth\/impersonations\/exchange'/)
|
||||
assert.match(authStoreSource, /\/login\?impersonation=failed/)
|
||||
assert.match(appSource, /impersonationOrigin/)
|
||||
assert.match(appSource, /await auth\.acceptImpersonationCode\(message\.code\)/)
|
||||
assert.match(appSource, /type: 'impersonation-complete'/)
|
||||
assert.match(appSource, /type: 'impersonation-failed'/)
|
||||
assert.doesNotMatch(authStoreSource, /message\.type !== 'impersonation-ticket'/)
|
||||
assert.match(routerSource, /isImpersonationHandoff[\s\S]*!isImpersonationHandoff/)
|
||||
assert.match(loginSource, /route\.query\.impersonation === 'failed'/)
|
||||
assert.match(loginSource, /代登录失败或授权已失效,请返回管理后台重新发起/)
|
||||
})
|
||||
|
||||
+2
-1
@@ -32,7 +32,8 @@ router.beforeEach(async (to) => {
|
||||
if (to.meta.requiresAuth && !auth.isAuthenticated) {
|
||||
return { name: 'login', query: { redirect: to.fullPath } }
|
||||
}
|
||||
if ((to.name === 'login' || to.name === 'register') && auth.isAuthenticated) {
|
||||
const isImpersonationHandoff = to.name === 'login' && typeof to.query.impersonationOrigin === 'string'
|
||||
if ((to.name === 'login' || to.name === 'register') && auth.isAuthenticated && !isImpersonationHandoff) {
|
||||
return getFirstAccessiblePath(auth.permissionCodes)
|
||||
}
|
||||
const permission = typeof to.meta.permission === 'string' ? to.meta.permission : undefined
|
||||
|
||||
Reference in New Issue
Block a user