/** * Admin API client for LeadPilot Admin Portal. * Uses the admin JWT (stored separately from the product JWT). * Do NOT use apiClient from api.ts here — that sends the product token. */ import { adminAuth } from "./admin-auth"; // ---- Standalone admin fetch client ---------------------------------------- const getBaseUrl = () => { if (process.env.NEXT_PUBLIC_API_BASE_URL) return process.env.NEXT_PUBLIC_API_BASE_URL; if (typeof window !== "undefined" && window.location.hostname === "localhost") { return "http://localhost:8000"; } return ""; }; interface ApiResponse { success: boolean; data?: T; error?: string; } async function adminRequest( path: string, options: RequestInit = {} ): Promise> { const base = getBaseUrl().replace(/\/$/, ""); const cleanPath = path.startsWith("/") ? path : `/${path}`; const url = `${base}/api/v1${cleanPath}`; const token = adminAuth.getToken(); const headers = new Headers(options.headers || {}); if (token) headers.set("Authorization", `Bearer ${token}`); if (!headers.has("Content-Type") && !(options.body instanceof FormData)) { headers.set("Content-Type", "application/json"); } try { const response = await fetch(url, { ...options, headers }); // 401 = token expired/invalid → force re-login if (response.status === 401) { adminAuth.logout(); return { success: false, error: "Admin session expired. Please log in again." }; } // 403 = forbidden (not superuser, module disabled, etc.) → surface error, don't logout if (response.status === 403) { const ct = response.headers.get("content-type"); if (ct?.includes("application/json")) { const json = await response.json(); return { success: false, error: json.error || json.detail || "Access denied (403)." }; } return { success: false, error: "Admin access denied (403)." }; } const contentType = response.headers.get("content-type"); if (contentType?.includes("application/json")) { const json = await response.json(); if (!response.ok) { return { success: false, error: json.error || json.detail || `Error (${response.status})` }; } if ("success" in json) return json as ApiResponse; return { success: true, data: json as T }; } const text = await response.text(); if (!response.ok) return { success: false, error: `Error (${response.status}): ${text.substring(0, 80)}` }; return { success: true, data: text as any }; } catch (err: any) { return { success: false, error: `Admin API unreachable: ${err.message || "Network error"}` }; } } const adminClient = { get: (path: string) => adminRequest(path, { method: "GET" }), post: (path: string, body?: any) => adminRequest(path, { method: "POST", body: body ? JSON.stringify(body) : undefined }), put: (path: string, body?: any) => adminRequest(path, { method: "PUT", body: body ? JSON.stringify(body) : undefined }), patch: (path: string, body?: any) => adminRequest(path, { method: "PATCH", body: body ? JSON.stringify(body) : undefined }), delete: (path: string) => adminRequest(path, { method: "DELETE" }), }; // ---- Type definitions ------------------------------------------------------- export interface ModuleConfig { module_name: string; is_enabled: boolean; config_json?: Record | null; updated_at?: string | null; } export interface SystemOverview { platform: string; python_version: string; users_total: number; workspaces_total: number; audit_log_entries: number; modules: Record; } export interface AuditLogEntry { id: string; actor_user_id?: string | null; actor_type?: string | null; action: string; entity_type: string; entity_id: string; outcome?: string | null; workspace_id?: string | null; agency_id?: string | null; metadata_json?: Record | null; correlation_id?: string | null; ip_address?: string | null; user_agent?: string | null; request_path?: string | null; request_method?: string | null; error_code?: string | null; error_message?: string | null; created_at: string; } export interface RuntimeEventEntry { id: string; workspace_id?: string | null; event_type: string; source: string; correlation_id?: string | null; related_ids?: Record | null; actor_user_id?: string | null; payload?: Record | null; outcome?: string | null; error_message?: string | null; duration_ms?: number | null; created_at: string; } // ---- Admin API methods ------------------------------------------------------ export const adminApi = { // Auth login: (email: string, password: string) => adminClient.post<{ access_token: string; token_type: string }>("/admin_auth/login", { email, password }), me: () => adminClient.get<{ id: string; email: string; full_name: string; is_superuser: boolean; role?: any }>("/admin_auth/me"), // System overview getOverview: () => adminClient.get("/admin/overview"), // Modules (global) getModules: () => adminClient.get("/admin/modules"), toggleModule: (module_name: string, enabled: boolean) => adminClient.patch(`/admin/modules/${module_name}`, { enabled }), // Modules (per workspace) getWorkspaceModules: (workspaceId: string) => adminClient.get<{ module_name: string; is_enabled: boolean; overridden: boolean }[]>( `/admin/workspaces/${workspaceId}/modules` ), setWorkspaceModule: (workspaceId: string, moduleName: string, isEnabled: boolean) => adminClient.patch(`/admin/workspaces/${workspaceId}/modules/${moduleName}`, { is_enabled: isEnabled }), // Users getUsers: (params?: { skip?: number; limit?: number; query?: string }) => { const qs = new URLSearchParams(); if (params?.skip !== undefined) qs.set("skip", String(params.skip)); if (params?.limit !== undefined) qs.set("limit", String(params.limit)); if (params?.query) qs.set("query", params.query); return adminClient.get<{ items: any[]; total: number }>(`/admin/users?${qs.toString()}`); }, toggleUserStatus: (userId: string, isActive: boolean) => adminClient.post(`/admin/users/${userId}/toggle`, { is_active: isActive }), impersonateUser: (userId: string) => adminClient.post<{ access_token: string }>(`/admin/users/${userId}/impersonate`, {}), // Workspaces getWorkspaces: (params?: { skip?: number; limit?: number; query?: string }) => { const qs = new URLSearchParams(); if (params?.skip !== undefined) qs.set("skip", String(params.skip)); if (params?.limit !== undefined) qs.set("limit", String(params.limit)); if (params?.query) qs.set("query", params.query); return adminClient.get<{ items: any[]; total: number }>(`/admin/workspaces?${qs.toString()}`); }, getWorkspaceDetail: (workspaceId: string) => adminClient.get(`/admin/workspaces/${workspaceId}`), // Audit log getAuditLog: (params?: { skip?: number; limit?: number; action?: string; entity_type?: string; actor_type?: string; outcome?: string; workspace_id?: string; agency_id?: string; date_from?: string; date_to?: string; correlation_id?: string; }) => { const qs = new URLSearchParams(); if (params?.skip !== undefined) qs.set("skip", String(params.skip)); if (params?.limit !== undefined) qs.set("limit", String(params.limit)); if (params?.action) qs.set("action", params.action); if (params?.entity_type) qs.set("entity_type", params.entity_type); if (params?.actor_type) qs.set("actor_type", params.actor_type); if (params?.outcome) qs.set("outcome", params.outcome); if (params?.workspace_id) qs.set("workspace_id", params.workspace_id); if (params?.agency_id) qs.set("agency_id", params.agency_id); if (params?.date_from) qs.set("date_from", params.date_from); if (params?.date_to) qs.set("date_to", params.date_to); if (params?.correlation_id) qs.set("correlation_id", params.correlation_id); return adminClient.get<{ items: AuditLogEntry[]; total: number; skip: number; limit: number }>( `/admin/audit-log?${qs.toString()}` ); }, getAuditLogDetail: (logId: string) => adminClient.get(`/admin/audit-log/${logId}`), // Runtime events (Mission 18) getRuntimeEvents: (params?: { skip?: number; limit?: number; source?: string; event_type?: string; outcome?: string; workspace_id?: string; correlation_id?: string; date_from?: string; date_to?: string; }) => { const qs = new URLSearchParams(); if (params?.skip !== undefined) qs.set("skip", String(params.skip)); if (params?.limit !== undefined) qs.set("limit", String(params.limit)); if (params?.source) qs.set("source", params.source); if (params?.event_type) qs.set("event_type", params.event_type); if (params?.outcome) qs.set("outcome", params.outcome); if (params?.workspace_id) qs.set("workspace_id", params.workspace_id); if (params?.correlation_id) qs.set("correlation_id", params.correlation_id); if (params?.date_from) qs.set("date_from", params.date_from); if (params?.date_to) qs.set("date_to", params.date_to); return adminClient.get<{ items: RuntimeEventEntry[]; total: number; skip: number; limit: number }>( `/admin/runtime-events?${qs.toString()}` ); }, getRuntimeEventDetail: (eventId: string) => adminClient.get(`/admin/runtime-events/${eventId}`), // Email logs + retry getEmailLogs: (params?: { status?: string; email_type?: string; skip?: number; limit?: number }) => { const qs = new URLSearchParams(); if (params?.status) qs.set("status", params.status); if (params?.email_type) qs.set("email_type", params.email_type); if (params?.skip !== undefined) qs.set("skip", String(params.skip)); if (params?.limit !== undefined) qs.set("limit", String(params.limit)); return adminClient.get<{ items: any[]; total: number }>(`/admin/email-logs?${qs.toString()}`); }, retryEmail: (outboxId: string) => adminClient.post(`/admin/email-logs/${outboxId}/retry`), // Webhooks + replay getWebhooks: (params?: { provider?: string; status?: string; skip?: number; limit?: number }) => { const qs = new URLSearchParams(); if (params?.provider) qs.set("provider", params.provider); if (params?.status) qs.set("status", params.status); if (params?.skip !== undefined) qs.set("skip", String(params.skip)); if (params?.limit !== undefined) qs.set("limit", String(params.limit)); return adminClient.get<{ items: any[] }>(`/admin/webhooks?${qs.toString()}`); }, replayWebhook: (eventLogId: string) => adminClient.post(`/admin/webhooks/${eventLogId}/replay`), // Dispatch getDispatchQueue: (params?: { skip?: number; limit?: number }) => { const qs = new URLSearchParams(); if (params?.skip !== undefined) qs.set("skip", String(params.skip)); if (params?.limit !== undefined) qs.set("limit", String(params.limit)); return adminClient.get<{ items: any[]; total: number }>(`/admin/dispatch?${qs.toString()}`); }, retryDispatch: (messageId: string) => adminClient.patch(`/admin/dispatch/${messageId}/retry`), deadLetterDispatch: (messageId: string) => adminClient.patch(`/admin/dispatch/${messageId}/dead-letter`), // Automations getAdminAutomations: (params?: { skip?: number; limit?: number }) => { const qs = new URLSearchParams(); if (params?.skip !== undefined) qs.set("skip", String(params.skip)); if (params?.limit !== undefined) qs.set("limit", String(params.limit)); return adminClient.get<{ items: any[]; total: number }>(`/admin/automations?${qs.toString()}`); }, disableFlow: (flowId: string) => adminClient.patch(`/admin/automations/${flowId}/disable`), // Prompt configs getPromptConfigs: (params?: { skip?: number; limit?: number }) => { const qs = new URLSearchParams(); if (params?.skip !== undefined) qs.set("skip", String(params.skip)); if (params?.limit !== undefined) qs.set("limit", String(params.limit)); return adminClient.get<{ items: any[]; total: number }>(`/admin/prompt-configs?${qs.toString()}`); }, // Zoho health getZohoHealth: () => adminClient.get<{ items: any[] }>("/admin/zoho-health"), // Monitoring (legacy) getIntegrations: () => adminClient.get<{ items: any[] }>("/admin/integrations"), getExecutions: () => adminClient.get<{ items: any[] }>("/admin/executions"), // Plans (Mission 14) getPlans: () => adminClient.get<{ items: any[] }>("/admin/plans"), createPlan: (data: { name: string; display_name: string; description?: string; sort_order?: number }) => adminClient.post("/admin/plans", data), getPlanDetail: (planId: string) => adminClient.get(`/admin/plans/${planId}`), updatePlan: (planId: string, data: { display_name?: string; description?: string; is_active?: boolean; sort_order?: number }) => adminClient.put(`/admin/plans/${planId}`, data), setPlanEntitlements: (planId: string, entitlements: { module_key: string; hard_limit: number | null }[]) => adminClient.put(`/admin/plans/${planId}/entitlements`, { entitlements }), // Workspace plan & usage (Mission 14) getWorkspacePlan: (workspaceId: string) => adminClient.get(`/admin/workspaces/${workspaceId}/plan`), assignWorkspacePlan: (workspaceId: string, planId: string) => adminClient.put(`/admin/workspaces/${workspaceId}/plan`, { plan_id: planId }), getWorkspaceUsage: (workspaceId: string) => adminClient.get(`/admin/workspaces/${workspaceId}/usage`), setWorkspaceOverrides: (workspaceId: string, overrides: { module_key: string; hard_limit: number | null }[]) => adminClient.put(`/admin/workspaces/${workspaceId}/overrides`, { overrides }), removeWorkspaceOverride: (workspaceId: string, moduleKey: string) => adminClient.delete(`/admin/workspaces/${workspaceId}/overrides/${moduleKey}`), // Plan time-bound overrides (Mission 34) getWorkspacePlanStatus: (workspaceId: string) => adminClient.get(`/admin/workspaces/${workspaceId}/plan-status`), createWorkspacePlanOverride: (workspaceId: string, data: { plan_id: string; duration_days?: number; ends_at?: string; starts_at?: string; reason?: string; }) => adminClient.post(`/admin/workspaces/${workspaceId}/plan-override`, data), revokeWorkspacePlanOverride: (workspaceId: string, data?: { reason?: string }) => adminClient.post(`/admin/workspaces/${workspaceId}/plan-override/revoke`, data ?? {}), // Agencies (Mission 15) getAgencies: (params?: { skip?: number; limit?: number; query?: string }) => { const qs = new URLSearchParams(); if (params?.skip !== undefined) qs.set("skip", String(params.skip)); if (params?.limit !== undefined) qs.set("limit", String(params.limit)); if (params?.query) qs.set("query", params.query); return adminClient.get<{ items: any[]; total: number }>(`/admin/agencies?${qs.toString()}`); }, getAgencyDetail: (agencyId: string) => adminClient.get(`/admin/agencies/${agencyId}`), updateAgencyStatus: (agencyId: string, status: "active" | "suspended") => adminClient.patch(`/admin/agencies/${agencyId}/status`, { status }), assignAgencyPlan: (agencyId: string, planId: string) => adminClient.put(`/admin/agencies/${agencyId}/plan`, { plan_id: planId }), }; // Canonical source: GET /api/v1/catalog/modules — prefer useCatalog("modules") for dynamic labels export const MODULE_LABELS: Record = { auth: "Authentication", email_engine: "Email Engine", email_verification: "Email Verification", prompt_studio: "Prompt Studio", knowledge_files: "Knowledge Files", integrations_hub: "Integrations Hub", integrations_connect: "Integration Connect", webhooks_ingestion: "Webhook Ingestion", runtime_engine: "Runtime Engine", dispatch_engine: "Dispatch Engine", inbox: "Inbox", zoho_sync: "Zoho Sync", analytics: "Analytics", automations: "Automations", diagnostics: "Diagnostics", admin_portal: "Admin Portal", support_impersonation_enabled: "Support Impersonation", dangerous_actions_enabled: "Dangerous Actions", }; export const LOCKED_MODULES = new Set(["admin_portal"]); // ---- System Settings ------------------------------------------------------- export async function getSystemSettings() { return adminRequest("/admin/system-settings"); } export async function patchSystemSettings(settings: Record) { return adminRequest("/admin/system-settings", { method: "PATCH", body: JSON.stringify({ settings }), }); } // ---- Template Catalog Admin (Mission 27) ----------------------------------- export interface AdminTemplateItem { id: string; slug: string; name: string; description: string | null; category: string; industry_tags: string[]; platforms: string[]; required_integrations: string[]; is_featured: boolean; is_active: boolean; created_at: string; } export interface AdminTemplateVersionItem { id: string; version_number: number; changelog: string | null; is_published: boolean; published_at: string | null; created_at: string; } export async function getAdminTemplates(skip = 0, limit = 50) { return adminRequest<{ items: AdminTemplateItem[]; total: number }>( `/admin/templates?skip=${skip}&limit=${limit}` ); } export async function createAdminTemplate(payload: { slug: string; name: string; description?: string; category?: string; industry_tags?: string[]; platforms?: string[]; required_integrations?: string[]; is_featured?: boolean; }) { return adminRequest<{ id: string; slug: string; name: string }>("/admin/templates", { method: "POST", body: JSON.stringify(payload), }); } export async function patchAdminTemplate( templateId: string, payload: { name?: string; description?: string; category?: string; is_featured?: boolean; is_active?: boolean; industry_tags?: string[]; platforms?: string[]; required_integrations?: string[]; } ) { return adminRequest<{ id: string; updated: boolean }>(`/admin/templates/${templateId}`, { method: "PATCH", body: JSON.stringify(payload), }); } export async function createTemplateVersion( templateId: string, payload: { builder_graph_json: Record; changelog?: string } ) { return adminRequest<{ valid: boolean; id?: string; version_number?: number; errors?: any[] }>( `/admin/templates/${templateId}/versions`, { method: "POST", body: JSON.stringify(payload), } ); } export async function publishTemplateVersion(templateId: string) { return adminRequest<{ published: boolean; version_number: number; published_at: string }>( `/admin/templates/${templateId}/publish`, { method: "POST" } ); } export async function getTemplateVersions(templateId: string) { return adminRequest(`/admin/templates/${templateId}/versions`); }