Spaces:
Running
Running
Download src/lib/getcertV2API.ts from Sarvesh213/getcert-backend: direct link, hf CLI and curl.
- Browser
- Download file 14.5 kB
-
https://huggingface.co/spaces/Sarvesh213/getcert-backend/resolve/main/src/lib/getcertV2API.ts
- Command line
-
hf download hf://spaces/Sarvesh213/getcert-backend/src/lib/getcertV2API.ts
-
curl -L -o getcertV2API.ts https://huggingface.co/spaces/Sarvesh213/getcert-backend/resolve/main/src/lib/getcertV2API.ts
14.5 kB
| import type { | |
| AssessmentPackV2, | |
| AssessmentGenerationContextV2, | |
| AssessmentResultV2, | |
| AssessmentSubmissionV2, | |
| CertificateDetailsV2, | |
| CertificationTrack, | |
| HealthStatusV2, | |
| GeneratedCourseV2, | |
| GeneratedProjectV2, | |
| GitHubValidationReportV2, | |
| IssuedCertificateV2, | |
| PaymentRequestResponseV2, | |
| ProjectEvaluationV2, | |
| PublicProfileV2, | |
| SmartQuestionV2, | |
| FreeCourseItem, | |
| FreeCourseCompletionPayload, | |
| YtAnalyzeResponse, | |
| YtAssessmentStartResponse, | |
| YtAssessmentGradeResult, | |
| } from '../types/getcertV2'; | |
| import { supabase } from './supabase'; | |
| import { DEFAULT_FREE_COURSES } from '../constants/defaultFreeCourses'; | |
| type ApiEnvelope<T> = { | |
| success: boolean; | |
| data?: T; | |
| error?: string; | |
| details?: unknown; | |
| }; | |
| const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL || '').trim().replace(/\/$/, ''); | |
| function parsePositiveTimeoutMs(value: unknown, fallbackMs: number): number { | |
| const parsed = Number(value); | |
| if (!Number.isFinite(parsed) || parsed <= 0) { | |
| return fallbackMs; | |
| } | |
| return Math.round(parsed); | |
| } | |
| const API_REQUEST_TIMEOUT_MS = parsePositiveTimeoutMs(import.meta.env.VITE_API_REQUEST_TIMEOUT_MS, 120000); | |
| function normalizePath(path: string): string { | |
| return path.startsWith('/') ? path : `/${path}`; | |
| } | |
| function joinBaseAndPath(base: string, path: string): string { | |
| const normalizedPath = normalizePath(path); | |
| // Backward-compatible guard: if base already ends with /api and callers | |
| // pass /api/... paths, avoid constructing /api/api/... | |
| if (base.endsWith('/api') && normalizedPath.startsWith('/api/')) { | |
| return `${base}${normalizedPath.slice(4)}`; | |
| } | |
| return `${base}${normalizedPath}`; | |
| } | |
| function buildUrl(path: string): string { | |
| if (/^https?:\/\//i.test(path)) { | |
| return path; | |
| } | |
| if (!API_BASE_URL) { | |
| return normalizePath(path); | |
| } | |
| return joinBaseAndPath(API_BASE_URL, path); | |
| } | |
| function getNetworkErrorMessage(error: unknown): string { | |
| const message = error instanceof Error ? error.message : 'Unknown network error'; | |
| return `Unable to reach API endpoint. ${message}. In local dev, run both frontend and API servers (npm run dev:full) or verify VITE_API_BASE_URL.`; | |
| } | |
| async function getAuthorizationHeader(): Promise<Record<string, string>> { | |
| try { | |
| const { data } = await supabase.auth.getSession(); | |
| const accessToken = data.session?.access_token?.trim(); | |
| if (!accessToken) { | |
| return {}; | |
| } | |
| return { Authorization: `Bearer ${accessToken}` }; | |
| } catch { | |
| return {}; | |
| } | |
| } | |
| async function fetchWithTimeout(input: RequestInfo | URL, init: RequestInit = {}, timeoutMs = API_REQUEST_TIMEOUT_MS): Promise<Response> { | |
| const controller = new AbortController(); | |
| const timeout = window.setTimeout(() => controller.abort(), timeoutMs); | |
| try { | |
| return await fetch(input, { | |
| ...init, | |
| signal: controller.signal, | |
| }); | |
| } finally { | |
| window.clearTimeout(timeout); | |
| } | |
| } | |
| // No withFallback wrappers β all errors propagate to the UI for retry | |
| async function parseResponse<T>(response: Response): Promise<T> { | |
| const raw = await response.text(); | |
| let payload: ApiEnvelope<T> | null = null; | |
| if (raw) { | |
| try { | |
| payload = JSON.parse(raw) as ApiEnvelope<T>; | |
| } catch { | |
| payload = null; | |
| } | |
| } | |
| if (!response.ok) { | |
| if (payload?.error) { | |
| throw new Error(payload.error); | |
| } | |
| if (!payload) { | |
| throw new Error( | |
| `API ${response.status} returned non-JSON at ${response.url}. Run npm run dev:full or confirm VITE_API_BASE_URL.` | |
| ); | |
| } | |
| throw new Error(`API request failed with status ${response.status}`); | |
| } | |
| if (!payload || !payload.success || payload.data === undefined) { | |
| throw new Error('API returned an unexpected response shape'); | |
| } | |
| return payload.data; | |
| } | |
| async function postJson<T>(path: string, body: unknown): Promise<T> { | |
| let response: Response; | |
| try { | |
| const authorizationHeader = await getAuthorizationHeader(); | |
| response = await fetchWithTimeout(buildUrl(path), { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', ...authorizationHeader }, | |
| body: JSON.stringify(body), | |
| }); | |
| } catch (error) { | |
| throw new Error(getNetworkErrorMessage(error)); | |
| } | |
| return parseResponse<T>(response); | |
| } | |
| async function getJson<T>(path: string): Promise<T> { | |
| let response: Response; | |
| try { | |
| const authorizationHeader = await getAuthorizationHeader(); | |
| response = await fetchWithTimeout(buildUrl(path), { | |
| method: 'GET', | |
| headers: { 'Content-Type': 'application/json', ...authorizationHeader }, | |
| }); | |
| } catch (error) { | |
| throw new Error(getNetworkErrorMessage(error)); | |
| } | |
| return parseResponse<T>(response); | |
| } | |
| let clientCachedFreeCourses: FreeCourseItem[] | null = null; | |
| export const getcertV2API = { | |
| generateCourse(skill: string): Promise<GeneratedCourseV2> { | |
| return postJson<GeneratedCourseV2>('/api/v2/course', { skill }); | |
| }, | |
| generateAssessment(skill: string, moduleTitles: string[], context?: AssessmentGenerationContextV2): Promise<AssessmentPackV2> { | |
| return postJson<AssessmentPackV2>('/api/v2/assessment', { | |
| mode: 'generate', | |
| skill, | |
| moduleTitles, | |
| courseSummary: context?.courseSummary, | |
| moduleContext: context?.modules, | |
| }); | |
| }, | |
| gradeAssessment(pack: AssessmentPackV2, submission: AssessmentSubmissionV2): Promise<AssessmentResultV2> { | |
| return postJson<AssessmentResultV2>('/api/v2/assessment', { | |
| mode: 'grade', | |
| pack, | |
| submission, | |
| }); | |
| }, | |
| generateProject(skill: string): Promise<GeneratedProjectV2> { | |
| return postJson<GeneratedProjectV2>('/api/v2/project', { skill }); | |
| }, | |
| validateGitHub(repoUrl: string): Promise<GitHubValidationReportV2> { | |
| return postJson<GitHubValidationReportV2>('/api/v2/github-validate', { repoUrl }); | |
| }, | |
| generateProjectQuestions( | |
| skill: string, | |
| url: string, | |
| retryCount = 0, | |
| isDesignProject = false | |
| ): Promise<{ | |
| questions: SmartQuestionV2[]; | |
| metadata: GitHubValidationReportV2['metadata']; | |
| validationSummary: { | |
| overallPassed: boolean; | |
| commitPassed: boolean; | |
| readmePassed: boolean; | |
| }; | |
| }> { | |
| return postJson('/api/v2/project-questions', { | |
| skill, | |
| repoUrl: isDesignProject ? undefined : url, | |
| portfolioUrl: isDesignProject ? url : undefined, | |
| retryCount, | |
| isDesignProject, | |
| }); | |
| }, | |
| evaluateProject(payload: { | |
| skill: string; | |
| repoUrl?: string; | |
| projectType: 'web' | 'python' | 'data' | 'design'; | |
| liveDemoUrl?: string; | |
| portfolioUrl?: string; | |
| notebookUrl?: string; | |
| questions: SmartQuestionV2[]; | |
| answers: Array<{ questionId: string; answer: string }>; | |
| }): Promise<ProjectEvaluationV2> { | |
| return postJson<ProjectEvaluationV2>('/api/v2/project-evaluate', payload); | |
| }, | |
| submitPaymentRequest(payload: { | |
| userId: string; | |
| txnId?: string; | |
| skill: string; | |
| projectTitle: string; | |
| githubUrl?: string; | |
| proofUrl: string; | |
| finalScore: number; | |
| scoreBreakdown?: ProjectEvaluationV2['scoreBreakdown']; | |
| validationSummary?: ProjectEvaluationV2['validationSummary']; | |
| projectType?: 'web' | 'python' | 'data' | 'design'; | |
| commitCount?: number; | |
| liveDemoUrl?: string; | |
| paymentRequestId?: string; | |
| track?: CertificationTrack; | |
| email?: string; | |
| phone?: string; | |
| promoCode?: string; | |
| amountInr?: number; | |
| }): Promise<PaymentRequestResponseV2> { | |
| return postJson<PaymentRequestResponseV2>('/api/v2/payment-submit', payload); | |
| }, | |
| getPaymentStatus( | |
| userId: string, | |
| paymentRequestId?: string, | |
| txnId?: string, | |
| skill?: string, | |
| projectTitle?: string, | |
| track?: string, | |
| ): Promise<PaymentRequestResponseV2> { | |
| const query = new URLSearchParams({ userId }); | |
| if (paymentRequestId) { | |
| query.set('paymentRequestId', paymentRequestId); | |
| } | |
| if (txnId) { | |
| query.set('txnId', txnId); | |
| } | |
| if (skill) { | |
| query.set('skill', skill); | |
| } | |
| if (projectTitle) { | |
| query.set('projectTitle', projectTitle); | |
| } | |
| if (track) { | |
| query.set('track', track); | |
| } | |
| return getJson<PaymentRequestResponseV2>( | |
| `/api/v2/payment-status?${query.toString()}` | |
| ); | |
| }, | |
| certify(payload: { | |
| userId: string; | |
| name: string; | |
| email: string; | |
| skill: string; | |
| projectTitle: string; | |
| githubUrl?: string; | |
| proofUrl: string; | |
| finalScore: number; | |
| scoreBreakdown?: ProjectEvaluationV2['scoreBreakdown']; | |
| validationSummary?: ProjectEvaluationV2['validationSummary']; | |
| projectType?: 'web' | 'python' | 'data' | 'design'; | |
| commitCount?: number; | |
| liveDemoUrl?: string; | |
| paymentRequestId?: string; | |
| txnId?: string; | |
| track?: CertificationTrack; | |
| }): Promise<IssuedCertificateV2> { | |
| return postJson<IssuedCertificateV2>('/api/v2/certify', payload); | |
| }, | |
| getCertificate(certificateId: string): Promise<CertificateDetailsV2> { | |
| return getJson<CertificateDetailsV2>(`/api/v2/certificate/${encodeURIComponent(certificateId)}`); | |
| }, | |
| getPublicProfile(userId: string): Promise<PublicProfileV2> { | |
| return getJson<PublicProfileV2>(`/api/v2/profile/${encodeURIComponent(userId)}`); | |
| }, | |
| getHealth(): Promise<HealthStatusV2> { | |
| return getJson<HealthStatusV2>('/api/v2/health'); | |
| }, | |
| async getFreeCourses(): Promise<FreeCourseItem[]> { | |
| if (clientCachedFreeCourses && clientCachedFreeCourses.length > 0) { | |
| return clientCachedFreeCourses; | |
| } | |
| // 1. Direct Supabase query (Lightning fast: 30-50ms directly from Supabase, zero server cold-start) | |
| try { | |
| const { data, error } = await supabase | |
| .from('free_courses') | |
| .select('*') | |
| .eq('is_active', true) | |
| .order('display_order', { ascending: true }); | |
| if (!error && data && data.length > 0) { | |
| const items = data.map((row: any) => { | |
| const videoId = row.youtube_video_id || 'kqtD5dpn9C8'; | |
| const maxStudents = typeof row.max_students === 'number' && row.max_students > 0 ? row.max_students : null; | |
| const certifiedCount = typeof row.certified_students_count === 'number' ? row.certified_students_count : 0; | |
| return { | |
| id: row.id, | |
| courseId: row.course_id, | |
| title: row.title, | |
| skill: row.skill, | |
| description: row.description || '', | |
| category: row.category || 'Programming', | |
| level: row.level || 'Beginner', | |
| durationMinutes: row.duration_minutes || 30, | |
| maxStudents, | |
| certifiedStudentsCount: certifiedCount, | |
| isSoldOut: Boolean(maxStudents !== null && certifiedCount >= maxStudents), | |
| remainingSlots: maxStudents !== null ? Math.max(0, maxStudents - certifiedCount) : null, | |
| youtubeUrl: row.youtube_url || `https://www.youtube.com/watch?v=${videoId}`, | |
| youtubeVideoId: videoId, | |
| channelTitle: row.channel_title || 'Programming with Mosh', | |
| thumbnailUrl: row.thumbnail_url || `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`, | |
| embedUrl: `https://www.youtube-nocookie.com/embed/${videoId}?rel=0`, | |
| isActive: Boolean(row.is_active ?? true), | |
| displayOrder: row.display_order ?? 0, | |
| keyTakeaways: Array.isArray(row.key_takeaways) ? row.key_takeaways : [], | |
| quizQuestions: Array.isArray(row.quiz_questions) ? row.quiz_questions : [], | |
| createdAt: row.created_at, | |
| updatedAt: row.updated_at, | |
| }; | |
| }); | |
| clientCachedFreeCourses = items; | |
| return items; | |
| } | |
| } catch { | |
| // Supabase query error or table not yet migrated | |
| } | |
| // 2. Fast API probe with 2000ms timeout (never hang for 120s on sleeping containers) | |
| try { | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), 2000); | |
| const url = buildUrl('/api/v2/free-courses'); | |
| const response = await fetch(url, { signal: controller.signal }); | |
| clearTimeout(timeout); | |
| if (response.ok) { | |
| const payload = await response.json(); | |
| if (payload?.success && Array.isArray(payload.data?.courses) && payload.data.courses.length > 0) { | |
| clientCachedFreeCourses = payload.data.courses; | |
| return payload.data.courses; | |
| } | |
| } | |
| } catch { | |
| // Backend cold starting or unreachable | |
| } | |
| // 3. Instant local fallback (0ms) | |
| clientCachedFreeCourses = DEFAULT_FREE_COURSES; | |
| return DEFAULT_FREE_COURSES; | |
| }, | |
| async updateFreeCourseProgress(payload: { | |
| courseId: string; | |
| courseTitle: string; | |
| progressPercentage?: number; | |
| currentStep?: string; | |
| status?: string; | |
| score?: number; | |
| }): Promise<{ success: boolean }> { | |
| try { | |
| return await postJson<{ success: boolean }>('/api/v2/free-courses/progress', payload); | |
| } catch { | |
| return { success: false }; | |
| } | |
| }, | |
| completeFreeCourse(payload: FreeCourseCompletionPayload): Promise<IssuedCertificateV2> { | |
| return postJson<IssuedCertificateV2>('/api/v2/free-courses/complete', payload); | |
| }, | |
| // βββ GetCert YT ("Certify Your Learning from YouTube") βββββββββββββββββββββ | |
| analyzeYouTubeCourse(url: string): Promise<YtAnalyzeResponse> { | |
| return postJson<YtAnalyzeResponse>('/api/v2/yt/analyze', { url }); | |
| }, | |
| startYtAssessment(snapshotId: string): Promise<YtAssessmentStartResponse> { | |
| return postJson<YtAssessmentStartResponse>('/api/v2/yt/assessment/start', { snapshotId }); | |
| }, | |
| submitYtAssessment(payload: { | |
| snapshotId: string; | |
| attemptId: string; | |
| answers: Record<string, number>; | |
| }): Promise<YtAssessmentGradeResult> { | |
| return postJson<YtAssessmentGradeResult>('/api/v2/yt/assessment/submit', payload); | |
| }, | |
| checkoutYtCertification(payload: { | |
| snapshotId: string; | |
| score: number; | |
| email?: string; | |
| }): Promise<{ | |
| paywallEnabled: boolean; | |
| amountInr: number; | |
| checkoutUrl: string; | |
| checkoutId: string; | |
| paymentSessionId: string; | |
| track: string; | |
| message: string; | |
| }> { | |
| return postJson('/api/v2/yt/checkout', payload); | |
| }, | |
| certifyYtCourse(payload: { | |
| userId: string; | |
| name: string; | |
| email: string; | |
| skill: string; | |
| projectTitle: string; | |
| finalScore: number; | |
| paymentRequestId?: string; | |
| txnId?: string; | |
| track: 'ytcert'; | |
| extraMetadata?: Record<string, any>; | |
| }): Promise<IssuedCertificateV2> { | |
| return postJson<IssuedCertificateV2>('/api/v2/certify', payload); | |
| }, | |
| }; | |