Files
pda-overseas-uniapp/src/stores/session.js
T
2026-09-29 18:47:52 +08:00

212 lines
8.4 KiB
JavaScript

import { reactive } from 'vue'
import { API_ROOT } from '../config/runtime.js'
import { resetReceivingSession } from './receiving.js'
import { resetFulfillmentSession } from './fulfillment.js'
import { resetWarehouseWork } from './warehouse-work.js'
const SESSION_KEY = 'overseas-pda-auth-session'
export const PDA_ROLE = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR'
export const PDA_AUTHORITY = 'lastleg:pda:operate'
export const session = reactive({
currentUser: null, token: '', refreshToken: '', tenantId: '', apiBase: API_ROOT,
authorization: { roles: [], authorities: [] }
})
function decodeBase64Url(value) {
const normalized = value.replace(/-/g, '+').replace(/_/g, '/')
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=')
if (typeof atob === 'function') return decodeURIComponent(Array.from(atob(padded), (char) => `%${char.charCodeAt(0).toString(16).padStart(2, '0')}`).join(''))
throw new Error('当前运行环境无法解析登录令牌')
}
export function decodeAccessToken(token) {
const payload = String(token || '').split('.')[1]
if (!payload) throw new Error('登录响应中的 accessToken 格式无效')
try {
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 (!Number.isFinite(Number(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}缺少完整作业主体`)
}
}
function requireAuthorizationSnapshot(authorization) {
if (!Array.isArray(authorization?.roles) || !Array.isArray(authorization?.authorities)) {
throw new Error('登录响应缺少完整权限快照')
}
const roles = authorization.roles.map(String)
const authorities = authorization.authorities.map(String)
if (!includesClaim(roles, PDA_ROLE)) throw new Error('当前账号不是海外仓 PDA 作业员')
if (!includesClaim(authorities, PDA_AUTHORITY)) throw new Error(`当前账号缺少${PDA_AUTHORITY}权限`)
return { roles, authorities }
}
export function validatePdaToken(accessToken, authorization) {
const claims = decodeAccessToken(accessToken)
requireActiveClaims(claims, '登录状态')
const warehouseId = claimId(claims, 'overseas_warehouse_id')
if (!/^\d+$/.test(warehouseId) || warehouseId === '0') throw new Error('海外PDA账号未绑定有效作业仓库')
return { claims, warehouseId, authorization: requireAuthorizationSnapshot(authorization) }
}
function userFromClaims(pair, saved = {}) {
const savedWarehouseId = String(saved?.warehouseId || '')
const sameWarehouse = savedWarehouseId === pair.warehouseId
return {
id: claimId(pair.claims, 'user_id'),
name: pair.claims.username || '仓库作业员',
role: PDA_ROLE,
roles: [...pair.authorization.roles],
warehouse: sameWarehouse && saved.warehouseName ? saved.warehouseName : '',
warehouseId: pair.warehouseId,
warehouseCode: sameWarehouse ? (saved.warehouseCode || '') : '',
warehouseName: sameWarehouse ? (saved.warehouseName || '') : ''
}
}
function storageAvailable() {
return typeof uni !== 'undefined' && typeof uni.setStorageSync === 'function' && typeof uni.getStorageSync === 'function'
}
function persistSession() {
if (!storageAvailable()) return
uni.setStorageSync(SESSION_KEY, {
version: 2,
currentUser: session.currentUser,
token: session.token,
refreshToken: session.refreshToken,
tenantId: session.tenantId,
apiBase: session.apiBase,
authorization: session.authorization
})
}
export function signIn(account, warehouse, options = {}) {
session.currentUser = { id: String(account.id), name: account.name, role: account.role, roles: account.roles || [], warehouse,
warehouseId: account.warehouseId || null, warehouseCode: account.warehouseCode || '', warehouseName: account.warehouseName || '' }
session.token = options.token || account.token || ''
session.refreshToken = options.refreshToken || account.refreshToken || ''
session.tenantId = options.tenantId || account.tenantId || ''
session.apiBase = options.apiBase || account.apiBase || API_ROOT
session.authorization = {
roles: [...(options.authorization?.roles || [])],
authorities: [...(options.authorization?.authorities || [])]
}
persistSession()
return session.currentUser
}
export function signInWithTokens(tokens) {
if (!tokens?.accessToken) throw new Error('登录响应缺少 accessToken')
const pair = validatePdaToken(tokens.accessToken, tokens)
const user = userFromClaims(pair)
return signIn(user, user.warehouse, {
token: tokens.accessToken,
refreshToken: tokens.refreshToken || '',
tenantId: claimId(pair.claims, 'tenant_id'),
authorization: pair.authorization
})
}
export function replaceTokens(tokens) {
if (!tokens?.accessToken) throw new Error('刷新响应缺少访问令牌')
const pair = validatePdaToken(tokens.accessToken, tokens)
if (session.currentUser) {
const previousClaims = decodeAccessToken(session.token)
const identityChanged = ['user_id', 'membership_id', 'tenant_id', 'app_code', 'overseas_warehouse_id']
.some((name) => claimId(previousClaims, name) !== claimId(pair.claims, name))
if (identityChanged || String(session.currentUser.id) !== claimId(pair.claims, 'user_id') || String(session.currentUser.warehouseId) !== pair.warehouseId) {
throw new Error('作业主体或绑定仓库已变更,请重新登录')
}
}
const user = userFromClaims(pair, session.currentUser)
const nextRefreshToken = tokens.refreshToken || session.refreshToken
// Validate the complete replacement before mutating reactive or persisted session state.
session.token = tokens.accessToken
session.refreshToken = nextRefreshToken
session.tenantId = claimId(pair.claims, 'tenant_id')
session.currentUser = user
session.authorization = pair.authorization
persistSession()
}
export function applyWarehouseProfile(profile) {
if (!session.currentUser) throw new Error('当前登录会话不存在')
const warehouseId = String(profile?.warehouseId || '')
if (!warehouseId || warehouseId !== String(session.currentUser.warehouseId)) throw new Error('仓库主档与当前登录仓库不一致')
session.currentUser = {
...session.currentUser,
warehouse: profile.warehouseName || '',
warehouseCode: String(profile.warehouseCode || ''),
warehouseName: String(profile.warehouseName || '')
}
persistSession()
return session.currentUser
}
export function restoreSession() {
if (!storageAvailable()) return false
const saved = uni.getStorageSync(SESSION_KEY)
if (!saved?.token || !saved?.refreshToken || !saved?.currentUser || saved.version !== 2) {
signOut()
return false
}
try {
const pair = validatePdaToken(saved.token, saved.authorization)
if (String(saved.currentUser.id) !== claimId(pair.claims, 'user_id') || String(saved.currentUser.warehouseId) !== pair.warehouseId) throw new Error('缓存作业主体不一致')
Object.assign(session, saved, {
currentUser: userFromClaims(pair, saved.currentUser), tenantId: claimId(pair.claims, 'tenant_id'),
apiBase: API_ROOT, authorization: pair.authorization
})
return true
} catch (_) {
signOut()
return false
}
}
export function signOut() {
session.currentUser = null
session.token = ''
session.refreshToken = ''
session.tenantId = ''
session.apiBase = API_ROOT
session.authorization = { roles: [], authorities: [] }
if (typeof uni !== 'undefined' && typeof uni.removeStorageSync === 'function') uni.removeStorageSync(SESSION_KEY)
resetReceivingSession()
resetFulfillmentSession()
resetWarehouseWork()
}
export function requireSession() {
if (session.currentUser || restoreSession()) return true
uni.reLaunch({ url: '/pages/login/login' })
return false
}