Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node_modules/
*.db
*.db-journal
*.db-shm
*.db-wal
dist/
.env
backend/uploads/videos/*
backend/uploads/files/*
!backend/uploads/videos/.gitkeep
!backend/uploads/files/.gitkeep
112 changes: 110 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,110 @@
# Edus
-
# EduScale — Online Learning Platform MVP

A full-stack online learning platform built with Node.js + Express + SQLite (backend) and React + Vite (frontend).

## Features

- **Auth**: Register/Login with JWT, roles: `student`, `instructor`, `admin`
- **Courses**: Create, publish, search/filter course catalog
- **Video Streaming**: Upload & stream videos with Range request support, progress tracking
- **Enrollments**: Enroll/unenroll in courses
- **Assignments**: Create, submit, and grade assignments
- **Discussions**: Course discussion threads with replies
- **Announcements**: Instructor announcements per course
- **Quizzes**: Create quizzes with auto-grading
- **Certificates**: PDF certificates on 100% course completion
- **Admin Dashboard**: User management, role changes, platform stats

## Tech Stack

| Layer | Technology |
|-----------|------------------------------------|
| Backend | Node.js, Express, SQLite (better-sqlite3) |
| Auth | bcryptjs, jsonwebtoken |
| File Upload | multer |
| PDF | pdfkit |
| Frontend | React 18, React Router v6, Vite |
| HTTP | Axios |

## Project Structure

```
Edus/
├── backend/
│ ├── routes/ # API route handlers
│ ├── middleware/ # JWT auth middleware
│ ├── uploads/ # Uploaded files (videos, files)
│ ├── db.js # SQLite initialization
│ ├── schema.sql # Database schema
│ └── server.js # Express entry point
└── frontend/
└── src/
├── pages/ # React pages
├── components/ # Shared components
├── context/ # Auth context
├── api.js # Axios instance
└── index.css # Global styles
```

## Setup & Running

### Prerequisites
- Node.js v18+ (v20+ recommended)
- npm

### Backend

```bash
cd backend
npm install
npm start
# Server runs on http://localhost:5000
```

### Frontend

```bash
cd frontend
npm install
npm run dev
# App runs on http://localhost:3000
```

### Environment Variables

Backend `.env` (already created):
```
JWT_SECRET=eduscale_super_secret_jwt_key_2024
PORT=5000
FRONTEND_URL=http://localhost:3000
```

## API Endpoints

| Method | Path | Description |
|--------|------|-------------|
| POST | /api/register | Register user |
| POST | /api/login | Login |
| GET | /api/me | Current user |
| GET | /api/courses | List published courses |
| POST | /api/courses | Create course (instructor) |
| GET | /api/courses/:id | Course detail |
| POST | /api/enrollments | Enroll in course |
| POST | /api/videos/upload | Upload video |
| GET | /api/videos/:id | Stream video |
| PUT | /api/videos/:id/progress | Update watch progress |
| GET | /api/assignments/:courseId | List assignments |
| POST | /api/assignments/:id/submit | Submit assignment |
| GET | /api/discussions/:courseId | List discussions |
| POST | /api/quizzes/:id/attempt | Submit quiz |
| POST | /api/certificates/:courseId | Issue certificate |
| GET | /api/certificates/:courseId/download | Download PDF |
| GET | /api/admin/stats | Platform statistics (admin) |

## Creating an Admin User

Register normally, then update the role via SQLite or through an existing admin account:

```bash
sqlite3 backend/eduscale.db "UPDATE users SET role='admin' WHERE email='your@email.com';"
```
16 changes: 16 additions & 0 deletions backend/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');

const DB_PATH = path.join(__dirname, 'eduscale.db');
const SCHEMA_PATH = path.join(__dirname, 'schema.sql');

const db = new Database(DB_PATH);

db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');

const schema = fs.readFileSync(SCHEMA_PATH, 'utf8');
db.exec(schema);

module.exports = db;
54 changes: 54 additions & 0 deletions backend/middleware/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const jwt = require('jsonwebtoken');
const db = require('../db');

function getJwtSecret() {
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error('JWT_SECRET environment variable is required');
return secret;
}

function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(token, getJwtSecret());
const user = db.prepare('SELECT id, name, email, role FROM users WHERE id = ?').get(payload.id);
if (!user) return res.status(401).json({ error: 'User not found' });
req.user = user;
next();
} catch {
return res.status(401).json({ error: 'Invalid token' });
}
}

function optionalAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
req.user = null;
return next();
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(token, getJwtSecret());
const user = db.prepare('SELECT id, name, email, role FROM users WHERE id = ?').get(payload.id);
req.user = user || null;
} catch {
req.user = null;
}
next();
}

function requireRole(...roles) {
return (req, res, next) => {
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}

module.exports = { authenticate, optionalAuth, requireRole };
Loading