From 5c53eba31b32e65532c1c0bfe0fe36a808e76a45 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 3 Oct 2026 03:54:06 +0800 Subject: [PATCH] fix(client): clear protected views on logout and show customer customs state --- src/api/http.ts | 2 +- src/api/unifiedHttp.test.mjs | 17 +++++++ src/layouts/AppLayout.test.mjs | 50 +++++++++++++++++- src/layouts/AppLayout.vue | 16 +++--- src/modules/auth/store/authSession.test.mjs | 18 ++++++- .../views/CustomsDocumentsView.test.mjs | 51 ++++++++++++++++++- .../first-leg/views/CustomsDocumentsView.vue | 16 +++--- 7 files changed, 149 insertions(+), 21 deletions(-) diff --git a/src/api/http.ts b/src/api/http.ts index 8b29363..50b2e5b 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -55,7 +55,7 @@ function mapError(error: unknown) { let refreshPromise: Promise | null = null let refreshIdentity: string | null = null let redirectingToLogin = false -const publicAuthPaths = ['/auth/login', '/auth/register', '/auth/refresh', '/auth/impersonations/exchange'] +const publicAuthPaths = ['/auth/login', '/auth/register', '/auth/refresh', '/auth/logout', '/auth/impersonations/exchange'] function isPublicAuthRequest(url?: string) { return publicAuthPaths.some((path) => url?.endsWith(path)) diff --git a/src/api/unifiedHttp.test.mjs b/src/api/unifiedHttp.test.mjs index 2221c59..ed66ff4 100644 --- a/src/api/unifiedHttp.test.mjs +++ b/src/api/unifiedHttp.test.mjs @@ -99,6 +99,23 @@ test('late refresh response cannot overwrite a session changed in another tab', }) function deferred() { let resolve; const promise = new Promise(yes => { resolve = yes }); return { promise, resolve } } + +test('logout after local clear sends only the original refresh token and never refreshes or clears a later login', async () => { + const sent = deferred(), release = deferred(), calls = [] + const http = harness(async config => { + calls.push(config); sent.resolve(); await release.promise; throw unauthorized(config) + }) + http.setSession('', '') + const api = compile('../modules/auth/api/auth.ts', { '@/api/http': http }) + const pending = api.logoutMerchant('old-refresh') + const rejected = assert.rejects(pending, error => error.code === 'A0401') + await sent.promise; http.setSession(otherAccess, 'refresh-b'); release.resolve(); await rejected + assert.equal(calls.length, 1) + assert.equal(calls[0].url, '/auth/logout') + assert.equal(calls[0].headers.Authorization, undefined) + assert.deepEqual(JSON.parse(calls[0].data), { refreshToken: 'old-refresh' }) + assert.deepEqual(http.session(), { token: otherAccess, refreshToken: 'refresh-b' }) +}) function unauthorized(config) { return new axios.AxiosError('Expired', 'ERR_BAD_REQUEST', config, null, { status: 401, data: { code: 'A0401', message: 'Expired' } }) } diff --git a/src/layouts/AppLayout.test.mjs b/src/layouts/AppLayout.test.mjs index 35e8d25..272ee3e 100644 --- a/src/layouts/AppLayout.test.mjs +++ b/src/layouts/AppLayout.test.mjs @@ -1,6 +1,8 @@ import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' +import ts from 'typescript' +import * as vue from 'vue' const layoutSource = readFileSync(new URL('./AppLayout.vue', import.meta.url), 'utf8') const shellStyles = readFileSync(new URL('../styles/shell.css', import.meta.url), 'utf8') @@ -108,7 +110,7 @@ test('source contract: locks body scrolling while open and restores it on close }) test('source contract: handles confirmation cancellation but rethrows unexpected logout errors', () => { - assert.match(layoutSource, /async function handleLogout\(\) \{\s*try \{/) + assert.match(layoutSource, /async function handleLogout\(\) \{\s*const logoutToken = auth.token\s*try \{/) assert.match(layoutSource, /\} catch \(error\) \{\s*if \(error === 'cancel' \|\| error === 'close'\) return\s*throw error\s*\}/) }) @@ -148,3 +150,49 @@ test('source contract: routes denied pages to an explicit 403 state', () => { assert.match(routerSource, /!auth\.hasPermission\(permission\)/) assert.match(routerSource, /getFirstAccessiblePath\(auth\.permissionCodes\)/) }) + +function logoutHarness(t) { + const script = layoutSource.match(/