Merge branch 'main' of https://github.com/classroomio/classroomio into feat/release-v2

This commit is contained in:
Rotimi Best
2025-12-24 04:18:39 +00:00
193 changed files with 10584 additions and 6399 deletions
@@ -0,0 +1,87 @@
/**
* Server-side file type validation utilities for preventing malicious uploads
* Specifically designed to prevent SVG XSS attacks and other security issues
*/
export const ALLOWED_IMAGE_TYPES = [
'image/jpeg',
'image/jpg',
'image/png',
'image/gif',
'image/webp'
] as const;
export const ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'] as const;
/**
* Validate file type by MIME type
* @param file - File object to validate
* @returns boolean indicating if file type is allowed
*/
export function validateImageType(file: File): boolean {
return ALLOWED_IMAGE_TYPES.includes(file.type as (typeof ALLOWED_IMAGE_TYPES)[number]);
}
/**
* Validate file extension
* @param filename - Filename to validate
* @returns boolean indicating if extension is allowed
*/
export function validateImageExtension(filename: string): boolean {
const extension = filename.toLowerCase().substring(filename.lastIndexOf('.'));
return ALLOWED_EXTENSIONS.includes(extension as (typeof ALLOWED_EXTENSIONS)[number]);
}
/**
* Comprehensive file validation
* @param file - File object to validate
* @returns object with validation result and error message
*/
export function validateImageUpload(file: File): { isValid: boolean; error?: string } {
// Check file type
if (!validateImageType(file)) {
return {
isValid: false,
error: 'Invalid file type. Only JPEG, PNG, GIF, and WebP files are allowed.'
};
}
// Check file extension (double check for security)
if (!validateImageExtension(file.name)) {
return {
isValid: false,
error: 'Invalid file extension. Only .jpg, .jpeg, .png, .gif, and .webp files are allowed.'
};
}
// Additional security checks
if (file.name.toLowerCase().includes('.svg')) {
return {
isValid: false,
error: 'SVG files are not allowed for security reasons.'
};
}
return { isValid: true };
}
/**
* Sanitize filename to prevent directory traversal and other issues
* @param filename - Original filename
* @returns sanitized filename
*/
export function sanitizeFilename(filename: string): string {
// Remove path traversal attempts
let sanitized = filename.replace(/[/\\]/g, '');
// Remove special characters that could cause issues
sanitized = sanitized.replace(/[<>:"|?*]/g, '');
// Limit length
if (sanitized.length > 255) {
const ext = sanitized.substring(sanitized.lastIndexOf('.'));
const name = sanitized.substring(0, sanitized.lastIndexOf('.'));
sanitized = name.substring(0, 255 - ext.length) + ext;
}
return sanitized;
}
@@ -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
};
}