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 }