├── 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 | You need to enable JavaScript to run this app.
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 | You need to enable JavaScript to run this app.
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 |
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 |
22 |
23 |
24 |
25 | {/* Notification Icon */}
26 |
27 |
28 |
29 | {notifications.length > 0 && (
30 |
31 | {notifications.length}
32 |
33 | )}
34 |
35 |
36 |
37 | {/* User Profile Icon */}
38 |
39 |
40 |
41 | {studentName.trim() ? studentName.charAt(0).toUpperCase() : "G"}
42 |
43 |
44 | {studentName.trim() || "Guest"}
45 |
46 |
47 |
48 |
49 |
54 |
55 | Logout
56 |
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 |
27 |
28 |
29 |
30 | {/* Notification Icon */}
31 |
32 |
33 |
34 | {notifications.length > 0 && (
35 |
36 | {notifications.length}
37 |
38 | )}
39 |
40 |
41 |
42 | {/* User Profile Icon */}
43 |
44 |
45 |
46 | {teacherName.charAt(0).toUpperCase()}
47 |
48 |
49 | {teacherName}
50 |
51 |
52 |
53 |
54 | {/* Logout Button */}
55 |
56 |
61 |
62 | Logout
63 |
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 |
55 | Go Back
56 |
57 |
58 |
62 | 🚪 {/* Logout emoji instead of lucide icon */}
63 | Logout
64 |
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 |
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 |
97 |
98 |
99 |
100 |
101 |
102 | {/*
103 | Continue Learning
104 | */}
105 | handleViewMaterials(course._id)}
108 | >
109 | View Materials
110 |
111 | {/*
112 | Discussion Forum
113 | */}
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 |
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 |
38 |
39 |
40 |
48 |
49 | Dashboard Overview
50 |
51 |
52 |
60 |
61 | Course Management
62 |
63 |
64 | {/*
72 |
73 | Scheduling & Planning
74 | */}
75 |
76 |
84 |
85 | Student Management
86 |
87 |
88 |
96 |
97 | Teacher Management
98 |
99 |
100 |
108 |
109 | Communications
110 |
111 |
112 |
120 |
121 | Attendance & Evaluations
122 |
123 |
124 |
132 |
133 | Certifications
134 |
135 |
136 |
137 |
145 |
146 | Settings
147 |
148 |
149 |
150 |
154 |
155 | Sign Out
156 |
157 |
158 |
159 |
160 |
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 |
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 |
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 |
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 |
20 | Message Instructor
21 |
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 |
35 | Contact Support
36 |
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 |
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 |
89 | All Notifications
90 | Unread
91 | Assignments
92 | Announcements
93 | Grades
94 |
95 |
96 | {unreadCount > 0 && (
97 |
98 | Mark all as read
99 |
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 | markAsRead(notification._id)} className="text-sm text-blue-500 hover:text-blue-700">
143 | Mark as read
144 |
145 | )}
146 | deleteNotification(notification._id)} className="text-sm text-gray-500 hover:text-gray-700">
147 | Delete
148 |
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 |
navigate("/student-dashboard/courses")}
73 | className="mt-4 px-4 py-2 bg-blue-500 text-white rounded-lg"
74 | >
75 | Back to Courses
76 |
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 |
navigate("/student-dashboard/courses")}
167 | className="mt-6 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
168 | >
169 | Back to Courses
170 |
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 | setActiveFilter("all")}
76 | className={`px-4 py-2 text-sm ${activeFilter === "all" ? "bg-[#19a4db] text-white" : "bg-white text-gray-700"}`}
77 | >
78 | All
79 |
80 | setActiveFilter("active")}
82 | className={`px-4 py-2 text-sm ${activeFilter === "active" ? "bg-[#19a4db] text-white" : "bg-white text-gray-700"}`}
83 | >
84 | Active
85 |
86 | setActiveFilter("upcoming")}
88 | className={`px-4 py-2 text-sm ${activeFilter === "upcoming" ? "bg-[#19a4db] text-white" : "bg-white text-gray-700"}`}
89 | >
90 | Upcoming
91 |
92 | setActiveFilter("completed")}
94 | className={`px-4 py-2 text-sm ${activeFilter === "completed" ? "bg-[#19a4db] text-white" : "bg-white text-gray-700"}`}
95 | >
96 | Completed
97 |
98 |
*/}
99 |
setIsModalOpen(true)}
101 | className="flex items-center px-4 py-2 bg-[#19a4db] text-white rounded-lg text-sm"
102 | >
103 |
104 | Add Course
105 |
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 |
142 | navigate(`/teacher-dashboard/courses/${course._id}/manage`)
143 | }
144 | className="flex justify-center items-center px-4 py-2 bg-[#19a4db] text-white rounded-lg text-sm font-medium [width: 200%]"
145 | >
146 | Manage Course
147 |
148 | {/*
149 | View Materials
150 |
151 |
152 | Student Performance
153 | */}
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 |
setIsModalOpen(true)}
173 | className="px-4 py-2 bg-[#19a4db] text-white rounded-lg text-sm font-medium"
174 | >
175 | Create Your First Course
176 |
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 |
--------------------------------------------------------------------------------