优化
This commit is contained in:
@@ -11,12 +11,7 @@ services:
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${HOST_PORT:-9001}:80"
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
CLIENT_WEB_URL: "${CLIENT_WEB_URL:-http://8.163.30.128:9002}"
|
||||
# Gateway 与前端容器在同一台测试服务器,通过宿主机内部地址访问。
|
||||
# 测试环境固定走 Docker 宿主机网关,避免 Jenkins 全局环境变量或
|
||||
# 工作区残留配置把上游覆盖成公网 IP,造成容器回连公网端口失败。
|
||||
API_UPSTREAM: "http://host.docker.internal:9000"
|
||||
OVERSEAS_API_UPSTREAM: "http://host.docker.internal:9300"
|
||||
CLIENT_WEB_URL: "${CLIENT_WEB_URL:-}"
|
||||
API_UPSTREAM: "http://test-api-cn.buggoglobal.com"
|
||||
OVERSEAS_API_UPSTREAM: "http://test-api-global.buggoglobal.com"
|
||||
|
||||
@@ -24,7 +24,7 @@ server {
|
||||
client_max_body_size 12m;
|
||||
proxy_pass ${API_UPSTREAM};
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Host $proxy_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
@@ -36,7 +36,7 @@ server {
|
||||
rewrite ^/overseas-api/(.*)$ /api/$1 break;
|
||||
proxy_pass ${OVERSEAS_API_UPSTREAM};
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Host $proxy_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
post: vi.fn(),
|
||||
@@ -89,6 +89,8 @@ describe('admin auth session', () => {
|
||||
mocks.refreshHandler = null
|
||||
})
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
|
||||
it('accepts a domestic-only token response when the test environment has no overseas services', async () => {
|
||||
mocks.post.mockResolvedValue(domesticOnlyLoginResponse())
|
||||
mocks.listCurrentPlatformPermissionCodes.mockResolvedValue(['platform:warehouse:list'])
|
||||
@@ -203,6 +205,32 @@ describe('admin auth session', () => {
|
||||
.toBe('["platform:warehouse:list","platform:firstleg:inbound:list"]')
|
||||
})
|
||||
|
||||
it('reuses a session rotated by another tab without refreshing it again', async () => {
|
||||
const previousToken = token({ token_version: 1 })
|
||||
const rotatedToken = token({ token_version: 2 })
|
||||
localStorage.setItem('cc-admin-token', previousToken)
|
||||
localStorage.setItem('cc-admin-refresh-token', 'refresh-old')
|
||||
localStorage.setItem('cc-admin-permission-codes', '["platform:warehouse:list"]')
|
||||
vi.stubGlobal('navigator', {
|
||||
locks: {
|
||||
request: vi.fn(async (_name: string, _options: LockOptions, callback: () => Promise<void>) => {
|
||||
localStorage.setItem('cc-admin-token', rotatedToken)
|
||||
localStorage.setItem('cc-admin-refresh-token', 'refresh-rotated')
|
||||
localStorage.setItem('cc-admin-permission-codes', '["platform:firstleg:inbound:list"]')
|
||||
return callback()
|
||||
}),
|
||||
},
|
||||
})
|
||||
const auth = useAuthStore()
|
||||
|
||||
await expect(auth.refreshSession()).resolves.toBeUndefined()
|
||||
|
||||
expect(mocks.post).not.toHaveBeenCalled()
|
||||
expect(auth.token).toBe(rotatedToken)
|
||||
expect(auth.refreshToken).toBe('refresh-rotated')
|
||||
expect([...auth.permissionCodes]).toEqual(['platform:firstleg:inbound:list'])
|
||||
})
|
||||
|
||||
it('does not let an in-flight refresh restore a session after logout', async () => {
|
||||
localStorage.setItem('cc-admin-token', token())
|
||||
localStorage.setItem('cc-admin-refresh-token', 'refresh-old')
|
||||
|
||||
@@ -203,14 +203,9 @@ export const useAuthStore = defineStore('admin-auth', () => {
|
||||
observedRefreshToken,
|
||||
currentRefreshToken: () => localStorage.getItem(REFRESH_TOKEN_KEY) || '',
|
||||
reuseCurrentSession: async () => {
|
||||
const storedAccessToken = localStorage.getItem(TOKEN_KEY) || ''
|
||||
const storedRefreshToken = localStorage.getItem(REFRESH_TOKEN_KEY) || ''
|
||||
if (!storedAccessToken || !storedRefreshToken) throw new Error('登录状态已过期')
|
||||
const response = await http.post<ApiEnvelope<LoginResponse>, ApiEnvelope<LoginResponse>>(
|
||||
'/auth/refresh',
|
||||
{ refreshToken: storedRefreshToken },
|
||||
)
|
||||
await applySession(unwrapApiResponse<LoginResponse>(response), expectedGeneration)
|
||||
applyStoredSession()
|
||||
ensureCurrentGeneration(expectedGeneration)
|
||||
if (!token.value || !refreshToken.value) throw new Error('登录状态已过期')
|
||||
},
|
||||
refresh: async (currentRefreshToken) => {
|
||||
const response = await http.post<ApiEnvelope<LoginResponse>, ApiEnvelope<LoginResponse>>(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { coordinateCrossTabRefresh } from './crossTabRefresh'
|
||||
import { coordinateCrossTabRefresh, CrossTabRefreshTimeoutError } from './crossTabRefresh'
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
|
||||
@@ -10,7 +10,7 @@ describe('cross-tab refresh coordination', () => {
|
||||
const reuseCurrentSession = vi.fn(async () => 'access-new')
|
||||
vi.stubGlobal('navigator', {
|
||||
locks: {
|
||||
request: vi.fn(async (_name: string, callback: () => Promise<string>) => {
|
||||
request: vi.fn(async (_name: string, _options: LockOptions, callback: () => Promise<string>) => {
|
||||
currentRefreshToken = 'refresh-new'
|
||||
return callback()
|
||||
}),
|
||||
@@ -32,7 +32,7 @@ describe('cross-tab refresh coordination', () => {
|
||||
it('performs the refresh when the token has not changed', async () => {
|
||||
const refresh = vi.fn(async () => 'access-new')
|
||||
vi.stubGlobal('navigator', {
|
||||
locks: { request: vi.fn(async (_name: string, callback: () => Promise<string>) => callback()) },
|
||||
locks: { request: vi.fn(async (_name: string, _options: LockOptions, callback: () => Promise<string>) => callback()) },
|
||||
})
|
||||
|
||||
await expect(coordinateCrossTabRefresh({
|
||||
@@ -61,4 +61,23 @@ describe('cross-tab refresh coordination', () => {
|
||||
|
||||
expect(refresh).toHaveBeenCalledWith('refresh-old')
|
||||
})
|
||||
|
||||
it('aborts a refresh lock that cannot be acquired', async () => {
|
||||
vi.stubGlobal('navigator', {
|
||||
locks: {
|
||||
request: vi.fn((_name: string, options: LockOptions) => new Promise((_resolve, reject) => {
|
||||
options.signal?.addEventListener('abort', () => reject(options.signal?.reason), { once: true })
|
||||
})),
|
||||
},
|
||||
})
|
||||
|
||||
await expect(coordinateCrossTabRefresh({
|
||||
lockName: 'admin-refresh',
|
||||
lockTimeoutMs: 5,
|
||||
observedRefreshToken: 'refresh-old',
|
||||
currentRefreshToken: () => 'refresh-old',
|
||||
reuseCurrentSession: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
})).rejects.toBeInstanceOf(CrossTabRefreshTimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
export interface CrossTabRefreshOptions<T> {
|
||||
lockName: string
|
||||
lockTimeoutMs?: number
|
||||
observedRefreshToken: string
|
||||
currentRefreshToken: () => string
|
||||
reuseCurrentSession: () => Promise<T>
|
||||
refresh: (refreshToken: string) => Promise<T>
|
||||
}
|
||||
|
||||
export class CrossTabRefreshTimeoutError extends Error {
|
||||
constructor() {
|
||||
super('登录状态刷新等待超时,请重新登录')
|
||||
this.name = 'CrossTabRefreshTimeoutError'
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_LOCK_TIMEOUT_MS = 20_000
|
||||
|
||||
export async function coordinateCrossTabRefresh<T>(options: CrossTabRefreshOptions<T>): Promise<T> {
|
||||
const run = async () => {
|
||||
const currentRefreshToken = options.currentRefreshToken()
|
||||
@@ -33,5 +43,14 @@ export async function coordinateCrossTabRefresh<T>(options: CrossTabRefreshOptio
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return navigator.locks.request(options.lockName, run)
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS)
|
||||
try {
|
||||
return await navigator.locks.request(options.lockName, { signal: controller.signal }, run)
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) throw new CrossTabRefreshTimeoutError()
|
||||
throw error
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,10 @@ describe('admin nginx deployment config', () => {
|
||||
})
|
||||
|
||||
it('declares an explicit overseas gateway upstream in container deployments', () => {
|
||||
expect(testCompose).toContain('OVERSEAS_API_UPSTREAM: "http://host.docker.internal:9300"')
|
||||
expect(developmentCompose).toContain('OVERSEAS_API_UPSTREAM: "http://host.docker.internal:9300"')
|
||||
expect(testCompose).toContain('API_UPSTREAM: "http://test-api-cn.buggoglobal.com"')
|
||||
expect(testCompose).toContain('OVERSEAS_API_UPSTREAM: "http://test-api-global.buggoglobal.com"')
|
||||
expect(developmentCompose).toContain('OVERSEAS_API_UPSTREAM: "http://firstleg-development-gateway-overseas:9300"')
|
||||
expect(config).toContain('proxy_set_header Host $proxy_host;')
|
||||
})
|
||||
|
||||
it('runs each deployment on its target node without controller-local SSH files', () => {
|
||||
|
||||
Reference in New Issue
Block a user