Files
course-craft-service/apps/web/src/lib/api.ts
2026-02-06 14:53:52 +00:00

433 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// В браузере — относительный 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<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
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<any>('/users/me');
}
async updateProfile(data: { name?: string; bio?: string }) {
return this.request<any>('/users/me', {
method: 'PATCH',
body: JSON.stringify(data),
});
}
async getSettings() {
return this.request<any>('/users/me/settings');
}
async updateSettings(data: { customAiModel?: string; language?: string; theme?: string }) {
return this.request<any>('/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<any>(`/courses/${id}`);
}
async createCourse(data: { title: string; description?: string }) {
return this.request<any>('/courses', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateCourse(id: string, data: any) {
return this.request<any>(`/courses/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
}
async deleteCourse(id: string) {
return this.request<void>(`/courses/${id}`, {
method: 'DELETE',
});
}
// Chapters
async createChapter(courseId: string, data: { title: string; description?: string }) {
return this.request<any>(`/courses/${courseId}/chapters`, {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateChapter(courseId: string, chapterId: string, data: any) {
return this.request<any>(`/courses/${courseId}/chapters/${chapterId}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
}
async deleteChapter(courseId: string, chapterId: string) {
return this.request<void>(`/courses/${courseId}/chapters/${chapterId}`, {
method: 'DELETE',
});
}
// Lessons (API: GET/PATCH /courses/:courseId/lessons/:lessonId)
async getLesson(courseId: string, lessonId: string) {
return this.request<any>(`/courses/${courseId}/lessons/${lessonId}`);
}
async updateLesson(courseId: string, lessonId: string, data: any) {
return this.request<any>(`/courses/${courseId}/lessons/${lessonId}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
}
async getLessonQuiz(courseId: string, lessonId: string) {
return this.request<any>(`/courses/${courseId}/lessons/${lessonId}/quiz`);
}
// 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<string, string | string[]>) {
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<any>('/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<any>(`/catalog/${id}`);
}
async publishCourse(id: string) {
return this.request<any>(`/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<any>(`/catalog/${id}/verify`, { method: 'PATCH' });
}
// Enrollment & Progress
async enrollInCourse(courseId: string) {
return this.request<any>(`/enrollment/${courseId}/enroll`, { method: 'POST' });
}
async getMyEnrollments() {
return this.request<any[]>('/enrollment');
}
async getEnrollmentProgress(courseId: string) {
return this.request<any>(`/enrollment/${courseId}/progress`);
}
async completeLesson(courseId: string, lessonId: string) {
return this.request<any>(`/enrollment/${courseId}/lessons/${lessonId}/complete`, { method: 'POST' });
}
async submitQuizAnswers(courseId: string, lessonId: string, answers: number[]) {
return this.request<any>(`/enrollment/${courseId}/lessons/${lessonId}/quiz`, {
method: 'POST',
body: JSON.stringify({ answers }),
});
}
async getLessonHomework(courseId: string, lessonId: string) {
return this.request<any>(`/enrollment/${courseId}/lessons/${lessonId}/homework`);
}
async submitLessonHomework(courseId: string, lessonId: string, content: string) {
return this.request<any>(`/enrollment/${courseId}/lessons/${lessonId}/homework`, {
method: 'POST',
body: JSON.stringify({ content }),
});
}
async createReview(courseId: string, data: { rating: number; title?: string; content?: string }) {
return this.request<any>(`/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<any>(`/courses/${courseId}/homework-submissions/${submissionId}/review`, {
method: 'POST',
body: JSON.stringify(data),
});
}
async getHomeworkSubmissions(courseId: string) {
return this.request<any[]>(`/courses/${courseId}/homework-submissions`);
}
// Groups
async getDefaultCourseGroup(courseId: string) {
return this.request<any>(`/groups/course/${courseId}/default`);
}
async getGroupMessages(groupId: string) {
return this.request<any[]>(`/groups/${groupId}/messages`);
}
async getGroupMembers(groupId: string) {
return this.request<any[]>(`/groups/${groupId}/members`);
}
async sendGroupMessage(groupId: string, content: string) {
return this.request<any>(`/groups/${groupId}/messages`, {
method: 'POST',
body: JSON.stringify({ content }),
});
}
async createGroupInviteLink(groupId: string) {
return this.request<{ inviteUrl: string }>(`/groups/${groupId}/invite-link`, {
method: 'POST',
});
}
async joinGroupByInvite(groupId: string) {
return this.request<any>(`/groups/join/${groupId}`, { method: 'POST' });
}
// Support
async createSupportTicket(data: { title: string; initialMessage?: string; priority?: string }) {
return this.request<any>('/support/tickets', {
method: 'POST',
body: JSON.stringify(data),
});
}
async getMySupportTickets() {
return this.request<any[]>('/support/tickets');
}
async getSupportTicketMessages(id: string) {
return this.request<any[]>(`/support/tickets/${id}/messages`);
}
async sendSupportMessage(id: string, content: string) {
return this.request<any>(`/support/tickets/${id}/messages`, {
method: 'POST',
body: JSON.stringify({ content }),
});
}
async getAdminSupportTickets() {
return this.request<any[]>('/support/admin/tickets');
}
async getAdminSupportTicketMessages(id: string) {
return this.request<any[]>(`/support/admin/tickets/${id}/messages`);
}
async sendAdminSupportMessage(id: string, content: string) {
return this.request<any>(`/support/admin/tickets/${id}/messages`, {
method: 'POST',
body: JSON.stringify({ content }),
});
}
async updateAdminSupportTicketStatus(id: string, status: string) {
return this.request<any>(`/support/admin/tickets/${id}/status`, {
method: 'POST',
body: JSON.stringify({ status }),
});
}
// Moderation reviews
async hideReview(reviewId: string) {
return this.request<any>(`/moderation/reviews/${reviewId}/hide`, { method: 'POST' });
}
async unhideReview(reviewId: string) {
return this.request<any>(`/moderation/reviews/${reviewId}/unhide`, { 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();