Spaces:
Running
Running
File size: 8,928 Bytes
6acfd72 7d627c0 6acfd72 7d627c0 6acfd72 7d627c0 778d373 7d627c0 6acfd72 7d627c0 6acfd72 7d627c0 6acfd72 7d627c0 6acfd72 7d627c0 6acfd72 7d627c0 6acfd72 7d627c0 6acfd72 7d627c0 6acfd72 7d627c0 6acfd72 7d627c0 6acfd72 e284c06 6acfd72 7d627c0 6acfd72 7d627c0 6acfd72 7d627c0 6acfd72 7d627c0 6acfd72 7d627c0 6acfd72 7d627c0 6acfd72 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | /**
* LeadPilot Robust API Client
* - Handles JSON vs Text response detection
* - Automatic Authorization header injection
* - Workspace context management
* - Consistent error handling
*/
import { auth } from "./auth";
const getBaseUrl = () => {
if (process.env.NEXT_PUBLIC_API_BASE_URL) return process.env.NEXT_PUBLIC_API_BASE_URL;
if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL;
if (typeof window !== "undefined") {
// Browser context: localhost dev hits backend directly; production uses
// relative paths so nginx can proxy /api/* to the backend.
if (window.location.hostname === "localhost") {
return "http://localhost:8000";
}
return ""; // relative URLs β works in any production deployment (HF, Docker, etc.)
}
// Server-side (SSR inside Docker): backend is accessible at 127.0.0.1:8000 internally.
return "http://127.0.0.1:8000";
};
const API_BASE_URL = getBaseUrl();
if (!API_BASE_URL && typeof window !== "undefined" && window.location.hostname === "localhost") {
console.warn("WARNING: NEXT_PUBLIC_API_BASE_URL not defined. Falling back to http://localhost:8000");
}
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
error?: string;
}
class ApiClient {
private getToken(): string | null {
return auth.getToken();
}
private getWorkspaceId(): string | null {
return auth.getWorkspaceId();
}
async request<T = any>(
path: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
// If API_BASE_URL is empty, it defaults to relative paths (e.g., /api/v1/...)
// Safe base URL with /api/v1 prefix - ALWAYS absolute
const base = API_BASE_URL.replace(/\/$/, "");
const cleanPath = path.startsWith("/") ? path : `/${path}`;
const url = `${base}/api/v1${cleanPath}`;
const method = options.method || "GET";
console.log(`[API] ${method} ${url}`);
const token = this.getToken();
const workspaceId = this.getWorkspaceId();
const headers = new Headers(options.headers || {});
// Always inject Authorization if token exists
if (token) {
headers.set("Authorization", `Bearer ${token}`);
}
// Always inject X-Workspace-ID
if (workspaceId) {
headers.set("X-Workspace-ID", workspaceId);
}
if (!headers.has("Content-Type") && !(options.body instanceof FormData)) {
headers.set("Content-Type", "application/json");
}
try {
const response = await fetch(url, {
...options,
headers,
});
// Handle 401 (expired/invalid session) β logout
if (response.status === 401) {
if (typeof window !== "undefined") {
auth.logout();
}
return { success: false, error: "Session expired. Please login again." };
}
// Handle 403 (forbidden / module disabled / insufficient role) β surface error, don't logout
if (response.status === 403) {
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
const json = await response.json();
return { success: false, error: json.error || json.detail || "Access denied (403)." };
}
return { success: false, error: "Access denied (403)." };
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
const json = await response.json();
if (!response.ok) {
return {
success: false,
error: json.error || json.detail || `API Error (${response.status})`
};
}
if (json.hasOwnProperty("success")) {
return json as ApiResponse<T>;
}
return { success: true, data: json as T };
} else {
const text = await response.text();
if (!response.ok) {
console.error("Non-JSON Error Response:", text.substring(0, 200));
return {
success: false,
error: `Backend Error (${response.status}): ${text.substring(0, 50)}...`
};
}
return { success: true, data: text as any };
}
} catch (error: any) {
console.error("API Connectivity Error:", error);
return {
success: false,
error: `Backend unreachable (${error.message || 'Network Error'}). Check API base URL or server status.`
};
}
}
get<T = any>(path: string, options?: RequestInit) {
return this.request<T>(path, { ...options, method: "GET" });
}
post<T = any>(path: string, body?: any, options?: RequestInit) {
return this.request<T>(path, {
...options,
method: "POST",
body: body instanceof FormData ? body : JSON.stringify(body),
});
}
put<T = any>(path: string, body?: any, options?: RequestInit) {
return this.request<T>(path, {
...options,
method: "PUT",
body: body instanceof FormData ? body : JSON.stringify(body),
});
}
patch<T = any>(path: string, body?: any, options?: RequestInit) {
return this.request<T>(path, {
...options,
method: "PATCH",
body: body instanceof FormData ? body : JSON.stringify(body),
});
}
delete<T = any>(path: string, options?: RequestInit) {
return this.request<T>(path, { ...options, method: "DELETE" });
}
}
export const apiClient = new ApiClient();
// ββ Marketing Website Functions ββ
export interface CatalogPlan {
id: string;
name: string;
display_name: string;
description: string | null;
sort_order: number;
entitlements: any[];
}
export interface CatalogModule {
key: string;
label: string;
is_enabled: boolean;
}
export interface CatalogProvider {
key: string;
label: string;
description: string;
icon_hint: string;
fields: { name: string; label: string; type: string }[];
}
export interface CatalogTemplate {
id: string;
slug: string;
name: string;
description: string;
category: string;
industry_tags: string[];
platforms: string[];
required_integrations: string[];
is_featured: boolean;
clone_count: number;
}
export const APP_URL = "/app";
export async function getPlans(): Promise<CatalogPlan[]> {
try {
const res = await fetch(`${API_BASE_URL}/api/v1/catalog/plans`, { next: { revalidate: 60 } });
const json = await res.json();
return json.success ? json.data : [];
} catch { return []; }
}
export async function getModules(): Promise<CatalogModule[]> {
try {
const res = await fetch(`${API_BASE_URL}/api/v1/catalog/modules`, { next: { revalidate: 60 } });
const json = await res.json();
return json.success ? json.data : [];
} catch { return []; }
}
export async function getIntegrationProviders(): Promise<CatalogProvider[]> {
try {
const res = await fetch(`${API_BASE_URL}/api/v1/catalog/integration-providers`, { next: { revalidate: 60 } });
const json = await res.json();
return json.success ? json.data : [];
} catch { return []; }
}
export async function getPublicTemplates(category?: string): Promise<CatalogTemplate[]> {
const params = category ? `?category=${encodeURIComponent(category)}` : "";
try {
const res = await fetch(`${API_BASE_URL}/api/v1/catalog/templates${params}`, { next: { revalidate: 60 } });
const json = await res.json();
return json.success ? json.data : [];
} catch { return []; }
}
export interface CatalogEnum {
key: string;
label: string;
}
export async function getTemplateCategories(): Promise<CatalogEnum[]> {
try {
const res = await fetch(`${API_BASE_URL}/api/v1/catalog/template-categories`, { next: { revalidate: 60 } });
const json = await res.json();
return json.success ? json.data : [];
} catch { return []; }
}
export async function getTemplatePlatforms(): Promise<CatalogEnum[]> {
try {
const res = await fetch(`${API_BASE_URL}/api/v1/catalog/template-platforms`, { next: { revalidate: 60 } });
const json = await res.json();
return json.success ? json.data : [];
} catch { return []; }
}
|