Files
rxminder/utils/token.ts
2025-09-23 10:11:14 -07:00

102 lines
2.0 KiB
TypeScript

import { logger } from '../services/logging';
export interface AuthTokens {
accessToken: string;
refreshToken?: string | null;
}
const TOKEN_STORAGE_KEY = 'meds_auth_tokens';
let memoryTokens: AuthTokens | null = null;
function getStorage(): Storage | null {
if (typeof window === 'undefined') {
return null;
}
try {
return window.localStorage;
} catch (error) {
// LocalStorage may be unavailable (e.g. Safari private mode); gracefully degrade.
logger.warn('Token storage fallback to memory', 'AUTH_TOKENS', error);
return null;
}
}
function persist(tokens: AuthTokens | null): void {
const storage = getStorage();
if (!storage) {
memoryTokens = tokens;
return;
}
if (!tokens) {
storage.removeItem(TOKEN_STORAGE_KEY);
memoryTokens = null;
return;
}
const payload: AuthTokens = {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken ?? null,
};
storage.setItem(TOKEN_STORAGE_KEY, JSON.stringify(payload));
memoryTokens = payload;
}
function readTokens(): AuthTokens | null {
const storage = getStorage();
if (!storage) {
return memoryTokens;
}
try {
const raw = storage.getItem(TOKEN_STORAGE_KEY);
if (!raw) {
memoryTokens = null;
return null;
}
const parsed = JSON.parse(raw) as AuthTokens;
memoryTokens = parsed;
return parsed;
} catch (error) {
logger.warn(
'Failed to parse stored tokens, clearing cache',
'AUTH_TOKENS',
error
);
storage.removeItem(TOKEN_STORAGE_KEY);
memoryTokens = null;
return null;
}
}
export const tokenStorage = {
save(tokens: AuthTokens): void {
if (!tokens || !tokens.accessToken) {
throw new Error('Token payload must include an access token');
}
persist(tokens);
},
getTokens(): AuthTokens | null {
return readTokens();
},
getAccessToken(): string | null {
const tokens = readTokens();
return tokens?.accessToken ?? null;
},
clear(): void {
persist(null);
},
};
export default tokenStorage;