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 | 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 { const res = await axios.post( `${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 { const res = await axios.post( `${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( method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', path: string, data?: Record ): Promise { const isBodyMethod = method === 'POST' || method === 'PUT' || method === 'PATCH'; const res = await this.client.request({ 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; } }