├── backend ├── .gitignore ├── pnpm-lock.yaml ├── requirements.txt ├── Dockerfile ├── docker-compose.yml ├── app │ ├── db │ │ ├── mongodb.py │ │ └── init_db.py │ ├── models │ │ ├── common.py │ │ ├── notification.py │ │ ├── attendance.py │ │ ├── certificate.py │ │ ├── material.py │ │ ├── assignment.py │ │ ├── user.py │ │ └── course.py │ ├── api │ │ ├── api_v1 │ │ │ └── api.py │ │ ├── deps.py │ │ └── v1 │ │ │ └── endpoints │ │ │ ├── notifications.py │ │ │ ├── users.py │ │ │ ├── students.py │ │ │ └── teachers.py │ ├── utils │ │ ├── file_upload.py │ │ ├── certificate_generator.py │ │ └── email.py │ └── core │ │ ├── security.py │ │ └── config.py ├── .env.example ├── .env └── main.py ├── frontend ├── client │ ├── src │ │ ├── context │ │ │ ├── AuthContext.jsx │ │ │ └── AuthContext.js │ │ ├── assets │ │ │ └── logo.png │ │ ├── setupTests.js │ │ ├── index.css │ │ ├── App.test.js │ │ ├── reportWebVitals.js │ │ ├── pages │ │ │ ├── LogoutPage.jsx │ │ │ ├── TeacherDashboard.jsx │ │ │ ├── teacher │ │ │ │ ├── Dashboard.jsx │ │ │ │ └── Courses.jsx │ │ │ ├── student │ │ │ │ ├── Dashboard.jsx │ │ │ │ ├── Certificates.jsx │ │ │ │ ├── Courses.jsx │ │ │ │ ├── Assignments.jsx │ │ │ │ ├── Progress.jsx │ │ │ │ ├── Support.jsx │ │ │ │ ├── Notifications.jsx │ │ │ │ └── CourseMaterials.jsx │ │ │ ├── Unauthorized.jsx │ │ │ └── StudentDashboard.jsx │ │ ├── App.css │ │ ├── components │ │ │ ├── ProtectedRoute.jsx │ │ │ ├── StudentHeader.jsx │ │ │ ├── TeacherHeader.jsx │ │ │ ├── TeacherSidebar.jsx │ │ │ └── StudentSidebar.jsx │ │ ├── index.js │ │ ├── logo.svg │ │ └── App.js │ ├── .env.development │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── index.html │ ├── tailwind.config.js │ ├── .gitignore │ ├── package.json │ └── README.md └── admin │ ├── .env.development │ ├── src │ ├── index.css │ ├── assets │ │ └── logo.png │ ├── setupTests.js │ ├── App.test.js │ ├── reportWebVitals.js │ ├── index.js │ ├── App.css │ ├── App.js │ ├── components │ │ ├── Header.jsx │ │ └── Sidebar.jsx │ ├── logo.svg │ ├── pages │ │ ├── AdminDashboard.jsx │ │ └── Login.jsx │ └── context │ │ └── AuthContext.js │ ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html │ ├── tailwind.config.js │ ├── .gitignore │ ├── config-overrides.js │ ├── package.json │ └── README.md └── README.md /backend/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /frontend/client/src/context/AuthContext.jsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/admin/.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=https://e-learning-lms-kas6.onrender.com 2 | -------------------------------------------------------------------------------- /frontend/admin/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /frontend/client/.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=https://e-learning-lms-kas6.onrender.com 2 | 3 | -------------------------------------------------------------------------------- /frontend/client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/admin/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/admin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mE-uMAr/E-learning-LMS/HEAD/frontend/admin/public/favicon.ico -------------------------------------------------------------------------------- /frontend/admin/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mE-uMAr/E-learning-LMS/HEAD/frontend/admin/public/logo192.png -------------------------------------------------------------------------------- /frontend/admin/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mE-uMAr/E-learning-LMS/HEAD/frontend/admin/public/logo512.png -------------------------------------------------------------------------------- /frontend/admin/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mE-uMAr/E-learning-LMS/HEAD/frontend/admin/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mE-uMAr/E-learning-LMS/HEAD/frontend/client/public/favicon.ico -------------------------------------------------------------------------------- /frontend/client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mE-uMAr/E-learning-LMS/HEAD/frontend/client/public/logo192.png -------------------------------------------------------------------------------- /frontend/client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mE-uMAr/E-learning-LMS/HEAD/frontend/client/public/logo512.png -------------------------------------------------------------------------------- /frontend/client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mE-uMAr/E-learning-LMS/HEAD/frontend/client/src/assets/logo.png -------------------------------------------------------------------------------- /backend/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | -------------------------------------------------------------------------------- /frontend/admin/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{js,jsx,ts,tsx}", 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } -------------------------------------------------------------------------------- /frontend/admin/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/client/src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | @layer base { 7 | body { 8 | @apply font-poppins; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/admin/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn==0.22.0 3 | motor==3.5.1 4 | pydantic==1.10.21 5 | pydantic_core==2.27.2 6 | pymongo==4.8.0 7 | python-jose==3.3.0 8 | passlib==1.7.4 9 | python-multipart==0.0.6 10 | email-validator==2.0.0 11 | python-dotenv==1.0.0 12 | bcrypt==4.0.1 13 | authlib 14 | httpx 15 | itsdangerous 16 | pillow 17 | -------------------------------------------------------------------------------- /frontend/client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'], 3 | theme: { 4 | extend: { 5 | fontFamily: { 6 | poppins: ['Poppins', 'sans-serif'], // Add Poppins as a custom font 7 | }, 8 | }, 9 | 10 | }, 11 | plugins: [], 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/client/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /frontend/admin/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/admin/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt . 6 | RUN pip install --no-cache-dir -r requirements.txt 7 | 8 | COPY . . 9 | 10 | # Create upload directory 11 | RUN mkdir -p uploads/profile_pictures uploads/course_thumbnails uploads/course_materials \ 12 | uploads/assignment_files uploads/assignment_submissions uploads/certificate_templates \ 13 | uploads/certificates 14 | 15 | CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] 16 | 17 | -------------------------------------------------------------------------------- /backend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | api: 5 | build: . 6 | ports: 7 | - "8000:8000" 8 | volumes: 9 | - ./uploads:/app/uploads 10 | environment: 11 | - MONGODB_URL=mongodb://mongo:27017 12 | depends_on: 13 | - mongo 14 | restart: always 15 | 16 | mongo: 17 | image: mongo:latest 18 | ports: 19 | - "27017:27017" 20 | volumes: 21 | - mongo_data:/data/db 22 | restart: always 23 | 24 | volumes: 25 | mongo_data: 26 | 27 | -------------------------------------------------------------------------------- /frontend/client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/admin/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /backend/app/db/mongodb.py: -------------------------------------------------------------------------------- 1 | import motor.motor_asyncio 2 | from app.core.config import settings 3 | import logging 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | client = motor.motor_asyncio.AsyncIOMotorClient(settings.MONGODB_URL) 8 | db = client[settings.DATABASE_NAME] 9 | 10 | async def connect_to_mongo(): 11 | try: 12 | await client.admin.command('ping') 13 | logger.info("Connected to MongoDB") 14 | except Exception as e: 15 | logger.error(f"Could not connect to MongoDB: {e}") 16 | raise 17 | 18 | async def close_mongo_connection(): 19 | client.close() 20 | logger.info("Closed MongoDB connection") 21 | 22 | -------------------------------------------------------------------------------- /frontend/client/src/pages/LogoutPage.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useAuth } from '../context/AuthContext'; // Adjust the import path 4 | 5 | const LogoutPage = () => { 6 | const { logout } = useAuth(); // Get the logout function from your AuthContext 7 | const navigate = useNavigate(); 8 | 9 | useEffect(() => { 10 | // Trigger logout and redirect to login 11 | logout(); 12 | navigate('/login'); // Optional: Redirect again if not already handled in `logout()` 13 | }, [logout, navigate]); 14 | 15 | return null; // No UI needed 16 | }; 17 | 18 | export default LogoutPage; -------------------------------------------------------------------------------- /backend/app/models/common.py: -------------------------------------------------------------------------------- 1 | from bson import ObjectId 2 | from pydantic import BaseModel, Field 3 | from typing import Optional, List, Any 4 | 5 | class PyObjectId(ObjectId): 6 | @classmethod 7 | def __get_validators__(cls): 8 | yield cls.validate 9 | 10 | @classmethod 11 | def validate(cls, v): 12 | if not ObjectId.is_valid(v): 13 | raise ValueError("Invalid ObjectId") 14 | return ObjectId(v) 15 | 16 | @classmethod 17 | def __modify_schema__(cls, field_schema): 18 | field_schema.update(type="string") 19 | 20 | class PaginatedResponse(BaseModel): 21 | total: int 22 | page: int 23 | page_size: int 24 | data: List[Any] 25 | 26 | -------------------------------------------------------------------------------- /frontend/admin/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | import { AuthProvider } from "./context/AuthContext"; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById("root")); 9 | root.render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | // If you want to start measuring performance in your app, pass a function 18 | // to log results (for example: reportWebVitals(console.log)) 19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 20 | reportWebVitals(); 21 | -------------------------------------------------------------------------------- /frontend/client/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/admin/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # API settings 2 | API_V1_STR=/api 3 | SECRET_KEY=your-secret-key-here 4 | ACCESS_TOKEN_EXPIRE_MINUTES=10080 # 7 days 5 | REFRESH_TOKEN_EXPIRE_MINUTES=43200 # 30 days 6 | 7 | # CORS settings 8 | BACKEND_CORS_ORIGINS=["http://localhost:3000"] 9 | 10 | # Database settings 11 | MONGODB_URL=mongodb://localhost:27017 12 | DATABASE_NAME=lms_db 13 | 14 | # Email settings 15 | SMTP_TLS=True 16 | SMTP_PORT=587 17 | SMTP_HOST=smtp.gmail.com 18 | SMTP_USER=your-email@gmail.com 19 | SMTP_PASSWORD=your-app-password 20 | EMAILS_FROM_EMAIL=your-email@gmail.com 21 | EMAILS_FROM_NAME=LMS System 22 | 23 | # Admin user 24 | FIRST_SUPERUSER_EMAIL=admin@example.com 25 | FIRST_SUPERUSER_PASSWORD=admin123 26 | 27 | # Upload settings 28 | UPLOAD_FOLDER=uploads 29 | MAX_UPLOAD_SIZE=20971520 # 20 MB 30 | 31 | -------------------------------------------------------------------------------- /frontend/admin/config-overrides.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = function override(config) { 4 | config.resolve.fallback = { 5 | ...config.resolve.fallback, 6 | "http": require.resolve("stream-http"), 7 | "https": require.resolve("https-browserify"), 8 | "util": require.resolve("util/"), 9 | "zlib": require.resolve("browserify-zlib"), 10 | "stream": require.resolve("stream-browserify"), 11 | "buffer": require.resolve("buffer"), 12 | "process": require.resolve("process/browser") // Keep this line 13 | }; 14 | 15 | config.plugins = [ 16 | ...config.plugins, 17 | new webpack.ProvidePlugin({ 18 | Buffer: ['buffer', 'Buffer'], 19 | process: 'process/browser', // Fix this line 20 | }), 21 | ]; 22 | 23 | return config; 24 | }; -------------------------------------------------------------------------------- /frontend/client/src/components/ProtectedRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Navigate, Outlet } from "react-router-dom"; 3 | import { useAuth } from "../context/AuthContext"; 4 | 5 | const ProtectedRoute = ({ allowedRoles, children }) => { 6 | const { user, role, loading } = useAuth(); 7 | 8 | if (loading) { 9 | return
Loading...
; // ✅ Prevents flashing before auth state loads 10 | } 11 | 12 | if (!user || !role) { 13 | return ; // ✅ Redirects if not logged in 14 | } 15 | 16 | if (!allowedRoles.includes(role)) { 17 | return ; // ✅ Redirects to the correct dashboard 18 | } 19 | 20 | return children ? children : ; // ✅ Ensures child components render 21 | }; 22 | 23 | export default ProtectedRoute; 24 | -------------------------------------------------------------------------------- /frontend/client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | import { BrowserRouter } from "react-router-dom"; 7 | import { AuthProvider } from "../src/context/AuthContext"; 8 | 9 | const root = ReactDOM.createRoot(document.getElementById("root")); 10 | root.render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | // If you want to start measuring performance in your app, pass a function 21 | // to log results (for example: reportWebVitals(console.log)) 22 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 23 | reportWebVitals(); 24 | -------------------------------------------------------------------------------- /frontend/admin/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom"; 3 | import Login from "./pages/Login"; // Ensure this path is correct 4 | import AdminDashboard from "./pages/AdminDashboard"; 5 | import { ToastContainer } from "react-toastify"; 6 | import "react-toastify/dist/ReactToastify.css"; 7 | 8 | const App = () => { 9 | return ( 10 | 11 | 12 | {/* Redirect root path to login page */} 13 | } /> 14 | } /> 15 | } /> 16 | {/* Add other routes here if needed */} 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | # API settings 2 | API_V1_STR=/api 3 | SECRET_KEY=your-secret-key-here 4 | ACCESS_TOKEN_EXPIRE_MINUTES=10080 # 7 days 5 | REFRESH_TOKEN_EXPIRE_MINUTES=43200 # 30 days 6 | 7 | # CORS settings 8 | BACKEND_CORS_ORIGINS=["lms.meharumar.codes","admin.lms.meharumar.codes"] 9 | 10 | # Database settings 11 | MONGODB_URL=mongodb+srv://memahr27:memahr27@cluster0.nkaxlm4.mongodb.net/lms_db?retryWrites=true&w=majority 12 | DATABASE_NAME=lms_db 13 | 14 | # Email settings 15 | SMTP_TLS=True 16 | SMTP_PORT=587 17 | SMTP_HOST=smtp.gmail.com 18 | SMTP_USER=codenova.tech.solution@gmail.com 19 | SMTP_PASSWORD= 20 | EMAILS_FROM_EMAIL=codenova.tech.solutio@gmail.com 21 | EMAILS_FROM_NAME=LMS System 22 | 23 | # Admin user 24 | FIRST_SUPERUSER_EMAIL=admin@example.com 25 | FIRST_SUPERUSER_PASSWORD=admin123 26 | 27 | # Upload settings 28 | UPLOAD_FOLDER=uploads 29 | MAX_UPLOAD_SIZE=20971520 # 20 MB 30 | 31 | #googlr login signup 32 | GOOGLE_CLIENT_ID=your-google-client-id 33 | GOOGLE_CLIENT_SECRET=your-google-client-secret 34 | GOOGLE_REDIRECT_URI= /auth/google/callback 35 | 36 | 37 | -------------------------------------------------------------------------------- /backend/app/db/init_db.py: -------------------------------------------------------------------------------- 1 | from app.db.mongodb import db 2 | from app.core.security import get_password_hash 3 | from app.core.config import settings 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | async def create_first_superuser(): 9 | """Create a superuser if it doesn't exist.""" 10 | try: 11 | # Check if admin user exists 12 | admin_user = await db.users.find_one({"email": settings.FIRST_SUPERUSER_EMAIL}) 13 | 14 | if not admin_user: 15 | user_data = { 16 | "email": settings.FIRST_SUPERUSER_EMAIL, 17 | "username": "admin", 18 | "hashed_password": get_password_hash(settings.FIRST_SUPERUSER_PASSWORD), 19 | "role": "admin", 20 | "is_active": True, 21 | "is_superuser": True 22 | } 23 | 24 | await db.users.insert_one(user_data) 25 | logger.info("Superuser created") 26 | else: 27 | logger.info("Superuser already exists") 28 | except Exception as e: 29 | logger.error(f"Error creating superuser: {e}") 30 | 31 | -------------------------------------------------------------------------------- /backend/app/models/notification.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from pydantic import BaseModel, Field 3 | from datetime import datetime 4 | from bson import ObjectId 5 | from app.models.common import PyObjectId 6 | 7 | class NotificationBase(BaseModel): 8 | title: str 9 | message: str 10 | type: str # assignment, material, announcement, grade, feedback 11 | 12 | class Config: 13 | orm_mode = True 14 | 15 | class NotificationCreate(NotificationBase): 16 | recipient_id: str 17 | course_id: Optional[str] = None 18 | 19 | class Config: 20 | orm_mode = True 21 | 22 | class Notification(NotificationBase): 23 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 24 | recipient_id: PyObjectId 25 | sender_id: Optional[PyObjectId] = None 26 | course_id: Optional[PyObjectId] = None 27 | read: bool = False 28 | created_at: datetime = Field(default_factory=datetime.utcnow) 29 | 30 | class Config: 31 | orm_mode = True 32 | allow_population_by_field_name = True 33 | json_encoders = { 34 | ObjectId: str 35 | } 36 | 37 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from app.api.v1.endpoints import ( 3 | auth, 4 | users, 5 | courses, 6 | assignments, 7 | attendance, 8 | materials, 9 | notifications, 10 | certificates, 11 | students, 12 | teachers 13 | ) 14 | 15 | api_router = APIRouter() 16 | 17 | api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"]) 18 | api_router.include_router(users.router, prefix="/users", tags=["Users"]) 19 | api_router.include_router(courses.router, prefix="/courses", tags=["Courses"]) 20 | api_router.include_router(assignments.router, prefix="/assignments", tags=["Assignments"]) 21 | api_router.include_router(attendance.router, prefix="/attendance", tags=["Attendance"]) 22 | api_router.include_router(materials.router, prefix="/materials", tags=["Materials"]) 23 | api_router.include_router(notifications.router, prefix="/notifications", tags=["Notifications"]) 24 | api_router.include_router(certificates.router, prefix="/certificates", tags=["Certificates"]) 25 | api_router.include_router(students.router, prefix="/students", tags=["Students"]) 26 | api_router.include_router(teachers.router, prefix="/teachers", tags=["Teachers"]) 27 | 28 | -------------------------------------------------------------------------------- /frontend/client/src/pages/TeacherDashboard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Outlet } from "react-router-dom"; 3 | import TeacherHeader from "../components/TeacherHeader"; 4 | import TeacherSidebar from "../components/TeacherSidebar"; 5 | 6 | const TeacherDashboard = () => { 7 | // Mock data for notifications 8 | const notifications = [ 9 | { id: 1, type: 'message', text: 'New message from student', time: '10m ago' }, 10 | { id: 2, type: 'alert', text: 'Assignment submissions ready for review', time: '1h ago' }, 11 | ]; 12 | 13 | return ( 14 |
15 | {/* Fixed Header */} 16 | 20 | 21 | {/* Main content with fixed sidebar */} 22 |
23 | {/* Fixed width sidebar */} 24 |
25 | 26 |
27 | 28 | {/* Main content area */} 29 |
30 | 31 |
32 |
33 |
34 | ); 35 | }; 36 | 37 | export default TeacherDashboard; -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import FastAPI 3 | from fastapi.middleware.cors import CORSMiddleware 4 | from app.core.config import settings 5 | from app.api.api_v1.api import api_router 6 | from app.db.init_db import create_first_superuser 7 | from fastapi.staticfiles import StaticFiles 8 | import os 9 | from starlette.middleware.sessions import SessionMiddleware 10 | 11 | app = FastAPI( 12 | title=settings.PROJECT_NAME, 13 | openapi_url=f"{settings.API_V1_STR}/openapi.json" 14 | ) 15 | 16 | # Set up CORS 17 | app.add_middleware( 18 | CORSMiddleware, 19 | allow_origins=["*"], 20 | allow_credentials=True, 21 | allow_methods=["*"], 22 | allow_headers=["*"], 23 | ) 24 | app.add_middleware(SessionMiddleware, secret_key=os.getenv("SECRET_KEY")) 25 | 26 | # Include API router 27 | app.include_router(api_router, prefix=settings.API_V1_STR) 28 | 29 | # Mount static files directory for uploads 30 | os.makedirs("uploads", exist_ok=True) 31 | app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads") 32 | 33 | @app.on_event("startup") 34 | async def startup_event(): 35 | await create_first_superuser() 36 | 37 | if __name__ == "__main__": 38 | uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True) 39 | 40 | -------------------------------------------------------------------------------- /backend/app/utils/file_upload.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | from fastapi import UploadFile 4 | from app.core.config import settings 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | async def save_upload(upload_file: UploadFile, folder: str) -> str: 10 | """ 11 | Save an uploaded file to the specified folder. 12 | Returns the relative path to the saved file. 13 | """ 14 | try: 15 | # Create folder if it doesn't exist 16 | folder_path = os.path.join(settings.UPLOAD_FOLDER, folder) 17 | os.makedirs(folder_path, exist_ok=True) 18 | 19 | # Generate unique filename 20 | file_extension = os.path.splitext(upload_file.filename)[1] 21 | unique_filename = f"{uuid.uuid4()}{file_extension}" 22 | 23 | # Save file 24 | file_path = os.path.join(folder_path, unique_filename) 25 | 26 | # Read file content 27 | contents = await upload_file.read() 28 | 29 | # Write to file 30 | with open(file_path, "wb") as f: 31 | f.write(contents) 32 | 33 | # Return relative path 34 | return os.path.join(folder, unique_filename) 35 | 36 | except Exception as e: 37 | logger.error(f"Error saving file: {e}") 38 | raise e 39 | 40 | -------------------------------------------------------------------------------- /frontend/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/dom": "^10.4.0", 7 | "@testing-library/jest-dom": "^6.6.3", 8 | "@testing-library/react": "^16.2.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^1.8.1", 11 | "crypto": "^1.0.1", 12 | "crypto-js": "^4.2.0", 13 | "framer-motion": "^12.4.7", 14 | "jwt-decode": "^4.0.0", 15 | "object-assign": "^4.1.1", 16 | "react": "^19.0.0", 17 | "react-dom": "^19.0.0", 18 | "react-icons": "^5.5.0", 19 | "react-router-dom": "^7.2.0", 20 | "react-scripts": "5.0.1", 21 | "react-toastify": "^11.0.5", 22 | "recharts": "^2.15.1", 23 | "web-vitals": "^2.1.4" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest" 35 | ] 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | }, 49 | "devDependencies": { 50 | "autoprefixer": "^10.4.20", 51 | "postcss": "^8.5.3", 52 | "tailwindcss": "^3.4.17" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /backend/app/models/attendance.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from pydantic import BaseModel, Field 3 | from datetime import datetime, date 4 | from bson import ObjectId 5 | from app.models.common import PyObjectId 6 | 7 | class AttendanceRecord(BaseModel): 8 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 9 | course_id: PyObjectId 10 | student_id: PyObjectId 11 | date: date 12 | status: str # Present, Absent, Late, Excused 13 | time: Optional[str] = None 14 | note: Optional[str] = None 15 | recorded_by: PyObjectId # teacher_id 16 | created_at: datetime = Field(default_factory=datetime.utcnow) 17 | 18 | class Config: 19 | orm_mode = True 20 | allow_population_by_field_name = True 21 | json_encoders = { 22 | ObjectId: str 23 | } 24 | 25 | class AttendanceCreate(BaseModel): 26 | course_id: str 27 | student_id: str 28 | date: date 29 | status: str 30 | time: Optional[str] = None 31 | note: Optional[str] = None 32 | 33 | class Config: 34 | orm_mode = True 35 | 36 | class AttendanceUpdate(BaseModel): 37 | status: Optional[str] = None 38 | time: Optional[str] = None 39 | note: Optional[str] = None 40 | 41 | class Config: 42 | orm_mode = True 43 | 44 | class AttendanceBulkCreate(BaseModel): 45 | course_id: str 46 | date: date 47 | records: List[dict] # List of {student_id, status, time, note} 48 | 49 | class Config: 50 | orm_mode = True 51 | 52 | -------------------------------------------------------------------------------- /backend/app/core/security.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Any, Optional, Union 3 | from jose import jwt 4 | from passlib.context import CryptContext 5 | from app.core.config import settings 6 | 7 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 8 | 9 | def create_access_token(subject: Union[str, Any], expires_delta: Optional[timedelta] = None) -> str: 10 | """Create JWT access token""" 11 | if expires_delta: 12 | expire = datetime.utcnow() + expires_delta 13 | else: 14 | expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) 15 | 16 | to_encode = {"exp": expire, "sub": str(subject), "type": "access"} 17 | encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM) 18 | return encoded_jwt 19 | 20 | def create_refresh_token(subject: Union[str, Any]) -> str: 21 | """Create JWT refresh token""" 22 | expire = datetime.utcnow() + timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES) 23 | to_encode = {"exp": expire, "sub": str(subject), "type": "refresh"} 24 | encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM) 25 | return encoded_jwt 26 | 27 | def verify_password(plain_password: str, hashed_password: str) -> bool: 28 | """Verify password against hash""" 29 | return pwd_context.verify(plain_password, hashed_password) 30 | 31 | def get_password_hash(password: str) -> str: 32 | """Hash password""" 33 | return pwd_context.hash(password) 34 | 35 | -------------------------------------------------------------------------------- /backend/app/models/certificate.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from pydantic import BaseModel, Field 3 | from datetime import datetime 4 | from bson import ObjectId 5 | from app.models.common import PyObjectId 6 | 7 | class CertificateBase(BaseModel): 8 | title: str 9 | description: Optional[str] = None 10 | 11 | class Config: 12 | orm_mode = True 13 | 14 | class CertificateCreate(CertificateBase): 15 | course_id: str 16 | 17 | class Config: 18 | orm_mode = True 19 | 20 | class Certificate(CertificateBase): 21 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 22 | course_id: PyObjectId 23 | template: Optional[str] = None 24 | created_at: datetime = Field(default_factory=datetime.utcnow) 25 | updated_at: datetime = Field(default_factory=datetime.utcnow) 26 | 27 | class Config: 28 | orm_mode = True 29 | allow_population_by_field_name = True 30 | json_encoders = { 31 | ObjectId: str 32 | } 33 | 34 | class StudentCertificate(BaseModel): 35 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 36 | certificate_id: PyObjectId 37 | student_id: PyObjectId 38 | course_id: PyObjectId 39 | issue_date: datetime = Field(default_factory=datetime.utcnow) 40 | completion_date: datetime 41 | credential_id: str 42 | certificate_url: Optional[str] = None 43 | status: str = "Available" # Available, Pending 44 | 45 | class Config: 46 | orm_mode = True 47 | allow_population_by_field_name = True 48 | json_encoders = { 49 | ObjectId: str 50 | } 51 | 52 | -------------------------------------------------------------------------------- /frontend/admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/dom": "^10.4.0", 7 | "@testing-library/jest-dom": "^6.6.3", 8 | "@testing-library/react": "^16.2.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^1.8.1", 11 | "browserify-zlib": "^0.2.0", 12 | "crypto": "^1.0.1", 13 | "crypto-js": "^4.2.0", 14 | "https-browserify": "^1.0.0", 15 | "jwt-decode": "^4.0.0", 16 | "process": "^0.11.10", 17 | "react": "^19.0.0", 18 | "react-dom": "^19.0.0", 19 | "react-icons": "^5.5.0", 20 | "react-router-dom": "^7.2.0", 21 | "react-scripts": "5.0.1", 22 | "react-toastify": "^11.0.5", 23 | "stream-browserify": "^3.0.0", 24 | "stream-http": "^3.2.0", 25 | "util": "^0.12.5", 26 | "web-vitals": "^2.1.4" 27 | }, 28 | "scripts": { 29 | "start": "set PORT=3001 && react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app", 37 | "react-app/jest" 38 | ] 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "devDependencies": { 53 | "tailwindcss": "^3.4.17" 54 | }, 55 | "browser": { 56 | "crypto": false 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /backend/app/models/material.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from pydantic import BaseModel, Field 3 | from datetime import datetime 4 | from bson import ObjectId 5 | from app.models.common import PyObjectId 6 | 7 | class MaterialBase(BaseModel): 8 | title: str 9 | type: str # document, video, link 10 | 11 | class Config: 12 | orm_mode = True 13 | 14 | class MaterialCreate(MaterialBase): 15 | course_id: str 16 | module_id: Optional[str] = None 17 | description: Optional[str] = None 18 | url: Optional[str] = None 19 | 20 | class Config: 21 | orm_mode = True 22 | 23 | class MaterialUpdate(BaseModel): 24 | title: Optional[str] = None 25 | description: Optional[str] = None 26 | url: Optional[str] = None 27 | 28 | class Config: 29 | orm_mode = True 30 | 31 | class Material(MaterialBase): 32 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 33 | course_id: PyObjectId 34 | module_id: Optional[PyObjectId] = None 35 | description: Optional[str] = None 36 | file_path: Optional[str] = None 37 | url: Optional[str] = None 38 | format: Optional[str] = None # PDF, MP4, etc. 39 | size: Optional[str] = None 40 | duration: Optional[str] = None # For videos 41 | uploaded_by: PyObjectId # teacher_id 42 | access_count: int = 0 43 | created_at: datetime = Field(default_factory=datetime.utcnow) 44 | updated_at: datetime = Field(default_factory=datetime.utcnow) 45 | 46 | class Config: 47 | orm_mode = True 48 | allow_population_by_field_name = True 49 | json_encoders = { 50 | ObjectId: str 51 | } 52 | 53 | -------------------------------------------------------------------------------- /frontend/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/admin/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /backend/app/core/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import secrets 3 | from typing import Any, Dict, List, Optional, Union 4 | from pydantic import AnyHttpUrl, BaseSettings, EmailStr, validator 5 | 6 | class Settings(BaseSettings): 7 | API_V1_STR: str = "/api" 8 | SECRET_KEY: str = secrets.token_urlsafe(32) 9 | ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 8 days 10 | REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 30 # 30 days 11 | 12 | # CORS settings 13 | BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = ["http://localhost:3000","http://localhost:3001"] 14 | 15 | @validator("BACKEND_CORS_ORIGINS", pre=True) 16 | def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: 17 | if isinstance(v, str) and not v.startswith("["): 18 | return [i.strip() for i in v.split(",")] 19 | elif isinstance(v, (list, str)): 20 | return v 21 | raise ValueError(v) 22 | 23 | # Database settings 24 | MONGODB_URL: str = "mongodb://localhost:27017" 25 | DATABASE_NAME: str = "lms_db" 26 | 27 | # Email settings 28 | SMTP_TLS: bool = True 29 | SMTP_PORT: Optional[int] = 587 30 | SMTP_HOST: Optional[str] = "smtp.gmail.com" 31 | SMTP_USER: Optional[str] = "" 32 | SMTP_PASSWORD: Optional[str] = "" 33 | EMAILS_FROM_EMAIL: Optional[EmailStr] = "info@example.com" 34 | EMAILS_FROM_NAME: Optional[str] = "LMS System" 35 | 36 | # Admin user 37 | FIRST_SUPERUSER_EMAIL: EmailStr = "admin@example.com" 38 | FIRST_SUPERUSER_PASSWORD: str = "admin123" 39 | 40 | # Upload settings 41 | UPLOAD_FOLDER: str = os.path.join(os.getcwd(), "uploads") 42 | MAX_UPLOAD_SIZE: int = 50 * 1024 * 1024 # 50 MB 43 | 44 | # JWT settings 45 | JWT_ALGORITHM: str = "HS256" 46 | 47 | # Project name 48 | PROJECT_NAME: str = "Learning Management System API" 49 | 50 | class Config: 51 | case_sensitive = True 52 | env_file = ".env" 53 | 54 | settings = Settings() 55 | 56 | -------------------------------------------------------------------------------- /backend/app/models/assignment.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from pydantic import BaseModel, Field 3 | from datetime import datetime 4 | from bson import ObjectId 5 | from app.models.common import PyObjectId 6 | 7 | class AssignmentBase(BaseModel): 8 | title: str 9 | description: Optional[str] = None 10 | deadline: datetime 11 | 12 | class Config: 13 | orm_mode = True 14 | 15 | class AssignmentCreate(AssignmentBase): 16 | courseId: str 17 | courseName: str 18 | 19 | class AssignmentUpdate(BaseModel): 20 | title: Optional[str] = None 21 | description: Optional[str] = None 22 | deadline: Optional[datetime] = None 23 | 24 | class Config: 25 | orm_mode = True 26 | 27 | class Assignment(AssignmentBase): 28 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 29 | course: PyObjectId 30 | courseName: str 31 | teacher_id: PyObjectId 32 | attachmentFile: Optional[str] = None 33 | created_at: datetime = Field(default_factory=datetime.utcnow) 34 | updated_at: datetime = Field(default_factory=datetime.utcnow) 35 | 36 | class Config: 37 | orm_mode = True 38 | allow_population_by_field_name = True 39 | json_encoders = { 40 | ObjectId: str 41 | } 42 | 43 | class AssignmentSubmission(BaseModel): 44 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 45 | assignment_id: PyObjectId 46 | student_id: PyObjectId 47 | submission_file: Optional[str] = None 48 | submission_text: Optional[str] = None 49 | submitted_at: datetime = Field(default_factory=datetime.utcnow) 50 | status: str = "Submitted" # Submitted, Graded, Late 51 | score: Optional[float] = None 52 | feedback: Optional[str] = None 53 | graded_at: Optional[datetime] = None 54 | 55 | class Config: 56 | orm_mode = True 57 | allow_population_by_field_name = True 58 | json_encoders = { 59 | ObjectId: str 60 | } 61 | 62 | -------------------------------------------------------------------------------- /backend/app/models/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from pydantic import BaseModel, EmailStr, Field 3 | from datetime import datetime 4 | from bson import ObjectId 5 | from app.models.common import PyObjectId 6 | 7 | class UserBase(BaseModel): 8 | email: EmailStr 9 | username: str 10 | role: str = "student" # student, teacher, admin 11 | is_active: bool = True 12 | 13 | class Config: 14 | orm_mode = True 15 | 16 | class UserCreate(UserBase): 17 | password: str 18 | 19 | class UserUpdate(BaseModel): 20 | email: Optional[EmailStr] = None 21 | username: Optional[str] = None 22 | password: Optional[str] = None 23 | is_active: Optional[bool] = None 24 | 25 | class Config: 26 | orm_mode = True 27 | 28 | class User(UserBase): 29 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 30 | hashed_password: str 31 | is_superuser: bool = False 32 | created_at: datetime = Field(default_factory=datetime.utcnow) 33 | updated_at: datetime = Field(default_factory=datetime.utcnow) 34 | 35 | class Config: 36 | orm_mode = True 37 | allow_population_by_field_name = True 38 | json_encoders = { 39 | ObjectId: str 40 | } 41 | 42 | class UserInDB(User): 43 | hashed_password: str 44 | 45 | class UserProfile(BaseModel): 46 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 47 | user_id: PyObjectId 48 | full_name: Optional[str] = None 49 | bio: Optional[str] = None 50 | profile_picture: Optional[str] = None 51 | phone: Optional[str] = None 52 | address: Optional[str] = None 53 | 54 | class Config: 55 | orm_mode = True 56 | allow_population_by_field_name = True 57 | json_encoders = { 58 | ObjectId: str 59 | } 60 | 61 | class TeacherProfile(UserProfile): 62 | department: Optional[str] = None 63 | position: Optional[str] = None 64 | office: Optional[str] = None 65 | 66 | class Config: 67 | orm_mode = True 68 | 69 | class StudentProfile(UserProfile): 70 | enrollment_date: Optional[datetime] = None 71 | student_id: Optional[str] = None 72 | 73 | class Config: 74 | orm_mode = True 75 | 76 | -------------------------------------------------------------------------------- /frontend/admin/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FiBell, FiSearch, FiMenu, FiX } from "react-icons/fi"; 3 | import Logo from "../assets/logo.png"; 4 | 5 | const Header = ({ toggleSidebar, isSidebarOpen, notifications }) => { 6 | return ( 7 |
8 |
9 | 15 | EduLearn 16 |
17 | 18 |
19 |
20 |
21 | 22 | 27 |
28 |
29 | 30 |
31 | 37 |
38 | 39 |
40 | 46 |
47 |
48 |
49 | ); 50 | }; 51 | 52 | export default Header; -------------------------------------------------------------------------------- /frontend/client/src/pages/teacher/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FiUsers, FiBook, FiClipboard, FiCalendar, FiSettings, FiFileText } from "react-icons/fi"; 3 | import { Link } from "react-router-dom"; 4 | 5 | const Dashboard = ({ teacherName, courses, upcomingClasses, pendingAssignments }) => { 6 | const teacherRoutes = [ 7 | { name: "Courses", path: "/teacher-dashboard/courses", icon: }, 8 | { name: "Students", path: "/teacher-dashboard/students", icon: }, 9 | { name: "Assignments", path: "/teacher-dashboard/assignments", icon: }, 10 | { name: "Attendance", path: "/teacher-dashboard/attendance", icon: }, 11 | { name: "Settings", path: "/teacher-dashboard/settings", icon: }, 12 | ]; 13 | 14 | return ( 15 |
16 |
17 |

Welcome back, {teacherName}!

18 |

Manage your courses, students, and assignments efficiently with your personalized LMS dashboard.

19 | 20 |
21 | {teacherRoutes.map((route, index) => ( 22 | 27 |
28 | {route.icon} 29 |
30 |

{route.name}

31 |

Quick access to {route.name.toLowerCase()}.

32 | 33 | ))} 34 |
35 |
36 |
37 | ); 38 | }; 39 | 40 | export default Dashboard; -------------------------------------------------------------------------------- /frontend/admin/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/client/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/client/src/components/StudentHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FiBell, FiMail, FiLogOut } from "react-icons/fi" 3 | import Logo from "../assets/logo.png" 4 | import { useNavigate } from "react-router-dom" 5 | import { useAuth } from "../context/AuthContext" 6 | const StudentHeader = ({ notifications = [], studentName = "Guest" }) => { 7 | const navigate = useNavigate() 8 | const { logout } = useAuth() 9 | 10 | const handleLogout = async () => { 11 | try { 12 | await logout() 13 | navigate("/login") 14 | } catch (error) { 15 | console.error("Logout failed:", error) 16 | } 17 | } 18 | return ( 19 |
20 |
21 | EduLearn 22 |
23 | 24 |
25 | {/* Notification Icon */} 26 |
27 | 35 |
36 | 37 | {/* User Profile Icon */} 38 |
39 | 47 |
48 |
49 | 57 |
58 |
59 |
60 | ); 61 | }; 62 | 63 | export default StudentHeader; 64 | -------------------------------------------------------------------------------- /frontend/client/src/pages/student/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FiBook, FiClipboard, FiCalendar, FiAward, FiBell, FiUser, FiFileText, FiHeadphones } from "react-icons/fi"; 3 | import { Link } from "react-router-dom"; 4 | 5 | const StudentDashboard = ({ studentName }) => { 6 | const studentRoutes = [ 7 | { name: "Courses", path: "/student-dashboard/courses", icon: }, 8 | { name: "New Courses", path: "/student-dashboard/new-courses", icon: }, 9 | { name: "Assignments", path: "/student-dashboard/assignments", icon: }, 10 | { name: "Certificates", path: "/student-dashboard/certificates", icon: }, 11 | { name: "Attendance", path: "/student-dashboard/attendance", icon: }, 12 | { name: "Notifications", path: "/student-dashboard/notifications", icon: }, 13 | { name: "Support", path: "/student-dashboard/support", icon: }, 14 | ]; 15 | 16 | return ( 17 |
18 |
19 |

Welcome back, {studentName}!

20 |

Explore your courses, track progress, and stay updated with your student dashboard.

21 | 22 |
23 | {studentRoutes.map((route, index) => ( 24 | 29 |
30 | {route.icon} 31 |
32 |

{route.name}

33 |

Quick access to {route.name.toLowerCase()}.

34 | 35 | ))} 36 |
37 |
38 |
39 | ); 40 | }; 41 | 42 | export default StudentDashboard; 43 | -------------------------------------------------------------------------------- /frontend/client/src/components/TeacherHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FiBell, FiLogOut } from "react-icons/fi"; 3 | import Logo from "../assets/logo.png"; 4 | import { useNavigate } from "react-router-dom"; 5 | import { useAuth } from "../context/AuthContext"; 6 | 7 | const TeacherHeader = ({ notifications = [] }) => { 8 | const navigate = useNavigate(); 9 | const { user, logout } = useAuth(); // ✅ Get `user` from AuthContext 10 | 11 | const handleLogout = async () => { 12 | try { 13 | await logout(); 14 | navigate("/login"); 15 | } catch (error) { 16 | console.error("Logout failed:", error); 17 | } 18 | }; 19 | 20 | // ✅ Ensure `teacherName` is correctly derived from `user` 21 | const teacherName = user?.username || "Teacher"; 22 | 23 | return ( 24 |
25 |
26 | EduLearn 27 |
28 | 29 |
30 | {/* Notification Icon */} 31 |
32 | 40 |
41 | 42 | {/* User Profile Icon */} 43 |
44 | 52 |
53 | 54 | {/* Logout Button */} 55 |
56 | 64 |
65 |
66 |
67 | ); 68 | }; 69 | 70 | export default TeacherHeader; 71 | -------------------------------------------------------------------------------- /frontend/admin/src/pages/AdminDashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Routes, Route, useNavigate } from "react-router-dom"; 3 | import { FiMenu, FiX } from "react-icons/fi"; 4 | import axios from "axios"; 5 | import Header from "../components/Header"; 6 | import Sidebar from "../components/Sidebar"; 7 | import Overview from "./dashboard/Overview"; 8 | import Courses from "./dashboard/Courses"; 9 | import Scheduling from "./dashboard/Scheduling"; 10 | import Students from "./dashboard/Students"; 11 | import Teachers from "./dashboard/Teachers"; 12 | import Communications from "./dashboard/Communications"; 13 | import Attendance from "./dashboard/Attendance"; 14 | import Certifications from "./dashboard/Certifications"; 15 | import Settings from "./dashboard/Settings"; 16 | 17 | const AdminDashboard = () => { 18 | const [isSidebarOpen, setIsSidebarOpen] = useState(true); 19 | const [notifications, setNotifications] = useState([]); 20 | const navigate = useNavigate(); 21 | 22 | useEffect(() => { 23 | const fetchNotifications = async () => { 24 | try { 25 | const token = localStorage.getItem("accessToken"); 26 | const res = await axios.get("http://localhost:8000/api/notifications", { 27 | withCredentials: true, 28 | },{Headers: { 29 | Authorization: `Bearer ${token}`, 30 | }}); 31 | setNotifications(res.data.notifications); 32 | } catch (error) { 33 | console.error("Error fetching notifications:", error); 34 | } 35 | }; 36 | 37 | fetchNotifications(); 38 | }, []); 39 | 40 | const toggleSidebar = () => { 41 | setIsSidebarOpen(!isSidebarOpen); 42 | }; 43 | 44 | return ( 45 |
46 |
51 |
52 | 53 |
54 |
55 | 56 | } /> 57 | } /> 58 | } /> 59 | } /> 60 | } /> 61 | } /> 62 | } /> 63 | } /> 64 | } /> 65 | 66 |
67 |
68 |
69 |
70 | ); 71 | }; 72 | 73 | export default AdminDashboard; 74 | -------------------------------------------------------------------------------- /frontend/client/src/pages/Unauthorized.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useNavigate } from "react-router-dom" 3 | import { useAuth } from "../context/AuthContext" 4 | 5 | const Unauthorized = () => { 6 | const navigate = useNavigate() 7 | const { logout, role } = useAuth() 8 | 9 | const handleLogout = async () => { 10 | await logout() 11 | navigate("/login") 12 | } 13 | 14 | const handleGoBack = () => { 15 | // Redirect based on role or go back to previous page 16 | if (role === "student") { 17 | navigate("/student-dashboard") 18 | } else if (role === "teacher") { 19 | navigate("/teacher-dashboard") 20 | } else { 21 | navigate(-1) 22 | } 23 | } 24 | 25 | return ( 26 |
27 |
28 |
29 | {/* Error Icon - using emoji instead of lucide icon */} 30 |
31 | 🛡️ 32 |
33 | 34 | {/* Error Title */} 35 |

Unauthorized Access

36 | 37 | {/* Alert Icon and Message - using emoji instead of lucide icon */} 38 |
39 | ⚠️ 40 |

You don't have permission to access this page

41 |
42 | 43 | {/* Error Description */} 44 |

45 | It seems you're trying to access a page that requires different permissions. Please contact your 46 | administrator if you believe this is an error. 47 |

48 | 49 | {/* Action Buttons */} 50 |
51 | 57 | 58 | 65 |
66 |
67 |
68 | 69 | {/* Footer */} 70 |
71 | If you need assistance, please contact support 72 |
73 |
74 | ) 75 | } 76 | 77 | export default Unauthorized 78 | 79 | -------------------------------------------------------------------------------- /backend/app/api/deps.py: -------------------------------------------------------------------------------- 1 | from typing import Generator, Optional 2 | from fastapi import Depends, HTTPException, status 3 | from fastapi.security import OAuth2PasswordBearer 4 | from jose import jwt, JWTError 5 | from pydantic import ValidationError 6 | from app.core.config import settings 7 | from app.models.user import User 8 | from app.db.mongodb import db 9 | from bson import ObjectId 10 | import logging 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login") 15 | 16 | async def get_current_user(token: str = Depends(oauth2_scheme)) -> User: 17 | """Get current user from token""" 18 | try: 19 | payload = jwt.decode( 20 | token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM] 21 | ) 22 | token_type = payload.get("type") 23 | if token_type != "access": 24 | raise HTTPException( 25 | status_code=status.HTTP_401_UNAUTHORIZED, 26 | detail="Invalid token type", 27 | headers={"WWW-Authenticate": "Bearer"}, 28 | ) 29 | user_id = payload.get("sub") 30 | if not user_id: 31 | raise HTTPException( 32 | status_code=status.HTTP_401_UNAUTHORIZED, 33 | detail="Could not validate credentials", 34 | headers={"WWW-Authenticate": "Bearer"}, 35 | ) 36 | except (JWTError, ValidationError) as e: 37 | logger.error(f"Token validation error: {e}") 38 | raise HTTPException( 39 | status_code=status.HTTP_401_UNAUTHORIZED, 40 | detail="Could not validate credentials", 41 | headers={"WWW-Authenticate": "Bearer"}, 42 | ) 43 | 44 | user = await db.users.find_one({"_id": ObjectId(user_id)}) 45 | if not user: 46 | raise HTTPException( 47 | status_code=status.HTTP_404_NOT_FOUND, 48 | detail="User not found" 49 | ) 50 | 51 | return User(**user) 52 | 53 | async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User: 54 | """Check if current user is active""" 55 | if not current_user.is_active: 56 | raise HTTPException( 57 | status_code=status.HTTP_400_BAD_REQUEST, 58 | detail="Inactive user" 59 | ) 60 | return current_user 61 | 62 | async def get_current_teacher(current_user: User = Depends(get_current_active_user)) -> User: 63 | """Check if current user is a teacher""" 64 | if current_user.role != "teacher" and not current_user.is_superuser: 65 | raise HTTPException( 66 | status_code=status.HTTP_403_FORBIDDEN, 67 | detail="Not enough permissions" 68 | ) 69 | return current_user 70 | 71 | async def get_current_student(current_user: User = Depends(get_current_active_user)) -> User: 72 | """Check if current user is a student""" 73 | if current_user.role != "student" and not current_user.is_superuser: 74 | raise HTTPException( 75 | status_code=status.HTTP_403_FORBIDDEN, 76 | detail="Not enough permissions" 77 | ) 78 | return current_user 79 | 80 | async def get_current_superuser(current_user: User = Depends(get_current_active_user)) -> User: 81 | """Check if current user is a superuser""" 82 | if not current_user.is_superuser: 83 | raise HTTPException( 84 | status_code=status.HTTP_403_FORBIDDEN, 85 | detail="Not enough permissions" 86 | ) 87 | return current_user 88 | 89 | -------------------------------------------------------------------------------- /frontend/client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /backend/app/models/course.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from pydantic import BaseModel, Field 3 | from datetime import datetime 4 | from bson import ObjectId 5 | from app.models.common import PyObjectId 6 | 7 | class CourseBase(BaseModel): 8 | courseName: str 9 | courseCode: str 10 | description: str 11 | category: str 12 | duration: int # in weeks 13 | price: float 14 | maxStudents: int 15 | difficulty: str = "beginner" # beginner, intermediate, advanced 16 | instructorName: str 17 | 18 | class Config: 19 | orm_mode = True 20 | 21 | class CourseCreate(CourseBase): 22 | pass 23 | 24 | class CourseUpdate(BaseModel): 25 | courseName: Optional[str] = None 26 | courseCode: Optional[str] = None 27 | description: Optional[str] = None 28 | category: Optional[str] = None 29 | duration: Optional[int] = None 30 | price: Optional[float] = None 31 | maxStudents: Optional[int] = None 32 | difficulty: Optional[str] = None 33 | instructorName: Optional[str] = None 34 | 35 | class Config: 36 | orm_mode = True 37 | 38 | class Course(CourseBase): 39 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 40 | teacher_id: PyObjectId 41 | thumbnail: Optional[str] = None 42 | enrollmentStatus: str = "Open" # Open, Closed, Full 43 | studentsEnrolled: int = 0 44 | hasModules: bool = False 45 | hasQuizzes: bool = False 46 | certificateOffered: bool = False 47 | certificateTitle: Optional[str] = None 48 | certificateDescription: Optional[str] = None 49 | created_at: datetime = Field(default_factory=datetime.utcnow) 50 | updated_at: datetime = Field(default_factory=datetime.utcnow) 51 | 52 | class Config: 53 | orm_mode = True 54 | allow_population_by_field_name = True 55 | json_encoders = { 56 | ObjectId: str 57 | } 58 | 59 | class Module(BaseModel): 60 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 61 | course_id: PyObjectId 62 | title: str 63 | description: Optional[str] = None 64 | order: int 65 | created_at: datetime = Field(default_factory=datetime.utcnow) 66 | 67 | class Config: 68 | orm_mode = True 69 | allow_population_by_field_name = True 70 | json_encoders = { 71 | ObjectId: str 72 | } 73 | 74 | class Lesson(BaseModel): 75 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 76 | module_id: PyObjectId 77 | title: str 78 | description: Optional[str] = None 79 | duration: str 80 | materialType: Optional[str] = None # video, pdf, link, none 81 | materialUrl: Optional[str] = None 82 | materialFile: Optional[str] = None 83 | order: int 84 | created_at: datetime = Field(default_factory=datetime.utcnow) 85 | 86 | class Config: 87 | orm_mode = True 88 | allow_population_by_field_name = True 89 | json_encoders = { 90 | ObjectId: str 91 | } 92 | 93 | class Enrollment(BaseModel): 94 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 95 | course_id: PyObjectId 96 | student_id: PyObjectId 97 | enrollment_date: datetime = Field(default_factory=datetime.utcnow) 98 | progress: int = 0 99 | status: str = "Active" # Active, Completed, Dropped 100 | 101 | class Config: 102 | orm_mode = True 103 | allow_population_by_field_name = True 104 | json_encoders = { 105 | ObjectId: str 106 | } 107 | 108 | -------------------------------------------------------------------------------- /frontend/admin/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | pushed 5 | 6 | ## Available Scripts 7 | 8 | pushedddd 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.\ 14 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 15 | 16 | The page will reload when you make changes.\ 17 | You may also see any lint errors in the console. 18 | 19 | ### `npm test` 20 | 21 | Launches the test runner in the interactive watch mode.\ 22 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 23 | 24 | ### `npm run build` 25 | 26 | Builds the app for production to the `build` folder.\ 27 | It correctly bundles React in production mode and optimizes the build for the best performance. 28 | 29 | The build is minified and the filenames include the hashes.\ 30 | Your app is ready to be deployed! 31 | 32 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 33 | 34 | ### `npm run eject` 35 | 36 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 37 | 38 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 39 | 40 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 41 | 42 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 43 | 44 | ## Learn More 45 | 46 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 47 | 48 | To learn React, check out the [React documentation](https://reactjs.org/). 49 | 50 | ### Code Splitting 51 | 52 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 53 | 54 | ### Analyzing the Bundle Size 55 | 56 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 57 | 58 | ### Making a Progressive Web App 59 | 60 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 61 | 62 | ### Advanced Configuration 63 | 64 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 65 | 66 | ### Deployment 67 | 68 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 69 | 70 | ### `npm run build` fails to minify 71 | 72 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 73 | -------------------------------------------------------------------------------- /frontend/client/src/pages/StudentDashboard.jsx: -------------------------------------------------------------------------------- 1 | // import React from "react"; 2 | // import { Routes, Route } from "react-router-dom"; 3 | // import StudentHeader from "../components/StudentHeader"; 4 | // import StudentSidebar from "../components/StudentSidebar"; 5 | // import Dashboard from "./student/Dashboard"; 6 | // import Courses from "./student/Courses"; 7 | // import NewCourses from "./student/NewCourses"; 8 | // import Progress from "./student/Progress"; 9 | // import Certificates from "./student/Certificates"; 10 | // import Attendance from "./student/Attendance"; 11 | // import Notifications from "./student/Notifications"; 12 | // import Support from "./student/Support"; 13 | 14 | // const StudentDashboard = () => { 15 | // const userName = localStorage.getItem("userName"); 16 | 17 | // // Mock notifications to avoid undefined issues 18 | // const notifications = [ 19 | // { 20 | // id: 1, 21 | // title: "Assignment Due", 22 | // message: "React Components assignment due tomorrow", 23 | // time: "2 hours ago", 24 | // }, 25 | // { 26 | // id: 2, 27 | // title: "New Material Available", 28 | // message: "New lecture notes uploaded for JavaScript course", 29 | // time: "Yesterday", 30 | // }, 31 | // ]; 32 | 33 | // return ( 34 | //
35 | // 36 | 37 | //
38 | // 39 | 40 | //
41 | // 42 | // } /> 43 | // } /> 44 | // } /> 45 | // } /> 46 | // } /> 47 | // } /> 48 | // } /> 49 | // } /> 50 | // 51 | //
52 | //
53 | //
54 | // ); 55 | // }; 56 | 57 | // export default StudentDashboard; 58 | 59 | import React from "react"; 60 | import { Outlet } from "react-router-dom"; 61 | import StudentHeader from "../components/StudentHeader"; 62 | import StudentSidebar from "../components/StudentSidebar"; 63 | 64 | const StudentDashboard = () => { 65 | const userName = localStorage.getItem("userName"); 66 | 67 | // Mock notifications to avoid undefined issues 68 | const notifications = [ 69 | { 70 | id: 1, 71 | title: "Assignment Due", 72 | message: "React Components assignment due tomorrow", 73 | time: "2 hours ago", 74 | }, 75 | { 76 | id: 2, 77 | title: "New Material Available", 78 | message: "New lecture notes uploaded for JavaScript course", 79 | time: "Yesterday", 80 | }, 81 | ]; 82 | 83 | return ( 84 |
85 | 86 | 87 |
88 | 89 | 90 | {/* Render nested routes dynamically */} 91 |
92 | {/* ✅ This dynamically renders the selected sub-route */} 93 |
94 |
95 |
96 | ); 97 | }; 98 | 99 | export default StudentDashboard; 100 | -------------------------------------------------------------------------------- /frontend/client/src/pages/student/Certificates.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { FiDownload, FiShare2 } from "react-icons/fi"; 3 | import axios from "axios"; 4 | 5 | const Certificates = () => { 6 | const [certificates, setCertificates] = useState([]); 7 | const [isLoading, setIsLoading] = useState(true); 8 | const BASE_URL = process.env.REACT_APP_API_URL || "http://localhost:8000"; 9 | 10 | useEffect(() => { 11 | fetchCertificates(); 12 | }, []); 13 | 14 | const fetchCertificates = async () => { 15 | setIsLoading(true); 16 | try { 17 | const token = localStorage.getItem("accessToken"); 18 | const response = await axios.get(`${BASE_URL}/api/certificates/student`, { 19 | headers: { Authorization: `Bearer ${token}` }, 20 | }); 21 | setCertificates(response.data.certificates || []); 22 | } catch (error) { 23 | console.error("Error fetching certificates:", error); 24 | } finally { 25 | setIsLoading(false); 26 | } 27 | }; 28 | 29 | return ( 30 |
31 |

My Certificates

32 |
33 |
34 |

Earned Certificates

35 |
36 | {isLoading ? ( 37 |

Loading certificates...

38 | ) : certificates.length > 0 ? ( 39 |
40 | {certificates.map((certificate) => ( 41 |
42 |
43 |
44 |

{certificate.title}

45 |

Instructor: {certificate.course.instructor}

46 |
47 |

ID: {certificate.credential_id}

48 |

Completed: {certificate.completion_date}

49 |
50 |
51 |
52 | 53 | Available 54 | 55 |
56 | 57 | 58 | Download 59 | 60 | 64 |
65 |
66 |
67 |
68 | ))} 69 |
70 | ) : ( 71 |
72 |

No certificates yet

73 |

Complete a course to earn your first certificate!

74 |
75 | )} 76 |
77 |
78 | ); 79 | }; 80 | 81 | export default Certificates; 82 | -------------------------------------------------------------------------------- /backend/app/utils/certificate_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | from PIL import Image, ImageDraw, ImageFont 4 | from app.core.config import settings 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | async def generate_certificate( 10 | student_name: str, 11 | course_name: str, 12 | certificate_title: str, 13 | instructor_name: str, 14 | issue_date: datetime, 15 | credential_id: str, 16 | template_path: str = None 17 | ) -> str: 18 | """ 19 | Generate a certificate image and save it. 20 | Returns the path to the generated certificate. 21 | Uses a default template for all certificates. 22 | """ 23 | try: 24 | # Create certificates folder if it doesn't exist 25 | certificates_folder = os.path.join(settings.UPLOAD_FOLDER, "certificates") 26 | os.makedirs(certificates_folder, exist_ok=True) 27 | 28 | # Generate unique filename 29 | filename = f"{credential_id}.png" 30 | file_path = os.path.join(certificates_folder, filename) 31 | 32 | # Check for default template 33 | default_template_path = os.path.join(settings.UPLOAD_FOLDER, "certificate_templates", "default_template.png") 34 | 35 | # Use default template if it exists, otherwise create a blank certificate 36 | if os.path.exists(default_template_path): 37 | img = Image.open(default_template_path) 38 | else: 39 | # Create a blank certificate 40 | img = Image.new('RGB', (1200, 900), color=(255, 255, 255)) 41 | draw = ImageDraw.Draw(img) 42 | 43 | # Add border 44 | draw.rectangle([(20, 20), (1180, 880)], outline=(25, 164, 219), width=5) 45 | 46 | # Add text to the certificate 47 | draw = ImageDraw.Draw(img) 48 | 49 | # Try to load fonts, fall back to default if not available 50 | try: 51 | header_font = ImageFont.truetype("arial.ttf", 60) 52 | title_font = ImageFont.truetype("arial.ttf", 40) 53 | name_font = ImageFont.truetype("arial.ttf", 50) 54 | course_font = ImageFont.truetype("arial.ttf", 30) 55 | details_font = ImageFont.truetype("arial.ttf", 25) 56 | id_font = ImageFont.truetype("arial.ttf", 20) 57 | except IOError: 58 | header_font = ImageFont.load_default() 59 | title_font = ImageFont.load_default() 60 | name_font = ImageFont.load_default() 61 | course_font = ImageFont.load_default() 62 | details_font = ImageFont.load_default() 63 | id_font = ImageFont.load_default() 64 | 65 | # Add certificate header 66 | draw.text((600, 100), "Certificate of Completion", fill=(25, 164, 219), font=header_font, anchor="mm") 67 | 68 | # Add certificate title 69 | draw.text((600, 200), certificate_title, fill=(0, 0, 0), font=title_font, anchor="mm") 70 | 71 | # Add student name 72 | draw.text((600, 350), student_name, fill=(0, 0, 0), font=name_font, anchor="mm") 73 | 74 | # Add course name 75 | draw.text((600, 450), f"has successfully completed the course", fill=(0, 0, 0), font=course_font, anchor="mm") 76 | draw.text((600, 500), course_name, fill=(0, 0, 0), font=course_font, anchor="mm") 77 | 78 | # Add date and instructor 79 | draw.text((300, 650), f"Issue Date: {issue_date.strftime('%B %d, %Y')}", fill=(0, 0, 0), font=details_font, anchor="mm") 80 | draw.text((900, 650), f"Instructor: {instructor_name}", fill=(0, 0, 0), font=details_font, anchor="mm") 81 | 82 | # Add credential ID 83 | draw.text((600, 800), f"Credential ID: {credential_id}", fill=(0, 0, 0), font=id_font, anchor="mm") 84 | 85 | # Save the certificate 86 | img.save(file_path) 87 | 88 | # Return relative path 89 | return os.path.join("certificates", filename) 90 | 91 | except Exception as e: 92 | logger.error(f"Error generating certificate: {e}") 93 | raise e 94 | 95 | -------------------------------------------------------------------------------- /backend/app/utils/email.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import smtplib 3 | from email.mime.text import MIMEText 4 | from email.mime.multipart import MIMEMultipart 5 | from app.core.config import settings 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | async def send_email( 10 | email_to: str, 11 | subject: str, 12 | html_content: str, 13 | text_content: str = None 14 | ) -> None: 15 | """ 16 | Send email using SMTP. 17 | """ 18 | if not settings.SMTP_HOST or not settings.SMTP_PORT: 19 | logger.warning("SMTP settings not configured, skipping email") 20 | return 21 | 22 | message = MIMEMultipart("alternative") 23 | message["Subject"] = subject 24 | message["From"] = f"{settings.EMAILS_FROM_NAME} <{settings.EMAILS_FROM_EMAIL}>" 25 | message["To"] = email_to 26 | 27 | if text_content: 28 | message.attach(MIMEText(text_content, "plain")) 29 | 30 | message.attach(MIMEText(html_content, "html")) 31 | 32 | try: 33 | with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT) as server: 34 | if settings.SMTP_TLS: 35 | server.starttls() 36 | if settings.SMTP_USER and settings.SMTP_PASSWORD: 37 | server.login(settings.SMTP_USER, settings.SMTP_PASSWORD) 38 | server.sendmail( 39 | settings.EMAILS_FROM_EMAIL, 40 | email_to, 41 | message.as_string() 42 | ) 43 | logger.info(f"Email sent to {email_to}") 44 | except Exception as e: 45 | logger.error(f"Failed to send email to {email_to}: {e}") 46 | raise 47 | 48 | async def send_reset_password_email(email: str, otp: str) -> None: 49 | """ 50 | Send password reset email with OTP. 51 | """ 52 | subject = "Password Reset Request" 53 | 54 | html_content = f""" 55 | 56 | 57 |

Password Reset Request

58 |

You have requested to reset your password. Use the following OTP code to verify your identity:

59 |

{otp}

60 |

This code will expire in 15 minutes.

61 |

If you did not request a password reset, please ignore this email.

62 | 63 | 64 | """ 65 | 66 | text_content = f""" 67 | Password Reset Request 68 | 69 | You have requested to reset your password. Use the following OTP code to verify your identity: 70 | 71 | {otp} 72 | 73 | This code will expire in 15 minutes. 74 | 75 | If you did not request a password reset, please ignore this email. 76 | """ 77 | 78 | await send_email( 79 | email_to=email, 80 | subject=subject, 81 | html_content=html_content, 82 | text_content=text_content 83 | ) 84 | 85 | async def send_verification_email(email: str) -> None: 86 | """ 87 | Send email verification link. 88 | """ 89 | subject = "Verify Your Email Address" 90 | 91 | html_content = f""" 92 | 93 | 94 |

Welcome to the Learning Management System!

95 |

Thank you for signing up. Please verify your email address by clicking the link below:

96 |

Verify Email

97 |

If you did not sign up for an account, please ignore this email.

98 | 99 | 100 | """ 101 | 102 | text_content = f""" 103 | Welcome to the Learning Management System! 104 | 105 | Thank you for signing up. Please verify your email address by visiting the link below: 106 | 107 | http://localhost:3000/verify-email?email={email} 108 | 109 | If you did not sign up for an account, please ignore this email. 110 | """ 111 | 112 | await send_email( 113 | email_to=email, 114 | subject=subject, 115 | html_content=html_content, 116 | text_content=text_content 117 | ) 118 | 119 | -------------------------------------------------------------------------------- /frontend/client/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Routes, Route, Navigate } from "react-router-dom"; 3 | import { ToastContainer } from "react-toastify"; 4 | import "react-toastify/dist/ReactToastify.css"; 5 | 6 | import Login from "./pages/Login"; 7 | import Signup from "./pages/SignUp"; 8 | import StudentDashboard from "./pages/StudentDashboard"; 9 | import TeacherDashboard from "./pages/TeacherDashboard"; 10 | import ForgotPassword from "./pages/ForgotPassword"; 11 | import ProtectedRoute from "./components/ProtectedRoute"; 12 | 13 | // Teacher Components 14 | import CourseManagement from "./pages/teacher/components/ManageCourse"; 15 | import Dashboard from "./pages/teacher/Dashboard"; 16 | import Courses from "./pages/teacher/Courses"; 17 | import Students from "./pages/teacher/Students"; 18 | import Assignments from "./pages/teacher/Assignments"; 19 | import Attendance from "./pages/teacher/Attendance"; 20 | import Grades from "./pages/teacher/Grades"; 21 | import Materials from "./pages/teacher/Materials"; 22 | import Settings from "./pages/teacher/Settings"; 23 | 24 | // Student Components 25 | import StdDashboard from "./pages/student/Dashboard"; 26 | import StudentCourses from "./pages/student/Courses"; 27 | import NewCourses from "./pages/student/NewCourses"; 28 | import Progress from "./pages/student/Progress"; 29 | import Certificates from "./pages/student/Certificates"; 30 | import StudentAttendance from "./pages/student/Attendance"; 31 | import Notifications from "./pages/student/Notifications"; 32 | import Support from "./pages/student/Support"; 33 | import CourseMaterials from "./pages/student/CourseMaterials"; 34 | import LogoutPage from './pages/LogoutPage'; 35 | import StudentAssignments from "./pages/student/Assignments"; 36 | 37 | function App() { 38 | return ( 39 | <> 40 | 41 | {/* Other routes */} 42 | } /> 43 | {/* Redirect root to login */} 44 | } /> 45 | } /> 46 | } /> 47 | } /> 48 | 49 | {/* Protected Student Routes */} 50 | 54 | 55 | 56 | } 57 | > 58 | } /> 59 | } /> 60 | } /> 61 | } /> 62 | } /> 63 | } /> 64 | } /> 65 | } /> 66 | } /> 67 | } /> 68 | 69 | 70 | {/* Protected Teacher Routes */} 71 | 75 | 76 | 77 | } 78 | > 79 | } /> 80 | } /> 81 | } 84 | /> 85 | } /> 86 | } /> 87 | } /> 88 | } /> 89 | } /> 90 | } /> 91 | 92 | 93 | 94 | 95 | 96 | ); 97 | } 98 | 99 | export default App; 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | 4 | # E-Learning LMS Platform 5 | 6 | An advanced Learning Management System (LMS) built with **FastAPI** on the backend and **React** on the frontend. This platform supports three types of users—**Admin**, **Teacher**, and **Student**—and provides comprehensive features for course management, assignments, attendance tracking, grading, and more. 7 | 8 | --- 9 | 10 | ## Features 11 | 12 | ### User Roles 13 | 1. **Admin** 14 | - Manage users (create, update, delete). 15 | - Oversee courses and assignments. 16 | - Access system-wide reports and analytics. 17 | 2. **Teacher** 18 | - Create and manage courses. 19 | - Create, assign, and grade assignments. 20 | - Mark and track student attendance. 21 | - Review student performance reports. 22 | 3. **Student** 23 | - Enroll in courses. 24 | - Submit assignments. 25 | - View grades and feedback. 26 | - Monitor attendance and progress. 27 | 28 | ### Core Functionalities 29 | - **Course Management**: Create, update, and delete courses; assign teachers and students. 30 | - **Assignment System**: Enable teachers to create assignments and grade student submissions. 31 | - **Attendance Tracking**: Allow teachers to mark and monitor student attendance. 32 | - **Grading System**: Facilitate the assignment of grades for assignments and exams, with a clear interface for student feedback. 33 | - **User Authentication**: Secure login with role-based access control. 34 | - **Reports and Analytics**: Generate detailed reports on attendance, grades, and overall performance. 35 | - **Certificates**: Automatically generate and issue certificates to students upon course completion. 36 | 37 | --- 38 | 39 | ## Tech Stack 40 | 41 | ### Backend 42 | - **FastAPI**: A modern, fast (high-performance) web framework for building APIs. 43 | - **Database**: MongoDB 44 | - **Authentication**: JWT-based authentication for secure access. 45 | - **Dependencies**: 46 | - `uvicorn==0.22.0` 47 | - `motor==3.5.1` 48 | - `pydantic==1.10.21` 49 | - `pydantic_core==2.27.2` 50 | - `pymongo==4.8.0` 51 | - `python-jose==3.3.0` 52 | - `passlib==1.7.4` 53 | - `python-multipart==0.0.6` 54 | - `email-validator==2.0.0` 55 | - `python-dotenv==1.0.0` 56 | - `bcrypt==4.0.1` 57 | - **Authlib**: Please ensure you install the correct version if you face any compatibility issues with FastAPI. 58 | 59 | > **Troubleshooting FastAPI Issues:** 60 | > If you encounter errors while running FastAPI, double-check that the installed package versions match those listed above. Misaligned versions can lead to unexpected errors. Also, verify that your environment is correctly set up with Python 3.9+. 61 | 62 | ### Frontend 63 | - **React**: A robust framework for building dynamic user interfaces. 64 | - **Installation Note**: Simply run `npm install` to install dependencies and `npm start` to launch the development server. 65 | 66 | ### Deployment 67 | - **Server**: Deployable on popular cloud platforms like AWS, Azure, or Heroku. 68 | - **Containerization**: The project is Dockerized for easy deployment and scalability. 69 | 70 | --- 71 | 72 | ## Installation 73 | 74 | ### Prerequisites 75 | - **Python 3.9+** 76 | - **MongoDB** 77 | - **Docker** (optional for containerization) 78 | - **Node.js and npm** (for frontend development) 79 | 80 | ### Setup Instructions 81 | 82 | 1. **Clone the Repository:** 83 | ```sh 84 | git clone https://github.com/mE-uMAr/E-learning-LMS.git 85 | cd E-learning-LMS 86 | ``` 87 | 88 | 2. **Backend Setup:** 89 | - Create a virtual environment and install dependencies: 90 | ```sh 91 | python -m venv venv 92 | source venv/bin/activate # On Windows use `venv\Scripts\activate` 93 | pip install -r requirements.txt 94 | ``` 95 | - **MongoDB Setup:** 96 | Create a database and configure your connection by updating the `.env` file with the proper credentials and database URL. 97 | 98 | - **Run the Backend:** 99 | ```sh 100 | python ./backend/main.py 101 | ``` 102 | 103 | 3. **Frontend Setup:** 104 | - Navigate to the frontend directory (if separate) and install dependencies: 105 | ```sh 106 | npm install 107 | npm start 108 | ``` 109 | 110 | --- 111 | 112 | ## Contributing 113 | 114 | Contributions are welcome! If you'd like to contribute: 115 | 1. Fork the repository. 116 | 2. Create a feature branch (`git checkout -b feature/your-feature`). 117 | 3. Commit your changes. 118 | 4. Push to your branch. 119 | 5. Open a pull request. 120 | 121 | Please ensure your code follows the project’s coding standards and includes appropriate tests. 122 | 123 | --- 124 | 125 | ## License 126 | 127 | This project is licensed under the [MIT License](LICENSE). 128 | -------------------------------------------------------------------------------- /backend/app/api/v1/endpoints/notifications.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Optional 2 | from fastapi import APIRouter, Depends, HTTPException, status, Body 3 | from app.models.user import User 4 | from app.models.notification import Notification, NotificationCreate 5 | from app.api.deps import get_current_user, get_current_superuser 6 | from app.db.mongodb import db 7 | from bson import ObjectId 8 | import logging 9 | from datetime import datetime 10 | 11 | router = APIRouter() 12 | logger = logging.getLogger(__name__) 13 | 14 | @router.get("/", response_model=dict) 15 | async def get_notifications(current_user: User = Depends(get_current_user)) -> Any: 16 | """ 17 | Get notifications for the current user. 18 | """ 19 | notifications = [] 20 | cursor = db.notifications.find({ 21 | "recipient_id": ObjectId(current_user.id) 22 | }).sort("created_at", -1) 23 | 24 | async for notification in cursor: 25 | notification["_id"] = str(notification["_id"]) 26 | notification["recipient_id"] = str(notification["recipient_id"]) 27 | if notification.get("sender_id"): 28 | notification["sender_id"] = str(notification["sender_id"]) 29 | if notification.get("course_id"): 30 | notification["course_id"] = str(notification["course_id"]) 31 | notifications.append(notification) 32 | 33 | # Count unread notifications 34 | unread_count = await db.notifications.count_documents({ 35 | "recipient_id": ObjectId(current_user.id), 36 | "read": False 37 | }) 38 | 39 | return { 40 | "notifications": notifications, 41 | "unread_count": unread_count 42 | } 43 | 44 | @router.post("/create", response_model=dict) 45 | async def create_notification( 46 | title: str = Body(..., embed=True), 47 | message: str = Body(..., embed=True), 48 | current_user: User = Depends(get_current_user) 49 | ) -> Any: 50 | """ 51 | Create a new notification for all users. 52 | """ 53 | notification_data = { 54 | "title": title, 55 | "message": message, 56 | "sender_id": str(current_user.id), 57 | "created_at": datetime.utcnow(), 58 | "read": False # Default to unread 59 | } 60 | 61 | # Fetch all users' IDs 62 | users_cursor = db.users.find({}, {"_id": 1}) 63 | user_ids = [user["_id"] async for user in users_cursor] 64 | 65 | if not user_ids: 66 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No users found") 67 | 68 | # Insert notifications for all users 69 | notifications = [ 70 | {**notification_data, "recipient_id": user_id} for user_id in user_ids 71 | ] 72 | 73 | await db.notifications.insert_many(notifications) 74 | 75 | return {"message": "Notification sent to all users"} 76 | 77 | @router.post("/mark-read/{notification_id}", response_model=dict) 78 | async def mark_notification_read( 79 | notification_id: str, 80 | current_user: User = Depends(get_current_user) 81 | ) -> Any: 82 | """ 83 | Mark a notification as read. 84 | """ 85 | # Check if notification exists and belongs to the user 86 | notification = await db.notifications.find_one({ 87 | "_id": ObjectId(notification_id), 88 | "recipient_id": ObjectId(current_user.id) 89 | }) 90 | 91 | if not notification: 92 | raise HTTPException( 93 | status_code=status.HTTP_404_NOT_FOUND, 94 | detail="Notification not found or doesn't belong to you" 95 | ) 96 | 97 | # Mark as read 98 | await db.notifications.update_one( 99 | {"_id": ObjectId(notification_id)}, 100 | {"$set": {"read": True}} 101 | ) 102 | 103 | return {"message": "Notification marked as read"} 104 | 105 | @router.post("/mark-all-read", response_model=dict) 106 | async def mark_all_notifications_read(current_user: User = Depends(get_current_user)) -> Any: 107 | """ 108 | Mark all notifications as read. 109 | """ 110 | await db.notifications.update_many( 111 | {"recipient_id": ObjectId(current_user.id)}, 112 | {"$set": {"read": True}} 113 | ) 114 | 115 | return {"message": "All notifications marked as read"} 116 | 117 | @router.delete("/{notification_id}", response_model=dict) 118 | async def delete_notification( 119 | notification_id: str, 120 | current_user: User = Depends(get_current_user) 121 | ) -> Any: 122 | """ 123 | Delete a notification. 124 | """ 125 | # Check if notification exists and belongs to the user 126 | notification = await db.notifications.find_one({ 127 | "_id": ObjectId(notification_id), 128 | "recipient_id": ObjectId(current_user.id) 129 | }) 130 | 131 | if not notification: 132 | raise HTTPException( 133 | status_code=status.HTTP_404_NOT_FOUND, 134 | detail="Notification not found or doesn't belong to you" 135 | ) 136 | 137 | # Delete notification 138 | await db.notifications.delete_one({"_id": ObjectId(notification_id)}) 139 | 140 | return {"message": "Notification deleted successfully"} 141 | 142 | -------------------------------------------------------------------------------- /frontend/client/src/pages/student/Courses.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { motion } from "framer-motion"; 3 | import { useNavigate } from "react-router-dom"; 4 | import axios from "axios"; 5 | 6 | const Courses = () => { 7 | const navigate = useNavigate(); 8 | const [courses, setCourses] = useState([]); 9 | const [loading, setLoading] = useState(true); 10 | const BASE_URL = process.env.REACT_APP_API_URL || "http://localhost:8000"; 11 | 12 | console.log("in course page"); 13 | // Load courses data 14 | useEffect(() => { 15 | const fetchEnrolledCourses = async () => { 16 | try { 17 | const response = await axios.get( 18 | `${BASE_URL}/api/courses/enrolled`, 19 | { 20 | headers: { 21 | Authorization: `Bearer ${localStorage.getItem("accessToken")}`, 22 | }, 23 | } 24 | ); 25 | setCourses(response.data.courses); 26 | console.log(response.data.courses); 27 | setLoading(false); 28 | } catch (error) { 29 | console.error("Failed to fetch enrolled courses", error); 30 | setLoading(false); 31 | } 32 | }; 33 | 34 | fetchEnrolledCourses(); 35 | }, []); 36 | 37 | // const handleViewMaterials = (courseId) => { 38 | // // Store the selected course ID in sessionStorage 39 | // sessionStorage.setItem('selectedCourseId', courseId); 40 | // navigate("/student-dashboard/courses/materials"); 41 | // }; 42 | const handleViewMaterials = (courseId) => { 43 | console.log("clicked view materials"); 44 | navigate("/student-dashboard/courses/materials", { state: { courseId } }); 45 | }; 46 | 47 | if (loading) { 48 | return ( 49 |

Loading courses...

50 | ); 51 | } 52 | 53 | return ( 54 |
55 |
56 |
57 |

My Courses

58 |
59 | 60 |
61 | {courses && courses.length > 0 ? ( 62 | courses.map((course) => ( 63 | 68 |
69 |
70 |

{course.courseName}

71 |

72 | Instructor: {course.instructorName} 73 |

74 | 75 |
76 | 77 | {course.category} 78 | 79 | 80 | {course.duration} Weeks Course 81 | 82 |
83 |
84 | 85 |
86 |
87 |
88 | Progress 89 | {course.progress}% 90 |
91 |
92 |
96 |
97 |
98 |
99 |
100 | 101 |
102 | {/* */} 105 | 111 | {/* */} 114 |
115 |
116 | )) 117 | ) : ( 118 |

No courses available.

119 | )} 120 |
121 |
122 |
123 | ); 124 | }; 125 | 126 | export default Courses; 127 | -------------------------------------------------------------------------------- /frontend/admin/src/context/AuthContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useEffect } from "react"; 2 | import axios from "axios"; 3 | import { jwtDecode } from "jwt-decode"; 4 | 5 | const api = axios.create({ 6 | baseURL: "https://e-learning-lms-kas6.onrender.com/api/auth", 7 | withCredentials: true, 8 | contentType:"application/json", 9 | 10 | }); 11 | 12 | const AuthContext = createContext(); 13 | 14 | export const AuthProvider = ({ children }) => { 15 | const [accessToken, setAccessToken] = useState(null); 16 | const [user, setUser] = useState(() => { 17 | const storedUser = localStorage.getItem("user"); 18 | return storedUser ? JSON.parse(storedUser) : null; 19 | }); 20 | const [loading, setLoading] = useState(true); 21 | 22 | const decodeToken = (token) => { 23 | try { 24 | if (!token || typeof token !== "string") { 25 | throw new Error("Invalid token specified: must be a string"); 26 | } 27 | const decoded = jwtDecode(token); 28 | return { id: decoded.id, email: decoded.email, is_superuser: decoded.is_superuser }; 29 | } catch (error) { 30 | console.error("Error decoding token:", error); 31 | return null; 32 | } 33 | }; 34 | 35 | 36 | const login = async (email, password) => { 37 | try { 38 | const formData = new URLSearchParams(); 39 | formData.append("username", email); 40 | formData.append("password", password); 41 | 42 | const res = await api.post("/login", formData, { 43 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 44 | }); 45 | 46 | console.log("Login Response:", res.data); 47 | 48 | const token = res.data.access_token; 49 | if (!token || typeof token !== "string") { 50 | throw new Error("Invalid token received from login"); 51 | } 52 | 53 | // Change this: 54 | // localStorage.setItem("accessToken", token); 55 | // To this: 56 | localStorage.setItem("token", token); 57 | setAccessToken(token); 58 | 59 | const decodedUser = decodeToken(token); 60 | setUser(decodedUser); 61 | localStorage.setItem("user", JSON.stringify(decodedUser)); 62 | } catch (error) { 63 | console.error("Login error:", error.response?.data || error.message); 64 | throw new Error(error.response?.data?.detail || "Invalid credentials or server error"); 65 | } 66 | }; 67 | 68 | 69 | 70 | 71 | const logout = async () => { 72 | try { 73 | await api.post("/logout"); 74 | setAccessToken(null); 75 | setUser(null); 76 | localStorage.removeItem("user"); 77 | } catch (error) { 78 | console.error( 79 | "Logout error:", 80 | error.response?.data?.message || error.message 81 | ); 82 | } 83 | }; 84 | 85 | const refreshAccessToken = async () => { 86 | try { 87 | const res = await api.post("/refresh-token"); 88 | // Use the correct field name from the response 89 | const token = res.data.access_token; // not res.data.accessToken 90 | if (!token || typeof token !== "string") { 91 | throw new Error("Received invalid token"); 92 | } 93 | setAccessToken(token); 94 | const decodedUser = decodeToken(token); 95 | setUser(decodedUser); 96 | localStorage.setItem("user", JSON.stringify(decodedUser)); 97 | return token; 98 | } catch (error) { 99 | console.error("Refresh token error:", error.response?.data?.message || error.message); 100 | setAccessToken(null); 101 | setUser(null); 102 | localStorage.removeItem("user"); 103 | } 104 | }; 105 | 106 | 107 | useEffect(() => { 108 | const interceptor = api.interceptors.response.use( 109 | (response) => response, 110 | async (error) => { 111 | // Skip refresh logic if the request was to /login or /refresh-token 112 | if (error.config && 113 | (error.config.url.includes('/login') || error.config.url.includes('/refresh-token'))) { 114 | return Promise.reject(error); 115 | } 116 | 117 | if (error.response?.status === 401) { 118 | try { 119 | const newAccessToken = await refreshAccessToken(); 120 | if (newAccessToken) { 121 | error.config.headers["Authorization"] = `Bearer ${newAccessToken}`; 122 | return api(error.config); 123 | } 124 | } catch (refreshError) { 125 | console.error("Failed to refresh token:", refreshError); 126 | logout(); 127 | } 128 | } 129 | return Promise.reject(error); 130 | } 131 | ); 132 | 133 | return () => api.interceptors.response.eject(interceptor); 134 | }, []); 135 | 136 | 137 | useEffect(() => { 138 | const fetchToken = async () => { 139 | if (localStorage.getItem("user")) { 140 | await refreshAccessToken(); 141 | } 142 | setLoading(false); 143 | }; 144 | fetchToken(); 145 | }, []); 146 | 147 | return ( 148 | 159 | {children} 160 | 161 | ); 162 | }; 163 | 164 | export default AuthContext; 165 | -------------------------------------------------------------------------------- /frontend/client/src/components/TeacherSidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link, useLocation } from "react-router-dom"; 3 | import { 4 | FiHome, 5 | FiBook, 6 | FiUsers, 7 | FiClipboard, 8 | FiCalendar, 9 | FiCheckSquare, 10 | FiFileText, 11 | FiSettings, 12 | FiLogOut, 13 | } from "react-icons/fi"; 14 | import { useAuth } from "../context/AuthContext"; 15 | import { useNavigate } from "react-router-dom"; // Import useNavigate 16 | import { toast } from "react-toastify"; 17 | 18 | const TeacherSidebar = () => { 19 | const location = useLocation(); // Get current route 20 | 21 | // Function to check if a tab is active 22 | const isActive = (path) => location.pathname.startsWith(path); 23 | 24 | const { logout } = useAuth(); 25 | const navigate = useNavigate(); 26 | 27 | const handleLogout = async () => { 28 | try { 29 | await logout(); 30 | navigate("/login"); 31 | } catch (error) { 32 | console.error("Logout failed:", error); 33 | toast.error("Logout failed. Please try again."); 34 | } 35 | }; 36 | 37 | return ( 38 | 149 | ); 150 | }; 151 | 152 | export default TeacherSidebar; 153 | -------------------------------------------------------------------------------- /frontend/client/src/pages/student/Assignments.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect } from "react" 4 | import { FiCheckCircle } from "react-icons/fi" 5 | import axios from "axios" 6 | 7 | const Assignments = () => { 8 | const [assignments, setAssignments] = useState([]) 9 | const [isLoading, setIsLoading] = useState(true) 10 | const [isSubmitting, setIsSubmitting] = useState(false) 11 | const [selectedFile, setSelectedFile] = useState(null) 12 | const [selectedAssignment, setSelectedAssignment] = useState(null) 13 | const BASE_URL = process.env.REACT_APP_API_URL || "http://localhost:8000" 14 | 15 | useEffect(() => { 16 | const fetchAssignments = async () => { 17 | setIsLoading(true) 18 | try { 19 | const token = localStorage.getItem("accessToken") 20 | const response = await axios.get(`${BASE_URL}/api/assignments/student`, { 21 | headers: { Authorization: `Bearer ${token}` }, 22 | }) 23 | setAssignments(response.data.assignments || []) 24 | } catch (error) { 25 | console.error("Error fetching assignments:", error) 26 | } finally { 27 | setIsLoading(false) 28 | } 29 | } 30 | fetchAssignments() 31 | }, []) 32 | 33 | const handleFileChange = (e) => { 34 | setSelectedFile(e.target.files[0]) 35 | } 36 | 37 | const submitAssignment = async (assignmentId) => { 38 | if (!selectedFile) { 39 | alert("Please select a file to submit.") 40 | return 41 | } 42 | setIsSubmitting(true) 43 | try { 44 | const token = localStorage.getItem("accessToken") 45 | const formData = new FormData() 46 | formData.append("submission_file", selectedFile) 47 | const response = await axios.post(`${BASE_URL}/api/assignments/${assignmentId}/submit`, formData, { 48 | headers: { Authorization: `Bearer ${token}`, "Content-Type": "multipart/form-data" }, 49 | }) 50 | alert(response.data.message) 51 | setAssignments( 52 | assignments.map((assignment) => 53 | assignment._id === assignmentId ? { ...assignment, submitted: true } : assignment, 54 | ), 55 | ) 56 | } catch (error) { 57 | console.error("Error submitting assignment:", error) 58 | alert("Failed to submit assignment.") 59 | } finally { 60 | setIsSubmitting(false) 61 | setSelectedFile(null) 62 | } 63 | } 64 | 65 | return ( 66 |
67 |
68 |

Assignments

69 |
70 | {isLoading ? ( 71 |

Loading assignments...

72 | ) : assignments.length > 0 ? ( 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | {assignments.map((assignment) => ( 85 | 86 | 87 | 88 | 95 | 100 | 116 | 117 | ))} 118 | 119 |
TitleDeadlineStatusScoreActions
{assignment.title}{new Date(assignment.deadline).toLocaleDateString()} 89 | {assignment.submitted 90 | ? assignment.submission && assignment.submission.status === "Graded" 91 | ? "Graded" 92 | : "Submitted" 93 | : "Pending"} 94 | 96 | {assignment.submitted && assignment.submission && assignment.submission.status === "Graded" 97 | ? assignment.submission.score 98 | : "N/A"} 99 | 101 | {assignment.submitted ? ( 102 | 103 | ) : ( 104 | <> 105 | 106 | 113 | 114 | )} 115 |
120 | ) : ( 121 |

No assignments found.

122 | )} 123 |
124 |
125 |
126 | ) 127 | } 128 | 129 | export default Assignments 130 | 131 | -------------------------------------------------------------------------------- /backend/app/api/v1/endpoints/users.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Optional 2 | from fastapi import APIRouter, Depends, HTTPException, status 3 | from app.models.user import User, UserUpdate 4 | from app.api.deps import get_current_user, get_current_superuser 5 | from app.core.security import get_password_hash 6 | from app.db.mongodb import db 7 | from bson import ObjectId 8 | import logging 9 | 10 | router = APIRouter() 11 | logger = logging.getLogger(__name__) 12 | 13 | @router.get("/me", response_model=dict) 14 | async def read_current_user(current_user: User = Depends(get_current_user)) -> Any: 15 | """ 16 | Get current user. 17 | """ 18 | return { 19 | "id": str(current_user.id), 20 | "email": current_user.email, 21 | "username": current_user.username, 22 | "role": current_user.role, 23 | "is_active": current_user.is_active, 24 | "is_superuser": current_user.is_superuser 25 | } 26 | 27 | @router.put("/me", response_model=dict) 28 | async def update_current_user( 29 | user_update: UserUpdate, 30 | current_user: User = Depends(get_current_user) 31 | ) -> Any: 32 | """ 33 | Update current user. 34 | """ 35 | update_data = user_update.dict(exclude_unset=True) 36 | 37 | if "password" in update_data: 38 | update_data["hashed_password"] = get_password_hash(update_data.pop("password")) 39 | 40 | if update_data: 41 | await db.users.update_one( 42 | {"_id": ObjectId(current_user.id)}, 43 | {"$set": update_data} 44 | ) 45 | 46 | # Get updated user 47 | updated_user = await db.users.find_one({"_id": ObjectId(current_user.id)}) 48 | 49 | return { 50 | "message": "User updated successfully", 51 | "user": { 52 | "id": str(updated_user["_id"]), 53 | "email": updated_user["email"], 54 | "username": updated_user["username"], 55 | "role": updated_user["role"], 56 | "is_active": updated_user["is_active"] 57 | } 58 | } 59 | 60 | @router.get("/", response_model=dict) 61 | async def read_users( 62 | skip: int = 0, 63 | limit: int = 100, 64 | role: Optional[str] = None, 65 | current_user: User = Depends(get_current_superuser) 66 | ) -> Any: 67 | """ 68 | Get all users (admin only). 69 | """ 70 | query = {} 71 | if role: 72 | query["role"] = role 73 | 74 | users = [] 75 | cursor = db.users.find(query).skip(skip).limit(limit) 76 | 77 | async for user in cursor: 78 | users.append({ 79 | "id": str(user["_id"]), 80 | "email": user["email"], 81 | "username": user["username"], 82 | "role": user["role"], 83 | "is_active": user["is_active"], 84 | "is_superuser": user.get("is_superuser", False) 85 | }) 86 | 87 | total = await db.users.count_documents(query) 88 | 89 | return { 90 | "total": total, 91 | "users": users 92 | } 93 | 94 | @router.get("/{user_id}", response_model=dict) 95 | async def read_user( 96 | user_id: str, 97 | current_user: User = Depends(get_current_superuser) 98 | ) -> Any: 99 | """ 100 | Get user by ID (admin only). 101 | """ 102 | user = await db.users.find_one({"_id": ObjectId(user_id)}) 103 | 104 | if not user: 105 | raise HTTPException( 106 | status_code=status.HTTP_404_NOT_FOUND, 107 | detail="User not found" 108 | ) 109 | 110 | return { 111 | "id": str(user["_id"]), 112 | "email": user["email"], 113 | "username": user["username"], 114 | "role": user["role"], 115 | "is_active": user["is_active"], 116 | "is_superuser": user.get("is_superuser", False) 117 | } 118 | 119 | @router.put("/{user_id}", response_model=dict) 120 | async def update_user( 121 | user_id: str, 122 | user_update: UserUpdate, 123 | current_user: User = Depends(get_current_superuser) 124 | ) -> Any: 125 | """ 126 | Update user by ID (admin only). 127 | """ 128 | user = await db.users.find_one({"_id": ObjectId(user_id)}) 129 | 130 | if not user: 131 | raise HTTPException( 132 | status_code=status.HTTP_404_NOT_FOUND, 133 | detail="User not found" 134 | ) 135 | 136 | update_data = user_update.dict(exclude_unset=True) 137 | 138 | if "password" in update_data: 139 | update_data["hashed_password"] = get_password_hash(update_data.pop("password")) 140 | 141 | if update_data: 142 | await db.users.update_one( 143 | {"_id": ObjectId(user_id)}, 144 | {"$set": update_data} 145 | ) 146 | 147 | # Get updated user 148 | updated_user = await db.users.find_one({"_id": ObjectId(user_id)}) 149 | 150 | return { 151 | "message": "User updated successfully", 152 | "user": { 153 | "id": str(updated_user["_id"]), 154 | "email": updated_user["email"], 155 | "username": updated_user["username"], 156 | "role": updated_user["role"], 157 | "is_active": updated_user["is_active"] 158 | } 159 | } 160 | 161 | @router.delete("/{user_id}", response_model=dict) 162 | async def delete_user( 163 | user_id: str, 164 | current_user: User = Depends(get_current_superuser) 165 | ) -> Any: 166 | """ 167 | Delete user by ID (admin only). 168 | """ 169 | user = await db.users.find_one({"_id": ObjectId(user_id)}) 170 | 171 | if not user: 172 | raise HTTPException( 173 | status_code=status.HTTP_404_NOT_FOUND, 174 | detail="User not found" 175 | ) 176 | 177 | await db.users.delete_one({"_id": ObjectId(user_id)}) 178 | 179 | return {"message": "User deleted successfully"} -------------------------------------------------------------------------------- /frontend/admin/src/components/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Link, useLocation } from "react-router-dom"; 3 | import { 4 | FiHome, 5 | FiBook, 6 | FiCalendar, 7 | FiUsers, 8 | FiPieChart, 9 | FiAward, 10 | FiSettings, 11 | FiLogOut, 12 | FiMessageSquare, 13 | FiClipboard, 14 | FiShield, 15 | } from "react-icons/fi"; 16 | import AuthContext from "../context/AuthContext"; 17 | 18 | const Sidebar = ({ isSidebarOpen, navigate }) => { 19 | const location = useLocation(); 20 | const { logout } = useContext(AuthContext); // Access logout from AuthContext 21 | 22 | const isActive = (path) => { 23 | return location.pathname === `/dashboard${path}`; 24 | }; 25 | 26 | const handleLogout = () => { 27 | logout(); // Clear tokens and user data 28 | navigate("/login"); // Redirect to login 29 | }; 30 | 31 | return ( 32 | 161 | ); 162 | }; 163 | 164 | export default Sidebar; 165 | -------------------------------------------------------------------------------- /frontend/client/src/components/StudentSidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link, useLocation } from "react-router-dom"; 3 | import { 4 | FiHome, 5 | FiMessageSquare, 6 | FiBook, 7 | FiClock, 8 | FiBell, 9 | FiAward, 10 | FiUsers, 11 | FiClipboard, 12 | FiPlusCircle, 13 | FiCalendar, 14 | FiCheckSquare, 15 | FiFileText, 16 | FiSettings, 17 | FiLogOut, 18 | } from "react-icons/fi"; 19 | import { useAuth } from "../context/AuthContext"; // Import useAuth for logout 20 | import { useNavigate } from "react-router-dom"; 21 | import { toast } from "react-toastify"; 22 | import "react-toastify/dist/ReactToastify.css"; 23 | 24 | const StudentSidebar = () => { 25 | const { logout } = useAuth(); 26 | const navigate = useNavigate(); 27 | const location = useLocation(); // Get the current route path 28 | 29 | // Function to check if a tab is active 30 | const isActive = (path) => location.pathname === path; 31 | 32 | // Handle logout 33 | const handleLogout = async () => { 34 | try { 35 | await logout(); 36 | toast.success("Logged out successfully!"); 37 | navigate("/login"); 38 | } catch (error) { 39 | console.error("Logout failed:", error); 40 | toast.error("Logout failed. Please try again!"); 41 | } 42 | }; 43 | 44 | return ( 45 | 168 | ); 169 | }; 170 | 171 | export default StudentSidebar; 172 | -------------------------------------------------------------------------------- /frontend/admin/src/pages/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from "react"; 2 | import { ToastContainer, toast } from "react-toastify"; 3 | import "react-toastify/dist/ReactToastify.css"; 4 | import { AiFillEye, AiFillEyeInvisible } from "react-icons/ai"; 5 | import { FiMail, FiLock } from "react-icons/fi"; 6 | import logo from "../assets/logo.png"; 7 | import AuthContext from "../context/AuthContext"; // Import AuthContext 8 | 9 | const Login = () => { 10 | const { login } = useContext(AuthContext); // Get login function from AuthContext 11 | 12 | const [email, setEmail] = useState(""); 13 | const [password, setPassword] = useState(""); 14 | const [showPassword, setShowPassword] = useState(false); 15 | const [loading, setLoading] = useState(false); 16 | 17 | const handleLogin = async () => { 18 | try { 19 | setLoading(true); 20 | // Call the login function from AuthContext 21 | await login(email, password); 22 | toast.success("Login successful!"); 23 | // Optionally, redirect the user after successful login: 24 | window.location.href = "/dashboard"; 25 | } catch (error) { 26 | console.error("Login error:", error); 27 | toast.error("Invalid credentials or server error"); 28 | } finally { 29 | setLoading(false); 30 | } 31 | }; 32 | 33 | return ( 34 |
35 | 36 |
37 |
38 |
39 |
40 |
41 | EduLearn Logo 46 |

Admin Portal

47 |

48 | Access your admin dashboard to manage courses, students, teachers, 49 | and monitor learning activities across the platform. 50 |

51 |
52 |
53 |
54 |
55 |
56 |
57 | EduLearn 58 |
59 |

Admin Login

60 |

61 | Access the e-learning platform controls 62 |

63 |
64 |
{ e.preventDefault(); handleLogin(); }}> 65 |
66 | 67 | setEmail(e.target.value)} 72 | required 73 | className="w-full pl-12 pr-4 py-4 border-2 border-gray-100 rounded-xl focus:outline-none focus:border-[#19a4db] transition-all duration-200 text-gray-900 text-lg hover:border-gray-200" 74 | /> 75 |
76 |
77 | 78 | setPassword(e.target.value)} 83 | required 84 | className="w-full pl-12 pr-12 py-4 border-2 border-gray-100 rounded-xl focus:outline-none focus:border-[#19a4db] transition-all duration-200 text-gray-900 text-lg hover:border-gray-200" 85 | /> 86 | 97 |
98 | 109 |
110 |

111 | Protected access to EduLearn administration portal. Subject to 112 | 116 | {" "} 117 | Privacy Policy 118 | {" "} 119 | and 120 | 124 | {" "} 125 | Terms of Service 126 | 127 |

128 |
129 |
130 |
131 | ); 132 | }; 133 | 134 | export default Login; 135 | -------------------------------------------------------------------------------- /frontend/client/src/pages/student/Progress.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | BarChart, 4 | Bar, 5 | XAxis, 6 | YAxis, 7 | CartesianGrid, 8 | Tooltip, 9 | ResponsiveContainer, 10 | LineChart, 11 | Line 12 | } from "recharts"; 13 | 14 | const Progress = () => { 15 | // Mock data for course progress 16 | const courses = [ 17 | { 18 | id: 1, 19 | title: "Introduction to React", 20 | instructor: "Sarah Johnson", 21 | progress: 65, 22 | score: 78, 23 | completed: 12, 24 | total: 20 25 | }, 26 | { 27 | id: 2, 28 | title: "Advanced JavaScript", 29 | instructor: "Michael Chen", 30 | progress: 40, 31 | score: 85, 32 | completed: 8, 33 | total: 15 34 | }, 35 | { 36 | id: 3, 37 | title: "UX/UI Design Fundamentals", 38 | instructor: "Priya Sharma", 39 | progress: 85, 40 | score: 92, 41 | completed: 15, 42 | total: 18 43 | }, 44 | ]; 45 | 46 | // Calculate overall progress 47 | const totalCompleted = courses.reduce((sum, course) => sum + course.completed, 0); 48 | const totalLessons = courses.reduce((sum, course) => sum + course.total, 0); 49 | const overallProgress = Math.round((totalCompleted / totalLessons) * 100); 50 | 51 | // Generate weekly activity data 52 | const weeklyActivity = [ 53 | { day: "Mon", hours: 2.5 }, 54 | { day: "Tue", hours: 1.8 }, 55 | { day: "Wed", hours: 3.2 }, 56 | { day: "Thu", hours: 2.0 }, 57 | { day: "Fri", hours: 1.5 }, 58 | { day: "Sat", hours: 0.5 }, 59 | { day: "Sun", hours: 1.0 }, 60 | ]; 61 | 62 | // Course completion data for the chart 63 | const courseChartData = courses.map(course => ({ 64 | name: course.title.length > 15 ? course.title.substring(0, 15) + "..." : course.title, 65 | progress: course.progress 66 | })); 67 | 68 | return ( 69 |
70 |

Learning Progress

71 | 72 | {/* Overall progress card */} 73 |
74 |

Overall Progress

75 |
76 |
77 | 78 | 84 | 91 | 92 |
93 | {overallProgress}% 94 |
95 |
96 |
97 |

98 | You've completed {totalCompleted} of {totalLessons} lessons 99 |

100 |

101 | Keep up the good work! 102 |

103 |
104 |
105 |
106 | 107 | {/* Course progress */} 108 |
109 |

Course Progress

110 |
111 | 112 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |
124 |
125 | 126 | {/* Weekly activity */} 127 |
128 |

Weekly Activity

129 |
130 | 131 | 135 | 136 | 137 | 138 | 139 | 145 | 146 | 147 |
148 |
149 | 150 | {/* Individual course progress details */} 151 |
152 |

Course Details

153 |
154 | {courses.map(course => ( 155 |
156 |
157 |

{course.title}

158 | 159 | {course.progress}% Complete 160 | 161 |
162 |
163 |
167 |
168 |
169 | {course.completed} of {course.total} lessons completed 170 | Score: {course.score}% 171 |
172 |
173 | ))} 174 |
175 |
176 |
177 | ); 178 | }; 179 | 180 | export default Progress; -------------------------------------------------------------------------------- /frontend/client/src/pages/student/Support.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FiMessageCircle, FiPhone, FiMail, FiHelpCircle } from "react-icons/fi"; 3 | 4 | const Support = () => { 5 | return ( 6 |
7 |
8 |

Support & Help

9 | 10 |
11 |
12 |
13 |
14 | 15 |
16 |
17 |

Contact Instructor

18 |

Reach out to your course instructors directly for course-related questions.

19 | 22 |
23 |
24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 |

Technical Support

33 |

Having issues with the platform? Get technical help from our support team.

34 | 37 |
38 |
39 |
40 |
41 | 42 |
43 |

Frequently Asked Questions

44 |
45 |
46 | How do I submit assignments? 47 |

48 | Navigate to your course page, locate the assignment section, and click on "Submit Assignment". You can then upload your work as instructed. 49 |

50 |
51 |
52 | How are courses graded? 53 |

54 | Courses are typically graded based on assignments, quizzes, participation, and final projects or exams. Specific grading criteria are provided by each instructor. 55 |

56 |
57 |
58 | Can I download course materials? 59 |

60 | Yes, most course materials can be downloaded for offline study. Look for the download button next to lecture materials. 61 |

62 |
63 |
64 | How do I get a certificate? 65 |

66 | Certificates are automatically issued once you complete all course requirements. You can view and download your certificates from the Certificates section. 67 |

68 |
69 |
70 |
71 | 72 |
73 |

Contact Support

74 |
75 |
76 | 77 | +1 (800) 123-4567 78 |
79 |
80 | 81 | support@edulearn.com 82 |
83 |
84 | 85 | Live Chat (9AM-5PM) 86 |
87 |
88 | 89 |
90 |
91 |
92 | 93 | 94 |
95 |
96 | 97 | 98 |
99 |
100 |
101 | 102 | 103 |
104 |
105 | 106 | 107 |
108 | 111 |
112 |
113 |
114 |
115 | ); 116 | }; 117 | 118 | export default Support; -------------------------------------------------------------------------------- /frontend/client/src/pages/student/Notifications.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { FiBell, FiClock, FiMail, FiCheckCircle, FiAlertCircle, FiTrash } from "react-icons/fi"; 3 | import axios from "axios"; 4 | 5 | const Notifications = () => { 6 | const [notifications, setNotifications] = useState([]); 7 | const [unreadCount, setUnreadCount] = useState(0); 8 | const BASE_URL = process.env.REACT_APP_API_URL || "http://localhost:8000"; 9 | 10 | useEffect(() => { 11 | fetchNotifications(); 12 | }, []); 13 | 14 | const fetchNotifications = async () => { 15 | try { 16 | const token = localStorage.getItem("accessToken"); 17 | const response = await axios.get(`${BASE_URL}/api/notifications/`, { 18 | headers: { Authorization: `Bearer ${token}` }, 19 | }); 20 | setNotifications(response.data.notifications); 21 | setUnreadCount(response.data.unread_count); 22 | } catch (error) { 23 | console.error("Error fetching notifications:", error); 24 | } 25 | }; 26 | 27 | const getNotificationIcon = (type) => { 28 | switch (type) { 29 | case "assignment": 30 | return ; 31 | case "material": 32 | return ; 33 | case "announcement": 34 | return ; 35 | case "grade": 36 | return ; 37 | case "feedback": 38 | return ; 39 | default: 40 | return ; 41 | } 42 | }; 43 | 44 | const markAsRead = async (id) => { 45 | try { 46 | const token = localStorage.getItem("accessToken"); 47 | await axios.post(`${BASE_URL}/api/notifications/mark-read/${id}`, {}, { 48 | headers: { Authorization: `Bearer ${token}` }, 49 | }); 50 | setNotifications(notifications.map(n => n._id === id ? { ...n, read: true } : n)); 51 | setUnreadCount(unreadCount - 1); 52 | } catch (error) { 53 | console.error("Error marking notification as read:", error); 54 | } 55 | }; 56 | 57 | const markAllAsRead = async () => { 58 | try { 59 | const token = localStorage.getItem("accessToken"); 60 | await axios.post(`${BASE_URL}/api/notifications/mark-all-read`, {}, { 61 | headers: { Authorization: `Bearer ${token}` }, 62 | }); 63 | setNotifications(notifications.map(n => ({ ...n, read: true }))); 64 | setUnreadCount(0); 65 | } catch (error) { 66 | console.error("Error marking all notifications as read:", error); 67 | } 68 | }; 69 | 70 | const deleteNotification = async (id) => { 71 | try { 72 | const token = localStorage.getItem("accessToken"); 73 | await axios.delete(`${BASE_URL}/api/notifications/${id}`, { 74 | headers: { Authorization: `Bearer ${token}` }, 75 | }); 76 | setNotifications(notifications.filter(n => n._id !== id)); 77 | } catch (error) { 78 | console.error("Error deleting notification:", error); 79 | } 80 | }; 81 | 82 | return ( 83 |
84 |
85 |

Notifications

86 | 87 |
88 | 95 | 96 | {unreadCount > 0 && ( 97 | 100 | )} 101 |
102 |
103 | 104 |
105 | {notifications.length > 0 ? ( 106 |
107 | {notifications.map((notification) => ( 108 |
112 |
113 |
114 | {getNotificationIcon(notification.type)} 115 |
116 | 117 |
118 |
119 |

120 | {notification.title} 121 |

122 | {notification.time} 123 |
124 | 125 |

{notification.message}

126 | 127 |
128 | 129 | {notification.course} 130 | 131 | 132 | {!notification.read && ( 133 | 134 | New 135 | 136 | )} 137 |
138 |
139 | 140 |
141 | {!notification.read && ( 142 | 145 | )} 146 | 149 |
150 |
151 |
152 | ))} 153 |
154 | ) : ( 155 |

No notifications found.

156 | )} 157 |
158 |
159 | ); 160 | }; 161 | 162 | export default Notifications; 163 | -------------------------------------------------------------------------------- /frontend/client/src/pages/student/CourseMaterials.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useNavigate, useLocation } from "react-router-dom"; 3 | import axios from "axios"; 4 | 5 | const CourseMaterials = () => { 6 | const navigate = useNavigate(); 7 | const location = useLocation(); 8 | const [course, setCourse] = useState(null); 9 | const [modules, setModules] = useState([]); 10 | const [activeModules, setActiveModules] = useState({}); 11 | const courseId = location.state?.courseId; 12 | 13 | useEffect(() => { 14 | if (courseId) { 15 | // Fetch course details using courseId 16 | const fetchCourseDetails = async () => { 17 | try { 18 | const response = await axios.get( 19 | `http://localhost:8000/api/courses/${courseId}/modules`, 20 | { 21 | headers: { 22 | Authorization: `Bearer ${localStorage.getItem("accessToken")}`, 23 | }, 24 | } 25 | ); 26 | setCourse(response); 27 | console.log("course", response.data.modules); 28 | 29 | // Fetch course modules 30 | const fetchModules = async () => { 31 | try { 32 | const response = await axios.get( 33 | `http://localhost:8000/api/courses/${courseId}/modules`, 34 | { 35 | headers: { 36 | Authorization: `Bearer ${localStorage.getItem( 37 | "accessToken" 38 | )}`, 39 | }, 40 | } 41 | ); 42 | setModules(response.data.modules); 43 | } catch (error) { 44 | console.error("Failed to fetch course modules", error); 45 | } 46 | }; 47 | 48 | fetchModules(); 49 | } catch (error) { 50 | console.error("Failed to fetch course details", error); 51 | } 52 | }; 53 | 54 | fetchCourseDetails(); 55 | } else { 56 | console.log("No course ID found in navigation state"); 57 | } 58 | }, [courseId]); 59 | 60 | const toggleModule = (moduleId) => { 61 | setActiveModules((prevState) => ({ 62 | ...prevState, 63 | [moduleId]: !prevState[moduleId], 64 | })); 65 | }; 66 | 67 | if (!course) { 68 | return ( 69 |
70 |

Loading course materials...

71 | 77 |
78 | ); 79 | } 80 | 81 | return ( 82 |
83 |

{course.title}

84 | 85 |

Instructor: {course.instructor}

86 | 87 | {/* Tabs similar to the image */} 88 |
89 |
90 | Course Content 91 |
92 |
93 | 94 |
95 |

Modules / Sections

96 |
97 | 98 |
99 | {modules && modules.length > 0 ? ( 100 | modules.map((module) => ( 101 |
102 | {/* Module header - clickable to expand/collapse */} 103 |
toggleModule(module._id)} 106 | > 107 |
108 | 113 | ▶ 114 | 115 | {module.title} 116 | 117 | {module.lessons.length}{" "} 118 | {module.lessons.length === 1 ? "lesson" : "lessons"} 119 | 120 |
121 |
122 | 123 | {/* Lessons - visible when module is expanded */} 124 | {activeModules[module._id] && ( 125 |
126 | {module.lessons.map((lesson) => ( 127 |
131 |
132 | 133 | {lesson.title} 134 |
135 | 136 | {lesson.materialType === "link" ? ( 137 | 143 | View now! 144 | 145 | ) : ( 146 | "" 147 | )} 148 | 149 | 150 | {lesson.duration} 151 | 152 |
153 | ))} 154 |
155 | )} 156 |
157 | )) 158 | ) : ( 159 |
160 |

No modules added to this course yet.

161 |
162 | )} 163 |
164 | 165 | 171 |
172 | ); 173 | }; 174 | 175 | export default CourseMaterials; 176 | -------------------------------------------------------------------------------- /frontend/client/src/context/AuthContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState, useEffect } from "react"; 2 | import axios from "axios"; 3 | import { jwtDecode } from "jwt-decode"; 4 | import { useNavigate } from "react-router-dom"; 5 | 6 | export const AuthContext = createContext(); 7 | 8 | export const AuthProvider = ({ children }) => { 9 | const navigate = useNavigate(); 10 | 11 | // Load initial state from localStorage 12 | const [accessToken, setAccessToken] = useState( 13 | () => localStorage.getItem("accessToken") || null 14 | ); 15 | const [user, setUser] = useState(() => { 16 | const storedUser = localStorage.getItem("user"); 17 | return storedUser ? JSON.parse(storedUser) : null; 18 | }); 19 | const [role, setRole] = useState(() => localStorage.getItem("role") || null); 20 | const [userName, setUserName] = useState( 21 | () => localStorage.getItem("userName") || null 22 | ); 23 | const [loading, setLoading] = useState(true); 24 | 25 | // Function to get the base URL based on the role 26 | const BASE_URL = process.env.REACT_APP_API_URL || "http://localhost:8000"; 27 | 28 | const getBaseURL = (role) => { 29 | switch (role) { 30 | case "student": 31 | return `${BASE_URL}/api/auth`; 32 | case "teacher": 33 | return `${BASE_URL}/api/auth`; 34 | default: 35 | return `${BASE_URL}/api/auth`; 36 | } 37 | }; 38 | 39 | 40 | // Function to get API instance dynamically 41 | const getApiInstance = (role) => 42 | axios.create({ 43 | baseURL: getBaseURL(role), 44 | withCredentials: true, 45 | }); 46 | 47 | // Function to decode JWT token 48 | const decodeToken = (token) => { 49 | try { 50 | return jwtDecode(token); 51 | } catch (error) { 52 | console.error("Error decoding token:", error); 53 | return null; 54 | } 55 | }; 56 | 57 | // Sign-up function 58 | const signUp = async ({ username, email, password, role }) => { 59 | try { 60 | const api = getApiInstance(role); 61 | const res = await api.post('/signup', { 62 | username, 63 | email, 64 | password, 65 | role, 66 | }); 67 | return res.data; 68 | } catch (error) { 69 | throw new Error(error.response?.data?.message || "Signup failed"); 70 | } 71 | }; 72 | 73 | // Login function 74 | const login = async ({ email, password, role }) => { 75 | try { 76 | const api = getApiInstance(role); 77 | 78 | // Use URLSearchParams instead of FormData 79 | const formData = new URLSearchParams(); 80 | formData.append("username", email); // OAuth2 expects "username" 81 | formData.append("password", password); 82 | 83 | // Make API request with correct Content-Type 84 | const res = await api.post("/login", formData, { 85 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 86 | }); 87 | 88 | const token = res.data.access_token; // ✅ Correct token key 89 | const userData = res.data.user; // ✅ Extract user data 90 | const userrole = userData.role; // ✅ Extract role from user object 91 | const isApproved = res.data.is_active; // ✅ Extract approved from user object 92 | console.log(res); 93 | if (!isApproved) { 94 | throw new Error("Not approved yet"); 95 | return; // Stop login process 96 | } 97 | console.log("pass") 98 | // Store token & user details 99 | setAccessToken(token); 100 | setUserName(userData.username); 101 | setUser(decodeToken(token)); 102 | setRole(userrole); 103 | 104 | localStorage.setItem("accessToken", token); 105 | localStorage.setItem("user", JSON.stringify(userData)); 106 | localStorage.setItem("role", userrole); 107 | localStorage.setItem("userName", userData.username); 108 | 109 | } catch (error) { 110 | console.error("Login failed:", error.response?.data || error.message); 111 | throw new Error(error.response?.data?.detail || "Error logging in"); 112 | } 113 | }; 114 | 115 | // Logout function 116 | const logout = async () => { 117 | try { 118 | const userRole = localStorage.getItem("role"); 119 | if (!userRole) throw new Error("No role found for logout"); 120 | 121 | const api = getApiInstance(userRole); 122 | 123 | console.log(userRole); 124 | 125 | // Clear state and storage 126 | setAccessToken(null); 127 | setUser(null); 128 | setRole(null); 129 | setUserName(null); 130 | localStorage.clear(); 131 | 132 | await api.post('/logout'); 133 | navigate("/login"); 134 | } catch (error) { 135 | console.error( 136 | "Logout error:", 137 | error.response?.data?.message || error.message 138 | ); 139 | } 140 | }; 141 | 142 | // Refresh access token 143 | const refreshAccessToken = async () => { 144 | try { 145 | const res = await axios.post( 146 | `${BASE_URL}/api/auth/refresh-token`, 147 | {}, 148 | { withCredentials: true } 149 | ); 150 | const token = res.data.access_token; 151 | setAccessToken(token); 152 | 153 | const decodedUser = decodeToken(token); 154 | setUser(decodedUser); 155 | localStorage.setItem("accessToken", token); 156 | localStorage.setItem("user", JSON.stringify(decodedUser)); 157 | 158 | return token; 159 | } catch (error) { 160 | console.error("Failed to refresh token:", error); 161 | logout(); 162 | } 163 | }; 164 | 165 | // Set up API interceptors 166 | useEffect(() => { 167 | if (!role) return; 168 | 169 | const api = getApiInstance(role); 170 | 171 | const interceptor = api.interceptors.response.use( 172 | (response) => response, 173 | async (error) => { 174 | if (error.response?.status === 401) { 175 | try { 176 | const newAccessToken = await refreshAccessToken(); 177 | if (newAccessToken) { 178 | error.config.headers[ 179 | "Authorization" 180 | ] =` Bearer ${newAccessToken}`; 181 | return api(error.config); 182 | } 183 | } catch (refreshError) { 184 | console.error("Failed to refresh token:", refreshError); 185 | logout(); 186 | } 187 | } 188 | return Promise.reject(error); 189 | } 190 | ); 191 | 192 | return () => api.interceptors.response.eject(interceptor); 193 | }, [role]); 194 | 195 | // Restore authentication state on page reload 196 | useEffect(() => { 197 | const storedToken = localStorage.getItem("accessToken"); 198 | const storedUser = localStorage.getItem("user"); 199 | 200 | if (storedToken && storedUser) { 201 | setAccessToken(storedToken); 202 | setUser(JSON.parse(storedUser)); 203 | setRole(localStorage.getItem("role") || null); 204 | setUserName(localStorage.getItem("userName") || null); 205 | } 206 | 207 | setLoading(false); 208 | }, []); 209 | 210 | // Helper function to check if the user is authenticated 211 | const isAuthenticated = () => !!accessToken && !!user; 212 | 213 | return ( 214 | 228 | {children} 229 | 230 | ); 231 | }; 232 | 233 | // Custom hook to use AuthContext 234 | export const useAuth = () => useContext(AuthContext); 235 | 236 | export default AuthContext; -------------------------------------------------------------------------------- /frontend/client/src/pages/teacher/Courses.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { 3 | FiUsers, 4 | FiCalendar, 5 | FiEdit, 6 | FiTrash2, 7 | FiPlus, 8 | FiBook, 9 | } from "react-icons/fi"; 10 | import AddCourseModal from "./components/AddCourseModal"; 11 | import { useNavigate } from "react-router-dom"; 12 | import axios from "axios"; 13 | 14 | const Courses = () => { 15 | const [activeFilter, setActiveFilter] = useState("all"); 16 | const [isModalOpen, setIsModalOpen] = useState(false); 17 | const [courses, setCourses] = useState([]); 18 | const [loading, setLoading] = useState(true); 19 | const [error, setError] = useState(null); 20 | const BASE_URL = process.env.REACT_APP_API_URL || "http://localhost:8000"; 21 | 22 | useEffect(() => { 23 | const fetchCourses = async () => { 24 | try { 25 | const token = localStorage.getItem("accessToken"); 26 | 27 | const response = await axios.get( 28 | `${BASE_URL}/api/courses/teacher/courses`, 29 | { 30 | headers: { 31 | Authorization: `Bearer ${token}`, 32 | }, 33 | } 34 | ); 35 | setCourses(response.data.courses); 36 | console.log(response.data.courses); 37 | } catch (err) { 38 | setError(err.message); 39 | } finally { 40 | setLoading(false); 41 | } 42 | }; 43 | 44 | fetchCourses(); 45 | }, []); 46 | 47 | const filteredCourses = 48 | activeFilter === "all" 49 | ? courses 50 | : courses.filter( 51 | (course) => course.status.toLowerCase() === activeFilter 52 | ); 53 | 54 | const handleAddCourse = (courseData) => { 55 | // Here you would typically send the data to your API 56 | console.log("Form submitted:", courseData); 57 | 58 | // Close modal 59 | setIsModalOpen(false); 60 | }; 61 | 62 | const navigate = useNavigate(); 63 | 64 | if (loading) return
Loading...
; 65 | if (error) return
Error: {error}
; 66 | 67 | return ( 68 |
69 |
70 |
71 |

My Courses

72 |
73 | {/*
74 | 80 | 86 | 92 | 98 |
*/} 99 | 106 |
107 |
108 | 109 |
110 | {filteredCourses.map((course, index) => ( 111 |
115 |

{course.courseName}

116 |

Code: {course.courseCode}

117 |

118 | duration : {course.duration} weeks{" "} 119 |

120 | 121 |
122 |
123 | 124 | 125 | Max Limit: {course.maxStudents} students 126 | 127 |
128 | 135 | {course.status} 136 | 137 |
138 | 139 |
140 | 148 | {/* 151 | */} 154 |
155 |
156 | ))} 157 | 158 | {filteredCourses.length === 0 && ( 159 |
160 |
161 | 162 |
163 |

164 | No Courses Found 165 |

166 |

167 | {activeFilter === "all" 168 | ? "You haven't created any courses yet." 169 | : `You don't have any ${activeFilter} courses.`} 170 |

171 | 177 |
178 | )} 179 |
180 |
181 | 182 | {/* Import the Course Modal Component */} 183 | setIsModalOpen(false)} 186 | onSubmit={handleAddCourse} 187 | /> 188 |
189 | ); 190 | }; 191 | 192 | export default Courses; 193 | -------------------------------------------------------------------------------- /backend/app/api/v1/endpoints/students.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Optional 2 | from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form 3 | from app.models.user import User, UserProfile, StudentProfile 4 | from app.api.deps import get_current_student, get_current_teacher 5 | from app.db.mongodb import db 6 | from app.utils.file_upload import save_upload 7 | from bson import ObjectId 8 | import logging 9 | from datetime import datetime 10 | 11 | router = APIRouter() 12 | logger = logging.getLogger(__name__) 13 | 14 | @router.get("/profile", response_model=dict) 15 | async def get_student_profile(current_user: User = Depends(get_current_student)) -> Any: 16 | """ 17 | Get current student's profile. 18 | """ 19 | # Get student profile 20 | profile = await db.student_profiles.find_one({"user_id": ObjectId(current_user.id)}) 21 | 22 | if not profile: 23 | # Create empty profile if it doesn't exist 24 | profile = { 25 | "user_id": ObjectId(current_user.id), 26 | "full_name": current_user.username, 27 | "bio": None, 28 | "profile_picture": None, 29 | "phone": None, 30 | "address": None, 31 | "enrollment_date": datetime.utcnow(), 32 | "student_id": f"ST-{str(current_user.id)[-6:]}" 33 | } 34 | 35 | await db.student_profiles.insert_one(profile) 36 | 37 | # Get enrollment data 38 | enrollments = [] 39 | enrollments_cursor = db.enrollments.find({"student_id": ObjectId(current_user.id)}) 40 | 41 | async for enrollment in enrollments_cursor: 42 | course = await db.courses.find_one({"_id": enrollment["course_id"]}) 43 | if course: 44 | enrollments.append({ 45 | "course_id": str(enrollment["course_id"]), 46 | "course_name": course["courseName"], 47 | "enrollment_date": enrollment["enrollment_date"], 48 | "progress": enrollment["progress"], 49 | "status": enrollment["status"] 50 | }) 51 | 52 | # Format profile data 53 | profile["_id"] = str(profile["_id"]) 54 | profile["user_id"] = str(profile["user_id"]) 55 | 56 | return { 57 | "profile": profile, 58 | "enrollments": enrollments, 59 | "email": current_user.email, 60 | "username": current_user.username 61 | } 62 | 63 | @router.put("/profile", response_model=dict) 64 | async def update_student_profile( 65 | full_name: Optional[str] = Form(None), 66 | bio: Optional[str] = Form(None), 67 | phone: Optional[str] = Form(None), 68 | address: Optional[str] = Form(None), 69 | profile_picture: Optional[UploadFile] = File(None), 70 | current_user: User = Depends(get_current_student) 71 | ) -> Any: 72 | """ 73 | Update student profile. 74 | """ 75 | # Get current profile 76 | profile = await db.student_profiles.find_one({"user_id": ObjectId(current_user.id)}) 77 | 78 | if not profile: 79 | raise HTTPException( 80 | status_code=status.HTTP_404_NOT_FOUND, 81 | detail="Profile not found" 82 | ) 83 | 84 | # Prepare update data 85 | update_data = {} 86 | 87 | if full_name is not None: 88 | update_data["full_name"] = full_name 89 | 90 | if bio is not None: 91 | update_data["bio"] = bio 92 | 93 | if phone is not None: 94 | update_data["phone"] = phone 95 | 96 | if address is not None: 97 | update_data["address"] = address 98 | 99 | # Save profile picture if provided 100 | if profile_picture: 101 | profile_picture_path = await save_upload(profile_picture, "profile_pictures") 102 | update_data["profile_picture"] = profile_picture_path 103 | 104 | # Update profile 105 | if update_data: 106 | await db.student_profiles.update_one( 107 | {"user_id": ObjectId(current_user.id)}, 108 | {"$set": update_data} 109 | ) 110 | 111 | # Get updated profile 112 | updated_profile = await db.student_profiles.find_one({"user_id": ObjectId(current_user.id)}) 113 | updated_profile["_id"] = str(updated_profile["_id"]) 114 | updated_profile["user_id"] = str(updated_profile["user_id"]) 115 | 116 | return { 117 | "message": "Profile updated successfully", 118 | "profile": updated_profile 119 | } 120 | 121 | @router.get("/progress", response_model=dict) 122 | async def get_student_progress(current_user: User = Depends(get_current_student)) -> Any: 123 | """ 124 | Get student's learning progress across all courses. 125 | """ 126 | # Get enrollments 127 | enrollments = [] 128 | enrollments_cursor = db.enrollments.find({"student_id": ObjectId(current_user.id)}) 129 | 130 | async for enrollment in enrollments_cursor: 131 | course = await db.courses.find_one({"_id": enrollment["course_id"]}) 132 | if course: 133 | # Get assignment submissions for this course 134 | assignments_cursor = db.assignments.find({"course": enrollment["course_id"]}) 135 | total_assignments = await assignments_cursor.count() 136 | 137 | submissions_cursor = db.assignment_submissions.find({ 138 | "student_id": ObjectId(current_user.id), 139 | "assignment_id": {"$in": [a["_id"] async for a in db.assignments.find({"course": enrollment["course_id"]})]} 140 | }) 141 | 142 | submissions = [] 143 | async for submission in submissions_cursor: 144 | submissions.append(submission) 145 | 146 | # Calculate score 147 | score = 0 148 | if submissions: 149 | graded_submissions = [s for s in submissions if s.get("score") is not None] 150 | if graded_submissions: 151 | total_score = sum(s["score"] for s in graded_submissions) 152 | score = (total_score / (len(graded_submissions) * 100)) * 100 153 | 154 | # Get attendance data 155 | attendance_records = [] 156 | attendance_cursor = db.attendance.find({ 157 | "course_id": enrollment["course_id"], 158 | "student_id": ObjectId(current_user.id) 159 | }) 160 | 161 | async for record in attendance_cursor: 162 | attendance_records.append(record) 163 | 164 | # Calculate attendance percentage 165 | attendance = 0 166 | if attendance_records: 167 | present_count = sum(1 for record in attendance_records if record["status"] == "Present") 168 | attendance = (present_count / len(attendance_records)) * 100 169 | 170 | enrollments.append({ 171 | "course_id": str(enrollment["course_id"]), 172 | "course_name": course["courseName"], 173 | "instructor": course["instructorName"], 174 | "progress": enrollment["progress"], 175 | "score": round(score), 176 | "attendance": round(attendance), 177 | "completed": enrollment["progress"] >= 100, 178 | "completed_lessons": 0, # This would need to be calculated based on lesson completion tracking 179 | "total_lessons": 0 # This would need to be calculated from course modules/lessons 180 | }) 181 | 182 | # Calculate overall progress 183 | overall_progress = 0 184 | if enrollments: 185 | overall_progress = sum(e["progress"] for e in enrollments) / len(enrollments) 186 | 187 | # Get weekly activity data (this would be more complex in a real app) 188 | weekly_activity = [ 189 | {"day": "Mon", "hours": 2.5}, 190 | {"day": "Tue", "hours": 1.8}, 191 | {"day": "Wed", "hours": 3.2}, 192 | {"day": "Thu", "hours": 2.0}, 193 | {"day": "Fri", "hours": 1.5}, 194 | {"day": "Sat", "hours": 0.5}, 195 | {"day": "Sun", "hours": 1.0} 196 | ] 197 | 198 | return { 199 | "overall_progress": round(overall_progress), 200 | "courses": enrollments, 201 | "weekly_activity": weekly_activity 202 | } 203 | 204 | -------------------------------------------------------------------------------- /backend/app/api/v1/endpoints/teachers.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Optional 2 | from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form 3 | from app.models.user import User, UserProfile, TeacherProfile 4 | from app.api.deps import get_current_teacher 5 | from app.db.mongodb import db 6 | from app.utils.file_upload import save_upload 7 | from bson import ObjectId 8 | import logging 9 | from datetime import datetime, timedelta 10 | 11 | router = APIRouter() 12 | logger = logging.getLogger(__name__) 13 | 14 | @router.get("/profile", response_model=dict) 15 | async def get_teacher_profile(current_user: User = Depends(get_current_teacher)) -> Any: 16 | """ 17 | Get current teacher's profile. 18 | """ 19 | # Get teacher profile 20 | profile = await db.teacher_profiles.find_one({"user_id": ObjectId(current_user.id)}) 21 | 22 | if not profile: 23 | # Create empty profile if it doesn't exist 24 | profile = { 25 | "user_id": ObjectId(current_user.id), 26 | "full_name": current_user.username, 27 | "bio": None, 28 | "profile_picture": None, 29 | "phone": None, 30 | "address": None, 31 | "department": None, 32 | "position": None, 33 | "office": None 34 | } 35 | 36 | await db.teacher_profiles.insert_one(profile) 37 | 38 | # Get courses data 39 | courses = [] 40 | courses_cursor = db.courses.find({"teacher_id": ObjectId(current_user.id)}) 41 | 42 | async for course in courses_cursor: 43 | # Get enrollment count 44 | enrollment_count = await db.enrollments.count_documents({ 45 | "course_id": course["_id"] 46 | }) 47 | 48 | courses.append({ 49 | "course_id": str(course["_id"]), 50 | "course_name": course["courseName"], 51 | "course_code": course["courseCode"], 52 | "students_count": enrollment_count, 53 | "status": course["enrollmentStatus"] 54 | }) 55 | 56 | # Format profile data 57 | profile["_id"] = str(profile["_id"]) 58 | profile["user_id"] = str(profile["user_id"]) 59 | 60 | return { 61 | "profile": profile, 62 | "courses": courses, 63 | "email": current_user.email, 64 | "username": current_user.username 65 | } 66 | 67 | @router.put("/profile", response_model=dict) 68 | async def update_teacher_profile( 69 | full_name: Optional[str] = Form(None), 70 | bio: Optional[str] = Form(None), 71 | phone: Optional[str] = Form(None), 72 | address: Optional[str] = Form(None), 73 | department: Optional[str] = Form(None), 74 | position: Optional[str] = Form(None), 75 | office: Optional[str] = Form(None), 76 | profile_picture: Optional[UploadFile] = File(None), 77 | current_user: User = Depends(get_current_teacher) 78 | ) -> Any: 79 | """ 80 | Update teacher profile. 81 | """ 82 | # Get current profile 83 | profile = await db.teacher_profiles.find_one({"user_id": ObjectId(current_user.id)}) 84 | 85 | if not profile: 86 | raise HTTPException( 87 | status_code=status.HTTP_404_NOT_FOUND, 88 | detail="Profile not found" 89 | ) 90 | 91 | # Prepare update data 92 | update_data = {} 93 | 94 | if full_name is not None: 95 | update_data["full_name"] = full_name 96 | 97 | if bio is not None: 98 | update_data["bio"] = bio 99 | 100 | if phone is not None: 101 | update_data["phone"] = phone 102 | 103 | if address is not None: 104 | update_data["address"] = address 105 | 106 | if department is not None: 107 | update_data["department"] = department 108 | 109 | if position is not None: 110 | update_data["position"] = position 111 | 112 | if office is not None: 113 | update_data["office"] = office 114 | 115 | # Save profile picture if provided 116 | if profile_picture: 117 | profile_picture_path = await save_upload(profile_picture, "profile_pictures") 118 | update_data["profile_picture"] = profile_picture_path 119 | 120 | # Update profile 121 | if update_data: 122 | await db.teacher_profiles.update_one( 123 | {"user_id": ObjectId(current_user.id)}, 124 | {"$set": update_data} 125 | ) 126 | 127 | # Get updated profile 128 | updated_profile = await db.teacher_profiles.find_one({"user_id": ObjectId(current_user.id)}) 129 | updated_profile["_id"] = str(updated_profile["_id"]) 130 | updated_profile["user_id"] = str(updated_profile["user_id"]) 131 | 132 | return { 133 | "message": "Profile updated successfully", 134 | "profile": updated_profile 135 | } 136 | 137 | @router.get("/dashboard", response_model=dict) 138 | async def get_teacher_dashboard(current_user: User = Depends(get_current_teacher)) -> Any: 139 | """ 140 | Get teacher dashboard data. 141 | """ 142 | # Get courses 143 | courses = [] 144 | courses_cursor = db.courses.find({"teacher_id": ObjectId(current_user.id)}) 145 | 146 | async for course in courses_cursor: 147 | # Get enrollment count 148 | enrollment_count = await db.enrollments.count_documents({ 149 | "course_id": course["_id"] 150 | }) 151 | 152 | courses.append({ 153 | "_id": str(course["_id"]), 154 | "name": course["courseName"], 155 | "code": course["courseCode"], 156 | "studentsCount": enrollment_count, 157 | "status": course["enrollmentStatus"] 158 | }) 159 | 160 | # Get total student count (unique students across all courses) 161 | student_ids = set() 162 | enrollments_cursor = db.enrollments.find({ 163 | "course_id": {"$in": [c["_id"] for c in await db.courses.find({"teacher_id": ObjectId(current_user.id)}).to_list(length=None)]} 164 | }) 165 | 166 | async for enrollment in enrollments_cursor: 167 | student_ids.add(str(enrollment["student_id"])) 168 | 169 | # Get pending assignments 170 | pending_assignments = [] 171 | assignments_cursor = db.assignments.find({ 172 | "teacher_id": ObjectId(current_user.id), 173 | "deadline": {"$gte": datetime.utcnow()} 174 | }).sort("deadline", 1).limit(5) 175 | 176 | async for assignment in assignments_cursor: 177 | # Get submission count 178 | submission_count = await db.assignment_submissions.count_documents({ 179 | "assignment_id": assignment["_id"] 180 | }) 181 | 182 | # Get total students in course 183 | total_students = await db.enrollments.count_documents({ 184 | "course_id": assignment["course"] 185 | }) 186 | 187 | pending_assignments.append({ 188 | "id": str(assignment["_id"]), 189 | "title": assignment["title"], 190 | "courseName": assignment["courseName"], 191 | "dueDate": assignment["deadline"].strftime("%b %d, %Y"), 192 | "submissionCount": submission_count, 193 | "totalStudents": total_students 194 | }) 195 | 196 | # Get upcoming classes (this would be more complex in a real app with scheduling) 197 | upcoming_classes = [ 198 | { 199 | "courseName": "Introduction to React", 200 | "topic": "React Hooks", 201 | "time": "10:00 AM", 202 | "date": datetime.utcnow().strftime("%b %d, %Y"), 203 | "studentsCount": 25 204 | }, 205 | { 206 | "courseName": "Advanced JavaScript", 207 | "topic": "Promises and Async/Await", 208 | "time": "2:00 PM", 209 | "date": datetime.utcnow().strftime("%b %d, %Y"), 210 | "studentsCount": 18 211 | }, 212 | { 213 | "courseName": "UX/UI Design Fundamentals", 214 | "topic": "User Research Methods", 215 | "time": "11:30 AM", 216 | "date": (datetime.utcnow() + timedelta(days=1)).strftime("%b %d, %Y"), 217 | "studentsCount": 22 218 | } 219 | ] 220 | 221 | return { 222 | "courses": courses, 223 | "totalStudents": len(student_ids), 224 | "pendingAssignments": pending_assignments, 225 | "upcomingClasses": upcoming_classes 226 | } 227 | 228 | --------------------------------------------------------------------------------