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
This commit is contained in:
@@ -15,26 +15,23 @@ function isPublicRoute(pathname: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
const response = await resolve(event);
|
|
||||||
|
|
||||||
const { pathname } = event.url;
|
const { pathname } = event.url;
|
||||||
|
|
||||||
// Only validate API routes
|
// Only validate API routes
|
||||||
if (!pathname.includes('/api')) {
|
if (!pathname.includes('/api')) {
|
||||||
return response;
|
return resolve(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip public routes
|
// Skip public routes
|
||||||
if (isPublicRoute(pathname)) {
|
if (isPublicRoute(pathname)) {
|
||||||
return response;
|
return resolve(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = event.request.headers.get('Authorization')!;
|
const accessToken = event.request.headers.get('Authorization')!;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await validateUser(accessToken);
|
const user = await validateUser(accessToken);
|
||||||
|
event.request.headers.set('user_id', `${user.id}`);
|
||||||
response.headers.set('user_id', `${user.id}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
if (error.message === 'Unauthenticated user') {
|
if (error.message === 'Unauthenticated user') {
|
||||||
@@ -47,6 +44,5 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return resolve(event);
|
||||||
return response;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ export async function setCourse(data: Course, setLesson = true) {
|
|||||||
// const tutorsById = {};
|
// const tutorsById = {};
|
||||||
|
|
||||||
if (data.group) {
|
if (data.group) {
|
||||||
const groupData = Object.assign(data.group, {
|
const copiedGroup = JSON.parse(JSON.stringify(data.group));
|
||||||
|
const groupData = Object.assign(copiedGroup, {
|
||||||
tutors: [],
|
tutors: [],
|
||||||
students: [],
|
students: [],
|
||||||
people: []
|
people: []
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Confetti from '../Confetti/index.svelte';
|
import Confetti from '../Confetti/index.svelte';
|
||||||
import { isMobile } from '$lib/utils/store/useMobile';
|
import { isMobile } from '$lib/utils/store/useMobile';
|
||||||
import { profile } from '$lib/utils/store/user';
|
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 { globalStore } from '$lib/utils/store/app';
|
||||||
import { lessons } from '../Course/components/Lesson/store/lessons';
|
import { lessons } from '../Course/components/Lesson/store/lessons';
|
||||||
import Modal from '$lib/components/Modal/index.svelte';
|
import Modal from '$lib/components/Modal/index.svelte';
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
course.set(defaultCourse);
|
course.set(defaultCourse);
|
||||||
lessons.set([]);
|
lessons.set([]);
|
||||||
|
|
||||||
const { data: _data } = await fetchCourse(courseId);
|
const { data: _data } = await fetchCourseFromAPI(courseId);
|
||||||
|
|
||||||
if (_data) {
|
if (_data) {
|
||||||
$course.type = _data.type;
|
$course.type = _data.type;
|
||||||
@@ -52,7 +52,6 @@
|
|||||||
$: onCourseIdChange(courseId);
|
$: onCourseIdChange(courseId);
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
console.log('isOrgAdmin', $isOrgAdmin);
|
|
||||||
const user = $group.people.find((person) => person.profile_id === $profile.id);
|
const user = $group.people.find((person) => person.profile_id === $profile.id);
|
||||||
if (user) {
|
if (user) {
|
||||||
$globalStore.isStudent = user.role_id === 3;
|
$globalStore.isStudent = user.role_id === 3;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { group } from '../Course/store';
|
import { group } from '../Course/store';
|
||||||
import type { GroupPerson } from '$lib/utils/types';
|
import type { GroupPerson } from '$lib/utils/types';
|
||||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
export let allowedRoles: number[] = [];
|
export let allowedRoles: number[] = [];
|
||||||
export let onDenied = () => {};
|
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)) {
|
if (!$isOrgAdmin && $group.people.length && !isAllowed(userRole)) {
|
||||||
onDenied();
|
onDenied();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (onlyStudent) {
|
$: if (onlyStudent) {
|
||||||
show = isAllowed(userRole);
|
show = isAllowed(userRole);
|
||||||
} else {
|
} else {
|
||||||
show = isAllowed(userRole) || $isOrgAdmin;
|
show = isAllowed(userRole) || !!$isOrgAdmin;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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<UserPermissionResult> {
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import { STATUS } from '$lib/utils/constants/course';
|
|||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||||
import { isUUID } from '$lib/utils/functions/isUUID';
|
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) {
|
export async function fetchCourses(profileId, orgId) {
|
||||||
if (!orgId || !profileId) return;
|
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) {
|
export async function fetchExploreCourses(profileId, orgId) {
|
||||||
if (!orgId || !profileId) return;
|
if (!orgId || !profileId) return;
|
||||||
|
|
||||||
@@ -379,8 +406,29 @@ export function createExercise(exercise: any) {
|
|||||||
return supabase.from('exercise').insert(exercise).select();
|
return supabase.from('exercise').insert(exercise).select();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchExercisesByMarks(courseId: Course['id']) {
|
export async function fetchExercisesByMarks(courseId: Course['id']) {
|
||||||
return supabase.rpc('get_exercises').eq('course_id', courseId);
|
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) {
|
function isNew(item: any) {
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Course } from '$lib/utils/types';
|
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';
|
import type { Reaction, FeedApi, Feed } from '$lib/utils/types/feed';
|
||||||
|
|
||||||
export async function fetchNewsFeedReaction(feedId: Feed['id']) {
|
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']) {
|
export async function fetchNewsFeeds(courseId?: Course['id']) {
|
||||||
const response = await supabase
|
const accessToken = await getAccessToken();
|
||||||
.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<FeedApi[]>();
|
|
||||||
|
|
||||||
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: {
|
export async function createNewFeed(post: {
|
||||||
|
|||||||
@@ -6,61 +6,41 @@ import type { OrganizationPlan } from '$lib/utils/types';
|
|||||||
import type { PostgrestError } from '@supabase/supabase-js';
|
import type { PostgrestError } from '@supabase/supabase-js';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { goto } from '$app/navigation';
|
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) {
|
export async function getOrgTeam(orgId: string) {
|
||||||
const { data, error } = await supabase
|
const accessToken = await getAccessToken();
|
||||||
.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 team: OrgTeamMember[] = [];
|
const response = await fetch(`/api/org/team?orgId=${orgId}`, {
|
||||||
if (data?.length) {
|
method: 'GET',
|
||||||
data.forEach((teamMember) => {
|
headers: {
|
||||||
team.push({
|
'Content-Type': 'application/json',
|
||||||
id: teamMember.id,
|
Authorization: accessToken
|
||||||
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
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
team: get(orgTeam),
|
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) {
|
export async function getOrgAudience(orgId: string) {
|
||||||
// get all students who are participants in any course belonging to an org
|
const accessToken = await getAccessToken();
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('profile')
|
const response = await fetch(`/api/org/audience?orgId=${orgId}`, {
|
||||||
.select(
|
method: 'GET',
|
||||||
`
|
headers: {
|
||||||
id,
|
'Content-Type': 'application/json',
|
||||||
fullname,
|
Authorization: accessToken
|
||||||
email,
|
}
|
||||||
avatar_url,
|
});
|
||||||
created_at,
|
|
||||||
groupmember!inner(
|
if (!response.ok) {
|
||||||
role_id,
|
const error = await response.text();
|
||||||
group_id:group!inner(
|
return {
|
||||||
organization_id
|
audience: [],
|
||||||
)
|
error: { message: error }
|
||||||
)
|
};
|
||||||
`
|
}
|
||||||
)
|
|
||||||
.eq('groupmember.group.organization_id', orgId)
|
const { audience, error } = await response.json();
|
||||||
.eq('groupmember.role_id', ROLE.STUDENT);
|
|
||||||
|
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);
|
orgAudience.set(audience);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
audience: audience,
|
audience: audience,
|
||||||
error
|
error: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
import type { ExerciseSubmissions } from '$lib/utils/types';
|
||||||
|
|
||||||
export function fetchSubmissionStatus() {
|
export function fetchSubmissionStatus() {
|
||||||
return supabase.from('submissionstatus').select(`*`);
|
return supabase.from('submissionstatus').select(`*`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchSubmissions(course_id: string) {
|
export async function fetchSubmissions(course_id: string) {
|
||||||
return supabase
|
const accessToken = await getAccessToken();
|
||||||
.from('submission')
|
|
||||||
.select(
|
const response = await fetch(`/api/courses/submissions?courseId=${course_id}`, {
|
||||||
`
|
method: 'GET',
|
||||||
id,
|
headers: {
|
||||||
created_at,
|
'Content-Type': 'application/json',
|
||||||
answers:question_answer(*),
|
Authorization: accessToken
|
||||||
exercise:exercise_id(
|
}
|
||||||
id, title, due_by,
|
});
|
||||||
lesson:lesson_id(id, title),
|
|
||||||
questions:question(
|
if (!response.ok) {
|
||||||
*,
|
const error = await response.text();
|
||||||
options:option(*),
|
return {
|
||||||
question_type:question_type_id(id, label)
|
data: null,
|
||||||
)
|
error: { message: error }
|
||||||
),
|
};
|
||||||
status_id,
|
}
|
||||||
feedback,
|
|
||||||
course:course_id(*),
|
const { success, data, message } = await response.json();
|
||||||
groupmember:submitted_by(
|
|
||||||
profile(*)
|
if (!success) {
|
||||||
)
|
return {
|
||||||
`
|
data: null,
|
||||||
)
|
error: { message }
|
||||||
.match({
|
};
|
||||||
course_id
|
}
|
||||||
});
|
|
||||||
|
return { data, error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchSubmission({
|
export async function fetchSubmission({
|
||||||
@@ -44,40 +45,38 @@ export async function fetchSubmission({
|
|||||||
courseId?: string;
|
courseId?: string;
|
||||||
submittedBy?: string;
|
submittedBy?: string;
|
||||||
}) {
|
}) {
|
||||||
const query: {
|
const accessToken = await getAccessToken();
|
||||||
exercise_id: string;
|
|
||||||
course_id?: string;
|
|
||||||
submitted_by?: string;
|
|
||||||
} = {
|
|
||||||
exercise_id: exerciseId
|
|
||||||
};
|
|
||||||
|
|
||||||
if (courseId) {
|
const params = new URLSearchParams({ exerciseId });
|
||||||
query.course_id = courseId;
|
if (courseId) params.append('courseId', courseId);
|
||||||
}
|
if (submittedBy) params.append('submittedBy', submittedBy);
|
||||||
if (submittedBy) {
|
|
||||||
query.submitted_by = 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
|
const { success, data, message } = await response.json();
|
||||||
.from('submission')
|
|
||||||
.select(
|
if (!success) {
|
||||||
`
|
return {
|
||||||
id,
|
data: null,
|
||||||
answers:question_answer(*),
|
error: { message }
|
||||||
status_id,
|
};
|
||||||
feedback,
|
}
|
||||||
submitted_by:groupmember!inner(
|
|
||||||
profile!inner(
|
return { data, error: null };
|
||||||
id,
|
|
||||||
fullname,
|
|
||||||
avatar_url
|
|
||||||
)
|
|
||||||
)
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.match(query)
|
|
||||||
.returns<ExerciseSubmissions[]>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSubmission(
|
export async function updateSubmission(
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export const load = ({ params = { id: '' } }) => {
|
|
||||||
return {
|
|
||||||
courseId: params.id
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
export const load = async ({ params = { id: '' } }) => {
|
||||||
|
return {
|
||||||
|
courseId: params.id
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
</select> -->
|
</select> -->
|
||||||
</div>
|
</div>
|
||||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||||
<p class="hidden w-20 text-lg dark:text-white lg:block" />
|
<p class="hidden w-20 text-lg lg:block dark:text-white" />
|
||||||
</RoleBasedSecurity>
|
</RoleBasedSecurity>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@
|
|||||||
>{$t('course.navItem.people.action')}</StructuredListCell
|
>{$t('course.navItem.people.action')}</StructuredListCell
|
||||||
>
|
>
|
||||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||||
<p class="hidden w-20 text-lg dark:text-white lg:block" />
|
<p class="hidden w-20 text-lg lg:block dark:text-white" />
|
||||||
</RoleBasedSecurity>
|
</RoleBasedSecurity>
|
||||||
</StructuredListRow>
|
</StructuredListRow>
|
||||||
</StructuredListHead>
|
</StructuredListHead>
|
||||||
|
|||||||
Reference in New Issue
Block a user