From ac1a05f0068b638e18ed5473fd427506f64a7a1e Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Fri, 5 Dec 2025 11:20:12 +0100 Subject: [PATCH] feat: move course analytics request to api --- .../course => courses/analytics}/+server.ts | 67 +++++++++++++------ .../courses/[id]/analytics/+page.svelte | 11 +-- 2 files changed, 51 insertions(+), 27 deletions(-) rename apps/dashboard/src/routes/api/{analytics/course => courses/analytics}/+server.ts (80%) diff --git a/apps/dashboard/src/routes/api/analytics/course/+server.ts b/apps/dashboard/src/routes/api/courses/analytics/+server.ts similarity index 80% rename from apps/dashboard/src/routes/api/analytics/course/+server.ts rename to apps/dashboard/src/routes/api/courses/analytics/+server.ts index 4a649cb6..f8562f71 100644 --- a/apps/dashboard/src/routes/api/analytics/course/+server.ts +++ b/apps/dashboard/src/routes/api/courses/analytics/+server.ts @@ -1,37 +1,55 @@ +import type { RequestHandler } from './$types'; import type { CourseAnalytics, StudentOverview } from '$lib/utils/types/analytics'; - import type { UserExercisesStats } from '$lib/utils/types/analytics'; import { calcPercentageWithRounding } from '$lib/utils/functions/number.js'; import { fetchProfileCourseProgress } from '$lib/utils/services/courses'; import { getServerSupabase } from '$lib/utils/functions/supabase.server'; +import { checkUserCoursePermissions } from '$lib/utils/functions/permissions'; import { json } from '@sveltejs/kit'; -const supabase = getServerSupabase(); - const CACHE_DURATION = 60 * 5; // 5 minutes -export async function POST({ setHeaders, request }) { - const { courseId } = await request.json(); - - if (!courseId) { - return json({ success: false, message: 'Request is missing required fields' }, { status: 400 }); +export const GET: RequestHandler = async ({ setHeaders, request }) => { + const userId = request.headers.get('user_id'); + if (!userId) { + return json({ success: false, message: 'Unauthorized' }, { status: 401 }); } - setHeaders({ - 'cache-control': `max-age=${CACHE_DURATION}`, - 'content-type': 'application/json' - }); + const url = new URL(request.url); + const courseId = url.searchParams.get('courseId'); + if (!courseId) { + return json({ success: false, message: 'Course ID is required' }, { status: 400 }); + } try { - const analytics = await getCourseAnalytics(courseId); - return json(analytics, { status: 200 }); + const supabase = getServerSupabase(); + + const hasPermission = await checkUserCoursePermissions(supabase, userId, courseId); + + if (!hasPermission) { + return json( + { + success: false, + message: 'Access denied. User is not a member of this course or organization.' + }, + { status: 403 } + ); + } + + setHeaders({ + 'cache-control': `max-age=${CACHE_DURATION}`, + 'content-type': 'application/json' + }); + + const analytics = await getCourseAnalytics(supabase, courseId); + return json({ success: true, data: analytics }); } catch (error) { console.error('Course analytics error:', error); - return json({ error: 'Something went wrong' }, { status: 500 }); + return json({ success: false, message: 'Internal server error' }, { status: 500 }); } -} +}; -async function getCourseAnalytics(courseId: string): Promise { +async function getCourseAnalytics(supabase: any, courseId: string): Promise { const analytics: CourseAnalytics = { totalTutors: 0, totalStudents: 0, @@ -93,7 +111,7 @@ async function getCourseAnalytics(courseId: string): Promise { const studentAnalytics = await Promise.all( members .filter((member) => member.role_id === 3) - .map((member) => getStudentOverview(courseId, member)) + .map((member) => getStudentOverview(supabase, courseId, member)) ); analytics.students = studentAnalytics.filter( @@ -126,7 +144,7 @@ async function getCourseAnalytics(courseId: string): Promise { return analytics; } -async function getLastLogin(userId: string): Promise { +async function getLastLogin(supabase: any, userId: string): Promise { try { const { data, error } = await supabase .from('analytics_login_events') @@ -144,7 +162,11 @@ async function getLastLogin(userId: string): Promise { } } -async function getStudentOverview(courseId: string, member: any): Promise { +async function getStudentOverview( + supabase: any, + courseId: string, + member: any +): Promise { try { // Use the same service function that the working analytics/user API uses const { data: courseProgressData, error: progressError } = await fetchProfileCourseProgress( @@ -164,7 +186,7 @@ async function getStudentOverview(courseId: string, member: any): Promise { diff --git a/apps/dashboard/src/routes/courses/[id]/analytics/+page.svelte b/apps/dashboard/src/routes/courses/[id]/analytics/+page.svelte index 593e6d5f..7303b059 100644 --- a/apps/dashboard/src/routes/courses/[id]/analytics/+page.svelte +++ b/apps/dashboard/src/routes/courses/[id]/analytics/+page.svelte @@ -28,17 +28,18 @@ isLoading = true; const accessToken = await getAccessToken(); - const response = await fetch('/api/analytics/course', { - method: 'POST', + const response = await fetch(`/api/courses/analytics?courseId=${data.courseId}`, { + method: 'GET', headers: { 'Content-Type': 'application/json', Authorization: accessToken - }, - body: JSON.stringify({ courseId: data.courseId }) + } }); if (response.ok) { - courseAnalytics = (await response.json()) as CourseAnalytics; + const { data } = await response.json(); + + courseAnalytics = data as CourseAnalytics; } else { console.error('Failed to fetch course analytics:', response); snackbar.error('Failed to load analytics data');