fix(auth): revoke overseas PDA session on user logout
This commit is contained in:
+19
-3
@@ -1,6 +1,8 @@
|
||||
import { API_ROOT } from '../config/runtime.js'
|
||||
|
||||
function authRequest(path, data) {
|
||||
const pendingRefreshes = new Map()
|
||||
|
||||
function authRequest(path, data, accessToken = '') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${API_ROOT}${path}`
|
||||
console.info('[Overseas PDA auth] request', { method: 'POST', url })
|
||||
@@ -8,6 +10,8 @@ function authRequest(path, data) {
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
header: { Accept: 'application/json', 'Content-Type': 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) },
|
||||
success(response) {
|
||||
const body = response.data || {}
|
||||
console.info('[Overseas PDA auth] response', { url, statusCode: response.statusCode, code: body.code,
|
||||
@@ -15,9 +19,10 @@ function authRequest(path, data) {
|
||||
contentType: response.header?.['Content-Type'] || response.header?.['content-type'],
|
||||
requestId: body.requestId || response.header?.['X-Request-Id'] || response.header?.['x-request-id'],
|
||||
traceId: body.traceId || response.header?.['X-Trace-Id'] || response.header?.['x-trace-id'] })
|
||||
if (String(body.code ?? response.statusCode) !== '200') {
|
||||
if (response.statusCode !== 200 || String(body.code) !== '200') {
|
||||
const error = new Error(body.message || '认证服务请求失败')
|
||||
error.code = body.code || response.statusCode
|
||||
error.statusCode = response.statusCode
|
||||
error.requestId = body.requestId || response.header?.['X-Request-Id'] || response.header?.['x-request-id']
|
||||
error.traceId = body.traceId || response.header?.['X-Trace-Id'] || response.header?.['x-trace-id']
|
||||
reject(error)
|
||||
@@ -40,5 +45,16 @@ export function login(loginId, password) {
|
||||
}
|
||||
|
||||
export function refresh(refreshToken) {
|
||||
return authRequest('/auth/refresh', { refreshToken })
|
||||
if (pendingRefreshes.has(refreshToken)) return pendingRefreshes.get(refreshToken)
|
||||
const pending = authRequest('/auth/refresh', { refreshToken }).finally(() => {
|
||||
if (pendingRefreshes.get(refreshToken) === pending) pendingRefreshes.delete(refreshToken)
|
||||
})
|
||||
pendingRefreshes.set(refreshToken, pending)
|
||||
return pending
|
||||
}
|
||||
|
||||
export function getPendingRefresh(refreshToken) { return pendingRefreshes.get(refreshToken) || null }
|
||||
|
||||
export function logout(accessToken, refreshToken) {
|
||||
return authRequest('/auth/logout', { refreshToken }, accessToken)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { logout, refresh, getPendingRefresh } from './auth.js'
|
||||
import { session, signOut, getSessionRevision, decodeAccessToken } from '../stores/session.js'
|
||||
|
||||
function replacementForCapturedSession(captured, tokens) {
|
||||
if (!tokens?.accessToken || typeof tokens.refreshToken !== 'string' || !tokens.refreshToken.trim()) {
|
||||
throw new Error('退出刷新响应缺少会话凭据')
|
||||
}
|
||||
const previous = decodeAccessToken(captured.accessToken)
|
||||
const replacement = decodeAccessToken(tokens.accessToken)
|
||||
if (!Number.isFinite(Number(replacement?.exp)) || Number(replacement.exp) * 1000 <= Date.now()) {
|
||||
throw new Error('退出刷新凭据无效或已过期')
|
||||
}
|
||||
for (const name of ['user_id', 'membership_id', 'tenant_id', 'app_code', 'overseas_warehouse_id']) {
|
||||
if (previous[name] == null || String(previous[name]) !== String(replacement[name])) {
|
||||
throw new Error('退出刷新返回了不同作业主体,无法确认原会话撤销')
|
||||
}
|
||||
}
|
||||
return { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken }
|
||||
}
|
||||
|
||||
// Active user logout only. Ordinary authentication failures continue to use local signOut.
|
||||
export function logoutCurrentSession() {
|
||||
const captured = {
|
||||
accessToken: session.token, refreshToken: session.refreshToken,
|
||||
pendingRefresh: getPendingRefresh(session.refreshToken)
|
||||
}
|
||||
signOut()
|
||||
const clearedRevision = getSessionRevision()
|
||||
const revoke = async () => {
|
||||
if (!captured.accessToken || !captured.refreshToken) return { revoked: false, reason: 'NO_CAPTURED_SESSION' }
|
||||
let credentials = captured
|
||||
let refreshed = false
|
||||
if (captured.pendingRefresh) {
|
||||
// Auth rotates access/refresh credentials. Revoke the captured refresh's result,
|
||||
// while the business refresh wrapper's revision guard prevents local restoration.
|
||||
refreshed = true
|
||||
credentials = replacementForCapturedSession(captured, await captured.pendingRefresh)
|
||||
}
|
||||
try {
|
||||
await logout(credentials.accessToken, credentials.refreshToken)
|
||||
} catch (error) {
|
||||
if (error.network || error.statusCode !== 401 || refreshed) throw error
|
||||
// Only an explicit expired-access HTTP 401 permits one refresh and one revoke.
|
||||
credentials = replacementForCapturedSession(captured, await refresh(captured.refreshToken))
|
||||
await logout(credentials.accessToken, credentials.refreshToken)
|
||||
}
|
||||
return { revoked: true }
|
||||
}
|
||||
return revoke().then(result => ({ ...result, clearedRevision }), error => {
|
||||
error.logoutRevision = clearedRevision
|
||||
throw error
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user