54 lines
2.4 KiB
JavaScript
54 lines
2.4 KiB
JavaScript
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
|
||
|
|
})
|
||
|
|
}
|