// В браузере — относительный URL (запросы на тот же хост, Next проксирует /api на бэкенд) const API_BASE = typeof window !== 'undefined' ? '' : (process.env.INTERNAL_API_URL || 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), }); } async getLessonQuiz(courseId: string, lessonId: string) { return this.request(`/courses/${courseId}/lessons/${lessonId}/quiz`); } async generateLessonHomework(courseId: string, lessonId: string, type?: 'TEXT' | 'FILE' | 'PROJECT' | 'QUIZ' | 'GITHUB') { return this.request(`/courses/${courseId}/lessons/${lessonId}/homework/generate`, { method: 'POST', body: JSON.stringify({ type }), }); } // 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', }); } // Catalog (public courses) async getCatalog(params?: { page?: number; limit?: number; search?: string; difficulty?: string }) { const searchParams = new URLSearchParams(); if (params?.page) searchParams.set('page', String(params.page)); if (params?.limit) searchParams.set('limit', String(params.limit)); if (params?.search) searchParams.set('search', params.search); if (params?.difficulty) searchParams.set('difficulty', params.difficulty); const query = searchParams.toString(); return this.request<{ data: any[]; meta: any }>(`/catalog${query ? `?${query}` : ''}`); } async getPublicCourse(id: string) { return this.request(`/catalog/${id}`); } async publishCourse(id: string) { return this.request(`/catalog/${id}/submit`, { method: 'POST' }); } async checkoutCourse(id: string) { return this.request<{ url: string }>(`/catalog/${id}/checkout`, { method: 'POST' }); } async toggleCourseVerification(id: string) { return this.request(`/catalog/${id}/verify`, { method: 'PATCH' }); } // Enrollment & Progress async enrollInCourse(courseId: string) { return this.request(`/enrollment/${courseId}/enroll`, { method: 'POST' }); } async getMyEnrollments() { return this.request('/enrollment'); } async getEnrollmentProgress(courseId: string) { return this.request(`/enrollment/${courseId}/progress`); } async completeLesson(courseId: string, lessonId: string) { return this.request(`/enrollment/${courseId}/lessons/${lessonId}/complete`, { method: 'POST' }); } async submitQuizAnswers(courseId: string, lessonId: string, answers: number[]) { return this.request(`/enrollment/${courseId}/lessons/${lessonId}/quiz`, { method: 'POST', body: JSON.stringify({ answers }), }); } async getLessonHomework(courseId: string, lessonId: string) { return this.request(`/enrollment/${courseId}/lessons/${lessonId}/homework`); } async submitLessonHomework( courseId: string, lessonId: string, data: { content?: string; type?: string; attachmentUrl?: string; githubUrl?: string } | string ) { const payload = typeof data === 'string' ? { content: data } : data; return this.request(`/enrollment/${courseId}/lessons/${lessonId}/homework`, { method: 'POST', body: JSON.stringify(payload), }); } async createReview(courseId: string, data: { rating: number; title?: string; content?: string }) { return this.request(`/enrollment/${courseId}/review`, { method: 'POST', body: JSON.stringify(data), }); } async getCourseReviews(courseId: string, page?: number) { const params = page ? `?page=${page}` : ''; return this.request<{ data: any[]; meta: any }>(`/enrollment/${courseId}/reviews${params}`); } // Certificates async getCertificate(courseId: string) { return this.request<{ certificateUrl: string; html: string }>(`/certificates/${courseId}`); } async getCertificateData(courseId: string) { return this.request<{ userName: string; courseTitle: string; completedAt: string }>( `/certificates/${courseId}/data` ); } async reviewHomeworkSubmission( courseId: string, submissionId: string, data: { teacherScore: number; teacherFeedback?: string } ) { return this.request(`/courses/${courseId}/homework-submissions/${submissionId}/review`, { method: 'POST', body: JSON.stringify(data), }); } async getHomeworkSubmissions(courseId: string) { return this.request(`/courses/${courseId}/homework-submissions`); } // Groups async getDefaultCourseGroup(courseId: string) { return this.request(`/groups/course/${courseId}/default`); } async getGroupMessages(groupId: string, lessonId?: string) { const query = lessonId ? `?lessonId=${encodeURIComponent(lessonId)}` : ''; return this.request(`/groups/${groupId}/messages${query}`); } async getGroupMembers(groupId: string) { return this.request(`/groups/${groupId}/members`); } async sendGroupMessage(groupId: string, content: string, lessonId?: string) { return this.request(`/groups/${groupId}/messages`, { method: 'POST', body: JSON.stringify({ content, lessonId }), }); } async createGroupInviteLink(groupId: string) { return this.request<{ inviteUrl: string }>(`/groups/${groupId}/invite-link`, { method: 'POST', }); } async joinGroupByInvite(groupId: string) { return this.request(`/groups/join/${groupId}`, { method: 'POST' }); } // Support async createSupportTicket(data: { title: string; initialMessage?: string; priority?: string }) { return this.request('/support/tickets', { method: 'POST', body: JSON.stringify(data), }); } async getMySupportTickets() { return this.request('/support/tickets'); } async getSupportTicketMessages(id: string) { return this.request(`/support/tickets/${id}/messages`); } async sendSupportMessage(id: string, content: string) { return this.request(`/support/tickets/${id}/messages`, { method: 'POST', body: JSON.stringify({ content }), }); } async getAdminSupportTickets() { return this.request('/support/admin/tickets'); } async getAdminSupportTicketMessages(id: string) { return this.request(`/support/admin/tickets/${id}/messages`); } async sendAdminSupportMessage(id: string, content: string) { return this.request(`/support/admin/tickets/${id}/messages`, { method: 'POST', body: JSON.stringify({ content }), }); } async updateAdminSupportTicketStatus(id: string, status: string) { return this.request(`/support/admin/tickets/${id}/status`, { method: 'POST', body: JSON.stringify({ status }), }); } // Moderation reviews async hideReview(reviewId: string) { return this.request(`/moderation/reviews/${reviewId}/hide`, { method: 'POST' }); } async unhideReview(reviewId: string) { return this.request(`/moderation/reviews/${reviewId}/unhide`, { method: 'POST' }); } async getModerationCourses(params?: { status?: string; search?: string }) { const searchParams = new URLSearchParams(); if (params?.status) searchParams.set('status', params.status); if (params?.search) searchParams.set('search', params.search); const query = searchParams.toString(); return this.request(`/moderation/courses${query ? `?${query}` : ''}`); } async getPendingModerationCourses() { return this.request('/moderation/pending'); } async getModerationCoursePreview(courseId: string) { return this.request(`/moderation/${courseId}/preview`); } async previewModerationQuiz(courseId: string, lessonId: string, answers?: number[]) { return this.request(`/moderation/${courseId}/quiz-preview`, { method: 'POST', body: JSON.stringify({ lessonId, answers }), }); } async approveModerationCourse(courseId: string, note?: string) { return this.request(`/moderation/${courseId}/approve`, { method: 'POST', body: JSON.stringify({ note }), }); } async rejectModerationCourse(courseId: string, reason: string) { return this.request(`/moderation/${courseId}/reject`, { method: 'POST', body: JSON.stringify({ reason }), }); } async deleteModerationCourse(courseId: string) { return this.request(`/moderation/${courseId}`, { method: 'DELETE', }); } async getAdminUsers(params?: { search?: string; role?: string; limit?: number }) { const searchParams = new URLSearchParams(); if (params?.search) searchParams.set('search', params.search); if (params?.role) searchParams.set('role', params.role); if (params?.limit) searchParams.set('limit', String(params.limit)); const query = searchParams.toString(); return this.request(`/admin/users${query ? `?${query}` : ''}`); } async updateAdminUserRole(userId: string, role: 'USER' | 'MODERATOR' | 'ADMIN') { return this.request(`/admin/users/${userId}/role`, { method: 'PATCH', body: JSON.stringify({ role }), }); } async getAdminPayments(params?: { mode?: 'DEV' | 'PROD'; provider?: 'STRIPE' | 'YOOMONEY'; status?: string; search?: string; limit?: number; }) { const searchParams = new URLSearchParams(); if (params?.mode) searchParams.set('mode', params.mode); if (params?.provider) searchParams.set('provider', params.provider); if (params?.status) searchParams.set('status', params.status); if (params?.search) searchParams.set('search', params.search); if (params?.limit) searchParams.set('limit', String(params.limit)); const query = searchParams.toString(); return this.request(`/admin/payments${query ? `?${query}` : ''}`); } async submitCooperationRequest(data: { organization: string; contactName: string; email: string; phone?: string; role?: string; organizationType?: string; message: string; }) { return this.request('/cooperation/requests', { method: 'POST', body: JSON.stringify(data), }); } async uploadCourseSource(courseId: string, file: File) { const token = getApiToken(); const formData = new FormData(); formData.append('file', file); const response = await fetch(`${API_URL}/courses/${courseId}/sources/upload`, { method: 'POST', headers: token ? { Authorization: `Bearer ${token}` } : undefined, body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Upload failed' })); throw new Error(error.message || `HTTP ${response.status}`); } return response.json(); } async getCourseSources(courseId: string) { return this.request(`/courses/${courseId}/sources`); } async getCourseSourceOutlineHints(courseId: string) { return this.request(`/courses/${courseId}/sources/outline-hints`); } // 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();