Merge branch 'main' of https://github.com/classroomio/classroomio into feat/release-v2
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const authHeader = request.headers.get('authorization');
|
||||
const cronSecret = process.env.CRON_SECRET;
|
||||
|
||||
if (!cronSecret || authHeader !== `Bearer ${cronSecret}`) {
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Unauthorized'
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const supabase = getServerSupabase();
|
||||
|
||||
try {
|
||||
// Clean up expired tokens
|
||||
const { data: cleanupResult, error } = await supabase.rpc(
|
||||
'cleanup_expired_verification_tokens'
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error('Token cleanup failed:', error);
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Token cleanup completed:', {
|
||||
deletedTokens: cleanupResult,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
deletedTokens: cleanupResult,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Unexpected error during cleanup:', error);
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Unexpected error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Health check endpoint
|
||||
export async function GET() {
|
||||
return json({
|
||||
service: 'email-verification-cleanup',
|
||||
status: 'active',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
||||
|
||||
export async function GET({ url, request }) {
|
||||
// Simple auth check - in production this should be more robust
|
||||
const authHeader = request.headers.get('authorization');
|
||||
const adminSecret = process.env.ADMIN_SECRET;
|
||||
|
||||
if (!adminSecret || authHeader !== `Bearer ${adminSecret}`) {
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Unauthorized'
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const supabase = getServerSupabase();
|
||||
|
||||
try {
|
||||
// Get verification token statistics
|
||||
const { data: tokenStats, error: tokenError } = await supabase
|
||||
.from('email_verification_tokens')
|
||||
.select('id, created_at, expires_at, used_at, created_by_ip, used_by_ip, email')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(100);
|
||||
|
||||
if (tokenError) {
|
||||
console.error('Failed to fetch token stats:', tokenError);
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Database error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate stats
|
||||
const now = new Date();
|
||||
const last24Hours = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
const recent = tokenStats.filter((token) => new Date(token.created_at) > last24Hours);
|
||||
const used = tokenStats.filter((token) => token.used_at);
|
||||
const expired = tokenStats.filter(
|
||||
(token) => new Date(token.expires_at) < now && !token.used_at
|
||||
);
|
||||
|
||||
// Count unique IPs for anomaly detection
|
||||
const uniqueCreationIPs = new Set(tokenStats.map((t) => t.created_by_ip)).size;
|
||||
const uniqueUsageIPs = new Set(tokenStats.filter((t) => t.used_by_ip).map((t) => t.used_by_ip))
|
||||
.size;
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
stats: {
|
||||
total_tokens: tokenStats.length,
|
||||
recent_24h: recent.length,
|
||||
used_tokens: used.length,
|
||||
expired_unused: expired.length,
|
||||
unique_creation_ips: uniqueCreationIPs,
|
||||
unique_usage_ips: uniqueUsageIPs,
|
||||
success_rate:
|
||||
tokenStats.length > 0 ? ((used.length / tokenStats.length) * 100).toFixed(1) : 0
|
||||
},
|
||||
recent_activity: recent.slice(0, 10).map((token) => ({
|
||||
created_at: token.created_at,
|
||||
email: token.email.replace(/(.{2}).*@/, '$1***@'), // Mask email for privacy
|
||||
used: !!token.used_at,
|
||||
expired: new Date(token.expires_at) < now,
|
||||
ip: token.created_by_ip
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Security monitoring error:', error);
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Unexpected error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
+51
-22
@@ -1,37 +1,56 @@
|
||||
import type { CourseAnalytics, StudentOverview } from '$lib/utils/types/analytics';
|
||||
|
||||
import type { RequestHandler } from './$types';
|
||||
import type { UserExercisesStats } from '$lib/utils/types/analytics';
|
||||
import { calcPercentageWithRounding } from '$lib/utils/functions/number.js';
|
||||
import { checkUserCoursePermissions } from '$lib/utils/functions/permissions';
|
||||
import { fetchProfileCourseProgress } from '$lib/utils/services/courses';
|
||||
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
||||
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<CourseAnalytics> {
|
||||
async function getCourseAnalytics(supabase: any, courseId: string): Promise<CourseAnalytics> {
|
||||
const analytics: CourseAnalytics = {
|
||||
totalTutors: 0,
|
||||
totalStudents: 0,
|
||||
@@ -89,7 +108,9 @@ async function getCourseAnalytics(courseId: string): Promise<CourseAnalytics> {
|
||||
|
||||
// Get student analytics
|
||||
const studentAnalytics = await Promise.all(
|
||||
members.filter((member) => member.role_id === 3).map((member) => getStudentOverview(courseId, member))
|
||||
members
|
||||
.filter((member) => member.role_id === 3)
|
||||
.map((member) => getStudentOverview(supabase, courseId, member))
|
||||
);
|
||||
|
||||
analytics.students = studentAnalytics.filter((student): student is StudentOverview => student !== null);
|
||||
@@ -116,7 +137,7 @@ async function getCourseAnalytics(courseId: string): Promise<CourseAnalytics> {
|
||||
return analytics;
|
||||
}
|
||||
|
||||
async function getLastLogin(userId: string): Promise<string | undefined> {
|
||||
async function getLastLogin(supabase: any, userId: string): Promise<string | undefined> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('analytics_login_events')
|
||||
@@ -134,7 +155,11 @@ async function getLastLogin(userId: string): Promise<string | undefined> {
|
||||
}
|
||||
}
|
||||
|
||||
async function getStudentOverview(courseId: string, member: any): Promise<StudentOverview | null> {
|
||||
async function getStudentOverview(
|
||||
supabase: any,
|
||||
courseId: string,
|
||||
member: any
|
||||
): Promise<StudentOverview | null> {
|
||||
try {
|
||||
// Use the same service function that the working analytics/user API uses
|
||||
const { data: courseProgressData, error: progressError } = await fetchProfileCourseProgress(
|
||||
@@ -154,14 +179,14 @@ async function getStudentOverview(courseId: string, member: any): Promise<Studen
|
||||
}
|
||||
|
||||
// Fetch exercise stats using the same method as the people page
|
||||
const userExercisesStats = await fetchUserExercisesStats(courseId, member.profile_id);
|
||||
const userExercisesStats = await fetchUserExercisesStats(supabase, courseId, member.profile_id);
|
||||
|
||||
// Calculate exercise completion using the same logic as people page
|
||||
const completedExercises = userExercisesStats?.filter((exercise) => exercise.isCompleted)?.length || 0;
|
||||
const totalExercises = userExercisesStats?.length || 0;
|
||||
|
||||
// Get last login using the same approach as analytics/user API
|
||||
const lastLoginDate = await getLastLogin(member.profile_id);
|
||||
const lastLoginDate = await getLastLogin(supabase, member.profile_id);
|
||||
|
||||
// Format last seen
|
||||
let lastSeen = 'Never';
|
||||
@@ -211,7 +236,11 @@ async function getStudentOverview(courseId: string, member: any): Promise<Studen
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUserExercisesStats(courseId: string, userId: string): Promise<UserExercisesStats[] | undefined> {
|
||||
async function fetchUserExercisesStats(
|
||||
supabase: any,
|
||||
courseId: string,
|
||||
userId: string
|
||||
): Promise<UserExercisesStats[] | undefined> {
|
||||
try {
|
||||
const { data: courseData, error: queryError } = await supabase
|
||||
.from('course')
|
||||
@@ -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,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: exercises, error } = await supabase
|
||||
.rpc('get_exercises')
|
||||
.eq('course_id', courseId);
|
||||
|
||||
if (error) {
|
||||
throw new Error('Error fetching exercises');
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
data: exercises
|
||||
});
|
||||
} 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 }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -168,7 +168,7 @@
|
||||
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<MarksPage courseId={data.courseId} />
|
||||
<MarksPage />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</RoleBasedSecurity>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { PeoplePage } from '$features/course/pages';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<PeoplePage courseId={data.courseId} />
|
||||
<PeoplePage />
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
try {
|
||||
const report = await request.json();
|
||||
|
||||
// Log the CSP violation for monitoring
|
||||
// console.warn('CSP Violation Report:', {
|
||||
// timestamp: new Date().toISOString(),
|
||||
// documentURI: report['document-uri'],
|
||||
// violatedDirective: report['violated-directive'],
|
||||
// blockedURI: report['blocked-uri'],
|
||||
// effectiveDirective: report['effective-directive'],
|
||||
// originalPolicy: report['original-policy'],
|
||||
// sourceFile: report['source-file'],
|
||||
// lineNumber: report['line-number'],
|
||||
// columnNumber: report['column-number'],
|
||||
// userAgent: request.headers.get('user-agent')
|
||||
// });
|
||||
|
||||
return new Response('OK', { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('Error processing CSP report:', error);
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
|
||||
let errorType = '';
|
||||
let errorMessage = '';
|
||||
let errorTitle = '';
|
||||
let showRetry = false;
|
||||
|
||||
onMount(() => {
|
||||
errorType = $page.url.searchParams.get('error') || 'unknown';
|
||||
|
||||
switch (errorType) {
|
||||
case 'invalid_token':
|
||||
errorTitle = $t('email_verification.errors.invalid_token.title');
|
||||
errorMessage = $t('email_verification.errors.invalid_token.message');
|
||||
showRetry = true;
|
||||
break;
|
||||
case 'token_already_used':
|
||||
errorTitle = $t('email_verification.errors.token_used.title');
|
||||
errorMessage = $t('email_verification.errors.token_used.message');
|
||||
showRetry = true;
|
||||
break;
|
||||
case 'token_expired':
|
||||
errorTitle = $t('email_verification.errors.token_expired.title');
|
||||
errorMessage = $t('email_verification.errors.token_expired.message');
|
||||
showRetry = true;
|
||||
break;
|
||||
case 'email_mismatch':
|
||||
errorTitle = $t('email_verification.errors.email_mismatch.title');
|
||||
errorMessage = $t('email_verification.errors.email_mismatch.message');
|
||||
showRetry = true;
|
||||
break;
|
||||
case 'legacy_method_blocked':
|
||||
errorTitle = $t('email_verification.errors.legacy_blocked.title');
|
||||
errorMessage = $t('email_verification.errors.legacy_blocked.message');
|
||||
showRetry = true;
|
||||
break;
|
||||
case 'database_error':
|
||||
errorTitle = $t('email_verification.errors.database.title');
|
||||
errorMessage = $t('email_verification.errors.database.message');
|
||||
showRetry = false;
|
||||
break;
|
||||
default:
|
||||
errorTitle = $t('email_verification.errors.unknown.title');
|
||||
errorMessage = $t('email_verification.errors.unknown.message');
|
||||
showRetry = true;
|
||||
}
|
||||
});
|
||||
|
||||
function handleRetry() {
|
||||
goto('/dashboard');
|
||||
}
|
||||
|
||||
function handleSupport() {
|
||||
window.open(
|
||||
'mailto:[email protected]?subject=Email Verification Issue&body=Error Type: ' + errorType,
|
||||
'_blank'
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Email Verification Error - ClassroomIO</title>
|
||||
<meta name="description" content="There was an issue verifying your email address." />
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="flex min-h-screen w-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8 dark:bg-gray-900"
|
||||
>
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div class="text-center">
|
||||
<!-- Error Icon -->
|
||||
<svg
|
||||
class="mx-auto h-12 w-12 text-red-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<h2 class="mt-6 text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
{errorTitle}
|
||||
</h2>
|
||||
|
||||
<p class="mt-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{errorMessage}
|
||||
</p>
|
||||
|
||||
{#if errorType === 'legacy_method_blocked'}
|
||||
<div
|
||||
class="mt-4 rounded-md border border-yellow-200 bg-yellow-50 p-4 dark:border-yellow-800 dark:bg-yellow-900/20"
|
||||
>
|
||||
<div class="flex">
|
||||
<svg
|
||||
class="h-5 w-5 flex-shrink-0 text-yellow-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
||||
{$t('email_verification.security_notice.title')}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-yellow-700 dark:text-yellow-300">
|
||||
{$t('email_verification.security_notice.message')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex w-full flex-col justify-center gap-4 md:flex-row">
|
||||
<PrimaryButton
|
||||
onClick={handleSupport}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('email_verification.actions.contact_support')}
|
||||
/>
|
||||
<PrimaryButton
|
||||
onClick={() => goto('/')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('email_verification.actions.back_to_dashboard')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Security Information -->
|
||||
<div class="mt-8 text-center text-xs text-gray-500 dark:text-gray-400">
|
||||
<p>{$t('email_verification.security_footer')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user