// В браузере — относительный URL (запросы на тот же хост, Next проксирует /api на бэкенд) const API_BASE = typeof window !== 'undefined' ? '' : (process.env.API_URL || 'http://localhost:3125'); const API_URL = API_BASE ? `${API_BASE.replace(/\/$/, '')}/api` : '/api'; const STORAGE_KEY = 'coursecraft_api_token'; /** JWT from backend (set after exchangeToken). API expects JWT in Authorization header. */ let apiToken: string | null = null; function getStoredToken(): string | null { if (typeof window === 'undefined') return null; try { return sessionStorage.getItem(STORAGE_KEY); } catch { return null; } } export function setApiToken(token: string | null) { apiToken = token; try { if (typeof window !== 'undefined') { if (token) sessionStorage.setItem(STORAGE_KEY, token); else sessionStorage.removeItem(STORAGE_KEY); } } catch { // ignore storage errors (e.g. private mode) } } /** Returns current JWT (memory or sessionStorage after reload). */ function getApiToken(): string | null { if (apiToken) return apiToken; const stored = getStoredToken(); if (stored) { apiToken = stored; return stored; } return null; } class ApiClient { private getAuthHeaders(): HeadersInit { const headers: HeadersInit = { 'Content-Type': 'application/json', }; const token = getApiToken(); if (token) { headers['Authorization'] = `Bearer ${token}`; } return headers; } private async request( endpoint: string, options: RequestInit = {} ): Promise { const headers = this.getAuthHeaders(); const response = await fetch(`${API_URL}${endpoint}`, { ...options, headers: { ...headers, ...options.headers, }, }); if (!response.ok) { if (response.status === 401) { setApiToken(null); if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('auth:unauthorized')); } } const error = await response.json().catch(() => ({ message: 'Request failed' })); throw new Error(error.message || `HTTP ${response.status}`); } return response.json(); } // Auth - Exchange Supabase token for internal JWT (no Authorization header) async exchangeToken(supabaseToken: string) { return this.request<{ accessToken: string; user: any }>('/auth/exchange', { method: 'POST', body: JSON.stringify({ supabaseToken }), }); } // User async getProfile() { return this.request('/users/me'); } async updateProfile(data: { name?: string; bio?: string }) { return this.request('/users/me', { method: 'PATCH', body: JSON.stringify(data), }); } async getSettings() { return this.request('/users/me/settings'); } async updateSettings(data: { customAiModel?: string; language?: string; theme?: string }) { return this.request('/users/me/settings', { method: 'PATCH', body: JSON.stringify(data), }); } // Courses async getCourses(params?: { status?: string; page?: number; limit?: number }) { const searchParams = new URLSearchParams(); if (params?.status) searchParams.set('status', params.status); if (params?.page) searchParams.set('page', String(params.page)); if (params?.limit) searchParams.set('limit', String(params.limit)); const query = searchParams.toString(); return this.request<{ data: any[]; meta: any }>(`/courses${query ? `?${query}` : ''}`); } async getCourse(id: string) { return this.request(`/courses/${id}`); } async createCourse(data: { title: string; description?: string }) { return this.request('/courses', { method: 'POST', body: JSON.stringify(data), }); } async updateCourse(id: string, data: any) { return this.request(`/courses/${id}`, { method: 'PATCH', body: JSON.stringify(data), }); } async deleteCourse(id: string) { return this.request(`/courses/${id}`, { method: 'DELETE', }); } // Chapters async createChapter(courseId: string, data: { title: string; description?: string }) { return this.request(`/courses/${courseId}/chapters`, { method: 'POST', body: JSON.stringify(data), }); } async updateChapter(courseId: string, chapterId: string, data: any) { return this.request(`/courses/${courseId}/chapters/${chapterId}`, { method: 'PATCH', body: JSON.stringify(data), }); } async deleteChapter(courseId: string, chapterId: string) { return this.request(`/courses/${courseId}/chapters/${chapterId}`, { method: 'DELETE', }); } // Lessons (API: GET/PATCH /courses/:courseId/lessons/:lessonId) async getLesson(courseId: string, lessonId: string) { return this.request(`/courses/${courseId}/lessons/${lessonId}`); } async updateLesson(courseId: string, lessonId: string, data: any) { return this.request(`/courses/${courseId}/lessons/${lessonId}`, { method: 'PATCH', body: JSON.stringify(data), }); } // Generation async startGeneration(prompt: string) { return this.request<{ id: string; status: string; progress: number }>('/generation/start', { method: 'POST', body: JSON.stringify({ prompt }), }); } async getGenerationStatus(id: string) { return this.request<{ id: string; status: string; progress: number; currentStep: string | null; questions: any[] | null; generatedOutline: any | null; errorMessage: string | null; course: { id: string; slug: string } | null; }>(`/generation/${id}/status`); } async answerQuestions(id: string, answers: Record) { return this.request<{ success: boolean }>(`/generation/${id}/answer`, { method: 'POST', body: JSON.stringify({ answers }), }); } async cancelGeneration(id: string) { return this.request<{ success: boolean }>(`/generation/${id}/cancel`, { method: 'POST', }); } // Subscription async getSubscription() { return this.request('/payments/subscription'); } async createCheckoutSession(priceId: string) { return this.request<{ url: string }>('/payments/create-checkout-session', { method: 'POST', body: JSON.stringify({ priceId }), }); } async createPortalSession() { return this.request<{ url: string }>('/payments/create-portal-session', { method: 'POST', }); } // Search async searchCourses(query: string, filters?: { category?: string; difficulty?: string }) { const searchParams = new URLSearchParams({ q: query }); if (filters?.category) searchParams.set('category', filters.category); if (filters?.difficulty) searchParams.set('difficulty', filters.difficulty); return this.request<{ hits: any[]; totalHits: number }>(`/search?${searchParams}`); } } export const api = new ApiClient();