79 lines
3.2 KiB
TypeScript
79 lines
3.2 KiB
TypeScript
export const ACCESS_TOKEN_KEY = 'cc-client-access-token'
|
|
export const REFRESH_TOKEN_KEY = 'cc-client-refresh-token'
|
|
export const REMEMBERED_LOGIN_ID_KEY = 'cc-client-remembered-login-id'
|
|
|
|
export interface TokenPair {
|
|
tokenType: string
|
|
accessToken: string
|
|
expiresIn: number
|
|
refreshToken: string
|
|
refreshExpiresIn: number
|
|
}
|
|
|
|
|
|
export function getAccessToken() {
|
|
return localStorage.getItem(ACCESS_TOKEN_KEY) || sessionStorage.getItem(ACCESS_TOKEN_KEY) || ''
|
|
}
|
|
|
|
export function getRefreshToken() {
|
|
return localStorage.getItem(REFRESH_TOKEN_KEY) || sessionStorage.getItem(REFRESH_TOKEN_KEY) || ''
|
|
}
|
|
|
|
// Only binds a retry to its original identity; never grants permissions. Expired
|
|
// access tokens remain readable here because they are the normal refresh trigger.
|
|
export function accessTokenIdentity(token: string): string | null {
|
|
try {
|
|
const encoded = token.split('.')[1]
|
|
if (!encoded) return null
|
|
const normalized = encoded.replace(/-/g, '+').replace(/_/g, '/')
|
|
const bytes = Uint8Array.from(atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=')), character => character.charCodeAt(0))
|
|
const json = new TextDecoder().decode(bytes)
|
|
// Match whole JSON strings first so digits inside arbitrary string values
|
|
// are never rewritten. Preserve integer identity claims before JSON.parse
|
|
// can round them; number/string representations then share one identity.
|
|
const exactIdentityJson = json.replace(/"(?:\\.|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g, (part, offset: number) => {
|
|
if (!/^-?\d+$/.test(part)) return part
|
|
const key = json.slice(0, offset).match(/"(user_id|tenant_id|membership_id|operator_tenant_id)"\s*:\s*$/)
|
|
return key ? JSON.stringify(part) : part
|
|
})
|
|
const value = JSON.parse(exactIdentityJson)
|
|
if (!value?.user_id || !value.tenant_id || !value.membership_id || value.app_code !== 'MERCHANT') return null
|
|
return JSON.stringify([value.user_id, value.tenant_id, value.membership_id, value.operator_tenant_id, value.app_code, value.client_type].map(item => item == null ? null : String(item)))
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
|
|
export function persistTokenPair(tokens: TokenPair) {
|
|
// Replace the pair without publishing a transient logout to other tabs.
|
|
sessionStorage.removeItem(ACCESS_TOKEN_KEY)
|
|
sessionStorage.removeItem(REFRESH_TOKEN_KEY)
|
|
sessionStorage.removeItem('cc-client-token')
|
|
localStorage.removeItem('cc-client-token')
|
|
localStorage.setItem(ACCESS_TOKEN_KEY, tokens.accessToken)
|
|
localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken)
|
|
}
|
|
|
|
export function getRememberedLoginId() {
|
|
return localStorage.getItem(REMEMBERED_LOGIN_ID_KEY) || ''
|
|
}
|
|
|
|
export function persistRememberedLoginId(loginId: string, remember: boolean) {
|
|
if (remember) localStorage.setItem(REMEMBERED_LOGIN_ID_KEY, loginId)
|
|
else localStorage.removeItem(REMEMBERED_LOGIN_ID_KEY)
|
|
}
|
|
|
|
export function persistTemporaryAccessToken(accessToken: string) {
|
|
clearSessionTokens()
|
|
sessionStorage.setItem(ACCESS_TOKEN_KEY, accessToken)
|
|
}
|
|
|
|
export function clearSessionTokens() {
|
|
for (const storage of [localStorage, sessionStorage]) {
|
|
storage.removeItem(ACCESS_TOKEN_KEY)
|
|
storage.removeItem(REFRESH_TOKEN_KEY)
|
|
storage.removeItem('cc-client-token')
|
|
}
|
|
}
|