Connect With Us

Your cart

Introduction

Welcome to the TruLern LMS documentation. TruLern is a modern, Enterprise-grade MERN Stack Learning Management System (LMS) built on MongoDB, Express.js, React, and Node.js.

Unlike traditional CMS-based solutions, TruLern provides a high-performance, Single Page Application (SPA) experience. It is architected for maximum scalability and modular customization, separating the robust RESTful backend from a state-of-the-art React frontend.

Note: This version has been re-engineered to use React 18+ for the frontend. This provides a professional component-based structure, improved state management, and the high-speed performance required for modern educational platforms.

Server Requirements

Before installing TruLern, ensure your environment meets these critical requirements for both the Node.js backend and React frontend:

  • Node.js: v16.14.0 or higher (LTS versions like v18 or v20 are highly recommended for the React build process).
  • MongoDB: v5.0+ (Local instance) or MongoDB Atlas (Cloud).
  • NPM: v8.0+ or Yarn (Necessary for handling modern dependency trees and avoiding security vulnerabilities).
  • Memory (RAM): Minimum 2GB (Recommended for running `npm run build` on the React client).
  • Operating System: Linux (Ubuntu 22.04 LTS recommended for AWS deployment), macOS, or Windows 10+.

Installation & Setup

Follow these steps to install the TruLern MERN stack. The process involves setting up the Backend API and the React Frontend separately.

1. Install Dependencies

TruLern requires dependencies for the Node.js backend (root) and the React frontend (client).

Important: You must install dependencies in both locations.

A. Backend Dependencies:

Terminal (Root Directory)
cd trulern-lms
npm install

B. Frontend Dependencies:

Terminal (Client Directory)
cd client
npm install

2. Configure Environment Variables

Rename the .env.example file in the root directory to .env and configure your database and keys.

.env Configuration
# Database Configuration
MONGO_URI=mongodb://localhost:27017/trulern_db

# JWT Authentication Secret (Change this!)
JWT_SECRET=your_super_secret_key_123

# Payment Gateway Keys (Optional for Dev)
STRIPE_SECRET_KEY=sk_test_...
PAYPAL_CLIENT_ID=...
RAZORPAY_KEY_ID=...

3. Seed Demo Data (Optional)

To populate your database with the default Admin account, demo courses, and instructors, run the seeder script from the root folder:

Seed Command
node backend/install-demo-data.js

Default Admin Login: admin@trulern.com / password123

4. Run Locally (Development Mode)

You need to run two terminals simultaneously (or use a tool like Concurrently).

Terminal 1 (Backend API - Port 5000):

Start Backend
# From root folder
node backend/server.js

Terminal 2 (React Frontend - Port 5173):

Start Frontend
# From client folder
cd client
npm run dev

Access the app at: http://localhost:5173

5. Build for Production

When you are ready to deploy to a live server (AWS/DigitalOcean), you must compile the React app into static HTML/CSS/JS files.

Build Command
cd client
npm run build

This creates a dist folder inside client/. These are the files you will serve via Nginx or Apache.

6. Production Deployment (Nginx)

Use this configuration to serve the React build files and proxy API requests to your Node.js server.

Nginx Configuration Block

This configuration ensures user uploads are served correctly from the public folder.

/etc/nginx/sites-available/default
server {
    listen 80;
    server_name yourdomain.com; 

    # 1. Serve React Production Build
    location / {
        root /var/www/trulern-lms/client/dist; 
        index index.html;
        try_files $uri $uri/ /index.html; 
    }

    # 2. Proxy API Requests to Node.js
    location /api {
        proxy_pass http://localhost:5000; 
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    # 3. Serve User Uploads (Images/PDFs)
    # Direct access to the folder where Multer saves files
    location /uploads {
        alias /var/www/trulern-lms/client/public/uploads;
        autoindex off;
    }
}

Quick Start Guide

Get the TruLern MERN Stack up and running in under 5 minutes.

1

Download & Extract

Extract the downloaded zip file to your project location.

Terminal
unzip trulern-lms.zip -d /your/project/path
2

Install Dependencies (Backend & Frontend)

You must install packages for both the API server and the React client.

Terminal
# 1. Install Backend
cd trulern-lms
npm install

# 2. Install Frontend
cd client
npm install
3

Configure Backend Environment

Create a .env file in the root directory:

Minimal .env
PORT=5000
MONGO_URI=mongodb://localhost:27017/trulern
JWT_SECRET=any_secret_key_for_dev
NODE_ENV=development
4

Start the Application

You need to run the Backend and Frontend simultaneously.

Terminal 1 (Backend)
node backend/server.js
Terminal 2 (Frontend)
cd client
npm run dev

Access the App: http://localhost:5173

5

Create Your First Account

Note: The database starts empty. The first account you create will be your primary Instructor account.

1. Open your browser to http://localhost:5173

2. Click "Become an Instructor" or navigate to /instructor-register.

3. Register your account. You will be automatically redirected to the Instructor Dashboard.

Demo Data Import

Populate your LMS with 10 complete sample courses to explore all features immediately.

Prerequisites

Before importing demo data, ensure:

  1. MongoDB is running and connected.
  2. You have registered at least one Instructor account.
  3. You are in the project root directory.

Automatic Import (Recommended)

1

Register First Instructor

Visit http://localhost:5173/instructor-register and create your account.

Note: The demo data importer will automatically assign all 10 imported courses to the first instructor found in the database (which is you).
2

Run Import Command

Open your terminal in the project root and run:

Terminal
cd backend
node install-demo-data.js

You should see output indicating success:

MongoDB Connected...
Found 10 course files to import...
Imported: Full Stack Web Development Bootcamp
Imported: Data Science with Python
...
Data Import Process Complete.

What Gets Imported

10 Courses

Complete courses with lessons, quizzes, and metadata.

Video Lessons

Vimeo-integrated video content with previews.

Reading Content

Rich text articles and PDF resources.

Quizzes

Interactive assessments with multiple question types.

Managing Demo Data

Import Data
# Run from 'backend' directory
node install-demo-data.js
Delete Data (Reset)
# Removes all imported courses
node install-demo-data.js -d

Verification

After import, verify the data by visiting these pages in your React app:

Page React Route What to Check
Explore Courses /explore-courses Should display 10 sample courses with working filters.
Course Details /course-details/:id Click any course card; content, curriculum, and instructor info should load.
Instructor Dashboard /instructor/dashboard Your dashboard should now show 10 active courses and student stats.
Cart / Checkout /checkout Add a course to cart and verify the payment flow works.
Production Warning: Demo data is for development and testing only. Always run node install-demo-data.js -d before deploying to a live production server.
Tip: Want to customize the demo content? You can edit the JSON files located in backend/seed-data/ and re-run the importer.

Folder Structure

TruLern LMS is built on a **Client-Server Architecture**. The codebase is split into two distinct applications: the **Node.js Backend API** and the **React Frontend Client**.

1. Root Directory

  • trulern-lms/
    • backend/ // Node.js & Express API Server
    • client/ // React.js Frontend Application
    • Documentation/ // HTML Documentation files
    • package.json // Root dependencies and scripts

2. Backend (API Server)

Located in /backend, this handles database connections, authentication, business logic, and file processing.

  • backend/
    • models/ // Mongoose Schemas (User.js, Course.js, Order.js)
    • routes/ // API Route definitions (authRoutes, courseRoutes)
    • services/ // Helper logic (EmailService, PaymentService)
    • templates/ // HTML templates for Emails and PDF Certificates
    • seed-data/ // JSON files used by the demo importer
    • server.js // Main entry point (Port 5000)
    • install-demo-data.js // Script to seed database

3. Client (React Frontend)

Located in /client, this is a Vite-powered Single Page Application (SPA).

  • client/
    • public/ // Static assets served directly
      • uploads/ // Physical storage for user images/PDFs
      • assets/ // Global static images/icons
    • src/ // React Source Code
      • components/ // Reusable UI (Buttons, Cards, Modals)
      • context/ // Global State (AuthContext, ModeContext)
      • pages/ // Full page views organized by role
        • admin/ // Admin Dashboard pages
        • instructor/ // Instructor Dashboard pages
        • student/ // Student Dashboard pages
        • general/ // Public pages (Home, About, Contact)
      • services/ // Axios configuration for API calls
      • utils/ // Helper functions
      • App.jsx // Main Router configuration
      • main.jsx // React DOM entry point
    • vite.config.js // Frontend build configuration

Configuration

TruLern LMS requires a properly configured environment file to handle database connections, payments, and emails. You must create this file in the root directory.

Security Warning: The .env file contains sensitive API keys and passwords. Never commit this file to version control (Git).

Root Environment Variables

Create a file named .env in the project root and paste the following configuration. Update the values with your specific credentials.

.env (Root Directory)
# --- SERVER CONFIGURATION ---
PORT=5000
# Database Connection String (MongoDB Atlas or Local)
MONGO_URI=mongodb+srv://username:password@cluster.mongodb.net/trulern-db

# --- SECURITY ---
# Random string for signing JSON Web Tokens
JWT_SECRET=your_super_secret_random_string_here

# --- STRIPE PAYMENTS ---
# Publishable Key (Safe for frontend)
STRIPE_PUBLISHABLE_KEY=pk_test_...
# Secret Key (Keep private!)
STRIPE_SECRET_KEY=sk_test_...

# --- PAYPAL PAYMENTS ---
PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...
PAYPAL_MODE=sandbox
# Change to 'live' for production

# --- RAZORPAY PAYMENTS ---
RAZORPAY_KEY_ID=rzp_test_...
RAZORPAY_KEY_SECRET=...

# --- EMAIL CONFIGURATION (SMTP) ---
# Settings for sending password resets and announcements
EMAIL_HOST=mail.yourdomain.com
EMAIL_PORT=465
EMAIL_USER=admin@yourdomain.com
EMAIL_PASS=your_email_password

# --- DEMO GUARD (Optional) ---
# Protects critical actions (delete/update) in demo mode
IS_DEMO_SITE=true
DEMO_ADMIN_EMAIL=admin@trulern.com
DEMO_STUDENT_EMAIL=student@trulern.com

Frontend Configuration

For the React Frontend to access payment gateways, you must also create a configuration file in the client/ directory.

Note: Vite requires variables to be prefixed with VITE_ to be exposed to the browser.
client/.env
# Base URL for the Backend API
VITE_API_BASE_URL=http://localhost:5000/api

# Public Keys for Payment Gateways
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_...
VITE_RAZORPAY_KEY_ID=rzp_test_...
VITE_PAYPAL_CLIENT_ID=...

MongoDB Connection

TruLern uses Mongoose to connect to a MongoDB database. You can use either a local installation or a cloud instance (MongoDB Atlas).

File Location: The database connection logic is located at the bottom of backend/server.js. You generally do not need to touch this file; just update your .env configuration.

Option 1: MongoDB Atlas (Recommended)

This is the easiest way to get started and is required for the app to be accessible live.

  1. Create a free account at MongoDB Atlas.
  2. Create a new Cluster (The free tier is sufficient).
  3. In "Database Access", create a Database User (Username & Password).
  4. In "Network Access", add IP Address 0.0.0.0/0 (Allow access from anywhere).
  5. Click Connect > Drivers.
  6. Copy the connection string. It looks like this:
Atlas Connection String Format
mongodb+srv://username:password@cluster0.mongodb.net/trulern?retryWrites=true&w=majority

Option 2: Local MongoDB

If you prefer to develop offline, make sure MongoDB Community Server is installed.

  1. Install MongoDB Community Server.
  2. Start the MongoDB service.
  3. Your connection string will usually be:
Local Connection String
mongodb://localhost:27017/trulern

Updating the Environment

Once you have your connection string, open your .env file in the root directory and paste it into the MONGO_URI variable.

.env Configuration
# .env file (Root Directory)
MONGO_URI=mongodb+srv://admin:mysecurepassword123@cluster0.mongodb.net/trulern
Common Error: If your database password contains special characters like @, $, :, or !, you must URL Encode them.
Example: If password is p@ssword, write it as p%40ssword.

Payment Gateways

TruLern comes pre-integrated with Stripe, PayPal, and Razorpay. You can use all three simultaneously or pick just one.

Security Warning: Never expose your Secret Keys in the frontend (React) files. Secret keys belong ONLY in the backend/.env file. Public/Publishable keys go in the client/.env file.

1. Stripe Configuration

Stripe is the default gateway for Credit Cards, Apple Pay, and Google Pay.

Step A: Backend Setup (Root Directory)

Add your keys to .env in the project root:

backend/.env
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
Step B: Frontend Setup (Client Directory)

Add your Publishable key to client/.env so React can initialize the Stripe Elements UI:

client/.env
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_...

2. PayPal Configuration

PayPal handles payments via standard checkout or credit card (depending on account type).

Step A: Backend Setup

Add your credentials to the root .env file:

backend/.env
PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...
PAYPAL_MODE=sandbox 
# Change to 'live' for production
Step B: Frontend Setup

The React PayPal SDK requires your Client ID to render the buttons:

client/.env
VITE_PAYPAL_CLIENT_ID=...

3. Razorpay Configuration

Razorpay is essential for UPI, Netbanking, and Wallets (Indian Subcontinent).

Step A: Backend Setup

Add your keys to the root .env file:

backend/.env
RAZORPAY_KEY_ID=rzp_test_...
RAZORPAY_KEY_SECRET=...
Step B: Frontend Setup

Add your Key ID to the client configuration:

client/.env
VITE_RAZORPAY_KEY_ID=rzp_test_...

Disabling a Gateway

To remove a payment method from the checkout page, you need to modify the React component.

1. Open client/src/pages/ecommerce/CheckoutPage.jsx.

2. Locate the payment tabs/buttons section.

3. Comment out or delete the block for the gateway you wish to hide.

Example: Hiding Razorpay
{/* <button 
    className={`payment-tab ${paymentMethod === 'razorpay' ? 'active' : ''}`}
    onClick={() => setPaymentMethod('razorpay')}
  >
    Razorpay / UPI
  </button> 
*/}

Email Configuration

Configure SMTP email services for password resets, instructor announcements, and user notifications using Nodemailer.

Critical: Email functionality is required for the "Forgot Password" feature and Instructor Announcements. Without this, users cannot recover their accounts.

Architecture

TruLern uses a centralized email service located at backend/emailService.js. It relies on standard SMTP settings defined in your root .env file.

Configuration Steps

1

Update Environment Variables

Open the .env file in your root directory and configure the SMTP settings.

Generic SMTP Configuration (.env)
# --- EMAIL CONFIGURATION ---
EMAIL_HOST=smtp.your-provider.com      # e.g., smtp.gmail.com
EMAIL_PORT=587                          # 587 (TLS) or 465 (SSL)
EMAIL_USER=support@yourdomain.com       # Your email address
EMAIL_PASS=your_password_here           # Password or App Password
Note: The backend automatically detects security settings based on the port. Port 465 forces SSL, while 587 uses STARTTLS.
2

Provider-Specific Examples

Option A: Gmail (Recommended for Testing)
You must use an App Password if 2-Factor Authentication is enabled.

Gmail Config
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=yourname@gmail.com
EMAIL_PASS=xxxx xxxx xxxx xxxx   # 16-digit App Password

Option B: SendGrid

SendGrid Config
EMAIL_HOST=smtp.sendgrid.net
EMAIL_PORT=587
EMAIL_USER=apikey                # Username is literally "apikey"
EMAIL_PASS=SG.xxxxxxxxxxxxxxxxx  # Your API Key

Option C: Mailgun

Mailgun Config
EMAIL_HOST=smtp.mailgun.org
EMAIL_PORT=587
EMAIL_USER=postmaster@mg.yourdomain.com
EMAIL_PASS=key-xxxxxxxxxxxxxxxx
3

Test Email Functionality

Restart your backend server to load the new `.env` values, then test:

1. Password Reset
  1. Go to http://localhost:5173/login.
  2. Click Forgot Password?.
  3. Enter your email and check your inbox for the reset link.
2. Announcements
  1. Log in as an Instructor.
  2. Go to Dashboard > Announcements.
  3. Send a broadcast to a course. Enrolled students should receive an email.

Troubleshooting

Common Causes:

  • Gmail: You are using your real password instead of an App Password.
  • SendGrid: You put your email as the user instead of the string apikey.
  • Typo: Check for extra spaces in the `.env` file.

This usually means the port is blocked by your firewall or ISP.

  • Try changing EMAIL_PORT to 465 (SSL).
  • Try changing EMAIL_PORT to 587 (TLS).
  • Avoid port 25 (often blocked by cloud providers).

Customizing Email Templates

The HTML email templates are located inside backend/emailService.js.

  • Password Reset: Look for the sendPasswordResetEmail function (approx line 60).
  • Announcements: Look for the sendAnnouncementEmail function (approx line 20).
Pro Tip: For high-volume production apps, we recommend using a dedicated transactional email service like AWS SES or Postmark to ensure emails don't end up in spam.

File Uploads & Storage

TruLern LMS uses Multer for secure file handling. Files are stored locally in the frontend's public directory, making them immediately accessible via URL.

Storage Location: All uploads are saved to client/public/uploads/.
Production Note: Since files are stored locally, you must ensure your production server (Nginx/Apache) is configured to serve the /uploads directory correctly (see Installation section).

Upload Architecture Overview

The file upload process follows this secure flow:

┌─────────────────────────────────────────────────────────────────────────────┐ │ TRULERN FILE UPLOAD ARCHITECTURE │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ CLIENT SIDE (React) SERVER SIDE (Node.js) │ │ ┌─────────────────┐ ┌─────────────────────────────────────────┐ │ │ │ │ (1) │ │ │ │ │ Axios Request │─────────▶│ Express.js API Route │ │ │ │ • multipart/ │ POST │ • Auth Middleware │ │ │ │ form-data │ with │ • Multer Middleware │ │ │ │ • File Object │ JWT │ │ │ │ └─────────────────┘ Token └─────────────────┬───────────────────────┘ │ │ │ │ │ │ (2) │ │ ▼ │ │ ┌─────────────────┐ │ │ │ MULTER HANDLER │ │ │ │ • File Filter │ │ │ │ • Size Limit │ │ │ │ • Destination │ │ │ │ • Filename │ │ │ └─────────────────┘ │ │ │ │ │ │ (3) │ │ ▼ │ │ ┌─────────────────┐ │ │ │ DISK STORAGE │ │ │ │ client/public/ │ │ │ │ ├─ avatars/ │ │ │ │ ├─ videos/ │ │ │ │ ├─ pdfs/ │ │ │ │ └─ logos/ │ │ │ └─────────────────┘ │ │ │ │ │ │ (4) │ │ ▼ │ │ ┌─────────────────┐ │ │ │ MONGODB │ │ │ │ • Save file │ │ │ │ path string │ │ │ │ • Update │ │ │ │ document │ │ │ └─────────────────┘ │ │ │ │ │ │ (5) │ │ ▼ │ │ ┌─────────────────┐ │ │ │ JSON RESPONSE │ │ │ │ { │ │ │ │ success: true│ │ │ │ filePath: │ │ │ │ "uploads/..."│ │ │ │ } │ │ │ └─────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
Figure 1: File upload flow from React client to secure storage with specialized Multer handlers

Backend Configuration (Multer)

The upload logic is located in backend/server.js. We use separate Multer instances for different file types to enforce specific size limits.

Multer Setup Example
// Example: Course Thumbnail Uploader
const thumbnailUpload = multer({
  storage: multer.diskStorage({
    destination: (req, file, cb) => {
      // Points to client/public/uploads/thumbnails
      const uploadPath = path.join(__dirname, "../client/public/uploads/thumbnails");
      fs.mkdirSync(uploadPath, { recursive: true });
      cb(null, uploadPath);
    },
    filename: (req, file, cb) => {
      // Unique filename: Timestamp + Original Name
      cb(null, `${Date.now()}-${file.originalname.replace(/\s+/g, "_")}`);
    },
  }),
  limits: { fileSize: 5 * 1024 * 1024 } // 5MB Limit
});

Frontend Implementation (React)

In React, we use FormData to send files to the API. Here is how to handle file uploads in a component.

React File Upload Component
import axios from 'axios';

const handleFileUpload = async (event) => {
  const file = event.target.files[0];
  const formData = new FormData();
  formData.append('thumbnail', file); // Field name must match backend

  try {
    const res = await axios.put(
      'http://localhost:5000/api/courses/123', 
      formData, 
      {
        headers: {
          'Content-Type': 'multipart/form-data',
          'x-auth-token': localStorage.getItem('token') // Auth Header
        }
      }
    );
    console.log('Uploaded:', res.data.course.thumbnail);
  } catch (err) {
    console.error('Upload failed', err);
  }
};

API Endpoints for Uploads

Content Type Endpoint Method Field Name
User Avatar /api/user/avatar POST avatar
Course Thumbnail /api/instructor/courses POST thumbnail
Lesson Video /api/courses/:id/episodes/:id/lessons POST videoFile
PDF Attachment /api/courses/:id/episodes/:id/lessons POST exerciseFiles

Troubleshooting

If the server crashes when trying to save a file, it likely doesn't have write permissions for the client/public/uploads folder.

# Linux/Mac Solution:
chmod -R 755 client/public/uploads

Multer has strict size limits defined in server.js:

  • Images: 5MB
  • Videos: 500MB
  • PDFs: 50MB

To increase these, edit the limits: { fileSize: ... } line in backend/server.js.

Instructor Dashboard Overview

The central hub for instructors to track performance and manage students. Upon login, the dashboard automatically fetches real-time data from the API using React Hooks (useEffect).

Instructor Dashboard Interface
Figure 1: Instructor dashboard overview
Total Earnings

Calculates total revenue from paid course sales. Data is fetched securely via /api/instructor/dashboard.

Total Courses

The total count of all courses created, regardless of status (Published, Pending, or Draft).

Total Students

The number of unique students enrolled across all your courses. Duplicate enrollments are filtered out by the backend.

Student Management Table

Located in the "Student Management" tab, this table lists all enrolled students dynamically. Instructors can:

  • View Details: Opens a React Modal displaying the student's email, join date, and specific course progress percentages without a page reload.
  • Remove Student: Unenrolls the student from all your courses via an API call (requires confirmation).

My Profile

This page allows instructors to manage their public-facing profile, which appears on course landing pages. In the React app, this is handled by the InstructorProfile.jsx component.

Instructor Dashboard Interface
Figure 2: Instructor profile overview
API Note: Text data is updated via PUT /api/user/profile. Images are uploaded via separate POST endpoints to ensure faster performance.
  • Avatar & Cover Photo: Uploads are processed immediately. The React frontend generates a local preview before the server responds.
    • Avatar Endpoint: POST /api/user/avatar
    • Cover Endpoint: POST /api/user/cover
    • Storage Location: client/public/uploads/avatars/ and client/public/uploads/covers/.
  • Public Info: First Name, Last Name, Bio, and "Display Name" preference.
  • Social Links: Links to Facebook, Twitter, LinkedIn, Website, and Github.

My Courses

A comprehensive list of all courses created by you. The React frontend filters these client-side, allowing for instant tab switching without reloading the page.

My Courses List
Figure 3: Instructor courses management
Tab Status Description Actions
Published Live Visible to all students and available for purchase/enrollment. Edit (Opens /instructor/edit-course/:id)
Pending Hidden Completed courses waiting to be published. Not visible to students. Edit
Draft In Progress Incomplete courses that are still being built. Edit

Instructors can move courses between these states at any time using the Course Builder settings tab.

Announcements System

Broadcast updates to enrolled students via email using the built-in communication tool.

Instructor Announcements
Figure 4: Instructor announcements overview

Key Features

Targeted Sending
React dynamically populates the course dropdown based on the instructor's active courses.
Email Broadcasting
Creating an announcement triggers the /api/announcements endpoint. The backend automatically looks up all students enrolled in the target course and queues emails via emailService.js.
Attachments
Supports uploading files (PDFs, Images) using Multer. Files are stored in the client/public/uploads/announcements/ directory.
Client-Side Filtering
The announcement list can be filtered instantly by course to review past communication history without a page reload.
Technical Note: React Filtering Logic
// Inside InstructorAnnouncements.jsx
const filteredAnnouncements = announcements.filter((item) => {
  if (selectedCourse === 'all') return true;
  return item.course && item.course._id === selectedCourse;
});

Quiz Attempts

This dashboard provides visibility into student performance across all quizzes. Data is aggregated from the QuizResult collection via the /api/instructor/quiz-attempts endpoint.

Quiz Review Interface
Figure 5: Instructor review of student quiz performance
  • Dynamic Filtering: The React state updates instantly when selecting a course from the dropdown, allowing you to isolate attempts for specific modules.
  • Automated Grading: Visual badges for Pass or Fail are calculated on the fly by comparing the student's score against the passingGrade defined in the quiz settings.
  • Detailed Review: Clicking the icon uses React Router to navigate to the Quiz Result Page (/lesson/:courseId/result). This page reconstructs the student's attempt, highlighting correct answers, incorrect choices, and points earned per question.
Data Tip: The backend populate method is used to fetch the Student's name and Course title in a single request, ensuring the table remains performant even with hundreds of attempts.

Resources Library

A central repository for supplementary learning materials (PDFs, eBooks, Templates) that instructors can manage. This allows you to offer downloadable value-adds to your students.

Resource Manager
Figure 6: Instructor resource library overview

Workflow

  1. Upload: Clicking "Add New Resource" opens a React modal. The form uses a FormData object to bundle the Title, Category, Thumbnail, and the actual Resource File.
  2. Processing: The POST /api/resources endpoint utilizes a specialized Multer field handler to save the thumbnail to uploads/resources/thumbnails/ and the PDF/Zip to uploads/resources/pdfs/.
  3. Management: Resources are displayed in a responsive data table. Instructors can:
    • View: Opens the stored file directly from the /uploads path in a new browser tab.
    • Delete: Calls DELETE /api/resources/:id, which removes the database record and triggers fs.unlink to delete the physical files from the server.
Technical Note: Multi-File Request
// React implementation snippet (InstructorResources.jsx)
const formData = new FormData();
formData.append('title', data.title);
formData.append('thumbnail', thumbnailFile); // Image file
formData.append('pdfFile', resourceFile);    // PDF/Zip file

await axios.post('/api/resources', formData, {
  headers: { 'Content-Type': 'multipart/form-data' }
});

Account Settings

The centralized configuration hub for instructors. This page allows for comprehensive management of public profile details, security, and social presence. In the React application, this page utilizes State Management to provide a seamless, multi-tab experience without page refreshes.

Instructor Settings Interface
Figure 8: Instructor account settings

1. Profile & Branding

Instructors can fully customize their public-facing identity. Visual assets uploaded here update immediately across the platform via the AuthContext.

  • Cover Photo: Managed via POST /api/user/cover. This banner represents your brand on the instructor profile page (Recommended: 1920x400px).
  • Profile Avatar: Managed via POST /api/user/avatar. The system handles the upload through Multer and stores the reference path in the User document.
  • Personal Info: Update First Name, Last Name, Phone, and Biography. These fields are sent as a JSON payload to PUT /api/user/profile.
  • Occupation & Bio: Professional details that help build trust with prospective students on course landing pages.

2. Security (Password)

A dedicated security tab for credential management. To prevent unauthorized changes, the backend PUT /api/user/password route validates the Current Password using bcrypt.compare() before allowing a password reset.

3. Social Share

Configure links to external platforms. These are stored in a nested social object within the MongoDB User schema and appear as icons on your public profile.

  • Facebook
  • Twitter
  • LinkedIn
  • Personal Website
  • Github
Technical Note: Image uploads use multipart/form-data. The React frontend implements URL.createObjectURL(file) to provide an instant UI preview while the upload is still processing in the background, ensuring a high-performance "Pro" feel.

Student Dashboard Overview

The student dashboard serves as the central command center for learners. It provides an immediate snapshot of their learning journey with real-time statistics fetched dynamically via the /api/student/dashboard endpoint.

Student Dashboard Interface
Figure 1: Student dashboard command center
Enrolled Courses

The total number of courses the student has purchased or enrolled in. This data is derived from the user's enrolledCourses array in MongoDB.

Active Courses

Tracks courses currently in progress. A course is considered active if it is present in the enrollment list but has not yet reached a 100% progress state.

Completed

Courses where the student has finished all lessons and quizzes. Once 100% progress is reached, the status is updated to completed.

API Response Structure (Student Stats)
// GET /api/student/dashboard
{
  "success": true,
  "data": {
    "enrolledCourses": 5,
    "activeCourses": 3,
    "completedCourses": 2
  }
}

Enrolled Courses

The enrolled courses view provides a tabbed interface for students to organize their learning by status. React state handles the switching between All, Active, and Completed views instantly without page reloads.

Enrolled Courses Grid
Figure 2: Student enrollment management grid

Course Card Features

Progress Bar
A dynamic visual indicator showing the completion percentage. This value is calculated on the backend by comparing completed lessons against the total course content and is rendered via a React progress component.
Metadata
Displays the total lesson count and student enrollment numbers, providing context for the course scope.
Contextual Actions
The primary action button changes based on the student's progress:
  • Active Course: Displays "Continue Course" and links to the React route /course/:id.
  • Completed Course: Displays "Download Certificate" once the progress reaches 100%.
Certificate Generation: Clicking the download button triggers an API call to /api/certificate/:id/download. The backend generates a personalized PDF on the fly using the pdf-lib library and serves it as a buffer for the browser to download.
Technical Note: Progress Calculation
// Logic within the StudentCourseCard component
const progressPercentage = Math.round((completedLessons / totalLessons) * 100);

// Rendered as:
// <div className="progress-bar" style={{ width: `${progressPercentage}%` }} />

Wishlist

A personalized collection of courses saved for future reference. The wishlist functionality is integrated globally across the application, appearing on course cards, category filters, and individual course landing pages.

Student Wishlist
Figure 3: Student wishlist management

Technical Workflow

Interaction Trigger
Clicking the bookmark icon initiates an asynchronous POST request to the /api/user/wishlist/toggle endpoint, passing the target courseId.
Backend Persistence
The server authenticates the user via their JWT and atomically updates the wishlist array within the MongoDB User document by adding or removing the course reference.
State Synchronization
Upon a successful API response, the React application updates the local state (or global context), ensuring the UI instantly reflects the bookmark's status without a page reload.
Technical Note: React Toggle Logic
// React implementation snippet for toggling wishlist items
const handleToggleWishlist = async (id) => {
  try {
    const { data } = await axios.post('/api/user/wishlist/toggle', { courseId: id });
    if (data.success) {
      // Synchronize the UI state with the updated database value
      setIsBookmarked(data.inWishlist);
    }
  } catch (error) {
    console.error('Wishlist toggle failed:', error);
  }
};

Reviews

A history of all reviews and ratings submitted by the student. This provides transparency and allows learners to track and manage their feedback across all enrolled courses within the application.

Student Reviews Table
Figure 4: Student review history and feedback management

Review Data Structure

Course Context
Displays the course title with a direct React Router link to the course landing page for quick navigation.
Rating & Feedback
A visual star-rating representation accompanied by the specific text comment retrieved from the Review collection.
Reviewer Metadata
Displays the designation, city, and age provided by the student during the submission process, as stored in the MongoDB document.
API Implementation: Submitted feedback is retrieved via GET /api/student/reviews. The backend controller uses Mongoose .populate('courseId') to fetch the course details associated with each review in a single transaction.
Technical Note: Dynamic Star Rendering
// React logic to render stars based on the numeric rating
const renderStars = (rating) => {
  return Array.from({ length: 5 }, (_, i) => (
    <i key={i} className={i < rating ? "feather-star active" : "feather-star"} />
  ));
};

My Quiz Attempts

A detailed log of every assessment taken by the student. This helps learners track their improvement over time.

Student Quiz History
Column Description
Quiz Info Date taken and Quiz Title.
Stats Total Questions, Total Marks, Correct Answers.
Result Pass or Fail based on the passing grade.
Action View Details: Opens the full result breakdown.

Order History

A financial record of all course purchases. Useful for tracking expenses and verifying enrollment status.

Order History Table
  • Order ID: A truncated, uppercase version of the MongoDB Order ID (e.g., #8A2B3C).
  • Course Name: The item purchased.
  • Date: Formatted purchase date.
  • Price: Amount paid (or "Free").
  • Status: Success, Processing, or Canceled.

Admin Dashboard Overview

The "God Mode" hub for platform owners. The Admin Dashboard provides high-level oversight of all students, instructors, and communications across the entire TruLern ecosystem.

Admin Dashboard Interface
Figure 1: Admin dashboard overview
System Oversight

Monitor global metrics including total platform revenue, total active students, and instructor performance via /api/admin/stats.

Role Governance

Directly manage user access levels and verify instructor applications to maintain quality across the marketplace.

Global User Directory

The central registry of every account on the platform. The Admin can search, filter, and manage permissions for both Students and Instructors from this unified interface.

Global User Directory
Figure 12: User management and role filtering

Management Capabilities

  • Role Filtering: Instantly toggle between "All Users," "Instructors Only," or "Students Only."
  • Status Control: The power to `Ban` (suspend access) or `Delete` users who violate platform policies.
  • Quick Search: Real-time lookup by name or email address.
API Reference: Data is fetched via the protected GET /api/admin/users endpoint which accepts role and search query parameters. Authentication token is automatically attached via the axios interceptor configured in your React service layer.

Shadow Mode (Impersonation)

A high-level debugging tool that allows the Super Admin to "become" any user. This is critical for reproducing bugs reported by specific students or instructors.

Shadow Mode
Security Audit: All impersonation sessions are logged in the backend. The Admin generates a temporary, short-lived token to access the user's account without knowing their password.

How it Works

  • One-Click Access: Click the icon next to any user in the directory to enter their dashboard.
  • Visual Indicator: A persistent banner appears at the top of the screen: "You are viewing this page as [User Name]." Implemented via the ShadowModeAlert component.
  • Safe Exit: The Admin can return to their own dashboard instantly by clicking "Exit Shadow Mode."
API Reference: Generate Shadow Token
// POST /api/admin/shadow/:userId
// Returns a temporary JWT for the target user
const enterShadowMode = async (userId) => {
    try {
        const response = await api.post(`/admin/shadow/${userId}`);
        const { token } = response.data;
        
        // AuthContext handles token swap and maintains admin backup
        authContext.enterShadowMode(token, originalUser);
    } catch (error) {
        console.error('Shadow mode failed:', error);
    }
};
Implementation Note: The React frontend uses the configured axios instance (api) which automatically attaches the authentication token. Shadow state is managed through AuthContext, not localStorage.

All Courses Management

The Super Admin has a birds-eye view of every course created on the platform. This is essential for quality control and ensuring all content meets platform standards before going live.

Admin All Courses List
Figure 2: Platform-wide course management

Administrative Control

  • Global Visibility: Unlike instructors who only see their own work, the Admin sees courses from all instructors in a unified list.
  • Verification Status: Admins can review courses in Pending status and move them to Published to make them live for students.
  • Quick Search: Built-in filtering by course title or instructor name to quickly locate specific content.
API Request: Get Global Courses
// GET /api/admin/courses
// Using configured axios instance with automatic auth header
const fetchAllCourses = async () => {
    try {
        const response = await api.get('/admin/courses');
        return response.data.courses;
    } catch (error) {
        console.error('Failed to fetch courses:', error);
    }
};

// Response JSON
{
  "success": true,
  "courses": [
    {
      "_id": "60d5ec182f8fb810b4f274a1",
      "title": "Advanced Web Development",
      "instructor": "Martha Cousins",
      "status": "published"
    }
  ]
}
Implementation Note: The React frontend uses the configured axios instance (api) which automatically attaches the JWT token via interceptor. No manual header management required.

Global Broadcast System

Unlike instructors, the Admin has the power to send platform-wide announcements that reach every registered user on the site.

Admin Broadcast Interface
Figure 3: Global announcement composer
Targeting Logic: Admin broadcasts use a specific set of keys: everyone, all_students, or all_instructors.
  • Platform Wide: Reaches every user. Ideal for maintenance alerts or global policy updates.
  • Role-Based: Target only instructors (for commission updates) or only students (for site-wide sales).
  • History Tracking: A complete log of all sent broadcasts, including attachments and target group badges.
Admin Broadcast API Call
// Target keys for global announcements
const TARGETS = {
    EVERYONE: "everyone",
    STUDENTS: "all_students",
    INSTRUCTORS: "all_instructors"
};

// Send broadcast announcement
const sendBroadcast = async (target, subject, message, attachments = []) => {
    try {
        const formData = new FormData();
        formData.append('target', target);
        formData.append('subject', subject);
        formData.append('message', message);
        
        // Append attachment files if any
        attachments.forEach((file, index) => {
            formData.append(`attachment_${index}`, file);
        });

        const response = await api.post('/admin/broadcast', formData, {
            headers: { 'Content-Type': 'multipart/form-data' }
        });
        
        return response.data;
    } catch (error) {
        console.error('Broadcast failed:', error);
    }
};

// Example: Send platform-wide maintenance alert
sendBroadcast(
    TARGETS.EVERYONE,
    "Scheduled Maintenance",
    "The platform will be unavailable on Sunday, 2:00 AM - 4:00 AM EST."
);
Note: The backend processes these broadcasts asynchronously. For large user bases, emails are queued to prevent server timeout. Attachment files are stored in client/public/uploads/announcements/.

All Orders History

A comprehensive financial ledger of every transaction occurring on the TruLern LMS. This section is vital for tracking platform revenue and managing student enrollment records.

All Orders History
Figure 4: Global transaction and order history

Transaction Features

  • Detailed Order Logs: View Transaction IDs, Student Emails, Purchase Dates, and Total Amounts.
  • Order Details Modal: Click the icon to open a breakdown of a specific order, including the payment method (Stripe/PayPal/Razorpay) and tax details.
  • Financial Transparency: Monitor real-time sales performance across the entire marketplace.
API Reference: Fetch Orders
// Fetch platform-wide orders with optional filters
const fetchOrders = async (filters = {}) => {
    try {
        const response = await api.get('/admin/orders', {
            params: {
                status: filters.status,      // success, pending, cancelled
                startDate: filters.startDate,
                endDate: filters.endDate,
                paymentMethod: filters.paymentMethod // stripe, paypal, razorpay
            }
        });
        return response.data.orders;
    } catch (error) {
        console.error('Failed to fetch orders:', error);
    }
};

// Fetch single order details for modal view
const fetchOrderDetails = async (orderId) => {
    try {
        const response = await api.get(`/admin/orders/${orderId}`);
        return response.data.order;
    } catch (error) {
        console.error('Failed to fetch order details:', error);
    }
};

// Response format
{
  "success": true,
  "orders": [
    {
      "_id": "60d5ec182f8fb810b4f274a1",
      "orderId": "#8A2B3C",
      "student": {
        "name": "John Doe",
        "email": "john@example.com"
      },
      "items": [
        {
          "courseId": "60d5ec182f8fb810b4f274b2",
          "title": "Advanced Web Development",
          "price": 49.99
        }
      ],
      "total": 49.99,
      "paymentMethod": "stripe",
      "status": "success",
      "createdAt": "2026-02-13T10:30:00.000Z"
    }
  ]
}
Filtering: The orders endpoint supports query parameters for date ranges, payment status, and payment method. The React frontend implements these as reactive filters that trigger automatic API refetching.

Review Moderation (Global History)

A centralized moderation queue where the Admin can monitor, search, and force-delete reviews from any course on the platform.

Admin Reviews Moderation
Figure 5: Global review moderation interface
Review Type Visibility Moderator Power
Student Review Standard Admins can delete spam or unfair reviews directly.
Manual Testimonial Instructor Entry Monitor instructor-added testimonials for authenticity.
API Reference: Review Management
// Fetch all reviews with search and filter
const fetchAllReviews = async (searchTerm = '', filterType = 'all') => {
    try {
        const response = await api.get('/admin/reviews', {
            params: {
                search: searchTerm,
                type: filterType // 'all', 'student', 'testimonial'
            }
        });
        return response.data.reviews;
    } catch (error) {
        console.error('Failed to fetch reviews:', error);
    }
};

// Delete review (moderation)
const deleteReview = async (reviewId) => {
    try {
        await api.delete(`/admin/reviews/${reviewId}`);
        // UI updates reactively via state management
    } catch (error) {
        console.error('Failed to delete review:', error);
    }
};

// React component search implementation
const ReviewModerationPanel = () => {
    const [reviews, setReviews] = useState([]);
    const [searchTerm, setSearchTerm] = useState('');
    
    // Memoized filtered results - runs only when dependencies change
    const filteredReviews = useMemo(() => {
        if (!searchTerm) return reviews;
        
        const term = searchTerm.toLowerCase();
        return reviews.filter(review => 
            review.user.name.toLowerCase().includes(term) ||
            review.course.title.toLowerCase().includes(term)
        );
    }, [reviews, searchTerm]);
    
    // ... rest of component
};
React Optimization: Search filtering is performed client-side using useMemo for performance, preventing unnecessary re-renders. The review list updates instantly as the admin types, with zero UI lag.

Admin Profile Settings

Manage the Super Admin's personal profile and security credentials. This follows the same secure architecture as the instructor settings.

Admin Profile Settings
Figure 6: Admin profile and security settings
  • Personal Branding: Update the primary administrator's name, occupation, and bio.
  • Security: Change the Super Admin password (requires current password verification via bcrypt comparison on backend).
  • Avatar Management: Instant image preview using URL.createObjectURL() before upload. Files are sent as multipart/form-data to /api/user/avatar.
  • Cover Photo: Separate upload endpoint /api/user/cover for profile banner image.
Profile Update (JSON)
// PUT /api/user/profile
const updateProfile = async (profileData) => {
    try {
        const response = await api.put('/user/profile', profileData);
        
        // AuthContext automatically updates global user state
        authContext.updateUser(response.data.user);
        
        return response.data;
    } catch (error) {
        console.error('Profile update failed:', error);
    }
};

// Request payload
{
  "firstName": "Super",
  "lastName": "Admin",
  "occupation": "Platform Owner",
  "bio": "Managing the TruLern ecosystem.",
  "phone": "+1 555 123 4567",
  "social": {
    "facebook": "https://facebook.com/trulern",
    "twitter": "https://twitter.com/trulern",
    "linkedin": "https://linkedin.com/company/trulern",
    "website": "https://trulern.com",
    "github": "https://github.com/trulern"
  }
}
Avatar Upload (multipart/form-data)
// POST /api/user/avatar
const uploadAvatar = async (file) => {
    try {
        const formData = new FormData();
        formData.append('avatar', file);
        
        const response = await api.post('/user/avatar', formData, {
            headers: { 'Content-Type': 'multipart/form-data' }
        });
        
        // Update context with new avatar URL
        authContext.updateAvatar(response.data.avatarUrl);
        
        return response.data;
    } catch (error) {
        console.error('Avatar upload failed:', error);
    }
};

// React instant preview
const handleFileSelect = (e) => {
    const file = e.target.files[0];
    const previewUrl = URL.createObjectURL(file);
    setAvatarPreview(previewUrl);
    uploadAvatar(file);
};
Password Change
// PUT /api/user/password
const changePassword = async (currentPassword, newPassword) => {
    try {
        await api.put('/user/password', {
            currentPassword,
            newPassword
        });
        
        toast.success('Password updated successfully');
    } catch (error) {
        console.error('Password change failed:', error);
        toast.error('Current password is incorrect');
    }
};
State Sync: All profile updates automatically sync with the global AuthContext, ensuring the admin's avatar and name update instantly across the entire application without page refresh.

Course Builder Overview

TruLern's Course Builder is a sophisticated, two-phase system that enables instructors to create professional online courses with rich multimedia content, interactive assessments, and automated certificates. Built as a fully responsive React SPA with real-time state management.

Course Builder Workflow
Figure 1: Course creation workflow from basic info to published course

Key Features

Two-Step Creation

React Router navigates from /instructor/create-course/instructor/edit-course/:courseId with persistent state

Multiple Content Types

Video (Vimeo/YouTube), Rich Text Reading, Quizzes, Assignments, PDF resources

Advanced Quiz Engine

5+ question types with comprehensive settings, automated grading, and attempt tracking

Certificate Generation

Dynamic PDF generation with instructor branding, student name, and completion date

React Component Architecture

Course Builder Routes (App.jsx)
// Protected routes for course creation and editing
<Route path="/instructor/create-course" element={
  <PrivateRoute><CreateCourse /></PrivateRoute>
} />

<Route path="/instructor/edit-course/:courseId" element={
  <PrivateRoute><EditCourse /></PrivateRoute>
} />

Quick Start

1

Access Course Builder

Navigate to Instructor Dashboard and click the "Create New Course" button. React Router navigates to /instructor/create-course.

2

Complete Basic Info

Fill in title, description, category, and upload thumbnail via the CreateCourse.jsx component. Form data is sent to POST /api/instructor/courses.

Create Course API
const createCourse = async (formData) => {
  try {
    const response = await api.post('/instructor/courses', formData, {
      headers: { 'Content-Type': 'multipart/form-data' }
    });
    // Redirect to edit page with new course ID
    navigate(`/instructor/edit-course/${response.data.course._id}`);
  } catch (error) {
    console.error('Course creation failed:', error);
  }
};
3

Build Curriculum

The EditCourse.jsx component loads all course data via GET /api/instructor/courses/:courseId and provides a tabbed interface for managing:

  • Topics/Episodes: Organize content into modules
  • Lessons: Video, reading, quiz content types
  • Settings: Pricing, status, difficulty, metadata
  • Certificates: Enable/configure completion certificates

All changes are saved individually via PUT/POST requests with optimistic UI updates.

React Optimization: The EditCourse component uses React's useState and useEffect for local state management, with debounced auto-save for critical fields. UI updates instantly while API requests happen in the background.

Course Creation (Step 1)

The initial course creation process captures essential metadata and prepares the course for detailed content building. Implemented in the CreateCourse.jsx component with React Hook Form validation.

Create Course Form
Figure 2: Create Course form with required fields

Required Fields

Field Description Validation Required
Course Title Public display name for the course Max 100 characters Yes
Course Slug URL-friendly identifier Auto-generated from title, unique constraint Yes
About Course Detailed description (supports rich text via TinyMCE) HTML formatting allowed No
Thumbnail Course cover image 700x430px recommended, ≤5MB Yes
Category Course classification Select from predefined categories Yes

File Upload Specifications

Thumbnail Requirements
{
  "dimensions": "700x430 pixels (16:9 ratio)",
  "formats": ["jpg", "jpeg", "png", "webp"],
  "maxSize": "5MB",
  "aspectRatio": "16:9",
  "colorMode": "RGB",
  "optimization": "Compress before upload for faster loading"
}
    

React Component Implementation

CreateCourse.jsx - Form Submission
const CreateCourse = () => {
  const navigate = useNavigate();
  const [thumbnailPreview, setThumbnailPreview] = useState('');
  const [loading, setLoading] = useState(false);
  
  // Slug auto-generation from title
  const generateSlug = (title) => {
    return title
      .toLowerCase()
      .replace(/[^\w\s]/g, '')
      .replace(/\s+/g, '-');
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    
    const formData = new FormData();
    formData.append('title', title);
    formData.append('slug', generateSlug(title));
    formData.append('description', description);
    formData.append('category', category);
    formData.append('thumbnail', thumbnailFile);
    formData.append('price', '0');        // Default - set in Step 2
    formData.append('originalPrice', '0'); // Default - set in Step 2
    formData.append('maxStudents', '0');   // Default - set in Step 2

    try {
      const response = await api.post('/instructor/courses', formData, {
        headers: { 'Content-Type': 'multipart/form-data' }
      });
      
      if (response.data.success) {
        // React Router navigation to edit page with course ID
        navigate(`/instructor/edit-course/${response.data.course._id}`);
      }
    } catch (error) {
      console.error('Course creation failed:', error);
      toast.error(error.response?.data?.message || 'Failed to create course');
    } finally {
      setLoading(false);
    }
  };
  
  // Instant thumbnail preview
  const handleThumbnailChange = (e) => {
    const file = e.target.files[0];
    if (file) {
      setThumbnailFile(file);
      setThumbnailPreview(URL.createObjectURL(file));
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
    </form>
  );
};

Success Response

JSON Response
{
  "success": true,
  "message": "Course created successfully!",
  "course": {
    "_id": "67a1b2c3d4e5f67890123456",
    "title": "Full Stack Web Development",
    "slug": "full-stack-web-development",
    "status": "Draft",
    "thumbnail": "/uploads/thumbnails/1704038400000-full-stack-web-dev.jpg",
    "category": "web-development",
    "instructor": {
      "_id": "instructor_user_id",
      "name": "John Doe"
    },
    "createdAt": "2024-12-30T10:30:00.000Z",
    "updatedAt": "2024-12-30T10:30:00.000Z"
  }
}
    

Common Errors

Error Message: "Course with this slug already exists"

Solution: The slug is auto-generated from title. If a duplicate exists, the system automatically appends a timestamp or unique ID. Manual slug editing is available in course settings.

Error Message: "Course thumbnail is required." or file size/format errors

Solution: Ensure thumbnail meets requirements:

  1. Check file size (≤5MB) - frontend validation prevents oversized uploads
  2. Verify format (JPG, PNG, WEBP only)
  3. Recommended 700x430px (16:9 ratio)
  4. Instant preview shows upload success/failure immediately

Error Message: "No token provided" or "Access denied"

Solution: This route is protected by PrivateRoute component. Users are redirected to login if not authenticated. The axios interceptor automatically attaches the JWT token to all requests.

React Optimization: The CreateCourse component features:
  • Instant thumbnail preview using URL.createObjectURL()
  • Real-time slug generation from title input
  • Form validation before submission
  • Loading states on submit button to prevent double submission
  • React Router navigation after successful creation
  • Toast notifications for success/error feedback

Course Editing (Step 2)

The full course builder provides comprehensive configuration through a tabbed React interface with real-time updates and nested curriculum management. Implemented in EditCourse.jsx with React Router params for course ID.

Edit Course Interface
Figure 3: Edit Course interface with tab navigation and curriculum builder

Tab Overview

1

Course Info & Media

Basic course information and media assets with preview functionality

  • Title & Slug: Editable with auto-slug generation
  • Description: Rich text editor (TinyMCE) support
  • Thumbnail: 700x430px required, preview on change
  • Course Logo: Square logo (500x500px) for certificates
  • Preview Video: YouTube/Vimeo URL integration
  • Course Intro: Optional welcome video/audio
2

Course Settings

General Settings
  • Maximum Students (0 = unlimited)
  • Difficulty Level (Beginner to Expert)
  • Course Status (Draft/Pending/Published)
  • School selection from master list
  • Categories (multi-select)
Feature Toggles
  • Public Course (no enrollment required)
  • Enable Q&A (student questions)
  • MasterClass (premium flag)
  • Includes Certificate
3

Pricing

Paid Course
  • Regular Price (displayed with strikethrough)
  • Discounted Price (actual sale price)
  • Automatic discount percentage calculation
Free Course
  • Checkbox to set as free
  • Both price fields set to 0
  • Students enroll without payment
4

Curriculum Builder

Nested drag-and-drop interface for course structure

  • Topics (Episodes): Containers for lessons/quizzes/assignments
  • Lessons: Video/content items with file attachments
  • Quizzes: Auto-graded assessments with multiple question types
  • Assignments: Instructor-graded submissions
  • Real-time reordering within and between topics
5

Certificate

  • Template selection (Landscape/Portrait)
  • Signature upload for instructor
  • Completion criteria settings

React Component Structure

EditCourse.jsx - Main Component
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';

// Tab Components
import CourseInfoTab from './tabs/CourseInfoTab';
import CourseSettingsTab from './tabs/CourseSettingsTab';
import PricingTab from './tabs/PricingTab';
import CurriculumTab from './tabs/CurriculumTab';
import CertificateTab from './tabs/CertificateTab';

const EditCourse = () => {
    const { courseId } = useParams();
    const { user } = useAuth();
    const [activeTab, setActiveTab] = useState('info');
    const [course, setCourse] = useState(null);
    const [loading, setLoading] = useState(true);
    const [saving, setSaving] = useState(false);
    const [error, setError] = useState('');

    // Fetch course data on mount
    useEffect(() => {
        fetchCourse();
    }, [courseId]);

    const fetchCourse = async () => {
        try {
            const token = localStorage.getItem('lmsToken');
            const response = await fetch(`/api/courses/${courseId}`, {
                headers: { 'x-auth-token': token }
            });
            const data = await response.json();
            if (data.success) {
                setCourse(data.course);
            }
        } catch (err) {
            setError('Failed to load course');
        } finally {
            setLoading(false);
        }
    };

    // Tab navigation
    const tabs = [
        { id: 'info', label: 'Course Info', icon: 'feather-info' },
        { id: 'settings', label: 'Settings', icon: 'feather-settings' },
        { id: 'pricing', label: 'Pricing', icon: 'feather-dollar-sign' },
        { id: 'curriculum', label: 'Curriculum', icon: 'feather-book-open' },
        { id: 'certificate', label: 'Certificate', icon: 'feather-award' }
    ];

    if (loading) return 
Loading course...
; if (!course) return
Course not found
; return ( <div className="tru-edit-course-area bg-color-white tru-section-gap"> <div className="container"> {/* Tab Navigation */} <ul className="nav nav-tabs course-edit-tabs" role="tablist"> {tabs.map(tab => ( <li key={tab.id} className="nav-item" role="presentation"> <button className={`nav-link ${activeTab === tab.id ? 'active' : ''}`} onClick={() => setActiveTab(tab.id)} type="button" > <i className={`${tab.icon} me-2`}></i> {tab.label} </button> </li> ))} </ul> {/* Tab Content */} <div className="tab-content mt-4"> {activeTab === 'info' && ( <CourseInfoTab course={course} setCourse={setCourse} setSaving={setSaving} setError={setError} /> )} {activeTab === 'settings' && ( <CourseSettingsTab course={course} setCourse={setCourse} setSaving={setSaving} /> )} {activeTab === 'pricing' && ( <PricingTab course={course} setCourse={setCourse} setSaving={setSaving} /> )} {activeTab === 'curriculum' && ( <CurriculumTab courseId={courseId} episodes={course.episodes || []} /> )} {activeTab === 'certificate' && ( <CertificateTab course={course} setCourse={setCourse} setSaving={setSaving} /> )} </div> {/* Save Status */} {saving && ( <div className="alert alert-info mt-3"> <i className="feather-loader spin me-2"></i> Saving changes... </div> )} {error && ( <div className="alert alert-danger mt-3"> <i className="feather-alert-circle me-2"></i> {error} </div> )} </div> </div> ); }; export default EditCourse;

Curriculum Builder Implementation

CurriculumTab.jsx - Nested Structure
const CurriculumTab = ({ courseId, episodes }) => {
    const [topics, setTopics] = useState(episodes || []);
    const [expandedTopic, setExpandedTopic] = useState(null);

    // Add new topic
    const addTopic = async () => {
        const title = prompt('Enter topic title:');
        if (!title) return;

        try {
            const token = localStorage.getItem('lmsToken');
            const response = await fetch(`/api/courses/${courseId}/episodes`, {
                method: 'POST',
                headers: {
                    'x-auth-token': token,
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ title, summary: '' })
            });
            
            const data = await response.json();
            if (data.success) {
                setTopics([...topics, data.episode]);
            }
        } catch (err) {
            alert('Failed to create topic');
        }
    };

    // Drag and drop reordering
    const moveItem = (fromIndex, toIndex, type, parentIndex = null) => {
        // Implementation for reordering topics and lessons
        // Updates state and syncs with backend
    };

    return (
        <div className="curriculum-builder">
            <div className="d-flex justify-content-between mb-3">
                <h4>Course Curriculum</h4>
                <button 
                    className="tru-btn btn-outline-primary"
                    onClick={addTopic}
                >
                    <i className="feather-plus me-2"></i>
                    Add Topic
                </button>
            </div>

            <div className="topics-container">
                {topics.map((topic, topicIndex) => (
                    <TopicItem
                        key={topic._id}
                        topic={topic}
                        topicIndex={topicIndex}
                        courseId={courseId}
                        isExpanded={expandedTopic === topic._id}
                        onToggle={() => setExpandedTopic(
                            expandedTopic === topic._id ? null : topic._id
                        )}
                        onMove={moveItem}
                    />
                ))}
            </div>
        </div>
    );
};
    

API Endpoints

Method Endpoint Description
GET /api/courses/:courseId Fetch full course data
PUT /api/courses/:courseId Update course details (multipart/form-data)
POST /api/courses/:courseId/episodes Create new topic
PUT /api/courses/:courseId/episodes/:episodeId Update topic
DELETE /api/courses/:courseId/episodes/:episodeId Delete topic
POST /api/courses/:courseId/episodes/:episodeId/lessons Add lesson to topic (supports file upload)
POST /api/courses/:courseId/episodes/:episodeId/quizzes Add quiz to topic
PUT /api/courses/:courseId/reorder Update curriculum order (bulk update)

Database Schema

Course Model Structure (models/Course.js)
const CourseSchema = new mongoose.Schema({
    title: { type: String, required: true },
    slug: { type: String, required: true, unique: true },
    description: String,
    instructor: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
    thumbnail: String,
    courseLogo: String,
    previewVideo: String,
    courseIntro: String,
    price: { type: Number, default: 0 },
    originalPrice: { type: Number, default: 0 },
    status: { 
        type: String, 
        enum: ['Draft', 'Pending', 'Published'], 
        default: 'Draft' 
    },
    difficultyLevel: {
        type: String,
        enum: ['Beginner', 'Intermediate', 'Expert', 'All Levels']
    },
    maxStudents: { type: Number, default: 0 },
    isPublic: { type: Boolean, default: false },
    isQAEnabled: { type: Boolean, default: true },
    isMasterclass: { type: Boolean, default: false },
    school: String,
    categories: [String],
    episodes: [{
        title: String,
        summary: String,
        lessons: [{
            title: String,
            content: String,
            video: String,
            attachments: [String],
            duration: Number,
            isPreview: Boolean
        }],
        quizzes: [{
            title: String,
            questions: [{
                question: String,
                options: [String],
                correctAnswer: Number,
                explanation: String
            }],
            passingScore: Number
        }],
        assignments: [{
            title: String,
            description: String,
            dueDays: Number
        }]
    }],
    certificateTemplate: {
        type: String,
        enum: ['landscape', 'portrait']
    },
    certificateOrientation: String,
    includesCertificate: { type: Boolean, default: true },
    signature: String,
    createdAt: { type: Date, default: Date.now },
    updatedAt: { type: Date, default: Date.now }
});
    
React-Specific Features:
  • Route parameter access via useParams() hook for course ID
  • Tab-based UI with conditional rendering of tab components
  • Real-time form updates with React state
  • Drag-and-drop curriculum builder with visual feedback
  • File uploads with preview using URL.createObjectURL()
  • Loading states during data fetch and save operations
  • Nested component structure for maintainability
Note: All course updates are performed through the main EditCourse component. Tab components receive course data and update functions as props, ensuring single source of truth.

Curriculum Management

Organize your course content into Topics (Episodes) containing Lessons, Quizzes, and Assignments. Implemented in CurriculumBuilder.jsx component with accordion-based navigation and modal forms for content creation.

Curriculum Builder Interface
Figure 4: Curriculum builder with topics and content items

Curriculum Structure

Topics (Episodes)

Organizational containers with title and summary

1+ per course
Lessons

Video or reading content with duration tracking

Unlimited per topic
Quizzes

Interactive assessments with multiple questions

Multiple per topic

React Component: CurriculumBuilder.jsx

Main Curriculum Component
import React, { useState } from 'react';

const CurriculumBuilder = ({ episodes = [], courseId, onRefresh, onOpenModal }) => {
    const [expandedEpisode, setExpandedEpisode] = useState(null);

    // Toggle episode accordion
    const toggleEpisode = (episodeId) => {
        setExpandedEpisode(expandedEpisode === episodeId ? null : episodeId);
    };

    // Delete an item (lesson, quiz, assignment)
    const handleDeleteItem = async (episodeId, itemId, itemType) => {
        if (!window.confirm(`Are you sure you want to delete this ${itemType}?`)) {
            return;
        }

        try {
            const token = localStorage.getItem('lmsToken');
            const pathSegment = itemType === 'quiz' ? 'quizzes' : `${itemType}s`;
            
            const response = await fetch(`/api/courses/${courseId}/episodes/${episodeId}/${pathSegment}/${itemId}`, {
                method: 'DELETE',
                headers: { 'x-auth-token': token }
            });

            const result = await response.json();
            if (result.success) {
                onRefresh();
            } else {
                alert(`Error: ${result.message}`);
            }
        } catch (error) {
            alert(`An error occurred while deleting the ${itemType}.`);
        }
    };

    // Delete an episode (topic)
    const handleDeleteEpisode = async (episodeId) => {
        if (!window.confirm('Are you sure you want to delete this topic? All lessons and quizzes within will also be deleted.')) {
            return;
        }

        try {
            const token = localStorage.getItem('lmsToken');
            const response = await fetch(`/api/courses/${courseId}/episodes/${episodeId}`, {
                method: 'DELETE',
                headers: { 'x-auth-token': token }
            });

            const result = await response.json();
            if (result.success) {
                onRefresh();
            }
        } catch (error) {
            alert('An error occurred while deleting the topic.');
        }
    };

    // Get icon based on item type
    const getItemIcon = (itemType, lessonType) => {
        if (itemType === 'lesson') {
            return lessonType === 'reading' ? 'feather-file-text' : 'feather-play-circle';
        }
        if (itemType === 'quiz') return 'feather-help-circle';
        if (itemType === 'assignment') return 'feather-clipboard';
        return 'feather-file';
    };

    // Render content items (lessons, quizzes)
    const renderContentItems = (episode) => {
        const items = [
            ...(episode.lessons || []).map(item => ({ ...item, type: 'lesson' })),
            ...(episode.quizzes || []).map(item => ({ ...item, type: 'quiz' })),
        ];

        if (items.length === 0) {
            return (
                <div className="text-center p-3 border rounded bg-light mb-4">
                    <i className="feather-info text-muted me-2"></i>
                    <span className="text-muted">No content yet. Add lessons or quizzes below.</span>
                </div>
            );
        }

        return items.map((item) => (
            <div key={item._id} className="d-flex justify-content-between tru-course-wrape mb-4">
                <div className="col-10 inner d-flex align-items-center gap-2">
                    <i className={getItemIcon(item.type, item.lessonType)}></i>
                    <h6 className="tru-title mb-0">{item.title}</h6>
                    {item.exerciseFiles?.length > 0 && (
                        <i className="feather-paperclip ms-2 text-muted"></i>
                    )}
                    {item.duration && (
                        <span className="badge bg-light text-dark border ms-2">
                            <i className="feather-clock me-1"></i>
                            {item.duration}
                        </span>
                    )}
                </div>
                <div className="col-2 inner">
                    <ul className="tru-list-style-1 tru-course-list d-flex gap-2">
                        <li>
                            <i 
                                className="feather-trash delete-item cb-action-icon"
                                onClick={() => handleDeleteItem(episode._id, item._id, item.type)}
                                title={`Delete ${item.type}`}
                            ></i>
                        </li>
                        <li>
                            <i 
                                className="feather-edit edit-item cb-action-icon"
                                onClick={() => {
                                    if (item.type === 'lesson') {
                                        onOpenModal('lesson', {
                                            episodeId: episode._id,
                                            lessonData: item
                                        });
                                    } else if (item.type === 'quiz') {
                                        onOpenModal('quiz', {
                                            episodeId: episode._id,
                                            quizData: item
                                        });
                                    }
                                }}
                                title={`Edit ${item.type}`}
                            ></i>
                        </li>
                    </ul>
                </div>
            </div>
        ));
    };

    return (
        <div className="course-builder-wrapper">
            {episodes.length === 0 ? (
                <div className="text-center p-5 border rounded bg-light">
                    <i className="feather-folder text-muted mb-3 cb-empty-icon"></i>
                    <h5 className="text-muted">No topics yet</h5>
                    <p className="text-muted">Click "Add New Topic" to start building your course curriculum</p>
                </div>
            ) : (
                episodes.map((episode) => (
                    <div key={episode._id} className="accordion-item card mb--20">
                        <h2 className="accordion-header card-header tru-course">
                            <button 
                                className={`accordion-button ${expandedEpisode === episode._id ? '' : 'collapsed'}`}
                                onClick={() => toggleEpisode(episode._id)}
                            >
                                {episode.title}
                            </button>
                            <span 
                                className="tru-course-icon tru-course-edit"
                                onClick={() => onOpenModal('topic', episode)}
                                title="Edit Topic"
                            ></span>
                            <span 
                                className="tru-course-icon tru-course-del"
                                onClick={() => handleDeleteEpisode(episode._id)}
                                title="Delete Topic"
                            ></span>
                        </h2>
                        
                        {expandedEpisode === episode._id && (
                            <div className="accordion-collapse collapse show">
                                <div className="accordion-body card-body">
                                    {episode.summary && (
                                        <div className="mb-4">
                                            <h6 className="d-flex align-items-center gap-2">
                                                <i className="feather-info text-primary"></i>
                                                <span>Topic Summary</span>
                                            </h6>
                                            <p className="text-muted ms-4">{episode.summary}</p>
                                        </div>
                                    )}

                                    <div className="mb-4">
                                        <h6 className="d-flex align-items-center gap-2 mb-3">
                                            <i className="feather-list text-primary"></i>
                                            <span>Content ({episode.lessons?.length || 0} lessons, {episode.quizzes?.length || 0} quizzes)</span>
                                        </h6>
                                        {renderContentItems(episode)}
                                    </div>

                                    <div className="border-top pt-4 mt-4">
                                        <div className="d-flex flex-wrap gap-3">
                                            <button 
                                                className="tru-btn btn-border hover-icon-reverse tru-sm-btn-2"
                                                onClick={() => onOpenModal('lesson', { episodeId: episode._id })}
                                            >
                                                <span className="icon-reverse-wrapper">
                                                    <span className="btn-text">Add Lesson</span>
                                                    <span className="btn-icon"><i className="feather-plus-circle"></i></span>
                                                    <span className="btn-icon"><i className="feather-plus-circle"></i></span>
                                                </span>
                                            </button>
                                            
                                            <button 
                                                className="tru-btn btn-border hover-icon-reverse tru-sm-btn-2"
                                                onClick={() => onOpenModal('quiz', { episodeId: episode._id })}
                                            >
                                                <span className="icon-reverse-wrapper">
                                                    <span className="btn-text">Add Quiz</span>
                                                    <span className="btn-icon"><i className="feather-plus-circle"></i></span>
                                                    <span className="btn-icon"><i className="feather-plus-circle"></i></span>
                                                </span>
                                            </button>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        )}
                    </div>
                ))
            )}
        </div>
    );
};

export default CurriculumBuilder;
    

Modal Components

The curriculum uses three modal components for creating and editing content:

Modal File Purpose
Topic Modal TopicModal.jsx Create and edit topics (episodes) with title and summary
Lesson Modal LessonModal.jsx Create and edit lessons with title, content, video URL, attachments, and duration
Quiz Modal QuizModal.jsx Create and edit quizzes with questions, options, correct answers, and explanations

Content Item Types

Type Icon Description Edit Modal
Video Lesson Vimeo/YouTube/Local video with duration tracking LessonModal.jsx
Reading Lesson Rich text content (lessonType='reading') LessonModal.jsx
Quiz Interactive assessment with multiple questions QuizModal.jsx
Assignment Practical exercises with file submissions (exerciseFiles) LessonModal.jsx (as lesson type)

API Endpoints

Method Endpoint Description
POST /api/courses/:courseId/episodes Create new topic
PUT /api/courses/:courseId/episodes/:episodeId Update topic
DELETE /api/courses/:courseId/episodes/:episodeId Delete topic
POST /api/courses/:courseId/episodes/:episodeId/lessons Add lesson to topic
PUT /api/courses/:courseId/episodes/:episodeId/lessons/:lessonId Update lesson
DELETE /api/courses/:courseId/episodes/:episodeId/lessons/:lessonId Delete lesson
POST /api/courses/:courseId/episodes/:episodeId/quizzes Add quiz to topic
PUT /api/courses/:courseId/episodes/:episodeId/quizzes/:quizId Update quiz
DELETE /api/courses/:courseId/episodes/:episodeId/quizzes/:quizId Delete quiz

Modal Opening Pattern

Parent Component Implementation (EditCourse.jsx)
// Modal state
const [modalConfig, setModalConfig] = useState({
    type: null, // 'topic', 'lesson', 'quiz'
    data: null
});

// Open modal handler
const handleOpenModal = (type, data = null) => {
    setModalConfig({ type, data });
};

// Close modal handler
const handleCloseModal = () => {
    setModalConfig({ type: null, data: null });
    refreshCourse(); // Refresh after modal closes
};

// Render modals
{modalConfig.type === 'topic' && (
    <TopicModal
        show={true}
        onClose={handleCloseModal}
        courseId={courseId}
        topicData={modalConfig.data}
    />
)}

{modalConfig.type === 'lesson' && (
    <LessonModal
        show={true}
        onClose={handleCloseModal}
        courseId={courseId}
        episodeId={modalConfig.data?.episodeId}
        lessonData={modalConfig.data?.lessonData}
    />
)}

{modalConfig.type === 'quiz' && (
    <QuizModal
        show={true}
        onClose={handleCloseModal}
        courseId={courseId}
        episodeId={modalConfig.data?.episodeId}
        quizData={modalConfig.data?.quizData}
    />
)}
    
React Features:
  • Accordion-based UI with expand/collapse state per topic
  • Conditional rendering of content based on episode expansion
  • Icon mapping based on lesson type (video/reading)
  • Delete confirmation with window.confirm()
  • Modal communication via onOpenModal callback
  • Automatic refresh after CRUD operations via onRefresh
Note: The CurriculumBuilder is a presentational component. All data fetching and modal state management is handled by the parent EditCourse component.

Lesson System

TruLern supports two types of lessons: Video Lessons (Vimeo/YouTube/Local) and Reading Lessons (rich text articles) with exercise file attachments. Implemented in LessonModal.jsx using React hooks and TinyMCE editor.

Lesson Modal Interface
Figure 5: Lesson creation modal with react-select dropdowns

Lesson Types Comparison

Video Lessons

  • Vimeo integration
  • YouTube embedding
  • Local video upload (500MB max)
  • Duration tracking (hr/min/sec)
  • Preview mode toggle (optional)
Best for: Demonstrations

Reading Lessons

  • TinyMCE rich text editor
  • Full HTML support
  • Image embedding
  • Code formatting options
  • Exercise file attachments
Best for: Theory & Articles

Video Sources Configuration

Source URL Format / Input Max Size Player
Vimeo https://vimeo.com/123456789 N/A Vimeo Embedded Player
YouTube https://youtube.com/watch?v=ABC123 N/A YouTube iFrame API
Local Upload File input (accept="video/*") 500 MB HTML5 Video Player

React Component: LessonModal.jsx

Lesson Modal with react-select and TinyMCE
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import { Editor } from '@tinymce/tinymce-react';
import Select from 'react-select';

const LessonModal = ({ show, onClose, onSave, courseId, episodeId, initialData = null }) => {
    const [lessonType, setLessonType] = useState('video');
    const [title, setTitle] = useState('');
    const [summary, setSummary] = useState('');
    const [isPreview, setIsPreview] = useState(false);

    // Video fields
    const [videoSource, setVideoSource] = useState('vimeo');
    const [videoUrl, setVideoUrl] = useState('');
    const [videoFile, setVideoFile] = useState(null);
    const [durationHr, setDurationHr] = useState('0');
    const [durationMin, setDurationMin] = useState('0');
    const [durationSec, setDurationSec] = useState('0');

    // Reading fields
    const [articleBody, setArticleBody] = useState('');

    // Exercise files
    const [exerciseFiles, setExerciseFiles] = useState([]);
    const [existingExerciseFiles, setExistingExerciseFiles] = useState([]);
    const [filesToDelete, setFilesToDelete] = useState([]);

    // State
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState('');
    const fileInputRef = useRef(null);

    // TinyMCE configuration
    const editorConfig = {
        height: 300,
        menubar: false,
        plugins: 'link lists preview',
        toolbar: 'undo redo | formatselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link | preview',
        content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
    };

    // Options for react-select
    const lessonTypeOptions = [
        { value: 'video', label: 'Video Lesson' },
        { value: 'reading', label: 'Reading / Article Lesson' }
    ];

    const videoSourceOptions = [
        { value: 'vimeo', label: 'Vimeo' },
        { value: 'youtube', label: 'YouTube' },
        { value: 'local', label: 'Local Video Upload' }
    ];

    // Initialize form when modal opens
    useEffect(() => {
        if (show) {
            if (initialData) {
                // Editing existing lesson
                setLessonType(initialData.lessonType || 'video');
                setTitle(initialData.title || '');
                setSummary(initialData.summary || '');
                setIsPreview(initialData.isPreview || false);

                // Video fields
                setVideoSource(initialData.videoSource || 'vimeo');
                setVideoUrl(initialData.videoUrl || initialData.vimeoUrl || '');

                // Parse duration
                if (initialData.duration) {
                    const durationMatch = initialData.duration.match(/(\d+)\s*hr\s*(\d+)\s*min\s*(\d+)\s*sec/);
                    if (durationMatch) {
                        setDurationHr(durationMatch[1] || '0');
                        setDurationMin(durationMatch[2] || '0');
                        setDurationSec(durationMatch[3] || '0');
                    }
                }

                // Reading fields
                setArticleBody(initialData.articleBody || '');

                // Exercise files
                setExistingExerciseFiles(initialData.exerciseFiles || []);
            } else {
                resetForm();
            }
            setError('');
        }
    }, [show, initialData]);

    const resetForm = () => {
        setLessonType('video');
        setTitle('');
        setSummary('');
        setIsPreview(false);
        setVideoSource('vimeo');
        setVideoUrl('');
        setVideoFile(null);
        setDurationHr('0');
        setDurationMin('0');
        setDurationSec('0');
        setArticleBody('');
        setExerciseFiles([]);
        setExistingExerciseFiles([]);
        setFilesToDelete([]);
        if (fileInputRef.current) {
            fileInputRef.current.value = '';
        }
    };

    const handleFileSelect = (e) => {
        const files = Array.from(e.target.files);
        setExerciseFiles(prev => [...prev, ...files]);
    };

    const removeNewFile = (index) => {
        setExerciseFiles(prev => prev.filter((_, i) => i !== index));
    };

    const removeExistingFile = async (filePath) => {
        if (!window.confirm('Are you sure you want to remove this file?')) return;

        try {
            const token = localStorage.getItem('lmsToken');
            const response = await fetch(
                `/api/courses/${courseId}/episodes/${episodeId}/lessons/${initialData._id}/files`,
                {
                    method: 'DELETE',
                    headers: {
                        'Content-Type': 'application/json',
                        'x-auth-token': token,
                    },
                    body: JSON.stringify({ filePath }),
                }
            );

            const result = await response.json();
            if (result.success) {
                setExistingExerciseFiles(prev => prev.filter(file => file.path !== filePath));
                setFilesToDelete(prev => [...prev, filePath]);
            } else {
                alert(`Error: ${result.message}`);
            }
        } catch (error) {
            alert('An error occurred while removing the file.');
        }
    };

    const validateForm = () => {
        if (!title.trim()) {
            return 'Please enter a lesson title.';
        }

        if (lessonType === 'video') {
            if (videoSource === 'local' && !videoFile && !initialData?.videoPath) {
                return 'Please upload a video file or select an existing video.';
            }
            if ((videoSource === 'vimeo' || videoSource === 'youtube') && !videoUrl.trim()) {
                return 'Please enter a video URL.';
            }
        } else if (lessonType === 'reading') {
            if (!articleBody.trim()) {
                return 'Please enter lesson content.';
            }
        }

        return null;
    };

    const handleSubmit = async (e) => {
        e.preventDefault();
        setLoading(true);
        setError('');

        const validationError = validateForm();
        if (validationError) {
            setError(validationError);
            setLoading(false);
            return;
        }

        try {
            const token = localStorage.getItem('lmsToken');
            const isEditing = !!initialData;

            const url = isEditing
                ? `/api/courses/${courseId}/episodes/${episodeId}/lessons/${initialData._id}`
                : `/api/courses/${courseId}/episodes/${episodeId}/lessons`;

            const formData = new FormData();
            formData.append('lessonType', lessonType);
            formData.append('title', title);
            formData.append('summary', summary);
            formData.append('isPreview', isPreview);

            if (lessonType === 'video') {
                formData.append('videoSource', videoSource);
                if (videoSource === 'local' && videoFile) {
                    formData.append('videoFile', videoFile);
                } else {
                    formData.append('videoUrl', videoUrl);
                }
                const duration = `${durationHr} hr ${durationMin} min ${durationSec} sec`;
                formData.append('duration', duration);
            } else {
                formData.append('articleBody', articleBody);
                formData.append('duration', '5 min'); // Default for reading
            }

            // Add exercise files
            exerciseFiles.forEach(file => {
                formData.append('exerciseFiles', file);
            });

            // Mark files to delete
            if (filesToDelete.length > 0) {
                formData.append('filesToDelete', JSON.stringify(filesToDelete));
            }

            const response = await fetch(url, {
                method: isEditing ? 'PUT' : 'POST',
                headers: { 'x-auth-token': token },
                body: formData,
            });

            const result = await response.json();

            if (result.success) {
                onSave(); // Refresh curriculum
                onClose(); // Close modal
                resetForm();
            } else {
                setError(result.message || `Failed to ${isEditing ? 'update' : 'add'} lesson.`);
            }
        } catch (err) {
            setError('An error occurred. Please try again.');
        } finally {
            setLoading(false);
        }
    };

    if (!show) return null;

    return ReactDOM.createPortal(
        // Modal JSX (truncated for documentation)
        <div className="tru-default-modal modal fade show d-block">
            {/* Modal content matching your implementation */}
        </div>,
        document.body
    );
};

export default LessonModal;
    

Form Fields Reference

Field Type Component Validation
Lesson Type Select react-select Required
Lesson Title Text Input <input type="text"> Required, max 100 chars
Video Source Select react-select Required for video lessons
Video URL Text Input <input type="text"> Required for Vimeo/YouTube
Video File File Input <input type="file" accept="video/*"> Required for local uploads
Duration Number Inputs (3) <input type="number"> (hr/min/sec) Required for video lessons
Lesson Content Rich Text @tinymce/tinymce-react Required for reading lessons
Lesson Summary Textarea <textarea> Optional, max 500 chars
Exercise Files Multi-file Upload <input type="file" multiple> Optional, max 10 files
Preview Toggle Switch <input type="checkbox" role="switch"> Optional (currently hidden)

Exercise Files System

File Specifications
  • Format: Any file type supported
  • Max Size: 50MB per file
  • Max Files: 10 per lesson
  • Multiple upload: Yes via multiple attribute
File Management
  • Add files during/after creation
  • Remove individual existing files (API call)
  • Remove newly added files (local state)
  • Preview links for existing files
State Tracking
  • exerciseFiles - New files to upload
  • existingExerciseFiles - Already uploaded files
  • filesToDelete - Files marked for deletion

API Endpoints

1

Create New Lesson

POST /api/courses/:courseId/episodes/:episodeId/lessons
// Headers
{
    "x-auth-token": "Bearer <token>",
    "Content-Type": "multipart/form-data"
}

// FormData fields
lessonType: "video"
title: "Introduction to Components"
summary: "Learn about React components"
isPreview: false
videoSource: "vimeo"
videoUrl: "https://vimeo.com/123456789"
duration: "1 hr 15 min 30 sec"
exerciseFiles: [File, File]  // Multiple files
            
2

Update Existing Lesson

PUT /api/courses/:courseId/episodes/:episodeId/lessons/:lessonId
// Same fields as POST plus:
filesToDelete: ["uploads/pdfs/file1.pdf", "uploads/pdfs/file2.pdf"]  // JSON stringified array
            
3

Delete Individual Exercise File

DELETE /api/courses/:courseId/episodes/:episodeId/lessons/:lessonId/files
// Request Body
{
    "filePath": "uploads/pdfs/1704038400000-assignment.pdf"
}
            

Database Schema

Lesson Structure in Course Model
// Inside episodes array
lessons: [{
    _id: ObjectId,
    title: String,
    summary: String,
    lessonType: { type: String, enum: ['video', 'reading'] },
    
    // Video-specific fields
    videoSource: { type: String, enum: ['vimeo', 'youtube', 'local'] },
    videoUrl: String,      // For Vimeo/YouTube
    videoPath: String,     // For local uploads
    duration: String,      // Format: "1 hr 15 min 30 sec"
    
    // Reading-specific fields
    articleBody: String,   // HTML content
    
    // Common fields
    isPreview: Boolean,
    exerciseFiles: [{
        filename: String,
        path: String,
        size: Number,
        uploadedAt: Date
    }],
    createdAt: Date
}]
    

Troubleshooting

Solution: Ensure classNamePrefix matches your CSS:

<Select
    classNamePrefix="tru-select"  // Matches your CSS classes
    className="react-select-container w-100"
/>
                    

Error: "This domain is not registered with Tiny Cloud"

Solution: The API key in your component (0f3rfxypgq3slagbe1wg2dkk6vzc4c23f6gg268x74tht4u3) is valid. If issues occur:

  1. Check if TinyMCE script loads properly
  2. Verify API key is correct in your Tiny Cloud account
  3. For production, register your domain with Tiny Cloud
Issue Solution
File size > 50MB Compress or split into multiple files
Multiple files selected Component handles arrays with exerciseFiles.forEach()
File deletion not working Check filesToDelete is stringified: JSON.stringify(filesToDelete)
React Features:
  • Portal rendering with ReactDOM.createPortal() for modal overlay
  • Conditional field rendering based on lesson type and video source
  • Multiple file upload with preview and removal
  • Existing file management with delete API integration
  • Loading states and form validation
  • TinyMCE integration with @tinymce/tinymce-react

Quiz Engine

Advanced assessment system with multi-step wizard interface, multiple question types, flexible settings, and automatic grading. Implemented in QuizModal.jsx with React hooks and react-select dropdowns.

Quiz Builder Interface
Figure 6: Multi-step quiz creation wizard with progress indicator

Wizard Steps

1
Quiz Info

Title and summary

33% progress
2
Questions

Add, edit, delete questions

66% progress
3
Settings

Configure quiz behavior

100% progress

Question Types

Single Choice

One correct answer (Radio buttons)

Auto-graded
Multiple Choice

Multiple correct answers (Checkboxes)

Auto-graded
Open Ended

Text answer, manual grading

Manual Grade

React Component: QuizModal.jsx

Multi-step Quiz Modal with State Management
import React, { useState, useEffect, useCallback, useRef } from 'react';
import ReactDOM from 'react-dom';
import Select from 'react-select';

const QuizModal = ({ show, onClose, onSave, courseId, episodeId, initialData = null }) => {
    // Wizard steps
    const steps = {
        INFO: 1,
        QUESTIONS_LIST: 2,
        ADD_EDIT_QUESTION_FORM: 3,
        SETTINGS: 4
    };

    const [currentStep, setCurrentStep] = useState(steps.INFO);
    const [quizId, setQuizId] = useState(null);

    // Quiz info
    const [title, setTitle] = useState('');
    const [summary, setSummary] = useState('');

    // Questions
    const [questions, setQuestions] = useState([]);
    const [currentEditingQuestionId, setCurrentEditingQuestionId] = useState(null);
    const [questionText, setQuestionText] = useState('');
    const [questionType, setQuestionType] = useState('single-choice');
    const [questionPoints, setQuestionPoints] = useState(10);
    const [answerOptions, setAnswerOptions] = useState([{ text: '', isCorrect: false }]);

    // Settings
    const [timeLimitValue, setTimeLimitValue] = useState(0);
    const [timeLimitUnit, setTimeLimitUnit] = useState({ value: 'Hour', label: 'Hour' });
    const [hideTimeLimit, setHideTimeLimit] = useState(false);
    const [feedbackMode, setFeedbackMode] = useState('default');
    const [passingGrade, setPassingGrade] = useState(50);
    const [maxQuestionsAllowed, setMaxQuestionsAllowed] = useState(10);
    const [autoStart, setAutoStart] = useState(false);
    const [questionLayout, setQuestionLayout] = useState({ value: 'single_question', label: 'Single Question' });
    const [questionOrder, setQuestionOrder] = useState({ value: 'Random', label: 'Random' });
    const [hideQuestionNumber, setHideQuestionNumber] = useState(false);
    const [shortAnswerCharLimit, setShortAnswerCharLimit] = useState(200);
    const [essayCharLimit, setEssayCharLimit] = useState(500);
    const [advanceSettingsOpen, setAdvancedSettingsOpen] = useState(false);

    // State
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState('');
    const [questionError, setQuestionError] = useState('');

    // Options for react-select
    const timeUnitOptions = [
        { value: 'Weeks', label: 'Weeks' },
        { value: 'Day', label: 'Day' },
        { value: 'Hour', label: 'Hour' }
    ];

    const questionTypeOptions = [
        { value: 'single-choice', label: 'Single Choice' },
        { value: 'multiple-choice', label: 'Multiple Choice' },
        { value: 'open-ended', label: 'Open Ended (Text Answer)' }
    ];

    const questionOrderOptions = [
        { value: 'Random', label: 'Random' },
        { value: 'sorting', label: 'Sorting' },
        { value: 'asc', label: 'Ascending' },
        { value: 'desc', label: 'Descending' }
    ];

    const questionLayoutOptions = [
        { value: 'single_question', label: 'Single Question' },
        { value: 'question_pagination', label: 'Question Pagination' },
        { value: 'question_below_each_other', label: 'Question below each other' }
    ];

    const feedbackModeOptions = [
        { value: 'default', label: 'Default', description: 'Answers shown after quiz is finished' },
        { value: 'reveal', label: 'Reveal Mode', description: 'Show result after the attempt.' },
        { value: 'retry', label: 'Retry Mode', description: 'Reattempt quiz any number of times.' }
    ];

    // Progress calculation
    const calculateProgress = () => {
        if (currentStep === steps.INFO) return 33.33;
        if (currentStep === steps.QUESTIONS_LIST || currentStep === steps.ADD_EDIT_QUESTION_FORM) return 66.66;
        if (currentStep === steps.SETTINGS) return 100;
        return 0;
    };

    // Save quiz info (Step 1)
    const saveQuizInfo = async () => {
        if (!title.trim()) {
            setError('Please enter a quiz title.');
            return;
        }

        setLoading(true);
        try {
            const token = localStorage.getItem('lmsToken');
            const isEditing = !!quizId;
            const url = isEditing
                ? `/api/courses/${courseId}/episodes/${episodeId}/quizzes/${quizId}`
                : `/api/courses/${courseId}/episodes/${episodeId}/quizzes`;

            const response = await fetch(url, {
                method: isEditing ? 'PUT' : 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'x-auth-token': token,
                },
                body: JSON.stringify({ title, summary }),
            });

            const result = await response.json();
            if (result.success) {
                if (!isEditing) setQuizId(result.quiz._id);
                setCurrentStep(steps.QUESTIONS_LIST);
                setError('');
            } else {
                setError(result.message || 'Failed to save quiz info.');
            }
        } catch (err) {
            setError('An error occurred. Please try again.');
        } finally {
            setLoading(false);
        }
    };

    // Save complete quiz (Step 3)
    const saveQuiz = async () => {
        setLoading(true);
        try {
            const token = localStorage.getItem('lmsToken');

            // Save questions first
            for (const question of questions) {
                const isTempId = question._id.startsWith('temp-');
                const method = isTempId ? 'POST' : 'PUT';
                const url = isTempId
                    ? `/api/courses/${courseId}/episodes/${episodeId}/quizzes/${quizId}/questions`
                    : `/api/courses/${courseId}/episodes/${episodeId}/quizzes/${quizId}/questions/${question._id}`;

                await fetch(url, {
                    method,
                    headers: {
                        'Content-Type': 'application/json',
                        'x-auth-token': token,
                    },
                    body: JSON.stringify({
                        questionText: question.questionText,
                        questionType: question.questionType,
                        points: question.points,
                        options: question.options || []
                    }),
                });
            }

            // Save settings
            const settingsData = {
                title,
                summary,
                settings: {
                    timeLimit: {
                        value: timeLimitValue,
                        unit: timeLimitUnit.value === 'Hour' ? 'Hours' : timeLimitUnit.value
                    },
                    hideTimeLimit,
                    feedbackMode,
                    passingGrade,
                    maxQuestionsAllowed,
                    autoStart,
                    questionLayout: questionLayout.value,
                    questionOrder: questionOrder.value,
                    hideQuestionNumber,
                    shortAnswerCharLimit,
                    essayCharLimit
                }
            };

            const response = await fetch(`/api/courses/${courseId}/episodes/${episodeId}/quizzes/${quizId}`, {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json',
                    'x-auth-token': token,
                },
                body: JSON.stringify(settingsData),
            });

            const result = await response.json();
            if (result.success) {
                onSave();
                onClose();
            } else {
                setError(result.message || 'Failed to save quiz.');
            }
        } catch (err) {
            setError('An error occurred. Please try again.');
        } finally {
            setLoading(false);
        }
    };

    return ReactDOM.createPortal(
        <div className="tru-quiz-modal-container show">
            {/* Modal JSX structure */}
        </div>,
        document.body
    );
};

export default QuizModal;
    

Progress Indicator

3-Step Progress Bar with Active States
<div className="course-field quiz-modal mb-4">
    <div className="d-flex justify-content-between mb-2">
        <span>Quiz info</span>
        <span>Question</span>
        <span>Settings</span>
    </div>
    <div className="position-relative m-4">
        <div className="progress" role="progressbar">
            <div 
                className="progress-bar tru-progress-bar" 
                data-progress={progress}
            ></div>
        </div>
        <button className="position-absolute top-0 start-0 translate-middle btn quiz-modal-btn">
            <i className="feather-check"></i>
        </button>
        <button className="position-absolute top-0 start-50 translate-middle btn quiz-modal-btn">
            2
        </button>
        <button className="position-absolute top-0 start-100 translate-middle btn quiz-modal-btn">
            3
        </button>
    </div>
</div>
    

Quiz Settings Configuration

Setting Component Default Description
Time Limit Number input + react-select 0 (unlimited) Countdown timer with Weeks/Day/Hour units
Hide Time Display Switch toggle false Hide timer from students
Feedback Mode Radio buttons with descriptions default Default, Reveal, or Retry mode
Passing Grade Number input 50% Minimum percentage to pass
Max Questions Number input 10 Randomly selected questions to show
Auto Start Switch toggle false Start timer immediately on page load
Question Layout react-select Single Question Display mode for questions
Question Order react-select Random Order of questions presented
Hide Question Number Switch toggle false Hide question numbering from students
Short Answer Limit Number input 200 chars Character limit for short answers
Essay Answer Limit Number input 500 chars Character limit for essay answers

Question Management

1

Questions List View

Displays all questions with type badges and edit/delete actions

{questions.map((q, index) => {
    const badgeColor = q.questionType === 'single-choice' ? 'primary'
        : q.questionType === 'multiple-choice' ? 'success'
            : 'warning';
    
    return (
        <div key={q._id} className="d-flex justify-content-between align-items-center tru-course-wrapper">
            <div className="inner d-flex align-items-center gap-3">
                <h6 className="tru-title mb-0">
                    <strong>{index + 1}.</strong> {q.questionText}
                </h6>
                <span className={`badge bg-${badgeColor}`}>{q.questionType}</span>
            </div>
            <div className="inner">
                <div className="tru-course-list d-flex gap-3">
                    <button onClick={() => startEditQuestion(q)}>
                        <i className="feather-edit tru-action-icon"></i>
                    </button>
                    <button onClick={() => deleteQuestion(q._id)}>
                        <i className="feather-trash tru-action-icon"></i>
                    </button>
                </div>
            </div>
        </div>
    );
})}
            
2

Add/Edit Question Form

Dynamic form with conditional rendering based on question type

<div className="row">
    <div className="col-lg-6">
        <div className="course-field mb-3 d-flex align-items-center">
            <label className="tru-form-label mb-0 me-3 text-nowrap">
                Question Type
            </label>
            <div className="w-100"> 
                <Select
                    options={questionTypeOptions}
                    value={questionTypeOptions.find(opt => opt.value === questionType)}
                    onChange={handleQuestionTypeChange}
                    classNamePrefix="tru-select"
                />
            </div>
        </div>
    </div>
    
    <div className="col-lg-6">
        <div className="course-field mb-3 d-flex align-items-center">
            <label className="tru-form-label mb-0 me-3">
                Points
            </label>
            <input
                type="number"
                className="form-control tru-input-aligned"
                value={questionPoints}
                onChange={(e) => setQuestionPoints(parseInt(e.target.value) || 10)}
            />
        </div>
    </div>
</div>
            
3

Answer Options (Choice-based Questions)

{answerOptions.map((option, index) => (
    <div key={index} className="d-flex align-items-center mb-2 quiz-option-row">
        <div className="flex-grow-1 me-2">
            <input
                type="text"
                className="form-control quiz-option-text"
                placeholder="Answer option text"
                value={option.text}
                onChange={(e) => updateAnswerOption(index, 'text', e.target.value)}
            />
        </div>
        <div className="form-check me-3">
            {questionType === 'single-choice' ? (
                <input
                    className="form-check-input quiz-option-iscorrect"
                    type="radio"
                    name={`correct-option-group`}
                    checked={option.isCorrect}
                    onChange={() => updateAnswerOption(index, 'isCorrect', true)}
                />
            ) : (
                <input
                    className="form-check-input quiz-option-iscorrect"
                    type="checkbox"
                    checked={option.isCorrect}
                    onChange={(e) => updateAnswerOption(index, 'isCorrect', e.target.checked)}
                />
            )}
        </div>
        {answerOptions.length > 1 && (
            <button onClick={() => removeAnswerOption(index)}>
                <i className="feather-x"></i>
            </button>
        )}
    </div>
))}
            

API Endpoints

1

Create/Update Quiz Info

POST /api/courses/:courseId/episodes/:episodeId/quizzes
// Request
{
    "title": "JavaScript Fundamentals Quiz",
    "summary": "Test your basic JavaScript knowledge"
}
            
2

Add Question to Quiz

POST /api/courses/:courseId/episodes/:episodeId/quizzes/:quizId/questions
// Single Choice Question
{
    "questionText": "What is the capital of France?",
    "questionType": "single-choice",
    "points": 10,
    "options": [
        { "text": "London", "isCorrect": false },
        { "text": "Berlin", "isCorrect": false },
        { "text": "Paris", "isCorrect": true },
        { "text": "Madrid", "isCorrect": false }
    ]
}

// Multiple Choice Question
{
    "questionText": "Which are JavaScript frameworks?",
    "questionType": "multiple-choice",
    "points": 15,
    "options": [
        { "text": "React", "isCorrect": true },
        { "text": "Angular", "isCorrect": true },
        { "text": "Django", "isCorrect": false },
        { "text": "Vue.js", "isCorrect": true }
    ]
}
            
3

Update Quiz Settings

PUT /api/courses/:courseId/episodes/:episodeId/quizzes/:quizId
{
    "title": "Updated Quiz Title",
    "summary": "New description",
    "settings": {
        "timeLimit": { "value": 45, "unit": "Hours" },
        "hideTimeLimit": true,
        "feedbackMode": "retry",
        "passingGrade": 80,
        "maxQuestionsAllowed": 20,
        "autoStart": true,
        "questionLayout": "single_question",
        "questionOrder": "asc",
        "hideQuestionNumber": false,
        "shortAnswerCharLimit": 250,
        "essayCharLimit": 1000
    }
}
            

Database Schema

Quiz Structure in Course Model
// Inside episodes array
quizzes: [{
    _id: ObjectId,
    title: String,
    summary: String,
    
    // Settings
    timeLimit: {
        value: Number,
        unit: String  // 'Weeks', 'Day', 'Hours'
    },
    hideTimeLimit: Boolean,
    feedbackMode: { type: String, enum: ['default', 'reveal', 'retry'] },
    passingGrade: Number,
    maxQuestionsAllowed: Number,
    autoStart: Boolean,
    questionLayout: { 
        type: String, 
        enum: ['single_question', 'question_pagination', 'question_below_each_other'] 
    },
    questionOrder: { type: String, enum: ['Random', 'sorting', 'asc', 'desc'] },
    hideQuestionNumber: Boolean,
    shortAnswerCharLimit: Number,
    essayCharLimit: Number,
    
    // Questions
    questions: [{
        _id: ObjectId,
        questionText: String,
        questionType: { 
            type: String, 
            enum: ['single-choice', 'multiple-choice', 'open-ended'] 
        },
        points: Number,
        options: [{
            _id: ObjectId,
            text: String,
            isCorrect: Boolean
        }]
    }],
    
    createdAt: { type: Date, default: Date.now }
}]
    

Troubleshooting

Debug steps:

// 1. Check if quiz ID exists (Step 1 must be completed first)
console.log('Quiz ID:', quizId);

// 2. Verify temporary IDs are being handled correctly
questions.forEach(q => {
    const isTemp = q._id.startsWith('temp-');
    console.log(`Question ${q._id}: ${isTemp ? 'Will use POST' : 'Will use PUT'}`);
});

// 3. Check network request for each question
fetch(`/api/courses/${courseId}/episodes/${episodeId}/quizzes/${quizId}/questions`, {
    method: 'POST',
    headers: { 
        'Content-Type': 'application/json',
        'x-auth-token': token 
    },
    body: JSON.stringify(questionData)
})
.then(response => {
    console.log('Response status:', response.status);
    return response.json();
})
.then(data => {
    console.log('Server response:', data);
})
.catch(error => {
    console.error('Fetch error:', error);
});
                    

Common fixes:

  • Ensure quiz is created first (Step 1) before adding questions
  • Check that quizId is set before calling saveQuiz()
  • Verify token is present in localStorage

Common causes:

Issue Solution
Option IDs mismatch in submission Regenerate question to get new ObjectIds or ensure frontend sends correct IDs
Question type mislabeled Verify questionType in database matches frontend enum
Multiple correct answers not selected Check "isCorrect" flags on all options - multiple-choice allows multiple true values
Open-ended marked as auto-graded Set questionType: 'open-ended' for manual grading
Radio button name grouping broken Ensure single-choice questions have same name attribute for all options

Grading logic reference:

// From server - POST /api/courses/:courseId/quizzes/:quizId/submit
if (question.questionType === 'single-choice') {
    // Exactly one option must match
    const submittedId = Array.isArray(submitted) ? submitted[0] : submitted;
    isCorrect = correctOptionIds.includes(submittedId);
} 
else if (question.questionType === 'multiple-choice') {
    // All correct options must be selected, no incorrect ones
    const submittedIds = submitted || [];
    isCorrect = submittedIds.length === correctOptionIds.length &&
                submittedIds.every(id => correctOptionIds.includes(id));
}
                    

Check portal rendering:

// Ensure modal is using ReactDOM.createPortal
return ReactDOM.createPortal(
    <div className={`tru-quiz-modal-container ${show ? 'show' : ''}`}>
        {/* modal content */}
    </div>,
    document.body
);

// Backdrop click handler should check target
const handleBackdropClick = useCallback((e) => {
    if (modalContentRef.current && 
        !modalContentRef.current.contains(e.target) &&
        modalRef.current && 
        modalRef.current.contains(e.target)) {
        handleClose();
    }
}, [handleClose]);

// Escape key handler
useEffect(() => {
    if (show) {
        document.addEventListener('keydown', handleEscape);
        document.body.classList.add('tru-modal-open');
        return () => {
            document.removeEventListener('keydown', handleEscape);
            document.body.classList.remove('tru-modal-open');
        };
    }
}, [show, handleEscape]);
                    

Solution: Ensure classNamePrefix matches your CSS:

<Select
    classNamePrefix="tru-select"  // Must match CSS classes
    className="react-select-container"
    menuPlacement="auto"
    menuPosition="fixed"  // Prevents overflow issues
/>
                    

For dropdowns inside modals with overflow:

// Add these props to prevent clipping
<Select
    menuPortalTarget={document.body}  // Render dropdown in portal
    styles={{
        menuPortal: (base) => ({ ...base, zIndex: 9999 })
    }}
/>
                    

Check calculateProgress function:

const calculateProgress = () => {
    if (currentStep === steps.INFO) return 33.33;
    if (currentStep === steps.QUESTIONS_LIST || 
        currentStep === steps.ADD_EDIT_QUESTION_FORM) return 66.66;
    if (currentStep === steps.SETTINGS) return 100;
    return 0;
};

// In JSX - using data attribute instead of inline style
<div 
    className="progress-bar tru-progress-bar" 
    data-progress={progress}
></div>

// CSS should handle the width
.tru-progress-bar {
    width: attr(data-progress %);
}
                    
React Features:
  • Multi-step wizard with progress indicator and step navigation
  • Dynamic form fields based on question type selection
  • Proper radio button grouping for single-choice questions
  • Checkbox toggles for multiple-choice options
  • Advanced settings accordion for additional configurations
  • Temporary ID generation (temp-${Date.now()}) for new questions
  • Batch saving of questions before final quiz submission
  • Portal rendering with backdrop click handling
Note: Questions with temporary IDs (starting with "temp-") are created via POST requests, while existing questions use PUT requests during final save.

Certificate System

Automated PDF certificate generation for students who complete courses. Certificates are dynamically generated with student details, course information, and school branding.

Certificate Example
Figure 7: Automatically generated certificate with dynamic content

How It Works

1

Course Completion Tracking

Student progress is tracked automatically when they:

  • Complete lessons (video/reading)
  • Pass quizzes (score ≥ passing grade)
  • System updates enrollment status to "completed" at 100%
2

Certificate Eligibility

Certificate becomes available when:

  • Course enrollment status = "completed"
  • Course has courseLogo set by instructor
  • includesCertificate: true in course settings
3

PDF Generation

When student clicks "Download Certificate":

  • System loads blank template PDF
  • Adds dynamic text (name, course, school, year)
  • Embeds course logo
  • Uses custom font for school name
  • Returns downloadable PDF file

Instructor Setup

Required Configuration
  • Upload Course Logo (square format)
  • Set School Name in course settings
  • Enable certificate in course builder
Course Logo Upload
Logo Requirements
Course Logo Specifications
{
  "format": "PNG or JPG",
  "dimensions": "Square (1:1 ratio)",
  "size": "500x500 pixels (recommended)",
  "maxSize": "5MB",
  "backgroundColor": "Transparent or white",
  "storage": "uploads/logos/ directory"
}

Logo appears in certificate at 172x172 pixels.

API Endpoint

GET /api/certificate/:courseId/download
// Headers: Requires authentication
{
  "x-auth-token": "Bearer <student_token>"
}

// Response: PDF file with headers:
Content-Type: application/pdf
Content-Disposition: attachment; filename="certificate-{course-slug}.pdf"

// Flow:
1. Checks if student has completed the course
2. Validates course has logo and certificate enabled
3. Loads template from: backend/templates/certificates/template-blank.pdf
4. Adds dynamic content
5. Returns PDF file for download

// Frontend Usage (React Component)
import React, { useState } from 'react';

const CertificateDownload = ({ courseId, courseSlug, isCompleted }) => {
  const [downloading, setDownloading] = useState(false);

  const downloadCertificate = async () => {
    if (!isCompleted) return;
    setDownloading(true);
    
    try {
      const token = localStorage.getItem('lmsToken');
      const response = await fetch(`/api/certificate/${courseId}/download`, {
        headers: { 'x-auth-token': token }
      });
      
      if (response.ok) {
        const blob = await response.blob();
        const url = window.URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `certificate-${courseSlug || courseId}.pdf`;
        document.body.appendChild(a);
        a.click();
        window.URL.revokeObjectURL(url);
        document.body.removeChild(a);
      }
    } catch (err) {
      console.error('Download failed:', err);
    } finally {
      setDownloading(false);
    }
  };

  if (!isCompleted) return null;

  return (
    <button 
      onClick={downloadCertificate} 
      disabled={downloading}
      className="tru-btn btn-gradient"
    >
      {downloading ? 'Generating...' : 'Download Certificate'}
    </button>
  );
};

export default CertificateDownload;

Server Implementation

server.js - Certificate Generation
// In server.js - Certificate Download Route
app.get('/api/certificate/:courseId/download', auth, async (req, res) => {
    try {
        const { courseId } = req.params;
        const userId = req.user.id;

        // 1. Check if student has completed the course
        const user = await User.findById(userId);
        const enrollment = user.enrolledCourses.find(e => e.course.equals(courseId));

        if (!enrollment || enrollment.status !== 'completed') {
            return res.status(403).send('Access Denied: You have not completed this course.');
        }

        // 2. Check if course has certificate enabled
        const course = await Course.findById(courseId);
        if (!course || !course.courseLogo) {
            return res.status(404).send('Certificate cannot be generated because a course logo has not been set.');
        }

        // 3. Prepare dynamic content
        const studentName = `${user.firstName} ${user.lastName}`;
        const courseName = course.title;
        const schoolName = course.school;
        const completionYear = new Date().getFullYear().toString();

        // 4. Load required files
        const templatePath = path.join(__dirname, 'templates', 'certificates', 'template-blank.pdf');
        const logoPath = path.join(__dirname, '..', 'frontend', 'static', course.courseLogo);
        const fontPath = path.join(__dirname, 'fonts', 'CormorantUnicase-Light.ttf');

        const pdfTemplateBytes = await fs.promises.readFile(templatePath);
        const logoImageBytes = await fs.promises.readFile(logoPath);
        const fontBytes = await fs.promises.readFile(fontPath);

        // 5. Create PDF document
        const pdfDoc = await PDFDocument.load(pdfTemplateBytes);
        
        // Register custom font
        pdfDoc.registerFontkit(fontkit);
        const schoolFont = await pdfDoc.embedFont(fontBytes);
        const otherFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
        const otherBoldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold);

        const firstPage = pdfDoc.getPages()[0];
        const { width, height } = firstPage.getSize();
        const logoImage = await pdfDoc.embedPng(logoImageBytes);

        // 6. Draw content onto PDF
        // Logo
        firstPage.drawImage(logoImage, { 
            x: 309.87, 
            y: 406.85, 
            width: 172, 
            height: 172 
        });

        // School Name (custom font)
        firstPage.drawText(schoolName || '', {
            x: 210.87,
            y: 380.42,
            font: schoolFont,
            size: 25,
            color: rgb(1, 1, 1), // White
        });

        // Student Name (centered)
        const studentNameWidth = otherBoldFont.widthOfTextAtSize(studentName, 15);
        firstPage.drawText(studentName, {
            x: (width - studentNameWidth) / 2,
            y: 285.08,
            font: otherBoldFont,
            size: 15,
            color: rgb(0.30, 0.30, 0.30), // Dark gray
        });

        // Course Name (centered)
        const courseNameWidth = otherBoldFont.widthOfTextAtSize(courseName, 15);
        firstPage.drawText(courseName, {
            x: (width - courseNameWidth) / 2,
            y: 235.92,
            font: otherBoldFont,
            size: 15,
            color: rgb(0.30, 0.30, 0.30),
        });

        // Completion Year
        firstPage.drawText(completionYear, {
            x: 677.56,
            y: 58.42,
            font: otherFont,
            size: 12,
            color: rgb(1, 1, 1),
        });

        // 7. Send PDF
        const pdfBytes = await pdfDoc.save();
        res.setHeader('Content-Type', 'application/pdf');
        res.setHeader('Content-Disposition', `attachment; filename="certificate-${course.slug}.pdf"`);
        res.send(Buffer.from(pdfBytes));

    } catch (error) {
        console.error('Error generating certificate:', error);
        res.status(500).send('Could not generate certificate.');
    }
});

Database Schema Requirements

Course Model Fields for Certificates
// In models/Course.js
const CourseSchema = new mongoose.Schema({
    // ... other fields ...
    
    // Required for certificates
    courseLogo: {
        type: String,
        required: false  // But required for certificate generation
    },
    
    school: {
        type: String,
        default: ''
    },
    
    includesCertificate: {
        type: Boolean,
        default: false
    },
    
    // Optional: For future template system
    certificateTemplate: {
        type: String,
        default: 'template-blank'
    },
    
    certificateOrientation: {
        type: String,
        enum: ['landscape', 'portrait'],
        default: 'landscape'
    }
});

File Structure

Certificate System Files
backend/
├── templates/
│   └── certificates/
│       └── template-blank.pdf        # Base certificate template
├── fonts/
│   └── CormorantUnicase-Light.ttf    # Custom font for school name
├── server.js                         # Certificate generation route
└── models/
    └── Course.js                     # Course schema with certificate fields

client/
├── public/
│   └── uploads/
│       ├── avatars/                   # User profile pictures
│       ├── covers/                     # Course cover images
│       ├── logos/                       # Course logos for certificates
│       ├── pdfs/                          # Exercise files
│       ├── resources/                  # Additional course resources
│       ├── thumbnails/                 # Course thumbnails
│       └── videos/                       # Local video uploads
│
└── src/
    └── pages/
        └── student/
            └── StudentEnrolledCourses.jsx # Contains certificate download logic

Student Experience

Progress Tracking
  • Real-time progress percentage
  • Visual progress bar on course cards
  • Lesson completion indicators
  • Quiz pass/fail status
Course Progress
Certificate Download
  • Button appears at 100% completion
  • PDF downloads automatically
  • File named: certificate-{course-slug}.pdf
  • Professional layout with branding
Certificate Button

Progress Calculation

Automatic Progress Tracking
// In server.js - When lesson is completed
app.post('/api/courses/:courseId/lessons/:lessonId/complete', auth, async (req, res) => {
    try {
        const { courseId, lessonId } = req.params;
        const userId = req.user.id;

        const course = await Course.findById(courseId).populate({
            path: 'episodes',
            populate: { path: 'lessons quizzes' }
        });

        const user = await User.findById(userId);
        const enrollment = user.enrolledCourses.find(e => e.course.equals(courseId));

        if (!enrollment.completedLessons.includes(lessonId)) {
            enrollment.completedLessons.push(lessonId);
        }

        // Calculate total items in course
        const totalLessons = course.episodes.reduce((sum, episode) => sum + episode.lessons.length, 0);
        const totalQuizzes = course.episodes.reduce((sum, episode) => sum + episode.quizzes.length, 0);
        const totalItems = totalLessons + totalQuizzes;

        // Calculate completed items
        const completedItems = enrollment.completedLessons.length + enrollment.completedQuizzes.length;
        const progress = totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0;
        
        enrollment.progress = progress;

        // Mark as completed at 100%
        if (progress === 100) {
            enrollment.status = 'completed';
        }

        await user.save();

        res.json({
            success: true,
            progress: enrollment.progress,
            status: enrollment.status
        });

    } catch (error) {
        console.error('Error marking lesson complete:', error);
        res.status(500).json({ success: false, message: 'Server error' });
    }
});

Troubleshooting

Common issues and solutions:

Error Message Cause Solution
"Course logo has not been set" Instructor didn't upload course logo Edit course → Upload square logo
"You have not completed this course" Progress < 100% Complete all lessons and quizzes
"Could not generate certificate" Missing template or font files Verify files exist in backend/templates/ and backend/fonts/
PDF downloads but is blank Template PDF corruption Replace template-blank.pdf with original

Debug steps:

// Check user's enrollment status
const user = await User.findById(userId);
const enrollment = user.enrolledCourses.find(e => e.course.equals(courseId));

console.log('Enrollment status:', {
  progress: enrollment.progress,
  status: enrollment.status,
  completedLessons: enrollment.completedLessons.length,
  completedQuizzes: enrollment.completedQuizzes.length
});

// Check course structure
const course = await Course.findById(courseId);
console.log('Course structure:', {
  totalLessons: course.episodes.reduce((sum, ep) => sum + ep.lessons.length, 0),
  totalQuizzes: course.episodes.reduce((sum, ep) => sum + ep.quizzes.length, 0),
  episodes: course.episodes.length
});

// Manual fix if needed (admin only)
enrollment.progress = 100;
enrollment.status = 'completed';
await user.save();
                    

Check conditional rendering in StudentEnrolledCourses.jsx:

// Ensure the button only renders when course is completed
{course.status === 'completed' && (
  <button
    onClick={() => handleDownloadCertificate(course._id, course.slug)}
    className="tru-btn btn-gradient"
  >
    Download Certificate
  </button>
)}

// Debug by logging course status
console.log('Course:', course.title, 'Status:', course.status);

// Check if API returns correct status
const fetchEnrolledCourses = async () => {
  const response = await fetch('/api/student/enrolled-courses', {
    headers: { 'x-auth-token': token }
  });
  const data = await response.json();
  console.log('Enrolled courses:', data.courses);
};
                        

Common issues:

  • course.status is not exactly "completed" (check for uppercase/lowercase)
  • Progress is 100% but status not updated - trigger manual refresh
  • Token missing or expired - check localStorage

Customization Options

For Developers: The certificate system can be customized by modifying:
  • Template PDF: Replace template-blank.pdf with custom design
  • Font: Change CormorantUnicase-Light.ttf to any TrueType font
  • Layout: Adjust coordinates in firstPage.drawText() calls
  • Content: Add more dynamic fields (date, instructor name, etc.)
Customizing Certificate Layout
// To add more fields, modify the PDF drawing section:

// Example: Add completion date
const completionDate = new Date().toLocaleDateString('en-US', {
  year: 'numeric',
  month: 'long',
  day: 'numeric'
});

firstPage.drawText(`Completed on: ${completionDate}`, {
  x: 300,
  y: 200,
  font: otherFont,
  size: 10,
  color: rgb(0.5, 0.5, 0.5)
});

// Example: Add instructor name
if (course.instructor) {
  firstPage.drawText(`Instructor: ${course.instructor.name}`, {
    x: 300,
    y: 180,
    font: otherFont,
    size: 10,
    color: rgb(0.5, 0.5, 0.5)
  });
}

// Example: Change logo position/size
firstPage.drawImage(logoImage, {
  x: 400,    // Horizontal position
  y: 500,    // Vertical position  
  width: 100, // Smaller logo
  height: 100
});

API Reference

Complete REST API documentation for the Course Builder system. All endpoints require JWT authentication with appropriate user roles.

Authentication Required: All endpoints require x-auth-token header with JWT token. Instructor endpoints require role: 'instructor'.

Course Management

Method Endpoint Description Role Request Body
POST /api/instructor/courses Create new course (Step 1) Instructor multipart/form-data
GET /api/courses/edit/:courseId Get course for editing Instructor -
PUT /api/courses/:courseId Update course details Instructor multipart/form-data
GET /api/courses/preview/:courseId Preview course (instructor view) Instructor -

Topic (Episode) Management

Method Endpoint Description Request Body Response
POST /api/courses/:courseId/episodes Add new topic {title, summary} Updated course with new episode
PUT /api/courses/:courseId/episodes/:episodeId Update topic {title, summary} Updated course
DELETE /api/courses/:courseId/episodes/:episodeId Delete topic - Updated course without episode

Lesson Management

Method Endpoint Description Content-Type File Uploads
POST /api/courses/:courseId/episodes/:episodeId/lessons Add new lesson multipart/form-data Video file, Exercise files
PUT /api/courses/:courseId/episodes/:episodeId/lessons/:lessonId Update lesson multipart/form-data Video file, Exercise files
DELETE /api/courses/:courseId/episodes/:episodeId/lessons/:lessonId Delete lesson - -
DELETE /api/courses/:courseId/episodes/:episodeId/lessons/:lessonId/files Remove exercise file application/json -
POST /api/courses/:courseId/lessons/:lessonId/complete Mark lesson as complete (student) - -

Quiz Management

Method Endpoint Description Request Body
POST /api/courses/:courseId/episodes/:episodeId/quizzes Create new quiz {title, summary, settings}
PUT /api/courses/:courseId/episodes/:episodeId/quizzes/:quizId Update quiz settings {title, summary, settings}
DELETE /api/courses/:courseId/episodes/:episodeId/quizzes/:quizId Delete quiz -
POST /api/courses/:courseId/episodes/:episodeId/quizzes/:quizId/questions Add question to quiz Question object
PUT /api/courses/:courseId/episodes/:episodeId/quizzes/:quizId/questions/:questionId Update question Updated question object
DELETE /api/courses/:courseId/episodes/:episodeId/quizzes/:quizId/questions/:questionId Delete question -
POST /api/courses/:courseId/quizzes/:quizId/submit Submit quiz (student) {answers: {}}

Certificate & Progress

Method Endpoint Description Role Response
GET /api/certificate/:courseId/download Download certificate PDF Student PDF file
GET /api/student/my-courses Get enrolled courses with progress Student Courses with progress data
GET /api/student/my-quiz-attempts Get quiz attempt history Student Quiz results array
GET /api/quiz-results/:resultId Get detailed quiz result Student/Instructor Quiz result with answers

Request & Response Examples

1

Create Course (Step 1)

Request Example
// JavaScript
const formData = new FormData();
formData.append('title', 'Web Development Bootcamp');
formData.append('slug', 'web-dev-bootcamp');
formData.append('description', 'Learn full-stack web development...');
formData.append('thumbnail', thumbnailFile);

fetch('/api/instructor/courses', {
  method: 'POST',
  headers: { 'x-auth-token': token },
  body: formData
});

// cURL
curl -X POST http://localhost:5000/api/instructor/courses \
  -H "x-auth-token: YOUR_JWT_TOKEN" \
  -F "title=Web Development Bootcamp" \
  -F "slug=web-dev-bootcamp" \
  -F "thumbnail=@thumbnail.jpg"
2

Update Course (Step 2)

Request Example
const formData = new FormData();
formData.append('title', 'Updated Course Title');
formData.append('price', '49.99');
formData.append('originalPrice', '99.99');
formData.append('status', 'Published');
formData.append('difficultyLevel', 'Intermediate');
formData.append('school', 'School of Code & Development');
formData.append('categories', JSON.stringify(['Web Development', 'JavaScript']));
formData.append('isMasterclass', 'true');

// Optional file updates
if (newThumbnail) formData.append('thumbnail', newThumbnail);
if (newLogo) formData.append('courseLogo', newLogo);

fetch(`/api/courses/${courseId}`, {
  method: 'PUT',
  headers: { 'x-auth-token': token },
  body: formData
});
3

Add Lesson with Files

Request Example
const formData = new FormData();
formData.append('lessonType', 'video');
formData.append('title', 'Introduction to React Components');
formData.append('summary', 'Learn the basics of React components');
formData.append('videoSource', 'vimeo');
formData.append('videoUrl', 'https://vimeo.com/123456789');
formData.append('duration', '1 hr 15 min 30 sec');

// Add exercise files
for (let file of exerciseFiles) {
  formData.append('exerciseFiles', file);
}

fetch(`/api/courses/${courseId}/episodes/${episodeId}/lessons`, {
  method: 'POST',
  headers: { 'x-auth-token': token },
  body: formData
});

Error Handling

HTTP Code Error Message Cause Solution
401 "No token, authorization denied" Missing or invalid JWT token Login again to get new token
403 "Access denied" Wrong user role or not course owner Check user role and course ownership
404 "Course not found" Invalid course ID Verify course exists and ID is correct
400 "Course thumbnail is required" Missing required field Provide all required form fields
500 "Server error" Backend error Check server logs and database connection

Response Format

Standard Response Structure
// Success Response
{
  "success": true,
  "message": "Operation completed successfully",
  "data": { ... },      // Optional: Response data
  "course": { ... }     // For course operations: updated course
}

// Error Response  
{
  "success": false,
  "message": "Error description",
  "error": "Detailed error message"  // Optional
}

// Paginated Response
{
  "success": true,
  "data": [...],
  "pagination": {
    "currentPage": 1,
    "totalPages": 5,
    "totalItems": 50,
    "itemsPerPage": 10
  }
}

Authentication

JWT Token Usage
// Get token after login
const response = await fetch('/api/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ username: 'email@example.com', password: 'password' })
});

const data = await response.json();
const token = data.token;  // JWT token
localStorage.setItem('lmsToken', token);

// Use token in requests
fetch('/api/instructor/courses', {
  headers: { 
    'x-auth-token': token,
    'Content-Type': 'application/json'
  }
});

// Token payload structure (decoded)
{
  "user": {
    "id": "user_id",
    "role": "instructor",  // or "student"
    "name": "John Doe",
    "avatar": "uploads/avatars/filename.jpg"
  },
  "iat": 1704038400,
  "exp": 1704056400  // 5-hour expiration
}

File Upload Endpoints

Important: File upload endpoints use multipart/form-data and have specific Multer configurations for different file types.
Endpoint Field Name File Type Max Size Storage Location
/api/instructor/courses thumbnail Image 5MB uploads/thumbnails/
/api/courses/:courseId courseLogo Image 5MB uploads/logos/
/api/courses/../lessons videoFile Video 500MB uploads/videos/
/api/courses/../lessons exerciseFiles PDF/DOC/ZIP 50MB each uploads/pdfs/
/api/user/avatar avatar Image 2MB uploads/avatars/

Customization Guide

Learn how to customize and extend the Course Builder system to fit your specific needs. This guide covers SCSS theming, JavaScript modifications, and backend extensions.

Customization Structure
Figure 8: Modular architecture for easy customization

Architecture Overview

Frontend Structure
  • HTML: Semantic, modular templates
  • SCSS: 80+ component files
  • JavaScript: Modular ES6+ with imports
  • Bootstrap 5: Customized with SCSS
Backend Structure
  • Express.js: REST API architecture
  • Mongoose: MongoDB ODM with schemas
  • Modular Routes: Organized by feature
  • Middleware: Authentication & validation
File Upload System
  • Multer: 7 specialized upload handlers
  • Organized Storage: Type-based directories
  • Validation: File type, size, and security
  • Extensible: Easy to add cloud storage

SCSS Theming System

Note: The system uses modular SCSS files in src/assets/scss/ for easy customization. All styles are compiled through main.scss.
SCSS Directory Structure
src/
└── assets/
    ├── scss/
    │   ├── _variables.scss          # Global variables (colors, fonts, spacing)
    │   ├── main.scss                 # Main compilation file
    │   └── components/                # Modular component files
    │       ├── _course.scss           # Course-related styles
    │       ├── _course-builder.scss   # Course builder specific
    │       ├── _lesson.scss           # Lesson player styles
    │       ├── _quiz.scss             # Quiz interface styles
    │       ├── _certificate.scss      # Certificate-related styles
    │       ├── _buttons.scss          # Button variations
    │       ├── _forms.scss             # Form styling
    │       ├── _modals.scss            # Modal windows
    │       └── ... 70+ more files
    └── css/
        └── main.css                    # Compiled CSS (imported in App.jsx)

Customizing Colors

Color Variables (scss/_variables.scss)
// Primary Colors
$primary: #6C5CE7;          // Main brand color
$secondary: #A29BFE;        // Secondary accent
$success: #00B894;          // Success states
$warning: #FDCB6E;          // Warnings
$danger: #D63031;           // Errors/danger
$info: #0984E3;             // Information

// Course Builder Specific
$course-builder-bg: #F8F9FA;
$course-builder-border: #E9ECEF;
$lesson-video-bg: #1A1A1A;
$quiz-option-correct: #D4EDDA;
$quiz-option-incorrect: #F8D7DA;

// Gradient Definitions
$gradient-primary: linear-gradient(135deg, $primary 0%, $secondary 100%);
$gradient-success: linear-gradient(135deg, $success 0%, #00CEA9 100%);

// To customize:
// 1. Change these variables
// 2. Run: npm run scss:compile
// 3. Refresh browser

Custom Component Styling

Example: Custom Lesson Player
// Create: scss/components/_custom-lesson.scss
.lesson-player-custom {
  // Override default styles
  .video-wrapper {
    border-radius: 20px;
    box-shadow: 0 10px 30px rgba(0,0,0,0.2);
    overflow: hidden;
    
    video {
      width: 100%;
      height: auto;
    }
  }
  
  .lesson-controls {
    background: rgba(0,0,0,0.8);
    padding: 15px;
    border-radius: 0 0 20px 20px;
    
    .btn-control {
      background: $primary;
      color: white;
      border: none;
      padding: 10px 20px;
      border-radius: 10px;
      
      &:hover {
        background: darken($primary, 10%);
        transform: translateY(-2px);
      }
    }
  }
  
  .exercise-files {
    background: white;
    border-radius: 15px;
    padding: 20px;
    margin-top: 20px;
    
    .file-item {
      display: flex;
      align-items: center;
      padding: 10px;
      border-bottom: 1px solid $course-builder-border;
      
      &:last-child {
        border-bottom: none;
      }
      
      .file-icon {
        color: $primary;
        margin-right: 10px;
      }
      
      .file-download {
        margin-left: auto;
        background: $gradient-primary;
        color: white;
        padding: 5px 15px;
        border-radius: 5px;
      }
    }
  }
}

// Then import in main.scss:
// @import 'components/custom-lesson';

React Component Customization

Extending Course Builder Components

Adding Custom Validation to CreateCourse.jsx
// src/utils/courseValidation.js
export const validateCourse = (formData, thumbnail) => {
  const errors = [];

  if (!formData.title || formData.title.trim().length < 10) {
    errors.push('Course title must be at least 10 characters');
  }
  if (formData.title && formData.title.length > 100) {
    errors.push('Course title must be less than 100 characters');
  }

  const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
  if (!slugRegex.test(formData.slug)) {
    errors.push('Slug can only contain lowercase letters, numbers, and hyphens');
  }

  if (!thumbnail) {
    errors.push('Course thumbnail is required');
  }

  return errors;
};

// Usage in CreateCourse.jsx
import { validateCourse } from '../../utils/courseValidation';

const handleSubmit = async (e) => {
  e.preventDefault();
  
  const validationErrors = validateCourse(formData, thumbnail);
  if (validationErrors.length > 0) {
    setError(validationErrors.join('\n'));
    return;
  }
  
  // Proceed with submission
};

Creating Custom Hooks

Custom Hook for Course Progress
// src/hooks/useCourseProgress.js
import { useState, useEffect } from 'react';
import { useAuth } from '../context/AuthContext';

export const useCourseProgress = (courseId) => {
  const { user } = useAuth();
  const [progress, setProgress] = useState(0);
  const [completedItems, setCompletedItems] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    if (!user || !courseId) return;

    const fetchProgress = async () => {
      try {
        const token = localStorage.getItem('lmsToken');
        const response = await fetch(`/api/courses/${courseId}/progress`, {
          headers: { 'x-auth-token': token }
        });
        const data = await response.json();
        
        if (data.success) {
          setProgress(data.progress);
          setCompletedItems(data.completedItems);
        }
      } catch (error) {
        console.error('Error fetching progress:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchProgress();
  }, [courseId, user]);

  const markLessonComplete = async (lessonId) => {
    try {
      const token = localStorage.getItem('lmsToken');
      const response = await fetch(`/api/courses/${courseId}/lessons/${lessonId}/complete`, {
        method: 'POST',
        headers: { 'x-auth-token': token }
      });
      
      const data = await response.json();
      if (data.success) {
        setProgress(data.progress);
        setCompletedItems(prev => [...prev, { type: 'lesson', id: lessonId }]);
      }
    } catch (error) {
      console.error('Error marking lesson complete:', error);
    }
  };

  return { progress, completedItems, loading, markLessonComplete };
};

// Usage in LessonPage.jsx
const LessonPage = () => {
  const { courseId, lessonId } = useParams();
  const { progress, markLessonComplete } = useCourseProgress(courseId);
  
  const handleComplete = async () => {
    await markLessonComplete(lessonId);
  };
  
  return (
    <div>
      <div className="progress-bar" style={{ width: `${progress}%` }} />
      <button onClick={handleComplete}>Mark Complete</button>
    </div>
  );
};

Adding Custom Lesson Types

Extending Lesson System in React
// 1. Extend LessonModal.jsx to support new types
const lessonTypeOptions = [
  { value: 'video', label: 'Video Lesson' },
  { value: 'reading', label: 'Reading / Article Lesson' },
  { value: 'interactive', label: 'Interactive Exercise' }, // New type
  { value: 'audio', label: 'Audio Lesson' } // New type
];

// 2. Add conditional rendering for new types
{lessonType === 'interactive' && (
  <div className="interactive-fields-wrapper">
    <div className="course-field mb--20">
      <label>Interactive Content URL</label>
      <input
        type="text"
        value={interactiveUrl}
        onChange={(e) => setInteractiveUrl(e.target.value)}
        placeholder="https://codepen.io/embed/..."
      />
      <small>Embed URL for CodePen, JSFiddle, etc.</small>
    </div>
  </div>
)}

{lessonType === 'audio' && (
  <div className="audio-fields-wrapper">
    <div className="course-field mb--20">
      <label>Audio File</label>
      <input
        type="file"
        accept="audio/*"
        onChange={(e) => setAudioFile(e.target.files[0])}
      />
    </div>
  </div>
)}

// 3. Update form submission
if (lessonType === 'interactive') {
  formData.append('interactiveUrl', interactiveUrl);
} else if (lessonType === 'audio') {
  formData.append('audioFile', audioFile);
}

Custom Context for Global Features

Creating a Gamification Context
// src/context/GamificationContext.jsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import { useAuth } from './AuthContext';

const GamificationContext = createContext();

export const useGamification = () => {
  const context = useContext(GamificationContext);
  if (!context) {
    throw new Error('useGamification must be used within GamificationProvider');
  }
  return context;
};

export const GamificationProvider = ({ children }) => {
  const { user } = useAuth();
  const [points, setPoints] = useState(0);
  const [badges, setBadges] = useState([]);
  const [level, setLevel] = useState(1);

  useEffect(() => {
    if (user) {
      fetchGamificationData();
    }
  }, [user]);

  const fetchGamificationData = async () => {
    try {
      const token = localStorage.getItem('lmsToken');
      const response = await fetch('/api/user/gamification', {
        headers: { 'x-auth-token': token }
      });
      const data = await response.json();
      if (data.success) {
        setPoints(data.points);
        setBadges(data.badges);
        setLevel(data.level);
      }
    } catch (error) {
      console.error('Error fetching gamification data:', error);
    }
  };

  const awardPoints = async (action, value) => {
    try {
      const token = localStorage.getItem('lmsToken');
      const response = await fetch('/api/gamification/award', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-auth-token': token
        },
        body: JSON.stringify({ action, value })
      });
      
      const data = await response.json();
      if (data.success) {
        setPoints(prev => prev + data.pointsAwarded);
        if (data.newBadges) {
          setBadges(prev => [...prev, ...data.newBadges]);
        }
        if (data.levelUp) {
          setLevel(data.newLevel);
        }
      }
    } catch (error) {
      console.error('Error awarding points:', error);
    }
  };

  return (
    <GamificationContext.Provider value={{ points, badges, level, awardPoints }}>
      {children}
    </GamificationContext.Provider>
  );
};

// Wrap in App.jsx
<GamificationProvider>
  <ComponentInitializer>
    {/* routes */}
  </ComponentInitializer>
</GamificationProvider>

// Usage in any component
const LessonPage = () => {
  const { awardPoints } = useGamification();
  
  const handleComplete = async () => {
    await markLessonComplete(lessonId);
    awardPoints('lesson_complete', 10);
  };
};

Backend Customization

Extending Course Model

Adding Custom Fields to Course
// In models/Course.js - Add custom fields
const CourseSchema = new mongoose.Schema({
  // ... existing fields ...
  
  // Custom metadata fields
  customMetadata: {
    seoTitle: String,
    seoDescription: String,
    metaKeywords: [String],
    featuredUntil: Date,
    priority: { type: Number, default: 0 },
    
    // Social media
    socialImage: String,
    twitterCard: String,
    openGraphTags: Map,
    
    // Custom fields for your use case
    learningOutcomes: [{
      outcome: String,
      measurable: Boolean,
      assessmentMethod: String
    }],
    
    prerequisites: [{
      type: { type: String, enum: ['course', 'skill', 'knowledge'] },
      description: String,
      required: Boolean
    }],
    
    // Certification details
    certification: {
      provider: String,
      hours: Number,
      accreditation: String,
      renewalPeriod: Number // in months
    },
    
    // Custom scheduling
    schedule: {
      startDate: Date,
      endDate: Date,
      liveSessions: [{
        title: String,
        date: Date,
        duration: Number, // minutes
        instructor: String,
        recordingUrl: String
      }],
      officeHours: [{
        day: String,
        time: String,
        duration: Number
      }]
    }
  },
  
  // Analytics tracking
  analytics: {
    views: { type: Number, default: 0 },
    enrollments: { type: Number, default: 0 },
    completionRate: Number,
    avgRating: Number,
    revenue: Number,
    lastUpdated: { type: Date, default: Date.now }
  },
  
  // Custom flags
  flags: {
    featured: { type: Boolean, default: false },
    newRelease: { type: Boolean, default: true },
    staffPick: { type: Boolean, default: false },
    trending: { type: Boolean, default: false },
    evergreen: { type: Boolean, default: false } // Always relevant
  }
}, {
  timestamps: true // Adds createdAt and updatedAt automatically
});

Creating Custom API Endpoints

Adding Custom Course Analytics
// Create: backend/routes/customAnalytics.js
const express = require('express');
const router = express.Router();
const Course = require('../models/Course');
const User = require('../models/User');
const auth = require('../authMiddleware');

// Custom analytics endpoint
router.get('/api/courses/:courseId/analytics/advanced', auth, async (req, res) => {
  try {
    const courseId = req.params.courseId;
    const course = await Course.findById(courseId);
    
    // Verify instructor ownership
    if (course.instructor.toString() !== req.user.id) {
      return res.status(403).json({ success: false, message: 'Access denied' });
    }

    // Get enrolled students
    const enrolledStudents = await User.find({
      'enrolledCourses.course': courseId
    }).select('firstName lastName email enrolledCourses');

    // Calculate advanced metrics
    const analytics = {
      basic: {
        totalEnrolled: enrolledStudents.length,
        totalCompleted: enrolledStudents.filter(s => 
          s.enrolledCourses.find(ec => 
            ec.course.equals(courseId) && ec.status === 'completed'
          )
        ).length,
        completionRate: enrolledStudents.length > 0 ? 
          (enrolledStudents.filter(s => 
            s.enrolledCourses.find(ec => 
              ec.course.equals(courseId) && ec.status === 'completed'
            )
          ).length / enrolledStudents.length * 100).toFixed(1) : 0
      },
      
      engagement: {
        avgProgress: enrolledStudents.reduce((sum, student) => {
          const enrollment = student.enrolledCourses.find(ec => ec.course.equals(courseId));
          return sum + (enrollment?.progress || 0);
        }, 0) / (enrolledStudents.length || 1),
        
        activeStudents: enrolledStudents.filter(s => {
          const enrollment = s.enrolledCourses.find(ec => ec.course.equals(courseId));
          return enrollment && enrollment.progress > 0 && enrollment.progress < 100;
        }).length,
        
        inactiveStudents: enrolledStudents.filter(s => {
          const enrollment = s.enrolledCourses.find(ec => ec.course.equals(courseId));
          return enrollment && enrollment.progress === 0;
        }).length
      },
      
      timeline: {
        enrollmentsByDay: await getEnrollmentsByDay(courseId),
        completionsByDay: await getCompletionsByDay(courseId),
        last30DaysActivity: await get30DayActivity(courseId)
      },
      
      content: {
        mostViewedLessons: await getMostViewedLessons(courseId),
        difficultQuizzes: await getQuizPerformance(courseId),
        popularResources: await getResourceDownloads(courseId)
      }
    };

    res.json({ success: true, analytics });
    
  } catch (error) {
    console.error('Analytics error:', error);
    res.status(500).json({ success: false, message: 'Server error' });
  }
});

// Helper functions
async function getEnrollmentsByDay(courseId) {
  const users = await User.find({ 'enrolledCourses.course': courseId });
  const enrollmentsByDay = {};
  
  users.forEach(user => {
    const enrollment = user.enrolledCourses.find(ec => ec.course.equals(courseId));
    if (enrollment && enrollment.enrolledAt) {
      const date = enrollment.enrolledAt.toISOString().split('T')[0];
      enrollmentsByDay[date] = (enrollmentsByDay[date] || 0) + 1;
    }
  });
  
  return enrollmentsByDay;
}

// Export router
module.exports = router;

// In server.js, add:
// app.use('/', require('./routes/customAnalytics'));

Custom Middleware

Creating Rate Limiting Middleware
// Create: backend/middleware/rateLimit.js
const rateLimit = require('express-rate-limit');

// General API rate limiter
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per windowMs
  message: {
    success: false,
    message: 'Too many requests from this IP, please try again after 15 minutes'
  },
  standardHeaders: true,
  legacyHeaders: false
});

// Course creation rate limiter (more restrictive)
const courseCreationLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 5, // Limit to 5 course creations per hour
  message: {
    success: false,
    message: 'Too many course creations. Please try again later.'
  },
  skip: (req) => {
    // Skip rate limiting for admins
    return req.user && req.user.role === 'admin';
  }
});

// Quiz submission rate limiter
const quizSubmissionLimiter = rateLimit({
  windowMs: 5 * 60 * 1000, // 5 minutes
  max: 10, // Limit to 10 quiz submissions per 5 minutes
  message: {
    success: false,
    message: 'Too many quiz submissions. Please wait a few minutes.'
  }
});

// File upload rate limiter
const fileUploadLimiter = rateLimit({
  windowMs: 10 * 60 * 1000, // 10 minutes
  max: 20, // Limit to 20 file uploads per 10 minutes
  message: {
    success: false,
    message: 'Too many file uploads. Please try again later.'
  }
});

module.exports = {
  apiLimiter,
  courseCreationLimiter,
  quizSubmissionLimiter,
  fileUploadLimiter
};

// Usage in server.js:
const { apiLimiter, courseCreationLimiter } = require('./middleware/rateLimit');

// Apply to specific routes
app.post('/api/instructor/courses', auth, courseCreationLimiter, thumbnailUpload.single('thumbnail'), async (req, res) => {
  // ... existing code
});

app.post('/api/courses/:courseId/quizzes/:quizId/submit', auth, quizSubmissionLimiter, async (req, res) => {
  // ... existing code
});

File Upload Customization

Adding Cloud Storage (AWS S3)

Replacing Local Storage with S3
// Install required packages
// npm install @aws-sdk/client-s3 multer-s3

// Create: backend/config/s3Storage.js
const { S3Client } = require('@aws-sdk/client-s3');
const multerS3 = require('multer-s3');
const path = require('path');

// Configure S3 client
const s3Client = new S3Client({
  region: process.env.AWS_REGION,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
  }
});

// Create S3 storage engine for Multer
const s3Storage = multerS3({
  s3: s3Client,
  bucket: process.env.AWS_BUCKET_NAME,
  acl: 'public-read',
  contentType: multerS3.AUTO_CONTENT_TYPE,
  metadata: function (req, file, cb) {
    cb(null, { fieldName: file.fieldname });
  },
  key: function (req, file, cb) {
    const folder = getFolderByFieldname(file.fieldname);
    const filename = `${Date.now()}-${path.basename(file.originalname, path.extname(file.originalname))}${path.extname(file.originalname)}`;
    cb(null, `${folder}/${filename}`);
  }
});

// Helper function to determine S3 folder
function getFolderByFieldname(fieldname) {
  const folderMap = {
    'thumbnail': 'thumbnails',
    'courseLogo': 'logos',
    'videoFile': 'videos',
    'exerciseFiles': 'exercise-files',
    'avatar': 'avatars',
    'coverPhoto': 'covers'
  };
  return folderMap[fieldname] || 'uploads';
}

// Update Multer configuration in server.js
// Replace local storage paths with S3:

// Update environment variables
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_BUCKET_NAME=your-bucket-name
USE_S3_STORAGE=true  // Flag to toggle between local and S3

// Utility function to get file URL
function getFileUrl(key) {
  if (process.env.USE_S3_STORAGE === 'true') {
    return `https://${process.env.AWS_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`;
  } else {
    return `/uploads/${key}`; // Local path (client/public/uploads)
  }
}

// Update course save logic
if (req.file) {
  if (process.env.USE_S3_STORAGE === 'true') {
    course.thumbnail = getFileUrl(req.file.key); // S3 key
  } else {
    course.thumbnail = `uploads/thumbnails/${req.file.filename}`; // Local path
  }
}

Plugin System

Creating Custom Plugins

Building a Gamification Plugin
// Directory: backend/plugins/gamification/
// gamificationPlugin.js
class GamificationPlugin {
  constructor() {
    this.name = 'Gamification Plugin';
    this.version = '1.0.0';
    this.events = [];
  }

  initialize(app) {
    console.log(`${this.name} v${this.version} initialized`);
    
    // Add custom routes
    app.get('/api/plugin/gamification/leaderboard', this.getLeaderboard.bind(this));
    app.post('/api/plugin/gamification/award-points', this.awardPoints.bind(this));
    
    // Listen to system events
    this.registerEvents();
  }

  registerEvents() {
    // Listen for lesson completion
    courseEvents.on('lesson.completed', (data) => {
      this.awardLessonCompletionPoints(data.userId, data.courseId);
    });

    // Listen for quiz completion
    courseEvents.on('quiz.completed', (data) => {
      this.awardQuizCompletionPoints(data.userId, data.score, data.courseId);
    });

    // Listen for course completion
    courseEvents.on('course.completed', (data) => {
      this.awardCourseCompletionBadge(data.userId, data.courseId);
    });
  }

  async getLeaderboard(req, res) {
    try {
      // Calculate leaderboard based on points
      const leaderboard = await this.calculateLeaderboard();
      res.json({ success: true, leaderboard });
    } catch (error) {
      res.status(500).json({ success: false, message: error.message });
    }
  }

  async awardPoints(req, res) {
    try {
      const { userId, points, reason } = req.body;
      
      // Award points to user
      await User.findByIdAndUpdate(userId, {
        $inc: { 'gamification.points': points }
      });

      // Log the award
      await this.logAward(userId, points, reason);

      res.json({ success: true, message: 'Points awarded' });
    } catch (error) {
      res.status(500).json({ success: false, message: error.message });
    }
  }

  async awardLessonCompletionPoints(userId, courseId) {
    const points = 10; // Base points for lesson completion
    await this.awardPointsToUser(userId, points, 'lesson_completion');
    
    // Streak bonus
    const streak = await this.checkStreak(userId);
    if (streak >= 7) {
      await this.awardPointsToUser(userId, 50, 'weekly_streak');
    }
  }

  async awardQuizCompletionPoints(userId, score, courseId) {
    let points = Math.floor(score / 10); // 1 point per 10% score
    
    // Perfect score bonus
    if (score === 100) {
      points += 20;
      await this.awardBadge(userId, 'perfectionist');
    }
    
    await this.awardPointsToUser(userId, points, 'quiz_completion');
  }

  async awardCourseCompletionBadge(userId, courseId) {
    await this.awardBadge(userId, 'course_completion');
    
    // Speed completion bonus (completed in < 7 days)
    const completionTime = await this.getCompletionTime(userId, courseId);
    if (completionTime < 7 * 24 * 60 * 60 * 1000) { // 7 days in milliseconds
      await this.awardBadge(userId, 'fast_learner');
      await this.awardPointsToUser(userId, 100, 'fast_course_completion');
    }
  }

  // Helper methods
  async awardPointsToUser(userId, points, reason) {
    await User.findByIdAndUpdate(userId, {
      $inc: { 'gamification.points': points },
      $push: {
        'gamification.history': {
          points,
          reason,
          date: new Date()
        }
      }
    });
  }

  async awardBadge(userId, badgeType) {
    await User.findByIdAndUpdate(userId, {
      $addToSet: { 'gamification.badges': badgeType }
    });
  }

  async calculateLeaderboard() {
    const users = await User.find({ 'gamification.points': { $gt: 0 } })
      .select('firstName lastName gamification.points gamification.badges avatar')
      .sort({ 'gamification.points': -1 })
      .limit(50);

    return users.map((user, index) => ({
      rank: index + 1,
      name: `${user.firstName} ${user.lastName}`,
      points: user.gamification?.points || 0,
      badges: user.gamification?.badges || [],
      avatar: user.avatar
    }));
  }
}

// Extend User model
// In models/User.js, add:
gamification: {
  points: { type: Number, default: 0 },
  badges: [String],
  level: { type: Number, default: 1 },
  history: [{
    points: Number,
    reason: String,
    date: { type: Date, default: Date.now }
  }],
  achievements: [{
    name: String,
    description: String,
    earnedAt: Date,
    icon: String
  }]
}

// Initialize plugin in server.js
const GamificationPlugin = require('./plugins/gamification/gamificationPlugin');
const gamificationPlugin = new GamificationPlugin();
gamificationPlugin.initialize(app);

Performance Optimization

Caching Strategy

Implementing Redis Caching
// Install: npm install redis
// Create: backend/middleware/cache.js
const redis = require('redis');
const { promisify } = require('util');

// Create Redis client
const redisClient = redis.createClient({
  host: process.env.REDIS_HOST || 'localhost',
  port: process.env.REDIS_PORT || 6379,
  password: process.env.REDIS_PASSWORD
});

// Promisify Redis methods
const getAsync = promisify(redisClient.get).bind(redisClient);
const setexAsync = promisify(redisClient.setex).bind(redisClient);
const delAsync = promisify(redisClient.del).bind(redisClient);

// Cache middleware
function cacheMiddleware(duration = 300) { // 5 minutes default
  return async (req, res, next) => {
    // Only cache GET requests
    if (req.method !== 'GET') {
      return next();
    }

    // Generate cache key from request
    const cacheKey = `cache:${req.originalUrl}`;

    try {
      // Try to get from cache
      const cachedData = await getAsync(cacheKey);
      
      if (cachedData) {
        console.log('Cache hit:', cacheKey);
        return res.json(JSON.parse(cachedData));
      }

      // Cache miss - override res.json to cache response
      const originalJson = res.json;
      res.json = function(data) {
        // Cache successful responses
        if (res.statusCode === 200 && data.success !== false) {
          setexAsync(cacheKey, duration, JSON.stringify(data))
            .catch(err => console.error('Redis set error:', err));
        }
        
        // Call original function
        return originalJson.call(this, data);
      };

      next();
    } catch (error) {
      console.error('Cache error:', error);
      next(); // Continue without caching on error
    }
  };
}

// Cache invalidation on updates
function invalidateCache(patterns) {
  return async (req, res, next) => {
    // Store original send function
    const originalSend = res.send;
    
    res.send = async function(data) {
      // Invalidate cache after successful update
      if (res.statusCode >= 200 && res.statusCode < 300) {
        try {
          // Get all keys matching patterns
          const keys = patterns.map(pattern => `cache:${pattern}`);
          await delAsync(keys);
          console.log('Cache invalidated for patterns:', patterns);
        } catch (error) {
          console.error('Cache invalidation error:', error);
        }
      }
      
      return originalSend.call(this, data);
    };
    
    next();
  };
}

// Usage in routes
app.get('/api/courses', cacheMiddleware(600), async (req, res) => {
  // This will be cached for 10 minutes
  const courses = await Course.find({ status: 'Published' });
  res.json({ success: true, courses });
});

app.put('/api/courses/:id', auth, invalidateCache(['/api/courses/*', '/api/courses']), async (req, res) => {
  // This will invalidate course cache when course is updated
  // ... update logic
});

Database Optimization

Mongoose Query Optimization
// 1. Indexing for Performance
// In models/Course.js
CourseSchema.index({ status: 1, createdAt: -1 }); // For listing published courses
CourseSchema.index({ instructor: 1, status: 1 }); // For instructor dashboard
CourseSchema.index({ slug: 1 }, { unique: true }); // Already exists
CourseSchema.index({ 'episodes.lessons._id': 1 }); // For lesson queries
CourseSchema.index({ 'episodes.quizzes._id': 1 }); // For quiz queries

// In models/User.js
UserSchema.index({ email: 1 }, { unique: true });
UserSchema.index({ role: 1 });
UserSchema.index({ 'enrolledCourses.course': 1 }); // For student enrollment queries
UserSchema.index({ 'wishlist': 1 }); // For wishlist queries

// In models/Review.js
ReviewSchema.index({ course: 1, createdAt: -1 });
ReviewSchema.index({ user: 1, course: 1 }, { unique: true }); // One review per user per course

// 2. Efficient Population Strategies
// Bad: N+1 query problem
const courses = await Course.find();
for (let course of courses) {
  const instructor = await User.findById(course.instructor); // Additional query per course!
}

// Good: Single query with population
const courses = await Course.find()
  .populate('instructor', 'firstName lastName avatar') // Only needed fields
  .select('title thumbnail price instructor') // Only needed fields
  .limit(20) // Pagination
  .lean(); // Returns plain JavaScript objects (faster)

// 3. Aggregation for Complex Queries
async function getInstructorStats(instructorId) {
  const stats = await Course.aggregate([
    { $match: { instructor: mongoose.Types.ObjectId(instructorId) } },
    {
      $lookup: {
        from: 'users',
        localField: '_id',
        foreignField: 'enrolledCourses.course',
        as: 'enrolledStudents'
      }
    },
    {
      $group: {
        _id: '$instructor',
        totalCourses: { $sum: 1 },
        totalStudents: { $sum: { $size: '$enrolledStudents' } },
        averagePrice: { $avg: '$price' },
        totalRevenue: { $sum: '$price' }
      }
    }
  ]);
  return stats[0];
}

// 4. Connection Pooling
// In your db connection file (backend/config/db.js)
mongoose.connect(process.env.MONGO_URI, {
  maxPoolSize: 10, // Maximum number of connections in pool
  minPoolSize: 5,  // Minimum number of connections
  socketTimeoutMS: 45000, // Close sockets after 45 seconds of inactivity
  family: 4 // Use IPv4, skip trying IPv6
});

// 5. Query Optimization Tips
// Use projection to select only needed fields
const course = await Course.findById(courseId)
  .select('title slug thumbnail price episodes')
  .populate({
    path: 'episodes.lessons',
    select: 'title duration lessonType' // Only needed lesson fields
  });

// Use $facet for multiple aggregations in one query
const analytics = await Course.aggregate([
  { $match: { instructor: instructorId } },
  {
    $facet: {
      priceStats: [
        { $group: {
          _id: null,
          avgPrice: { $avg: '$price' },
          maxPrice: { $max: '$price' },
          minPrice: { $min: '$price' }
        }}
      ],
      statusCounts: [
        { $group: {
          _id: '$status',
          count: { $sum: 1 }
        }}
      ],
      recentCourses: [
        { $sort: { createdAt: -1 } },
        { $limit: 5 }
      ]
    }
  }
]);

// 6. Bulk Operations
// Instead of multiple updates, use bulkWrite
await Course.bulkWrite([
  {
    updateOne: {
      filter: { _id: courseId1 },
      update: { $set: { status: 'Published' } }
    }
  },
  {
    updateOne: {
      filter: { _id: courseId2 },
      update: { $set: { status: 'Published' } }
    }
  }
  // ... more operations
]);

// 7. Text Search Optimization
CourseSchema.index({ 
  title: 'text', 
  description: 'text',
  'episodes.title': 'text',
  'episodes.summary': 'text'
});

const searchResults = await Course.find(
  { $text: { $search: searchQuery } },
  { score: { $meta: 'textScore' } }
).sort({ score: { $meta: 'textScore' } });

Frontend Performance

JavaScript Performance Optimization
// 1. Lazy Loading Images
// In your HTML
<img data-src="image.jpg" class="lazy" alt="...">

// JavaScript
document.addEventListener("DOMContentLoaded", function() {
  const lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

  if ("IntersectionObserver" in window) {
    const lazyImageObserver = new IntersectionObserver(function(entries) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          const lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.classList.remove("lazy");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });

    lazyImages.forEach(function(lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  }
});

// 2. Code Splitting for Course Builder
// Create separate bundle for course builder
// In your build configuration or using dynamic imports

// Dynamic import for heavy modules
async function loadCourseBuilder() {
  if (!window.courseBuilderLoaded) {
    const module = await import('./modules/course-builder-heavy.js');
    window.courseBuilderLoaded = true;
    return module;
  }
}

// 3. Debouncing Search Input
let searchTimeout;
document.getElementById('course-search').addEventListener('input', function(e) {
  clearTimeout(searchTimeout);
  searchTimeout = setTimeout(() => {
    searchCourses(e.target.value);
  }, 300); // Wait 300ms after user stops typing
});

// 4. Virtual Scrolling for Long Lists
class VirtualScroll {
  constructor(container, items, itemHeight, renderItem) {
    this.container = container;
    this.items = items;
    this.itemHeight = itemHeight;
    this.renderItem = renderItem;
    this.visibleItems = Math.ceil(container.clientHeight / itemHeight);
    
    this.container.style.overflowY = 'auto';
    this.container.style.height = `${this.visibleItems * itemHeight}px`;
    
    this.render();
    this.container.addEventListener('scroll', this.handleScroll.bind(this));
  }
  
  render() {
    const scrollTop = this.container.scrollTop;
    const startIndex = Math.floor(scrollTop / this.itemHeight);
    const endIndex = startIndex + this.visibleItems;
    
    const visibleItems = this.items.slice(startIndex, endIndex);
    this.container.innerHTML = '';
    
    visibleItems.forEach((item, index) => {
      const itemElement = this.renderItem(item);
      itemElement.style.position = 'absolute';
      itemElement.style.top = `${(startIndex + index) * this.itemHeight}px`;
      this.container.appendChild(itemElement);
    });
  }
  
  handleScroll() {
    this.render();
  }
}

// 5. Memoization for Expensive Functions
const memoize = (fn) => {
  const cache = {};
  return (...args) => {
    const key = JSON.stringify(args);
    if (cache[key]) {
      return cache[key];
    }
    const result = fn(...args);
    cache[key] = result;
    return result;
  };
};

// Expensive calculation function
const calculateCourseStats = memoize((courseId) => {
  // Complex calculations here
  return {
    // ... calculated stats
  };
});

// 6. Web Workers for Heavy Processing
// main.js
const worker = new Worker('worker.js');

worker.postMessage({
  type: 'processCourses',
  data: largeCourseArray
});

worker.onmessage = function(e) {
  const processedData = e.data;
  // Update UI with processed data
};

// worker.js
self.onmessage = function(e) {
  if (e.data.type === 'processCourses') {
    const processed = processCourseData(e.data.data);
    self.postMessage(processed);
  }
};

function processCourseData(courses) {
  // Heavy processing that would block main thread
  return courses.map(course => ({
    ...course,
    calculatedStats: complexCalculation(course)
  }));
}

// 7. Bundle Optimization
// Use tools like:
// - Webpack for bundling
// - Babel for transpiling
// - Terser for minification
// - PurgeCSS for removing unused CSS

// Example webpack.config.js for production
module.exports = {
  mode: 'production',
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin()],
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
};

Asset Optimization

Optimizing Images and Assets
// 1. Image Compression Strategy
// Recommended image sizes and formats:
/*
Course Thumbnails: 700x430px, WebP (fallback JPG), ≤ 100KB
User Avatars: 200x200px, WebP, ≤ 50KB
Course Logos: 500x500px, PNG (transparent), ≤ 150KB
Lesson Images: Max width 1200px, WebP, ≤ 200KB
Background Images: 1920x1080px, JPG (progressive), ≤ 300KB
*/

// 2. Using Sharp for Server-Side Image Processing
// npm install sharp
const sharp = require('sharp');

async function optimizeImage(inputPath, outputPath, options) {
  await sharp(inputPath)
    .resize(options.width, options.height, {
      fit: 'cover',
      position: 'center'
    })
    .toFormat(options.format, {
      quality: options.quality || 80,
      progressive: true
    })
    .toFile(outputPath);
}

// Usage in upload handler
const optimizedPath = `uploads/thumbnails/optimized-${Date.now()}.webp`;
await optimizeImage(req.file.path, optimizedPath, {
  width: 700,
  height: 430,
  format: 'webp',
  quality: 85
});

// 3. Responsive Images with srcset
<img 
  src="thumbnail-700.webp"
  srcset="thumbnail-350.webp 350w,
          thumbnail-700.webp 700w,
          thumbnail-1400.webp 1400w"
  sizes="(max-width: 768px) 100vw, 700px"
  alt="Course thumbnail"
>

// 4. Font Optimization
/* Use font-display: swap for custom fonts */
@font-face {
  font-family: 'CustomFont';
  src: url('font.woff2') format('woff2'),
       url('font.woff') format('woff');
  font-display: swap; /* Shows fallback font first */
  font-weight: 400;
  font-style: normal;
}

// 5. CSS Optimization
// SCSS to optimized CSS workflow
/*
1. Development: SCSS with source maps
2. Build: Compress, autoprefix, remove unused
3. Production: Minified, critical CSS inlined
*/

// package.json scripts
"scripts": {
  "scss:dev": "sass scss/main.scss static/assets/css/main.css --source-map",
  "scss:build": "sass scss/main.scss static/assets/css/main.css --style=compressed",
  "css:optimize": "postcss static/assets/css/main.css --use autoprefixer cssnano -o static/assets/css/main.min.css",
  "purge:css": "purgecss --css static/assets/css/main.css --content '**/*.html' '**/*.js' --output static/assets/css/"
}

// 6. JavaScript Bundle Analysis
// Install: npm install webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-analysis.html',
      openAnalyzer: false
    })
  ]
};

// 7. Service Worker for Offline Support
// sw.js
const CACHE_NAME = 'trulern-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/assets/css/main.min.css',
  '/assets/js/main.min.js',
  '/assets/images/logo.png'
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        if (response) {
          return response;
        }
        return fetch(event.request);
      })
  );
});

// 8. GZIP Compression
// In server.js
const compression = require('compression');
app.use(compression({
  level: 6, // Compression level (0-9)
  threshold: 0, // Compress all responses
  filter: (req, res) => {
    if (req.headers['x-no-compression']) {
      return false;
    }
    return compression.filter(req, res);
  }
}));

Security Customization

Enhanced Security Measures

Advanced Security Configuration
// 1. Helmet.js for Security Headers
const helmet = require('helmet');
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      imgSrc: ["'self'", "data:", "https:"],
      scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
      connectSrc: ["'self'", "https://api.razorpay.com"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));

// 2. Rate Limiting by User ID
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');

const userRateLimiter = rateLimit({
  store: new RedisStore({
    prefix: 'rl_user:'
  }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each user to 100 requests per windowMs
  keyGenerator: (req) => {
    return req.user ? req.user.id : req.ip; // User-based or IP-based
  },
  message: 'Too many requests from this user, please try again later.'
});

// 3. Input Sanitization
const sanitizeHtml = require('sanitize-html');
const validator = require('validator');

function sanitizeUserInput(input) {
  // Remove HTML tags
  const sanitized = sanitizeHtml(input, {
    allowedTags: [], // No HTML allowed
    allowedAttributes: {}
  });
  
  // Validate email
  if (input.includes('@')) {
    return validator.normalizeEmail(sanitized);
  }
  
  // Validate URL
  if (input.startsWith('http')) {
    return validator.normalizeUrl(sanitized);
  }
  
  return sanitized;
}

// 4. File Upload Security
const multer = require('multer');
const path = require('path');

const upload = multer({
  storage: multer.memoryStorage(), // Process in memory first
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB max
    files: 1
  },
  fileFilter: (req, file, cb) => {
    // Check file type
    const allowedTypes = /jpeg|jpg|png|gif|webp/;
    const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
    const mimetype = allowedTypes.test(file.mimetype);
    
    if (extname && mimetype) {
      // Additional security check
      checkFileForMalware(file.buffer)
        .then(isSafe => {
          if (isSafe) {
            cb(null, true);
          } else {
            cb(new Error('File security check failed'));
          }
        })
        .catch(err => cb(err));
    } else {
      cb(new Error('Only image files are allowed'));
    }
  }
});

// 5. JWT Security Enhancement
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// Generate secure JWT secret
const generateJWTSecret = () => {
  return crypto.randomBytes(64).toString('hex');
};

// Enhanced JWT options
const jwtOptions = {
  expiresIn: '5h',
  issuer: 'trulern-lms',
  audience: 'trulern-users',
  jwtid: crypto.randomBytes(16).toString('hex') // Unique JWT ID
};

// Token blacklisting
const tokenBlacklist = new Set();

const blacklistToken = (token) => {
  const decoded = jwt.decode(token);
  if (decoded && decoded.exp) {
    // Store until token expires
    const ttl = decoded.exp - Math.floor(Date.now() / 1000);
    if (ttl > 0) {
      tokenBlacklist.add(token);
      setTimeout(() => tokenBlacklist.delete(token), ttl * 1000);
    }
  }
};

// 6. Password Security
const bcrypt = require('bcryptjs');
const zxcvbn = require('zxcvbn');

function validatePasswordStrength(password) {
  const result = zxcvbn(password);
  
  if (result.score < 3) {
    return {
      valid: false,
      score: result.score,
      warning: result.feedback.warning,
      suggestions: result.feedback.suggestions
    };
  }
  
  return {
    valid: true,
    score: result.score
  };
}

async function hashPassword(password) {
  const saltRounds = 12; // Increased from default 10
  return await bcrypt.hash(password, saltRounds);
}

// 7. Session Management
const session = require('express-session');
const MongoStore = require('connect-mongo');

app.use(session({
  secret: process.env.SESSION_SECRET || generateJWTSecret(),
  resave: false,
  saveUninitialized: false,
  store: MongoStore.create({
    mongoUrl: process.env.MONGO_URI,
    ttl: 14 * 24 * 60 * 60, // 14 days
    autoRemove: 'native'
  }),
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    sameSite: 'strict',
    maxAge: 14 * 24 * 60 * 60 * 1000 // 14 days
  }
}));

// 8. SQL/NoSQL Injection Prevention
// Mongoose already prevents NoSQL injection, but add additional validation
const mongoSanitize = require('express-mongo-sanitize');

app.use(mongoSanitize({
  replaceWith: '_',
  onSanitize: ({ req, key }) => {
    console.warn(`Attempted NoSQL injection: ${key}`, req.ip);
  }
}));

// 9. API Key Security
const apiKeys = new Map();

function generateAPIKey() {
  return crypto.randomBytes(32).toString('hex');
}

function validateAPIKey(req, res, next) {
  const apiKey = req.headers['x-api-key'];
  
  if (!apiKey) {
    return res.status(401).json({ error: 'API key required' });
  }
  
  if (!apiKeys.has(apiKey)) {
    return res.status(403).json({ error: 'Invalid API key' });
  }
  
  const keyData = apiKeys.get(apiKey);
  
  // Check rate limits
  if (keyData.requests > keyData.limit) {
    return res.status(429).json({ error: 'Rate limit exceeded' });
  }
  
  // Update request count
  keyData.requests++;
  apiKeys.set(apiKey, keyData);
  
  next();
}

// 10. Logging and Monitoring
const winston = require('winston');
const { Logtail } = require('@logtail/node');
const { LogtailTransport } = require('@logtail/winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
    new LogtailTransport(new Logtail(process.env.LOGTAIL_SOURCE_TOKEN))
  ]
});

// Security event logging
function logSecurityEvent(event, details) {
  logger.warn('SECURITY_EVENT', {
    event,
    ...details,
    timestamp: new Date().toISOString(),
    ip: req.ip,
    userAgent: req.headers['user-agent']
  });
}

API Overview

TruLern LMS provides a comprehensive REST API that follows RESTful principles. All API endpoints return JSON responses and use HTTP status codes appropriately.

Base URL: All API endpoints are prefixed with /api/. The base URL depends on your deployment: https://yourdomain.com/api/ or http://localhost:5000/api/ for development.

API Design Principles

Authentication

JWT-based authentication with role-based access control

Validation

Input validation with meaningful error messages

Response Format

Standard Response Structure
// Success Response
{
  "success": true,
  "message": "Operation completed successfully",
  "data": { /* Response data */ },
  "timestamp": "2024-01-15T10:30:00.000Z"
}

// Error Response
{
  "success": false,
  "message": "Descriptive error message",
  "error": "Detailed error information",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "code": "ERROR_CODE" // Optional error code
}

// Paginated Response
{
  "success": true,
  "data": [ /* Array of items */ ],
  "pagination": {
    "total": 150,
    "page": 1,
    "limit": 10,
    "pages": 15,
    "hasNext": true,
    "hasPrev": false
  }
}

HTTP Status Codes

Code Description Usage
200 OK Successful GET, PUT, DELETE requests
201 Created Resource created successfully (POST)
204 No Content Successful request with no body to return
400 Bad Request Invalid request parameters or body
401 Unauthorized Missing or invalid authentication
403 Forbidden Authenticated but insufficient permissions
404 Not Found Resource doesn't exist
409 Conflict Resource conflict (duplicate, constraint violation)
422 Unprocessable Entity Validation errors in request body
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Server-side error

API Versioning

Versioning Strategy
// Current version: v1
// All endpoints are implicitly v1
// Example: /api/courses = /api/v1/courses

// Future versioning options:
// 1. URL Path Versioning
// /api/v2/courses
// /api/v2/users

// 2. Header Versioning
// Accept: application/vnd.trulern.v2+json

// 3. Query Parameter Versioning
// /api/courses?version=2

// Recommended: URL Path Versioning for clarity
app.use('/api/v1', require('./routes/v1'));
app.use('/api/v2', require('./routes/v2'));

Rate Limiting

Rate Limit Headers
// Response headers include rate limit information
X-RateLimit-Limit: 100        // Requests per window
X-RateLimit-Remaining: 95     // Remaining requests
X-RateLimit-Reset: 1704038400 // Unix timestamp when limit resets
Retry-After: 900              // Seconds to wait (if limited)

// Different limits for different endpoints
const rateLimits = {
  public: {
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100                  // 100 requests per 15 minutes
  },
  authenticated: {
    windowMs: 15 * 60 * 1000,
    max: 500                  // 500 requests per 15 minutes
  },
  instructor: {
    windowMs: 15 * 60 * 1000,
    max: 1000                 // 1000 requests per 15 minutes
  },
  payment: {
    windowMs: 60 * 1000,      // 1 minute
    max: 10                   // 10 payment requests per minute
  }
};

Error Codes Reference

Error Code HTTP Status Description Possible Causes
AUTH_001 401 Invalid credentials Wrong email/password, expired token
AUTH_002 403 Insufficient permissions Student trying to access instructor endpoints
VALIDATION_001 400 Validation failed Missing required fields, invalid format
COURSE_001 404 Course not found Invalid course ID, deleted course
PAYMENT_001 400 Payment failed Insufficient funds, declined card
DB_001 500 Database error Connection issue, query timeout
FILE_001 400 File upload failed Invalid file type, size too large
RATE_LIMIT_001 429 Rate limit exceeded Too many requests in short time

Authentication API

The authentication system uses JWT (JSON Web Tokens) for stateless authentication. Tokens expire after 5 hours and must be included in the x-auth-token header for protected endpoints.

Authentication Flow
Figure 9: JWT authentication flow from login to protected API access

Authentication Flow

1

User Registration

User creates account with email and password. Password is hashed using bcrypt before storage.

2

Login

User provides credentials, server validates and returns JWT token.

3

Protected Requests

Client includes JWT in x-auth-token header for all subsequent requests.

4

Token Validation

Middleware validates token on each request, extracts user data, and checks permissions.

Endpoint Reference

Method Endpoint Description Auth Required
POST /api/register Register as student No
POST /api/register-instructor Register as instructor No
POST /api/login Login and get JWT token No
GET /api/user/profile Get current user profile Yes
PUT /api/user/profile Update user profile Yes
POST /api/forgot-password Request password reset email No
POST /api/reset-password Reset password with token No

Detailed Endpoint Documentation

1

POST /api/register

Register a new student account.

Request Example
{
  "firstName": "John",
  "lastName": "Doe",
  "email": "john@example.com",
  "password": "SecurePass123!"
}

// Response (Success - 201 Created)
{
  "success": true,
  "message": "User registered successfully!",
  "user": {
    "id": "507f1f77bcf86cd799439011",
    "firstName": "John",
    "lastName": "Doe",
    "email": "john@example.com",
    "role": "student",
    "createdAt": "2024-01-15T10:30:00.000Z"
  }
}

// Response (Error - 400 Bad Request)
{
  "success": false,
  "message": "User already exists",
  "error": "Email is already registered"
}
2

POST /api/login

Authenticate user and receive JWT token.

Request Example
{
  "username": "john@example.com",
  "password": "SecurePass123!"
}

// Response (Success - 200 OK)
{
  "success": true,
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": "507f1f77bcf86cd799439011",
    "role": "student",
    "name": "John Doe",
    "avatar": "uploads/avatars/default.jpg"
  }
}

// Decoded JWT Payload
{
  "user": {
    "id": "507f1f77bcf86cd799439011",
    "role": "student",
    "name": "John Doe",
    "avatar": "uploads/avatars/default.jpg"
  },
  "iat": 1704038400,
  "exp": 1704056400
}

// Response (Error - 400 Bad Request)
{
  "success": false,
  "message": "Invalid Credentials"
}
Token Usage: The JWT token must be included in the x-auth-token header for all authenticated requests. The token expires after 5 hours.
3

Password Reset Flow

Two-step process for password recovery.

Step 1: Request Reset
// POST /api/forgot-password
{
  "email": "john@example.com"
}

// Response (Always returns 200 to prevent email enumeration)
{
  "success": true,
  "message": "If a matching account was found, a password reset email has been sent."
}

// Email contains link like:
// http://localhost:5000/reset-password.html?token=reset_token_here
Step 2: Reset Password
// POST /api/reset-password
{
  "token": "reset_token_from_email",
  "newPassword": "NewSecurePass456!",
  "confirmNewPassword": "NewSecurePass456!"
}

// Response (Success - 200 OK)
{
  "success": true,
  "message": "Password has been successfully reset. You can now log in."
}

// Response (Error - 400 Bad Request)
{
  "success": false,
  "message": "Password reset link is invalid or has expired. Please try again."
}

Authentication Middleware

backend/authMiddleware.js
const jwt = require('jsonwebtoken');
const User = require('./models/User');

module.exports = async function(req, res, next) {
    // Get token from header
    const token = req.header('x-auth-token');

    // Check if no token
    if (!token) {
        return res.status(401).json({ 
            success: false, 
            message: 'No token, authorization denied' 
        });
    }

    try {
        // Verify token
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        
        // Check if user still exists
        const user = await User.findById(decoded.user.id).select('-password');
        if (!user) {
            return res.status(401).json({ 
                success: false, 
                message: 'User no longer exists' 
            });
        }

        // Check if user is active (optional)
        if (user.status === 'suspended') {
            return res.status(403).json({ 
                success: false, 
                message: 'Account is suspended' 
            });
        }

        // Add user to request object
        req.user = decoded.user;
        next();
    } catch (err) {
        console.error('Auth middleware error:', err.message);
        
        if (err.name === 'TokenExpiredError') {
            return res.status(401).json({ 
                success: false, 
                message: 'Token has expired. Please log in again.' 
            });
        }
        
        if (err.name === 'JsonWebTokenError') {
            return res.status(401).json({ 
                success: false, 
                message: 'Invalid token. Please log in again.' 
            });
        }
        
        res.status(500).json({ 
            success: false, 
            message: 'Server error during authentication' 
        });
    }
};

Role-Based Access Control

Instructor Middleware
// In server.js or separate middleware file
const isInstructor = (req, res, next) => {
    if (req.user && req.user.role === 'instructor') {
        return next();
    }
    return res.status(403).json({ 
        success: false, 
        message: 'Access denied. Instructor role required.' 
    });
};

// Usage
app.get('/api/instructor/dashboard', auth, isInstructor, async (req, res) => {
    // Only instructors can access this
});

// Student middleware (optional)
const isStudent = (req, res, next) => {
    if (req.user && req.user.role === 'student') {
        return next();
    }
    return res.status(403).json({ 
        success: false, 
        message: 'Access denied. Student role required.' 
    });
};

// Admin middleware
const isAdmin = (req, res, next) => {
    if (req.user && req.user.role === 'admin') {
        return next();
    }
    return res.status(403).json({ 
        success: false, 
        message: 'Access denied. Admin role required.' 
    });
};

// Course ownership middleware
const isCourseOwner = async (req, res, next) => {
    try {
        const course = await Course.findById(req.params.courseId);
        
        if (!course) {
            return res.status(404).json({ 
                success: false, 
                message: 'Course not found' 
            });
        }
        
        if (course.instructor.toString() !== req.user.id) {
            return res.status(403).json({ 
                success: false, 
                message: 'User not authorized to access this course' 
            });
        }
        
        next();
    } catch (error) {
        res.status(500).json({ 
            success: false, 
            message: 'Server error' 
        });
    }
};

React Authentication Implementation

src/context/AuthContext.jsx
import React, { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext();

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [token, setToken] = useState(null);
  const [loading, setLoading] = useState(true);

  // Check for existing session on mount
  useEffect(() => {
    const storedToken = localStorage.getItem('lmsToken');
    const storedUser = localStorage.getItem('lmsUser');

    if (storedToken && storedUser) {
      setToken(storedToken);
      setUser(JSON.parse(storedUser));
    }
    setLoading(false);
  }, []);

  // Login function
  const login = async (credentials) => {
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      });

      const data = await response.json();

      if (data.success) {
        // Store in localStorage
        localStorage.setItem('lmsToken', data.token);
        localStorage.setItem('lmsUser', JSON.stringify(data.user));
        
        // Update state
        setToken(data.token);
        setUser(data.user);
        
        return { success: true, user: data.user };
      } else {
        return { success: false, message: data.message };
      }
    } catch (error) {
      console.error('Login error:', error);
      return { success: false, message: 'Network error' };
    }
  };

  // Register function
  const register = async (userData) => {
    try {
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData)
      });

      return await response.json();
    } catch (error) {
      console.error('Registration error:', error);
      return { success: false, message: 'Network error' };
    }
  };

  // Logout function
  const logout = () => {
    localStorage.removeItem('lmsToken');
    localStorage.removeItem('lmsUser');
    setToken(null);
    setUser(null);
  };

  // Check if user has specific role
  const isInstructor = () => user?.role === 'instructor';
  const isStudent = () => user?.role === 'student';
  const isAdmin = () => user?.role === 'admin';

  // Fetch with auth header
  const fetchWithAuth = async (url, options = {}) => {
    if (!token) {
      throw new Error('No authentication token');
    }

    const response = await fetch(url, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        'x-auth-token': token,
        ...options.headers
      }
    });

    // Handle token expiration
    if (response.status === 401) {
      const data = await response.json().catch(() => ({}));
      if (data.message?.includes('expired')) {
        logout();
        throw new Error('Session expired. Please log in again.');
      }
    }

    return response;
  };

  // Password reset functions
  const requestPasswordReset = async (email) => {
    try {
      const response = await fetch('/api/forgot-password', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email })
      });
      return await response.json();
    } catch (error) {
      console.error('Password reset request error:', error);
      return { success: false, message: 'Network error' };
    }
  };

  const resetPassword = async (token, newPassword, confirmPassword) => {
    try {
      const response = await fetch('/api/reset-password', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ 
          token, 
          newPassword, 
          confirmNewPassword: confirmPassword 
        })
      });
      return await response.json();
    } catch (error) {
      console.error('Password reset error:', error);
      return { success: false, message: 'Network error' };
    }
  };

  const value = {
    user,
    token,
    loading,
    login,
    register,
    logout,
    isInstructor,
    isStudent,
    isAdmin,
    fetchWithAuth,
    requestPasswordReset,
    resetPassword
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
};
Usage in Components
// src/pages/Login.jsx
import React, { useState } from 'react';
import { useAuth } from '../context/AuthContext';
import { useNavigate } from 'react-router-dom';

const LoginPage = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);
  
  const { login } = useAuth();
  const navigate = useNavigate();

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    const result = await login({ username: email, password });

    if (result.success) {
      // Redirect based on role
      if (result.user.role === 'instructor') {
        navigate('/instructor/dashboard');
      } else if (result.user.role === 'admin') {
        navigate('/admin/dashboard');
      } else {
        navigate('/student/dashboard');
      }
    } else {
      setError(result.message);
    }
    
    setLoading(false);
  };

  return (
    <form onSubmit={handleSubmit}>
      {error && <div className="alert alert-danger">{error}</div>}
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
};

export default LoginPage;
Protecting Routes with PrivateRoute
// src/components/PrivateRoute.jsx
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

const PrivateRoute = ({ children, allowedRoles = [] }) => {
  const { user, loading } = useAuth();

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return <Navigate to="/login" replace />;
  }

  if (allowedRoles.length > 0 && !allowedRoles.includes(user.role)) {
    return <Navigate to="/dashboard" replace />;
  }

  return children;
};

export default PrivateRoute;

// Usage in App.jsx
<Route path="/instructor/dashboard" element={
  <PrivateRoute allowedRoles={['instructor']}>
    <InstructorDashboard />
  </PrivateRoute>
} />

Troubleshooting Authentication

Symptoms: Getting 401 errors after some time of inactivity.

Solution: Implement automatic token refresh or redirect to login.

// In AuthContext.jsx - Add token refresh
import { useCallback, useEffect } from 'react';

const TOKEN_LIFETIME = 5 * 60 * 60 * 1000; // 5 hours
const REFRESH_BUFFER = 5 * 60 * 1000; // 5 minutes before expiry

export const AuthProvider = ({ children }) => {
  // ... existing state

  const refreshToken = useCallback(async () => {
    try {
      const response = await fetch('/api/refresh-token', {
        method: 'POST',
        headers: { 'x-auth-token': token }
      });
      
      if (response.ok) {
        const data = await response.json();
        localStorage.setItem('lmsToken', data.token);
        setToken(data.token);
        localStorage.setItem('tokenTimestamp', Date.now());
      } else {
        // Refresh failed, logout
        logout();
      }
    } catch (error) {
      console.error('Token refresh failed:', error);
      logout();
    }
  }, [token, logout]);

  useEffect(() => {
    if (!token || !user) return;

    const tokenTimestamp = localStorage.getItem('tokenTimestamp');
    if (!tokenTimestamp) return;

    const tokenAge = Date.now() - parseInt(tokenTimestamp);
    
    if (tokenAge > TOKEN_LIFETIME - REFRESH_BUFFER) {
      refreshToken();
    }

    // Set up periodic check
    const interval = setInterval(() => {
      const currentAge = Date.now() - parseInt(localStorage.getItem('tokenTimestamp') || 0);
      if (currentAge > TOKEN_LIFETIME - REFRESH_BUFFER) {
        refreshToken();
      }
    }, 60 * 1000); // Check every minute

    return () => clearInterval(interval);
  }, [token, user, refreshToken]);

  // ... rest of provider
};

Symptoms: Authentication works in development but fails in production with CORS errors.

Solution: Configure CORS properly in server.js.

// In server.js
const cors = require('cors');

const allowedOrigins = [
    'http://localhost:3000',
    'http://localhost:5000',
    'https://yourdomain.com',
    'https://www.yourdomain.com'
];

const corsOptions = {
    origin: function (origin, callback) {
        // Allow requests with no origin (like mobile apps or curl requests)
        if (!origin) return callback(null, true);
        
        if (allowedOrigins.indexOf(origin) === -1) {
            const msg = 'The CORS policy for this site does not allow access from the specified Origin.';
            return callback(new Error(msg), false);
        }
        return callback(null, true);
    },
    credentials: true, // Allow cookies if needed
    exposedHeaders: ['x-auth-token'] // Expose custom headers
};

app.use(cors(corsOptions));

// For preflight requests
app.options('*', cors(corsOptions));

Security Best Practices

Password Security
  • Minimum 8 characters with complexity requirements
  • Bcrypt hashing with cost factor 12
  • Password strength meter using zxcvbn
  • Rate limiting on login attempts
Token Security
  • JWT tokens with 5-hour expiry
  • Tokens stored in localStorage (consider HttpOnly cookies)
  • Token blacklisting on logout (optional)
  • Secure transmission via HTTPS only

Course Management API

The Course Management API provides comprehensive endpoints for creating, reading, updating, and deleting courses. This includes curriculum management, lesson content, quizzes, and student enrollment.

Course API Flow Diagram
Figure 10: Course management API flow from creation to publication

Course Structure Overview

Course Hierarchy
  • Course: Top-level container with metadata
  • Episodes: Topics or modules within course
  • Lessons: Video or reading content
  • Quizzes: Assessments within episodes
  • Assignments: Practical exercises (optional)
Course Status Flow
  • Draft → Initial creation state
  • Pending → Submitted for review
  • Published → Live and available
  • Archived → Hidden from listings

Endpoint Reference

Method Endpoint Description Auth Required
GET /api/courses Get all published courses (with filters) No
GET /api/courses/:id Get single course details No
GET /api/courses/edit/:id Get course for editing Yes (Instructor)
POST /api/instructor/courses Create new course (Step 1) Yes (Instructor)
PUT /api/courses/:courseId Update course details Yes (Instructor/Owner)
DELETE /api/courses/:courseId Delete course Yes (Instructor/Owner)
GET /api/courses/:courseId/enrollment-status Check student enrollment Yes (Student)
GET /api/instructor/my-courses-status Get instructor's courses with stats Yes (Instructor)

Detailed Endpoint Documentation

1

GET /api/courses - Course Listing with Filters

Retrieve published courses with advanced filtering and pagination.

Query Parameters
// Available query parameters:
?search=web+development           // Search in title/description
?school=School+of+Code            // Filter by school
?categories=Web+Development,JavaScript  // Filter by categories (comma-separated)
?price=free                       // Show only free courses
?price=paid                       // Show only paid courses
?minPrice=0&maxPrice=100          // Price range filter
?sortBy=latest                    // Sort options: latest, price_asc, price_desc
?limit=20                         // Limit results per page
?page=2                           // Pagination (requires frontend calculation)

// Example Request
GET /api/courses?search=react&categories=Web+Development&sortBy=latest&limit=10

// Response (Success - 200 OK)
{
  "success": true,
  "courses": [
    {
      "_id": "67a1b2c3d4e5f67890123456",
      "title": "Complete React Developer Course",
      "slug": "complete-react-developer-course",
      "description": "Learn React from scratch...",
      "instructor": {
        "_id": "67a1b2c3d4e5f67890123457",
        "firstName": "John",
        "lastName": "Doe",
        "avatar": "uploads/avatars/john.jpg"
      },
      "thumbnail": "uploads/thumbnails/react-course.jpg",
      "price": 49.99,
      "originalPrice": 99.99,
      "difficultyLevel": "Intermediate",
      "averageRating": 4.5,
      "reviewCount": 128,
      "enrolledCount": 1542,
      "duration": {
        "hours": 25,
        "minutes": 30
      },
      "status": "Published",
      "createdAt": "2024-01-10T10:30:00.000Z"
    }
    // ... more courses
  ],
  "pagination": {
    "totalCourses": 150
  }
}
2

GET /api/courses/:id - Course Details

Get detailed information about a specific course, including curriculum and instructor info.

Request Example
GET /api/courses/67a1b2c3d4e5f67890123456

// Response (Success - 200 OK)
{
  "success": true,
  "course": {
    "_id": "67a1b2c3d4e5f67890123456",
    "title": "Complete React Developer Course",
    "slug": "complete-react-developer-course",
    "description": "Learn React from scratch...",
    "instructor": {
      "_id": "67a1b2c3d4e5f67890123457",
      "firstName": "John",
      "lastName": "Doe",
      "avatar": "uploads/avatars/john.jpg",
      "occupation": "Senior Frontend Developer",
      "bio": "10+ years of experience...",
      "social": {
        "website": "https://johndoe.dev",
        "twitter": "johndoe",
        "linkedin": "johndoe"
      }
    },
    "thumbnail": "uploads/thumbnails/react-course.jpg",
    "courseLogo": "uploads/logos/react-logo.png",
    "price": 49.99,
    "originalPrice": 99.99,
    "previewVideoUrl": "https://vimeo.com/123456789",
    "difficultyLevel": "Intermediate",
    "maxStudents": 0, // 0 = unlimited
    "isPublic": false,
    "isQAEnabled": true,
    "isMasterclass": false,
    "school": "School of Code & Development",
    "categories": ["Web Development", "JavaScript", "React"],
    "language": ["English", "Spanish"],
    "requirements": ["Basic HTML/CSS knowledge", "JavaScript fundamentals"],
    "whatYoullLearn": ["Build React applications", "State management", "React hooks"],
    "targetedAudience": ["Beginner developers", "Frontend developers"],
    "tags": ["react", "frontend", "javascript", "webdev"],
    "duration": {
      "hours": 25,
      "minutes": 30
    },
    "certificateTemplate": "template-1",
    "certificateOrientation": "landscape",
    "includesCertificate": true,
    "averageRating": 4.5,
    "reviewCount": 128,
    "enrolledCount": 1542,
    "status": "Published",
    "episodes": [
      {
        "_id": "67a1b2c3d4e5f67890123458",
        "title": "Introduction to React",
        "summary": "Get started with React basics",
        "lessons": [
          {
            "_id": "67a1b2c3d4e5f67890123459",
            "title": "What is React?",
            "summary": "Introduction to React library",
            "lessonType": "video",
            "videoSource": "vimeo",
            "videoUrl": "https://vimeo.com/123456790",
            "duration": "15 min 30 sec",
            "isPreview": true,
            "exerciseFiles": [
              {
                "filename": "react-intro.pdf",
                "path": "uploads/pdfs/react-intro.pdf",
                "size": 2048000
              }
            ]
          }
          // ... more lessons
        ],
        "quizzes": [
          {
            "_id": "67a1b2c3d4e5f67890123460",
            "title": "React Basics Quiz",
            "summary": "Test your React knowledge",
            "questions": [
              {
                "_id": "67a1b2c3d4e5f67890123461",
                "questionText": "What is JSX?",
                "questionType": "single-choice",
                "points": 10,
                "options": [
                  {
                    "_id": "67a1b2c3d4e5f67890123462",
                    "text": "JavaScript XML",
                    "isCorrect": true
                  },
                  // ... more options
                ]
              }
              // ... more questions
            ],
            "settings": {
              "timeLimit": { "value": 30, "unit": "Minutes" },
              "feedbackMode": "reveal",
              "passingGrade": 70
            }
          }
          // ... more quizzes
        ]
      }
      // ... more episodes
    ],
    "createdAt": "2024-01-10T10:30:00.000Z",
    "updatedAt": "2024-01-15T14:20:00.000Z"
  },
  "hasAccess": false // true if user is enrolled or instructor
}
Note: The hasAccess field indicates whether the authenticated user has access to the course content. This is automatically determined based on enrollment status or instructor ownership.
3

POST /api/instructor/courses - Create Course (Step 1)

Create a new course with basic information. This is the first step in the course creation workflow.

Request Example (multipart/form-data)
// Headers
Content-Type: multipart/form-data
x-auth-token: YOUR_JWT_TOKEN

// Form Data (example using JavaScript FormData)
const formData = new FormData();
formData.append('title', 'Complete React Developer Course');
formData.append('slug', 'complete-react-developer-course');
formData.append('description', 'Learn React from scratch...');
formData.append('price', '49.99');
formData.append('originalPrice', '99.99');
formData.append('difficultyLevel', 'Intermediate');
formData.append('maxStudents', '0');
formData.append('isPublic', 'false');
formData.append('isQAEnabled', 'true');
formData.append('previewVideoUrl', 'https://vimeo.com/123456789');
formData.append('isMasterclass', 'false');
formData.append('thumbnail', thumbnailFile); // Required file

// JavaScript implementation
const response = await fetch('/api/instructor/courses', {
    method: 'POST',
    headers: {
        'x-auth-token': token
    },
    body: formData
});

// Response (Success - 201 Created)
{
  "success": true,
  "message": "Course created successfully!",
  "course": {
    "_id": "67a1b2c3d4e5f67890123456",
    "title": "Complete React Developer Course",
    "slug": "complete-react-developer-course",
    "description": "Learn React from scratch...",
    "instructor": "67a1b2c3d4e5f67890123457",
    "price": 49.99,
    "originalPrice": 99.99,
    "thumbnail": "uploads/thumbnails/1704038400000-react-course.jpg",
    "previewVideoUrl": "https://vimeo.com/123456789",
    "status": "Draft",
    "difficultyLevel": "Intermediate",
    "maxStudents": 0,
    "isPublic": false,
    "isQAEnabled": true,
    "isMasterclass": false,
    "episodes": [],
    "createdAt": "2024-01-15T10:30:00.000Z",
    "updatedAt": "2024-01-15T10:30:00.000Z"
  }
}

// Response (Error - 400 Bad Request)
{
  "success": false,
  "message": "Course thumbnail is required."
}
Important: The course is created in "Draft" status. After this step, the instructor should redirect to the course builder (edit-course.html?courseId=COURSE_ID) to add curriculum content.
4

PUT /api/courses/:courseId - Update Course Details

Update course information including metadata, pricing, and settings. This is used in the course builder (Step 2).

Request Example (multipart/form-data)
// Headers
Content-Type: multipart/form-data
x-auth-token: YOUR_JWT_TOKEN

// Form Data (partial example)
const formData = new FormData();
formData.append('title', 'Updated Course Title');
formData.append('slug', 'updated-course-slug');
formData.append('description', 'Updated description...');
formData.append('price', '59.99');
formData.append('originalPrice', '119.99');
formData.append('status', 'Published');
formData.append('difficultyLevel', 'Advanced');
formData.append('maxStudents', '100');
formData.append('isPublic', 'false');
formData.append('isQAEnabled', 'true');
formData.append('isMasterclass', 'true');
formData.append('school', 'School of Code & Development');
formData.append('categories', JSON.stringify(['Web Development', 'JavaScript', 'React']));
formData.append('language', JSON.stringify(['English']));
formData.append('requirements', 'Basic HTML\\nBasic CSS\\nJavaScript fundamentals');
formData.append('whatYoullLearn', 'React components\\nState management\\nReact hooks');
formData.append('targetedAudience', 'Beginner developers\\nFrontend developers');
formData.append('tags', 'react,frontend,javascript,webdev');
formData.append('durationHours', '30');
formData.append('durationMinutes', '45');
formData.append('certificateTemplate', 'template-1');
formData.append('certificateOrientation', 'landscape');

// Optional file updates
if (newThumbnail) formData.append('thumbnail', newThumbnail);
if (newLogo) formData.append('courseLogo', newLogo);

// JavaScript implementation
const response = await fetch(`/api/courses/${courseId}`, {
    method: 'PUT',
    headers: { 'x-auth-token': token },
    body: formData
});

// Response (Success - 200 OK)
{
  "success": true,
  "message": "Course updated successfully!",
  "course": {
    // Updated course object with all fields
  }
}

Curriculum Management Endpoints

1

Episode (Topic) Management

POST /api/courses/:courseId/episodes - Add Topic
// Request
{
  "title": "Introduction to React",
  "summary": "Learn the basics of React components"
}

// Response
{
  "success": true,
  "message": "Topic added successfully!",
  "course": {
    "episodes": [
      {
        "_id": "episode_id_123",
        "title": "Introduction to React",
        "summary": "Learn the basics...",
        "lessons": [],
        "quizzes": []
      }
    ]
  }
}
PUT /api/courses/:courseId/episodes/:episodeId - Update Topic
// Request
{
  "title": "Advanced React Concepts",
  "summary": "Updated description"
}

// Response
{
  "success": true,
  "message": "Topic updated successfully!",
  "course": { /* updated course object */ }
}
DELETE /api/courses/:courseId/episodes/:episodeId - Delete Topic
// Response
{
  "success": true,
  "message": "Topic deleted successfully!",
  "course": { /* updated course without the episode */ }
}
2

Lesson Management

POST /api/courses/:courseId/episodes/:episodeId/lessons - Add Lesson
// Headers: multipart/form-data
// Form Data:
lessonType: video  // or 'reading'
title: Introduction to Components
summary: Learn about React components
videoSource: vimeo  // or 'youtube', 'local'
videoUrl: https://vimeo.com/123456789  // if videoSource is vimeo/youtube
duration: 1 hr 15 min 30 sec
isPreview: false
// For reading lessons:
articleBody: <h1>Introduction</h1><p>Content...</p>
// For local video upload:
videoFile: [file]  // if videoSource is 'local'
// Exercise files (multiple):
exerciseFiles: [file1, file2]

// Response
{
  "success": true,
  "message": "Lesson added successfully!",
  "course": { /* updated course with new lesson */ }
}
PUT /api/courses/:courseId/episodes/:episodeId/lessons/:lessonId - Update Lesson
// Similar to POST but includes lesson ID
// Can change lesson type (video ↔ reading)

// To remove exercise files:
DELETE /api/courses/:courseId/episodes/:episodeId/lessons/:lessonId/files
// Request Body: { "filePath": "uploads/pdfs/filename.pdf" }
3

Quiz Management

POST /api/courses/:courseId/episodes/:episodeId/quizzes - Create Quiz
// Request
{
  "title": "JavaScript Fundamentals Quiz",
  "summary": "Test your basic JavaScript knowledge",
  "settings": {
    "timeLimit": { "value": 30, "unit": "Minutes" },
    "feedbackMode": "reveal",
    "passingGrade": 70,
    "maxQuestionsAllowed": 15,
    "questionLayout": "single_question",
    "questionOrder": "random"
  }
}

// Response
{
  "success": true,
  "message": "Quiz added successfully!",
  "course": { /* updated course with new quiz */ }
}
POST /api/courses/:courseId/episodes/:episodeId/quizzes/:quizId/questions - Add Question
// Single Choice Question Example
{
  "questionText": "What is the capital of France?",
  "questionType": "single-choice",
  "points": 10,
  "options": [
    { "text": "London", "isCorrect": false },
    { "text": "Berlin", "isCorrect": false },
    { "text": "Paris", "isCorrect": true },
    { "text": "Madrid", "isCorrect": false }
  ]
}

// Open-Ended Question Example
{
  "questionText": "Explain the concept of closures in JavaScript.",
  "questionType": "open-ended",
  "points": 25,
  "options": []  // Empty array for text-based questions
}

Student Enrollment Endpoints

1

GET /api/courses/:courseId/enrollment-status - Check Enrollment

Check if the authenticated student is enrolled in a course.

Request Example
GET /api/courses/67a1b2c3d4e5f67890123456/enrollment-status
Headers: x-auth-token: STUDENT_JWT_TOKEN

// Response (Enrolled)
{
  "success": true,
  "isEnrolled": true
}

// Response (Not Enrolled)
{
  "success": true,
  "isEnrolled": false
}
2

POST /api/courses/:courseId/lessons/:lessonId/complete - Mark Lesson Complete

Mark a lesson as completed for a student and update progress.

Request Example
POST /api/courses/67a1b2c3d4e5f67890123456/lessons/67a1b2c3d4e5f67890123459/complete
Headers: x-auth-token: STUDENT_JWT_TOKEN

// Response
{
  "success": true,
  "message": "Lesson marked as complete.",
  "progress": 25, // Updated progress percentage
  "status": "active" // Course status
}

Progress Calculation Logic

Progress Calculation Algorithm
// From server.js - Progress calculation
const calculateProgress = (enrollment, course) => {
  // Count total items (lessons + quizzes)
  const totalLessons = course.episodes.reduce(
    (sum, episode) => sum + episode.lessons.length, 0
  );
  const totalQuizzes = course.episodes.reduce(
    (sum, episode) => sum + episode.quizzes.length, 0
  );
  const totalItems = totalLessons + totalQuizzes;

  // Count completed items
  const completedItems = enrollment.completedLessons.length + 
                         enrollment.completedQuizzes.length;

  // Calculate percentage
  const progress = totalItems > 0 
    ? Math.round((completedItems / totalItems) * 100) 
    : 0;

  // Update course status
  if (progress === 100) {
    enrollment.status = 'completed';
  } else if (progress > 0) {
    enrollment.status = 'active';
  }

  return {
    progress,
    status: enrollment.status,
    totalItems,
    completedItems
  };
};

Instructor Dashboard Endpoints

1

GET /api/instructor/dashboard - Dashboard Statistics

Get instructor dashboard metrics including earnings, student counts, and course statistics.

Response Example
{
  "success": true,
  "data": {
    "totalCourses": 5,
    "totalStudents": 342,
    "totalReviews": 128,
    "averageRating": 4.5,
    "totalEarnings": 12500.50
  }
}
2

GET /api/instructor/my-courses-status - Instructor's Courses

Get all courses created by the instructor with enrollment statistics.

Response Example
{
  "success": true,
  "courses": [
    {
      "_id": "67a1b2c3d4e5f67890123456",
      "title": "Complete React Developer Course",
      "slug": "complete-react-developer-course",
      "thumbnail": "uploads/thumbnails/react-course.jpg",
      "price": 49.99,
      "status": "Published",
      "createdAt": "2024-01-10T10:30:00.000Z",
      "enrolledCount": 1542, // Added by the API
      "averageRating": 4.5,
      "reviewCount": 128
    }
    // ... more courses
  ]
}

Database Schema Reference

Course Model Structure (models/Course.js)
const CourseSchema = new mongoose.Schema({
  // Basic Information
  title: { type: String, required: true },
  slug: { type: String, required: true, unique: true },
  description: String,
  instructor: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  
  // Media
  thumbnail: String,
  courseLogo: String,
  previewVideoUrl: String,
  
  // Pricing
  price: Number,
  originalPrice: Number,
  
  // Settings
  status: { 
    type: String, 
    enum: ['Draft', 'Pending', 'Published'], 
    default: 'Draft' 
  },
  difficultyLevel: String,
  maxStudents: { type: Number, default: 0 }, // 0 = unlimited
  isPublic: Boolean,
  isQAEnabled: Boolean,
  isMasterclass: Boolean,
  
  // Metadata
  school: String,
  categories: [String],
  language: [String],
  requirements: [String],
  whatYoullLearn: [String],
  targetedAudience: [String],
  tags: [String],
  
  // Duration
  duration: {
    hours: Number,
    minutes: Number
  },
  
  // Certificate
  certificateTemplate: String,
  certificateOrientation: String,
  includesCertificate: Boolean,
  
  // Ratings
  averageRating: Number,
  reviewCount: Number,
  
  // Curriculum
  episodes: [{
    title: String,
    summary: String,
    lessons: [{
      title: String,
      summary: String,
      lessonType: { type: String, enum: ['video', 'reading'] },
      videoSource: String,
      videoUrl: String,
      videoPath: String,
      duration: String,
      articleBody: String,
      isPreview: Boolean,
      exerciseFiles: [{
        filename: String,
        path: String,
        size: Number
      }]
    }],
    quizzes: [{
      title: String,
      summary: String,
      questions: [{
        questionText: String,
        questionType: String,
        points: Number,
        options: [{
          text: String,
          isCorrect: Boolean
        }]
      }],
      settings: {
        timeLimit: { value: Number, unit: String },
        feedbackMode: String,
        passingGrade: Number,
        maxQuestionsAllowed: Number,
        questionLayout: String,
        questionOrder: String
      }
    }]
  }],
  
  // Timestamps
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

Error Handling

Error Code HTTP Status Description Solution
COURSE_001 404 Course not found Check course ID, ensure course exists
COURSE_002 403 Not course owner Only course instructor can modify
COURSE_003 400 Duplicate slug Choose unique course slug
COURSE_004 400 Missing required fields Provide all required course data
EPISODE_001 404 Episode not found Check episode ID
LESSON_001 404 Lesson not found Check lesson ID
QUIZ_001 404 Quiz not found Check quiz ID

Best Practices for Course Management

Performance Optimization
  • Use .select() to fetch only needed fields
  • Implement pagination for course lists
  • Cache frequently accessed course data
  • Use lean() for read-only operations
Security Considerations
  • Always validate user ownership
  • Sanitize HTML content in descriptions
  • Validate file uploads (type, size)
  • Implement rate limiting on updates

Common Use Cases

Complete Course Creation Flow
// 1. Create course (Step 1)
async function createCourse(courseData, thumbnailFile) {
  const formData = new FormData();
  Object.keys(courseData).forEach(key => {
    formData.append(key, courseData[key]);
  });
  formData.append('thumbnail', thumbnailFile);

  const response = await fetch('/api/instructor/courses', {
    method: 'POST',
    headers: { 'x-auth-token': token },
    body: formData
  });
  
  return await response.json();
}

// 2. Update course details (Step 2)
async function updateCourse(courseId, updatedData, files = {}) {
  const formData = new FormData();
  Object.keys(updatedData).forEach(key => {
    if (typeof updatedData[key] === 'object') {
      formData.append(key, JSON.stringify(updatedData[key]));
    } else {
      formData.append(key, updatedData[key]);
    }
  });
  
  if (files.thumbnail) formData.append('thumbnail', files.thumbnail);
  if (files.logo) formData.append('courseLogo', files.logo);

  const response = await fetch(`/api/courses/${courseId}`, {
    method: 'PUT',
    headers: { 'x-auth-token': token },
    body: formData
  });
  
  return await response.json();
}

// 3. Add curriculum content
async function addCurriculum(courseId, episodes) {
  for (const episode of episodes) {
    // Add episode
    const episodeRes = await fetch(`/api/courses/${courseId}/episodes`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-auth-token': token
      },
      body: JSON.stringify({
        title: episode.title,
        summary: episode.summary
      })
    });
    
    const episodeData = await episodeRes.json();
    const episodeId = episodeData.course.episodes[0]._id;
    
    // Add lessons to episode
    for (const lesson of episode.lessons) {
      await fetch(`/api/courses/${courseId}/episodes/${episodeId}/lessons`, {
        method: 'POST',
        headers: { 'x-auth-token': token },
        body: lesson.formData // FormData with files
      });
    }
    
    // Add quizzes to episode
    for (const quiz of episode.quizzes) {
      await fetch(`/api/courses/${courseId}/episodes/${episodeId}/quizzes`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-auth-token': token
        },
        body: JSON.stringify(quiz)
      });
    }
  }
}

// 4. Publish course
async function publishCourse(courseId) {
  const response = await fetch(`/api/courses/${courseId}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      'x-auth-token': token
    },
    body: JSON.stringify({ status: 'Published' })
  });
  
  return await response.json();
}

User Management API

Comprehensive user management system handling authentication, profiles, roles, wishlists, enrollments, and password recovery for both students and instructors.

User Management Flow
Figure 11: Complete user lifecycle from registration to profile management

User Model Overview

Core User Fields
  • firstName, lastName - Required
  • email - Unique, lowercase, required
  • password - Bcrypt hashed, required
  • role - Student or Instructor
  • avatar, coverPhoto - Profile media
Extended Features
  • enrolledCourses - Course progress tracking
  • wishlist - Saved courses for later
  • social - Social media links
  • purchasedAssessments - Bought assessments
  • resetPasswordToken - Password recovery

Endpoint Reference

Method Endpoint Description Role Required
POST /api/register Register as student None
POST /api/register-instructor Register as instructor None
POST /api/login Login and get JWT None
GET /api/user/profile Get current user profile Any
PUT /api/user/profile Update profile information Any
PUT /api/user/password Change password Any
PUT /api/user/social Update social links Any
POST /api/user/avatar Upload avatar image Any
POST /api/user/cover Upload cover photo Any
POST /api/user/wishlist/toggle Add/remove course from wishlist Any
GET /api/student/wishlist Get user's wishlist Student
POST /api/forgot-password Request password reset email None
POST /api/reset-password Reset password with token None

Detailed Endpoint Documentation

1

User Registration Endpoints

POST /api/register - Register as Student
// Request
{
  "firstName": "John",
  "lastName": "Doe",
  "email": "john@example.com",
  "password": "SecurePass123!"
}

// Response (Success - 201 Created)
{
  "success": true,
  "message": "User registered successfully!",
  "user": {
    "id": "507f1f77bcf86cd799439011",
    "firstName": "John",
    "lastName": "Doe",
    "email": "john@example.com",
    "role": "student",
    "createdAt": "2024-01-15T10:30:00.000Z"
  }
}

// Response (Error - 400 Bad Request)
{
  "success": false,
  "message": "User already exists"
}
POST /api/register-instructor - Register as Instructor
// Same request structure as student registration
// Response includes role: 'instructor'

// Response (Success - 201 Created)
{
  "success": true,
  "message": "Instructor registered successfully!",
  "user": {
    "id": "507f1f77bcf86cd799439012",
    "firstName": "Jane",
    "lastName": "Smith",
    "email": "jane@example.com",
    "role": "instructor",  // ← Note this difference
    "createdAt": "2024-01-15T10:30:00.000Z"
  }
}
Security Note: Passwords are automatically hashed using bcrypt with salt rounds of 10 before saving to the database. The original password is never stored.
2

Authentication & Profile Endpoints

POST /api/login - User Authentication
// Request
{
  "username": "john@example.com",  // Note: field is 'username' not 'email'
  "password": "SecurePass123!"
}

// Response (Success - 200 OK)
{
  "success": true,
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": "507f1f77bcf86cd799439011",
    "role": "student",
    "name": "John Doe",
    "avatar": "uploads/avatars/default.jpg"
  }
}

// JWT Payload Structure
{
  "user": {
    "id": "507f1f77bcf86cd799439011",
    "role": "student",
    "name": "John Doe",
    "avatar": "uploads/avatars/default.jpg"
  },
  "iat": 1704038400,
  "exp": 1704056400  // 5-hour expiration
}

// Response (Error - 400 Bad Request)
{
  "success": false,
  "message": "Invalid Credentials"
}
GET /api/user/profile - Get User Profile
// Headers
x-auth-token: YOUR_JWT_TOKEN

// Response (Success - 200 OK)
{
  "success": true,
  "data": {
    "registrationDate": "Mon Jan 15 2024",
    "firstName": "John",
    "lastName": "Doe",
    "username": "john@example.com",
    "email": "john@example.com",
    "phone": "+1234567890",
    "occupation": "Web Developer",
    "bio": "Passionate about learning and teaching web technologies.",
    "avatar": "uploads/avatars/1704038400000-profile.jpg",
    "coverPhoto": "uploads/covers/1704038400000-cover.jpg",
    "social": {
      "facebook": "https://facebook.com/johndoe",
      "twitter": "https://twitter.com/johndoe",
      "linkedin": "https://linkedin.com/in/johndoe",
      "website": "https://johndoe.dev",
      "github": "https://github.com/johndoe"
    }
  }
}
3

Profile Update Endpoints

PUT /api/user/profile - Update Profile
// Request
{
  "firstName": "John",
  "lastName": "Doe",
  "phone": "+1234567890",
  "occupation": "Senior Web Developer",
  "bio": "Updated bio with more details...",
  "email": "john.new@example.com"  // Email can be updated
}

// Response (Success - 200 OK)
{
  "success": true,
  "message": "Profile updated successfully"
}

// Response (Error - 400 Bad Request)
{
  "success": false,
  "message": "Email is already in use."
}
PUT /api/user/password - Change Password
// Request
{
  "currentPassword": "OldPass123!",
  "newPassword": "NewSecurePass456!"
}

// Response (Success - 200 OK)
{
  "success": true,
  "message": "Password updated successfully!"
}

// Response (Error - 400 Bad Request)
{
  "success": false,
  "message": "Incorrect current password."
}
PUT /api/user/social - Update Social Links
// Request
{
  "facebook": "https://facebook.com/johndoe",
  "twitter": "https://twitter.com/johndoe",
  "linkedin": "https://linkedin.com/in/johndoe",
  "website": "https://johndoe.dev",
  "github": "https://github.com/johndoe"
}

// Response (Success - 200 OK)
{
  "success": true,
  "message": "Social links updated successfully!"
}
4

File Upload Endpoints

POST /api/user/avatar - Upload Avatar
// Headers
Content-Type: multipart/form-data
x-auth-token: YOUR_JWT_TOKEN

// Form Data
avatar: [image file]  // Max 2MB, JPG/PNG

// Response (Success - 200 OK)
{
  "success": true,
  "msg": "Avatar updated successfully",
  "filePath": "uploads/avatars/1704038400000-profile.jpg"
}

// Frontend Implementation
async function uploadAvatar(file) {
  const formData = new FormData();
  formData.append('avatar', file);
  
  const response = await fetch('/api/user/avatar', {
    method: 'POST',
    headers: {
      'x-auth-token': auth.getToken()
    },
    body: formData
  });
  
  return await response.json();
}
POST /api/user/cover - Upload Cover Photo
// Similar to avatar upload but different field
// Form Data: coverPhoto: [image file]

// Response
{
  "success": true,
  "msg": "Cover photo updated successfully",
  "filePath": "uploads/covers/1704038400000-cover.jpg"
}
File Requirements: Avatar images max 2MB, Cover photos max 5MB. Supported formats: JPG, PNG, GIF, WebP.
5

Wishlist Management

POST /api/user/wishlist/toggle - Toggle Wishlist
// Request
{
  "courseId": "67a1b2c3d4e5f67890123456"
}

// Response (Added to wishlist)
{
  "success": true,
  "wishlist": [
    "67a1b2c3d4e5f67890123456",
    "67a1b2c3d4e5f67890123457"
  ]
}

// Response (Removed from wishlist)
{
  "success": true,
  "wishlist": [
    "67a1b2c3d4e5f67890123457"
  ]
}

// Frontend Implementation
async function toggleWishlist(courseId) {
  const response = await fetch('/api/user/wishlist/toggle', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-auth-token': auth.getToken()
    },
    body: JSON.stringify({ courseId })
  });
  
  return await response.json();
}
GET /api/student/wishlist - Get Wishlist
// Response (Success - 200 OK)
{
  "success": true,
  "courses": [
    {
      "_id": "67a1b2c3d4e5f67890123456",
      "title": "Complete React Developer Course",
      "thumbnail": "uploads/thumbnails/react-course.jpg",
      "price": 49.99,
      "originalPrice": 99.99,
      "instructor": {
        "_id": "67a1b2c3d4e5f67890123457",
        "firstName": "John",
        "lastName": "Doe"
      },
      "averageRating": 4.5,
      "reviewCount": 128
    }
    // ... more wishlisted courses
  ]
}
6

Password Recovery System

POST /api/forgot-password - Request Reset
// Request
{
  "email": "john@example.com"
}

// Response (Always returns 200 for security)
{
  "success": true,
  "message": "If a matching account was found, a password reset email has been sent."
}

// Process:
// 1. Generates cryptographically secure token
// 2. Sets token expiration (1 hour)
// 3. Sends email with reset link
// 4. Link format: /reset-password.html?token=token_here
POST /api/reset-password - Reset Password
// Request
{
  "token": "reset_token_from_email",
  "newPassword": "NewSecurePass456!",
  "confirmNewPassword": "NewSecurePass456!"
}

// Response (Success - 200 OK)
{
  "success": true,
  "message": "Password has been successfully reset. You can now log in."
}

// Response (Error - 400 Bad Request)
{
  "success": false,
  "message": "Password reset link is invalid or has expired. Please try again."
}

// Security Features:
// 1. Token expiration check
// 2. Password match validation
// 3. Password strength requirements
// 4. Token single-use (cleared after reset)

Database Schema Reference

User Model Structure (models/User.js)
const userSchema = new mongoose.Schema({
  // Basic Information
  firstName: { type: String, required: true },
  lastName: { type: String, required: true },
  email: { 
    type: String, 
    required: true, 
    unique: true, 
    lowercase: true 
  },
  password: { type: String, required: true },
  role: { 
    type: String, 
    enum: ['student', 'instructor'], 
    default: 'student' 
  },
  
  // Profile Media
  avatar: { type: String },
  coverPhoto: { type: String },
  
  // Profile Details
  phone: String,
  occupation: String,
  bio: String,
  
  // Social Media Links
  social: {
    facebook: String,
    twitter: String,
    linkedin: String,
    website: String,
    github: String,
  },
  
  // Registration
  registrationDate: { type: Date, default: Date.now },
  
  // Course Management
  wishlist: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Course'
  }],
  
  // Enrollment Tracking
  enrolledCourses: [{
    course: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'Course'
    },
    progress: {
      type: Number,
      default: 0
    },
    status: {
      type: String,
      default: 'active',
      enum: ['active', 'completed']
    },
    completedLessons: [{
      type: mongoose.Schema.Types.ObjectId
    }],
    completedQuizzes: [{
      type: mongoose.Schema.Types.ObjectId
    }]
  }],
  
  // Password Recovery
  resetPasswordToken: String,
  resetPasswordExpires: Date,
  
  // Assessments
  purchasedAssessments: {
    type: [{
      type: mongoose.Schema.Types.ObjectId,
      ref: 'Assessment'
    }],
    default: []
  }
}, { 
  timestamps: true  // Adds createdAt and updatedAt automatically
});

// Password Hashing Middleware
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) {
    return next();
  }
  
  try {
    const salt = await bcrypt.genSalt(10);
    this.password = await bcrypt.hash(this.password, salt);
    next();
  } catch (error) {
    next(error);
  }
});

// Password Comparison Method
userSchema.methods.comparePassword = function(candidatePassword) {
  return bcrypt.compare(candidatePassword, this.password);
};

Authentication Middleware

backend/authMiddleware.js - Complete Implementation
const jwt = require('jsonwebtoken');
const User = require('./models/User');

module.exports = async function(req, res, next) {
  // Get token from header
  const token = req.header('x-auth-token');

  // Check if no token
  if (!token) {
    return res.status(401).json({ 
      success: false, 
      message: 'No token, authorization denied' 
    });
  }

  try {
    // Verify token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Optional: Check if user still exists and is active
    const user = await User.findById(decoded.user.id).select('-password');
    if (!user) {
      return res.status(401).json({ 
        success: false, 
        message: 'User no longer exists' 
      });
    }

    // Add user to request object
    req.user = decoded.user;
    next();
  } catch (err) {
    console.error('Auth middleware error:', err.message);
    
    // Handle specific JWT errors
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ 
        success: false, 
        message: 'Token has expired. Please log in again.' 
      });
    }
    
    if (err.name === 'JsonWebTokenError') {
      return res.status(401).json({ 
        success: false, 
        message: 'Invalid token. Please log in again.' 
      });
    }
    
    // Generic error
    res.status(500).json({ 
      success: false, 
      message: 'Server error during authentication' 
    });
  }
};

// Role-based middleware examples
const isInstructor = (req, res, next) => {
  if (req.user && req.user.role === 'instructor') {
    return next();
  }
  return res.status(403).json({ 
    success: false, 
    message: 'Access denied. Instructor role required.' 
  });
};

const isStudent = (req, res, next) => {
  if (req.user && req.user.role === 'student') {
    return next();
  }
  return res.status(403).json({ 
    success: false, 
    message: 'Access denied. Student role required.' 
  });
};

// Course ownership middleware
const isCourseOwner = async (req, res, next) => {
  try {
    const course = await Course.findById(req.params.courseId);
    
    if (!course) {
      return res.status(404).json({ 
        success: false, 
        message: 'Course not found' 
      });
    }
    
    if (course.instructor.toString() !== req.user.id) {
      return res.status(403).json({ 
        success: false, 
        message: 'User not authorized to access this course' 
      });
    }
    
    next();
  } catch (error) {
    res.status(500).json({ 
      success: false, 
      message: 'Server error' 
    });
  }
};

module.exports = { auth, isInstructor, isStudent, isCourseOwner };

JWT Token Configuration

Token Generation and Validation
// In server.js - Login endpoint
app.post('/api/login', async (req, res) => {
  try {
    const { username, password } = req.body;
    const user = await User.findOne({ email: username });
    
    if (!user) {
      return res.status(400).json({ 
        success: false, 
        message: 'Invalid Credentials' 
      });
    }
    
    const isMatch = await user.comparePassword(password);
    if (!isMatch) {
      return res.status(400).json({ 
        success: false, 
        message: 'Invalid Credentials' 
      });
    }
    
    // Create JWT payload
    const payload = { 
      user: { 
        id: user.id, 
        role: user.role, 
        name: `${user.firstName} ${user.lastName}`, 
        avatar: user.avatar 
      } 
    };
    
    // Sign token with 5-hour expiration
    jwt.sign(
      payload, 
      process.env.JWT_SECRET, 
      { expiresIn: '5h' }, 
      (err, token) => {
        if (err) throw err;
        res.status(200).json({ 
          success: true, 
          token, 
          user: payload.user 
        });
      }
    );
  } catch (error) {
    console.error('Login error:', error);
    res.status(500).json({ 
      success: false, 
      message: 'Server error' 
    });
  }
});

// Required environment variable
// .env file:
JWT_SECRET=your_super_secret_key_change_in_production

Email Service Integration

Password Reset Email Service
// In emailService.js
const nodemailer = require('nodemailer');
const crypto = require('crypto');

// Configure email transporter
const transporter = nodemailer.createTransport({
  host: process.env.EMAIL_HOST,
  port: process.env.EMAIL_PORT,
  secure: process.env.EMAIL_PORT === '465',
  auth: {
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASS
  }
});

// Send password reset email
async function sendPasswordResetEmail(email, userName, resetToken) {
  const resetUrl = `${process.env.APP_URL}/reset-password.html?token=${resetToken}`;
  
  const mailOptions = {
    from: `"TruLern LMS" <${process.env.EMAIL_USER}>`,
    to: email,
    subject: 'Password Reset Request - TruLern LMS',
    html: `
      <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
        <h2 style="color: #333;">Password Reset Request</h2>
        <p>Hello ${userName},</p>
        <p>Click the button below to reset your password:</p>
        <div style="text-align: center; margin: 30px 0;">
          <a href="${resetUrl}" 
             style="background-color: #4CAF50; color: white; padding: 12px 24px; 
                    text-decoration: none; border-radius: 5px; font-weight: bold;">
            Reset Password
          </a>
        </div>
      </div>
    `
  };
  
  try {
    await transporter.sendMail(mailOptions);
    console.log(`Password reset email sent to ${email}`);
  } catch (error) {
    console.error('Error sending password reset email:', error);
    throw error;
  }
}

module.exports = { sendPasswordResetEmail };

Error Handling Reference

Error Code HTTP Status Description Solution
AUTH_001 401 No authentication token Login required
AUTH_002 401 Invalid or expired token Re-login required
AUTH_003 403 Insufficient permissions Check user role
USER_001 400 User already exists Use different email
USER_002 400 Invalid credentials Check email/password
USER_003 400 Email already in use Use different email
PASSWORD_001 400 Incorrect current password Verify current password
PASSWORD_002 400 Password reset token expired Request new reset link
FILE_001 400 Invalid file type/size Check upload requirements

Security Best Practices

Password Security
  • Bcrypt hashing with salt rounds 10
  • Minimum 8 characters required
  • Rate limiting on login attempts
  • Password strength validation
Token Security
  • JWT with 5-hour expiration
  • Secure random token generation
  • HTTPS transmission only
  • Token invalidation on logout
Data Protection
  • Email uniqueness validation
  • Input sanitization for XSS
  • File upload validation
  • Role-based access control

Common Use Cases & Examples

Complete User Registration Flow
// Example: Complete user registration and setup
async function completeUserRegistration(userData) {
  try {
    // 1. Register user
    const registerResponse = await fetch('/api/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(userData)
    });

    const registerResult = await registerResponse.json();
    
    if (!registerResult.success) {
      throw new Error(registerResult.message);
    }

    // 2. Login to get token
    const loginResponse = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        username: userData.email,
        password: userData.password
      })
    });

    const loginResult = await loginResponse.json();
    
    if (!loginResult.success) {
      throw new Error(loginResult.message);
    }

    // 3. Update profile with additional info
    const updateResponse = await fetch('/api/user/profile', {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'x-auth-token': loginResult.token
      },
      body: JSON.stringify({
        phone: '+1234567890',
        occupation: 'Web Developer',
        bio: 'Passionate about learning...'
      })
    });

    const updateResult = await updateResponse.json();
    
    if (!updateResult.success) {
      throw new Error(updateResult.message);
    }

    // 4. Upload profile picture (optional)
    if (profilePicture) {
      const formData = new FormData();
      formData.append('avatar', profilePicture);
      
      const avatarResponse = await fetch('/api/user/avatar', {
        method: 'POST',
        headers: { 'x-auth-token': loginResult.token },
        body: formData
      });
      
      const avatarResult = await avatarResponse.json();
    }

    return {
      success: true,
      message: 'Registration complete',
      token: loginResult.token,
      user: loginResult.user
    };

  } catch (error) {
    console.error('Registration error:', error);
    return {
      success: false,
      message: error.message
    };
  }
}

// Example: Password reset flow
async function handlePasswordReset(email) {
  try {
    // 1. Request reset email
    const resetRequest = await auth.requestPasswordReset(email);
    
    if (!resetRequest.success) {
      return resetRequest;
    }

    // 2. User receives email and clicks link
    // 3. On reset page, collect new password
    const resetResult = await auth.resetPassword(
      resetTokenFromURL,
      newPassword,
      confirmPassword
    );

    if (resetResult.success) {
      // 4. Redirect to login with success message
      window.location.href = '/login.html?message=Password reset successful';
    }

    return resetResult;

  } catch (error) {
    console.error('Password reset error:', error);
    return { success: false, message: error.message };
  }
}

Troubleshooting Guide

Symptoms: Getting 401 errors, being logged out unexpectedly.

Debug steps:

// 1. Check if token exists
console.log('Token exists:', localStorage.getItem('lmsToken') !== null);

// 2. Check token age
const tokenAge = Date.now() - localStorage.getItem('tokenTimestamp');
console.log('Token age (hours):', tokenAge / (60 * 60 * 1000));

// 3. Check JWT payload (debug only)
const token = localStorage.getItem('lmsToken');
const payload = JSON.parse(atob(token.split('.')[1]));
console.log('Token payload:', {
  userId: payload.user.id,
  role: payload.user.role,
  expires: new Date(payload.exp * 1000)
});

// 4. Manual token refresh (if implemented)
async function refreshToken() {
  try {
    const response = await fetch('/api/refresh-token', {
      method: 'POST',
      headers: { 'x-auth-token': auth.getToken() }
    });
    
    if (response.ok) {
      const data = await response.json();
      auth.setAuth(data.token, data.user);
    }
  } catch (error) {
    auth.logout();
  }
}
                    

Common issues:

Issue Solution
Email not received Check spam folder, verify SMTP configuration
Reset link expired Links expire in 1 hour, request new one
Invalid token error Token may have been used already
Password requirements not met Minimum 8 characters with complexity

Checklist:

  1. File size ≤ 2MB for avatars, ≤ 5MB for covers
  2. Supported formats: JPG, PNG, GIF, WebP
  3. Check browser console for CORS errors
  4. Verify uploads directory permissions
  5. Ensure authentication token is valid
// Debug file upload
const file = document.getElementById('avatar-input').files[0];
console.log('File details:', {
  name: file.name,
  size: file.size,
  type: file.type,
  lastModified: file.lastModified
});

// Test upload with curl
curl -X POST http://localhost:5000/api/user/avatar \
  -H "x-auth-token: YOUR_TOKEN" \
  -F "avatar=@profile.jpg"
                    

Payment API

TruLern LMS features a comprehensive multi-gateway payment system with support for Stripe, PayPal, and Razorpay. All gateways follow a consistent verification and enrollment flow with automatic order creation.

Payment Flow Architecture
Figure 12: Unified payment flow across all supported gateways

Payment Architecture Overview

Multi-Gateway Support

Stripe, PayPal, and Razorpay with consistent API patterns

Secure Verification

Server-side verification with signature validation

Automatic Enrollment

Automatic course/assessment enrollment after successful payment

Payment Flow

1

Client-Side Initiation

User selects payment method, frontend calls gateway-specific create endpoint

2

Gateway Processing

Payment processed by selected gateway (Stripe, PayPal, or Razorpay)

3

Server Verification

Backend verifies payment authenticity and processes enrollment

4

Database Updates

User enrolled in purchased items and order records created

Endpoint Reference

Gateway Method Endpoint Description Auth Required
Stripe POST /api/payments/stripe/create-intent Create Stripe PaymentIntent Yes
POST /api/payments/stripe/verify Verify Stripe payment and enroll user Yes
PayPal POST /api/payments/paypal/create-order Create PayPal order Yes
POST /api/payments/paypal/capture-order Capture PayPal payment and enroll user Yes
Razorpay POST /api/payments/create-order Create Razorpay order Yes
POST /api/payments/verify Verify Razorpay payment and enroll user Yes

Shared Enrollment Helper

backend/routes/paymentRoutes.js - enrollUser Function
 // ========================================== // 🛠️ SHARED HELPER: ENROLL USER // ========================================== // This function handles the database logic for ALL gateways. const enrollUser = async (userId, items) => { console.log(`[ENROLLMENT] Starting enrollment for user: ${userId}`); const user = await User.findById(userId); if (!user) throw new Error('User not found'); let changesMade = false; // 1. Process Courses const courseIds = items.filter(item => item.type === 'course').map(item => item.id); if (courseIds.length > 0) { const newEnrollments = courseIds .filter(courseId => !user.enrolledCourses.some(e => e.course && e.course.toString() === courseId)) .map(courseId => ({ course: courseId })); if (newEnrollments.length > 0) { user.enrolledCourses.push(...newEnrollments); changesMade = true; console.log(`[ENROLLMENT] Enrolled in ${newEnrollments.length} new courses.`); } } // 2. Process Assessments const assessmentIds = items.filter(item => item.type === 'assessment').map(item => item.id); if (assessmentIds.length > 0) { const newAssessments = assessmentIds .filter(id => !user.purchasedAssessments.some(ownedId => ownedId.toString() === id)) .map(id => new mongoose.Types.ObjectId(id)); if (newAssessments.length > 0) { user.purchasedAssessments.push(...newAssessments); changesMade = true; console.log(`[ENROLLMENT] Added ${newAssessments.length} new assessments.`); } } // 3. Save to DB if (changesMade) { await user.save(); console.log("[ENROLLMENT] User data saved successfully."); return true; } else { console.log("[ENROLLMENT] No new items to enroll."); return false; } }; 

Stripe Integration

1

POST /api/payments/stripe/create-intent

Create a Stripe PaymentIntent with support for multiple payment methods.

Request Example
 // Request { "amount": 4999, // Amount in smallest currency unit (cents for USD) "currency": "usd" // Optional, defaults to 'usd' } // Response (Success - 200 OK) { "success": true, "clientSecret": "pi_3Mx123456789_secret_abc123456789" } // Frontend Implementation (checkout.js) async function createStripePaymentIntent(amount) { const response = await fetch('/api/payments/stripe/create-intent', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-token': auth.getToken() }, body: JSON.stringify({ amount: Math.round(amount * 100), // Convert dollars to cents currency: 'usd' }) }); return await response.json(); } // Stripe.js Integration const stripe = Stripe(STRIPE_PUBLISHABLE_KEY); const elements = stripe.elements(); const paymentElement = elements.create('payment'); paymentElement.mount('#payment-element'); // Confirm Payment const { error } = await stripe.confirmPayment({ elements, confirmParams: { return_url: `${window.location.origin}/success.html`, }, }); 
Supported Payment Methods: Stripe automatically supports cards, Apple Pay, and Google Pay. Additional methods include Amazon Pay, Alipay, and WeChat Pay.
2

POST /api/payments/stripe/verify

Verify Stripe payment, enroll user, and create order records.

Request Example
 // Request { "paymentIntentId": "pi_3Mx123456789", "items": [ { "id": "67a1b2c3d4e5f67890123456", "type": "course", "price": 49.99, "title": "Complete React Developer Course" } ], "totalAmount": 49.99 } // Response (Success - 200 OK) { "success": true, "message": "Payment verified, user enrolled, and orders created." } // Response (Error - 400 Bad Request) { "success": false, "message": "Payment not successful." } // Server-side Processing Logic 1. Retrieve PaymentIntent from Stripe 2. Check if status === 'succeeded' 3. Call enrollUser() to process enrollment 4. Create Order documents for each course 5. Return success response 
Important: This endpoint creates individual Order documents for each purchased course, enabling detailed order history and reporting.

PayPal Integration

1

POST /api/payments/paypal/create-order

Create a PayPal order using PayPal Orders API v2.

Request Example
 // Request { "amount": "49.99" // String value required by PayPal } // Response (Success - 200 OK) { "success": true, "id": "5O190127TN364715T" } // PayPal Configuration (server-side) const PAYPAL_API = process.env.PAYPAL_MODE === 'sandbox' ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com'; // PayPal Access Token Helper const getPayPalAccessToken = async () => { const authString = Buffer.from(process.env.PAYPAL_CLIENT_ID + ':' + process.env.PAYPAL_CLIENT_SECRET).toString('base64'); const response = await axios.post(`${PAYPAL_API}/v1/oauth2/token`, 'grant_type=client_credentials', { headers: { 'Authorization': `Basic ${authString}`, 'Content-Type': 'application/x-www-form-urlencoded' } }); return response.data.access_token; }; // Frontend Implementation async function createPayPalOrder(amount) { const response = await fetch('/api/payments/paypal/create-order', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-token': auth.getToken() }, body: JSON.stringify({ amount }) }); return await response.json(); } 
2

POST /api/payments/paypal/capture-order

Capture PayPal payment and process enrollment.

Request Example
 // Request { "orderID": "5O190127TN364715T", "items": [ { "id": "67a1b2c3d4e5f67890123456", "type": "course", "price": 49.99, "title": "Complete React Developer Course" } ], "totalAmount": 49.99 } // Response (Success - 200 OK) { "success": true, "status": "COMPLETED", "data": { /* PayPal order details */ } } // PayPal Button Integration (Frontend) paypal.Buttons({ createOrder: async () => { const response = await fetch('/api/payments/paypal/create-order', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-token': auth.getToken() }, body: JSON.stringify({ amount: cartTotal.toString() }) }); const orderData = await response.json(); return orderData.id; // Return PayPal order ID }, onApprove: async (data) => { // Capture the order const response = await fetch('/api/payments/paypal/capture-order', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-token': auth.getToken() }, body: JSON.stringify({ orderID: data.orderID, items: cartItems, totalAmount: cartTotal }) }); const result = await response.json(); if (result.success) { window.location.href = '/success.html'; } } }).render('#paypal-button-container'); 

Razorpay Integration (India-Focused)

1

POST /api/payments/create-order

Create a Razorpay order for Indian payment methods (UPI, Netbanking, Wallets).

Request Example
 // Request { "items": [ { "id": "67a1b2c3d4e5f67890123456", "type": "course", "price": 2999, "title": "Complete React Developer Course" } ], "totalAmount": 2999 // Amount in INR } // Response (Success - 200 OK) { "success": true, "order": { "id": "order_JfFw8k8k8k8k8", "entity": "order", "amount": 299900, // Amount in paise "amount_paid": 0, "amount_due": 299900, "currency": "INR", "receipt": "receipt_1704038400000", "status": "created", "attempts": 0, "created_at": 1704038400 }, "calculatedAmount": 2999 } // Razorpay Configuration const razorpay = new Razorpay({ key_id: process.env.RAZORPAY_KEY_ID, key_secret: process.env.RAZORPAY_KEY_SECRET, }); // Amount Calculation Logic const options = { amount: Math.round(totalAmount * 100), // Convert to paise currency: "INR", receipt: `receipt_${Date.now()}`, }; 
2

POST /api/payments/verify

Verify Razorpay payment with signature validation and process enrollment.

Request Example
 // Request { "razorpay_order_id": "order_JfFw8k8k8k8k8", "razorpay_payment_id": "pay_JfFw9l9l9l9l9", "razorpay_signature": "abc123def456ghi789", "items": [ { "id": "67a1b2c3d4e5f67890123456", "type": "course", "price": 2999, "title": "Complete React Developer Course" } ], "totalAmount": 2999 } // Response (Success - 200 OK) { "success": true, "message": "Payment verified successfully and orders created!" } // Response (Error - 400 Bad Request) { "success": false, "message": "Transaction not legit!" } // Signature Verification Logic const shasum = crypto.createHmac('sha256', process.env.RAZORPAY_KEY_SECRET); shasum.update(`${razorpay_order_id}|${razorpay_payment_id}`); const digest = shasum.digest('hex'); if (digest !== razorpay_signature) { return res.status(400).json({ success: false, message: 'Transaction not legit!' }); } // Frontend Razorpay Integration const options = { key: RAZORPAY_KEY, amount: order.amount, currency: order.currency, name: "TruLern LMS", description: "Course Purchase", order_id: order.id, handler: async function(response) { // Verify payment on backend const verifyResponse = await fetch('/api/payments/verify', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-token': auth.getToken() }, body: JSON.stringify({ razorpay_order_id: response.razorpay_order_id, razorpay_payment_id: response.razorpay_payment_id, razorpay_signature: response.razorpay_signature, items: cartItems, totalAmount: cartTotal }) }); const result = await verifyResponse.json(); if (result.success) { window.location.href = '/success.html'; } }, prefill: { name: user.name, email: user.email, contact: user.phone || '' }, theme: { color: "#6C5CE7" } }; const rzp = new Razorpay(options); rzp.open(); 

Order Model Schema

backend/models/Order.js - Complete Schema
 const mongoose = require('mongoose'); const OrderSchema = new mongoose.Schema({ user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, course: { type: mongoose.Schema.Types.ObjectId, ref: 'Course', required: true }, amount: { type: Number, required: true }, status: { type: String, enum: ['pending', 'success', 'failed', 'refunded', 'processing', 'canceled', 'on hold'], default: 'pending' }, paymentId: { type: String // Stores Razorpay/Stripe Payment ID }, orderId: { type: String // Stores Razorpay Order ID }, createdAt: { type: Date, default: Date.now } }); module.exports = mongoose.model('Order', OrderSchema); 

Environment Configuration

.env File Configuration
 # ================================ # PAYMENT GATEWAY CONFIGURATION # ================================ # --- STRIPE --- STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # --- PAYPAL --- PAYPAL_CLIENT_ID=AYxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx PAYPAL_CLIENT_SECRET=ECxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx PAYPAL_MODE=sandbox # or 'live' CURRENCY_CODE=USD # Default currency # --- RAZORPAY (India) --- RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxxxxxx RAZORPAY_KEY_SECRET=yyyyyyyyyyyyyyyyyyyyyyyyyyyy # ================================ # PAYMENT SETTINGS # ================================ DEFAULT_CURRENCY=USD ENABLE_TEST_MODE=true # Set to false in production 

React Cart System

src/components/cart/CartSideMenu.jsx - Slide-out Cart
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useUI } from '../../context/UIContext';

const CartSideMenu = () => {
  const { isCartOpen, closeAll } = useUI();
  
  const [cartItems, setCartItems] = useState([]);
  const [subtotal, setSubtotal] = useState(0);

  // Load cart from localStorage
  const loadCart = () => {
    try {
      const storedCart = localStorage.getItem('cart_items');
      const cartData = storedCart ? JSON.parse(storedCart) : [];
      setCartItems(cartData);

      const total = cartData.reduce((sum, item) =>
        sum + (item.price * (item.quantity || 1)), 0);
      setSubtotal(total);
    } catch (error) {
      console.error('Error loading cart:', error);
    }
  };

  useEffect(() => {
    loadCart();
    window.addEventListener('cartUpdated', loadCart);
    return () => window.removeEventListener('cartUpdated', loadCart);
  }, []);

  // Helper to update storage and notify
  const syncAndNotify = (updatedItems) => {
    localStorage.setItem('cart_items', JSON.stringify(updatedItems));
    window.dispatchEvent(new Event('cartUpdated'));
  };

  // Actions
  const removeItem = (id) => {
    const updatedItems = cartItems.filter(item => String(item.id) !== String(id));
    syncAndNotify(updatedItems);
  };

  const updateQuantity = (id, newQuantity) => {
    if (newQuantity <= 0) {
      removeItem(id);
      return;
    }
    const updatedItems = cartItems.map(item =>
      String(item.id) === String(id) ? { ...item, quantity: newQuantity } : item
    );
    syncAndNotify(updatedItems);
  };

  const clearCart = () => {
    if (window.confirm('Are you sure you want to clear your cart?')) {
      syncAndNotify([]);
    }
  };

  return (
    <div className={`tru-cart-side-menu ${isCartOpen ? 'active' : ''}`}>
      <div className="inner-wrapper">
        <div className="inner-top">
          <div className="content">
            <div className="title">
              <h4 className="title mb--0">Your Cart</h4>
            </div>
            <div className="tru-btn-close">
              <button className="minicart-close-button tru-round-btn" onClick={closeAll}>
                <i className="feather-x"></i>
              </button>
            </div>
          </div>
        </div>

        <nav className="side-nav w-100">
          {cartItems.length === 0 ? (
            <div className="empty-cart-message">
              <div className="empty-icon">
                <i className="feather-shopping-cart"></i>
              </div>
              <h5>Your cart is empty</h5>
              <p>Add some courses to get started!</p>
              <Link to="/explore-courses" className="tru-btn btn-border-gradient radius-round btn-sm" onClick={closeAll}>
                Browse Courses
              </Link>
            </div>
          ) : (
            <ul className="tru-minicart-wrapper">
              {cartItems.map(item => (
                <li key={item.id} className="minicart-item">
                  <div className="minicart-thumb">
                    <img src={item.image || item.thumbnail} alt={item.name || item.title} />
                  </div>
                  <div className="minicart-item-content">
                    <h6 className="title">
                      <Link to={`/course-details/${item.id}`} onClick={closeAll}>
                        {item.name || item.title}
                      </Link>
                    </h6>
                    <div className="quantity-price">
                      <div className="quantity-control">
                        <button
                          className="qty-btn dec"
                          onClick={() => updateQuantity(item.id, (item.quantity || 1) - 1)}
                        >
                          <i className="feather-minus"></i>
                        </button>
                        <span className="quantity">{item.quantity || 1}</span>
                        <button
                          className="qty-btn inc"
                          onClick={() => updateQuantity(item.id, (item.quantity || 1) + 1)}
                        >
                          <i className="feather-plus"></i>
                        </button>
                      </div>
                      <span className="price">
                        ${(item.price * (item.quantity || 1)).toFixed(2)}
                      </span>
                    </div>
                  </div>
                  <button
                    className="remove-item"
                    onClick={() => removeItem(item.id)}
                    aria-label="Remove item"
                  >
                    <i className="feather-x"></i>
                  </button>
                </li>
              ))}
            </ul>
          )}
        </nav>

        {cartItems.length > 0 && (
          <div className="tru-minicart-footer">
            <hr className="mb--0" />
            <div className="tru-cart-subttotal">
              <p className="subtotal"><strong>Subtotal:</strong></p>
              <p className="price">${subtotal.toFixed(2)}</p>
            </div>
            
            <div className="tru-cart-actions mt--15">
              <button
                className="tru-btn btn-border btn-sm w-100 mb--10"
                onClick={clearCart}
              >
                Clear Cart
              </button>
            </div>
            
            <hr className="mb--0" />
            
            <div className="tru-minicart-bottom mt--20">
              <div className="view-cart-btn">
                <Link
                  className="tru-btn btn-border-gradient hover-icon-reverse radius-round w-100 text-center"
                  to="/cart"
                  onClick={closeAll}
                >
                  <span className="btn-text">View Cart</span>
                  <span className="btn-icon"><i className="feather-arrow-right"></i></span>
                  <span className="btn-icon"><i className="feather-arrow-right"></i></span>
                </Link>
              </div>

              <div className="checkout-btn mt--20">
                <Link
                  className="tru-btn btn-border-gradient hover-icon-reverse radius-round w-100 text-center"
                  to="/checkout"
                  onClick={closeAll}
                >
                  <span className="btn-text">Proceed to Checkout</span>
                  <span className="btn-icon"><i className="feather-arrow-right"></i></span>
                  <span className="btn-icon"><i className="feather-arrow-right"></i></span>
                </Link>
              </div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

export default CartSideMenu;
src/pages/ecommerce/Cart.jsx - Full Cart Page
import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';

const CartPage = () => {
  const navigate = useNavigate();
  const [cartItems, setCartItems] = useState([]);
  const [subTotal, setSubTotal] = useState(0);
  const [isLoading, setIsLoading] = useState(true);

  // Load cart from localStorage
  const loadCartData = () => {
    setIsLoading(true);
    try {
      const storedCart = localStorage.getItem('cart_items');
      const cart = storedCart ? JSON.parse(storedCart) : [];

      const itemsWithTotals = cart.map(item => ({
        ...item,
        price: parseFloat(item.price) || 0,
        quantity: parseInt(item.quantity) || 1,
        itemTotal: (parseFloat(item.price) || 0) * (parseInt(item.quantity) || 1)
      }));

      setCartItems(itemsWithTotals);
      const total = itemsWithTotals.reduce((sum, item) => sum + item.itemTotal, 0);
      setSubTotal(total);
    } catch (error) {
      console.error('Error loading cart:', error);
    } finally {
      setIsLoading(false);
    }
  };

  useEffect(() => {
    loadCartData();
    window.addEventListener('cartUpdated', loadCartData);
    return () => window.removeEventListener('cartUpdated', loadCartData);
  }, []);

  // Helper to sync and notify
  const syncAndNotify = (updatedItems) => {
    localStorage.setItem('cart_items', JSON.stringify(updatedItems));
    window.dispatchEvent(new Event('cartUpdated'));
  };

  // Actions
  const updateQuantity = (itemId, newQuantity) => {
    if (newQuantity <= 0) return handleRemoveItem(itemId);
    const updatedItems = cartItems.map(item =>
      String(item.id) === String(itemId) ? { ...item, quantity: newQuantity } : item
    );
    syncAndNotify(updatedItems);
  };

  const handleRemoveItem = (itemId) => {
    const updatedItems = cartItems.filter(item => String(item.id) !== String(itemId));
    syncAndNotify(updatedItems);
  };

  const handleClearCart = () => {
    if (window.confirm('Are you sure you want to clear your cart?')) {
      syncAndNotify([]);
    }
  };

  const formatCurrency = (amount) => {
    return `$${(amount || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
  };

  return (
    <div className="main-wrapper">
      <div className="tru-breadcrumb-default ptb--100 ptb_md--50 ptb_sm--30 bg-gradient-1">
        <div className="container">
          <div className="row">
            <div className="col-lg-12">
              <div className="breadcrumb-inner text-center">
                <h2 className="title">Cart</h2>
                <ul className="page-list">
                  <li className="tru-breadcrumb-item"><Link to="/">Home</Link></li>
                  <li><div className="icon-right"><i className="feather-chevron-right"></i></div></li>
                  <li className="tru-breadcrumb-item active">Cart</li>
                </ul>
              </div>
            </div>
          </div>
        </div>
      </div>

      <div className="tru-cart-area bg-color-white tru-section-gap">
        <div className="container">
          <div className="row">
            <div className="col-12">
              {isLoading ? (
                <div className="text-center py-5">
                  <div className="spinner-border text-primary" role="status"></div>
                </div>
              ) : cartItems.length === 0 ? (
                <div className="text-center py-5">
                  <i className="feather-shopping-cart mb--20 tru-cart-empty-icon"></i>
                  <h4>Your cart is empty</h4>
                  <Link to="/explore-courses" className="tru-btn btn-gradient mt--20">Browse Courses</Link>
                </div>
              ) : (
                <>
                  <div className="cart-table table-responsive mb--60">
                    <table className="table">
                      <thead>
                        <tr>
                          <th className="pro-title">Product</th>
                          <th className="pro-price">Price</th>
                          <th className="pro-quantity">Quantity</th>
                          <th className="pro-subtotal">Total</th>
                          <th className="pro-remove">Remove</th>
                        </tr>
                      </thead>
                      <tbody>
                        {cartItems.map((item) => (
                          <tr key={item.id}>
                            <td className="pro-title">
                              <Link to={`/course-details/${item.id}`} className="d-flex align-items-center gap-3">
                                <img
                                  src={item.image || item.thumbnail || '/assets/images/course/course-01.jpg'}
                                  alt={item.title}
                                  className="tru-cart-thumbnail"
                                />
                                <span className="tru-theme-text">{item.name || item.title}</span>
                              </Link>
                            </td>
                            <td className="pro-price"><span>{formatCurrency(item.price)}</span></td>
                            <td className="pro-quantity">
                              <div className="quantity-control justify-content-center">
                                <button className="qty-btn dec" onClick={() => updateQuantity(item.id, item.quantity - 1)}>
                                  <i className="feather-minus"></i>
                                </button>
                                <span className="quantity px-3">{item.quantity}</span>
                                <button className="qty-btn inc" onClick={() => updateQuantity(item.id, item.quantity + 1)}>
                                  <i className="feather-plus"></i>
                                </button>
                              </div>
                            </td>
                            <td className="pro-subtotal"><span>{formatCurrency(item.itemTotal)}</span></td>
                            <td className="pro-remove">
                              <button onClick={() => handleRemoveItem(item.id)} className="bg-transparent border-0">
                                <i className="feather-x"></i>
                              </button>
                            </td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>

                  <div className="row g-5">
                    <div className="col-lg-6">
                      <button className="tru-btn btn-border btn-sm" onClick={handleClearCart}>
                        <i className="feather-trash-2 me-2"></i> Clear Cart
                      </button>
                    </div>
                    <div className="col-lg-5 offset-lg-1">
                      <div className="cart-summary">
                        <div className="cart-summary-wrap">
                          <div className="section-title text-start">
                            <h4 className="title mb--30">Cart Summary</h4>
                          </div>
                          <p>Sub Total <span>{formatCurrency(subTotal)}</span></p>
                          <p>Shipping Cost <span>$0.00</span></p>
                          <h2>Grand Total <span>{formatCurrency(subTotal)}</span></h2>
                        </div>
                        <div className="cart-submit-btn-group">
                          <button className="tru-btn btn-gradient w-100" onClick={() => navigate('/checkout')}>
                            Proceed to Checkout
                          </button>
                        </div>
                      </div>
                    </div>
                  </div>
                </>
              )}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default CartPage;
src/context/UIContext.jsx - Cart Side Menu Control
import React, { createContext, useContext, useState } from 'react';

const UIContext = createContext();

export const useUI = () => {
  const context = useContext(UIContext);
  if (!context) {
    throw new Error('useUI must be used within UIProvider');
  }
  return context;
};

export const UIProvider = ({ children }) => {
  const [isCartOpen, setIsCartOpen] = useState(false);

  const openCart = () => setIsCartOpen(true);
  const closeAll = () => setIsCartOpen(false);

  return (
    <UIContext.Provider value={{
      isCartOpen,
      openCart,
      closeAll
    }}>
      {children}
    </UIContext.Provider>
  );
};
Cart Synchronization: The cart system uses the cartUpdated custom event to keep all components in sync. When items are added/removed/updated, the event is dispatched and all listening components (Header cart badge, CartSideMenu, CartPage) update automatically.

React Checkout Component

Location: src/pages/ecommerce/CheckoutPage.jsx

The checkout page implements a multi-gateway payment system with the following features:

  • Multi-gateway support: Stripe, PayPal, and Razorpay with tab switching
  • Billing/Shipping forms: Separate address forms with toggle option
  • User data prefill: Auto-fills from localStorage and user profile API
  • Payment element mounting: Uses refs for Stripe Elements and PayPal buttons
  • Cart synchronization: Dispatches cartUpdated event on success
  • Country selector: Complete country list dropdown

Key State Variables

// Payment method state
const [activePaymentMethod, setActivePaymentMethod] = useState('stripe');

// Form states
const [billingForm, setBillingForm] = useState({...});
const [shippingForm, setShippingForm] = useState({...});

// Cart data
const [cartItems, setCartItems] = useState([]);
const [subTotal, setSubTotal] = useState(0);
const [grandTotal, setGrandTotal] = useState(0);

// Payment gateway instances
const [stripe, setStripe] = useState(null);
const [elements, setElements] = useState(null);

// Refs for mounting payment elements
const stripeElementRef = useRef(null);
const paypalButtonRef = useRef(null);
                

Payment Method Handlers

// Stripe payment
const handleStripePayment = async () => {
  const { error } = await stripe.confirmPayment({
    elements,
    confirmParams: {
      return_url: `${window.location.origin}/stripe-success`,
      payment_method_data: { /* billing details */ }
    }
  });
};

// Razorpay payment
const handleRazorpayPayment = async () => {
  const orderRes = await fetch('/api/payment/create-order', {...});
  const options = { /* Razorpay options */ };
  const rzp = new window.Razorpay(options);
  rzp.open();
};

// PayPal (mounted via useEffect)
useEffect(() => {
  if (activePaymentMethod === 'paypal' && window.paypal) {
    window.paypal.Buttons({ /* config */ }).render(paypalButtonRef.current);
  }
}, [activePaymentMethod]);
                

Success Handler

const handlePaymentSuccess = () => {
  localStorage.removeItem('cart_items');
  window.dispatchEvent(new Event('cartUpdated'));
  navigate('/student/enrolled-courses');
};
                
API Requirements:
  • POST /api/payment/stripe/create-intent - Creates Stripe PaymentIntent
  • POST /api/payment/paypal/create-order - Creates PayPal order
  • POST /api/payment/paypal/capture-order - Captures PayPal payment
  • POST /api/payment/create-order - Creates Razorpay order
  • POST /api/payment/verify - Verifies Razorpay payment

Payment Flow Security

Signature Validation
  • Razorpay: HMAC SHA256 signature verification
  • Stripe: Webhook signature verification
  • PayPal: Webhook verification
Database Integrity
  • Order documents created for each course
  • User enrollment with duplicate prevention
  • Transaction status tracking

Error Handling

Error Type HTTP Status Message Recovery Action
Invalid Amount 400 "Invalid amount" Recalculate cart total, ensure amount > 0
Signature Mismatch 400 "Transaction not legit!" Re-initiate payment, check API keys
Payment Failed 400 "Payment not successful" Retry with different payment method
Gateway Error 500 "Server error during verification" Check gateway configuration, retry later
User Not Found 404 "User not found" Re-authenticate user, check token validity

Testing Payment Systems

1

Stripe Test Cards

Card Number Description Result
4242 4242 4242 4242 Visa (success) Success
4000 0000 0000 0002 Card declined Declined
4000 0025 0000 3155 3D Secure required 3D Secure
2

PayPal Sandbox Accounts

Use these test accounts in PayPal sandbox mode:

  • Buyer: sb-abc123@business.example.com
  • Password: test123
  • Seller: sb-xyz789@business.example.com
3

Razorpay Test Details

Method Test Details
Card 4111 1111 1111 1111 with any future expiry
UPI Use any test UPI ID
Netbanking Select any bank, use test credentials

Troubleshooting Payment Issues

Debug Steps:

// Check server logs for verification errors
console.log('Payment verification debug:');

// 1. Check API keys
console.log('Stripe key exists:', !!process.env.STRIPE_SECRET_KEY);
console.log('Razorpay key exists:', !!process.env.RAZORPAY_KEY_SECRET);

// 2. Test gateway connectivity
// Stripe test
const stripeTest = await stripe.paymentIntents.retrieve('test_id');
console.log('Stripe connection:', stripeTest ? 'OK' : 'Failed');

// 3. Verify signature generation
const expectedSig = crypto.createHmac('sha256', RAZORPAY_SECRET)
.update(orderId + '|' + paymentId)
.digest('hex');
console.log('Expected signature:', expectedSig);
console.log('Received signature:', receivedSignature);

// 4. Check enrollment logic
const user = await User.findById(userId);
console.log('User found:', !!user);
console.log('User enrolled courses:', user.enrolledCourses.length);

Common Causes:

Symptom Solution
Order document not created Check Order model import and save() calls
User already enrolled enrollUser() prevents duplicates - check logs
Database save error Check MongoDB connection and permissions
Items array empty Verify cart items are passed correctly

Production Deployment Checklist

Critical for Production:
  • Set PAYPAL_MODE=live and use live API credentials
  • Replace Stripe test keys with live keys
  • Configure Razorpay for production environment
  • Set up webhooks for payment notifications
  • Implement proper logging and monitoring
  • Enable HTTPS for all payment pages

Webhook Integration (Recommended)

Stripe Webhook Example
 // In server.js const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; app.post('/webhook/stripe', express.raw({type: 'application/json'}), (req, res) => { const sig = req.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret); } catch (err) { console.error('Webhook signature verification failed:', err.message); return res.status(400).send(`Webhook Error: ${err.message}`); } // Handle the event switch (event.type) { case 'payment_intent.succeeded': const paymentIntent = event.data.object; // Update order status, send confirmation email, etc. break; case 'payment_intent.payment_failed': // Handle failed payment break; default: console.log(`Unhandled event type ${event.type}`); } res.json({received: true}); }); 

Best Practices

Security
  • Never expose secret keys in frontend code
  • Validate all payment data server-side
  • Use HTTPS in production
  • Implement rate limiting on payment endpoints
User Experience
  • Show clear payment status messages
  • Provide multiple payment options
  • Handle errors gracefully
  • Send payment confirmation emails

Database Schema

TruLern uses a normalized MongoDB schema design with Mongoose. Below are the reference definitions for all core collections.

Note: ObjectId is a unique MongoDB identifier used to create relationships between documents (e.g., linking a Course to an Instructor).

1. Users Collection

Stores all user accounts including students, instructors, and administrators.

Field Type Description
firstName String User's first name
lastName String User's last name
email String Unique email address (login username)
password String Bcrypt hashed password
role String Enum: ['student', 'instructor', 'admin']
avatar String Path to profile picture
enrolledCourses Array List of objects: { course: ObjectId, progress: Number, status: String }
wishlist Array Array of Course ObjectIds
purchasedAssessments Array List of standalone assessments purchased by the user

2. Courses Collection

The central collection containing course metadata and the nested curriculum.

Field Type Description
title String Course title
slug String URL-friendly unique identifier
instructor ObjectId Reference to User
price Number Current selling price (0 for free)
thumbnail String Path to course cover image
status String Enum: ['Draft', 'Pending', 'Published']
episodes Array Curriculum Tree: Array of Topic objects containing nested lessons and quizzes.
certificateTemplate String ID of the PDF template to use

3. Lessons (Sub-document)

Stored inside course.episodes.

Field Type Description
title String Lesson title
lessonType String Enum: ['video', 'reading']
videoSource String Enum: ['vimeo', 'youtube', 'local']
videoUrl / videoPath String External URL or local file path
articleBody String HTML content for reading lessons
exerciseFiles Array List of file objects: { filename, path, size }
isPreview Boolean Allow free access without enrollment

4. Resources Collection

Digital library items uploaded by instructors.

Field Type Description
title String Resource title
category String Main category
subCategory String Sub-category
instructor ObjectId Owner of the resource
file String Path to the downloadable file (PDF/Zip)
thumbnail String Path to preview image

5. Orders Collection

Financial transaction records.

Field Type Description
user ObjectId Buyer
course ObjectId Purchased Item
amount Number Final paid amount
paymentGateway String 'stripe', 'paypal', or 'razorpay'
transactionId String Gateway reference ID
status String 'pending', 'success', 'failed'

6. Reviews Collection

Course ratings and feedback.

Field Type Description
user ObjectId Reviewer
course ObjectId Target Course
rating Number 1-5 Star Rating
review String Text feedback

7. Announcements Collection

Instructor-to-student broadcasts.

Field Type Description
instructor ObjectId Sender
course ObjectId Target Course (optional if global)
subject String Email subject
message String HTML message body
attachment Object { filename, path }

File Structure & Component Architecture

TruLern uses a modern React component-based architecture with Vite as the build tool. The application follows a feature-based organization with reusable components, contexts for state management, and page-level routing.

Pro Tip: Use Ctrl+P (or Cmd+P) in VS Code to quickly jump to these files by name.

1. Frontend Architecture (React)

  • client/
    • public/ // Static assets served directly
      • assets/ // Images, videos, etc.
      • uploads/ // User uploaded files (avatars, logos, thumbnails)
        • avatars/ // Profile pictures
        • logos/ // Course logos for certificates
        • thumbnails/ // Course cover images
        • videos/ // Local video uploads
        • pdfs/ // Exercise files
    • src/ // Main source code
      • App.jsx // Root component with routing configuration
      • main.jsx // Entry point, renders App with providers
      • assets/ // Compiled styles
        • scss/ // 80+ modular SCSS files
          • _variables.scss // Global colors, fonts, spacing
          • main.scss // Main compilation file
          • components/ // Component-specific styles
            • _course.scss, _quiz.scss, _lesson.scss, _buttons.scss, etc.
      • components/ // Reusable UI components
        • auth/ // Login/register components
        • common/ // Shared components (alerts, loaders, modals)
        • course-builder/ // Curriculum management
          • CurriculumBuilder.jsx // Main curriculum interface
          • TopicModal.jsx // Add/edit topics
          • LessonModal.jsx // Add/edit lessons
          • QuizModal.jsx // Add/edit quizzes
        • cart/ // Shopping cart components
          • CartSideMenu.jsx // Slide-out cart drawer
        • dashboard/ // Dashboard widgets
        • layout/ // Layout components
          • Layout.jsx // Main layout with header/footer
          • DashboardLayout.jsx // Dashboard layout with sidebar
        • lesson/ // Lesson player components
        • PrivateRoute.jsx // Route protection component
      • context/ // React Context providers
        • AuthContext.jsx // User authentication state
        • UIContext.jsx // UI state (cart sidebar, modals)
        • ModeContext.jsx // Light/dark mode preference
        • ComponentInitializer.jsx // Global component setup
      • pages/ // Page-level components (organized by feature)
        • admin/ // Admin dashboard pages
        • course/
          • CourseDetails.jsx // Public course page
          • ExploreCourses.jsx // Course listing with filters
        • ecommerce/
          • Cart.jsx // Shopping cart page
          • CheckoutPage.jsx // Multi-gateway checkout
        • instructor/
          • InstructorDashboard.jsx // Instructor home
          • CreateCourse.jsx // Step 1: Basic info
          • EditCourse.jsx // Step 2: Full course builder
          • InstructorCourses.jsx // Course listing
        • lesson/
          • LessonPage.jsx // Video/reading lesson player
          • QuizResultPage.jsx // Quiz results display
        • student/
          • StudentDashboard.jsx // Student home
          • StudentEnrolledCourses.jsx // My courses with progress
          • StudentWishlist.jsx // Saved courses
        • Login.jsx // Login page
        • InstructorRegisterPage.jsx // Instructor signup
        • IndexDemo.jsx // Landing page demo
      • services/
        • api.js // Axios configuration and interceptors
      • utils/
        • DOMSyncManager.jsx // Legacy support utilities
    • vite.config.js // Vite build configuration
    • package.json // Dependencies and scripts
    • index.html // Vite entry HTML

2. Page-to-Component Mapping

Every visible page in the application maps to a React component in the src/pages/ directory.

Page URL Component Description
/ or /demo IndexDemo.jsx Main landing page with multiple homepage variations
/login Login.jsx User login form with JWT authentication
/instructor-register InstructorRegisterPage.jsx Instructor registration form
/reset-password ResetPassword.jsx Password reset form with token validation
/explore-courses ExploreCourses.jsx Course catalog with filtering and search
/course-details/:id CourseDetails.jsx Public course landing page
/lesson/:courseId LessonPage.jsx Video/reading lesson player (standalone layout)
/instructor/dashboard InstructorDashboard.jsx Instructor dashboard with analytics
/instructor/create-course CreateCourse.jsx Step 1: Basic course info and thumbnail
/instructor/edit-course/:courseId EditCourse.jsx Step 2: Full course builder with curriculum
/student/dashboard StudentDashboard.jsx Student dashboard overview
/student/enrolled-courses StudentEnrolledCourses.jsx My courses with progress tracking and certificate download
/cart Cart.jsx Shopping cart page with quantity controls
/checkout CheckoutPage.jsx Multi-gateway checkout (Stripe, PayPal, Razorpay)
/stripe-success StripeSuccess.jsx Post-payment success handler
/resources ResourcesPage.jsx Digital library of resources
/design-system-* Various in pages/design-system/ UI component reference pages (buttons, forms, cards, etc.)

3. Routing Architecture

The application uses React Router v6 with nested layouts. The routing structure is defined in App.jsx.

Layout Hierarchy
// Russian Doll Nested Layouts
<Router>
  <AuthProvider>
    <UIProvider>
      <ModeProvider>
        <ComponentInitializer>
          <Routes>
            {/* Standalone pages (no header/footer) */}
            <Route path="/lesson/:courseId" element={<LessonPage />} />

            {/* Main website layout with header/footer */}
            <Route element={<Layout />}>
              <Route path="/" element={<IndexDemo />} />
              <Route path="/login" element={<LoginPage />} />
              <Route path="/explore-courses" element={<ExploreCourses />} />
              
              {/* Dashboard routes with sidebar */}
              <Route path="/instructor" element={<DashboardLayout />}>
                <Route path="dashboard" element={<InstructorDashboard />} />
                <Route path="courses" element={<InstructorCourses />} />
              </Route>
              
              <Route path="/student" element={<DashboardLayout />}>
                <Route path="dashboard" element={<StudentDashboard />} />
                <Route path="enrolled-courses" element={<StudentEnrolledCourses />} />
              </Route>
            </Route>
          </Routes>
        </ComponentInitializer>
      </ModeProvider>
    </UIProvider>
  </AuthProvider>
</Router>
    

4. Context Providers

Global state is managed through React Context providers located in src/context/.

Context File Purpose
AuthContext AuthContext.jsx Manages user authentication state, login/logout, token storage, and role-based access
UIContext UIContext.jsx Controls UI state like cart sidebar visibility, modals, and global UI toggles
ModeContext ModeContext.jsx Handles light/dark mode preference and persistence in localStorage
ComponentInitializer ComponentInitializer.jsx Initializes global components like the shadow mode alert and syncs with legacy systems

5. Backend Structure (Node.js)

The backend follows a clean MVC pattern and is completely separate from the React frontend.

  • backend/
    • server.js // Entry point. Connects to MongoDB, configures CORS/Multer, and starts the server.
    • authMiddleware.js // Protects routes by verifying JWT tokens from headers.
    • emailService.js // Configures Nodemailer for sending password resets and announcements.
    • install-demo-data.js // Utility: Run this to populate your database with the sample JSON data.
    • models/ // Mongoose Schemas
      • User.js // Auth, profile, role, wishlist, enrollments.
      • Course.js // Metadata + Curriculum (Episodes/Lessons/Quizzes).
      • Order.js // Financial transaction records.
      • Review.js, Announcement.js, Resource.js
      • Assessment.js, QuizResult.js, Book.js
    • routes/ // API Endpoints
      • authRoutes.js // Login, Register, Profile, Password Reset.
      • courseRoutes.js // Course CRUD, Public Listing, Filtering.
      • paymentRoutes.js // Stripe, PayPal, and Razorpay logic.
    • seed-data/
      • course-1.json to course-10.json // JSON data used by the installer script.
    • templates/certificates/
      • template-blank.pdf // The base PDF used for generating dynamic certificates.
    • fonts/
      • CormorantUnicase-Light.ttf // Custom font embedded into generated certificates.
Note: The frontend and backend are completely separate. The React app makes API calls to the backend using the fetchWithAuth utility from AuthContext or the api.js service with Axios interceptors.

Theming with SCSS

TruLern uses a powerful, modular SCSS architecture integrated with Vite. You can customize the entire visual theme by modifying the variable configuration.

Requirement: Vite handles SCSS compilation automatically during development. No separate compiler needed.

1. Theme Configuration

Navigate to src/assets/scss/_variables.scss. This file controls global settings for colors, typography, spacing, and component behaviors.

src/assets/scss/_variables.scss
// Brand Colors
$primary-color: #2f57ef;    /* Main brand color (Buttons, Links, Accents) */
$secondary-color: #b12add;  /* Secondary accent */
$heading-color: #393939;    /* H1-H6 color */
$body-color: #525252;       /* Paragraph text color */

// Fonts
$font-primary: 'Inter', sans-serif;

// Layout
$container-width: 1200px;
$spacing-unit: 8px;

// Component Specific
$border-radius: 8px;
$box-shadow: 0 5px 20px rgba(0,0,0,0.05);

2. Development vs Production

Vite handles SCSS compilation automatically:

Mode Command Behavior
Development npm run dev Vite compiles SCSS on-the-fly with hot module replacement. Changes appear instantly.
Production npm run build Vite compiles and minifies SCSS into the final dist/ folder.

3. SCSS Directory Structure

Modular SCSS Files
src/assets/scss/
├── _variables.scss          # Global variables (colors, fonts, spacing)
├── main.scss                # Main compilation file (imports all others)
└── components/              # 80+ modular component files
    ├── _buttons.scss        # All button variations
    ├── _forms.scss          # Form inputs, selects, checkboxes
    ├── _course.scss         # Course cards and details
    ├── _course-builder.scss # Curriculum builder specific
    ├── _lesson.scss         # Lesson player
    ├── _quiz.scss           # Quiz interface
    ├── _cart.scss           # Shopping cart
    ├── _checkout.scss       # Checkout forms
    ├── _modals.scss         # Modal windows
    └── ... 70+ more files

4. Adding Custom Styles

To add custom styles for a specific component:

1

Option A: Component-specific SCSS

Add styles to the appropriate component file in components/ directory.

// In src/assets/scss/components/_custom-component.scss
.my-custom-component {
    background: $primary-color;
    padding: $spacing-unit * 3;
    
    .custom-title {
        color: white;
        font-size: 1.5rem;
    }
}

// Then import in main.scss
// @import 'components/custom-component';
            
2

Option B: Module-specific CSS (for isolated styling)

Create a CSS module alongside your component for scoped styles.

// src/components/MyComponent.module.scss
.root {
    padding: 20px;
}

.title {
    color: $primary-color;
}

// In your component
import styles from './MyComponent.module.scss';

function MyComponent() {
    return (
        <div className={styles.root}>
            <h2 className={styles.title}>Hello</h2>
        </div>
    );
}
            

5. Build Configuration

Vite's SCSS configuration is in vite.config.js:

vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
    plugins: [react()],
    css: {
        preprocessorOptions: {
            scss: {
                additionalData: `@import "src/assets/scss/_variables.scss";`,
            },
        },
    },
});
Note: The additionalData setting automatically imports variables into every SCSS file, so you don't need to manually @import '_variables' in each component.

6. Available npm Scripts

From package.json in the client directory:

package.json scripts
{
    "scripts": {
        "dev": "vite",              // Start dev server with live SCSS compilation
        "build": "vite build",       // Build for production (compiles & minifies SCSS)
        "preview": "vite preview"    // Preview production build locally
    }
}
Quick Start: After modifying SCSS, simply run npm run dev to see changes instantly, or npm run build when you're ready to deploy.

Modifying Components

The styling is organized into modular components found in src/assets/scss/components/. Use this reference to find exactly which file to edit for any UI element.

Note: After modifying any SCSS file, changes are automatically applied if you're running npm run dev. For production, run npm run build.
Category SCSS Files (in src/assets/scss/components/)
Core UI & Typography _buttons.scss, _typography.scss, _colors.scss, _common.scss, _reset-styles.scss, _utilities.scss, _spacing.scss, _bgimage.scss, _bg-color-darker.scss, _background-gradient.scss
Layout & Navigation _header.scss, _footer.scss, _mobile-menu.scss, _sidebar-styles.scss, _sidebar-widgets.scss, _inner-page-layout.scss, _copyright.scss, _back-to-top.scss, _section.scss, _container.scss
Courses & Learning _course.scss, _course-details.scss, _course-sidebar.scss, _course-features.scss, _course-shapes.scss, _lesson.scss, _video.scss, _edit-course.scss, _assessment.scss, _certificate-template.scss, _quiz.scss, _course-builder.scss
Dashboards _instructor_dashboard.scss, _my-account.scss, _progress.scss, _tables.scss, _counter.scss, _list.scss
Shop & Cart _shop.scss, _cart.scss, _mini-cart.scss, _cart-summary.scss, _checkout.scss, _pricing.scss
UI Elements _accordion.scss, _tabs.scss, _modals.scss, _badge.scss, _blockquote.scss, _social.scss, _pagination.scss, _search.scss, _filter-select.scss, _dropdowns.scss, _forms.scss, _show-more.scss
Marketing Sections _banner.scss, _banner-core.scss, _banner-variants.scss, _banner-components.scss, _testimonial.scss, _service.scss, _service-area.scss, _splash-service-area.scss, _team.scss, _blog.scss, _portfolio.scss, _gallery.scss, _brand-styles.scss, _brand-assets.scss, _cta.scss, _cta-footer.scss, _about.scss, _contact.scss
Advanced & Plugins _animation.scss, _dark-mode.scss, _overlays.scss, _swiperslider.scss, _slider-default.scss, _image-content-scroller.scss, _feature.scss, _feature-plugin.scss, _feature-presentation.scss, _layout-presentation.scss, _single-demo.scss, _switcher.scss, _hotfix.scss, _extra.scss, _event-shapes.scss
Example

Modifying Button Styles

To change the appearance of all buttons across the application:

  1. Open src/assets/scss/components/_buttons.scss
  2. Find the button variant you want to modify (e.g., .tru-btn-primary)
  3. Update the properties:
// In _buttons.scss
.tru-btn-primary {
    background: $primary-color;
    border-radius: 30px;  // Change from 8px to 30px
    padding: 15px 30px;    // Increase padding
    font-weight: 600;      // Make text bolder
    
    &:hover {
        background: darken($primary-color, 10%);
        transform: translateY(-2px);
    }
}
            

Changes will appear immediately in your development server.

Important: Always use the SCSS variables (like $primary-color) defined in _variables.scss rather than hardcoding color values. This ensures theme consistency and makes future updates easier.

Adding New Pages

TruLern uses React Router for client-side routing. Adding a new page involves creating a React component and adding a route in App.jsx.

Note: All routes are defined in the frontend. The backend only serves the API.
1

Create the Page Component

Create a new React component in the appropriate folder inside src/pages/:

Example: src/pages/Webinar.jsx
import React from 'react';
import { Link } from 'react-router-dom';

const Webinar = () => {
    return (
        <div className="webinar-page">
            <h1>Upcoming Webinars</h1>
            <div className="webinar-list">
                {/* Your page content here */}
            </div>
        </div>
    );
};

export default Webinar;
2

Add the Route

Open src/App.jsx and add your new route inside the appropriate layout:

src/App.jsx - Adding a Route
// 1. Import your new component
import Webinar from './pages/Webinar';

// 2. Add the route inside the Layout wrapper
<Route element={<Layout />}>
    {/* Existing routes */}
    <Route path="/webinar" element={<Webinar />} />
</Route>
3

Add Navigation Link

Add a link to your new page using React Router's Link component:

Example: Adding to Header
import { Link } from 'react-router-dom';

// In your navigation component
<li>
    <Link to="/webinar">Webinars</Link>
</li>
4

Add Page-Specific Logic (Optional)

If your page needs specific logic, you can:

  • Add state and effects directly in the component
  • Create custom hooks in src/hooks/ for reusable logic
  • Use context for global state
Example with data fetching
import React, { useState, useEffect } from 'react';
import { useAuth } from '../context/AuthContext';

const Webinar = () => {
    const [webinars, setWebinars] = useState([]);
    const [loading, setLoading] = useState(true);
    const { fetchWithAuth } = useAuth();

    useEffect(() => {
        const loadWebinars = async () => {
            try {
                const response = await fetchWithAuth('/api/webinars');
                const data = await response.json();
                setWebinars(data.webinars);
            } catch (error) {
                console.error('Failed to load webinars:', error);
            } finally {
                setLoading(false);
            }
        };
        
        loadWebinars();
    }, []);

    if (loading) return <div>Loading...</div>;

    return (
        <div>
            {webinars.map(webinar => (
                <div key={webinar.id}>{webinar.title}</div>
            ))}
        </div>
    );
};
5

Add Styling (Optional)

Add custom styles in the appropriate SCSS file:

  1. Open src/assets/scss/components/
  2. Add to an existing file or create a new one (e.g., _webinar.scss)
  3. Import it in main.scss if you created a new file:
// In src/assets/scss/main.scss
@import 'components/webinar';
            
Important: Your page will automatically inherit the site-wide header and footer if you place it inside the <Layout /> route. For pages without header/footer (like lesson player), place it outside the Layout wrapper in App.jsx.

Quick Reference: Route Placement

Route Type Placement in App.jsx Example
With Header & Footer Inside <Route element={<Layout />}> /webinar, /about, /contact
With Dashboard Sidebar Inside <Route element={<DashboardLayout />}> /instructor/*, /student/*
Standalone (No Layout) Outside any layout wrapper /lesson/:courseId

Extending the Backend

TruLern follows the standard MVC (Model-View-Controller) pattern. To add a new feature (e.g., a "Ticket" system for support), follow these steps:

1

Create Model

Create backend/models/Ticket.js:

backend/models/Ticket.js
const mongoose = require('mongoose');
const ticketSchema = new mongoose.Schema({
    subject: String,
    message: String,
    user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
    status: { type: String, enum: ['Open', 'Closed'], default: 'Open' },
    createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Ticket', ticketSchema);
2

Create Routes

Create backend/routes/ticketRoutes.js:

backend/routes/ticketRoutes.js
const express = require('express');
const router = express.Router();
const Ticket = require('../models/Ticket');
const auth = require('../authMiddleware');

// Get all tickets
router.get('/', auth, async (req, res) => {
    const tickets = await Ticket.find({ user: req.user.id });
    res.json(tickets);
});

// Create ticket
router.post('/', auth, async (req, res) => {
    const newTicket = new Ticket({ ...req.body, user: req.user.id });
    await newTicket.save();
    res.json(newTicket);
});

module.exports = router;
3

Register Route

Open backend/server.js and add this line where other routes are defined:

backend/server.js
app.use('/api/tickets', require('./routes/ticketRoutes'));
4

Consume in React Frontend

Once the backend endpoint is created, you can call it from any React component using the fetchWithAuth method from AuthContext or the Axios instance in src/services/api.js:

Example React Component
import React, { useState, useEffect } from 'react';
import { useAuth } from '../../context/AuthContext';

const TicketsPage = () => {
    const [tickets, setTickets] = useState([]);
    const { fetchWithAuth } = useAuth();

    useEffect(() => {
        const loadTickets = async () => {
            const response = await fetchWithAuth('/api/tickets');
            const data = await response.json();
            setTickets(data);
        };
        loadTickets();
    }, []);

    return (
        <div>
            {tickets.map(ticket => (
                <div key={ticket._id}>{ticket.subject}</div>
            ))}
        </div>
    );
};
                        

Adding Payment Gateways

TruLern includes Stripe, PayPal, and Razorpay. To add a custom gateway (e.g., Paystack, Flutterwave), follow this architecture.

1

Frontend: Add Payment Tab

In src/pages/ecommerce/CheckoutPage.jsx, add your new gateway to the gateways array and create a corresponding section in the JSX.

CheckoutPage.jsx - Add Gateway Option
// In the gateways array
const gateways = [
    { id: 'stripe', name: 'Stripe', icon: '/assets/images/payment/stripe.svg' },
    { id: 'paypal', name: 'PayPal', icon: '/assets/images/payment/paypal.svg' },
    { id: 'razorpay', name: 'Razorpay', icon: '/assets/images/payment/razorpay.svg' },
    { id: 'paystack', name: 'Paystack', icon: '/assets/images/payment/paystack.svg' } // New
];

// In the payment sections container, add:
<div id="paystack-section" className={`payment-section ${activePaymentMethod === 'paystack' ? 'active' : ''}`}>
    <p className="text-muted mb-3">
        Pay with Paystack (Cards, Bank Transfer, Mobile Money)
    </p>
    <button
        className="tru-btn hover-icon-reverse btn-border-gradient radius-round w-100 mt-3"
        onClick={() => handlePayment('paystack')}
        disabled={cartItems.length === 0}
    >
        <div className="icon-reverse-wrapper">
            <span className="btn-text">Pay with Paystack</span>
            <span className="btn-icon"><i className="feather-credit-card"></i></span>
            <span className="btn-icon"><i className="feather-credit-card"></i></span>
        </div>
    </button>
</div>
2

Frontend: Add Payment Handler

In CheckoutPage.jsx, add a handler function for your new gateway:

CheckoutPage.jsx - Payment Handler
const handlePaystackPayment = async () => {
    const formData = getFormData();
    if (!formData) return;

    const token = getLocalStorageItem('lmsToken');
    
    try {
        // 1. Initialize payment on your backend
        const initResponse = await fetch('/api/payment/paystack/initialize', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'x-auth-token': token
            },
            body: JSON.stringify({
                amount: grandTotal,
                email: formData.billing.email,
                items: cartItems,
                metadata: {
                    billing: formData.billing,
                    shipping: showShipping ? shippingForm : formData.billing
                }
            })
        });

        const initData = await initResponse.json();
        
        if (!initData.success) {
            throw new Error(initData.message);
        }

        // 2. Open Paystack popup
        const handler = window.PaystackPop.setup({
            key: process.env.REACT_APP_PAYSTACK_PUBLIC_KEY,
            email: formData.billing.email,
            amount: Math.round(grandTotal * 100),
            currency: 'GHS', // or your currency
            reference: initData.reference,
            metadata: initData.metadata,
            callback: async (response) => {
                // 3. Verify payment on backend
                const verifyResponse = await fetch('/api/payment/paystack/verify', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'x-auth-token': token
                    },
                    body: JSON.stringify({
                        reference: response.reference,
                        items: cartItems,
                        totalAmount: grandTotal
                    })
                });

                const verifyData = await verifyResponse.json();
                if (verifyData.success) {
                    handlePaymentSuccess();
                } else {
                    alert('Payment verification failed: ' + verifyData.message);
                }
            },
            onClose: () => {
                console.log('Payment window closed');
            }
        });
        
        handler.openIframe();

    } catch (error) {
        console.error('Paystack payment error:', error);
        alert('Payment error: ' + error.message);
    }
};

// Add to the main handlePayment function
const handlePayment = async (method) => {
    if (!acceptTerms) {
        alert('Please accept the terms & conditions to proceed.');
        return;
    }

    if (cartItems.length === 0) {
        alert('Your cart is empty!');
        return;
    }

    switch (method) {
        case 'stripe':
            await handleStripePayment();
            break;
        case 'paypal':
            await handlePayPalPayment();
            break;
        case 'razorpay':
            await handleRazorpayPayment();
            break;
        case 'paystack':
            await handlePaystackPayment(); // New
            break;
        default:
            console.error('Unknown payment method:', method);
    }
};
3

Backend: Create Routes

In backend/routes/paymentRoutes.js, add initialization and verification endpoints that use the shared enrollUser() helper.

backend/routes/paymentRoutes.js
// Paystack initialization
router.post('/paystack/initialize', auth, async (req, res) => {
    try {
        const { amount, email, items, metadata } = req.body;
        
        // Initialize with Paystack API
        const response = await axios.post('https://api.paystack.co/transaction/initialize', {
            email,
            amount: Math.round(amount * 100),
            currency: 'GHS',
            metadata: {
                ...metadata,
                userId: req.user.id,
                items
            }
        }, {
            headers: {
                Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
                'Content-Type': 'application/json'
            }
        });

        res.json({
            success: true,
            reference: response.data.data.reference,
            metadata: response.data.data.metadata
        });
    } catch (error) {
        console.error('Paystack init error:', error);
        res.status(500).json({ success: false, message: 'Failed to initialize payment' });
    }
});

// Paystack verification
router.post('/paystack/verify', auth, async (req, res) => {
    try {
        const { reference, items, totalAmount } = req.body;
        
        // Verify with Paystack API
        const response = await axios.get(`https://api.paystack.co/transaction/verify/${reference}`, {
            headers: {
                Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`
            }
        });

        const paymentData = response.data.data;
        
        if (paymentData.status === 'success') {
            // Use the shared enrollUser helper
            const enrollmentSuccess = await enrollUser(req.user.id, items);
            
            if (enrollmentSuccess) {
                // Create order records
                await Promise.all(items.map(item => 
                    new Order({
                        user: req.user.id,
                        course: item.id,
                        amount: item.price,
                        status: 'success',
                        paymentGateway: 'paystack',
                        transactionId: reference
                    }).save()
                ));
                
                return res.json({ success: true });
            }
        }
        
        res.status(400).json({ success: false, message: 'Payment verification failed' });
    } catch (error) {
        console.error('Paystack verify error:', error);
        res.status(500).json({ success: false, message: 'Verification error' });
    }
});
4

Environment Configuration

Add your gateway credentials to .env files:

backend/.env
# Paystack (example)
PAYSTACK_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxx
PAYSTACK_PUBLIC_KEY=pk_live_xxxxxxxxxxxxxxxxxxxx
            
client/.env
VITE_PAYSTACK_PUBLIC_KEY=pk_live_xxxxxxxxxxxxxxxxxxxx
            
Important: Always use the shared enrollUser() helper for enrollment logic to maintain consistency across all payment gateways. Never duplicate enrollment code.
Pattern: All payment gateways follow the same pattern:
  1. Frontend collects billing/shipping data via getFormData()
  2. Backend initializes payment and returns reference/secret
  3. Frontend opens gateway's payment modal
  4. On success, backend verifies and calls enrollUser()
  5. Order documents are created and cart is cleared

Production Deployment

TruLern is optimized for deployment on any VPS (Virtual Private Server) running Ubuntu/Linux, such as DigitalOcean, AWS EC2, or Linode. The application consists of two separate parts: the React frontend and the Node.js backend API.

Prerequisites:
  • A VPS running Ubuntu 20.04 or 22.04
  • Node.js (v16 or v18 LTS) installed
  • Nginx (as a reverse proxy for both frontend and backend)
  • MongoDB (Installed locally or using MongoDB Atlas)
  • PM2 (for process management) - npm install -g pm2

1. Server Setup & File Structure

1

Upload Code to Server

Upload your project files to the server. Recommended structure:

/var/www/trulern/
├── backend/          # Node.js API server
└── client/           # React frontend source (will be built to /var/www/trulern/html)
            
2

Backend Configuration

Navigate to the backend directory and install dependencies:

Terminal Commands
cd /var/www/trulern/backend
npm install --production
            

Create environment configuration file:

backend/.env
NODE_ENV=production
PORT=5000
MONGO_URI=mongodb+srv://username:password@cluster.mongodb.net/trulern
JWT_SECRET=your_secure_random_string_here

# Email Configuration
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=your-email@gmail.com
EMAIL_PASS=your-app-password

# Payment Gateway Keys
STRIPE_SECRET_KEY=sk_live_...
PAYPAL_CLIENT_ID=AY...
RAZORPAY_KEY_ID=rzp_live...
RAZORPAY_KEY_SECRET=...
            
3

Frontend Build

Navigate to the client directory, install dependencies, and create production build:

Terminal Commands
cd /var/www/trulern/client
npm install
            

Create frontend environment file:

client/.env
VITE_API_URL=https://your-domain.com/api  # Or http://localhost:5000/api if using same server
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_...
VITE_RAZORPAY_KEY_ID=rzp_live_...
            

Build the React application:

Build Command
npm run build
            

This creates a dist/ folder with static files that will be served by Nginx.

4

Set Up PM2 for Backend

Use PM2 to keep your Node.js backend running continuously:

PM2 Commands
cd /var/www/trulern/backend
pm2 start server.js --name trulern-api
pm2 save
pm2 startup  # Follow the instructions to enable PM2 on system restart
            

2. Nginx Configuration

Nginx will serve both the React frontend (static files) and proxy API requests to your Node.js backend.

1

Create Nginx Site Configuration

/etc/nginx/sites-available/trulern
server {
    listen 80;
    server_name your-domain.com www.your-domain.com;
    
    # Root directory for React static files
    root /var/www/trulern/client/dist;
    index index.html;

    # Gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    # API requests - proxy to backend
    location /api/ {
        proxy_pass http://localhost:5000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Uploaded files
    location /uploads/ {
        alias /var/www/trulern/client/public/uploads/;
        expires 30d;
        add_header Cache-Control "public, no-transform";
    }

    # All other routes - serve React app (for client-side routing)
    location / {
        try_files $uri $uri/ /index.html;
        expires 1h;
        add_header Cache-Control "public, no-transform";
    }
}
            
2

Enable Site and Test

Terminal Commands
# Create symbolic link
sudo ln -s /etc/nginx/sites-available/trulern /etc/nginx/sites-enabled/

# Test Nginx configuration
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx
            

3. SSL Certificate (HTTPS)

Use Let's Encrypt to secure your site with free SSL certificates:

Install Certbot and Get SSL
# Install Certbot
sudo apt update
sudo apt install certbot python3-certbot-nginx

# Obtain SSL certificate
sudo certbot --nginx -d your-domain.com -d www.your-domain.com

# Certbot will automatically update your Nginx configuration for HTTPS
# Certificates auto-renew, test with:
sudo certbot renew --dry-run
    

4. Upload Directories & Permissions

Ensure upload directories exist and have correct permissions:

Setup Upload Directories
cd /var/www/trulern/client/public
mkdir -p uploads/{avatars,covers,logos,pdfs,resources,thumbnails,videos}

# Set permissions (Nginx runs as www-data)
sudo chown -R www-data:www-data uploads/
sudo chmod -R 755 uploads/
    

5. Verify Deployment

Checklist

  • Visit https://your-domain.com - Should load the React app
  • Test API: https://your-domain.com/api/courses - Should return JSON
  • Test authentication: Try logging in
  • Check file uploads: Upload a test avatar
  • Monitor logs: pm2 logs trulern-api
Alternative Hosting: You can also deploy the React frontend to Vercel or Netlify, and the backend to services like Railway, Render, or Heroku. Just ensure CORS is properly configured in the backend to accept requests from your frontend domain.

Process Management (PM2)

TruLern uses PM2 to keep your Node.js backend API running 24/7. The React frontend is served by Nginx (static files) and does not need PM2.

Note: PM2 only manages the backend API server. The frontend React files are served directly by Nginx.
1

Install PM2 Globally

Terminal
sudo npm install pm2 -g
2

Start the Backend API

Navigate to the backend folder where your server.js is located.

Terminal
cd /var/www/trulern/backend
pm2 start server.js --name trulern-api

This starts the Node.js backend server. You can also use the pre-configured ecosystem file if available:

pm2 start ecosystem.config.js
3

Monitor the Process

View logs, status, and resource usage:

Useful PM2 Commands
# List all running processes
pm2 list

# View real-time logs
pm2 logs trulern-api

# Monitor CPU/Memory usage
pm2 monit

# Restart the application
pm2 restart trulern-api

# Stop the application
pm2 stop trulern-api
4

Setup Automatic Startup on Reboot

Ensure the backend API restarts automatically if the server reboots.

Terminal
# Save the current process list
pm2 save

# Generate startup script
pm2 startup

# Follow the on-screen instructions (usually just run the command it outputs)

Example output and command to run:

[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u youruser --hp /home/youruser
5

PM2 Ecosystem File (Optional)

If you have multiple instances or need advanced configuration, create ecosystem.config.js in your backend folder:

backend/ecosystem.config.js
module.exports = {
    apps: [{
        name: 'trulern-api',
        script: 'server.js',
        instances: 1,              // Set to 'max' for cluster mode
        exec_mode: 'fork',          // or 'cluster'
        watch: false,
        max_memory_restart: '1G',
        env: {
            NODE_ENV: 'production',
            PORT: 5000
        },
        error_file: './logs/err.log',
        out_file: './logs/out.log',
        log_file: './logs/combined.log',
        time: true
    }]
};

Then start with: pm2 start ecosystem.config.js

Important: After deploying code updates, always restart PM2 to apply changes:
pm2 restart trulern-api
        
Verification: Check that your API is running:
curl http://localhost:5000/api/courses
# Should return JSON response
        

Nginx & SSL Configuration

Nginx serves two purposes: it serves your built React frontend static files, and acts as a reverse proxy to forward API requests to your Node.js backend running on port 5000.

Architecture:
  • React frontend (static files) → Served directly by Nginx from /var/www/trulern/client/dist
  • API requests (routes starting with /api/) → Proxied to Node.js backend on port 5000
  • Uploaded files (images, PDFs) → Served from /var/www/trulern/client/public/uploads

1. Configure Nginx

Create a configuration file for your site.

Terminal
sudo nano /etc/nginx/sites-available/trulern

Paste the following configuration (replace your-domain.com with your actual domain):

Nginx Config (HTTP - Port 80)
server {
    listen 80;
    server_name your-domain.com www.your-domain.com;
    
    # Root directory for React built files
    root /var/www/trulern/client/dist;
    index index.html;

    # Gzip compression for faster loading
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;

    # API requests - proxy to Node.js backend
    location /api/ {
        proxy_pass http://localhost:5000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # Increase timeout for file uploads
        proxy_connect_timeout 300s;
        proxy_send_timeout 300s;
        proxy_read_timeout 300s;
    }

    # Serve uploaded files directly
    location /uploads/ {
        alias /var/www/trulern/client/public/uploads/;
        expires 30d;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

    # Handle static assets with cache headers
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

    # All other routes - serve React app (for client-side routing)
    location / {
        try_files $uri $uri/ /index.html;
        expires 1h;
        add_header Cache-Control "public, no-transform";
    }

    # Deny access to hidden files
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Enable the site and test configuration:

Terminal
# Create symbolic link to enable site
sudo ln -s /etc/nginx/sites-available/trulern /etc/nginx/sites-enabled/

# Test Nginx configuration for syntax errors
sudo nginx -t

# Reload Nginx to apply changes
sudo systemctl reload nginx

2. Enable SSL (HTTPS) with Let's Encrypt

Secure your site with a free SSL certificate from Let's Encrypt. This will automatically update your Nginx configuration.

1

Install Certbot

Terminal
sudo apt update
sudo apt install certbot python3-certbot-nginx
2

Obtain SSL Certificate

Run Certbot with the Nginx plugin. It will automatically modify your Nginx configuration to use HTTPS.

Terminal
sudo certbot --nginx -d your-domain.com -d www.your-domain.com

Follow the interactive prompts:

  • Enter your email for renewal notifications
  • Agree to the terms of service
  • Choose whether to redirect HTTP to HTTPS (recommended: Yes)
3

Verify SSL Installation

After completion, Certbot will generate a configuration like this:

Generated HTTPS Config (simplified)
server {
    listen 443 ssl;
    server_name your-domain.com;

    ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
    
    # ... rest of your configuration ...
}

server {
    listen 80;
    server_name your-domain.com;
    return 301 https://$host$request_uri;  # Redirect HTTP to HTTPS
}
4

Auto-Renewal

Let's Encrypt certificates are valid for 90 days. Certbot automatically sets up a cron job for renewal. Test it:

Test Auto-Renewal
sudo certbot renew --dry-run

This should run without errors, confirming that auto-renewal is working.

3. Verify Deployment

After configuration, test your setup:

Test Commands
# Check if frontend loads
curl -I https://your-domain.com

# Test API endpoint
curl https://your-domain.com/api/courses

# Check SSL certificate
openssl s_client -connect your-domain.com:443 -servername your-domain.com 2>/dev/null | openssl x509 -noout -dates
Firewall Notes: Ensure your server firewall allows HTTP (80) and HTTPS (443) traffic:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload
        
Troubleshooting: If you get 404 errors for API routes, check that:
  • PM2 is running: pm2 list
  • Backend is listening on port 5000: curl http://localhost:5000/api/courses
  • Nginx proxy_pass path is correct (trailing slash matters)

Performance Tuning (Optional)

Optimize your TruLern installation for better performance, especially when handling large video files and high traffic.

1

Increase File Upload Limits

If you face errors uploading large video files (default limit is usually 1MB), update your Nginx configuration:

/etc/nginx/nginx.conf (inside http block)
http {
    ...
    client_max_body_size 500M;  # Allow uploads up to 500MB
    client_body_timeout 300s;    # Increase timeout for large uploads
    ...
}

Also update the proxy timeouts in your site configuration:

/etc/nginx/sites-available/trulern
location /api/ {
    proxy_pass http://localhost:5000;
    proxy_connect_timeout 300s;
    proxy_send_timeout 300s;
    proxy_read_timeout 300s;
}

After changes, reload Nginx: sudo systemctl reload nginx

2

Enable Gzip Compression

Compress text-based assets (HTML, CSS, JS, JSON) to reduce bandwidth and improve load times. Add to your Nginx config:

/etc/nginx/nginx.conf
http {
    ...
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/javascript
        application/json
        application/xml+rss
        application/x-javascript
        application/xml
        application/xhtml+xml
        application/rss+xml
        application/atom+xml
        font/truetype
        font/opentype
        image/svg+xml;
    ...
}
3

Browser Caching for Static Assets

Add cache headers to your Nginx configuration to leverage browser caching:

/etc/nginx/sites-available/trulern
# Cache static assets for 1 year
location /assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# Cache images for 30 days
location ~* \.(jpg|jpeg|png|gif|ico|webp|svg)$ {
    expires 30d;
    add_header Cache-Control "public, no-transform";
}

# Cache CSS/JS for 7 days
location ~* \.(css|js)$ {
    expires 7d;
    add_header Cache-Control "public, no-transform";
}
4

Node.js/PM2 Performance Tuning

Optimize your Node.js backend with cluster mode and memory limits:

backend/ecosystem.config.js
module.exports = {
    apps: [{
        name: 'trulern-api',
        script: 'server.js',
        instances: 'max',          // Use all CPU cores
        exec_mode: 'cluster',       // Enable cluster mode
        max_memory_restart: '1G',   // Restart if memory exceeds 1GB
        node_args: '--max-old-space-size=1024', // Node.js memory limit
        env: {
            NODE_ENV: 'production',
            PORT: 5000
        },
        error_file: './logs/err.log',
        out_file: './logs/out.log',
        combine_logs: true,
        time: true
    }]
};
5

MongoDB Performance

Ensure MongoDB indexes are properly set for frequently queried fields:

MongoDB Shell Commands
# Check existing indexes
db.courses.getIndexes()

# Create indexes for common queries
db.courses.createIndex({ status: 1, createdAt: -1 })
db.courses.createIndex({ instructor: 1, status: 1 })
db.courses.createIndex({ slug: 1 }, { unique: true })
db.courses.createIndex({ title: "text", description: "text" })

db.users.createIndex({ email: 1 }, { unique: true })
db.users.createIndex({ role: 1 })
db.users.createIndex({ 'enrolledCourses.course': 1 })
6

React Build Optimization

When building the React frontend, Vite automatically optimizes the bundle. You can further optimize with:

client/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
    plugins: [react()],
    build: {
        rollupOptions: {
            output: {
                manualChunks: {
                    vendor: ['react', 'react-dom', 'react-router-dom'],
                    stripe: ['@stripe/stripe-js', '@stripe/react-stripe-js'],
                    ui: ['react-select', '@tinymce/tinymce-react']
                }
            }
        },
        chunkSizeWarningLimit: 1000,
        minify: 'terser',
        terserOptions: {
            compress: {
                drop_console: true,  // Remove console.log in production
                drop_debugger: true
            }
        }
    }
});
7

CDN Integration (Optional)

For high-traffic sites, consider using a CDN like Cloudflare or Amazon CloudFront to serve static assets:

  • Cloudflare: Free CDN, SSL, and DDoS protection
  • Amazon CloudFront: Integrates with S3 for asset storage
  • KeyCDN: Simple pay-as-you-go CDN

With a CDN, update your asset URLs in the React build to point to the CDN endpoint.

Quick Wins:
  • Enable Gzip compression (can reduce transfer size by 70-80%)
  • Add cache headers for static assets
  • Use PM2 cluster mode to utilize all CPU cores
  • Create MongoDB indexes for frequently queried fields
Note: Always test performance changes in a staging environment first. Monitor server resources after each change.

Backup & Recovery

Data loss can happen due to server failure or accidental deletion. A robust backup strategy involves securing your Database (MongoDB) and your User Files (Uploads).

Critical Data Locations:
  • Database: All user accounts, courses, orders, and progress data
  • Uploaded Files: /var/www/trulern/client/public/uploads/ (avatars, thumbnails, videos, PDFs)

1. Database Backup (MongoDB)

1

Manual Backup (mongodump)

Use the standard MongoDB utility to create a snapshot of your data.

Terminal
# Syntax: mongodump --uri="YOUR_MONGO_URI" --out=/path/to/backup

# Example (local MongoDB):
mongodump --db=trulern --out=./backups/$(date +%F)

# Example (MongoDB Atlas):
mongodump --uri="mongodb+srv://username:password@cluster.mongodb.net/trulern" --out=./backups/$(date +%F)
2

Restoring Data (mongorestore)

To restore a database from a previous backup:

Terminal
# Syntax: mongorestore --uri="YOUR_MONGO_URI" /path/to/backup/folder

# Example (local):
mongorestore --db=trulern ./backups/2024-12-31/trulern

# Example (Atlas):
mongorestore --uri="mongodb+srv://username:password@cluster.mongodb.net/trulern" ./backups/2024-12-31/trulern

2. File Storage Backup

User-uploaded content (avatars, course thumbnails, lesson videos, exercise PDFs) lives in the file system. These must be backed up separately from the database.

Directory to Backup: /var/www/trulern/client/public/uploads/
Create Archive
# Navigate to project root
cd /var/www/trulern

# Create a tar archive of all uploads (preserves permissions)
tar -czf uploads-backup-$(date +%F).tar.gz client/public/uploads/

# Or create a zip archive
zip -r uploads-backup-$(date +%F).zip client/public/uploads/

3. Automating Backups (Cron Job)

You can automate this process using a simple shell script and Cron.

1

Create Backup Script

Create a file named /home/username/backup-trulern.sh:

backup-trulern.sh
#!/bin/bash

# Configuration
DATE=$(date +%Y-%m-%d)
BACKUP_DIR="/root/backups/trulern/$DATE"
PROJECT_ROOT="/var/www/trulern"
MONGO_URI="mongodb+srv://username:password@cluster.mongodb.net/trulern"  # Update this!

# Create backup directory
mkdir -p $BACKUP_DIR/{db,files}

echo "[$(date)] Starting TruLern backup..."

# 1. Backup Database
echo "Backing up MongoDB..."
mongodump --uri="$MONGO_URI" --out=$BACKUP_DIR/db &>/dev/null
if [ $? -eq 0 ]; then
    echo "✓ Database backup successful"
else
    echo "✗ Database backup FAILED"
fi

# 2. Backup Uploaded Files
echo "Backing up uploaded files..."
cp -r $PROJECT_ROOT/client/public/uploads $BACKUP_DIR/files/
if [ $? -eq 0 ]; then
    echo "✓ Files backup successful"
else
    echo "✗ Files backup FAILED"
fi

# 3. Create single archive
echo "Creating compressed archive..."
tar -czf /root/backups/trulern/trulern-full-backup-$DATE.tar.gz -C $BACKUP_DIR .

# 4. Remove raw backup directory (optional)
rm -rf $BACKUP_DIR

# 5. Keep only last 30 days of backups
find /root/backups/trulern -name "*.tar.gz" -mtime +30 -delete

echo "[$(date)] Backup completed"
echo "----------------------------------------"

Make the script executable:

chmod +x /home/username/backup-trulern.sh
2

Schedule with Crontab

Run crontab -e and add this line to back up every day at 3 AM:

0 3 * * * /bin/bash /home/username/backup-trulern.sh >> /var/log/trulern-backup.log 2>&1

This will:

  • Run daily at 3:00 AM
  • Execute the backup script
  • Log output to /var/log/trulern-backup.log
3

Test Your Backup

Always test that your backups are restorable:

# Extract a backup to test
cd /tmp
tar -xzf /root/backups/trulern/trulern-full-backup-2024-12-31.tar.gz

# Check database dump
ls db/trulern/

# Check files
ls files/
            

4. Off-site Backup (Optional but Recommended)

For disaster recovery, copy backups to a different location or cloud storage.

Upload to S3 (using AWS CLI)
# Install AWS CLI if not present
sudo apt install awscli

# Configure AWS credentials
aws configure

# Upload backup to S3
aws s3 cp /root/backups/trulern/trulern-full-backup-2024-12-31.tar.gz s3://your-bucket/backups/
Upload to Google Drive (using rclone)
# Install rclone
sudo apt install rclone

# Configure rclone for Google Drive
rclone config

# Upload backup
rclone copy /root/backups/trulern/trulern-full-backup-2024-12-31.tar.gz gdrive:trulern-backups/

5. Recovery Procedure

In case of data loss, follow these steps:

1

Restore Database

# Extract backup
cd /tmp
tar -xzf /path/to/backup/trulern-full-backup-2024-12-31.tar.gz

# Restore MongoDB
mongorestore --uri="mongodb+srv://username:password@cluster.mongodb.net/trulern" --drop db/
2

Restore Uploaded Files

# Stop the application (optional)
pm2 stop trulern-api

# Restore files
cp -r files/* /var/www/trulern/client/public/uploads/

# Fix permissions
sudo chown -R www-data:www-data /var/www/trulern/client/public/uploads/
sudo chmod -R 755 /var/www/trulern/client/public/uploads/

# Restart application
pm2 restart trulern-api
Best Practices:
  • Test your backups monthly by restoring to a staging environment
  • Keep at least 30 days of daily backups
  • Store backups in a different physical location or cloud
  • Document your backup and recovery procedures
  • Use MongoDB Atlas automated backups if using their service

Common Errors & Fixes

Before reaching out for support, please check if you are facing one of these common configuration issues.

Cause: The backend cannot connect to your MongoDB database.

Solution:

  1. Ensure MongoDB is running locally (mongod) or your Atlas IP whitelist allows your server's IP.
  2. Check your .env file: MONGO_URI=mongodb://localhost:27017/trulern (for local) or your Atlas URI.
  3. If using Atlas, ensure your password does not contain unescaped special characters like @ (use %40 instead).

Cause: Another process is already running on port 5000.

Solution:

  1. Kill the existing process: npx kill-port 5000
  2. Or change the PORT variable in your .env file to 5001.

Cause: The JWT_SECRET in your .env file has changed, invalidating old tokens.

Solution: Clear your browser's Local Storage (Application tab in DevTools) and log in again to generate a new token.

Cause: React frontend is trying to call the backend API from a different domain/port.

Solution:

  1. In development, ensure the VITE_API_URL in client/.env points to the correct backend URL:
    VITE_API_URL=http://localhost:5000/api
  2. In production, check your Nginx configuration to ensure API requests are properly proxied.
  3. Verify CORS is enabled in backend/server.js:
    app.use(cors({ origin: ['http://localhost:5173', 'https://your-domain.com'] }));

Cause: Missing dependency or incorrect import path.

Solution:

  1. Run npm install in the client/ directory to ensure all dependencies are installed.
  2. Check the import path in your component - it should be relative to the file location.
  3. If the error persists, delete node_modules and package-lock.json, then run npm install again.

Cause: React environment variables are not properly configured.

Solution:

  1. Ensure you have a .env file in the client/ directory (not backend/).
  2. All React environment variables must start with VITE_ (e.g., VITE_API_URL).
  3. Restart the dev server after creating/changing the .env file.
  4. Access variables using import.meta.env.VITE_VARIABLE_NAME (not process.env).

Cause: Syntax error or unsupported JavaScript feature in your code.

Solution:

  1. Check the terminal output for the specific file and line number where the error occurred.
  2. Ensure you're using supported JavaScript syntax (Vite supports modern ES features).
  3. Try building with npm run build -- --debug for more detailed error output.
  4. Clear Vite cache: rm -rf node_modules/.vite

Cause: React Router configuration issue or missing base path.

Solution:

  1. Open browser DevTools (F12) and check the Console tab for actual errors.
  2. Check Network tab to ensure all assets (JS, CSS) are loading correctly (no 404s).
  3. If assets are loading but page is blank, check React Router's basename prop if deployed in a subdirectory.
  4. Ensure Nginx is configured to serve index.html for all routes:
    location / {
        try_files $uri $uri/ /index.html;
    }

Cause: API URL is incorrect or Nginx proxy is misconfigured.

Solution:

  1. Check the API URL being used in your React component. Open DevTools Network tab to see the actual request URL.
  2. In development, ensure your VITE_API_URL ends with /api or your fetch calls include it:
    fetch(`${import.meta.env.VITE_API_URL}/courses`)
  3. In production, test the API directly: curl https://your-domain.com/api/courses
  4. Check Nginx configuration to ensure the location /api/ block is correctly set up.

Debugging Guide

If you encounter unexpected behavior, follow these steps to diagnose the issue.

1

Check Backend Logs

Look at your terminal (or PM2 logs) where the server is running. We have added extensive logging for payments and emails.

# If running with PM2
pm2 logs trulern-api

# If running directly
npm run dev  # in backend directory

# Example Success Log
✅ MongoDB connected successfully.
DEBUG: Connecting to: smtp.spacemail.com Port: 465
📧 Password reset email sent to user@example.com
💳 Payment verified for order: 12345

# Example Error Log
❌ Error sending password reset email: Invalid login
❌ Payment verification failed: Transaction not legit!
            
2

Check Frontend Console

Open browser DevTools (F12) > Console tab. Look for:

  • Red error messages about API calls (400, 401, 500)
  • Yellow warnings about deprecated features or missing dependencies
  • Blue info logs from React (strict mode, component mounts)
// Helpful console statements you can add
console.log('User data:', user);
console.log('API response:', data);
console.trace('Function called from:'); // Shows call stack
            
3

Verify Network Requests

In the Inspector, go to the Network tab:

  1. Click on the failing request (highlighted in red)
  2. Check the Preview or Response tab for the exact error message from the backend
  3. Check the Headers tab to verify:
    • Authorization header (should contain your JWT token)
    • Content-Type (should be application/json or multipart/form-data)
    • Request URL (should point to correct API endpoint)
// Example: Check if token is being sent
Headers: {
    'x-auth-token': 'eyJhbGciOiJIUzI1NiIs...',  // Should be present
    'Content-Type': 'application/json'
}
            
4

React DevTools

Install the React Developer Tools browser extension for advanced debugging:

  • Components tab: Inspect component props, state, and hooks
  • Profiler tab: Measure performance and find re-render issues
// Add debug prop to see component renders
const MyComponent = (props) => {
    console.log('MyComponent rendered', props);
    return <div>...</div>;
};

// Check if context is working
const auth = useAuth();
console.log('Auth context:', auth);  // Should contain user, token, etc.
            
5

Debug Authentication Issues

If you're having login/session problems:

// In browser console
// Check if token exists
console.log('Token:', localStorage.getItem('lmsToken'));

// Decode JWT to see expiration
const token = localStorage.getItem('lmsToken');
if (token) {
    const payload = JSON.parse(atob(token.split('.')[1]));
    console.log('Token payload:', payload);
    console.log('Expires:', new Date(payload.exp * 1000));
}

// Check user data
console.log('User:', JSON.parse(localStorage.getItem('lmsUser')));
            
6

Debug State with React Hooks

Use useEffect to track state changes:

useEffect(() => {
    console.log('cartItems changed:', cartItems);
}, [cartItems]);

useEffect(() => {
    console.log('Component mounted');
    return () => console.log('Component unmounted');
}, []);
            
7

Check Environment Variables

Verify that your environment variables are loaded correctly:

// In browser console (React)
console.log('API URL:', import.meta.env.VITE_API_URL);
console.log('Stripe Key:', import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY?.substring(0, 10) + '...');

// On server (Node.js)
console.log('NODE_ENV:', process.env.NODE_ENV);
console.log('MONGO_URI:', process.env.MONGO_URI?.substring(0, 20) + '...');
            
8

Test API Directly

Bypass the frontend and test your API with curl or Postman:

# Test public endpoint
curl http://localhost:5000/api/courses

# Test authenticated endpoint
curl -H "x-auth-token: YOUR_TOKEN" http://localhost:5000/api/user/profile

# Test file upload
curl -X POST -H "x-auth-token: YOUR_TOKEN" \
  -F "thumbnail=@image.jpg" \
  http://localhost:5000/api/instructor/courses
            
9

Check Browser Storage

Open Application tab in DevTools to inspect:

  • Local Storage: Should contain lmsToken, lmsUser, cart_items
  • Session Storage: (if used)
  • Cookies: (if using session cookies)
// Clear problematic data
localStorage.removeItem('lmsToken');
localStorage.removeItem('lmsUser');
localStorage.removeItem('cart_items');
// Then reload page
            
10

Debug Production Issues

When debugging in production:

  • Check Nginx error logs: sudo tail -f /var/log/nginx/error.log
  • Check PM2 logs: pm2 logs trulern-api --lines 100
  • Verify file permissions: ls -la client/public/uploads/
  • Check disk space: df -h
  • Check memory usage: free -m
Quick Debug Checklist:
  • ✓ Check browser console for errors
  • ✓ Check Network tab for failed API calls
  • ✓ Verify token in localStorage
  • ✓ Check backend logs (PM2 or terminal)
  • ✓ Test API directly with curl
  • ✓ Clear browser storage and reload

Payment Issues

Common causes and solutions:

  • API Keys:
    • Backend: Ensure STRIPE_SECRET_KEY in backend/.env starts with sk_test_ (test mode) or sk_live_ (live mode)
    • Frontend: Ensure VITE_STRIPE_PUBLISHABLE_KEY in client/.env starts with pk_test_ or pk_live_
    • Keys must match (both test or both live)
  • Currency Mismatch: Check if the currency code (USD/INR/EUR) matches your Stripe account settings
  • Payment Intent Error: Check backend logs for Stripe API errors:
    pm2 logs trulern-api | grep Stripe
  • Checkout Page Error: Open browser console (F12) and look for Stripe.js errors
  • Test Card: Use Stripe test card 4242 4242 4242 4242 with any future expiry and CVC

Common causes and solutions:

  • Invalid Client ID:
    • Backend: PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET in backend/.env must be correct
    • Ensure you are using a Sandbox Client ID for testing
    • Set PAYPAL_MODE=sandbox in backend/.env for testing
  • Missing PayPal SDK: Check if PayPal script is loading in browser console. Look for errors like "paypal is not defined"
  • CORS Issues: PayPal requires your domain to be whitelisted in your PayPal app settings
  • Order Creation Failed: Check backend logs:
    pm2 logs trulern-api | grep PayPal
  • Test Account: Use sandbox buyer account: sb-abc123@business.example.com / test123

Common causes and solutions:

  • Signature Verification Failed: This error means the payment signature doesn't match
    • Check that RAZORPAY_KEY_SECRET in backend/.env is correct
    • Ensure the key secret matches the key ID used in frontend
  • Key Mismatch:
    • Backend: RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET in backend/.env
    • Frontend: VITE_RAZORPAY_KEY_ID in client/.env must match the backend key ID
    • Both must be test keys (starting with rzp_test_) or both live keys
  • Amount in Paise: Razorpay expects amount in smallest currency unit (paise for INR). Check that you're multiplying by 100 correctly.
  • Check Browser Console: Look for Razorpay SDK loading errors
  • Test Cards:
    • Success: 4111 1111 1111 1111 with any future expiry
    • Failure: 1234 1234 1234 1234 (invalid card)

Payment succeeds but user not enrolled:

  • Check the enrollUser() function in backend/routes/paymentRoutes.js
  • Verify that order documents are being created in the database
  • Check MongoDB for the user's enrolledCourses array
  • Look for errors in the verification endpoint logs

Debug Steps for Any Payment Issue:

  1. Open browser DevTools (F12) → Network tab
  2. Look for the payment verification request (e.g., /api/payment/verify)
  3. Click on it and check the Response tab for the exact error message
  4. Check backend logs for the same request:
    pm2 logs trulern-api --lines 50
  5. Test the endpoint directly with curl:
    curl -X POST http://localhost:5000/api/payment/verify \
      -H "Content-Type: application/json" \
      -H "x-auth-token: YOUR_TOKEN" \
      -d '{"paymentId":"test","items":[]}'

Environment Variable Checklist:

# backend/.env
STRIPE_SECRET_KEY=sk_test_...      # or sk_live_...
PAYPAL_CLIENT_ID=AY...             # from developer dashboard
PAYPAL_CLIENT_SECRET=EC...         # from developer dashboard
RAZORPAY_KEY_ID=rzp_test_...       # or rzp_live_...
RAZORPAY_KEY_SECRET=...            # from Razorpay dashboard

# client/.env
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_...
VITE_RAZORPAY_KEY_ID=rzp_test_...
                    

Email Delivery Issues

TruLern uses nodemailer for sending announcements and password resets. The configuration is centralized in backend/emailService.js.

Debug Mode: When you start the server, check the console. It will print your connection details:
DEBUG: Connecting to: smtp.your-host.com Port: 465 User: your@email.com

Common SMTP Configurations

Configure these in your backend/.env file:

Environment Variable Description Example (Spacemail/Zoho)
EMAIL_HOST SMTP Server Address smtp.spacemail.com
EMAIL_PORT Port Number 465 (SSL) or 587 (TLS)
EMAIL_USER Your Email Address support@trulern.com
EMAIL_PASS Email Password Your secure password or app-specific password
Port 465 vs 587: The system automatically detects SSL requirements. If you set EMAIL_PORT=465, the system sets secure: true. For all other ports, it uses secure: false. Ensure your host supports the port you choose.

Testing Password Reset

If emails are not arriving:

  1. Check the server console for errors like EAUTH: Invalid login.
  2. Ensure your server can reach the external SMTP server (firewalls sometimes block port 465/587).
  3. Verify the "From" address in emailService.js matches your authenticated user.
  4. For Gmail, you may need to use an App Password instead of your regular password:
    • Enable 2-Factor Authentication on your Google account
    • Generate an App Password at: https://myaccount.google.com/apppasswords
    • Use that 16-character password in EMAIL_PASS
  5. Test your SMTP configuration manually:
    openssl s_client -connect smtp.gmail.com:465 -crlf -ign_eof
    # or using telnet (for port 587)
    telnet smtp.gmail.com 587
                

Common Email Provider Settings

Provider Host Port (SSL) Port (TLS) Notes
Gmail smtp.gmail.com 465 587 Requires App Password with 2FA
Zoho smtp.zoho.com 465 587 Use full email address as username
SendGrid smtp.sendgrid.net 465 587 Username is "apikey", password is your API key
Mailgun smtp.mailgun.org 465 587 Use your domain's SMTP credentials

Contact Support

Still stuck? We provide dedicated support for all our customers.

RainmakerLab Support

Official support channel via HubSpot

Please open a support ticket for technical assistance, bug reports, or feature requests. Include your purchase code for faster service.

Open Support Ticket

Support Policy

Support covers installation issues and bug fixes. It does not cover customization, adding new features, or server management/configuration beyond this documentation.


General Questions

No. TruLern is a modern MERN Stack application (MongoDB, Express, React, Node.js). It's a standalone full-stack application with a React frontend and Node.js backend. It offers significantly better performance, security, and user experience than WordPress, but requires a Node.js server environment (VPS or specialized hosting).

You can change the branding in two ways:

Option 1: Replace Image Files (Quick Method)

Replace the logo files in client/public/assets/images/logo/. Keep the same filenames (logo.png, logo-light.png) to avoid changing code.

Option 2: Update SCSS Variables (Recommended)

Change the primary color and other theme variables:

  1. Edit src/assets/scss/_variables.scss:
    $primary-color: #2f57ef;    /* Change this to your brand color */
    $secondary-color: #b12add;  /* Change secondary accent */
    $heading-color: #393939;    /* Heading text color */
    $body-color: #525252;       /* Paragraph text color */
  2. If you're running npm run dev, changes appear instantly. For production, rebuild with npm run build.

Yes, but only if your hosting plan includes "Setup Node.js App" in cPanel. Standard PHP hosting will not work because:

  • Frontend: Built with React and needs to be served as static files
  • Backend: Requires Node.js runtime to run the API server
  • Database: Uses MongoDB (not MySQL)

We highly recommend a VPS (DigitalOcean, Linode, AWS EC2) or specialized Node.js hosting platforms like:

  • VPS: DigitalOcean, Linode, Vultr (full control, best performance)
  • PaaS: Railway, Render, Heroku (easier setup)
  • Static + API: Frontend on Vercel/Netlify, backend on Railway/Render

It depends on what you want to change:

  • Colors and styling: No React knowledge needed. Just edit the SCSS variables in src/assets/scss/.
  • Content and text: No React knowledge needed. Text is in the JSX components, which use HTML-like syntax.
  • Layout changes: Basic HTML/CSS knowledge is enough for simple layout tweaks in the JSX.
  • Adding new features: Yes, you'll need React knowledge to add new components or modify functionality.

Technical Questions

TruLern uses React Router for client-side routing. To add a new page:

  1. Create a new React component in src/pages/ (e.g., Webinar.jsx)
  2. Add the route in src/App.jsx:
    // Import your component
    import Webinar from './pages/Webinar';
    
    // Add route inside the Layout wrapper
    <Route element={<Layout />}>
        <Route path="/webinar" element={<Webinar />} />
    </Route>
  3. The page will be available at /webinar

No backend changes are required as routing is handled entirely in the frontend.

Since this is a file-based delivery, follow these steps to update:

Before Updating:
  1. Backup your database: mongodump --uri="YOUR_MONGO_URI" --out=./backup
  2. Backup uploaded files: cp -r client/public/uploads ./uploads-backup
  3. Backup environment files: cp backend/.env backend.env.backup and cp client/.env client.env.backup
Update Process:
  1. Download the new version and extract it
  2. Copy your backed-up .env files to the new backend/ and client/ folders
  3. Copy your backed-up uploads/ folder to client/public/uploads/
  4. In the backend directory: npm install
  5. In the client directory: npm install
  6. Build the React frontend: cd client && npm run build
  7. Restart the backend: pm2 restart trulern-api

Email logic and HTML templates are centralized in backend/emailService.js. You can modify the HTML strings directly within the functions:

// Password reset email template
function sendPasswordResetEmail(email, userName, resetToken) {
    const resetUrl = `${process.env.APP_URL}/reset-password?token=${resetToken}`;
    
    const html = `
        <div style="font-family: Arial, sans-serif;">
            <h2>Password Reset Request</h2>
            <p>Hello ${userName},</p>
            <p>Click the button below to reset your password:</p>
            <a href="${resetUrl}" 
               style="background-color: #4CAF50; color: white; padding: 12px 24px; 
                      text-decoration: none; border-radius: 5px;">
                Reset Password
            </a>
        </div>
    `;
    
    // Send email...
}

You can customize the HTML, CSS, and text content of these emails. The reset link URL will automatically point to your React frontend's reset password page.

The API URL is controlled by environment variables:

  1. In client/.env, set:
    VITE_API_URL=https://your-domain.com/api
  2. If you're using a different port or subdomain:
    VITE_API_URL=https://api.your-domain.com
  3. After changing, rebuild the frontend:
    cd client && npm run build

The frontend will use this base URL for all API calls. In development, you can use http://localhost:5000/api.

Dark mode is handled by ModeContext.jsx and controlled by the theme switcher in the header. To customize dark mode colors:

  1. Edit src/assets/scss/_dark-mode.scss
  2. Find the dark mode color variables:
    .dark-mode {
        --bg-color: #1a1a1a;
        --text-color: #f0f0f0;
        --primary-color: #6c5ce7;
        // ... other variables
    }
  3. Modify the colors to match your branding

Best Practices

To ensure your LMS runs smoothly and scales effectively, follow these recommendations.

Image Optimization

Always compress images before uploading or adding them to the assets folder. Use WebP format where possible for faster loading times.

Process Management

Always use PM2 in production. Never run npm start or node server.js directly for a live site, as it won't restart on crashes or reboots.

Regular Backups

Schedule a cron job to backup your MongoDB database and the uploads/ folder. Data loss is irreversible without backups.

Version Control

Use Git to track your changes. If you modify core files, having a git history makes it much easier to merge future updates from us.

Security Tips

Critical: Do not skip these steps before going live.
1

Change Default Secrets

Your .env file contains sensitive keys. Ensure JWT_SECRET is a long, random string (e.g., 64 characters) unrelated to the demo data.

2

Use HTTPS

Never run an LMS dealing with payments and passwords over HTTP. Use Nginx with a free Let's Encrypt SSL certificate to encrypt all traffic.

3

Production Mode

Ensure NODE_ENV=production is set in your environment variables. This disables stack trace leakage to the client and enables performance optimizations in Express.

4

Database Access

If using MongoDB Atlas, whitelist only your VPS IP address. If using local MongoDB, ensure port 27017 is blocked from outside access using a firewall (UFW).


Sources & Credits

TruLern LMS uses these open-source libraries and assets. We're grateful to their creators.

Core Technologies
React Ecosystem
Vendor CSS & Plugins (from main.jsx)
Backend & Security
Fonts
Payment & Services
Demo Assets (Not Included)
Important Notice

Demo assets (Flaticon, Freepik, Pexels, Unsplash) are for preview only. These assets are not included in the download package. Buyers must replace them with their own properly licensed images and icons for production use.

Changelog

Version 1.0.0 - 14 Feb, 2026
  • Initial Release of TruLern LMS (v1.0.0).
  • Added 10 Unique Homepage Variations (Education, Marketplace, University, etc.).
  • Core LMS: Advanced Course Builder (Sections & Lessons).
  • Payments: Integrated Stripe, PayPal and Razorpay Payment Gateways with Secure Checkout.
  • Dashboards: Dedicated Student, Instructor, and Admin Dashboards.
  • Admin Features: Super Admin Panel with User Management (Ban/Approve), Category Manager, and Platform Analytics.
  • Shadow Mode: Special Admin view to preview the platform as any user (student/instructor) for debugging and support.
  • Learning Tools: Built-in Video Player, Reading Lesson & Quiz Management System.
  • Interactivity: Course Review & Rating System.
  • E-commerce: Shopping Cart, Wishlist, and Order History functionality.
  • Monetization: Instructor Payout Requests & Earning Reports.
  • Security: Secure JWT Authentication & Password Encryption.
  • Design: Fully Responsive Layout (Mobile/Tablet/Desktop) with Light/Dark Mode toggle.