From ac4fdf1dd35772a20899e36f5bf55decb1e0ebda Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Fri, 5 Dec 2025 10:48:01 +0100 Subject: [PATCH] feat: migrate client-side database calls to secure server-side API endpoints - Create secure server-side API endpoints for course data, marks, exercises, newsfeeds, submissions, and organization management - Implement role-based permission checking using checkUserCoursePermissions utility - Move all client-side Supabase calls to authenticated server endpoints in /api/courses/ and /api/org/ - Add proper authentication and authorization middleware via hooks.server.ts - Update service functions to use getAccessToken() for API authentication - Ensure data filtering based on user roles (students only see their own data) - Maintain backward compatibility with existing component interfaces --- apps/dashboard/src/hooks.server.ts | 12 +- .../src/lib/components/Course/store.ts | 3 +- .../components/CourseContainer/index.svelte | 5 +- .../components/RoleBasedSecurity/index.svelte | 13 +- .../src/lib/utils/functions/permissions.ts | 56 ++++++++ .../src/lib/utils/services/courses/index.ts | 54 +++++++- .../src/lib/utils/services/marks/index.js | 7 - .../src/lib/utils/services/marks/index.ts | 26 ++++ .../src/lib/utils/services/newsfeed/index.ts | 58 ++++---- .../src/lib/utils/services/org/index.ts | 129 ++++++++---------- .../lib/utils/services/submissions/index.ts | 119 ++++++++-------- .../src/routes/api/courses/data/+server.ts | 123 +++++++++++++++++ .../routes/api/courses/exercises/+server.ts | 73 ++++++++++ .../src/routes/api/courses/marks/+server.ts | 66 +++++++++ .../routes/api/courses/newsfeed/+server.ts | 80 +++++++++++ .../routes/api/courses/submission/+server.ts | 92 +++++++++++++ .../routes/api/courses/submissions/+server.ts | 80 +++++++++++ .../src/routes/api/org/audience/+server.ts | 86 ++++++++++++ .../src/routes/api/org/team/+server.ts | 88 ++++++++++++ .../src/routes/courses/[id]/+page.js | 5 - .../src/routes/courses/[id]/+page.ts | 7 + .../routes/courses/[id]/people/+page.svelte | 4 +- 22 files changed, 986 insertions(+), 200 deletions(-) create mode 100644 apps/dashboard/src/lib/utils/functions/permissions.ts delete mode 100644 apps/dashboard/src/lib/utils/services/marks/index.js create mode 100644 apps/dashboard/src/lib/utils/services/marks/index.ts create mode 100644 apps/dashboard/src/routes/api/courses/data/+server.ts create mode 100644 apps/dashboard/src/routes/api/courses/exercises/+server.ts create mode 100644 apps/dashboard/src/routes/api/courses/marks/+server.ts create mode 100644 apps/dashboard/src/routes/api/courses/newsfeed/+server.ts create mode 100644 apps/dashboard/src/routes/api/courses/submission/+server.ts create mode 100644 apps/dashboard/src/routes/api/courses/submissions/+server.ts create mode 100644 apps/dashboard/src/routes/api/org/audience/+server.ts create mode 100644 apps/dashboard/src/routes/api/org/team/+server.ts delete mode 100644 apps/dashboard/src/routes/courses/[id]/+page.js create mode 100644 apps/dashboard/src/routes/courses/[id]/+page.ts diff --git a/apps/dashboard/src/hooks.server.ts b/apps/dashboard/src/hooks.server.ts index 0af239ff..75e14077 100644 --- a/apps/dashboard/src/hooks.server.ts +++ b/apps/dashboard/src/hooks.server.ts @@ -15,26 +15,23 @@ function isPublicRoute(pathname: string) { } export const handle: Handle = async ({ event, resolve }) => { - const response = await resolve(event); - const { pathname } = event.url; // Only validate API routes if (!pathname.includes('/api')) { - return response; + return resolve(event); } // Skip public routes if (isPublicRoute(pathname)) { - return response; + return resolve(event); } const accessToken = event.request.headers.get('Authorization')!; try { const user = await validateUser(accessToken); - - response.headers.set('user_id', `${user.id}`); + event.request.headers.set('user_id', `${user.id}`); } catch (error) { if (error instanceof Error) { if (error.message === 'Unauthenticated user') { @@ -47,6 +44,5 @@ export const handle: Handle = async ({ event, resolve }) => { } } } - - return response; + return resolve(event); }; diff --git a/apps/dashboard/src/lib/components/Course/store.ts b/apps/dashboard/src/lib/components/Course/store.ts index 21f28e48..9d291aa5 100644 --- a/apps/dashboard/src/lib/components/Course/store.ts +++ b/apps/dashboard/src/lib/components/Course/store.ts @@ -91,7 +91,8 @@ export async function setCourse(data: Course, setLesson = true) { // const tutorsById = {}; if (data.group) { - const groupData = Object.assign(data.group, { + const copiedGroup = JSON.parse(JSON.stringify(data.group)); + const groupData = Object.assign(copiedGroup, { tutors: [], students: [], people: [] diff --git a/apps/dashboard/src/lib/components/CourseContainer/index.svelte b/apps/dashboard/src/lib/components/CourseContainer/index.svelte index 901e583a..cb236c2f 100644 --- a/apps/dashboard/src/lib/components/CourseContainer/index.svelte +++ b/apps/dashboard/src/lib/components/CourseContainer/index.svelte @@ -8,7 +8,7 @@ import Confetti from '../Confetti/index.svelte'; import { isMobile } from '$lib/utils/store/useMobile'; import { profile } from '$lib/utils/store/user'; - import { fetchCourse } from '$lib/utils/services/courses'; + import { fetchCourseFromAPI } from '$lib/utils/services/courses'; import { globalStore } from '$lib/utils/store/app'; import { lessons } from '../Course/components/Lesson/store/lessons'; import Modal from '$lib/components/Modal/index.svelte'; @@ -32,7 +32,7 @@ course.set(defaultCourse); lessons.set([]); - const { data: _data } = await fetchCourse(courseId); + const { data: _data } = await fetchCourseFromAPI(courseId); if (_data) { $course.type = _data.type; @@ -52,7 +52,6 @@ $: onCourseIdChange(courseId); $: { - console.log('isOrgAdmin', $isOrgAdmin); const user = $group.people.find((person) => person.profile_id === $profile.id); if (user) { $globalStore.isStudent = user.role_id === 3; diff --git a/apps/dashboard/src/lib/components/RoleBasedSecurity/index.svelte b/apps/dashboard/src/lib/components/RoleBasedSecurity/index.svelte index e6b7779b..a75b3399 100644 --- a/apps/dashboard/src/lib/components/RoleBasedSecurity/index.svelte +++ b/apps/dashboard/src/lib/components/RoleBasedSecurity/index.svelte @@ -3,6 +3,7 @@ import { group } from '../Course/store'; import type { GroupPerson } from '$lib/utils/types'; import { isOrgAdmin } from '$lib/utils/store/org'; + import { browser } from '$app/environment'; export let allowedRoles: number[] = []; export let onDenied = () => {}; @@ -17,19 +18,21 @@ } $: { - const user: GroupPerson = $group.people.find((person) => person.profile_id === $profile.id)!; + if (browser) { + const user: GroupPerson = $group.people.find((person) => person.profile_id === $profile.id)!; - userRole = user ? user.role_id : userRole; + userRole = user ? user.role_id : userRole; - if (!$isOrgAdmin && $group.people.length && !isAllowed(userRole)) { - onDenied(); + if (!$isOrgAdmin && $group.people.length && !isAllowed(userRole)) { + onDenied(); + } } } $: if (onlyStudent) { show = isAllowed(userRole); } else { - show = isAllowed(userRole) || $isOrgAdmin; + show = isAllowed(userRole) || !!$isOrgAdmin; } diff --git a/apps/dashboard/src/lib/utils/functions/permissions.ts b/apps/dashboard/src/lib/utils/functions/permissions.ts new file mode 100644 index 00000000..dfd6beec --- /dev/null +++ b/apps/dashboard/src/lib/utils/functions/permissions.ts @@ -0,0 +1,56 @@ +import type { SupabaseClient } from '@supabase/supabase-js'; + +export interface UserPermissionResult { + hasAccess: boolean; + isOrgAdmin: boolean; + userMembership: any; + isStudent: boolean; +} + +/** + * Check if a user has access to a course and determine their role + */ +export async function checkUserCoursePermissions( + supabase: SupabaseClient, + userId: string, + courseGroupId: string +): Promise { + // Check if user is a member of the course group + const { data: userMembership } = await supabase + .from('groupmember') + .select('role_id, id, profile_id, email, created_at, assigned_student_id, profile(*)') + .eq('group_id', courseGroupId) + .eq('profile_id', userId) + .single(); + + // Check if user is org admin + const { data: orgData } = await supabase + .from('group') + .select('organization_id') + .eq('id', courseGroupId) + .single(); + + let isOrgAdmin = false; + if (orgData?.organization_id) { + const { data: orgMembership } = await supabase + .from('organizationmember') + .select('role_id') + .eq('organization_id', orgData.organization_id) + .eq('profile_id', userId) + .eq('role_id', 1) // Admin role + .single(); + + isOrgAdmin = !!orgMembership; + } + + // Check if user has access (either is course member or org admin) + const hasAccess = !!userMembership || isOrgAdmin; + const isStudent = userMembership?.role_id === 3 && !isOrgAdmin; + + return { + hasAccess, + isOrgAdmin, + userMembership, + isStudent + }; +} diff --git a/apps/dashboard/src/lib/utils/services/courses/index.ts b/apps/dashboard/src/lib/utils/services/courses/index.ts index 6b625bd4..5342cd4d 100644 --- a/apps/dashboard/src/lib/utils/services/courses/index.ts +++ b/apps/dashboard/src/lib/utils/services/courses/index.ts @@ -17,7 +17,7 @@ import { STATUS } from '$lib/utils/constants/course'; import { get } from 'svelte/store'; import { isOrgAdmin } from '$lib/utils/store/org'; import { isUUID } from '$lib/utils/functions/isUUID'; -import { supabase } from '$lib/utils/functions/supabase'; +import { supabase, getAccessToken } from '$lib/utils/functions/supabase'; export async function fetchCourses(profileId, orgId) { if (!orgId || !profileId) return; @@ -156,6 +156,33 @@ export async function fetchCourse(courseId?: Course['id'], slug?: Course['slug'] }; } +export async function fetchCourseFromAPI(courseId: Course['id']) { + try { + const accessToken = await getAccessToken(); + + const response = await fetch('/api/courses/data', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: accessToken + }, + body: JSON.stringify({ courseId }) + }); + + const result = await response.json(); + + if (!result.success) { + console.error('fetchCourseFromAPI error:', result.message); + return { data: null, error: result.error || result.message }; + } + + return { data: result.data, error: null }; + } catch (error) { + console.error('fetchCourseFromAPI error:', error); + return { data: null, error }; + } +} + export async function fetchExploreCourses(profileId, orgId) { if (!orgId || !profileId) return; @@ -379,8 +406,29 @@ export function createExercise(exercise: any) { return supabase.from('exercise').insert(exercise).select(); } -export function fetchExercisesByMarks(courseId: Course['id']) { - return supabase.rpc('get_exercises').eq('course_id', courseId); +export async function fetchExercisesByMarks(courseId: Course['id']) { + try { + const accessToken = await getAccessToken(); + + const response = await fetch('/api/courses/exercises', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: accessToken + }, + body: JSON.stringify({ courseId }) + }); + + const result = await response.json(); + + if (!result.success) { + return { data: null, error: result.message }; + } + + return { data: result.data, error: null }; + } catch (error) { + return { data: null, error }; + } } function isNew(item: any) { diff --git a/apps/dashboard/src/lib/utils/services/marks/index.js b/apps/dashboard/src/lib/utils/services/marks/index.js deleted file mode 100644 index 3f9807a2..00000000 --- a/apps/dashboard/src/lib/utils/services/marks/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import { supabase } from '$lib/utils/functions/supabase'; - -export async function fetchMarks(courseId) { - const { data, error } = await supabase.rpc('get_marks').eq('course_id', courseId); - - return { data, error }; -} diff --git a/apps/dashboard/src/lib/utils/services/marks/index.ts b/apps/dashboard/src/lib/utils/services/marks/index.ts new file mode 100644 index 00000000..90f9bb33 --- /dev/null +++ b/apps/dashboard/src/lib/utils/services/marks/index.ts @@ -0,0 +1,26 @@ +import { getAccessToken } from '$lib/utils/functions/supabase'; + +export async function fetchMarks(courseId: string) { + try { + const accessToken = await getAccessToken(); + + const response = await fetch('/api/courses/marks', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: accessToken + }, + body: JSON.stringify({ courseId }) + }); + + const result = await response.json(); + + if (!result.success) { + return { data: null, error: result.message }; + } + + return { data: result.data, error: null }; + } catch (error) { + return { data: null, error }; + } +} diff --git a/apps/dashboard/src/lib/utils/services/newsfeed/index.ts b/apps/dashboard/src/lib/utils/services/newsfeed/index.ts index f72d87a2..0b732d85 100644 --- a/apps/dashboard/src/lib/utils/services/newsfeed/index.ts +++ b/apps/dashboard/src/lib/utils/services/newsfeed/index.ts @@ -1,5 +1,5 @@ import type { Course } from '$lib/utils/types'; -import { supabase } from '$lib/utils/functions/supabase'; +import { supabase, getAccessToken } from '$lib/utils/functions/supabase'; import type { Reaction, FeedApi, Feed } from '$lib/utils/types/feed'; export async function fetchNewsFeedReaction(feedId: Feed['id']) { @@ -7,38 +7,34 @@ export async function fetchNewsFeedReaction(feedId: Feed['id']) { } export async function fetchNewsFeeds(courseId?: Course['id']) { - const response = await supabase - .from('course_newsfeed') - .select( - ` - id, - created_at, - content, - course_id, - author:groupmember( - profile( - id, - fullname, - avatar_url - ) - ), - reaction, - is_pinned, - comment:course_newsfeed_comment( - id, - created_at, - author:groupmember( profile(id, fullname, avatar_url) ), - content, - course_newsfeed_id) - ` - ) - .match({ course_id: courseId }) - .order('created_at', { ascending: false }) - .returns(); + const accessToken = await getAccessToken(); - const { data, error } = response; + const response = await fetch(`/api/courses/newsfeed?courseId=${courseId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: accessToken + } + }); - return { data, error }; + if (!response.ok) { + const error = await response.text(); + return { + data: null, + error: { message: error } + }; + } + + const { success, data, message } = await response.json(); + + if (!success) { + return { + data: null, + error: { message } + }; + } + + return { data, error: null }; } export async function createNewFeed(post: { diff --git a/apps/dashboard/src/lib/utils/services/org/index.ts b/apps/dashboard/src/lib/utils/services/org/index.ts index 300e7c52..dcdce02d 100644 --- a/apps/dashboard/src/lib/utils/services/org/index.ts +++ b/apps/dashboard/src/lib/utils/services/org/index.ts @@ -6,61 +6,41 @@ import type { OrganizationPlan } from '$lib/utils/types'; import type { PostgrestError } from '@supabase/supabase-js'; import { get } from 'svelte/store'; import { goto } from '$app/navigation'; -import { supabase } from '$lib/utils/functions/supabase'; +import { supabase, getAccessToken } from '$lib/utils/functions/supabase'; export async function getOrgTeam(orgId: string) { - const { data, error } = await supabase - .from('organizationmember') - .select( - ` - id, - email, - verified, - role_id, - profile( - id, - fullname, - email - ) - ` - ) - .eq('organization_id', orgId) - .neq('role_id', ROLE.STUDENT) - .order('id', { ascending: false }) - .returns< - { - id: number; - email: string; - verified: boolean; - role_id: number; - profile: { - id: string; - fullname: string; - email: string; - }; - }[] - >(); + const accessToken = await getAccessToken(); - const team: OrgTeamMember[] = []; - if (data?.length) { - data.forEach((teamMember) => { - team.push({ - id: teamMember.id, - email: teamMember?.profile?.email || teamMember.email, - verified: teamMember.verified, - profileId: teamMember?.profile?.id, - fullname: teamMember?.profile?.fullname || '', - role: ROLE_LABEL[teamMember?.role_id] || '', - isAdmin: teamMember?.role_id === ROLE.ADMIN - }); - }); + const response = await fetch(`/api/org/team?orgId=${orgId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: accessToken + } + }); - orgTeam.set(team); + if (!response.ok) { + const error = await response.text(); + return { + team: [], + error: { message: error } + }; } + const { success, team, message } = await response.json(); + + if (!success) { + return { + team: [], + error: { message } + }; + } + + orgTeam.set(team); + return { team: get(orgTeam), - error + error: null }; } @@ -146,39 +126,38 @@ export async function getOrganizations(userId: string, isOrgSite?: boolean, orgS } export async function getOrgAudience(orgId: string) { - // get all students who are participants in any course belonging to an org - const { data, error } = await supabase - .from('profile') - .select( - ` - id, - fullname, - email, - avatar_url, - created_at, - groupmember!inner( - role_id, - group_id:group!inner( - organization_id - ) - ) - ` - ) - .eq('groupmember.group.organization_id', orgId) - .eq('groupmember.role_id', ROLE.STUDENT); + const accessToken = await getAccessToken(); + + const response = await fetch(`/api/org/audience?orgId=${orgId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: accessToken + } + }); + + if (!response.ok) { + const error = await response.text(); + return { + audience: [], + error: { message: error } + }; + } + + const { audience, error } = await response.json(); + + if (error) { + return { + audience: [], + error + }; + } - const audience = (data || []).map((profile) => ({ - id: profile.id, - name: profile.fullname, - email: profile.email, - avatar_url: profile.avatar_url, - date_joined: new Date(profile.created_at).toDateString() - })); orgAudience.set(audience); return { audience: audience, - error + error: null }; } diff --git a/apps/dashboard/src/lib/utils/services/submissions/index.ts b/apps/dashboard/src/lib/utils/services/submissions/index.ts index ec886567..8a301e65 100644 --- a/apps/dashboard/src/lib/utils/services/submissions/index.ts +++ b/apps/dashboard/src/lib/utils/services/submissions/index.ts @@ -1,38 +1,39 @@ -import { supabase } from '$lib/utils/functions/supabase'; +import { supabase, getAccessToken } from '$lib/utils/functions/supabase'; import type { ExerciseSubmissions } from '$lib/utils/types'; export function fetchSubmissionStatus() { return supabase.from('submissionstatus').select(`*`); } -export function fetchSubmissions(course_id: string) { - return supabase - .from('submission') - .select( - ` - id, - created_at, - answers:question_answer(*), - exercise:exercise_id( - id, title, due_by, - lesson:lesson_id(id, title), - questions:question( - *, - options:option(*), - question_type:question_type_id(id, label) - ) - ), - status_id, - feedback, - course:course_id(*), - groupmember:submitted_by( - profile(*) - ) - ` - ) - .match({ - course_id - }); +export async function fetchSubmissions(course_id: string) { + const accessToken = await getAccessToken(); + + const response = await fetch(`/api/courses/submissions?courseId=${course_id}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: accessToken + } + }); + + if (!response.ok) { + const error = await response.text(); + return { + data: null, + error: { message: error } + }; + } + + const { success, data, message } = await response.json(); + + if (!success) { + return { + data: null, + error: { message } + }; + } + + return { data, error: null }; } export async function fetchSubmission({ @@ -44,40 +45,38 @@ export async function fetchSubmission({ courseId?: string; submittedBy?: string; }) { - const query: { - exercise_id: string; - course_id?: string; - submitted_by?: string; - } = { - exercise_id: exerciseId - }; + const accessToken = await getAccessToken(); - if (courseId) { - query.course_id = courseId; - } - if (submittedBy) { - query.submitted_by = submittedBy; + const params = new URLSearchParams({ exerciseId }); + if (courseId) params.append('courseId', courseId); + if (submittedBy) params.append('submittedBy', submittedBy); + + const response = await fetch(`/api/courses/submission?${params.toString()}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: accessToken + } + }); + + if (!response.ok) { + const error = await response.text(); + return { + data: null, + error: { message: error } + }; } - return supabase - .from('submission') - .select( - ` - id, - answers:question_answer(*), - status_id, - feedback, - submitted_by:groupmember!inner( - profile!inner( - id, - fullname, - avatar_url - ) - ) - ` - ) - .match(query) - .returns(); + const { success, data, message } = await response.json(); + + if (!success) { + return { + data: null, + error: { message } + }; + } + + return { data, error: null }; } export async function updateSubmission( diff --git a/apps/dashboard/src/routes/api/courses/data/+server.ts b/apps/dashboard/src/routes/api/courses/data/+server.ts new file mode 100644 index 00000000..ba00514b --- /dev/null +++ b/apps/dashboard/src/routes/api/courses/data/+server.ts @@ -0,0 +1,123 @@ +import type { RequestHandler } from './$types'; +import { json } from '@sveltejs/kit'; +import { getServerSupabase } from '$lib/utils/functions/supabase.server'; +import { STATUS } from '$lib/utils/constants/course'; +import { checkUserCoursePermissions } from '$lib/utils/functions/permissions'; + +const COURSE_SELECT_QUERY = ` + id, title, type, description, overview, logo, is_published, version, group_id, + slug, cost, currency, metadata, is_certificate_downloadable, certificate_theme, + lesson_section(id, title, order, created_at), + lessons:lesson( + id, title, public, lesson_at, is_unlocked, order, created_at, section_id, + note, videos, slide_url, call_url, totalExercises:exercise(count), + totalComments:lesson_comment(count), profile:teacher_id(id, avatar_url, fullname), + lesson_completion(id, profile_id, is_complete) + ), + attendance:group_attendance(*), + polls:apps_poll(status) +`; + +const GROUP_MEMBERS_SELECT = ` + id, + members:groupmember( + id, role_id, profile_id, email, created_at, assigned_student_id, profile(*) + ) +`; + +async function getCourse(supabase: any, courseId: string) { + const { data: course, error } = await supabase + .from('course') + .select(COURSE_SELECT_QUERY) + .eq('id', courseId) + .eq('status', STATUS[STATUS.ACTIVE]) + .single(); + + if (error || !course) { + throw new Error('Course not found'); + } + + return course; +} + +async function getGroupMembers( + supabase: any, + groupId: string, + isStudent: boolean, + userMembership: any +) { + if (isStudent) { + return { + id: groupId, + members: userMembership ? [userMembership] : [] + }; + } + + const { data: group, error } = await supabase + .from('group') + .select(GROUP_MEMBERS_SELECT) + .eq('id', groupId) + .single(); + + if (error) { + throw new Error('Error fetching group members'); + } + + return { + id: group.id, + members: group.members + }; +} + +export const POST: RequestHandler = async ({ request }) => { + const userId = request.headers.get('user_id'); + if (!userId) { + return json({ success: false, message: 'Unauthorized' }, { status: 401 }); + } + + const { courseId } = await request.json(); + if (!courseId) { + return json({ success: false, message: 'Course ID is required' }, { status: 400 }); + } + + try { + const supabase = getServerSupabase(); + const course = await getCourse(supabase, courseId); + + const { hasAccess, isStudent, userMembership } = await checkUserCoursePermissions( + supabase, + userId, + course.group_id + ); + + if (!hasAccess) { + return json( + { + success: false, + message: 'Access denied. User is not a member of this course.', + data: null + }, + { status: 403 } + ); + } + + const group = await getGroupMembers(supabase, course.group_id, isStudent, userMembership); + + return json({ + success: true, + data: { ...course, group } + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Internal server error'; + const status = message === 'Course not found' ? 404 : 500; + + return json( + { + success: false, + message, + data: null + }, + { status } + ); + } +}; diff --git a/apps/dashboard/src/routes/api/courses/exercises/+server.ts b/apps/dashboard/src/routes/api/courses/exercises/+server.ts new file mode 100644 index 00000000..8a7134e3 --- /dev/null +++ b/apps/dashboard/src/routes/api/courses/exercises/+server.ts @@ -0,0 +1,73 @@ +import type { RequestHandler } from './$types'; +import { json } from '@sveltejs/kit'; +import { getServerSupabase } from '$lib/utils/functions/supabase.server'; +import { checkUserCoursePermissions } from '$lib/utils/functions/permissions'; + +export const POST: RequestHandler = async ({ request }) => { + const userId = request.headers.get('user_id'); + const { courseId } = await request.json(); + + if (!courseId) { + return json({ success: false, message: 'Course ID is required' }, { status: 400 }); + } + + if (!userId) { + return json({ success: false, message: 'User ID is required' }, { status: 401 }); + } + + try { + const supabase = getServerSupabase(); + + // Get course group_id for permission check + const { data: course } = await supabase + .from('course') + .select('group_id') + .eq('id', courseId) + .single(); + + if (!course) { + return json({ success: false, message: 'Course not found' }, { status: 404 }); + } + + const { hasAccess } = await checkUserCoursePermissions(supabase, userId, course.group_id); + + if (!hasAccess) { + return json( + { + success: false, + message: 'Access denied' + }, + { status: 403 } + ); + } + + const { data: exercises, error } = await supabase + .rpc('get_exercises') + .eq('course_id', courseId); + + if (error) { + throw new Error('Error fetching exercises'); + } + + const formattedExercises = exercises.map((exercise) => ({ + lesson_id: exercise.lesson_id, + exercise_id: exercise.id, + exercise_title: exercise.title, + points: exercise.points + })); + + return json({ + success: true, + data: formattedExercises + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Internal server error'; + return json( + { + success: false, + message + }, + { status: 500 } + ); + } +}; diff --git a/apps/dashboard/src/routes/api/courses/marks/+server.ts b/apps/dashboard/src/routes/api/courses/marks/+server.ts new file mode 100644 index 00000000..56f7ec5c --- /dev/null +++ b/apps/dashboard/src/routes/api/courses/marks/+server.ts @@ -0,0 +1,66 @@ +import type { RequestHandler } from './$types'; +import { json } from '@sveltejs/kit'; +import { getServerSupabase } from '$lib/utils/functions/supabase.server'; +import { checkUserCoursePermissions } from '$lib/utils/functions/permissions'; + +export const POST: RequestHandler = async ({ request }) => { + const userId = request.headers.get('user_id'); + const { courseId } = await request.json(); + + if (!courseId) { + return json({ success: false, message: 'Course ID is required' }, { status: 400 }); + } + + if (!userId) { + return json({ success: false, message: 'User ID is required' }, { status: 401 }); + } + + try { + const supabase = getServerSupabase(); + + // Get course group_id for permission check + const { data: course } = await supabase + .from('course') + .select('group_id') + .eq('id', courseId) + .single(); + + if (!course) { + return json({ success: false, message: 'Course not found' }, { status: 404 }); + } + + const { hasAccess } = await checkUserCoursePermissions(supabase, userId, course.group_id); + + if (!hasAccess) { + return json( + { + success: false, + message: 'Access denied' + }, + { status: 403 } + ); + } + + const { data: marks, error } = await supabase.rpc('get_marks').eq('course_id', courseId); + + if (error) { + console.log('error', error); + throw new Error('Error fetching marks'); + } + + return json({ + success: true, + data: marks + }); + } catch (error) { + console.log('error', error); + const message = error instanceof Error ? error.message : 'Internal server error'; + return json( + { + success: false, + message + }, + { status: 500 } + ); + } +}; diff --git a/apps/dashboard/src/routes/api/courses/newsfeed/+server.ts b/apps/dashboard/src/routes/api/courses/newsfeed/+server.ts new file mode 100644 index 00000000..f4588d02 --- /dev/null +++ b/apps/dashboard/src/routes/api/courses/newsfeed/+server.ts @@ -0,0 +1,80 @@ +import type { RequestHandler } from './$types'; +import { json } from '@sveltejs/kit'; +import { getServerSupabase } from '$lib/utils/functions/supabase.server'; +import { checkUserCoursePermissions } from '$lib/utils/functions/permissions'; + +export const GET: RequestHandler = async ({ request, url }) => { + const courseId = url.searchParams.get('courseId'); + const userId = request.headers.get('user_id'); + + if (!userId) { + return json({ success: false, message: 'Unauthorized' }, { status: 401 }); + } + + if (!courseId) { + return json({ success: false, message: 'Course ID is required' }, { status: 400 }); + } + + try { + 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 } + ); + } + + // Fetch newsfeeds + const { data, error } = await supabase + .from('course_newsfeed') + .select( + ` + id, + created_at, + content, + course_id, + author:groupmember( + profile( + id, + fullname, + avatar_url + ) + ), + reaction, + is_pinned, + comment:course_newsfeed_comment( + id, + created_at, + author:groupmember( profile(id, fullname, avatar_url) ), + content, + course_newsfeed_id) + ` + ) + .match({ course_id: courseId }) + .order('created_at', { ascending: false }); + + if (error) { + throw new Error('Error fetching newsfeeds'); + } + + return json({ + success: true, + data: data || [] + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Internal server error'; + return json( + { + success: false, + message + }, + { status: 500 } + ); + } +}; diff --git a/apps/dashboard/src/routes/api/courses/submission/+server.ts b/apps/dashboard/src/routes/api/courses/submission/+server.ts new file mode 100644 index 00000000..72b1131f --- /dev/null +++ b/apps/dashboard/src/routes/api/courses/submission/+server.ts @@ -0,0 +1,92 @@ +import type { RequestHandler } from './$types'; +import { json } from '@sveltejs/kit'; +import { getServerSupabase } from '$lib/utils/functions/supabase.server'; +import { checkUserCoursePermissions } from '$lib/utils/functions/permissions'; + +export const GET: RequestHandler = async ({ request, url }) => { + const exerciseId = url.searchParams.get('exerciseId'); + const courseId = url.searchParams.get('courseId'); + const submittedBy = url.searchParams.get('submittedBy'); + const userId = request.headers.get('user_id'); + + if (!userId) { + return json({ success: false, message: 'Unauthorized' }, { status: 401 }); + } + + if (!exerciseId) { + return json({ success: false, message: 'Exercise ID is required' }, { status: 400 }); + } + + try { + const supabase = getServerSupabase(); + + // If courseId is provided, check permissions + if (courseId) { + 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 } + ); + } + } + + // Build query object + const query: { + exercise_id: string; + course_id?: string; + submitted_by?: string; + } = { + exercise_id: exerciseId + }; + + if (courseId) { + query.course_id = courseId; + } + if (submittedBy) { + query.submitted_by = submittedBy; + } + + // Fetch submission + const { data, error } = await supabase + .from('submission') + .select( + ` + id, + answers:question_answer(*), + status_id, + feedback, + submitted_by:groupmember!inner( + profile!inner( + id, + fullname, + avatar_url + ) + ) + ` + ) + .match(query); + + if (error) { + throw new Error('Error fetching submission'); + } + + return json({ + success: true, + data: data || [] + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Internal server error'; + return json( + { + success: false, + message + }, + { status: 500 } + ); + } +}; diff --git a/apps/dashboard/src/routes/api/courses/submissions/+server.ts b/apps/dashboard/src/routes/api/courses/submissions/+server.ts new file mode 100644 index 00000000..4508b879 --- /dev/null +++ b/apps/dashboard/src/routes/api/courses/submissions/+server.ts @@ -0,0 +1,80 @@ +import type { RequestHandler } from './$types'; +import { json } from '@sveltejs/kit'; +import { getServerSupabase } from '$lib/utils/functions/supabase.server'; +import { checkUserCoursePermissions } from '$lib/utils/functions/permissions'; + +export const GET: RequestHandler = async ({ request, url }) => { + const courseId = url.searchParams.get('courseId'); + const userId = request.headers.get('user_id'); + + if (!userId) { + return json({ success: false, message: 'Unauthorized' }, { status: 401 }); + } + + if (!courseId) { + return json({ success: false, message: 'Course ID is required' }, { status: 400 }); + } + + try { + 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 } + ); + } + + // Fetch submissions + const { data, error } = await supabase + .from('submission') + .select( + ` + id, + created_at, + answers:question_answer(*), + exercise:exercise_id( + id, title, due_by, + lesson:lesson_id(id, title), + questions:question( + *, + options:option(*), + question_type:question_type_id(id, label) + ) + ), + status_id, + feedback, + course:course_id(*), + groupmember:submitted_by( + profile(*) + ) + ` + ) + .match({ + course_id: courseId + }); + + if (error) { + throw new Error('Error fetching submissions'); + } + + return json({ + success: true, + data: data || [] + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Internal server error'; + return json( + { + success: false, + message + }, + { status: 500 } + ); + } +}; diff --git a/apps/dashboard/src/routes/api/org/audience/+server.ts b/apps/dashboard/src/routes/api/org/audience/+server.ts new file mode 100644 index 00000000..65cf862f --- /dev/null +++ b/apps/dashboard/src/routes/api/org/audience/+server.ts @@ -0,0 +1,86 @@ +import type { RequestHandler } from './$types'; +import { json } from '@sveltejs/kit'; +import { getServerSupabase } from '$lib/utils/functions/supabase.server'; + +export const GET: RequestHandler = async ({ request, url }) => { + const userId = request.headers.get('user_id'); + const orgId = url.searchParams.get('orgId'); + + if (!orgId) { + return json({ success: false, message: 'Organization ID is required' }, { status: 400 }); + } + + if (!userId) { + return json({ success: false, message: 'User ID is required' }, { status: 401 }); + } + + try { + const supabase = getServerSupabase(); + + // Check if user has access to this organization + const { data: orgMember } = await supabase + .from('organizationmember') + .select('role_id') + .eq('organization_id', orgId) + .eq('profile_id', userId) + .in('role_id', [1, 2]) // Admin or Member roles + .single(); + + if (!orgMember) { + return json( + { + success: false, + message: 'Access denied. User is not a member of this organization.' + }, + { status: 403 } + ); + } + + // Get all students who are participants in any course belonging to an org + const { data, error } = await supabase + .from('profile') + .select( + ` + id, + fullname, + email, + avatar_url, + created_at, + groupmember!inner( + role_id, + group_id:group!inner( + organization_id + ) + ) + ` + ) + .eq('groupmember.group.organization_id', orgId) + .eq('groupmember.role_id', 3); // STUDENT role + + if (error) { + throw new Error('Error fetching organization audience'); + } + + const audience = (data || []).map((profile) => ({ + id: profile.id, + name: profile.fullname, + email: profile.email, + avatar_url: profile.avatar_url, + date_joined: new Date(profile.created_at).toDateString() + })); + + return json({ + success: true, + audience: audience + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Internal server error'; + return json( + { + success: false, + message + }, + { status: 500 } + ); + } +}; diff --git a/apps/dashboard/src/routes/api/org/team/+server.ts b/apps/dashboard/src/routes/api/org/team/+server.ts new file mode 100644 index 00000000..66f7925b --- /dev/null +++ b/apps/dashboard/src/routes/api/org/team/+server.ts @@ -0,0 +1,88 @@ +import type { RequestHandler } from './$types'; +import { json } from '@sveltejs/kit'; +import { getServerSupabase } from '$lib/utils/functions/supabase.server'; +import { ROLE, ROLE_LABEL } from '$lib/utils/constants/roles'; + +export const GET: RequestHandler = async ({ request, url }) => { + const orgId = url.searchParams.get('orgId'); + const userId = request.headers.get('user_id'); + + if (!userId) { + return json({ success: false, message: 'Unauthorized' }, { status: 401 }); + } + + if (!orgId) { + return json({ success: false, message: 'Organization ID is required' }, { status: 400 }); + } + + try { + const supabase = getServerSupabase(); + + // Check if user has access to this organization + const { data: orgMember } = await supabase + .from('organizationmember') + .select('role_id') + .eq('organization_id', orgId) + .eq('profile_id', userId) + .in('role_id', [1]) // Admin roles only + .single(); + + if (!orgMember) { + return json( + { + success: false, + message: 'Access denied. User is not a member of this organization.' + }, + { status: 403 } + ); + } + + // Fetch organization team + const { data, error } = await supabase + .from('organizationmember') + .select( + ` + id, + email, + verified, + role_id, + profile( + id, + fullname, + email + ) + ` + ) + .eq('organization_id', orgId) + .neq('role_id', ROLE.STUDENT) + .order('id', { ascending: false }); + + if (error) { + throw new Error('Error fetching organization team'); + } + + const team = (data || []).map((teamMember) => ({ + id: teamMember.id, + email: teamMember?.profile?.email || teamMember.email, + verified: teamMember.verified, + profileId: teamMember?.profile?.id, + fullname: teamMember?.profile?.fullname || '', + role: ROLE_LABEL[teamMember?.role_id] || '', + isAdmin: teamMember?.role_id === ROLE.ADMIN + })); + + return json({ + success: true, + team: team + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Internal server error'; + return json( + { + success: false, + message + }, + { status: 500 } + ); + } +}; diff --git a/apps/dashboard/src/routes/courses/[id]/+page.js b/apps/dashboard/src/routes/courses/[id]/+page.js deleted file mode 100644 index 4190b349..00000000 --- a/apps/dashboard/src/routes/courses/[id]/+page.js +++ /dev/null @@ -1,5 +0,0 @@ -export const load = ({ params = { id: '' } }) => { - return { - courseId: params.id - }; -}; diff --git a/apps/dashboard/src/routes/courses/[id]/+page.ts b/apps/dashboard/src/routes/courses/[id]/+page.ts new file mode 100644 index 00000000..209dbc0f --- /dev/null +++ b/apps/dashboard/src/routes/courses/[id]/+page.ts @@ -0,0 +1,7 @@ +import { browser } from '$app/environment'; + +export const load = async ({ params = { id: '' } }) => { + return { + courseId: params.id + }; +}; diff --git a/apps/dashboard/src/routes/courses/[id]/people/+page.svelte b/apps/dashboard/src/routes/courses/[id]/people/+page.svelte index 2c9c2f24..ac9b5ab9 100644 --- a/apps/dashboard/src/routes/courses/[id]/people/+page.svelte +++ b/apps/dashboard/src/routes/courses/[id]/people/+page.svelte @@ -117,7 +117,7 @@ --> -