This repository has been archived on 2026-07-21. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
cloud-reader/mobile/src/api/client.ts
T
crisleo94 22ded87250 feat: implement mobile reader
- implement mobile epub reader
2026-06-20 13:33:44 -05:00

170 lines
4.7 KiB
TypeScript

import axios, { type AxiosError, type InternalAxiosRequestConfig } from "axios";
import AsyncStorage from "@react-native-async-storage/async-storage";
import type { TokenResponse } from "@cloud-reader/shared";
const STORAGE_KEYS = {
ACCESS_TOKEN: "access_token",
REFRESH_TOKEN: "refresh_token",
} as const;
interface RetryConfig extends InternalAxiosRequestConfig {
_retry?: boolean;
}
const api = axios.create({
baseURL: process.env.EXPO_PUBLIC_API_URL || "http://localhost:8000",
headers: {
"Content-Type": "application/json",
},
});
// ── Token helpers ────────────────────────────────────────────────────
async function getAccessToken(): Promise<string | null> {
return AsyncStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
}
async function getRefreshToken(): Promise<string | null> {
return AsyncStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
}
async function setTokens(access: string, refresh: string): Promise<void> {
await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, access);
await AsyncStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refresh);
}
async function clearTokens(): Promise<void> {
await AsyncStorage.multiRemove([
STORAGE_KEYS.ACCESS_TOKEN,
STORAGE_KEYS.REFRESH_TOKEN,
]);
}
async function saveTokens(tokens: TokenResponse): Promise<void> {
await setTokens(tokens.access, tokens.refresh);
}
async function loadTokens(): Promise<TokenResponse | null> {
const [access, refresh] = await Promise.all([
getAccessToken(),
getRefreshToken(),
]);
if (!access || !refresh) {
return null;
}
return { access, refresh };
}
/** Absolute base URL used for direct (non-axios) requests like file downloads. */
function getApiBaseUrl(): string {
return api.defaults.baseURL ?? "";
}
// ── Request interceptor ─────────────────────────────────────────────
api.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
const token = await getAccessToken();
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// ── Response interceptor: auto-refresh on 401 ───────────────────────
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string) => void;
reject: (err: unknown) => void;
}> = [];
function processQueue(error: unknown, token: string | null = null): void {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else if (token) {
prom.resolve(token);
}
});
failedQueue = [];
}
api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as RetryConfig | undefined;
if (!originalRequest) {
return Promise.reject(error);
}
if (
error.response?.status !== 401 ||
originalRequest._retry ||
originalRequest.url?.includes("/api/auth/token/refresh/") ||
originalRequest.url?.includes("/api/auth/login/") ||
originalRequest.url?.includes("/api/auth/register/") ||
originalRequest.url?.includes("/api/auth/logout/")
) {
return Promise.reject(error);
}
if (isRefreshing) {
return new Promise<string>((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${token}`;
}
return api(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
const refreshToken = await getRefreshToken();
if (!refreshToken) {
isRefreshing = false;
await clearTokens();
return Promise.reject(error);
}
try {
const response = await axios.post(
`${api.defaults.baseURL}/api/auth/token/refresh/`,
{ refresh: refreshToken },
);
const newAccess = response.data.access as string;
const newRefresh = response.data.refresh as string;
await setTokens(newAccess, newRefresh);
processQueue(null, newAccess);
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newAccess}`;
}
return api(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
await clearTokens();
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
},
);
const apiClient = api;
export {
apiClient,
getAccessToken,
saveTokens,
loadTokens,
clearTokens,
getApiBaseUrl,
};
export default api;