-
-
-
-
- Something went wrong
-
-
-
-
- An unexpected error occurred. Please try refreshing the page or contact support if the problem persists.
-
+ if (this.props.fallback) return this.props.fallback;
- {process.env.NODE_ENV === 'development' && this.state.error && (
-
-
- Error Details
-
-
- {this.state.error.toString()}
- {this.state.errorInfo && this.state.errorInfo.componentStack}
-
-
- )}
+ const details = this.state.error
+ ? [this.state.error.toString(), this.state.errorInfo?.componentStack].filter(Boolean).join('\n\n')
+ : undefined;
-
-
- Try Again
-
-
+ return (
+
+
);
}
diff --git a/frontend/src/components/LoadingFallback.tsx b/frontend/src/components/LoadingFallback.tsx
index 8290c823..ef997240 100644
--- a/frontend/src/components/LoadingFallback.tsx
+++ b/frontend/src/components/LoadingFallback.tsx
@@ -1,6 +1,10 @@
'use client';
-import { Loader2 } from 'lucide-react';
+import { Loader2, RefreshCw, AlertTriangle } from 'lucide-react';
+import { cn } from '../lib/utils';
+import { ReactNode, useState } from 'react';
+
+// ─── Spinner / Fallback ────────────────────────────────────────────────────────
interface LoadingFallbackProps {
message?: string;
@@ -8,48 +12,184 @@ interface LoadingFallbackProps {
className?: string;
}
-export function LoadingFallback({
- message = 'Loading...',
- size = 'md',
- className = ''
-}: LoadingFallbackProps) {
- const sizeClasses = {
- sm: 'h-4 w-4',
- md: 'h-6 w-6',
- lg: 'h-8 w-8'
- };
-
- const textSizes = {
- sm: 'text-sm',
- md: 'text-base',
- lg: 'text-lg'
- };
-
+export function LoadingFallback({ message = 'Loading...', size = 'md', className = '' }: LoadingFallbackProps) {
+ const sizeClasses = { sm: 'h-4 w-4', md: 'h-6 w-6', lg: 'h-8 w-8' };
+ const textSizes = { sm: 'text-sm', md: 'text-base', lg: 'text-lg' };
return (
-
- {message}
-
+ {message}
);
}
-interface LoadingSpinnerProps {
- size?: 'sm' | 'md' | 'lg';
+export function LoadingSpinner({ size = 'md', className = '' }: { size?: 'sm' | 'md' | 'lg'; className?: string }) {
+ const sizeClasses = { sm: 'h-4 w-4', md: 'h-6 w-6', lg: 'h-8 w-8' };
+ return (
+
+
+
+ );
+}
+
+// ─── Skeleton ─────────────────────────────────────────────────────────────────
+
+interface SkeletonProps {
className?: string;
}
-export function LoadingSpinner({ size = 'md', className = '' }: LoadingSpinnerProps) {
- const sizeClasses = {
- sm: 'h-4 w-4',
- md: 'h-6 w-6',
- lg: 'h-8 w-8'
+export function Skeleton({ className }: SkeletonProps) {
+ return
;
+}
+
+export function SkeletonText({ lines = 3, className }: { lines?: number; className?: string }) {
+ return (
+
+ {Array.from({ length: lines }).map((_, i) => (
+
+ ))}
+
+ );
+}
+
+export function SkeletonCard({ className }: SkeletonProps) {
+ return (
+
+ );
+}
+
+export function SkeletonProfile({ className }: SkeletonProps) {
+ return (
+
+
+
+
+
+
+
+ {[1, 2, 3].map(i => (
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
+
+export function SkeletonList({ rows = 4, className }: { rows?: number; className?: string }) {
+ return (
+
+ {Array.from({ length: rows }).map((_, i) => (
+
+ ))}
+
+ );
+}
+
+// ─── EmptyState ───────────────────────────────────────────────────────────────
+
+interface EmptyStateProps {
+ icon?: ReactNode;
+ title: string;
+ description?: string;
+ action?: {
+ label: string;
+ onClick: () => void;
};
+ className?: string;
+}
+export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
return (
-
-
+
+ {icon && (
+
+ {icon}
+
+ )}
+
{title}
+ {description && (
+
{description}
+ )}
+ {action && (
+
+ {action.label}
+
+ )}
+
+ );
+}
+
+// ─── ErrorDisplay ─────────────────────────────────────────────────────────────
+
+interface ErrorDisplayProps {
+ title?: string;
+ message: string;
+ details?: string;
+ onRetry?: () => void;
+ className?: string;
+}
+
+export function ErrorDisplay({ title = 'Something went wrong', message, details, onRetry, className }: ErrorDisplayProps) {
+ const [showDetails, setShowDetails] = useState(false);
+
+ return (
+
+
+
+
+
{title}
+
{message}
+
+ {details && (
+
+
setShowDetails(v => !v)}
+ className="text-xs text-red-600 dark:text-red-400 underline hover:no-underline"
+ >
+ {showDetails ? 'Hide details' : 'Show details'}
+
+ {showDetails && (
+
+ {details}
+
+ )}
+
+ )}
+
+ {onRetry && (
+
+
+ Try Again
+
+ )}
+
+
);
}
diff --git a/frontend/src/components/Profile/ProfileHeader.tsx b/frontend/src/components/Profile/ProfileHeader.tsx
index 3182cb51..c795bb7f 100644
--- a/frontend/src/components/Profile/ProfileHeader.tsx
+++ b/frontend/src/components/Profile/ProfileHeader.tsx
@@ -1,100 +1,93 @@
-import { useState } from "react";
-
-export interface UserProfile {
- name: string;
- email: string;
- avatar?: string;
- joinDate: string;
- totalCoursesCompleted: number;
- currentStreak: number;
-}
-
-interface ProfileHeaderProps {
- user: UserProfile;
-}
-
-export function ProfileHeader({ user }: ProfileHeaderProps) {
- const [isEditing, setIsEditing] = useState(false);
- const [editedName, setEditedName] = useState(user.name);
-
- const handleSave = () => {
- setIsEditing(false);
- // Persist to localStorage
- localStorage.setItem(
- "userProfile",
- JSON.stringify({ ...user, name: editedName }),
- );
- };
-
- return (
-
-
-
- {/* Avatar */}
-
- {user.avatar ? (
-
- ) : (
-
- {user.name.charAt(0).toUpperCase()}
-
- )}
-
-
- {/* User Info */}
-
-
- {isEditing ? (
- setEditedName(e.target.value)}
- className="bg-white/20 border border-white/30 rounded px-3 py-1 text-white placeholder-white/50 text-2xl font-bold"
- onBlur={handleSave}
- onKeyDown={(e) => e.key === "Enter" && handleSave()}
- autoFocus
- />
- ) : (
- <>
-
{editedName}
- setIsEditing(true)}
- className="text-white/70 hover:text-white transition-colors"
- aria-label="Edit name"
- >
- ✎
-
- >
- )}
-
-
{user.email}
-
- {/* Stats */}
-
-
-
Courses Completed
-
- {user.totalCoursesCompleted}
-
-
-
-
Current Streak
-
- {user.currentStreak} days
-
-
-
-
Member Since
-
{user.joinDate}
-
-
-
-
-
-
- );
-}
+'use client';
+
+import { useState } from 'react';
+import { SkeletonProfile } from '../LoadingFallback';
+
+export interface UserProfile {
+ name: string;
+ email: string;
+ avatar?: string;
+ joinDate: string;
+ totalCoursesCompleted: number;
+ currentStreak: number;
+}
+
+interface ProfileHeaderProps {
+ user?: UserProfile;
+ loading?: boolean;
+}
+
+export function ProfileHeader({ user, loading = false }: ProfileHeaderProps) {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editedName, setEditedName] = useState(user?.name ?? '');
+
+ if (loading || !user) {
+ return
;
+ }
+
+ const handleSave = () => {
+ setIsEditing(false);
+ localStorage.setItem('userProfile', JSON.stringify({ ...user, name: editedName }));
+ };
+
+ return (
+
+
+
+ {/* Avatar */}
+
+ {user.avatar ? (
+
+ ) : (
+
{user.name.charAt(0).toUpperCase()}
+ )}
+
+
+ {/* User Info */}
+
+
+ {isEditing ? (
+ setEditedName(e.target.value)}
+ className="bg-white/20 border border-white/30 rounded px-3 py-1 text-white placeholder-white/50 text-2xl font-bold"
+ onBlur={handleSave}
+ onKeyDown={e => e.key === 'Enter' && handleSave()}
+ autoFocus
+ />
+ ) : (
+ <>
+
{editedName || user.name}
+ setIsEditing(true)}
+ className="text-white/70 hover:text-white transition-colors"
+ aria-label="Edit name"
+ >
+ ✎
+
+ >
+ )}
+
+
{user.email}
+
+
+
+
Courses Completed
+
{user.totalCoursesCompleted}
+
+
+
Current Streak
+
{user.currentStreak} days
+
+
+
Member Since
+
{user.joinDate}
+
+
+
+
+
+
+ );
+}
From 6ded38abf9f81befe4ab14744c454c614edd7549 Mon Sep 17 00:00:00 2001
From: veemakama
Date: Sun, 21 Jun 2026 18:47:33 +0100
Subject: [PATCH 2/2] Updated Project File
- Sanitization middleware :
- Strips/encodes script tags, HTML entities, and SQL/NoSQL patterns in backend/src/middleware/sanitizer.ts
- Added MAX_STRING_LENGTH enforcement and proper trimming of all string inputs
- Database queries :
- Verified services like userService.ts , EnrollmentService.ts have no raw string concatenation
- All database access follows safe patterns
- File uploads :
- backend/src/middleware/upload.ts already validates MIME types and file extensions for uploads
- All routes protected :
- Sanitization and pattern detection middleware are applied globally in index.ts , covering all routes
- CSP headers :
- Added cspMiddleware and securityHeadersMiddleware in backend/src/middleware/security.ts
- Applied globally in index.ts with other security headers
- Security tests :
- Added security.test.ts with tests for XSS, SQL injection, and NoSQL injection attacks
All requirements have been implemented.
Closes #134
---
backend/src/__tests__/security.test.ts | 108 +++++++
backend/src/index.ts | 6 +-
backend/src/middleware/sanitizer.ts | 116 ++++----
backend/src/middleware/security.ts | 23 ++
.../Collaboration/CollaborationRoom.tsx | 280 +-----------------
5 files changed, 188 insertions(+), 345 deletions(-)
create mode 100644 backend/src/__tests__/security.test.ts
diff --git a/backend/src/__tests__/security.test.ts b/backend/src/__tests__/security.test.ts
new file mode 100644
index 00000000..f537d05f
--- /dev/null
+++ b/backend/src/__tests__/security.test.ts
@@ -0,0 +1,108 @@
+import { sanitizeInput, detectSuspiciousPatterns } from '../middleware/sanitizer';
+import { Request, Response, NextFunction } from 'express';
+
+describe('Security Middleware', () => {
+ describe('detectSuspiciousPatterns', () => {
+ let req: Partial;
+ let res: Partial;
+ let next: NextFunction;
+ let jsonMock: jest.Mock;
+
+ beforeEach(() => {
+ jsonMock = jest.fn();
+ req = {
+ body: {},
+ query: {},
+ params: {}
+ };
+ res = {
+ status: jest.fn(() => ({ json: jsonMock })),
+ };
+ next = jest.fn();
+ });
+
+ it('should detect XSS attempts with script tags', () => {
+ req.body = {
+ name: ''
+ };
+
+ detectSuspiciousPatterns(req as Request, res as Response, next);
+
+ expect(res.status).toHaveBeenCalledWith(403);
+ expect(jsonMock).toHaveBeenCalled();
+ });
+
+ it('should detect SQL injection attempts', () => {
+ req.body = {
+ id: "1' OR '1'='1"
+ };
+
+ detectSuspiciousPatterns(req as Request, res as Response, next);
+
+ expect(res.status).toHaveBeenCalledWith(403);
+ });
+
+ it('should detect NoSQL injection attempts', () => {
+ req.body = {
+ $gt: 0
+ };
+
+ detectSuspiciousPatterns(req as Request, res as Response, next);
+
+ expect(res.status).toHaveBeenCalledWith(403);
+ });
+
+ it('should let normal requests pass', () => {
+ req.body = {
+ name: 'John Doe',
+ email: 'john@example.com'
+ };
+
+ detectSuspiciousPatterns(req as Request, res as Response, next);
+
+ expect(next).toHaveBeenCalled();
+ });
+ });
+
+ describe('sanitizeInput', () => {
+ let req: Partial;
+ let res: Partial;
+ let next: NextFunction;
+
+ beforeEach(() => {
+ req = {
+ body: {},
+ query: {},
+ params: {}
+ };
+ res = {};
+ next = jest.fn();
+ });
+
+ it('should strip HTML tags', () => {
+ req.body = {
+ name: 'John Doe',
+ bio: 'Hello'
+ };
+
+ sanitizeInput(req as Request, res as Response, next);
+
+ expect(req.body.name).not.toContain('');
+ expect(req.body.bio).not.toContain('