From dc9cba22ebba38b1ea20b4af261d7900c51ad7db Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 16 Apr 2026 13:23:02 +0000
Subject: [PATCH 1/9] Initial plan
From c4b4003fa5c40e1c79b41ad469994e8fe770cf13 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 16 Apr 2026 13:41:20 +0000
Subject: [PATCH 2/9] feat: build complete EduScale full-stack MVP
- Backend: Node.js + Express + SQLite (better-sqlite3)
- JWT auth with bcryptjs (register, login, /me)
- Courses CRUD with search/category filter
- Video upload and Range-request streaming
- Enrollment management with progress tracking
- Assignments with file submission and grading
- Discussion threads with replies
- Announcements per course
- Quizzes with auto-grading
- PDF certificate generation (pdfkit + uuid)
- Admin dashboard (user management, stats)
- Multer for file uploads (videos + files)
- Frontend: React 18 + Vite + React Router v6
- AuthContext with localStorage JWT persistence
- Axios instance with auth interceptor
- Home: course catalog with search/filter
- Login & Register (with role selection)
- CourseDetail: sections, videos, discussions, announcements tabs
- VideoPlayer: HTML5 video, Range streaming, progress tracking, speed control
- StudentDashboard: enrollments + progress bars + upcoming assignments
- InstructorDashboard: course/section/video/assignment/quiz management
- AdminDashboard: user table with role changes + stats
- Certificates: list and download PDF certificates
- Responsive CSS with CSS variables (indigo theme)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Gabru44 <150784667+Gabru44@users.noreply.github.com>
---
.gitignore | 9 +
README.md | 112 +-
backend/db.js | 16 +
backend/middleware/auth.js | 50 +
backend/package-lock.json | 2367 ++++++++++++++++++++
backend/package.json | 20 +
backend/routes/admin.js | 49 +
backend/routes/announcements.js | 36 +
backend/routes/assignments.js | 150 ++
backend/routes/auth.js | 59 +
backend/routes/certificates.js | 114 +
backend/routes/courses.js | 124 +
backend/routes/discussions.js | 75 +
backend/routes/enrollments.js | 47 +
backend/routes/progress.js | 41 +
backend/routes/quizzes.js | 112 +
backend/routes/videos.js | 108 +
backend/schema.sql | 142 ++
backend/server.js | 41 +
backend/uploads/files/.gitkeep | 0
backend/uploads/videos/.gitkeep | 0
frontend/index.html | 12 +
frontend/package-lock.json | 2041 +++++++++++++++++
frontend/package.json | 20 +
frontend/src/App.jsx | 61 +
frontend/src/api.js | 23 +
frontend/src/components/Navbar.jsx | 54 +
frontend/src/context/AuthContext.jsx | 50 +
frontend/src/index.css | 476 ++++
frontend/src/main.jsx | 10 +
frontend/src/pages/AdminDashboard.jsx | 143 ++
frontend/src/pages/Certificates.jsx | 97 +
frontend/src/pages/CourseDetail.jsx | 227 ++
frontend/src/pages/Home.jsx | 114 +
frontend/src/pages/InstructorDashboard.jsx | 395 ++++
frontend/src/pages/Login.jsx | 68 +
frontend/src/pages/Register.jsx | 105 +
frontend/src/pages/StudentDashboard.jsx | 152 ++
frontend/src/pages/VideoPlayer.jsx | 164 ++
frontend/vite.config.js | 12 +
40 files changed, 7894 insertions(+), 2 deletions(-)
create mode 100644 .gitignore
create mode 100644 backend/db.js
create mode 100644 backend/middleware/auth.js
create mode 100644 backend/package-lock.json
create mode 100644 backend/package.json
create mode 100644 backend/routes/admin.js
create mode 100644 backend/routes/announcements.js
create mode 100644 backend/routes/assignments.js
create mode 100644 backend/routes/auth.js
create mode 100644 backend/routes/certificates.js
create mode 100644 backend/routes/courses.js
create mode 100644 backend/routes/discussions.js
create mode 100644 backend/routes/enrollments.js
create mode 100644 backend/routes/progress.js
create mode 100644 backend/routes/quizzes.js
create mode 100644 backend/routes/videos.js
create mode 100644 backend/schema.sql
create mode 100644 backend/server.js
create mode 100644 backend/uploads/files/.gitkeep
create mode 100644 backend/uploads/videos/.gitkeep
create mode 100644 frontend/index.html
create mode 100644 frontend/package-lock.json
create mode 100644 frontend/package.json
create mode 100644 frontend/src/App.jsx
create mode 100644 frontend/src/api.js
create mode 100644 frontend/src/components/Navbar.jsx
create mode 100644 frontend/src/context/AuthContext.jsx
create mode 100644 frontend/src/index.css
create mode 100644 frontend/src/main.jsx
create mode 100644 frontend/src/pages/AdminDashboard.jsx
create mode 100644 frontend/src/pages/Certificates.jsx
create mode 100644 frontend/src/pages/CourseDetail.jsx
create mode 100644 frontend/src/pages/Home.jsx
create mode 100644 frontend/src/pages/InstructorDashboard.jsx
create mode 100644 frontend/src/pages/Login.jsx
create mode 100644 frontend/src/pages/Register.jsx
create mode 100644 frontend/src/pages/StudentDashboard.jsx
create mode 100644 frontend/src/pages/VideoPlayer.jsx
create mode 100644 frontend/vite.config.js
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0579695
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules/
+*.db
+*.db-journal
+dist/
+.env
+backend/uploads/videos/*
+backend/uploads/files/*
+!backend/uploads/videos/.gitkeep
+!backend/uploads/files/.gitkeep
diff --git a/README.md b/README.md
index 9be96fe..14d45e6 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,110 @@
-# Edus
--
+# EduScale — Online Learning Platform MVP
+
+A full-stack online learning platform built with Node.js + Express + SQLite (backend) and React + Vite (frontend).
+
+## Features
+
+- **Auth**: Register/Login with JWT, roles: `student`, `instructor`, `admin`
+- **Courses**: Create, publish, search/filter course catalog
+- **Video Streaming**: Upload & stream videos with Range request support, progress tracking
+- **Enrollments**: Enroll/unenroll in courses
+- **Assignments**: Create, submit, and grade assignments
+- **Discussions**: Course discussion threads with replies
+- **Announcements**: Instructor announcements per course
+- **Quizzes**: Create quizzes with auto-grading
+- **Certificates**: PDF certificates on 100% course completion
+- **Admin Dashboard**: User management, role changes, platform stats
+
+## Tech Stack
+
+| Layer | Technology |
+|-----------|------------------------------------|
+| Backend | Node.js, Express, SQLite (better-sqlite3) |
+| Auth | bcryptjs, jsonwebtoken |
+| File Upload | multer |
+| PDF | pdfkit |
+| Frontend | React 18, React Router v6, Vite |
+| HTTP | Axios |
+
+## Project Structure
+
+```
+Edus/
+├── backend/
+│ ├── routes/ # API route handlers
+│ ├── middleware/ # JWT auth middleware
+│ ├── uploads/ # Uploaded files (videos, files)
+│ ├── db.js # SQLite initialization
+│ ├── schema.sql # Database schema
+│ └── server.js # Express entry point
+└── frontend/
+ └── src/
+ ├── pages/ # React pages
+ ├── components/ # Shared components
+ ├── context/ # Auth context
+ ├── api.js # Axios instance
+ └── index.css # Global styles
+```
+
+## Setup & Running
+
+### Prerequisites
+- Node.js v18+ (v20+ recommended)
+- npm
+
+### Backend
+
+```bash
+cd backend
+npm install
+npm start
+# Server runs on http://localhost:5000
+```
+
+### Frontend
+
+```bash
+cd frontend
+npm install
+npm run dev
+# App runs on http://localhost:3000
+```
+
+### Environment Variables
+
+Backend `.env` (already created):
+```
+JWT_SECRET=eduscale_super_secret_jwt_key_2024
+PORT=5000
+FRONTEND_URL=http://localhost:3000
+```
+
+## API Endpoints
+
+| Method | Path | Description |
+|--------|------|-------------|
+| POST | /api/register | Register user |
+| POST | /api/login | Login |
+| GET | /api/me | Current user |
+| GET | /api/courses | List published courses |
+| POST | /api/courses | Create course (instructor) |
+| GET | /api/courses/:id | Course detail |
+| POST | /api/enrollments | Enroll in course |
+| POST | /api/videos/upload | Upload video |
+| GET | /api/videos/:id | Stream video |
+| PUT | /api/videos/:id/progress | Update watch progress |
+| GET | /api/assignments/:courseId | List assignments |
+| POST | /api/assignments/:id/submit | Submit assignment |
+| GET | /api/discussions/:courseId | List discussions |
+| POST | /api/quizzes/:id/attempt | Submit quiz |
+| POST | /api/certificates/:courseId | Issue certificate |
+| GET | /api/certificates/:courseId/download | Download PDF |
+| GET | /api/admin/stats | Platform statistics (admin) |
+
+## Creating an Admin User
+
+Register normally, then update the role via SQLite or through an existing admin account:
+
+```bash
+sqlite3 backend/eduscale.db "UPDATE users SET role='admin' WHERE email='your@email.com';"
+```
diff --git a/backend/db.js b/backend/db.js
new file mode 100644
index 0000000..885cded
--- /dev/null
+++ b/backend/db.js
@@ -0,0 +1,16 @@
+const Database = require('better-sqlite3');
+const path = require('path');
+const fs = require('fs');
+
+const DB_PATH = path.join(__dirname, 'eduscale.db');
+const SCHEMA_PATH = path.join(__dirname, 'schema.sql');
+
+const db = new Database(DB_PATH);
+
+db.pragma('journal_mode = WAL');
+db.pragma('foreign_keys = ON');
+
+const schema = fs.readFileSync(SCHEMA_PATH, 'utf8');
+db.exec(schema);
+
+module.exports = db;
diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js
new file mode 100644
index 0000000..09b0273
--- /dev/null
+++ b/backend/middleware/auth.js
@@ -0,0 +1,50 @@
+const jwt = require('jsonwebtoken');
+const db = require('../db');
+
+const JWT_SECRET = process.env.JWT_SECRET || 'eduscale_super_secret_jwt_key_2024';
+
+function authenticate(req, res, next) {
+ const authHeader = req.headers.authorization;
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ return res.status(401).json({ error: 'No token provided' });
+ }
+ const token = authHeader.slice(7);
+ try {
+ const payload = jwt.verify(token, JWT_SECRET);
+ const user = db.prepare('SELECT id, name, email, role FROM users WHERE id = ?').get(payload.id);
+ if (!user) return res.status(401).json({ error: 'User not found' });
+ req.user = user;
+ next();
+ } catch {
+ return res.status(401).json({ error: 'Invalid token' });
+ }
+}
+
+function optionalAuth(req, res, next) {
+ const authHeader = req.headers.authorization;
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ req.user = null;
+ return next();
+ }
+ const token = authHeader.slice(7);
+ try {
+ const payload = jwt.verify(token, JWT_SECRET);
+ const user = db.prepare('SELECT id, name, email, role FROM users WHERE id = ?').get(payload.id);
+ req.user = user || null;
+ } catch {
+ req.user = null;
+ }
+ next();
+}
+
+function requireRole(...roles) {
+ return (req, res, next) => {
+ if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
+ if (!roles.includes(req.user.role)) {
+ return res.status(403).json({ error: 'Insufficient permissions' });
+ }
+ next();
+ };
+}
+
+module.exports = { authenticate, optionalAuth, requireRole };
diff --git a/backend/package-lock.json b/backend/package-lock.json
new file mode 100644
index 0000000..ce82533
--- /dev/null
+++ b/backend/package-lock.json
@@ -0,0 +1,2367 @@
+{
+ "name": "eduscale-backend",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "eduscale-backend",
+ "version": "1.0.0",
+ "dependencies": {
+ "bcryptjs": "^2.4.3",
+ "better-sqlite3": "^12.9.0",
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.1",
+ "express": "^4.18.2",
+ "jsonwebtoken": "^9.0.2",
+ "multer": "^1.4.5-lts.1",
+ "pdfkit": "^0.14.0",
+ "uuid": "^9.0.0"
+ }
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.3.17",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.3.17.tgz",
+ "integrity": "sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/append-field": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+ "license": "MIT"
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/bcryptjs": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
+ "license": "MIT"
+ },
+ "node_modules/better-sqlite3": {
+ "version": "12.9.0",
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz",
+ "integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "prebuild-install": "^7.1.1"
+ },
+ "engines": {
+ "node": "20.x || 22.x || 23.x || 24.x || 25.x"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bl/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/brotli": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
+ "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.1.2"
+ }
+ },
+ "node_modules/browserify-zlib": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+ "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "~1.0.5"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
+ "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "get-intrinsic": "^1.3.0",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
+ "node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "engines": [
+ "node >= 0.8"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/crypto-js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-equal": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
+ "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "call-bind": "^1.0.5",
+ "es-get-iterator": "^1.1.3",
+ "get-intrinsic": "^1.2.2",
+ "is-arguments": "^1.1.1",
+ "is-array-buffer": "^3.0.2",
+ "is-date-object": "^1.0.5",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "isarray": "^2.0.5",
+ "object-is": "^1.1.5",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.5.1",
+ "side-channel": "^1.0.4",
+ "which-boxed-primitive": "^1.0.2",
+ "which-collection": "^1.0.1",
+ "which-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dfa": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
+ "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
+ "license": "MIT"
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-get-iterator": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
+ "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "has-symbols": "^1.0.3",
+ "is-arguments": "^1.1.1",
+ "is-map": "^2.0.2",
+ "is-set": "^2.0.2",
+ "is-string": "^1.0.7",
+ "isarray": "^2.0.5",
+ "stop-iteration-iterator": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "license": "MIT"
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fontkit": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.9.0.tgz",
+ "integrity": "sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==",
+ "license": "MIT",
+ "dependencies": {
+ "@swc/helpers": "^0.3.13",
+ "brotli": "^1.3.2",
+ "clone": "^2.1.2",
+ "deep-equal": "^2.0.5",
+ "dfa": "^1.2.0",
+ "restructure": "^2.0.1",
+ "tiny-inflate": "^1.0.3",
+ "unicode-properties": "^1.3.1",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-arguments": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
+ "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "license": "MIT"
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^4.0.1",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/linebreak": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
+ "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "0.0.8",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/linebreak/node_modules/base64-js": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
+ "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/multer": {
+ "version": "1.4.5-lts.2",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
+ "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
+ "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.",
+ "license": "MIT",
+ "dependencies": {
+ "append-field": "^1.0.0",
+ "busboy": "^1.0.0",
+ "concat-stream": "^1.5.2",
+ "mkdirp": "^0.5.4",
+ "object-assign": "^4.1.1",
+ "type-is": "^1.6.4",
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-abi": {
+ "version": "3.89.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz",
+ "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-is": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+ "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/pdfkit": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.14.0.tgz",
+ "integrity": "sha512-Hnor8/78jhHm6ONrxWhrqOwAVALlBnFyWOF8sstBZMiqHZgZ5A6RU+Q3yahhw82plxpT7LOfH3b3qcOX6rzMQg==",
+ "license": "MIT",
+ "dependencies": {
+ "crypto-js": "^4.2.0",
+ "fontkit": "^1.8.1",
+ "linebreak": "^1.0.2",
+ "png-js": "^1.0.0"
+ }
+ },
+ "node_modules/png-js": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.1.0.tgz",
+ "integrity": "sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==",
+ "dependencies": {
+ "browserify-zlib": "^0.2.0"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/readable-stream/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/readable-stream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/restructure": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz",
+ "integrity": "sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==",
+ "license": "MIT"
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/string_decoder/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar-stream/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/tiny-inflate": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
+ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
+ "license": "MIT"
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+ "license": "MIT"
+ },
+ "node_modules/unicode-properties": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
+ "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/unicode-trie": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
+ "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "^0.2.5",
+ "tiny-inflate": "^1.0.0"
+ }
+ },
+ "node_modules/unicode-trie/node_modules/pako": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.20",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
+ "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ }
+ }
+}
diff --git a/backend/package.json b/backend/package.json
new file mode 100644
index 0000000..fa34573
--- /dev/null
+++ b/backend/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "eduscale-backend",
+ "version": "1.0.0",
+ "type": "commonjs",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "nodemon server.js"
+ },
+ "dependencies": {
+ "bcryptjs": "^2.4.3",
+ "better-sqlite3": "^12.9.0",
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.1",
+ "express": "^4.18.2",
+ "jsonwebtoken": "^9.0.2",
+ "multer": "^1.4.5-lts.1",
+ "pdfkit": "^0.14.0",
+ "uuid": "^9.0.0"
+ }
+}
diff --git a/backend/routes/admin.js b/backend/routes/admin.js
new file mode 100644
index 0000000..4b59c8c
--- /dev/null
+++ b/backend/routes/admin.js
@@ -0,0 +1,49 @@
+const express = require('express');
+const db = require('../db');
+const { authenticate, requireRole } = require('../middleware/auth');
+
+const router = express.Router();
+
+router.get('/admin/users', authenticate, requireRole('admin'), (req, res) => {
+ const users = db.prepare('SELECT id, name, email, role, created_at FROM users ORDER BY created_at DESC').all();
+ res.json({ users });
+});
+
+router.put('/admin/users/:id/role', authenticate, requireRole('admin'), (req, res) => {
+ const { role } = req.body;
+ if (!['student', 'instructor', 'admin'].includes(role)) {
+ return res.status(400).json({ error: 'Invalid role' });
+ }
+ const user = db.prepare('SELECT id FROM users WHERE id = ?').get(req.params.id);
+ if (!user) return res.status(404).json({ error: 'User not found' });
+
+ db.prepare('UPDATE users SET role = ? WHERE id = ?').run(role, req.params.id);
+ const updated = db.prepare('SELECT id, name, email, role, created_at FROM users WHERE id = ?').get(req.params.id);
+ res.json({ user: updated });
+});
+
+router.get('/admin/stats', authenticate, requireRole('admin'), (req, res) => {
+ const userCount = db.prepare('SELECT COUNT(*) AS cnt FROM users').get().cnt;
+ const courseCount = db.prepare('SELECT COUNT(*) AS cnt FROM courses').get().cnt;
+ const publishedCount = db.prepare("SELECT COUNT(*) AS cnt FROM courses WHERE status='published'").get().cnt;
+ const enrollmentCount = db.prepare('SELECT COUNT(*) AS cnt FROM enrollments').get().cnt;
+ const studentCount = db.prepare("SELECT COUNT(*) AS cnt FROM users WHERE role='student'").get().cnt;
+ const instructorCount = db.prepare("SELECT COUNT(*) AS cnt FROM users WHERE role='instructor'").get().cnt;
+ const submissionCount = db.prepare('SELECT COUNT(*) AS cnt FROM submissions').get().cnt;
+ const certificateCount = db.prepare('SELECT COUNT(*) AS cnt FROM certificates').get().cnt;
+
+ res.json({
+ stats: {
+ userCount,
+ courseCount,
+ publishedCount,
+ enrollmentCount,
+ studentCount,
+ instructorCount,
+ submissionCount,
+ certificateCount
+ }
+ });
+});
+
+module.exports = router;
diff --git a/backend/routes/announcements.js b/backend/routes/announcements.js
new file mode 100644
index 0000000..0c2b8ca
--- /dev/null
+++ b/backend/routes/announcements.js
@@ -0,0 +1,36 @@
+const express = require('express');
+const db = require('../db');
+const { authenticate, requireRole } = require('../middleware/auth');
+
+const router = express.Router();
+
+router.get('/announcements/:courseId', authenticate, (req, res) => {
+ const announcements = db.prepare(`
+ SELECT a.*, u.name AS instructor_name
+ FROM announcements a
+ JOIN users u ON a.instructor_id = u.id
+ WHERE a.course_id = ?
+ ORDER BY a.created_at DESC
+ `).all(req.params.courseId);
+ res.json({ announcements });
+});
+
+router.post('/announcements', authenticate, requireRole('instructor', 'admin'), (req, res) => {
+ const { course_id, title, body } = req.body;
+ if (!course_id || !title) return res.status(400).json({ error: 'course_id and title are required' });
+
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(course_id);
+ if (!course) return res.status(404).json({ error: 'Course not found' });
+ if (course.instructor_id !== req.user.id && req.user.role !== 'admin') {
+ return res.status(403).json({ error: 'Forbidden' });
+ }
+
+ const result = db.prepare(
+ 'INSERT INTO announcements (course_id, instructor_id, title, body) VALUES (?, ?, ?, ?)'
+ ).run(course_id, req.user.id, title, body || null);
+
+ const announcement = db.prepare('SELECT * FROM announcements WHERE id = ?').get(result.lastInsertRowid);
+ res.status(201).json({ announcement });
+});
+
+module.exports = router;
diff --git a/backend/routes/assignments.js b/backend/routes/assignments.js
new file mode 100644
index 0000000..795fa14
--- /dev/null
+++ b/backend/routes/assignments.js
@@ -0,0 +1,150 @@
+const express = require('express');
+const multer = require('multer');
+const path = require('path');
+const db = require('../db');
+const { authenticate, requireRole } = require('../middleware/auth');
+
+const router = express.Router();
+
+const storage = multer.diskStorage({
+ destination: path.join(__dirname, '../uploads/files'),
+ filename: (req, file, cb) => {
+ const unique = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
+ cb(null, unique + path.extname(file.originalname));
+ }
+});
+const upload = multer({ storage, limits: { fileSize: 50 * 1024 * 1024 } });
+
+router.get('/assignments/:courseId', authenticate, (req, res) => {
+ const assignments = db.prepare(
+ 'SELECT * FROM assignments WHERE course_id = ? ORDER BY due_date ASC'
+ ).all(req.params.courseId);
+ res.json({ assignments });
+});
+
+router.post('/assignments', authenticate, requireRole('instructor', 'admin'), (req, res) => {
+ const { course_id, section_id, title, description, due_date, max_points } = req.body;
+ if (!course_id || !title) return res.status(400).json({ error: 'course_id and title are required' });
+
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(course_id);
+ if (!course) return res.status(404).json({ error: 'Course not found' });
+ if (course.instructor_id !== req.user.id && req.user.role !== 'admin') {
+ return res.status(403).json({ error: 'Forbidden' });
+ }
+
+ const result = db.prepare(
+ `INSERT INTO assignments (course_id, section_id, title, description, due_date, max_points)
+ VALUES (?, ?, ?, ?, ?, ?)`
+ ).run(course_id, section_id || null, title, description || null, due_date || null, max_points || 100);
+
+ const assignment = db.prepare('SELECT * FROM assignments WHERE id = ?').get(result.lastInsertRowid);
+ res.status(201).json({ assignment });
+});
+
+router.put('/assignments/:id', authenticate, requireRole('instructor', 'admin'), (req, res) => {
+ const assignment = db.prepare('SELECT * FROM assignments WHERE id = ?').get(req.params.id);
+ if (!assignment) return res.status(404).json({ error: 'Assignment not found' });
+
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(assignment.course_id);
+ if (course.instructor_id !== req.user.id && req.user.role !== 'admin') {
+ return res.status(403).json({ error: 'Forbidden' });
+ }
+
+ const { title, description, due_date, max_points } = req.body;
+ db.prepare(
+ 'UPDATE assignments SET title=?, description=?, due_date=?, max_points=? WHERE id=?'
+ ).run(
+ title ?? assignment.title,
+ description ?? assignment.description,
+ due_date ?? assignment.due_date,
+ max_points ?? assignment.max_points,
+ assignment.id
+ );
+ const updated = db.prepare('SELECT * FROM assignments WHERE id = ?').get(assignment.id);
+ res.json({ assignment: updated });
+});
+
+router.delete('/assignments/:id', authenticate, requireRole('instructor', 'admin'), (req, res) => {
+ const assignment = db.prepare('SELECT * FROM assignments WHERE id = ?').get(req.params.id);
+ if (!assignment) return res.status(404).json({ error: 'Assignment not found' });
+
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(assignment.course_id);
+ if (course.instructor_id !== req.user.id && req.user.role !== 'admin') {
+ return res.status(403).json({ error: 'Forbidden' });
+ }
+
+ db.prepare('DELETE FROM assignments WHERE id = ?').run(assignment.id);
+ res.json({ message: 'Assignment deleted' });
+});
+
+router.post('/assignments/:id/submit', authenticate, upload.single('file'), (req, res) => {
+ const assignment = db.prepare('SELECT * FROM assignments WHERE id = ?').get(req.params.id);
+ if (!assignment) return res.status(404).json({ error: 'Assignment not found' });
+
+ const enrollment = db.prepare('SELECT * FROM enrollments WHERE user_id = ? AND course_id = ?')
+ .get(req.user.id, assignment.course_id);
+ if (!enrollment) return res.status(403).json({ error: 'Not enrolled in this course' });
+
+ const { content } = req.body;
+ const file_path = req.file ? req.file.filename : null;
+
+ const existing = db.prepare('SELECT * FROM submissions WHERE assignment_id = ? AND user_id = ?')
+ .get(assignment.id, req.user.id);
+
+ let submission;
+ if (existing) {
+ db.prepare(
+ "UPDATE submissions SET content=?, file_path=?, submitted_at=datetime('now') WHERE id=?"
+ ).run(content || null, file_path || existing.file_path, existing.id);
+ submission = db.prepare('SELECT * FROM submissions WHERE id = ?').get(existing.id);
+ } else {
+ const result = db.prepare(
+ 'INSERT INTO submissions (assignment_id, user_id, content, file_path) VALUES (?, ?, ?, ?)'
+ ).run(assignment.id, req.user.id, content || null, file_path);
+ submission = db.prepare('SELECT * FROM submissions WHERE id = ?').get(result.lastInsertRowid);
+ }
+ res.status(201).json({ submission });
+});
+
+router.get('/assignments/:id/submissions', authenticate, (req, res) => {
+ const assignment = db.prepare('SELECT * FROM assignments WHERE id = ?').get(req.params.id);
+ if (!assignment) return res.status(404).json({ error: 'Assignment not found' });
+
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(assignment.course_id);
+ const isInstructor = course.instructor_id === req.user.id || req.user.role === 'admin';
+
+ let submissions;
+ if (isInstructor) {
+ submissions = db.prepare(`
+ SELECT s.*, u.name AS student_name, u.email AS student_email
+ FROM submissions s
+ JOIN users u ON s.user_id = u.id
+ WHERE s.assignment_id = ?
+ ORDER BY s.submitted_at DESC
+ `).all(assignment.id);
+ } else {
+ submissions = db.prepare('SELECT * FROM submissions WHERE assignment_id = ? AND user_id = ?')
+ .all(assignment.id, req.user.id);
+ }
+ res.json({ submissions });
+});
+
+router.put('/assignments/:id/grade/:submissionId', authenticate, requireRole('instructor', 'admin'), (req, res) => {
+ const assignment = db.prepare('SELECT * FROM assignments WHERE id = ?').get(req.params.id);
+ if (!assignment) return res.status(404).json({ error: 'Assignment not found' });
+
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(assignment.course_id);
+ if (course.instructor_id !== req.user.id && req.user.role !== 'admin') {
+ return res.status(403).json({ error: 'Forbidden' });
+ }
+
+ const { grade, feedback } = req.body;
+ db.prepare(
+ "UPDATE submissions SET grade=?, feedback=?, graded_at=datetime('now') WHERE id=? AND assignment_id=?"
+ ).run(grade, feedback || null, req.params.submissionId, assignment.id);
+
+ const submission = db.prepare('SELECT * FROM submissions WHERE id = ?').get(req.params.submissionId);
+ res.json({ submission });
+});
+
+module.exports = router;
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
new file mode 100644
index 0000000..660b426
--- /dev/null
+++ b/backend/routes/auth.js
@@ -0,0 +1,59 @@
+const express = require('express');
+const bcrypt = require('bcryptjs');
+const jwt = require('jsonwebtoken');
+const db = require('../db');
+const { authenticate } = require('../middleware/auth');
+
+const router = express.Router();
+const JWT_SECRET = process.env.JWT_SECRET || 'eduscale_super_secret_jwt_key_2024';
+
+function signToken(user) {
+ return jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, { expiresIn: '7d' });
+}
+
+router.post('/register', (req, res) => {
+ const { name, email, password, role } = req.body;
+ if (!name || !email || !password) {
+ return res.status(400).json({ error: 'name, email, and password are required' });
+ }
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+ return res.status(400).json({ error: 'Invalid email format' });
+ }
+ if (password.length < 6) {
+ return res.status(400).json({ error: 'Password must be at least 6 characters' });
+ }
+ const allowedRoles = ['student', 'instructor'];
+ const userRole = allowedRoles.includes(role) ? role : 'student';
+
+ const existing = db.prepare('SELECT id FROM users WHERE email = ?').get(email);
+ if (existing) return res.status(409).json({ error: 'Email already registered' });
+
+ const password_hash = bcrypt.hashSync(password, 10);
+ const result = db.prepare(
+ 'INSERT INTO users (name, email, password_hash, role) VALUES (?, ?, ?, ?)'
+ ).run(name, email, password_hash, userRole);
+
+ const user = db.prepare('SELECT id, name, email, role FROM users WHERE id = ?').get(result.lastInsertRowid);
+ const token = signToken(user);
+ res.status(201).json({ token, user });
+});
+
+router.post('/login', (req, res) => {
+ const { email, password } = req.body;
+ if (!email || !password) {
+ return res.status(400).json({ error: 'email and password are required' });
+ }
+ const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email);
+ if (!user || !bcrypt.compareSync(password, user.password_hash)) {
+ return res.status(401).json({ error: 'Invalid credentials' });
+ }
+ const { password_hash, ...safeUser } = user;
+ const token = signToken(safeUser);
+ res.json({ token, user: safeUser });
+});
+
+router.get('/me', authenticate, (req, res) => {
+ res.json({ user: req.user });
+});
+
+module.exports = router;
diff --git a/backend/routes/certificates.js b/backend/routes/certificates.js
new file mode 100644
index 0000000..76ef818
--- /dev/null
+++ b/backend/routes/certificates.js
@@ -0,0 +1,114 @@
+const express = require('express');
+const { v4: uuidv4 } = require('uuid');
+const PDFDocument = require('pdfkit');
+const db = require('../db');
+const { authenticate } = require('../middleware/auth');
+
+const router = express.Router();
+
+router.post('/certificates/:courseId', authenticate, (req, res) => {
+ const { courseId } = req.params;
+
+ const enrollment = db.prepare('SELECT * FROM enrollments WHERE user_id = ? AND course_id = ?')
+ .get(req.user.id, courseId);
+ if (!enrollment) return res.status(403).json({ error: 'Not enrolled in this course' });
+
+ const totalVideos = db.prepare('SELECT COUNT(*) AS cnt FROM videos WHERE course_id = ?').get(courseId).cnt;
+ const completedVideos = db.prepare(`
+ SELECT COUNT(*) AS cnt
+ FROM progress p
+ JOIN videos v ON p.video_id = v.id
+ WHERE p.user_id = ? AND v.course_id = ? AND p.completed = 1
+ `).get(req.user.id, courseId).cnt;
+
+ if (totalVideos > 0 && completedVideos < totalVideos) {
+ return res.status(400).json({ error: 'Course not 100% completed yet', progress: (completedVideos / totalVideos) * 100 });
+ }
+
+ const existing = db.prepare('SELECT * FROM certificates WHERE user_id = ? AND course_id = ?')
+ .get(req.user.id, courseId);
+ if (existing) return res.json({ certificate: existing });
+
+ const cert_uuid = uuidv4();
+ const result = db.prepare(
+ 'INSERT INTO certificates (user_id, course_id, cert_uuid) VALUES (?, ?, ?)'
+ ).run(req.user.id, courseId, cert_uuid);
+
+ const certificate = db.prepare('SELECT * FROM certificates WHERE id = ?').get(result.lastInsertRowid);
+ res.status(201).json({ certificate });
+});
+
+router.get('/certificates/:courseId', authenticate, (req, res) => {
+ const certificate = db.prepare('SELECT * FROM certificates WHERE user_id = ? AND course_id = ?')
+ .get(req.user.id, req.params.courseId);
+ if (!certificate) return res.status(404).json({ error: 'Certificate not found' });
+ res.json({ certificate });
+});
+
+router.get('/certificates/:courseId/download', authenticate, (req, res) => {
+ const certificate = db.prepare('SELECT * FROM certificates WHERE user_id = ? AND course_id = ?')
+ .get(req.user.id, req.params.courseId);
+ if (!certificate) return res.status(404).json({ error: 'Certificate not found' });
+
+ const user = db.prepare('SELECT name FROM users WHERE id = ?').get(req.user.id);
+ const course = db.prepare('SELECT title FROM courses WHERE id = ?').get(req.params.courseId);
+
+ const doc = new PDFDocument({ size: 'A4', layout: 'landscape', margin: 50 });
+ res.setHeader('Content-Type', 'application/pdf');
+ res.setHeader('Content-Disposition', `attachment; filename="certificate-${certificate.cert_uuid}.pdf"`);
+ doc.pipe(res);
+
+ doc.rect(0, 0, doc.page.width, doc.page.height).fill('#f8f6ff');
+
+ doc.rect(20, 20, doc.page.width - 40, doc.page.height - 40)
+ .lineWidth(3).stroke('#4f46e5');
+
+ doc.rect(30, 30, doc.page.width - 60, doc.page.height - 60)
+ .lineWidth(1).stroke('#818cf8');
+
+ doc.fillColor('#4f46e5')
+ .fontSize(42)
+ .font('Helvetica-Bold')
+ .text('EduScale', 0, 80, { align: 'center' });
+
+ doc.fillColor('#374151')
+ .fontSize(20)
+ .font('Helvetica')
+ .text('Certificate of Completion', 0, 140, { align: 'center' });
+
+ doc.fillColor('#6b7280')
+ .fontSize(14)
+ .text('This is to certify that', 0, 200, { align: 'center' });
+
+ doc.fillColor('#111827')
+ .fontSize(32)
+ .font('Helvetica-Bold')
+ .text(user.name, 0, 230, { align: 'center' });
+
+ doc.fillColor('#6b7280')
+ .fontSize(14)
+ .font('Helvetica')
+ .text('has successfully completed the course', 0, 290, { align: 'center' });
+
+ doc.fillColor('#4f46e5')
+ .fontSize(24)
+ .font('Helvetica-Bold')
+ .text(course.title, 0, 320, { align: 'center' });
+
+ const issuedDate = new Date(certificate.issued_at).toLocaleDateString('en-US', {
+ year: 'numeric', month: 'long', day: 'numeric'
+ });
+
+ doc.fillColor('#6b7280')
+ .fontSize(12)
+ .font('Helvetica')
+ .text(`Issued on: ${issuedDate}`, 0, 390, { align: 'center' });
+
+ doc.fillColor('#9ca3af')
+ .fontSize(9)
+ .text(`Certificate ID: ${certificate.cert_uuid}`, 0, 420, { align: 'center' });
+
+ doc.end();
+});
+
+module.exports = router;
diff --git a/backend/routes/courses.js b/backend/routes/courses.js
new file mode 100644
index 0000000..c745ca8
--- /dev/null
+++ b/backend/routes/courses.js
@@ -0,0 +1,124 @@
+const express = require('express');
+const db = require('../db');
+const { authenticate, optionalAuth, requireRole } = require('../middleware/auth');
+
+const router = express.Router();
+
+router.get('/courses', optionalAuth, (req, res) => {
+ const { search, category } = req.query;
+ let query = `
+ SELECT c.*, u.name AS instructor_name
+ FROM courses c
+ JOIN users u ON c.instructor_id = u.id
+ WHERE c.status = 'published'
+ `;
+ const params = [];
+ if (search) {
+ query += ` AND (c.title LIKE ? OR c.description LIKE ?)`;
+ params.push(`%${search}%`, `%${search}%`);
+ }
+ if (category) {
+ query += ` AND c.category = ?`;
+ params.push(category);
+ }
+ query += ' ORDER BY c.created_at DESC';
+ const courses = db.prepare(query).all(...params);
+ res.json({ courses });
+});
+
+router.post('/courses', authenticate, requireRole('instructor', 'admin'), (req, res) => {
+ const { title, description, category, status, price, thumbnail } = req.body;
+ if (!title) return res.status(400).json({ error: 'title is required' });
+ const result = db.prepare(
+ `INSERT INTO courses (instructor_id, title, description, category, status, price, thumbnail)
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
+ ).run(req.user.id, title, description || null, category || null,
+ status === 'published' ? 'published' : 'draft',
+ price || 0, thumbnail || null);
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(result.lastInsertRowid);
+ res.status(201).json({ course });
+});
+
+router.get('/courses/:id', optionalAuth, (req, res) => {
+ const course = db.prepare(`
+ SELECT c.*, u.name AS instructor_name
+ FROM courses c
+ JOIN users u ON c.instructor_id = u.id
+ WHERE c.id = ?
+ `).get(req.params.id);
+ if (!course) return res.status(404).json({ error: 'Course not found' });
+
+ const sections = db.prepare('SELECT * FROM sections WHERE course_id = ? ORDER BY order_index').all(course.id);
+ for (const section of sections) {
+ section.videos = db.prepare('SELECT * FROM videos WHERE section_id = ? ORDER BY order_index').all(section.id);
+ section.assignments = db.prepare('SELECT * FROM assignments WHERE section_id = ? ORDER BY created_at').all(section.id);
+ section.quizzes = db.prepare('SELECT * FROM quizzes WHERE section_id = ? ORDER BY created_at').all(section.id);
+ }
+
+ let enrolled = false;
+ if (req.user) {
+ const enrollment = db.prepare('SELECT id FROM enrollments WHERE user_id = ? AND course_id = ?').get(req.user.id, course.id);
+ enrolled = !!enrollment;
+ }
+
+ res.json({ course, sections, enrolled });
+});
+
+router.put('/courses/:id', authenticate, (req, res) => {
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(req.params.id);
+ if (!course) return res.status(404).json({ error: 'Course not found' });
+ if (course.instructor_id !== req.user.id && req.user.role !== 'admin') {
+ return res.status(403).json({ error: 'Forbidden' });
+ }
+ const { title, description, category, status, price, thumbnail } = req.body;
+ db.prepare(`
+ UPDATE courses SET title=?, description=?, category=?, status=?, price=?, thumbnail=? WHERE id=?
+ `).run(
+ title ?? course.title,
+ description ?? course.description,
+ category ?? course.category,
+ status ?? course.status,
+ price ?? course.price,
+ thumbnail ?? course.thumbnail,
+ course.id
+ );
+ const updated = db.prepare('SELECT * FROM courses WHERE id = ?').get(course.id);
+ res.json({ course: updated });
+});
+
+router.delete('/courses/:id', authenticate, (req, res) => {
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(req.params.id);
+ if (!course) return res.status(404).json({ error: 'Course not found' });
+ if (course.instructor_id !== req.user.id && req.user.role !== 'admin') {
+ return res.status(403).json({ error: 'Forbidden' });
+ }
+ db.prepare('DELETE FROM courses WHERE id = ?').run(course.id);
+ res.json({ message: 'Course deleted' });
+});
+
+router.post('/courses/:id/sections', authenticate, requireRole('instructor', 'admin'), (req, res) => {
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(req.params.id);
+ if (!course) return res.status(404).json({ error: 'Course not found' });
+ if (course.instructor_id !== req.user.id && req.user.role !== 'admin') {
+ return res.status(403).json({ error: 'Forbidden' });
+ }
+ const { title, order_index } = req.body;
+ if (!title) return res.status(400).json({ error: 'title is required' });
+ const result = db.prepare(
+ 'INSERT INTO sections (course_id, title, order_index) VALUES (?, ?, ?)'
+ ).run(course.id, title, order_index || 0);
+ const section = db.prepare('SELECT * FROM sections WHERE id = ?').get(result.lastInsertRowid);
+ res.status(201).json({ section });
+});
+
+router.delete('/courses/:id/sections/:sectionId', authenticate, (req, res) => {
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(req.params.id);
+ if (!course) return res.status(404).json({ error: 'Course not found' });
+ if (course.instructor_id !== req.user.id && req.user.role !== 'admin') {
+ return res.status(403).json({ error: 'Forbidden' });
+ }
+ db.prepare('DELETE FROM sections WHERE id = ? AND course_id = ?').run(req.params.sectionId, course.id);
+ res.json({ message: 'Section deleted' });
+});
+
+module.exports = router;
diff --git a/backend/routes/discussions.js b/backend/routes/discussions.js
new file mode 100644
index 0000000..306bc2f
--- /dev/null
+++ b/backend/routes/discussions.js
@@ -0,0 +1,75 @@
+const express = require('express');
+const db = require('../db');
+const { authenticate } = require('../middleware/auth');
+
+const router = express.Router();
+
+router.get('/discussions/:courseId', authenticate, (req, res) => {
+ const discussions = db.prepare(`
+ SELECT d.*, u.name AS author_name,
+ (SELECT COUNT(*) FROM discussion_replies WHERE discussion_id = d.id) AS reply_count
+ FROM discussions d
+ JOIN users u ON d.user_id = u.id
+ WHERE d.course_id = ?
+ ORDER BY d.created_at DESC
+ `).all(req.params.courseId);
+ res.json({ discussions });
+});
+
+router.post('/discussions', authenticate, (req, res) => {
+ const { course_id, title, body } = req.body;
+ if (!course_id || !title) return res.status(400).json({ error: 'course_id and title are required' });
+
+ const enrollment = db.prepare('SELECT * FROM enrollments WHERE user_id = ? AND course_id = ?')
+ .get(req.user.id, course_id);
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(course_id);
+ const isInstructor = course && (course.instructor_id === req.user.id || req.user.role === 'admin');
+
+ if (!enrollment && !isInstructor) {
+ return res.status(403).json({ error: 'Must be enrolled or instructor to post discussions' });
+ }
+
+ const result = db.prepare(
+ 'INSERT INTO discussions (course_id, user_id, title, body) VALUES (?, ?, ?, ?)'
+ ).run(course_id, req.user.id, title, body || null);
+
+ const discussion = db.prepare('SELECT * FROM discussions WHERE id = ?').get(result.lastInsertRowid);
+ res.status(201).json({ discussion });
+});
+
+router.get('/discussions/:courseId/:discussionId', authenticate, (req, res) => {
+ const discussion = db.prepare(`
+ SELECT d.*, u.name AS author_name
+ FROM discussions d
+ JOIN users u ON d.user_id = u.id
+ WHERE d.id = ? AND d.course_id = ?
+ `).get(req.params.discussionId, req.params.courseId);
+ if (!discussion) return res.status(404).json({ error: 'Discussion not found' });
+
+ const replies = db.prepare(`
+ SELECT r.*, u.name AS author_name
+ FROM discussion_replies r
+ JOIN users u ON r.user_id = u.id
+ WHERE r.discussion_id = ?
+ ORDER BY r.created_at ASC
+ `).all(discussion.id);
+
+ res.json({ discussion, replies });
+});
+
+router.post('/discussions/:discussionId/reply', authenticate, (req, res) => {
+ const discussion = db.prepare('SELECT * FROM discussions WHERE id = ?').get(req.params.discussionId);
+ if (!discussion) return res.status(404).json({ error: 'Discussion not found' });
+
+ const { body } = req.body;
+ if (!body) return res.status(400).json({ error: 'body is required' });
+
+ const result = db.prepare(
+ 'INSERT INTO discussion_replies (discussion_id, user_id, body) VALUES (?, ?, ?)'
+ ).run(discussion.id, req.user.id, body);
+
+ const reply = db.prepare('SELECT * FROM discussion_replies WHERE id = ?').get(result.lastInsertRowid);
+ res.status(201).json({ reply });
+});
+
+module.exports = router;
diff --git a/backend/routes/enrollments.js b/backend/routes/enrollments.js
new file mode 100644
index 0000000..2c32579
--- /dev/null
+++ b/backend/routes/enrollments.js
@@ -0,0 +1,47 @@
+const express = require('express');
+const db = require('../db');
+const { authenticate } = require('../middleware/auth');
+
+const router = express.Router();
+
+router.post('/enrollments', authenticate, (req, res) => {
+ const { course_id } = req.body;
+ if (!course_id) return res.status(400).json({ error: 'course_id is required' });
+
+ const course = db.prepare("SELECT * FROM courses WHERE id = ? AND status = 'published'").get(course_id);
+ if (!course) return res.status(404).json({ error: 'Course not found or not published' });
+
+ const existing = db.prepare('SELECT * FROM enrollments WHERE user_id = ? AND course_id = ?').get(req.user.id, course_id);
+ if (existing) return res.status(409).json({ error: 'Already enrolled' });
+
+ const result = db.prepare('INSERT INTO enrollments (user_id, course_id) VALUES (?, ?)').run(req.user.id, course_id);
+ const enrollment = db.prepare('SELECT * FROM enrollments WHERE id = ?').get(result.lastInsertRowid);
+ res.status(201).json({ enrollment });
+});
+
+router.get('/enrollments', authenticate, (req, res) => {
+ const enrollments = db.prepare(`
+ SELECT e.*, c.title AS course_title, c.description, c.thumbnail, c.category,
+ u.name AS instructor_name
+ FROM enrollments e
+ JOIN courses c ON e.course_id = c.id
+ JOIN users u ON c.instructor_id = u.id
+ WHERE e.user_id = ?
+ ORDER BY e.enrolled_at DESC
+ `).all(req.user.id);
+ res.json({ enrollments });
+});
+
+router.get('/enrollments/:courseId', authenticate, (req, res) => {
+ const enrollment = db.prepare('SELECT * FROM enrollments WHERE user_id = ? AND course_id = ?')
+ .get(req.user.id, req.params.courseId);
+ res.json({ enrolled: !!enrollment, enrollment: enrollment || null });
+});
+
+router.delete('/enrollments/:courseId', authenticate, (req, res) => {
+ db.prepare('DELETE FROM enrollments WHERE user_id = ? AND course_id = ?')
+ .run(req.user.id, req.params.courseId);
+ res.json({ message: 'Unenrolled successfully' });
+});
+
+module.exports = router;
diff --git a/backend/routes/progress.js b/backend/routes/progress.js
new file mode 100644
index 0000000..20c2438
--- /dev/null
+++ b/backend/routes/progress.js
@@ -0,0 +1,41 @@
+const express = require('express');
+const db = require('../db');
+const { authenticate } = require('../middleware/auth');
+
+const router = express.Router();
+
+router.get('/progress/:courseId', authenticate, (req, res) => {
+ const { courseId } = req.params;
+
+ const totalVideos = db.prepare('SELECT COUNT(*) AS cnt FROM videos WHERE course_id = ?').get(courseId).cnt;
+
+ if (totalVideos === 0) {
+ return res.json({ progress: 0, completed: 0, total: 0 });
+ }
+
+ const completedVideos = db.prepare(`
+ SELECT COUNT(*) AS cnt
+ FROM progress p
+ JOIN videos v ON p.video_id = v.id
+ WHERE p.user_id = ? AND v.course_id = ? AND p.completed = 1
+ `).get(req.user.id, courseId).cnt;
+
+ const percentage = (completedVideos / totalVideos) * 100;
+
+ const videoProgress = db.prepare(`
+ SELECT v.id, v.title, v.duration, p.watched_seconds, p.completed, p.last_watched
+ FROM videos v
+ LEFT JOIN progress p ON p.video_id = v.id AND p.user_id = ?
+ WHERE v.course_id = ?
+ ORDER BY v.order_index
+ `).all(req.user.id, courseId);
+
+ res.json({
+ progress: Math.round(percentage * 100) / 100,
+ completed: completedVideos,
+ total: totalVideos,
+ videos: videoProgress
+ });
+});
+
+module.exports = router;
diff --git a/backend/routes/quizzes.js b/backend/routes/quizzes.js
new file mode 100644
index 0000000..5e8abf4
--- /dev/null
+++ b/backend/routes/quizzes.js
@@ -0,0 +1,112 @@
+const express = require('express');
+const db = require('../db');
+const { authenticate, requireRole } = require('../middleware/auth');
+
+const router = express.Router();
+
+router.get('/quizzes/:courseId', authenticate, (req, res) => {
+ const quizzes = db.prepare('SELECT * FROM quizzes WHERE course_id = ? ORDER BY created_at').all(req.params.courseId);
+ res.json({ quizzes });
+});
+
+router.post('/quizzes', authenticate, requireRole('instructor', 'admin'), (req, res) => {
+ const { course_id, section_id, title } = req.body;
+ if (!course_id || !title) return res.status(400).json({ error: 'course_id and title are required' });
+
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(course_id);
+ if (!course) return res.status(404).json({ error: 'Course not found' });
+ if (course.instructor_id !== req.user.id && req.user.role !== 'admin') {
+ return res.status(403).json({ error: 'Forbidden' });
+ }
+
+ const result = db.prepare('INSERT INTO quizzes (course_id, section_id, title) VALUES (?, ?, ?)')
+ .run(course_id, section_id || null, title);
+ const quiz = db.prepare('SELECT * FROM quizzes WHERE id = ?').get(result.lastInsertRowid);
+ res.status(201).json({ quiz });
+});
+
+router.post('/quizzes/:id/questions', authenticate, requireRole('instructor', 'admin'), (req, res) => {
+ const quiz = db.prepare('SELECT * FROM quizzes WHERE id = ?').get(req.params.id);
+ if (!quiz) return res.status(404).json({ error: 'Quiz not found' });
+
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(quiz.course_id);
+ if (course.instructor_id !== req.user.id && req.user.role !== 'admin') {
+ return res.status(403).json({ error: 'Forbidden' });
+ }
+
+ const { question, options, correct_answer, points } = req.body;
+ if (!question || !options || !correct_answer) {
+ return res.status(400).json({ error: 'question, options, and correct_answer are required' });
+ }
+
+ const options_json = Array.isArray(options) ? JSON.stringify(options) : options;
+ const result = db.prepare(
+ 'INSERT INTO quiz_questions (quiz_id, question, options_json, correct_answer, points) VALUES (?, ?, ?, ?, ?)'
+ ).run(quiz.id, question, options_json, correct_answer, points || 1);
+
+ const qq = db.prepare('SELECT * FROM quiz_questions WHERE id = ?').get(result.lastInsertRowid);
+ res.status(201).json({ question: qq });
+});
+
+router.post('/quizzes/:id/attempt', authenticate, (req, res) => {
+ const quiz = db.prepare('SELECT * FROM quizzes WHERE id = ?').get(req.params.id);
+ if (!quiz) return res.status(404).json({ error: 'Quiz not found' });
+
+ const enrollment = db.prepare('SELECT * FROM enrollments WHERE user_id = ? AND course_id = ?')
+ .get(req.user.id, quiz.course_id);
+ if (!enrollment) return res.status(403).json({ error: 'Not enrolled in this course' });
+
+ const { answers } = req.body;
+ if (!answers || typeof answers !== 'object') {
+ return res.status(400).json({ error: 'answers object is required (questionId -> answer)' });
+ }
+
+ const questions = db.prepare('SELECT * FROM quiz_questions WHERE quiz_id = ?').all(quiz.id);
+ let score = 0;
+ let total_points = 0;
+ const gradedAnswers = {};
+
+ for (const q of questions) {
+ total_points += q.points;
+ const userAnswer = answers[String(q.id)];
+ const correct = userAnswer === q.correct_answer;
+ if (correct) score += q.points;
+ gradedAnswers[q.id] = { userAnswer, correct, correct_answer: q.correct_answer, points: q.points };
+ }
+
+ const answers_json = JSON.stringify(answers);
+ const result = db.prepare(
+ 'INSERT INTO quiz_attempts (quiz_id, user_id, answers_json, score, total_points) VALUES (?, ?, ?, ?, ?)'
+ ).run(quiz.id, req.user.id, answers_json, score, total_points);
+
+ const attempt = db.prepare('SELECT * FROM quiz_attempts WHERE id = ?').get(result.lastInsertRowid);
+ res.status(201).json({ attempt, gradedAnswers, score, total_points });
+});
+
+router.get('/quizzes/:id/results', authenticate, (req, res) => {
+ const quiz = db.prepare('SELECT * FROM quizzes WHERE id = ?').get(req.params.id);
+ if (!quiz) return res.status(404).json({ error: 'Quiz not found' });
+
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(quiz.course_id);
+ const isInstructor = course.instructor_id === req.user.id || req.user.role === 'admin';
+
+ const questions = db.prepare('SELECT * FROM quiz_questions WHERE quiz_id = ?').all(quiz.id);
+
+ let attempts;
+ if (isInstructor) {
+ attempts = db.prepare(`
+ SELECT a.*, u.name AS student_name
+ FROM quiz_attempts a
+ JOIN users u ON a.user_id = u.id
+ WHERE a.quiz_id = ?
+ ORDER BY a.attempted_at DESC
+ `).all(quiz.id);
+ } else {
+ attempts = db.prepare('SELECT * FROM quiz_attempts WHERE quiz_id = ? AND user_id = ? ORDER BY attempted_at DESC')
+ .all(quiz.id, req.user.id);
+ }
+
+ res.json({ quiz, questions, attempts });
+});
+
+module.exports = router;
diff --git a/backend/routes/videos.js b/backend/routes/videos.js
new file mode 100644
index 0000000..eb5d9c2
--- /dev/null
+++ b/backend/routes/videos.js
@@ -0,0 +1,108 @@
+const express = require('express');
+const path = require('path');
+const fs = require('fs');
+const multer = require('multer');
+const db = require('../db');
+const { authenticate, optionalAuth } = require('../middleware/auth');
+
+const router = express.Router();
+
+const storage = multer.diskStorage({
+ destination: path.join(__dirname, '../uploads/videos'),
+ filename: (req, file, cb) => {
+ const unique = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
+ cb(null, unique + path.extname(file.originalname));
+ }
+});
+const upload = multer({ storage, limits: { fileSize: 2 * 1024 * 1024 * 1024 } });
+
+router.post('/videos/upload', authenticate, upload.single('video'), (req, res) => {
+ if (!req.file) return res.status(400).json({ error: 'No video file provided' });
+ const { course_id, section_id, title, duration, order_index } = req.body;
+ if (!course_id || !title) return res.status(400).json({ error: 'course_id and title are required' });
+
+ const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(course_id);
+ if (!course) return res.status(404).json({ error: 'Course not found' });
+ if (course.instructor_id !== req.user.id && req.user.role !== 'admin') {
+ return res.status(403).json({ error: 'Forbidden' });
+ }
+
+ const file_path = req.file.filename;
+ const result = db.prepare(
+ `INSERT INTO videos (course_id, section_id, title, file_path, duration, order_index)
+ VALUES (?, ?, ?, ?, ?, ?)`
+ ).run(course_id, section_id || null, title, file_path, duration || 0, order_index || 0);
+
+ const video = db.prepare('SELECT * FROM videos WHERE id = ?').get(result.lastInsertRowid);
+ res.status(201).json({ video });
+});
+
+router.get('/videos/:id', optionalAuth, (req, res) => {
+ const video = db.prepare('SELECT * FROM videos WHERE id = ?').get(req.params.id);
+ if (!video || !video.file_path) return res.status(404).json({ error: 'Video not found' });
+
+ const filePath = path.join(__dirname, '../uploads/videos', video.file_path);
+ if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Video file not found' });
+
+ const stat = fs.statSync(filePath);
+ const fileSize = stat.size;
+ const range = req.headers.range;
+
+ if (range) {
+ const parts = range.replace(/bytes=/, '').split('-');
+ const start = parseInt(parts[0], 10);
+ const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
+ const chunkSize = end - start + 1;
+ const file = fs.createReadStream(filePath, { start, end });
+ res.writeHead(206, {
+ 'Content-Range': `bytes ${start}-${end}/${fileSize}`,
+ 'Accept-Ranges': 'bytes',
+ 'Content-Length': chunkSize,
+ 'Content-Type': 'video/mp4'
+ });
+ file.pipe(res);
+ } else {
+ res.writeHead(200, {
+ 'Content-Length': fileSize,
+ 'Content-Type': 'video/mp4',
+ 'Accept-Ranges': 'bytes'
+ });
+ fs.createReadStream(filePath).pipe(res);
+ }
+});
+
+router.put('/videos/:id/progress', authenticate, (req, res) => {
+ const { watched_seconds, completed } = req.body;
+ const video = db.prepare('SELECT * FROM videos WHERE id = ?').get(req.params.id);
+ if (!video) return res.status(404).json({ error: 'Video not found' });
+
+ db.prepare(`
+ INSERT INTO progress (user_id, video_id, watched_seconds, completed, last_watched)
+ VALUES (?, ?, ?, ?, datetime('now'))
+ ON CONFLICT(user_id, video_id) DO UPDATE SET
+ watched_seconds = excluded.watched_seconds,
+ completed = excluded.completed,
+ last_watched = excluded.last_watched
+ `).run(req.user.id, video.id, watched_seconds || 0, completed ? 1 : 0);
+
+ const totalVideos = db.prepare('SELECT COUNT(*) AS cnt FROM videos WHERE course_id = ?').get(video.course_id).cnt;
+ const completedVideos = db.prepare(`
+ SELECT COUNT(*) AS cnt FROM progress p
+ JOIN videos v ON p.video_id = v.id
+ WHERE p.user_id = ? AND v.course_id = ? AND p.completed = 1
+ `).get(req.user.id, video.course_id).cnt;
+
+ const courseProgress = totalVideos > 0 ? (completedVideos / totalVideos) * 100 : 0;
+ db.prepare('UPDATE enrollments SET progress = ? WHERE user_id = ? AND course_id = ?')
+ .run(courseProgress, req.user.id, video.course_id);
+
+ res.json({ message: 'Progress updated', courseProgress });
+});
+
+router.get('/videos/:id/progress', authenticate, (req, res) => {
+ const p = db.prepare('SELECT * FROM progress WHERE user_id = ? AND video_id = ?')
+ .get(req.user.id, req.params.id);
+ res.json({ progress: p || { watched_seconds: 0, completed: false } });
+});
+
+module.exports = router;
diff --git a/backend/schema.sql b/backend/schema.sql
new file mode 100644
index 0000000..d0428a3
--- /dev/null
+++ b/backend/schema.sql
@@ -0,0 +1,142 @@
+CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ email TEXT NOT NULL UNIQUE,
+ password_hash TEXT NOT NULL,
+ role TEXT NOT NULL DEFAULT 'student' CHECK(role IN ('student','instructor','admin')),
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS courses (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ instructor_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ title TEXT NOT NULL,
+ description TEXT,
+ category TEXT,
+ status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft','published')),
+ price REAL NOT NULL DEFAULT 0,
+ thumbnail TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS enrollments (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
+ progress REAL NOT NULL DEFAULT 0,
+ enrolled_at TEXT NOT NULL DEFAULT (datetime('now')),
+ UNIQUE(user_id, course_id)
+);
+
+CREATE TABLE IF NOT EXISTS sections (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
+ title TEXT NOT NULL,
+ order_index INTEGER NOT NULL DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS videos (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
+ section_id INTEGER REFERENCES sections(id) ON DELETE SET NULL,
+ title TEXT NOT NULL,
+ file_path TEXT,
+ duration INTEGER DEFAULT 0,
+ order_index INTEGER NOT NULL DEFAULT 0,
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS assignments (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
+ section_id INTEGER REFERENCES sections(id) ON DELETE SET NULL,
+ title TEXT NOT NULL,
+ description TEXT,
+ due_date TEXT,
+ max_points REAL NOT NULL DEFAULT 100,
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS submissions (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ assignment_id INTEGER NOT NULL REFERENCES assignments(id) ON DELETE CASCADE,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ content TEXT,
+ file_path TEXT,
+ grade REAL,
+ feedback TEXT,
+ submitted_at TEXT NOT NULL DEFAULT (datetime('now')),
+ graded_at TEXT
+);
+
+CREATE TABLE IF NOT EXISTS discussions (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ title TEXT NOT NULL,
+ body TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS discussion_replies (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ discussion_id INTEGER NOT NULL REFERENCES discussions(id) ON DELETE CASCADE,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ body TEXT NOT NULL,
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS announcements (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
+ instructor_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ title TEXT NOT NULL,
+ body TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS progress (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
+ watched_seconds INTEGER NOT NULL DEFAULT 0,
+ completed INTEGER NOT NULL DEFAULT 0,
+ last_watched TEXT NOT NULL DEFAULT (datetime('now')),
+ UNIQUE(user_id, video_id)
+);
+
+CREATE TABLE IF NOT EXISTS certificates (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
+ issued_at TEXT NOT NULL DEFAULT (datetime('now')),
+ cert_uuid TEXT NOT NULL UNIQUE,
+ UNIQUE(user_id, course_id)
+);
+
+CREATE TABLE IF NOT EXISTS quizzes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
+ section_id INTEGER REFERENCES sections(id) ON DELETE SET NULL,
+ title TEXT NOT NULL,
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS quiz_questions (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ quiz_id INTEGER NOT NULL REFERENCES quizzes(id) ON DELETE CASCADE,
+ question TEXT NOT NULL,
+ options_json TEXT NOT NULL,
+ correct_answer TEXT NOT NULL,
+ points REAL NOT NULL DEFAULT 1
+);
+
+CREATE TABLE IF NOT EXISTS quiz_attempts (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ quiz_id INTEGER NOT NULL REFERENCES quizzes(id) ON DELETE CASCADE,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ answers_json TEXT NOT NULL,
+ score REAL NOT NULL DEFAULT 0,
+ total_points REAL NOT NULL DEFAULT 0,
+ attempted_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
diff --git a/backend/server.js b/backend/server.js
new file mode 100644
index 0000000..ca45a6e
--- /dev/null
+++ b/backend/server.js
@@ -0,0 +1,41 @@
+require('dotenv').config();
+const express = require('express');
+const cors = require('cors');
+const path = require('path');
+
+const app = express();
+const PORT = process.env.PORT || 5000;
+
+app.use(cors({
+ origin: process.env.FRONTEND_URL || 'http://localhost:3000',
+ credentials: true
+}));
+app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
+
+app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
+
+app.use('/api', require('./routes/auth'));
+app.use('/api', require('./routes/courses'));
+app.use('/api', require('./routes/videos'));
+app.use('/api', require('./routes/enrollments'));
+app.use('/api', require('./routes/assignments'));
+app.use('/api', require('./routes/discussions'));
+app.use('/api', require('./routes/announcements'));
+app.use('/api', require('./routes/progress'));
+app.use('/api', require('./routes/certificates'));
+app.use('/api', require('./routes/quizzes'));
+app.use('/api', require('./routes/admin'));
+
+app.get('/api/health', (req, res) => res.json({ status: 'ok', timestamp: new Date().toISOString() }));
+
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).json({ error: 'Internal server error' });
+});
+
+app.listen(PORT, () => {
+ console.log(`EduScale backend running on port ${PORT}`);
+});
+
+module.exports = app;
diff --git a/backend/uploads/files/.gitkeep b/backend/uploads/files/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/backend/uploads/videos/.gitkeep b/backend/uploads/videos/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..0ebb5fe
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ EduScale
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..16ec33c
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,2041 @@
+{
+ "name": "eduscale-frontend",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "eduscale-frontend",
+ "version": "1.0.0",
+ "dependencies": {
+ "axios": "^1.6.7",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.22.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.2.1",
+ "vite": "^5.1.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
+ "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
+ "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
+ "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
+ "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
+ "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
+ "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
+ "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
+ "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
+ "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
+ "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
+ "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
+ "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
+ "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
+ "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
+ "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
+ "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
+ "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
+ "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
+ "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
+ "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
+ "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
+ "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
+ "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
+ "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz",
+ "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001788",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
+ "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.339",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.339.tgz",
+ "integrity": "sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.37",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.10",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
+ "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.1",
+ "@rollup/rollup-android-arm64": "4.60.1",
+ "@rollup/rollup-darwin-arm64": "4.60.1",
+ "@rollup/rollup-darwin-x64": "4.60.1",
+ "@rollup/rollup-freebsd-arm64": "4.60.1",
+ "@rollup/rollup-freebsd-x64": "4.60.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.1",
+ "@rollup/rollup-linux-arm64-musl": "4.60.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.1",
+ "@rollup/rollup-linux-loong64-musl": "4.60.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-musl": "4.60.1",
+ "@rollup/rollup-openbsd-x64": "4.60.1",
+ "@rollup/rollup-openharmony-arm64": "4.60.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.1",
+ "@rollup/rollup-win32-x64-gnu": "4.60.1",
+ "@rollup/rollup-win32-x64-msvc": "4.60.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..355c359
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "eduscale-frontend",
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "start": "vite --port 3000"
+ },
+ "dependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.22.0",
+ "axios": "^1.6.7"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.2.1",
+ "vite": "^5.1.0"
+ }
+}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
new file mode 100644
index 0000000..d9eb6bf
--- /dev/null
+++ b/frontend/src/App.jsx
@@ -0,0 +1,61 @@
+import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
+import { AuthProvider, useAuth } from './context/AuthContext'
+import Navbar from './components/Navbar'
+import Home from './pages/Home'
+import Login from './pages/Login'
+import Register from './pages/Register'
+import CourseDetail from './pages/CourseDetail'
+import VideoPlayer from './pages/VideoPlayer'
+import StudentDashboard from './pages/StudentDashboard'
+import InstructorDashboard from './pages/InstructorDashboard'
+import AdminDashboard from './pages/AdminDashboard'
+import Certificates from './pages/Certificates'
+
+function ProtectedRoute({ children, roles }) {
+ const { user, loading } = useAuth()
+ if (loading) return Loading...
+ if (!user) return
+ if (roles && !roles.includes(user.role)) return
+ return children
+}
+
+function AppRoutes() {
+ return (
+ <>
+
+
+
+ } />
+ } />
+ } />
+ } />
+
+ } />
+
+ } />
+
+ } />
+
+ } />
+
+ } />
+
+
+ >
+ )
+}
+
+export default function App() {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/api.js b/frontend/src/api.js
new file mode 100644
index 0000000..516227c
--- /dev/null
+++ b/frontend/src/api.js
@@ -0,0 +1,23 @@
+import axios from 'axios'
+
+const api = axios.create({
+ baseURL: '/api'
+})
+
+const token = localStorage.getItem('token')
+if (token) {
+ api.defaults.headers.common['Authorization'] = `Bearer ${token}`
+}
+
+api.interceptors.response.use(
+ res => res,
+ err => {
+ if (err.response?.status === 401) {
+ localStorage.removeItem('token')
+ delete api.defaults.headers.common['Authorization']
+ }
+ return Promise.reject(err)
+ }
+)
+
+export default api
diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx
new file mode 100644
index 0000000..e5633c7
--- /dev/null
+++ b/frontend/src/components/Navbar.jsx
@@ -0,0 +1,54 @@
+import { Link, useNavigate } from 'react-router-dom'
+import { useAuth } from '../context/AuthContext'
+
+export default function Navbar() {
+ const { user, logout } = useAuth()
+ const navigate = useNavigate()
+
+ function handleLogout() {
+ logout()
+ navigate('/')
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx
new file mode 100644
index 0000000..b30aa84
--- /dev/null
+++ b/frontend/src/context/AuthContext.jsx
@@ -0,0 +1,50 @@
+import { createContext, useContext, useState, useEffect } from 'react'
+import api from '../api'
+
+const AuthContext = createContext(null)
+
+export function AuthProvider({ children }) {
+ const [user, setUser] = useState(null)
+ const [token, setToken] = useState(() => localStorage.getItem('token'))
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ if (token) {
+ api.defaults.headers.common['Authorization'] = `Bearer ${token}`
+ api.get('/me')
+ .then(res => setUser(res.data.user))
+ .catch(() => {
+ localStorage.removeItem('token')
+ setToken(null)
+ delete api.defaults.headers.common['Authorization']
+ })
+ .finally(() => setLoading(false))
+ } else {
+ setLoading(false)
+ }
+ }, [token])
+
+ function login(tokenValue, userData) {
+ localStorage.setItem('token', tokenValue)
+ api.defaults.headers.common['Authorization'] = `Bearer ${tokenValue}`
+ setToken(tokenValue)
+ setUser(userData)
+ }
+
+ function logout() {
+ localStorage.removeItem('token')
+ delete api.defaults.headers.common['Authorization']
+ setToken(null)
+ setUser(null)
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useAuth() {
+ return useContext(AuthContext)
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..db3d94e
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,476 @@
+:root {
+ --primary: #4f46e5;
+ --primary-dark: #3730a3;
+ --primary-light: #818cf8;
+ --secondary: #64748b;
+ --success: #22c55e;
+ --danger: #ef4444;
+ --warning: #f59e0b;
+ --info: #3b82f6;
+ --bg: #f8fafc;
+ --surface: #ffffff;
+ --border: #e2e8f0;
+ --text: #1e293b;
+ --text-muted: #64748b;
+ --radius: 8px;
+ --radius-lg: 12px;
+ --shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
+ --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06);
+ --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05);
+}
+
+* { box-sizing: border-box; margin: 0; padding: 0; }
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: var(--bg);
+ color: var(--text);
+ line-height: 1.6;
+}
+
+a { color: var(--primary); text-decoration: none; }
+a:hover { text-decoration: underline; }
+
+/* Navbar */
+.navbar {
+ background: var(--surface);
+ border-bottom: 1px solid var(--border);
+ padding: 0 2rem;
+ height: 64px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ box-shadow: var(--shadow);
+}
+
+.navbar-brand { display: flex; align-items: center; }
+.brand-link { display: flex; align-items: center; gap: 8px; text-decoration: none; }
+.brand-icon { font-size: 1.5rem; }
+.brand-name { font-size: 1.25rem; font-weight: 700; color: var(--primary); }
+
+.navbar-links { display: flex; align-items: center; gap: 1.5rem; }
+.nav-link { color: var(--text); font-weight: 500; transition: color 0.2s; }
+.nav-link:hover { color: var(--primary); text-decoration: none; }
+
+.navbar-auth { display: flex; align-items: center; }
+.user-menu { display: flex; align-items: center; gap: 0.75rem; }
+.user-name { font-weight: 500; color: var(--text); }
+.auth-links { display: flex; gap: 0.5rem; }
+
+/* Buttons */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.4rem;
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: var(--radius);
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+ text-decoration: none;
+ white-space: nowrap;
+}
+.btn:hover { text-decoration: none; }
+.btn:disabled { opacity: 0.6; cursor: not-allowed; }
+
+.btn-primary { background: var(--primary); color: white; }
+.btn-primary:hover:not(:disabled) { background: var(--primary-dark); }
+.btn-outline { background: transparent; color: var(--primary); border: 1px solid var(--primary); }
+.btn-outline:hover:not(:disabled) { background: var(--primary); color: white; }
+.btn-danger { background: var(--danger); color: white; }
+.btn-danger:hover:not(:disabled) { background: #dc2626; }
+.btn-full { width: 100%; }
+.btn-lg { padding: 0.75rem 1.5rem; font-size: 1rem; }
+.btn-sm { padding: 0.35rem 0.75rem; font-size: 0.8rem; }
+.btn-xs { padding: 0.2rem 0.5rem; font-size: 0.75rem; }
+
+/* Badges */
+.badge {
+ display: inline-block;
+ padding: 0.2rem 0.6rem;
+ border-radius: 999px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: capitalize;
+}
+.badge-primary { background: #ede9fe; color: var(--primary); }
+.badge-success { background: #dcfce7; color: #16a34a; }
+.badge-danger { background: #fee2e2; color: #dc2626; }
+.badge-secondary { background: #f1f5f9; color: var(--secondary); }
+.badge-info { background: #dbeafe; color: #2563eb; }
+.badge-lg { padding: 0.4rem 1rem; font-size: 0.9rem; }
+
+/* Layout */
+.main-content { min-height: calc(100vh - 64px); }
+.page { max-width: 1200px; margin: 0 auto; padding: 2rem 1.5rem; }
+.page-header { margin-bottom: 2rem; }
+.page-header h1 { font-size: 2rem; font-weight: 700; }
+.page-header p { color: var(--text-muted); margin-top: 0.5rem; }
+
+/* Loading */
+.loading { display: flex; align-items: center; justify-content: center; padding: 4rem; color: var(--text-muted); font-size: 1.1rem; }
+.spacer { flex: 1; }
+
+/* Empty State */
+.empty-state { text-align: center; padding: 3rem; color: var(--text-muted); }
+.empty-state .empty-icon { font-size: 3rem; margin-bottom: 1rem; }
+.empty-state h3 { font-size: 1.25rem; margin-bottom: 0.5rem; color: var(--text); }
+.empty-state-sm { color: var(--text-muted); font-size: 0.9rem; padding: 0.5rem 0; }
+
+/* Cards */
+.card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 1.5rem;
+ box-shadow: var(--shadow);
+ margin-bottom: 1.5rem;
+}
+.card h3 { font-size: 1.1rem; margin-bottom: 1rem; color: var(--text); }
+.card h4 { font-size: 0.95rem; margin-bottom: 0.75rem; color: var(--text); }
+
+/* Hero */
+.hero {
+ text-align: center;
+ padding: 3rem 1rem;
+ background: linear-gradient(135deg, #ede9fe 0%, #dbeafe 100%);
+ border-radius: var(--radius-lg);
+ margin-bottom: 2rem;
+}
+.hero h1 { font-size: 2.5rem; font-weight: 800; color: var(--text); margin-bottom: 0.75rem; }
+.hero p { font-size: 1.1rem; color: var(--text-muted); }
+
+/* Search */
+.search-bar {
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 2rem;
+}
+.search-input {
+ flex: 1;
+ padding: 0.6rem 1rem;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ font-size: 0.95rem;
+ outline: none;
+ transition: border-color 0.2s;
+}
+.search-input:focus { border-color: var(--primary); }
+.category-select {
+ padding: 0.6rem 1rem;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ font-size: 0.9rem;
+ background: white;
+ cursor: pointer;
+}
+
+/* Course Grid */
+.course-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 1.5rem;
+}
+
+.course-card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ box-shadow: var(--shadow);
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+.course-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
+
+.course-thumbnail {
+ height: 160px;
+ background: linear-gradient(135deg, #ede9fe, #dbeafe);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+.course-thumbnail img { width: 100%; height: 100%; object-fit: cover; }
+.thumbnail-placeholder { font-size: 3rem; }
+
+.course-info { padding: 1rem; }
+.course-category { font-size: 0.75rem; text-transform: uppercase; font-weight: 600; color: var(--primary); }
+.course-title { margin: 0.4rem 0; font-size: 1rem; font-weight: 600; line-height: 1.4; }
+.course-title a { color: var(--text); }
+.course-title a:hover { color: var(--primary); text-decoration: none; }
+.course-description { font-size: 0.85rem; color: var(--text-muted); margin: 0.4rem 0; line-height: 1.5; }
+.course-instructor { font-size: 0.85rem; color: var(--text-muted); margin: 0.4rem 0; }
+.course-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 0.75rem; }
+.course-price { font-size: 1rem; font-weight: 700; color: var(--primary); }
+
+/* Auth */
+.auth-page { display: flex; align-items: center; justify-content: center; min-height: calc(100vh - 64px); padding: 2rem; }
+.auth-card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 2.5rem;
+ width: 100%;
+ max-width: 420px;
+ box-shadow: var(--shadow-md);
+}
+.auth-card h2 { font-size: 1.75rem; font-weight: 700; margin-bottom: 0.25rem; }
+.auth-subtitle { color: var(--text-muted); margin-bottom: 1.5rem; }
+.auth-form { display: flex; flex-direction: column; gap: 1rem; }
+.auth-footer { margin-top: 1.5rem; text-align: center; color: var(--text-muted); font-size: 0.9rem; }
+
+/* Forms */
+.form-group { display: flex; flex-direction: column; gap: 0.4rem; }
+.form-group label { font-size: 0.85rem; font-weight: 500; color: var(--text); }
+.form-group input, .form-group textarea, .form-group select {
+ padding: 0.6rem 0.9rem;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ font-size: 0.9rem;
+ outline: none;
+ transition: border-color 0.2s;
+ background: white;
+ width: 100%;
+ font-family: inherit;
+}
+.form-group input:focus, .form-group textarea:focus, .form-group select:focus { border-color: var(--primary); }
+
+.form-grid { display: grid; gap: 0.75rem; }
+.form-grid input, .form-grid textarea, .form-grid select {
+ width: 100%;
+ padding: 0.6rem 0.9rem;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ font-size: 0.9rem;
+ outline: none;
+ font-family: inherit;
+}
+.form-stack { display: flex; flex-direction: column; gap: 0.5rem; }
+.form-stack input, .form-stack textarea, .form-stack select {
+ padding: 0.5rem 0.75rem;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ font-size: 0.85rem;
+ font-family: inherit;
+}
+.form-inline { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
+.form-inline input { padding: 0.5rem 0.75rem; border: 1px solid var(--border); border-radius: var(--radius); font-size: 0.85rem; flex: 1; }
+
+/* Role Selector */
+.role-selector { display: flex; gap: 0.75rem; }
+.role-option {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.4rem;
+ padding: 0.75rem;
+ border: 2px solid var(--border);
+ border-radius: var(--radius);
+ cursor: pointer;
+ transition: all 0.2s;
+ font-size: 0.9rem;
+}
+.role-option input { display: none; }
+.role-option.active { border-color: var(--primary); background: #ede9fe; color: var(--primary); }
+
+/* Alerts */
+.alert {
+ padding: 0.75rem 1rem;
+ border-radius: var(--radius);
+ margin-bottom: 1rem;
+ font-size: 0.9rem;
+ cursor: pointer;
+}
+.alert-error { background: #fee2e2; color: #dc2626; border: 1px solid #fca5a5; }
+.alert-info { background: #dbeafe; color: #2563eb; border: 1px solid #93c5fd; }
+.alert-success { background: #dcfce7; color: #16a34a; border: 1px solid #86efac; }
+
+/* Course Detail */
+.course-detail-header {
+ display: flex;
+ gap: 2rem;
+ align-items: flex-start;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 2rem;
+ margin-bottom: 2rem;
+ box-shadow: var(--shadow);
+}
+.course-detail-info { flex: 1; }
+.course-detail-info h1 { font-size: 1.75rem; margin: 0.5rem 0; }
+.course-detail-info .course-description { color: var(--text-muted); margin: 0.75rem 0; line-height: 1.6; }
+.course-meta { display: flex; gap: 1rem; align-items: center; margin-top: 1rem; }
+.course-detail-action { display: flex; flex-direction: column; align-items: center; gap: 1rem; min-width: 180px; }
+
+/* Tabs */
+.tab-nav { display: flex; gap: 0; border-bottom: 2px solid var(--border); margin-bottom: 1.5rem; }
+.tab-btn {
+ padding: 0.6rem 1.2rem;
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ margin-bottom: -2px;
+ cursor: pointer;
+ font-size: 0.9rem;
+ font-weight: 500;
+ color: var(--text-muted);
+ transition: all 0.2s;
+}
+.tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }
+
+/* Sections */
+.section-block { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 1rem; overflow: hidden; }
+.section-title { padding: 0.75rem 1rem; background: #f8fafc; border-bottom: 1px solid var(--border); font-size: 1rem; font-weight: 600; }
+
+.video-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 1rem; border-bottom: 1px solid var(--border); }
+.video-item:last-child { border-bottom: none; }
+.video-icon { color: var(--primary); font-size: 0.8rem; }
+.video-title { flex: 1; font-size: 0.9rem; }
+.video-duration { color: var(--text-muted); font-size: 0.8rem; }
+
+.assignment-item, .quiz-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 1rem; border-top: 1px solid var(--border); }
+.assignment-icon, .quiz-icon { font-size: 0.9rem; }
+.due-date { color: var(--warning); font-size: 0.8rem; margin-left: auto; }
+
+/* Discussions */
+.discussion-form { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem; margin-bottom: 1.5rem; }
+.discussion-form h4 { margin-bottom: 0.75rem; }
+.discussion-form input, .discussion-form textarea { width: 100%; padding: 0.5rem 0.75rem; border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 0.5rem; font-family: inherit; font-size: 0.9rem; }
+.discussion-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem; margin-bottom: 0.75rem; }
+.discussion-card h4 { font-size: 1rem; margin-bottom: 0.4rem; }
+.discussion-card p { color: var(--text-muted); font-size: 0.9rem; margin-bottom: 0.4rem; }
+.discussion-card small { color: var(--text-muted); font-size: 0.8rem; }
+
+/* Announcements */
+.announcement-card { background: linear-gradient(135deg, #ede9fe, #fff); border: 1px solid #c4b5fd; border-radius: var(--radius); padding: 1rem; margin-bottom: 0.75rem; }
+.announcement-card h4 { color: var(--primary); margin-bottom: 0.5rem; }
+.announcement-card small { color: var(--text-muted); font-size: 0.8rem; }
+
+/* Dashboard */
+.dashboard h1 { font-size: 2rem; font-weight: 700; margin-bottom: 0.25rem; }
+.subtitle { color: var(--text-muted); margin-bottom: 2rem; }
+.dashboard-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem; }
+.dashboard-main { }
+.dashboard-sidebar { }
+.dashboard-section { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 1.5rem; margin-bottom: 1.5rem; }
+.dashboard-section h2 { font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); }
+
+.enrollment-list { display: flex; flex-direction: column; gap: 1rem; }
+.enrollment-card { border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem; }
+.enrollment-info h3 { font-size: 1rem; margin-bottom: 0.25rem; }
+.instructor-name { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 0.5rem; }
+.enrollment-progress { margin: 0.75rem 0; }
+.progress-bar-container { height: 8px; background: #e2e8f0; border-radius: 999px; overflow: hidden; margin-bottom: 0.3rem; }
+.progress-bar { height: 100%; background: linear-gradient(90deg, var(--primary), var(--primary-light)); border-radius: 999px; transition: width 0.3s; }
+.progress-text { font-size: 0.8rem; color: var(--text-muted); }
+.enrollment-actions { display: flex; align-items: center; gap: 0.75rem; }
+
+.assignment-list { list-style: none; }
+.assignment-item-sm { display: flex; align-items: flex-start; gap: 0.75rem; padding: 0.6rem 0; border-bottom: 1px solid var(--border); }
+.assignment-item-sm:last-child { border-bottom: none; }
+.assignment-title { font-size: 0.9rem; font-weight: 500; }
+.assignment-course { font-size: 0.8rem; color: var(--text-muted); }
+.assignment-due { font-size: 0.8rem; color: var(--warning); }
+
+.cert-list { list-style: none; }
+.cert-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 0; border-bottom: 1px solid var(--border); }
+.cert-item:last-child { border-bottom: none; }
+.cert-item p { font-size: 0.9rem; margin-bottom: 0.3rem; }
+
+/* Certificates Page */
+.certificates-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem; }
+.certificate-card {
+ background: linear-gradient(135deg, #fdf4ff, #ede9fe);
+ border: 2px solid #c4b5fd;
+ border-radius: var(--radius-lg);
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ gap: 1rem;
+ box-shadow: var(--shadow-md);
+}
+.cert-badge { font-size: 3rem; }
+.cert-info h3 { font-size: 1.1rem; font-weight: 700; color: var(--text); }
+.cert-instructor { font-size: 0.85rem; color: var(--text-muted); margin: 0.25rem 0; }
+.cert-date { font-size: 0.85rem; color: var(--primary); font-weight: 500; }
+.cert-id { font-size: 0.75rem; color: var(--text-muted); font-family: monospace; }
+
+/* Video Player */
+.video-player-page { display: grid; grid-template-columns: 1fr 320px; gap: 1.5rem; max-width: 1400px; margin: 0 auto; padding: 1.5rem; height: calc(100vh - 64px); }
+.video-main { display: flex; flex-direction: column; gap: 1rem; overflow-y: auto; }
+.video-container { background: #000; border-radius: var(--radius); overflow: hidden; aspect-ratio: 16/9; }
+.video-element { width: 100%; height: 100%; display: block; }
+.video-controls-bar { display: flex; flex-direction: column; gap: 0.5rem; }
+.video-title { font-size: 1.2rem; font-weight: 600; }
+.playback-speed { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; }
+.speed-btn { padding: 0.2rem 0.5rem; border: 1px solid var(--border); border-radius: var(--radius); background: white; cursor: pointer; font-size: 0.8rem; }
+.speed-btn.active { background: var(--primary); color: white; border-color: var(--primary); }
+.video-nav { display: flex; align-items: center; gap: 1rem; }
+.progress-info { display: flex; align-items: center; gap: 0.75rem; }
+
+.video-sidebar { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow-y: auto; display: flex; flex-direction: column; }
+.sidebar-title { padding: 1rem; font-weight: 600; border-bottom: 1px solid var(--border); font-size: 0.95rem; }
+.video-list { flex: 1; overflow-y: auto; }
+.sidebar-section { }
+.sidebar-section-title { padding: 0.6rem 1rem; font-size: 0.8rem; text-transform: uppercase; font-weight: 600; color: var(--text-muted); background: #f8fafc; border-bottom: 1px solid var(--border); }
+.sidebar-video-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 1rem; font-size: 0.85rem; color: var(--text); border-bottom: 1px solid var(--border); transition: background 0.2s; }
+.sidebar-video-item:hover { background: #f1f5f9; text-decoration: none; }
+.sidebar-video-item.active { background: #ede9fe; color: var(--primary); font-weight: 600; }
+.sidebar-video-icon { font-size: 0.7rem; color: var(--text-muted); }
+.sidebar-video-title { flex: 1; line-height: 1.4; }
+
+/* Instructor Dashboard */
+.manage-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem; }
+.manage-header h2 { font-size: 1.5rem; }
+.manage-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
+.manage-col { display: flex; flex-direction: column; gap: 1rem; }
+.course-list { display: flex; flex-direction: column; gap: 0.75rem; }
+.course-manage-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem; display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
+.course-manage-card h3 { font-size: 1rem; margin-bottom: 0.25rem; }
+.course-manage-card p { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 0.5rem; }
+.course-manage-actions { display: flex; gap: 0.5rem; flex-shrink: 0; }
+.section-manage-item { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
+.submission-section { margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border); }
+.submission-section h4 { font-size: 0.9rem; margin-bottom: 0.5rem; }
+.submission-item { background: #f8fafc; border-radius: var(--radius); padding: 0.5rem; margin-bottom: 0.5rem; font-size: 0.85rem; }
+.grade-form { display: flex; gap: 0.4rem; margin-top: 0.4rem; flex-wrap: wrap; }
+.grade-form input { padding: 0.3rem 0.5rem; border: 1px solid var(--border); border-radius: var(--radius); font-size: 0.8rem; width: 80px; }
+.grade-form input:last-of-type { flex: 1; }
+
+/* Admin Dashboard */
+.stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
+.stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 1.25rem; display: flex; align-items: center; gap: 1rem; box-shadow: var(--shadow); }
+.stat-icon { font-size: 2rem; }
+.stat-value { font-size: 1.75rem; font-weight: 700; color: var(--primary); }
+.stat-label { font-size: 0.8rem; color: var(--text-muted); }
+
+.table-container { overflow-x: auto; }
+.data-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
+.data-table th { text-align: left; padding: 0.75rem; border-bottom: 2px solid var(--border); color: var(--text-muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; }
+.data-table td { padding: 0.75rem; border-bottom: 1px solid var(--border); }
+.data-table tr:hover td { background: #f8fafc; }
+.role-select { padding: 0.3rem 0.5rem; border: 1px solid var(--border); border-radius: var(--radius); font-size: 0.85rem; background: white; cursor: pointer; }
+
+/* Responsive */
+@media (max-width: 768px) {
+ .navbar { padding: 0 1rem; }
+ .navbar-links { display: none; }
+ .page { padding: 1rem; }
+ .dashboard-grid { grid-template-columns: 1fr; }
+ .manage-grid { grid-template-columns: 1fr; }
+ .video-player-page { grid-template-columns: 1fr; height: auto; }
+ .video-sidebar { height: 300px; }
+ .course-detail-header { flex-direction: column; }
+ .stats-grid { grid-template-columns: repeat(2, 1fr); }
+ .hero h1 { font-size: 1.75rem; }
+}
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
new file mode 100644
index 0000000..5e8d112
--- /dev/null
+++ b/frontend/src/main.jsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.jsx'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+)
diff --git a/frontend/src/pages/AdminDashboard.jsx b/frontend/src/pages/AdminDashboard.jsx
new file mode 100644
index 0000000..599ef08
--- /dev/null
+++ b/frontend/src/pages/AdminDashboard.jsx
@@ -0,0 +1,143 @@
+import { useState, useEffect } from 'react'
+import api from '../api'
+
+export default function AdminDashboard() {
+ const [users, setUsers] = useState([])
+ const [stats, setStats] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [msg, setMsg] = useState('')
+
+ useEffect(() => {
+ Promise.all([api.get('/admin/users'), api.get('/admin/stats')])
+ .then(([usersRes, statsRes]) => {
+ setUsers(usersRes.data.users)
+ setStats(statsRes.data.stats)
+ })
+ .finally(() => setLoading(false))
+ }, [])
+
+ async function handleRoleChange(userId, newRole) {
+ try {
+ const res = await api.put(`/admin/users/${userId}/role`, { role: newRole })
+ setUsers(users.map(u => u.id === userId ? res.data.user : u))
+ setMsg('Role updated successfully')
+ } catch (err) {
+ setMsg(err.response?.data?.error || 'Failed to update role')
+ }
+ }
+
+ if (loading) return Loading admin dashboard...
+
+ return (
+
+
Admin Dashboard
+
+ {msg &&
setMsg('')}>{msg} ✕
}
+
+ {stats && (
+
+
+
👥
+
+
{stats.userCount}
+
Total Users
+
+
+
+
📚
+
+
{stats.courseCount}
+
Total Courses
+
+
+
+
✅
+
+
{stats.publishedCount}
+
Published
+
+
+
+
🎓
+
+
{stats.enrollmentCount}
+
Enrollments
+
+
+
+
🧑🎓
+
+
{stats.studentCount}
+
Students
+
+
+
+
🧑🏫
+
+
{stats.instructorCount}
+
Instructors
+
+
+
+
📄
+
+
{stats.submissionCount}
+
Submissions
+
+
+
+
🏆
+
+
{stats.certificateCount}
+
Certificates
+
+
+
+ )}
+
+
+
User Management
+
+
+
+
+ | ID |
+ Name |
+ Email |
+ Role |
+ Joined |
+ Actions |
+
+
+
+ {users.map(u => (
+
+ | {u.id} |
+ {u.name} |
+ {u.email} |
+
+
+ {u.role}
+
+ |
+ {new Date(u.created_at).toLocaleDateString()} |
+
+
+ |
+
+ ))}
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/Certificates.jsx b/frontend/src/pages/Certificates.jsx
new file mode 100644
index 0000000..c4027c1
--- /dev/null
+++ b/frontend/src/pages/Certificates.jsx
@@ -0,0 +1,97 @@
+import { useState, useEffect } from 'react'
+import api from '../api'
+import { useAuth } from '../context/AuthContext'
+
+export default function Certificates() {
+ const { user } = useAuth()
+ const [certificates, setCertificates] = useState([])
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ fetchCertificates()
+ }, [])
+
+ async function fetchCertificates() {
+ try {
+ const enrollRes = await api.get('/enrollments')
+ const certs = []
+ for (const e of enrollRes.data.enrollments) {
+ try {
+ const res = await api.get(`/certificates/${e.course_id}`)
+ certs.push({
+ ...res.data.certificate,
+ course_title: e.course_title,
+ course_id: e.course_id,
+ instructor_name: e.instructor_name
+ })
+ } catch {}
+ }
+ setCertificates(certs)
+ } catch {} finally {
+ setLoading(false)
+ }
+ }
+
+ async function handleIssueCertificate(courseId) {
+ try {
+ const res = await api.post(`/certificates/${courseId}`)
+ const enrollRes = await api.get('/enrollments')
+ const enrollment = enrollRes.data.enrollments.find(e => e.course_id === courseId)
+ setCertificates(prev => [...prev.filter(c => c.course_id !== courseId), {
+ ...res.data.certificate,
+ course_title: enrollment?.course_title,
+ course_id: courseId,
+ instructor_name: enrollment?.instructor_name
+ }])
+ } catch (err) {
+ alert(err.response?.data?.error || 'Failed to issue certificate')
+ }
+ }
+
+ if (loading) return Loading certificates...
+
+ return (
+
+
+
My Certificates
+
Your earned certificates from completed courses
+
+
+ {certificates.length === 0 ? (
+
+
🏆
+
No certificates yet
+
Complete a course to earn your first certificate!
+
+ ) : (
+
+ {certificates.map(cert => (
+
+
🏆
+
+
{cert.course_title}
+
Instructor: {cert.instructor_name}
+
+ Issued: {new Date(cert.issued_at).toLocaleDateString('en-US', {
+ year: 'numeric', month: 'long', day: 'numeric'
+ })}
+
+
ID: {cert.cert_uuid?.slice(0, 8)}...
+
+
+
+ ))}
+
+ )}
+
+ )
+}
diff --git a/frontend/src/pages/CourseDetail.jsx b/frontend/src/pages/CourseDetail.jsx
new file mode 100644
index 0000000..e734375
--- /dev/null
+++ b/frontend/src/pages/CourseDetail.jsx
@@ -0,0 +1,227 @@
+import { useState, useEffect } from 'react'
+import { useParams, Link, useNavigate } from 'react-router-dom'
+import api from '../api'
+import { useAuth } from '../context/AuthContext'
+
+export default function CourseDetail() {
+ const { id } = useParams()
+ const { user } = useAuth()
+ const navigate = useNavigate()
+ const [course, setCourse] = useState(null)
+ const [sections, setSections] = useState([])
+ const [enrolled, setEnrolled] = useState(false)
+ const [activeTab, setActiveTab] = useState('content')
+ const [announcements, setAnnouncements] = useState([])
+ const [discussions, setDiscussions] = useState([])
+ const [newDiscussion, setNewDiscussion] = useState({ title: '', body: '' })
+ const [loading, setLoading] = useState(true)
+ const [enrolling, setEnrolling] = useState(false)
+
+ useEffect(() => {
+ fetchCourse()
+ }, [id])
+
+ useEffect(() => {
+ if (enrolled || (user && course?.instructor_id === user.id)) {
+ fetchAnnouncements()
+ fetchDiscussions()
+ }
+ }, [enrolled, course])
+
+ async function fetchCourse() {
+ try {
+ const res = await api.get(`/courses/${id}`)
+ setCourse(res.data.course)
+ setSections(res.data.sections)
+ setEnrolled(res.data.enrolled)
+ } catch {
+ navigate('/')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ async function fetchAnnouncements() {
+ try {
+ const res = await api.get(`/announcements/${id}`)
+ setAnnouncements(res.data.announcements)
+ } catch {}
+ }
+
+ async function fetchDiscussions() {
+ try {
+ const res = await api.get(`/discussions/${id}`)
+ setDiscussions(res.data.discussions)
+ } catch {}
+ }
+
+ async function handleEnroll() {
+ if (!user) return navigate('/login')
+ setEnrolling(true)
+ try {
+ await api.post('/enrollments', { course_id: id })
+ setEnrolled(true)
+ fetchAnnouncements()
+ fetchDiscussions()
+ } catch (err) {
+ if (err.response?.status === 409) {
+ setEnrolled(true)
+ } else {
+ alert(err.response?.data?.error || 'Enrollment failed')
+ }
+ } finally {
+ setEnrolling(false)
+ }
+ }
+
+ async function handlePostDiscussion(e) {
+ e.preventDefault()
+ try {
+ await api.post('/discussions', { course_id: id, ...newDiscussion })
+ setNewDiscussion({ title: '', body: '' })
+ fetchDiscussions()
+ } catch (err) {
+ alert(err.response?.data?.error || 'Failed to post discussion')
+ }
+ }
+
+ if (loading) return Loading course...
+ if (!course) return Course not found
+
+ const isInstructor = user && (course.instructor_id === user.id || user.role === 'admin')
+
+ return (
+
+
+
+
{course.category || 'General'}
+
{course.title}
+
{course.description}
+
Instructor: {course.instructor_name}
+
+ {course.status}
+ {course.price === 0 ? 'Free' : `$${course.price}`}
+
+
+
+ {!enrolled && !isInstructor && (
+
+ )}
+ {enrolled && ✓ Enrolled}
+ {isInstructor && Your Course}
+
+
+
+
+ {['content', 'announcements', 'discussions'].map(tab => (
+
+ ))}
+
+
+ {activeTab === 'content' && (
+
+ {sections.length === 0 ? (
+
No content yet.
+ ) : (
+ sections.map(section => (
+
+
{section.title}
+ {section.videos?.map(video => (
+
+ ▶
+ {video.title}
+ {video.duration > 0 && (
+ {Math.floor(video.duration / 60)}m
+ )}
+ {(enrolled || isInstructor) && (
+
+ Watch
+
+ )}
+
+ ))}
+ {section.assignments?.map(assignment => (
+
+ 📝
+ {assignment.title}
+ {assignment.due_date && (
+ Due: {new Date(assignment.due_date).toLocaleDateString()}
+ )}
+
+ ))}
+ {section.quizzes?.map(quiz => (
+
+ ❓
+ {quiz.title}
+
+ ))}
+
+ ))
+ )}
+
+ )}
+
+ {activeTab === 'announcements' && (
+
+ {announcements.length === 0 ? (
+
No announcements yet.
+ ) : (
+ announcements.map(a => (
+
+
{a.title}
+
{a.body}
+
by {a.instructor_name} · {new Date(a.created_at).toLocaleDateString()}
+
+ ))
+ )}
+
+ )}
+
+ {activeTab === 'discussions' && (
+
+ {(enrolled || isInstructor) && (
+
+ )}
+ {discussions.length === 0 ? (
+
No discussions yet. Start one!
+ ) : (
+ discussions.map(d => (
+
+
{d.title}
+
{d.body}
+
by {d.author_name} · {new Date(d.created_at).toLocaleDateString()} · {d.reply_count} replies
+
+ ))
+ )}
+
+ )}
+
+ )
+}
diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx
new file mode 100644
index 0000000..e790c66
--- /dev/null
+++ b/frontend/src/pages/Home.jsx
@@ -0,0 +1,114 @@
+import { useState, useEffect } from 'react'
+import { Link, useNavigate } from 'react-router-dom'
+import api from '../api'
+import { useAuth } from '../context/AuthContext'
+
+const CATEGORIES = ['Technology', 'Business', 'Design', 'Marketing', 'Science', 'Language', 'Arts', 'Other']
+
+export default function Home() {
+ const [courses, setCourses] = useState([])
+ const [search, setSearch] = useState('')
+ const [category, setCategory] = useState('')
+ const [loading, setLoading] = useState(true)
+ const [enrolling, setEnrolling] = useState(null)
+ const { user } = useAuth()
+ const navigate = useNavigate()
+
+ useEffect(() => {
+ fetchCourses()
+ }, [search, category])
+
+ async function fetchCourses() {
+ setLoading(true)
+ try {
+ const params = {}
+ if (search) params.search = search
+ if (category) params.category = category
+ const res = await api.get('/courses', { params })
+ setCourses(res.data.courses)
+ } catch {
+ setCourses([])
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ async function handleEnroll(courseId) {
+ if (!user) return navigate('/login')
+ setEnrolling(courseId)
+ try {
+ await api.post('/enrollments', { course_id: courseId })
+ navigate(`/courses/${courseId}`)
+ } catch (err) {
+ if (err.response?.status === 409) {
+ navigate(`/courses/${courseId}`)
+ } else {
+ alert(err.response?.data?.error || 'Failed to enroll')
+ }
+ } finally {
+ setEnrolling(null)
+ }
+ }
+
+ return (
+
+
+
Learn Something New Today
+
Explore thousands of courses taught by expert instructors.
+
+
+
+ setSearch(e.target.value)}
+ className="search-input"
+ />
+
+
+
+ {loading ? (
+
Loading courses...
+ ) : courses.length === 0 ? (
+
+
No courses found. {search || category ? 'Try different filters.' : 'Check back soon!'}
+
+ ) : (
+
+ {courses.map(course => (
+
+
+ {course.thumbnail
+ ?

+ :
🎓
+ }
+
+
+
{course.category || 'General'}
+
+ {course.title}
+
+
{course.description?.slice(0, 100)}{course.description?.length > 100 ? '...' : ''}
+
By {course.instructor_name}
+
+ {course.price === 0 ? 'Free' : `$${course.price}`}
+
+
+
+
+ ))}
+
+ )}
+
+ )
+}
diff --git a/frontend/src/pages/InstructorDashboard.jsx b/frontend/src/pages/InstructorDashboard.jsx
new file mode 100644
index 0000000..6e47f57
--- /dev/null
+++ b/frontend/src/pages/InstructorDashboard.jsx
@@ -0,0 +1,395 @@
+import { useState, useEffect } from 'react'
+import api from '../api'
+import { useAuth } from '../context/AuthContext'
+
+const CATEGORIES = ['Technology', 'Business', 'Design', 'Marketing', 'Science', 'Language', 'Arts', 'Other']
+
+export default function InstructorDashboard() {
+ const { user } = useAuth()
+ const [courses, setCourses] = useState([])
+ const [activeTab, setActiveTab] = useState('courses')
+ const [selectedCourse, setSelectedCourse] = useState(null)
+ const [sections, setSections] = useState([])
+ const [submissions, setSubmissions] = useState([])
+ const [assignments, setAssignments] = useState([])
+ const [quizzes, setQuizzes] = useState([])
+ const [loading, setLoading] = useState(true)
+
+ const [courseForm, setCourseForm] = useState({ title: '', description: '', category: '', price: 0, status: 'draft' })
+ const [sectionForm, setSectionForm] = useState({ title: '', order_index: 0 })
+ const [videoForm, setVideoForm] = useState({ title: '', section_id: '', order_index: 0, file: null })
+ const [assignmentForm, setAssignmentForm] = useState({ title: '', description: '', due_date: '', max_points: 100, section_id: '' })
+ const [announcementForm, setAnnouncementForm] = useState({ title: '', body: '' })
+ const [quizForm, setQuizForm] = useState({ title: '', section_id: '' })
+ const [questionForm, setQuestionForm] = useState({ question: '', options: '', correct_answer: '', points: 1 })
+ const [gradeForm, setGradeForm] = useState({})
+ const [msg, setMsg] = useState('')
+
+ useEffect(() => {
+ fetchCourses()
+ }, [])
+
+ async function fetchCourses() {
+ setLoading(true)
+ try {
+ const res = await api.get('/courses', { params: { all: true } })
+ const myCourses = res.data.courses.filter(c => c.instructor_id === user.id || user.role === 'admin')
+ setCourses(myCourses)
+ } catch {} finally {
+ setLoading(false)
+ }
+ }
+
+ async function fetchCourseDetail(courseId) {
+ try {
+ const [courseRes, assignmentsRes, quizzesRes] = await Promise.all([
+ api.get(`/courses/${courseId}`),
+ api.get(`/assignments/${courseId}`),
+ api.get(`/quizzes/${courseId}`)
+ ])
+ setSelectedCourse(courseRes.data.course)
+ setSections(courseRes.data.sections)
+ setAssignments(assignmentsRes.data.assignments)
+ setQuizzes(quizzesRes.data.quizzes)
+ } catch {}
+ }
+
+ async function fetchSubmissions(assignmentId) {
+ try {
+ const res = await api.get(`/assignments/${assignmentId}/submissions`)
+ setSubmissions(res.data.submissions)
+ } catch {}
+ }
+
+ async function handleCreateCourse(e) {
+ e.preventDefault()
+ try {
+ const res = await api.post('/courses', courseForm)
+ setCourses([res.data.course, ...courses])
+ setCourseForm({ title: '', description: '', category: '', price: 0, status: 'draft' })
+ setMsg('Course created!')
+ } catch (err) {
+ setMsg(err.response?.data?.error || 'Failed to create course')
+ }
+ }
+
+ async function handlePublishToggle(course) {
+ const newStatus = course.status === 'published' ? 'draft' : 'published'
+ try {
+ const res = await api.put(`/courses/${course.id}`, { status: newStatus })
+ setCourses(courses.map(c => c.id === course.id ? res.data.course : c))
+ if (selectedCourse?.id === course.id) setSelectedCourse(res.data.course)
+ } catch {}
+ }
+
+ async function handleDeleteCourse(courseId) {
+ if (!confirm('Delete this course?')) return
+ try {
+ await api.delete(`/courses/${courseId}`)
+ setCourses(courses.filter(c => c.id !== courseId))
+ if (selectedCourse?.id === courseId) setSelectedCourse(null)
+ } catch {}
+ }
+
+ async function handleAddSection(e) {
+ e.preventDefault()
+ try {
+ const res = await api.post(`/courses/${selectedCourse.id}/sections`, sectionForm)
+ setSections([...sections, { ...res.data.section, videos: [], assignments: [], quizzes: [] }])
+ setSectionForm({ title: '', order_index: sections.length })
+ setMsg('Section added!')
+ } catch (err) {
+ setMsg(err.response?.data?.error || 'Failed to add section')
+ }
+ }
+
+ async function handleDeleteSection(sectionId) {
+ if (!confirm('Delete this section?')) return
+ try {
+ await api.delete(`/courses/${selectedCourse.id}/sections/${sectionId}`)
+ setSections(sections.filter(s => s.id !== sectionId))
+ } catch {}
+ }
+
+ async function handleUploadVideo(e) {
+ e.preventDefault()
+ if (!videoForm.file) return setMsg('Please select a video file')
+ const fd = new FormData()
+ fd.append('video', videoForm.file)
+ fd.append('course_id', selectedCourse.id)
+ fd.append('title', videoForm.title)
+ if (videoForm.section_id) fd.append('section_id', videoForm.section_id)
+ fd.append('order_index', videoForm.order_index)
+ try {
+ await api.post('/videos/upload', fd, { headers: { 'Content-Type': 'multipart/form-data' } })
+ setVideoForm({ title: '', section_id: '', order_index: 0, file: null })
+ fetchCourseDetail(selectedCourse.id)
+ setMsg('Video uploaded!')
+ } catch (err) {
+ setMsg(err.response?.data?.error || 'Upload failed')
+ }
+ }
+
+ async function handleCreateAssignment(e) {
+ e.preventDefault()
+ try {
+ const res = await api.post('/assignments', { ...assignmentForm, course_id: selectedCourse.id })
+ setAssignments([...assignments, res.data.assignment])
+ setAssignmentForm({ title: '', description: '', due_date: '', max_points: 100, section_id: '' })
+ setMsg('Assignment created!')
+ } catch (err) {
+ setMsg(err.response?.data?.error || 'Failed')
+ }
+ }
+
+ async function handleCreateAnnouncement(e) {
+ e.preventDefault()
+ try {
+ await api.post('/announcements', { ...announcementForm, course_id: selectedCourse.id })
+ setAnnouncementForm({ title: '', body: '' })
+ setMsg('Announcement posted!')
+ } catch {}
+ }
+
+ async function handleCreateQuiz(e) {
+ e.preventDefault()
+ try {
+ const res = await api.post('/quizzes', { ...quizForm, course_id: selectedCourse.id })
+ setQuizzes([...quizzes, res.data.quiz])
+ setQuizForm({ title: '', section_id: '' })
+ setMsg('Quiz created!')
+ } catch {}
+ }
+
+ async function handleAddQuestion(quizId, e) {
+ e.preventDefault()
+ const options = questionForm.options.split(',').map(o => o.trim())
+ try {
+ await api.post(`/quizzes/${quizId}/questions`, { ...questionForm, options })
+ setQuestionForm({ question: '', options: '', correct_answer: '', points: 1 })
+ setMsg('Question added!')
+ } catch {}
+ }
+
+ async function handleGrade(submissionId, assignmentId) {
+ const g = gradeForm[submissionId] || {}
+ try {
+ await api.put(`/assignments/${assignmentId}/grade/${submissionId}`, g)
+ setMsg('Graded!')
+ fetchSubmissions(assignmentId)
+ } catch {}
+ }
+
+ if (loading) return Loading...
+
+ return (
+
+
Instructor Dashboard
+
+ {msg &&
setMsg('')}>{msg} ✕
}
+
+
+ {['courses', 'manage'].map(t => (
+
+ ))}
+
+
+ {activeTab === 'courses' && (
+
+
+
Create New Course
+
+
+
+
+ {courses.length === 0 ? (
+
No courses yet. Create your first course above!
+ ) : (
+ courses.map(course => (
+
+
+
{course.title}
+
{course.description?.slice(0, 80)}
+
+ {course.status}
+
+
{course.price === 0 ? 'Free' : `$${course.price}`}
+
+
+
+
+
+
+
+ ))
+ )}
+
+
+ )}
+
+ {activeTab === 'manage' && selectedCourse && (
+
+
+
{selectedCourse.title}
+
+
+
+
+
+
+
+
+
Add Assignment
+
+
+
+
+
Submissions
+ {assignments.map(a => (
+
+
{a.title}
+
+ {submissions.filter(s => s.assignment_id === a.id).map(sub => (
+
+
{sub.student_name} — {sub.content?.slice(0, 80)}
+ {sub.grade !== null && sub.grade !== undefined
+ ?
Graded: {sub.grade}/{a.max_points}
+ :
+ setGradeForm({ ...gradeForm, [sub.id]: { ...gradeForm[sub.id], grade: parseFloat(e.target.value) } })} />
+ setGradeForm({ ...gradeForm, [sub.id]: { ...gradeForm[sub.id], feedback: e.target.value } })} />
+
+
+ }
+
+ ))}
+
+ ))}
+
+
+
+
+
+
+ )}
+
+ )
+}
diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx
new file mode 100644
index 0000000..4ada407
--- /dev/null
+++ b/frontend/src/pages/Login.jsx
@@ -0,0 +1,68 @@
+import { useState } from 'react'
+import { Link, useNavigate } from 'react-router-dom'
+import api from '../api'
+import { useAuth } from '../context/AuthContext'
+
+export default function Login() {
+ const [form, setForm] = useState({ email: '', password: '' })
+ const [error, setError] = useState('')
+ const [loading, setLoading] = useState(false)
+ const { login } = useAuth()
+ const navigate = useNavigate()
+
+ async function handleSubmit(e) {
+ e.preventDefault()
+ setError('')
+ setLoading(true)
+ try {
+ const res = await api.post('/login', form)
+ login(res.data.token, res.data.user)
+ const { role } = res.data.user
+ if (role === 'admin') navigate('/admin')
+ else if (role === 'instructor') navigate('/instructor')
+ else navigate('/dashboard')
+ } catch (err) {
+ setError(err.response?.data?.error || 'Login failed')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
Welcome Back
+
Sign in to continue learning
+ {error &&
{error}
}
+
+
+ Don't have an account? Sign up
+
+
+
+ )
+}
diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx
new file mode 100644
index 0000000..2f8ba68
--- /dev/null
+++ b/frontend/src/pages/Register.jsx
@@ -0,0 +1,105 @@
+import { useState } from 'react'
+import { Link, useNavigate } from 'react-router-dom'
+import api from '../api'
+import { useAuth } from '../context/AuthContext'
+
+export default function Register() {
+ const [form, setForm] = useState({ name: '', email: '', password: '', role: 'student' })
+ const [error, setError] = useState('')
+ const [loading, setLoading] = useState(false)
+ const { login } = useAuth()
+ const navigate = useNavigate()
+
+ async function handleSubmit(e) {
+ e.preventDefault()
+ setError('')
+ if (form.password.length < 6) {
+ return setError('Password must be at least 6 characters')
+ }
+ setLoading(true)
+ try {
+ const res = await api.post('/register', form)
+ login(res.data.token, res.data.user)
+ const { role } = res.data.user
+ if (role === 'instructor') navigate('/instructor')
+ else navigate('/dashboard')
+ } catch (err) {
+ setError(err.response?.data?.error || 'Registration failed')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
Create Account
+
Join EduScale and start learning
+ {error &&
{error}
}
+
+
+ Already have an account? Sign in
+
+
+
+ )
+}
diff --git a/frontend/src/pages/StudentDashboard.jsx b/frontend/src/pages/StudentDashboard.jsx
new file mode 100644
index 0000000..0be0195
--- /dev/null
+++ b/frontend/src/pages/StudentDashboard.jsx
@@ -0,0 +1,152 @@
+import { useState, useEffect } from 'react'
+import { Link } from 'react-router-dom'
+import api from '../api'
+import { useAuth } from '../context/AuthContext'
+
+export default function StudentDashboard() {
+ const { user } = useAuth()
+ const [enrollments, setEnrollments] = useState([])
+ const [assignments, setAssignments] = useState([])
+ const [certificates, setCertificates] = useState([])
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ Promise.all([
+ api.get('/enrollments'),
+ api.get('/certificates/0').catch(() => ({ data: { certificates: [] } }))
+ ]).then(([enrollRes]) => {
+ setEnrollments(enrollRes.data.enrollments)
+ fetchAssignments(enrollRes.data.enrollments)
+ }).finally(() => setLoading(false))
+
+ fetchCertificates()
+ }, [])
+
+ async function fetchCertificates() {
+ try {
+ const enrollRes = await api.get('/enrollments')
+ const certs = []
+ for (const e of enrollRes.data.enrollments) {
+ try {
+ const res = await api.get(`/certificates/${e.course_id}`)
+ certs.push({ ...res.data.certificate, course_title: e.course_title })
+ } catch {}
+ }
+ setCertificates(certs)
+ } catch {}
+ }
+
+ async function fetchAssignments(enrollments) {
+ const all = []
+ for (const e of enrollments) {
+ try {
+ const res = await api.get(`/assignments/${e.course_id}`)
+ const upcoming = res.data.assignments.filter(a => !a.due_date || new Date(a.due_date) >= new Date())
+ all.push(...upcoming.map(a => ({ ...a, course_title: e.course_title })))
+ } catch {}
+ }
+ setAssignments(all.sort((a, b) => {
+ if (!a.due_date) return 1
+ if (!b.due_date) return -1
+ return new Date(a.due_date) - new Date(b.due_date)
+ }))
+ }
+
+ if (loading) return Loading dashboard...
+
+ return (
+
+
My Learning
+
Welcome back, {user?.name}!
+
+
+
+
+ Enrolled Courses ({enrollments.length})
+ {enrollments.length === 0 ? (
+
+
No courses yet. Browse courses
+
+ ) : (
+
+ {enrollments.map(e => (
+
+
+
{e.course_title}
+
By {e.instructor_name}
+ {e.category &&
{e.category}}
+
+
+
+
{Math.round(e.progress || 0)}% complete
+
+
+
+ View Course
+
+ {e.progress >= 100 && (
+ Completed ✓
+ )}
+
+
+ ))}
+
+ )}
+
+
+
+
+
+ Upcoming Assignments
+ {assignments.length === 0 ? (
+ No upcoming assignments
+ ) : (
+
+ {assignments.slice(0, 5).map(a => (
+ -
+ 📝
+
+
{a.title}
+
{a.course_title}
+ {a.due_date && (
+
Due: {new Date(a.due_date).toLocaleDateString()}
+ )}
+
+
+ ))}
+
+ )}
+
+
+
+ Certificates ({certificates.length})
+ {certificates.length === 0 ? (
+ Complete a course to earn a certificate!
+ ) : (
+
+ {certificates.map(c => (
+ -
+ 🏆
+
+
+ ))}
+
+ )}
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/VideoPlayer.jsx b/frontend/src/pages/VideoPlayer.jsx
new file mode 100644
index 0000000..b52adda
--- /dev/null
+++ b/frontend/src/pages/VideoPlayer.jsx
@@ -0,0 +1,164 @@
+import { useState, useEffect, useRef, useCallback } from 'react'
+import { useParams, Link } from 'react-router-dom'
+import api from '../api'
+
+export default function VideoPlayer() {
+ const { id: courseId, videoId } = useParams()
+ const videoRef = useRef(null)
+ const progressTimer = useRef(null)
+ const [video, setVideo] = useState(null)
+ const [course, setCourse] = useState(null)
+ const [sections, setSections] = useState([])
+ const [progress, setProgress] = useState({ watched_seconds: 0, completed: false })
+ const [playbackRate, setPlaybackRate] = useState(1)
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ fetchData()
+ return () => clearInterval(progressTimer.current)
+ }, [videoId])
+
+ async function fetchData() {
+ setLoading(true)
+ try {
+ const [courseRes, progressRes] = await Promise.all([
+ api.get(`/courses/${courseId}`),
+ api.get(`/videos/${videoId}/progress`)
+ ])
+ setCourse(courseRes.data.course)
+ setSections(courseRes.data.sections)
+
+ const allVideos = courseRes.data.sections.flatMap(s => s.videos || [])
+ const currentVideo = allVideos.find(v => v.id === parseInt(videoId))
+ setVideo(currentVideo)
+
+ setProgress(courseRes.data.progress || progressRes.data.progress)
+ } catch (err) {
+ console.error(err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const saveProgress = useCallback(async (completed = false) => {
+ if (!videoRef.current) return
+ const watched_seconds = Math.floor(videoRef.current.currentTime)
+ try {
+ await api.put(`/videos/${videoId}/progress`, { watched_seconds, completed })
+ setProgress(p => ({ ...p, watched_seconds, completed }))
+ } catch {}
+ }, [videoId])
+
+ function handleTimeUpdate() {
+ if (!videoRef.current) return
+ const { currentTime, duration } = videoRef.current
+ if (duration && currentTime / duration >= 0.9) {
+ saveProgress(true)
+ }
+ }
+
+ function handlePlay() {
+ progressTimer.current = setInterval(() => saveProgress(false), 15000)
+ }
+
+ function handlePause() {
+ clearInterval(progressTimer.current)
+ saveProgress(false)
+ }
+
+ function handleEnded() {
+ clearInterval(progressTimer.current)
+ saveProgress(true)
+ }
+
+ function handleSpeedChange(speed) {
+ setPlaybackRate(speed)
+ if (videoRef.current) videoRef.current.playbackRate = speed
+ }
+
+ if (loading) return Loading video...
+ if (!video) return Video not found
+
+ const allVideos = sections.flatMap(s => s.videos || [])
+ const currentIndex = allVideos.findIndex(v => v.id === parseInt(videoId))
+ const prevVideo = currentIndex > 0 ? allVideos[currentIndex - 1] : null
+ const nextVideo = currentIndex < allVideos.length - 1 ? allVideos[currentIndex + 1] : null
+
+ return (
+
+
+
+
+
+
+
+
{video.title}
+
+ Speed:
+ {[0.5, 0.75, 1, 1.25, 1.5, 2].map(speed => (
+
+ ))}
+
+
+
+
+ {prevVideo && (
+
+ ← {prevVideo.title}
+
+ )}
+
+ {nextVideo && (
+
+ {nextVideo.title} →
+
+ )}
+
+
+
+ {progress.completed
+ ? ✓ Completed
+ : Watched {Math.floor((progress.watched_seconds || 0) / 60)}m {(progress.watched_seconds || 0) % 60}s
+ }
+
+
+
+
+
{course?.title}
+
+ {sections.map(section => (
+
+
{section.title}
+ {(section.videos || []).map(v => (
+
+ ▶
+ {v.title}
+
+ ))}
+
+ ))}
+
+
+
+ )
+}
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
new file mode 100644
index 0000000..a74791f
--- /dev/null
+++ b/frontend/vite.config.js
@@ -0,0 +1,12 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3000,
+ proxy: {
+ '/api': 'http://localhost:5000'
+ }
+ }
+})
From 44510e5870409a58c8f93439da437d29ade4da22 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 16 Apr 2026 13:42:54 +0000
Subject: [PATCH 3/9] fix: add rate limiting to all API routes to prevent brute
force attacks
- Auth endpoints (login/register): 30 req/15min
- All other API endpoints: 300 req/15min
- Uses express-rate-limit with standardHeaders
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Gabru44 <150784667+Gabru44@users.noreply.github.com>
---
backend/eduscale.db-shm | Bin 0 -> 32768 bytes
backend/eduscale.db-wal | Bin 0 -> 61832 bytes
backend/package-lock.json | 28 ++++++++++++++++++++++++++++
backend/package.json | 1 +
backend/server.js | 21 +++++++++++++++++++++
5 files changed, 50 insertions(+)
create mode 100644 backend/eduscale.db-shm
create mode 100644 backend/eduscale.db-wal
diff --git a/backend/eduscale.db-shm b/backend/eduscale.db-shm
new file mode 100644
index 0000000000000000000000000000000000000000..9a8ead9f43d21eeb0f7f09e9a7feb1e667aca572
GIT binary patch
literal 32768
zcmeI)yG;W@5CG7%4>*pm6H6C3B()roK^3lnHb@D`oRUBRZow@$vxr>)Ex@dGhb(6f
zVc$rjo!eWjc3uI~Zl@{7S>zz4(QT}4-ivPgcNs=iMV>_N*ZKRj+s|*T5B}$~f&c*m1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
ze@$Qz(^jrhp3EW$lqN8YxkA^aHLYI)JAo=De$}Z-H#`UwCvaX|#~LQk3taStl|XR<
zm&J9gVFCmQ5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+0D=D_
F@B>k`C9wbi
literal 0
HcmV?d00001
diff --git a/backend/eduscale.db-wal b/backend/eduscale.db-wal
new file mode 100644
index 0000000000000000000000000000000000000000..bb23e75b434b882d8e0635c20d41e1a4ac5e9a21
GIT binary patch
literal 61832
zcmeI*Z)_8F7{KvskKsyW$6mlNn7D;T7+U2K0r5#A!qmg*W=b-?glbPz@?OMJ#
zS<1Sr9gg@rm@|t0aMmB{_K#>rWvr6U$7AV|F`F{=X?r@a@7JQ)L-9mn*WBKzLhtc#
zSsTv`#`~>Uawy%GoUxs9UN1S}?r?8cccd%S+a6N4sIe_-ScE0Jp)KYMwmg60Tyf{N
zg*9TIN4Y79g@FJ92q1s}0tg_000IagfB*uIgg~p@>1)h3h_F_P*T%)Xzz4%e!%e^C
za??0tg_000IagfB*srAb`LM
z5Ln}tH~Df#*|K%JF)z?0uD(9+;K7eJ?A8wc^j@uA;DV%FSOKwdi3lKo00IagfB*sr
zAbwd5N@@K7H;D)5!SV=*1tq35100IagfB*sr
zAb~)$>(cUm(&aNM8#*H5U+Y4LFUPw@6Vs*@P;8FgN#KJ%T0R#|0
z009ILKmY**5I_KdM^9kYDxc3M!gA*xtk(q!009IL
zKmY**5I_I{1Q0;re-xONo252&zG;)H<%{A`du3C%vhKSbZUcQVXB7S6tUuK4kL4m}
z|J3gOsAkM&GwM*>O!ptx=R$|=nURS~U%9AhSuHcTCz6~@O*tc#{bSSWr0taRqO^XQ
zU(b(uo28Dxd{c)yP_iAXoO2AT;YojYPOCeyDa#p{iKmA1nZd+FJmo~4WVU-EmN3kH
z=7HQ;$V_C_Jx7A0!SL|NsA~3RhTH?y4!Pvak;tq^oVnADxH?}zHSA?09?^`-SS6j0
z$I>NZHf8G5_H-
z2(~V)e`l`m#UI^z0a^J%Qf?~O#D;+Y0tg_000IagfB*srAb-`s1#jH(rW=$Gn3N>CNV{5I_I{1Q0*~0R#|0009IL
zxF3Q0n|IKy6>$3q>Un{$^`0~9ue|Yet&iYrt&iaB{hW}`Ab= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": ">= 4.11"
+ }
+ },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -1004,6 +1023,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/ip-address": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
+ "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
diff --git a/backend/package.json b/backend/package.json
index fa34573..b9ca568 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -12,6 +12,7 @@
"cors": "^2.8.5",
"dotenv": "^16.4.1",
"express": "^4.18.2",
+ "express-rate-limit": "^8.3.2",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"pdfkit": "^0.14.0",
diff --git a/backend/server.js b/backend/server.js
index ca45a6e..2558811 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -2,6 +2,7 @@ require('dotenv').config();
const express = require('express');
const cors = require('cors');
const path = require('path');
+const rateLimit = require('express-rate-limit');
const app = express();
const PORT = process.env.PORT || 5000;
@@ -13,6 +14,26 @@ app.use(cors({
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
+const authLimiter = rateLimit({
+ windowMs: 15 * 60 * 1000,
+ max: 30,
+ standardHeaders: true,
+ legacyHeaders: false,
+ message: { error: 'Too many requests, please try again later' }
+});
+
+const apiLimiter = rateLimit({
+ windowMs: 15 * 60 * 1000,
+ max: 300,
+ standardHeaders: true,
+ legacyHeaders: false,
+ message: { error: 'Too many requests, please try again later' }
+});
+
+app.use('/api/login', authLimiter);
+app.use('/api/register', authLimiter);
+app.use('/api', apiLimiter);
+
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
app.use('/api', require('./routes/auth'));
From e042dcced6e1dee944d80363eb1837780e4a3a13 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 16 Apr 2026 13:42:59 +0000
Subject: [PATCH 4/9] chore: remove db wal files from tracking
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Gabru44 <150784667+Gabru44@users.noreply.github.com>
---
backend/eduscale.db-shm | Bin 32768 -> 0 bytes
backend/eduscale.db-wal | Bin 61832 -> 0 bytes
2 files changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 backend/eduscale.db-shm
delete mode 100644 backend/eduscale.db-wal
diff --git a/backend/eduscale.db-shm b/backend/eduscale.db-shm
deleted file mode 100644
index 9a8ead9f43d21eeb0f7f09e9a7feb1e667aca572..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 32768
zcmeI)yG;W@5CG7%4>*pm6H6C3B()roK^3lnHb@D`oRUBRZow@$vxr>)Ex@dGhb(6f
zVc$rjo!eWjc3uI~Zl@{7S>zz4(QT}4-ivPgcNs=iMV>_N*ZKRj+s|*T5B}$~f&c*m1PBlyK!5-N0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk
ze@$Qz(^jrhp3EW$lqN8YxkA^aHLYI)JAo=De$}Z-H#`UwCvaX|#~LQk3taStl|XR<
zm&J9gVFCmQ5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+0D=D_
F@B>k`C9wbi
diff --git a/backend/eduscale.db-wal b/backend/eduscale.db-wal
deleted file mode 100644
index bb23e75b434b882d8e0635c20d41e1a4ac5e9a21..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 61832
zcmeI*Z)_8F7{KvskKsyW$6mlNn7D;T7+U2K0r5#A!qmg*W=b-?glbPz@?OMJ#
zS<1Sr9gg@rm@|t0aMmB{_K#>rWvr6U$7AV|F`F{=X?r@a@7JQ)L-9mn*WBKzLhtc#
zSsTv`#`~>Uawy%GoUxs9UN1S}?r?8cccd%S+a6N4sIe_-ScE0Jp)KYMwmg60Tyf{N
zg*9TIN4Y79g@FJ92q1s}0tg_000IagfB*uIgg~p@>1)h3h_F_P*T%)Xzz4%e!%e^C
za??0tg_000IagfB*srAb`LM
z5Ln}tH~Df#*|K%JF)z?0uD(9+;K7eJ?A8wc^j@uA;DV%FSOKwdi3lKo00IagfB*sr
zAbwd5N@@K7H;D)5!SV=*1tq35100IagfB*sr
zAb~)$>(cUm(&aNM8#*H5U+Y4LFUPw@6Vs*@P;8FgN#KJ%T0R#|0
z009ILKmY**5I_KdM^9kYDxc3M!gA*xtk(q!009IL
zKmY**5I_I{1Q0;re-xONo252&zG;)H<%{A`du3C%vhKSbZUcQVXB7S6tUuK4kL4m}
z|J3gOsAkM&GwM*>O!ptx=R$|=nURS~U%9AhSuHcTCz6~@O*tc#{bSSWr0taRqO^XQ
zU(b(uo28Dxd{c)yP_iAXoO2AT;YojYPOCeyDa#p{iKmA1nZd+FJmo~4WVU-EmN3kH
z=7HQ;$V_C_Jx7A0!SL|NsA~3RhTH?y4!Pvak;tq^oVnADxH?}zHSA?09?^`-SS6j0
z$I>NZHf8G5_H-
z2(~V)e`l`m#UI^z0a^J%Qf?~O#D;+Y0tg_000IagfB*srAb-`s1#jH(rW=$Gn3N>CNV{5I_I{1Q0*~0R#|0009IL
zxF3Q0n|IKy6>$3q>Un{$^`0~9ue|Yet&iYrt&iaB{hW}`Ab
Date: Thu, 16 Apr 2026 13:46:32 +0000
Subject: [PATCH 5/9] fix: address security and code quality issues from review
- Remove hardcoded JWT_SECRET fallback; require env var in both auth.js and middleware/auth.js
- Fix potential ReDoS in email validation; replace regex with safe indexOf-based check
- Add named constants MAX_VIDEO_SIZE_BYTES and MAX_ASSIGNMENT_FILE_SIZE_BYTES
- Add error logging in VideoPlayer saveProgress catch block
- Add error logging in InstructorDashboard fetchCourses catch block
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Gabru44 <150784667+Gabru44@users.noreply.github.com>
---
backend/middleware/auth.js | 10 +++++++---
backend/routes/assignments.js | 4 +++-
backend/routes/auth.js | 19 ++++++++++++++++---
backend/routes/videos.js | 4 +++-
frontend/src/pages/InstructorDashboard.jsx | 4 +++-
frontend/src/pages/VideoPlayer.jsx | 4 +++-
6 files changed, 35 insertions(+), 10 deletions(-)
diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js
index 09b0273..a7299f9 100644
--- a/backend/middleware/auth.js
+++ b/backend/middleware/auth.js
@@ -1,7 +1,11 @@
const jwt = require('jsonwebtoken');
const db = require('../db');
-const JWT_SECRET = process.env.JWT_SECRET || 'eduscale_super_secret_jwt_key_2024';
+function getJwtSecret() {
+ const secret = process.env.JWT_SECRET;
+ if (!secret) throw new Error('JWT_SECRET environment variable is required');
+ return secret;
+}
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
@@ -10,7 +14,7 @@ function authenticate(req, res, next) {
}
const token = authHeader.slice(7);
try {
- const payload = jwt.verify(token, JWT_SECRET);
+ const payload = jwt.verify(token, getJwtSecret());
const user = db.prepare('SELECT id, name, email, role FROM users WHERE id = ?').get(payload.id);
if (!user) return res.status(401).json({ error: 'User not found' });
req.user = user;
@@ -28,7 +32,7 @@ function optionalAuth(req, res, next) {
}
const token = authHeader.slice(7);
try {
- const payload = jwt.verify(token, JWT_SECRET);
+ const payload = jwt.verify(token, getJwtSecret());
const user = db.prepare('SELECT id, name, email, role FROM users WHERE id = ?').get(payload.id);
req.user = user || null;
} catch {
diff --git a/backend/routes/assignments.js b/backend/routes/assignments.js
index 795fa14..5d6fbde 100644
--- a/backend/routes/assignments.js
+++ b/backend/routes/assignments.js
@@ -6,6 +6,8 @@ const { authenticate, requireRole } = require('../middleware/auth');
const router = express.Router();
+const MAX_ASSIGNMENT_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB
+
const storage = multer.diskStorage({
destination: path.join(__dirname, '../uploads/files'),
filename: (req, file, cb) => {
@@ -13,7 +15,7 @@ const storage = multer.diskStorage({
cb(null, unique + path.extname(file.originalname));
}
});
-const upload = multer({ storage, limits: { fileSize: 50 * 1024 * 1024 } });
+const upload = multer({ storage, limits: { fileSize: MAX_ASSIGNMENT_FILE_SIZE_BYTES } });
router.get('/assignments/:courseId', authenticate, (req, res) => {
const assignments = db.prepare(
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
index 660b426..48f3f23 100644
--- a/backend/routes/auth.js
+++ b/backend/routes/auth.js
@@ -5,10 +5,23 @@ const db = require('../db');
const { authenticate } = require('../middleware/auth');
const router = express.Router();
-const JWT_SECRET = process.env.JWT_SECRET || 'eduscale_super_secret_jwt_key_2024';
+
+function getJwtSecret() {
+ const secret = process.env.JWT_SECRET;
+ if (!secret) throw new Error('JWT_SECRET environment variable is required');
+ return secret;
+}
function signToken(user) {
- return jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, { expiresIn: '7d' });
+ return jwt.sign({ id: user.id, role: user.role }, getJwtSecret(), { expiresIn: '7d' });
+}
+
+function isValidEmail(email) {
+ if (typeof email !== 'string') return false;
+ const at = email.indexOf('@')
+ if (at < 1) return false;
+ const dot = email.lastIndexOf('.')
+ return dot > at + 1 && dot < email.length - 1;
}
router.post('/register', (req, res) => {
@@ -16,7 +29,7 @@ router.post('/register', (req, res) => {
if (!name || !email || !password) {
return res.status(400).json({ error: 'name, email, and password are required' });
}
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+ if (!isValidEmail(email)) {
return res.status(400).json({ error: 'Invalid email format' });
}
if (password.length < 6) {
diff --git a/backend/routes/videos.js b/backend/routes/videos.js
index eb5d9c2..708259f 100644
--- a/backend/routes/videos.js
+++ b/backend/routes/videos.js
@@ -7,6 +7,8 @@ const { authenticate, optionalAuth } = require('../middleware/auth');
const router = express.Router();
+const MAX_VIDEO_SIZE_BYTES = 2 * 1024 * 1024 * 1024; // 2 GB
+
const storage = multer.diskStorage({
destination: path.join(__dirname, '../uploads/videos'),
filename: (req, file, cb) => {
@@ -14,7 +16,7 @@ const storage = multer.diskStorage({
cb(null, unique + path.extname(file.originalname));
}
});
-const upload = multer({ storage, limits: { fileSize: 2 * 1024 * 1024 * 1024 } });
+const upload = multer({ storage, limits: { fileSize: MAX_VIDEO_SIZE_BYTES } });
router.post('/videos/upload', authenticate, upload.single('video'), (req, res) => {
if (!req.file) return res.status(400).json({ error: 'No video file provided' });
diff --git a/frontend/src/pages/InstructorDashboard.jsx b/frontend/src/pages/InstructorDashboard.jsx
index 6e47f57..0a3f9d7 100644
--- a/frontend/src/pages/InstructorDashboard.jsx
+++ b/frontend/src/pages/InstructorDashboard.jsx
@@ -35,7 +35,9 @@ export default function InstructorDashboard() {
const res = await api.get('/courses', { params: { all: true } })
const myCourses = res.data.courses.filter(c => c.instructor_id === user.id || user.role === 'admin')
setCourses(myCourses)
- } catch {} finally {
+ } catch (err) {
+ console.error('Failed to fetch courses:', err)
+ } finally {
setLoading(false)
}
}
diff --git a/frontend/src/pages/VideoPlayer.jsx b/frontend/src/pages/VideoPlayer.jsx
index b52adda..51c38a1 100644
--- a/frontend/src/pages/VideoPlayer.jsx
+++ b/frontend/src/pages/VideoPlayer.jsx
@@ -46,7 +46,9 @@ export default function VideoPlayer() {
try {
await api.put(`/videos/${videoId}/progress`, { watched_seconds, completed })
setProgress(p => ({ ...p, watched_seconds, completed }))
- } catch {}
+ } catch (err) {
+ console.error('Failed to save video progress:', err)
+ }
}, [videoId])
function handleTimeUpdate() {
From 13a32e850334bdf6f691870a192c72a116b1d8d4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 16 Apr 2026 13:48:02 +0000
Subject: [PATCH 6/9] style: improve variable names in isValidEmail function
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Gabru44 <150784667+Gabru44@users.noreply.github.com>
---
backend/routes/auth.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
index 48f3f23..5f488ec 100644
--- a/backend/routes/auth.js
+++ b/backend/routes/auth.js
@@ -18,10 +18,10 @@ function signToken(user) {
function isValidEmail(email) {
if (typeof email !== 'string') return false;
- const at = email.indexOf('@')
- if (at < 1) return false;
- const dot = email.lastIndexOf('.')
- return dot > at + 1 && dot < email.length - 1;
+ const atIndex = email.indexOf('@')
+ if (atIndex < 1) return false;
+ const lastDotIndex = email.lastIndexOf('.')
+ return lastDotIndex > atIndex + 1 && lastDotIndex < email.length - 1;
}
router.post('/register', (req, res) => {
From deb413f9a661ffa7069fb571b9612b352879c0c0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 16 Apr 2026 13:58:47 +0000
Subject: [PATCH 7/9] feat: implement EduScale online learning platform MVP
Agent-Logs-Url: https://github.com/Gabru44/Edus/sessions/3cc0ab1b-7a12-4358-8e13-9bc049280552
Co-authored-by: Gabru44 <150784667+Gabru44@users.noreply.github.com>
---
backend/eduscale.db-shm | Bin 0 -> 32768 bytes
backend/eduscale.db-wal | 0
frontend/src/pages/StudentDashboard.jsx | 36 +++++++++++-------------
3 files changed, 16 insertions(+), 20 deletions(-)
create mode 100644 backend/eduscale.db-shm
create mode 100644 backend/eduscale.db-wal
diff --git a/backend/eduscale.db-shm b/backend/eduscale.db-shm
new file mode 100644
index 0000000000000000000000000000000000000000..fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10
GIT binary patch
literal 32768
zcmeIuAr62r3 {
- Promise.all([
- api.get('/enrollments'),
- api.get('/certificates/0').catch(() => ({ data: { certificates: [] } }))
- ]).then(([enrollRes]) => {
- setEnrollments(enrollRes.data.enrollments)
- fetchAssignments(enrollRes.data.enrollments)
- }).finally(() => setLoading(false))
-
- fetchCertificates()
+ api.get('/enrollments').then(enrollRes => {
+ const enrollments = enrollRes.data.enrollments
+ setEnrollments(enrollments)
+ fetchAssignments(enrollments)
+ fetchCertificates(enrollments)
+ }).catch(() => {}).finally(() => setLoading(false))
}, [])
- async function fetchCertificates() {
- try {
- const enrollRes = await api.get('/enrollments')
- const certs = []
- for (const e of enrollRes.data.enrollments) {
- try {
- const res = await api.get(`/certificates/${e.course_id}`)
+ async function fetchCertificates(enrollments) {
+ const certs = []
+ for (const e of enrollments) {
+ try {
+ const res = await api.get(`/certificates/${e.course_id}`)
+ if (res.data.certificate) {
certs.push({ ...res.data.certificate, course_title: e.course_title })
- } catch {}
- }
- setCertificates(certs)
- } catch {}
+ }
+ } catch {}
+ }
+ setCertificates(certs)
}
async function fetchAssignments(enrollments) {
From 08675f1445104d9808bee23e92b5f3d588384f1c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 16 Apr 2026 13:59:51 +0000
Subject: [PATCH 8/9] chore: exclude SQLite WAL files from git tracking
Agent-Logs-Url: https://github.com/Gabru44/Edus/sessions/3cc0ab1b-7a12-4358-8e13-9bc049280552
Co-authored-by: Gabru44 <150784667+Gabru44@users.noreply.github.com>
---
.gitignore | 2 ++
backend/eduscale.db-shm | Bin 32768 -> 0 bytes
backend/eduscale.db-wal | 0
3 files changed, 2 insertions(+)
delete mode 100644 backend/eduscale.db-shm
delete mode 100644 backend/eduscale.db-wal
diff --git a/.gitignore b/.gitignore
index 0579695..ac0613d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
node_modules/
*.db
*.db-journal
+*.db-shm
+*.db-wal
dist/
.env
backend/uploads/videos/*
diff --git a/backend/eduscale.db-shm b/backend/eduscale.db-shm
deleted file mode 100644
index fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 32768
zcmeIuAr62r3
Date: Thu, 16 Apr 2026 14:02:48 +0000
Subject: [PATCH 9/9] fix: upgrade multer to 2.1.1 to resolve DoS
vulnerabilities
Agent-Logs-Url: https://github.com/Gabru44/Edus/sessions/3cc0ab1b-7a12-4358-8e13-9bc049280552
Co-authored-by: Gabru44 <150784667+Gabru44@users.noreply.github.com>
---
backend/package-lock.json | 126 ++++++++------------------------------
backend/package.json | 2 +-
2 files changed, 27 insertions(+), 101 deletions(-)
diff --git a/backend/package-lock.json b/backend/package-lock.json
index e85809b..3c3aaea 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -15,7 +15,7 @@
"express": "^4.18.2",
"express-rate-limit": "^8.3.2",
"jsonwebtoken": "^9.0.2",
- "multer": "^1.4.5-lts.1",
+ "multer": "^2.1.1",
"pdfkit": "^0.14.0",
"uuid": "^9.0.0"
}
@@ -145,20 +145,6 @@
"readable-stream": "^3.4.0"
}
},
- "node_modules/bl/node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "license": "MIT",
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
@@ -320,17 +306,17 @@
}
},
"node_modules/concat-stream": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
- "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
- "node >= 0.8"
+ "node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
- "readable-stream": "^2.2.2",
+ "readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
@@ -370,12 +356,6 @@
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
- "node_modules/core-util-is": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
- "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
- "license": "MIT"
- },
"node_modules/cors": {
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
@@ -1473,18 +1453,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/mkdirp": {
- "version": "0.5.6",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
- "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
- "license": "MIT",
- "dependencies": {
- "minimist": "^1.2.6"
- },
- "bin": {
- "mkdirp": "bin/cmd.js"
- }
- },
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
@@ -1498,22 +1466,22 @@
"license": "MIT"
},
"node_modules/multer": {
- "version": "1.4.5-lts.2",
- "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
- "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
- "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
+ "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
- "busboy": "^1.0.0",
- "concat-stream": "^1.5.2",
- "mkdirp": "^0.5.4",
- "object-assign": "^4.1.1",
- "type-is": "^1.6.4",
- "xtend": "^4.0.0"
+ "busboy": "^1.6.0",
+ "concat-stream": "^2.0.0",
+ "type-is": "^1.6.18"
},
"engines": {
- "node": ">= 6.0.0"
+ "node": ">= 10.16.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/napi-build-utils": {
@@ -1707,12 +1675,6 @@
"node": ">=10"
}
},
- "node_modules/process-nextick-args": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
- "license": "MIT"
- },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1791,32 +1753,19 @@
}
},
"node_modules/readable-stream": {
- "version": "2.3.8",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
- "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
}
},
- "node_modules/readable-stream/node_modules/isarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
- "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
- "license": "MIT"
- },
- "node_modules/readable-stream/node_modules/safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "license": "MIT"
- },
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -2180,20 +2129,6 @@
"node": ">=6"
}
},
- "node_modules/tar-stream/node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "license": "MIT",
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
@@ -2381,15 +2316,6 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
- },
- "node_modules/xtend": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
- "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4"
- }
}
}
}
diff --git a/backend/package.json b/backend/package.json
index b9ca568..cd12c4a 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -14,7 +14,7 @@
"express": "^4.18.2",
"express-rate-limit": "^8.3.2",
"jsonwebtoken": "^9.0.2",
- "multer": "^1.4.5-lts.1",
+ "multer": "^2.1.1",
"pdfkit": "^0.14.0",
"uuid": "^9.0.0"
}