From c2e8526f8f857be5e3a8c6f2b39c5efd424de33e Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 15 十月 2026 11:50:24 +0800
Subject: [PATCH] refactor(auth): 优化认证逻辑和界面组件结构
---
scripts/check-logout.cjs | 69 +++++++++++++++++++++++++++++++++-
1 files changed, 67 insertions(+), 2 deletions(-)
diff --git a/scripts/check-logout.cjs b/scripts/check-logout.cjs
index 6b42309..6506f0f 100644
--- a/scripts/check-logout.cjs
+++ b/scripts/check-logout.cjs
@@ -62,7 +62,8 @@
'vue-router': { useRouter: () => ({ currentRoute: vue.ref({ fullPath: '/analytics?tab=1' }), replace: async (to) => navigations.push(to) }) },
'@vben/constants': { LOGIN_PATH: '/auth/login' }, '@vben/preferences': {},
'@vben/stores': { ...setup, ...accessModule, ...userModule }, 'ant-design-vue': {}, '#/locales': {},
- '#/api': { logoutApi: async (token) => { apiCalls.push(token); if (apiFails) throw new Error('offline'); } },
+ '#/store/audit-policy': { automaticApproval: vue.ref(false), loadAuditPolicy: async () => {} },
+ '#/api': { logoutApi: async (token) => { apiCalls.push(token); if (apiFails === 'pending') return new Promise(() => {}); if (apiFails) throw new Error('offline'); } },
}).useAuthStore();
const errors = [];
const originalError = console.error;
@@ -90,10 +91,74 @@
assert.equal(rtc.getGroupCall(1), undefined);
assert.equal(errors.length, resetFails ? 1 : 0);
}
+
+async function sessionFixture(api) {
+ const app=vue.createApp({});
+ const setup=load('src/packages/stores/src/setup.ts',{'secure-ls':class{},'pinia-plugin-persistedstate':{createPersistedState:()=>()=>{}}});
+ pinia.setActivePinia(await setup.initStores(app,{namespace:'logout-races'}));
+ const accessModule=load('src/packages/stores/src/modules/access.ts'),userModule=load('src/packages/stores/src/modules/user.ts');
+ const access=accessModule.useAccessStore(),user=userModule.useUserStore();const navigations=[];
+ access.setAccessToken('old-access');access.setRefreshToken('old-refresh');
+ const auth=load('src/store/auth.ts',{
+ 'vue-router':{useRouter:()=>({currentRoute:vue.ref({fullPath:'/private'}),replace:async to=>navigations.push(to),push:async to=>navigations.push(to)})},
+ '@vben/constants':{LOGIN_PATH:'/auth/login'},'@vben/preferences':{},
+ '@vben/stores':{...setup,...accessModule,...userModule},'ant-design-vue':{notification:{success(){throw Error('Stale success notification')}}},'#/locales':{$t:v=>v},
+ '#/store/audit-policy':{automaticApproval:vue.ref(false),loadAuditPolicy:async()=>{}},
+ '#/api':{logoutApi:async()=>{},...api},
+ }).useAuthStore();
+ return{access,user,auth,navigations};
+}
+function deferred(){let resolve,reject;const promise=new Promise((a,b)=>{resolve=a;reject=b});return{promise,resolve,reject};}
+async function pendingLoginCannotRestoreSession(){
+ const pending=deferred();const f=await sessionFixture({loginApi:()=>pending.promise});
+ const login=f.auth.authLogin('username',{});await f.auth.logout(false);
+ pending.resolve({accessToken:'late-access',refreshToken:'late-refresh'});await login;
+ assert.equal(f.access.accessToken,null);assert.equal(f.access.refreshToken,null);assert.equal(f.navigations.length,1);
+}
+async function pendingUserInfoCannotRestorePrivateData(){
+ const pending=deferred();const f=await sessionFixture({getAuthPermissionInfoApi:()=>pending.promise});
+ const info=f.auth.fetchUserInfo();const rejected=assert.rejects(info,/鐧诲綍鐘舵�佸凡鍙樺寲/);
+ await f.auth.logout(false);pending.resolve({user:{nickname:'old-user'},roles:['old-role'],menus:[],permissions:['old-permission']});await rejected;
+ assert.equal(f.user.userInfo,null);assert.deepEqual(f.access.accessCodes,[]);
+}
+async function requestRaces(){
+ const f=await sessionFixture({});const pending=deferred();let hooks;
+ class Client{responses=[];requests=[];isRefreshing=false;refreshTokenQueue=[];addRequestInterceptor(x){this.requests.push(x)}addResponseInterceptor(x){this.responses.push(x)}}
+ const presets=load('src/packages/effects/request/src/request-client/preset-interceptors.ts',{
+ '../../../../locales/src':{$t:v=>v},'../../../../utils/src':{isFunction:v=>typeof v==='function'},axios:{isCancel:()=>false},
+ });
+ const request=load('src/api/request.ts',{
+ '@vben/hooks':{isTenantEnable:()=>true,useAppConfig:()=>({apiURL:'/mock'})},
+ '@vben/preferences':{preferences:{app:{enableRefreshToken:true,loginExpiredMode:'page'}}},
+ '@vben/request':{RequestClient:Client,defaultResponseInterceptor:()=>({}),errorMessageResponseInterceptor:()=>({}),authenticateResponseInterceptor:options=>{hooks=options;return presets.authenticateResponseInterceptor(options)}},
+ '@vben/stores':{useAccessStore:()=>f.access},'@vben/utils':{createApiEncrypt:()=>({})},
+ 'ant-design-vue':{message:{}},'#/store':{useAuthStore:()=>f.auth},'./core':{refreshTokenApi:()=>pending.promise},
+ });
+ const refresh=hooks.doRefreshToken();const rejected=assert.rejects(refresh,/鐧诲綍鐘舵�佸凡鍙樺寲/);
+ await f.auth.logout(false);pending.resolve({data:{data:{accessToken:'late-refreshed'}}});await rejected;assert.equal(f.access.accessToken,null);
+ f.access.setAccessToken('new-account-token');f.access.setRefreshToken('new-account-refresh');
+ await hooks.doReAuthenticate({config:{headers:{Authorization:'Bearer old-access'}}});assert.equal(f.access.accessToken,'new-account-token');
+ const cfg={headers:{'tenant-id':7,'visit-tenant-id':9}};
+ await request.baseRequestClient.requests[0].fulfilled(cfg);assert.equal(cfg.headers['tenant-id'],7);assert.equal(cfg.headers['visit-tenant-id'],9);
+
+ const client={isRefreshing:false,refreshTokenQueue:[],request(){throw Error('Failed refresh must not replay queued requests')}};
+ const failed=deferred();let reauthError;const authError={config:{url:'/private',headers:{Authorization:'Bearer old'}},response:{status:401}};
+ const interceptor=presets.authenticateResponseInterceptor({client,doRefreshToken:()=>failed.promise,doReAuthenticate:async e=>{reauthError=e},enableRefreshToken:true,formatToken:t=>t});
+ const first=interceptor.rejected(authError),firstRejected=assert.rejects(first,/refresh failed/);
+ const second=interceptor.rejected({config:{url:'/other',headers:{}},response:{status:401}}),secondRejected=assert.rejects(second,/refresh failed/);
+ const originalError=console.error;console.error=()=>{};
+ try{failed.reject(new Error('refresh failed'));await Promise.all([firstRejected,secondRejected]);}finally{console.error=originalError;}
+ assert.equal(reauthError,authError);assert.equal(client.isRefreshing,false);assert.equal(client.refreshTokenQueue.length,0);
+}
+
(async () => {
await scenario(false, false, false);
await scenario(true, false, true);
await scenario(false, true, false);
await scenario(true, true, false);
- console.log('PASS: logout redirects and clears session with API success/failure and store reset failures; company and IM stores reset');
+ await scenario('pending', false, false);
+ await pendingLoginCannotRestoreSession();
+ await pendingUserInfoCannotRestorePrivateData();
+ await requestRaces();
+ console.log('PASS: pending APIs do not block logout, stale login/user/refresh cannot restore sessions, failed refresh queue rejects; logout redirects and clears session with API success/failure and store reset failures; company and IM stores reset');
})();
--
Gitblit v1.9.3