test: harden overseas pda token session
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { API_ROOT } from '@/config/runtime.js'
|
||||
import { API_ROOT } from '../config/runtime.js'
|
||||
|
||||
function authRequest(path, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
+24
-10
@@ -1,5 +1,5 @@
|
||||
import { refresh } from '@/api/auth.js'
|
||||
import { session, replaceTokens } from '@/stores/session.js'
|
||||
import { refresh } from './auth.js'
|
||||
import { session, replaceTokens, signOut } from '../stores/session.js'
|
||||
|
||||
let refreshPromise = null
|
||||
|
||||
@@ -22,14 +22,24 @@ function successful(response, body) {
|
||||
}
|
||||
|
||||
async function refreshOverseasToken() {
|
||||
if (!session.refreshToken) throw new Error('登录状态已过期,请重新登录')
|
||||
if (!session.refreshToken) {
|
||||
signOut()
|
||||
throw new Error('登录状态已过期,请重新登录')
|
||||
}
|
||||
if (!refreshPromise) {
|
||||
refreshPromise = refresh(session.refreshToken).then((tokens) => { replaceTokens(tokens); return session.token })
|
||||
refreshPromise = refresh(session.refreshToken)
|
||||
.then((tokens) => { replaceTokens(tokens); return session.token })
|
||||
.catch((error) => { signOut(); throw error })
|
||||
.finally(() => { refreshPromise = null })
|
||||
}
|
||||
return refreshPromise
|
||||
}
|
||||
|
||||
async function tokenAfterUnauthorized(failedToken) {
|
||||
if (session.token && session.token !== failedToken) return session.token
|
||||
return refreshOverseasToken()
|
||||
}
|
||||
|
||||
function rawRequest(method, path, data, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
@@ -44,10 +54,11 @@ function rawRequest(method, path, data, token) {
|
||||
}
|
||||
|
||||
export async function overseasRequest(method, path, data, retry = true) {
|
||||
let response = await rawRequest(method, path, data, session.token)
|
||||
const attemptedToken = session.token
|
||||
let response = await rawRequest(method, path, data, attemptedToken)
|
||||
if (retry && unauthorized(response)) {
|
||||
await refreshOverseasToken()
|
||||
response = await rawRequest(method, path, data, session.token)
|
||||
const replacementToken = await tokenAfterUnauthorized(attemptedToken)
|
||||
response = await rawRequest(method, path, data, replacementToken)
|
||||
}
|
||||
const body = response?.data || {}
|
||||
if (!successful(response, body)) throw errorFrom(response, '海外作业请求失败')
|
||||
@@ -68,14 +79,17 @@ function rawUpload(path, filePath, name, token) {
|
||||
}
|
||||
|
||||
export async function overseasUpload(path, filePath, name = 'file', retry = true) {
|
||||
let response = await rawUpload(path, filePath, name, session.token)
|
||||
const attemptedToken = session.token
|
||||
let response = await rawUpload(path, filePath, name, attemptedToken)
|
||||
let body = null
|
||||
try { body = JSON.parse(response.data || '{}') } catch (_) { /* handled below */ }
|
||||
if (retry && unauthorized({ ...response, data: body })) {
|
||||
await refreshOverseasToken()
|
||||
response = await rawUpload(path, filePath, name, session.token)
|
||||
const replacementToken = await tokenAfterUnauthorized(attemptedToken)
|
||||
response = await rawUpload(path, filePath, name, replacementToken)
|
||||
try { body = JSON.parse(response.data || '{}') } catch (_) { body = null }
|
||||
}
|
||||
if (!successful(response, body)) throw errorFrom({ ...response, data: body }, '卸柜照片上传失败')
|
||||
return body.data
|
||||
}
|
||||
|
||||
export function resetOverseasRequestStateForTest() { refreshPromise = null }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const configuredAuthApiRoot = import.meta.env.VITE_AUTH_API_BASE_URL || ''
|
||||
const configuredOverseasApiRoot = import.meta.env.VITE_OVERSEAS_API_BASE_URL || ''
|
||||
const runtimeEnvironment = import.meta.env || {}
|
||||
const configuredAuthApiRoot = runtimeEnvironment.VITE_AUTH_API_BASE_URL || ''
|
||||
const configuredOverseasApiRoot = runtimeEnvironment.VITE_OVERSEAS_API_BASE_URL || ''
|
||||
|
||||
export const API_ROOT = String(configuredAuthApiRoot || '/api').replace(/\/$/, '')
|
||||
export const OVERSEAS_API_ROOT = String(configuredOverseasApiRoot || '/api/overseas').replace(/\/$/, '')
|
||||
|
||||
+76
-21
@@ -1,7 +1,9 @@
|
||||
import { reactive } from 'vue'
|
||||
import { OVERSEAS_API_ROOT } from '@/config/runtime.js'
|
||||
import { OVERSEAS_API_ROOT } from '../config/runtime.js'
|
||||
|
||||
const SESSION_KEY = 'overseas-pda-auth-session'
|
||||
export const PDA_ROLE = 'WAREHOUSE_PICKER'
|
||||
export const PDA_AUTHORITY = 'lastleg:pda:operate'
|
||||
|
||||
export const session = reactive({ currentUser: null, token: '', domesticToken: '', refreshToken: '', tenantId: '', apiBase: OVERSEAS_API_ROOT })
|
||||
|
||||
@@ -16,13 +18,64 @@ export function decodeAccessToken(token) {
|
||||
const payload = String(token || '').split('.')[1]
|
||||
if (!payload) throw new Error('登录响应中的 accessToken 格式无效')
|
||||
try {
|
||||
return JSON.parse(decodeBase64Url(payload))
|
||||
const decoded = decodeBase64Url(payload)
|
||||
// Preserve snowflake IDs before JSON.parse rounds unquoted JWT numbers beyond Number.MAX_SAFE_INTEGER.
|
||||
const safeLongClaims = decoded.replace(
|
||||
/("(?:user_id|membership_id|tenant_id|overseas_warehouse_id)"\s*:\s*)(-?\d{16,})(?=\s*[,}])/g,
|
||||
'$1"$2"'
|
||||
)
|
||||
return JSON.parse(safeLongClaims)
|
||||
} catch (error) {
|
||||
if (error.message === '当前运行环境无法解析登录令牌') throw error
|
||||
throw new Error('登录响应中的 accessToken 无法解析')
|
||||
}
|
||||
}
|
||||
|
||||
function claimId(claims, name) {
|
||||
const value = claims?.[name]
|
||||
return value === null || value === undefined ? '' : String(value)
|
||||
}
|
||||
|
||||
function includesClaim(values, expected) {
|
||||
return Array.isArray(values) && values.map(String).includes(expected)
|
||||
}
|
||||
|
||||
function requireActiveClaims(claims, label) {
|
||||
if (claims.exp && Number(claims.exp) * 1000 <= Date.now()) throw new Error(`${label}已过期`)
|
||||
if (claims.app_code !== 'PLATFORM' || claimId(claims, 'tenant_id') !== '0') {
|
||||
throw new Error('当前账号不属于平台仓库作业端')
|
||||
}
|
||||
if (!claimId(claims, 'user_id') || !claimId(claims, 'membership_id')) {
|
||||
throw new Error(`${label}缺少完整作业主体`)
|
||||
}
|
||||
}
|
||||
|
||||
export function validatePdaTokenPair(accessToken, overseasAccessToken) {
|
||||
const domestic = decodeAccessToken(accessToken)
|
||||
const overseas = decodeAccessToken(overseasAccessToken)
|
||||
requireActiveClaims(domestic, '登录状态')
|
||||
requireActiveClaims(overseas, '海外作业令牌')
|
||||
for (const claim of ['user_id', 'membership_id', 'tenant_id', 'app_code', 'overseas_warehouse_id']) {
|
||||
if (claimId(domestic, claim) !== claimId(overseas, claim)) throw new Error('国内与海外令牌作业主体不一致')
|
||||
}
|
||||
const warehouseId = claimId(overseas, 'overseas_warehouse_id')
|
||||
if (!/^\d+$/.test(warehouseId) || warehouseId === '0') throw new Error('海外PDA账号未绑定有效作业仓库')
|
||||
if (!includesClaim(overseas.roles, PDA_ROLE)) throw new Error('当前账号不是海外仓库拣货员')
|
||||
if (!includesClaim(overseas.authorities, PDA_AUTHORITY)) throw new Error(`当前账号缺少${PDA_AUTHORITY}权限`)
|
||||
return { domestic, overseas, warehouseId }
|
||||
}
|
||||
|
||||
function userFromClaims(pair) {
|
||||
return {
|
||||
id: claimId(pair.domestic, 'user_id'),
|
||||
name: pair.domestic.username || '仓库作业员',
|
||||
role: PDA_ROLE,
|
||||
roles: [...pair.overseas.roles],
|
||||
warehouse: `海外仓 #${pair.warehouseId}`,
|
||||
warehouseId: pair.warehouseId
|
||||
}
|
||||
}
|
||||
|
||||
function storageAvailable() {
|
||||
return typeof uni !== 'undefined' && typeof uni.setStorageSync === 'function' && typeof uni.getStorageSync === 'function'
|
||||
}
|
||||
@@ -53,44 +106,46 @@ export function signIn(account, warehouse, options = {}) {
|
||||
export function signInWithTokens(tokens) {
|
||||
if (!tokens?.accessToken) throw new Error('登录响应缺少 accessToken')
|
||||
if (!tokens?.overseasAccessToken) throw new Error('登录响应缺少 overseasAccessToken')
|
||||
const claims = decodeAccessToken(tokens.accessToken)
|
||||
const overseasClaims = decodeAccessToken(tokens.overseasAccessToken)
|
||||
if (claims.app_code !== 'PLATFORM') throw new Error('当前账号不属于平台作业端')
|
||||
if (claims.exp && claims.exp * 1000 <= Date.now()) throw new Error('登录状态已过期')
|
||||
if (overseasClaims.exp && overseasClaims.exp * 1000 <= Date.now()) throw new Error('海外作业令牌已过期')
|
||||
return signIn({
|
||||
id: claims.user_id ?? '',
|
||||
name: claims.username || '仓库作业员',
|
||||
role: Array.isArray(claims.roles) ? claims.roles.join('、') : '',
|
||||
roles: claims.roles || []
|
||||
}, '', {
|
||||
const pair = validatePdaTokenPair(tokens.accessToken, tokens.overseasAccessToken)
|
||||
const user = userFromClaims(pair)
|
||||
return signIn(user, user.warehouse, {
|
||||
token: tokens.overseasAccessToken,
|
||||
domesticToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken || '',
|
||||
tenantId: claims.tenant_id == null ? '' : String(claims.tenant_id)
|
||||
tenantId: claimId(pair.domestic, 'tenant_id')
|
||||
})
|
||||
}
|
||||
|
||||
export function replaceTokens(tokens) {
|
||||
if (!tokens?.accessToken || !tokens?.overseasAccessToken) throw new Error('刷新响应缺少双区域访问令牌')
|
||||
decodeAccessToken(tokens.accessToken)
|
||||
decodeAccessToken(tokens.overseasAccessToken)
|
||||
const pair = validatePdaTokenPair(tokens.accessToken, tokens.overseasAccessToken)
|
||||
const user = userFromClaims(pair)
|
||||
const nextRefreshToken = tokens.refreshToken || session.refreshToken
|
||||
// Validate the complete replacement before mutating reactive or persisted session state.
|
||||
session.domesticToken = tokens.accessToken
|
||||
session.token = tokens.overseasAccessToken
|
||||
session.refreshToken = tokens.refreshToken || session.refreshToken
|
||||
session.refreshToken = nextRefreshToken
|
||||
session.tenantId = claimId(pair.domestic, 'tenant_id')
|
||||
session.currentUser = user
|
||||
persistSession()
|
||||
}
|
||||
|
||||
export function restoreSession() {
|
||||
if (!storageAvailable()) return false
|
||||
const saved = uni.getStorageSync(SESSION_KEY)
|
||||
if (!saved?.token || !saved?.currentUser) return false
|
||||
if (!saved?.token || !saved?.domesticToken || !saved?.refreshToken || !saved?.currentUser) {
|
||||
signOut()
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const claims = decodeAccessToken(saved.token)
|
||||
if (claims.exp && claims.exp * 1000 <= Date.now()) return false
|
||||
Object.assign(session, saved, { apiBase: saved.apiBase || OVERSEAS_API_ROOT })
|
||||
const pair = validatePdaTokenPair(saved.domesticToken, saved.token)
|
||||
Object.assign(session, saved, {
|
||||
currentUser: userFromClaims(pair), tenantId: claimId(pair.domestic, 'tenant_id'),
|
||||
apiBase: saved.apiBase || OVERSEAS_API_ROOT
|
||||
})
|
||||
return true
|
||||
} catch (_) {
|
||||
signOut()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user