127 lines
3.8 KiB
TypeScript

import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
interface AuthTokens {
accessToken: string;
refreshToken: string;
}
interface AuthResponse {
accessToken: string;
refreshToken: string;
user: { id: number; email: string; role: string; name?: string };
}
export class ApiClient {
private client: AxiosInstance;
private tokens: AuthTokens | null = null;
private refreshPromise: Promise<AuthTokens> | null = null;
private baseUrl: string;
private email: string;
private password: string;
constructor(config: { baseUrl: string; email: string; password: string }) {
this.baseUrl = config.baseUrl;
this.email = config.email;
this.password = config.password;
this.client = axios.create({
baseURL: config.baseUrl,
timeout: 30_000,
headers: { 'X-MCP-Session': 'true' },
});
// Attach access token to every request
this.client.interceptors.request.use((req: InternalAxiosRequestConfig) => {
if (this.tokens?.accessToken) {
req.headers.Authorization = `Bearer ${this.tokens.accessToken}`;
}
return req;
});
// Auto-refresh on 401 (mirrors admin/src/lib/api.ts:39-83)
this.client.interceptors.response.use(
(res) => res,
async (error: AxiosError) => {
const original = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
const errorCode = (error.response?.data as any)?.error?.code;
if (
error.response?.status === 401 &&
(errorCode === 'INVALID_TOKEN' || errorCode === 'AUTH_REQUIRED') &&
!original._retry
) {
original._retry = true;
if (!this.tokens?.refreshToken) {
throw new Error('No refresh token available — call login() first');
}
try {
if (!this.refreshPromise) {
this.refreshPromise = this.doRefresh(this.tokens.refreshToken);
}
this.tokens = await this.refreshPromise;
this.refreshPromise = null;
original.headers.Authorization = `Bearer ${this.tokens.accessToken}`;
return this.client(original);
} catch {
this.refreshPromise = null;
// Refresh failed — try full re-login
await this.login();
original.headers.Authorization = `Bearer ${this.tokens!.accessToken}`;
return this.client(original);
}
}
throw error;
}
);
}
/** Authenticate with the API and store tokens */
async login(): Promise<void> {
const res = await axios.post<AuthResponse>(
`${this.baseUrl}/api/auth/login`,
{ email: this.email, password: this.password },
{ headers: { 'X-MCP-Session': 'true' } }
);
this.tokens = {
accessToken: res.data.accessToken,
refreshToken: res.data.refreshToken,
};
}
private async doRefresh(refreshToken: string): Promise<AuthTokens> {
const res = await axios.post<AuthResponse>(
`${this.baseUrl}/api/auth/refresh`,
{ refreshToken },
{ headers: { 'X-MCP-Session': 'true' } }
);
return {
accessToken: res.data.accessToken,
refreshToken: res.data.refreshToken,
};
}
/** Make an authenticated API request */
async request<T = any>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
path: string,
data?: Record<string, unknown>
): Promise<T> {
const isBodyMethod = method === 'POST' || method === 'PUT' || method === 'PATCH';
const res = await this.client.request<T>({
method,
url: path,
...(isBodyMethod ? { data } : { params: data }),
});
return res.data;
}
/** Check if we have valid tokens (not necessarily unexpired) */
get isAuthenticated(): boolean {
return this.tokens !== null;
}
}