mohmmed5787's picture
Deploying Affiliate Launch Kit - Production Ready
d44ff09 verified
Raw
History Blame Contribute Delete
5.19 kB
/**
* API client — talks to the backend (Section 8 endpoints).
* - Injects `Authorization: Bearer <access_token>` on every call.
* - On 401, transparently calls /auth/refresh (using the httpOnly cookie) and
* retries the original request once (Section 6.4 session expiry UX).
* - Refresh tokens never touch JS (httpOnly cookie set by the backend).
*/
const BASE = '/api';
export class ApiError extends Error {
code: string;
status: number;
constructor(status: number, code: string, message: string) {
super(message);
this.status = status;
this.code = code;
}
}
let accessToken: string | null = null;
let onAuthFailure: (() => void) | null = null;
export function setAccessToken(t: string | null) { accessToken = t; }
export function getAccessToken() { return accessToken; }
export function setOnAuthFailure(cb: () => void) { onAuthFailure = cb; }
let refreshing: Promise<boolean> | null = null;
async function tryRefresh(): Promise<boolean> {
if (!refreshing) {
refreshing = (async () => {
try {
const res = await fetch(`${BASE}/auth/refresh`, { method: 'POST', credentials: 'include' });
if (!res.ok) return false;
const data = await res.json();
accessToken = data.access_token;
return true;
} catch {
return false;
} finally {
refreshing = null;
}
})();
}
return refreshing;
}
async function request<T = any>(
method: string,
path: string,
body?: unknown,
attempt = 0,
): Promise<T> {
const headers: Record<string, string> = {};
if (body !== undefined) headers['Content-Type'] = 'application/json';
if (accessToken) headers['Authorization'] = `Bearer ${accessToken}`;
const res = await fetch(`${BASE}${path}`, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
credentials: 'include',
});
if (res.status === 401 && attempt === 0) {
// Transparent refresh + retry once (Section 6.4).
const ok = await tryRefresh();
if (ok) return request<T>(method, path, body, attempt + 1);
onAuthFailure?.();
throw new ApiError(401, 'SESSION_EXPIRED', 'Session expired, please log in again');
}
const text = await res.text();
let data: any = null;
if (text) {
try { data = JSON.parse(text); } catch { data = text; }
}
if (!res.ok) {
const code = data?.error?.code || 'ERROR';
const message = data?.error?.message || (typeof data === 'string' ? data : 'Request failed');
throw new ApiError(res.status, code, message);
}
// 204 No Content
if (res.status === 204) return undefined as T;
return data as T;
}
export const api = {
// Auth & Licensing (8.1)
register: (email: string, password: string) =>
request<{ user_id: string; access_token: string }>('POST', '/auth/register', { email, password }),
login: (email: string, password: string) =>
request<{ user_id: string; access_token: string; license_status: string }>('POST', '/auth/login', { email, password }),
refresh: () => request<{ access_token: string }>('POST', '/auth/refresh'),
logout: () => request<void>('POST', '/auth/logout'),
activateLicense: (license_key: string) =>
request<{ status: string; activated_at: string }>('POST', '/auth/activate-license', { license_key }),
// Campaigns (8.2 / 8.3)
createCampaign: (payload: any) => request<{ campaign_id: string; status: string }>('POST', '/campaigns', payload),
listCampaigns: (query: Record<string, any>) =>
request<{ campaigns: any[]; total: number; page: number }>('GET', `/campaigns?${new URLSearchParams(query)}`),
getCampaign: (id: string) => request<{ campaign: any }>('GET', `/campaigns/${id}`),
getAssets: (id: string) => request<{ assets: Record<string, any> }>('GET', `/campaigns/${id}/assets`),
duplicateCampaign: (id: string) => request<{ new_campaign_id: string }>('POST', `/campaigns/${id}/duplicate`),
deleteCampaign: (id: string) => request<void>('DELETE', `/campaigns/${id}`),
updateAsset: (id: string, assetType: string, content: string) =>
request<{ asset: any; version: number; is_manual_edit: boolean }>('PATCH', `/campaigns/${id}/assets/${assetType}`, { content }),
regenerateAsset: (id: string, assetType: string, custom_instruction?: string) =>
request<{ asset: any; version: number }>('POST', `/campaigns/${id}/assets/${assetType}/regenerate`, { custom_instruction }),
// Export (8.4)
createExport: (id: string, formats: string[], bundle_as_zip: boolean) =>
request<{ export_id: string; status: string }>('POST', `/campaigns/${id}/export`, { formats, bundle_as_zip }),
listExports: (id: string) => request<{ exports: any[] }>('GET', `/campaigns/${id}/exports`),
downloadUrl: (id: string) => `${BASE}/exports/${id}/download`,
};
export function downloadFile(url: string) {
// Opens with the Authorization header via a fetch blob (cookie + token).
const a = document.createElement('a');
a.href = url + (url.includes('?') ? '&' : '?') + '_t=' + Date.now();
a.download = '';
// Note: for token-protected downloads we rely on the same-origin cookie path
// in dev proxy; production would use a signed URL from storage.
a.click();
}