From 97d0831a72d0a6c0a0cd1821da4c6d8f1a013e01 Mon Sep 17 00:00:00 2001 From: Priyamanjare54 <163539431+Priyamanjare54@users.noreply.github.com> Date: Sat, 17 Jan 2026 23:42:06 +0530 Subject: [PATCH 1/3] Backend schema change --- server/index.js | 2 + server/prisma/schema.prisma | 60 ++++++++++++++++++- server/routes/labs.js | 111 ++++++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 server/routes/labs.js diff --git a/server/index.js b/server/index.js index 07481e9..5ae72eb 100644 --- a/server/index.js +++ b/server/index.js @@ -20,6 +20,7 @@ import sitemapRoutes from "./routes/sitemap.js"; import vaultRoutes from "./routes/vault.js"; import { setupSocketHandlers } from "./socket/socketHandlers.js"; import initRedis from "./utils/redis.js"; +import labsRoutes from "./routes/labs.js"; BigInt.prototype.toJSON = function () { return this.toString(); @@ -105,6 +106,7 @@ app.use("/api/feedback", feedbackRoutes); app.use("/api/pdf-chat", pdfChatRoutes); app.use("/api/vault", vaultRoutes); app.use("/", sitemapRoutes); +app.use("/api/labs", labsRoutes); // Setup Socket.IO handlers setupSocketHandlers(io); diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index f3f2977..df2f6fe 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -20,13 +20,13 @@ model User { resetToken String? @map("reset_token") resetTokenExpires DateTime? @map("reset_token_expires") createdAt DateTime @default(now()) @map("created_at") - + // OAuth fields googleId String? @map("google_id") authProvider String @default("local") @map("auth_provider") // "local" or "google" profilePicture String? @map("profile_picture") needsUsername Boolean @default(false) @map("needs_username") - + @@index([googleId], map: "users_google_id_key") // Relations @@ -51,6 +51,10 @@ model User { bugReports BugReport[] pdfChatSessions PdfChatSession[] vault Vault? + labs Lab[] + labShares LabShare[] + labProgress LabProgress[] + @@map("users") } @@ -511,4 +515,56 @@ model VaultOutput { file VaultFile @relation(fields: [fileId], references: [id], onDelete: Cascade) @@map("vault_outputs") +} +model Lab { + id String @id @default(auto()) @map("_id") @db.ObjectId + title String + description String? + language String + difficulty String + content String + starterCode String + tasks String + testCases String? + solution String? + visibility String @default("private") + + creatorId String @db.ObjectId + creator User @relation(fields: [creatorId], references: [id]) + + shares LabShare[] + progress LabProgress[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model LabShare { + id String @id @default(auto()) @map("_id") @db.ObjectId + + labId String @db.ObjectId + lab Lab @relation(fields: [labId], references: [id], onDelete: Cascade) + + userId String @db.ObjectId + user User @relation(fields: [userId], references: [id]) + + createdAt DateTime @default(now()) + + @@unique([labId, userId]) +} + +model LabProgress { + id String @id @default(auto()) @map("_id") @db.ObjectId + + labId String @db.ObjectId + lab Lab @relation(fields: [labId], references: [id], onDelete: Cascade) + + userId String @db.ObjectId + user User @relation(fields: [userId], references: [id]) + + userCode String + completed Boolean @default(false) + lastAccessed DateTime @default(now()) + + @@unique([labId, userId]) } \ No newline at end of file diff --git a/server/routes/labs.js b/server/routes/labs.js new file mode 100644 index 0000000..c5b3d13 --- /dev/null +++ b/server/routes/labs.js @@ -0,0 +1,111 @@ +import express from "express"; +import { PrismaClient } from "@prisma/client"; +import { authenticateToken } from "../middleware/auth.js"; // existing JWT middleware + +const router = express.Router(); +const prisma = new PrismaClient(); + +// --- CREATE Lab --- +router.post("/", authenticateToken, async (req, res) => { + try { + const { title, description, language, difficulty, content, starterCode, tasks, testCases, solution, visibility } = req.body; + + const lab = await prisma.lab.create({ + data: { + title, + description, + language, + difficulty, + content, + starterCode, + tasks, + testCases, + solution, + visibility, + creatorId: req.user.id, // from JWT + }, + }); + + res.status(201).json(lab); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to create lab" }); + } +}); + +// --- GET ALL Labs --- +router.get("/", async (req, res) => { + try { + const labs = await prisma.lab.findMany({ + include: { + creator: true, + shares: true, + progress: true, + }, + }); + res.json(labs); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to fetch labs" }); + } +}); + +// --- GET SINGLE Lab --- +router.get("/:id", async (req, res) => { + try { + const { id } = req.params; + const lab = await prisma.lab.findUnique({ + where: { id }, + include: { + creator: true, + shares: true, + progress: true, + }, + }); + if (!lab) return res.status(404).json({ error: "Lab not found" }); + res.json(lab); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to fetch lab" }); + } +}); + +// --- UPDATE Lab --- +router.put("/:id", authenticateToken, async (req, res) => { + try { + const { id } = req.params; + const existing = await prisma.lab.findUnique({ where: { id } }); + if (!existing) return res.status(404).json({ error: "Lab not found" }); + + if (existing.creatorId !== req.user.id) return res.status(403).json({ error: "Forbidden" }); + + const lab = await prisma.lab.update({ + where: { id }, + data: req.body, + }); + + res.json(lab); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to update lab" }); + } +}); + +// --- DELETE Lab --- +router.delete("/:id", authenticateToken, async (req, res) => { + try { + const { id } = req.params; + const existing = await prisma.lab.findUnique({ where: { id } }); + if (!existing) return res.status(404).json({ error: "Lab not found" }); + + if (existing.creatorId !== req.user.id) return res.status(403).json({ error: "Forbidden" }); + + await prisma.lab.delete({ where: { id } }); + res.json({ message: "Lab deleted" }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to delete lab" }); + } +}); + +export default router; From a308f8be54f9c8de5b2f0acc6fad88b0de9bc288 Mon Sep 17 00:00:00 2001 From: Priyamanjare54 <163539431+Priyamanjare54@users.noreply.github.com> Date: Thu, 22 Jan 2026 08:44:04 +0530 Subject: [PATCH 2/3] fix: address review feedback for labs CRUD --- .gitignore | 3 +- Readme.md | 18 + server/package-lock.json | 655 ++++-------------------------------- server/package.json | 4 +- server/prisma/schema.prisma | 211 ++++++------ server/routes/labs.js | 116 ++++++- server/utils/validator.js | 4 + 7 files changed, 304 insertions(+), 707 deletions(-) create mode 100644 server/utils/validator.js diff --git a/.gitignore b/.gitignore index 8bad6cf..4cc8015 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vscode myapp -cmd.md \ No newline at end of file +cmd.md +/generated/prisma diff --git a/Readme.md b/Readme.md index a4d501c..9d43268 100644 --- a/Readme.md +++ b/Readme.md @@ -178,6 +178,24 @@ uvicorn main:app --reload - Server: http://localhost:3000 - Python Backend: http://localhost:8080 + +## MongoDB + Prisma (Important) + +This project uses Prisma with MongoDB. + +⚠️ MongoDB MUST be running as a replica set, even for local development. + +Quick setup (Windows): + +1. Stop MongoDB service +2. Start MongoDB with replication enabled: + mongod --replSet rs0 +3. In another terminal: + rs.initiate() +4. Use connection string: + mongodb://127.0.0.1:27017/edulume?replicaSet=rs0 + + 📖 **For detailed setup instructions, see [INSTALLATION.md](INSTALLATION.md)** --- diff --git a/server/package-lock.json b/server/package-lock.json index e97d1f1..1dbd171 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.864.0", "@prisma/client": "^5.22.0", - "@vercel/blob": "^1.1.1", + "@vercel/blob": "^0.0.2", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -30,7 +30,7 @@ "socket.io": "^4.8.1" }, "devDependencies": { - "@flydotio/dockerfile": "^0.7.10", + "@flydotio/dockerfile": "^0.7.4", "prisma": "^5.22.0" } }, @@ -932,27 +932,17 @@ "node": ">=18.0.0" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/@flydotio/dockerfile": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/@flydotio/dockerfile/-/dockerfile-0.7.10.tgz", - "integrity": "sha512-dTXqBjCl7nFmnhlyeDjjPtX+sdfYBWFH9PUKNqAYttvBiczKcYXxr7/0A0wZ+g1FB1tmMzsOzedgr6xap/AB9g==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@flydotio/dockerfile/-/dockerfile-0.7.4.tgz", + "integrity": "sha512-TFp5U/1W1AuevUMeXTA5pLnwFmK4nkgZkYpS3jnVsPN4pN4AxBIUKofoH6VUYVEYYEj2AtEtiHGhM7SvJY7zuA==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^5.4.1", - "diff": "^7.0.0", + "chalk": "^5.3.0", + "diff": "^5.2.0", "ejs": "^3.1.10", - "inquirer": "^12.4.1", - "shell-quote": "^1.8.2", + "shell-quote": "^1.8.1", "yargs": "^17.7.2" }, "bin": { @@ -962,373 +952,6 @@ "node": ">=16.0.0" } }, - "node_modules/@inquirer/ansi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", - "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/checkbox": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", - "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/confirm": { - "version": "5.1.21", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", - "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "10.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", - "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/editor": { - "version": "4.2.23", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", - "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/external-editor": "^1.0.3", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/expand": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", - "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", - "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", - "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", - "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/input": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", - "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/number": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", - "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/password": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", - "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/prompts": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", - "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/checkbox": "^4.3.2", - "@inquirer/confirm": "^5.1.21", - "@inquirer/editor": "^4.2.23", - "@inquirer/expand": "^4.0.23", - "@inquirer/input": "^4.3.1", - "@inquirer/number": "^3.0.23", - "@inquirer/password": "^4.0.23", - "@inquirer/rawlist": "^4.1.11", - "@inquirer/search": "^3.2.2", - "@inquirer/select": "^4.4.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/rawlist": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", - "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/search": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", - "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/select": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", - "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/type": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", - "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, "node_modules/@mongodb-js/saslprep": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.4.tgz", @@ -1423,6 +1046,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.10.0.tgz", "integrity": "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -2248,19 +1872,15 @@ } }, "node_modules/@vercel/blob": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@vercel/blob/-/blob-1.1.1.tgz", - "integrity": "sha512-heiJGj2qt5qTv6yiShH9f6KRAoZGj+lz61GQ+lBRL4lhvUmKI9A51KYlQTnsUd9ymdFlKHBlvmPeG+yGz2Qsbg==", - "license": "Apache-2.0", + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@vercel/blob/-/blob-0.0.2.tgz", + "integrity": "sha512-axJ3HSddUio3oHPfwU58Kk7z/amoxbv3hn5J2uRZN0B+1CWjn+05b2zuIGrfgzlHyTjUsHtixSOvW9Z/SKYSog==", + "license": "MIT", "dependencies": { - "async-retry": "^1.3.3", - "is-buffer": "^2.0.5", - "is-node-process": "^1.2.0", - "throttleit": "^2.1.0", - "undici": "^5.28.4" + "cross-fetch": "3.1.5" }, "engines": { - "node": ">=16.14" + "node": ">=14.x" } }, "node_modules/abort-controller": { @@ -2339,15 +1959,6 @@ "dev": true, "license": "MIT" }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", - "license": "MIT", - "dependencies": { - "retry": "0.13.1" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2499,23 +2110,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chardet": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", - "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2652,6 +2246,57 @@ "node": ">= 0.10" } }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "license": "MIT", + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/cross-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/cross-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/cross-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -2699,9 +2344,9 @@ } }, "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -3105,6 +2750,7 @@ "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, @@ -3326,33 +2972,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/inquirer": { - "version": "12.11.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.11.1.tgz", - "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/prompts": "^7.10.1", - "@inquirer/type": "^3.0.10", - "mute-stream": "^2.0.0", - "run-async": "^4.0.6", - "rxjs": "^7.8.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3362,29 +2981,6 @@ "node": ">= 0.10" } }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "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", - "engines": { - "node": ">=4" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3395,12 +2991,6 @@ "node": ">=8" } }, - "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", - "license": "MIT" - }, "node_modules/jake": { "version": "10.9.4", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", @@ -3660,16 +3250,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -3877,6 +3457,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/engines": "5.22.0" }, @@ -3977,35 +3558,6 @@ "node": ">=0.10.0" } }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/run-async": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", - "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4180,19 +3732,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/socket.io": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", @@ -4361,18 +3900,6 @@ ], "license": "MIT" }, - "node_modules/throttleit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", - "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -4419,18 +3946,6 @@ "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", "license": "MIT" }, - "node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", - "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, - "engines": { - "node": ">=14.0" - } - }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -4495,21 +4010,6 @@ "node": ">=18" } }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -4569,19 +4069,6 @@ "engines": { "node": ">=12" } - }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", - "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/server/package.json b/server/package.json index 47df894..0f13244 100644 --- a/server/package.json +++ b/server/package.json @@ -16,7 +16,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.864.0", "@prisma/client": "^5.22.0", - "@vercel/blob": "^1.1.1", + "@vercel/blob": "^0.0.2", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -34,7 +34,7 @@ "socket.io": "^4.8.1" }, "devDependencies": { - "@flydotio/dockerfile": "^0.7.10", + "@flydotio/dockerfile": "^0.7.4", "prisma": "^5.22.0" }, "keywords": [ diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index df2f6fe..31f2361 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -11,51 +11,49 @@ datasource db { } model User { - id String @id @default(auto()) @map("_id") @db.ObjectId - username String? @unique - email String @unique - passwordHash String? @map("password_hash") - isVerified Boolean @default(false) @map("is_verified") - verificationToken String? @map("verification_token") - resetToken String? @map("reset_token") + id String @id @default(auto()) @map("_id") @db.ObjectId + username String? @unique + email String @unique + passwordHash String? @map("password_hash") + isVerified Boolean @default(false) @map("is_verified") + verificationToken String? @map("verification_token") + resetToken String? @map("reset_token") resetTokenExpires DateTime? @map("reset_token_expires") - createdAt DateTime @default(now()) @map("created_at") + createdAt DateTime @default(now()) @map("created_at") // OAuth fields - googleId String? @map("google_id") - authProvider String @default("local") @map("auth_provider") // "local" or "google" - profilePicture String? @map("profile_picture") - needsUsername Boolean @default(false) @map("needs_username") - - @@index([googleId], map: "users_google_id_key") + googleId String? @map("google_id") + authProvider String @default("local") @map("auth_provider") // "local" or "google" + profilePicture String? @map("profile_picture") + needsUsername Boolean @default(false) @map("needs_username") // Relations - pdfs Pdf[] - ebooks Ebook[] - discussions Discussion[] - discussionAnswers DiscussionAnswer[] - discussionReplies DiscussionReply[] - discussionVotes DiscussionVote[] - answerVotes AnswerVote[] - replyVotes ReplyVote[] - notifications Notification[] @relation("UserNotifications") - sentNotifications Notification[] @relation("NotificationSender") - courses Course[] - bookmarkedCourses CourseBookmark[] - enrolledCourses CourseEnrollment[] - chapterProgress ChapterProgress[] - courseTests CourseTest[] - roadmaps Roadmap[] + pdfs Pdf[] + ebooks Ebook[] + discussions Discussion[] + discussionAnswers DiscussionAnswer[] + discussionReplies DiscussionReply[] + discussionVotes DiscussionVote[] + answerVotes AnswerVote[] + replyVotes ReplyVote[] + notifications Notification[] @relation("UserNotifications") + sentNotifications Notification[] @relation("NotificationSender") + courses Course[] + bookmarkedCourses CourseBookmark[] + enrolledCourses CourseEnrollment[] + chapterProgress ChapterProgress[] + courseTests CourseTest[] + roadmaps Roadmap[] bookmarkedRoadmaps RoadmapBookmark[] featureSuggestions FeatureSuggestion[] - bugReports BugReport[] - pdfChatSessions PdfChatSession[] - vault Vault? - labs Lab[] - labShares LabShare[] - labProgress LabProgress[] - + bugReports BugReport[] + pdfChatSessions PdfChatSession[] + vault Vault? + labs Lab[] + labShares LabShare[] + labProgress LabProgress[] + @@index([googleId], map: "users_google_id_key") @@map("users") } @@ -267,7 +265,7 @@ model CourseChapter { updatedAt DateTime @updatedAt @map("updated_at") // Relations - course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) progress ChapterProgress[] @@map("course_chapters") @@ -292,7 +290,7 @@ model Roadmap { title String description String topic String - content String // JSON string containing the roadmap data + content String // JSON string containing the roadmap data authorId String @map("author_id") @db.ObjectId isPublic Boolean @default(true) @map("is_public") views Int @default(0) @@ -324,7 +322,7 @@ model FeatureSuggestion { id String @id @default(auto()) @map("_id") @db.ObjectId title String description String - category String // 'ui', 'functionality', 'performance', 'other' + category String // 'ui', 'functionality', 'performance', 'other' priority String @default("medium") // 'low', 'medium', 'high' status String @default("pending") // 'pending', 'in-progress', 'completed', 'rejected' userEmail String? @map("user_email") @@ -341,22 +339,22 @@ model FeatureSuggestion { } model BugReport { - id String @id @default(auto()) @map("_id") @db.ObjectId - title String - description String - stepsToReproduce String? @map("steps_to_reproduce") - expectedBehavior String? @map("expected_behavior") - actualBehavior String? @map("actual_behavior") - severity String @default("medium") // 'low', 'medium', 'high', 'critical' - status String @default("open") // 'open', 'in-progress', 'resolved', 'closed' - browserInfo String? @map("browser_info") - deviceInfo String? @map("device_info") - userEmail String? @map("user_email") - userName String? @map("user_name") - userId String? @map("user_id") @db.ObjectId - adminNotes String? @map("admin_notes") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + id String @id @default(auto()) @map("_id") @db.ObjectId + title String + description String + stepsToReproduce String? @map("steps_to_reproduce") + expectedBehavior String? @map("expected_behavior") + actualBehavior String? @map("actual_behavior") + severity String @default("medium") // 'low', 'medium', 'high', 'critical' + status String @default("open") // 'open', 'in-progress', 'resolved', 'closed' + browserInfo String? @map("browser_info") + deviceInfo String? @map("device_info") + userEmail String? @map("user_email") + userName String? @map("user_name") + userId String? @map("user_id") @db.ObjectId + adminNotes String? @map("admin_notes") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") // Relations user User? @relation(fields: [userId], references: [id]) @@ -365,18 +363,18 @@ model BugReport { } model PdfChatSession { - id String @id @default(auto()) @map("_id") @db.ObjectId - userId String @map("user_id") @db.ObjectId - sessionId String @unique @map("session_id") - pdfUrl String @map("pdf_url") - pdfName String @map("pdf_name") - cloudinaryPublicId String? @map("cloudinary_public_id") - isActive Boolean @default(true) @map("is_active") - createdAt DateTime @default(now()) @map("created_at") + id String @id @default(auto()) @map("_id") @db.ObjectId + userId String @map("user_id") @db.ObjectId + sessionId String @unique @map("session_id") + pdfUrl String @map("pdf_url") + pdfName String @map("pdf_name") + cloudinaryPublicId String? @map("cloudinary_public_id") + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") endedAt DateTime? @map("ended_at") // Relations - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id]) messages PdfChatMessage[] @@map("pdf_chat_sessions") @@ -396,14 +394,14 @@ model PdfChatMessage { } model CourseEnrollment { - id String @id @default(auto()) @map("_id") @db.ObjectId - courseId String @map("course_id") @db.ObjectId - userId String @map("user_id") @db.ObjectId - enrolledAt DateTime @default(now()) @map("enrolled_at") - completedAt DateTime? @map("completed_at") - isCompleted Boolean @default(false) @map("is_completed") - progressPercentage Int @default(0) @map("progress_percentage") - lastAccessedAt DateTime? @map("last_accessed_at") + id String @id @default(auto()) @map("_id") @db.ObjectId + courseId String @map("course_id") @db.ObjectId + userId String @map("user_id") @db.ObjectId + enrolledAt DateTime @default(now()) @map("enrolled_at") + completedAt DateTime? @map("completed_at") + isCompleted Boolean @default(false) @map("is_completed") + progressPercentage Int @default(0) @map("progress_percentage") + lastAccessedAt DateTime? @map("last_accessed_at") // Relations course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) @@ -414,13 +412,13 @@ model CourseEnrollment { } model ChapterProgress { - id String @id @default(auto()) @map("_id") @db.ObjectId - chapterId String @map("chapter_id") @db.ObjectId - userId String @map("user_id") @db.ObjectId - isCompleted Boolean @default(false) @map("is_completed") + id String @id @default(auto()) @map("_id") @db.ObjectId + chapterId String @map("chapter_id") @db.ObjectId + userId String @map("user_id") @db.ObjectId + isCompleted Boolean @default(false) @map("is_completed") completedAt DateTime? @map("completed_at") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") // Relations chapter CourseChapter @relation(fields: [chapterId], references: [id], onDelete: Cascade) @@ -431,23 +429,23 @@ model ChapterProgress { } model CourseTest { - id String @id @default(auto()) @map("_id") @db.ObjectId - courseId String @map("course_id") @db.ObjectId - userId String @map("user_id") @db.ObjectId - questions String // JSON string containing the test questions - testInstructions String @map("test_instructions") // JSON string containing test instructions - answers String? // JSON string containing user answers - evaluationResults String? @map("evaluation_results") // JSON string containing evaluation results - timeLimit Int @map("time_limit") @default(180) // Time limit in minutes - passingScore Int @map("passing_score") @default(80) // Passing score percentage - totalMarks Int @map("total_marks") @default(100) - marksObtained Float? @map("marks_obtained") - score Int? // Score as percentage - hasPassed Boolean? @map("has_passed") - status String @default("in_progress") // 'in_progress', 'processing', 'completed', 'evaluation_failed', 'expired' - createdAt DateTime @default(now()) @map("created_at") - submittedAt DateTime? @map("submitted_at") - updatedAt DateTime @updatedAt @map("updated_at") + id String @id @default(auto()) @map("_id") @db.ObjectId + courseId String @map("course_id") @db.ObjectId + userId String @map("user_id") @db.ObjectId + questions String // JSON string containing the test questions + testInstructions String @map("test_instructions") // JSON string containing test instructions + answers String? // JSON string containing user answers + evaluationResults String? @map("evaluation_results") // JSON string containing evaluation results + timeLimit Int @default(180) @map("time_limit") // Time limit in minutes + passingScore Int @default(80) @map("passing_score") // Passing score percentage + totalMarks Int @default(100) @map("total_marks") + marksObtained Float? @map("marks_obtained") + score Int? // Score as percentage + hasPassed Boolean? @map("has_passed") + status String @default("in_progress") // 'in_progress', 'processing', 'completed', 'evaluation_failed', 'expired' + createdAt DateTime @default(now()) @map("created_at") + submittedAt DateTime? @map("submitted_at") + updatedAt DateTime @updatedAt @map("updated_at") // Relations course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) @@ -459,25 +457,25 @@ model CourseTest { model Vault { id String @id @default(auto()) @map("_id") @db.ObjectId - userId String @map("user_id") @db.ObjectId @unique + userId String @unique @map("user_id") @db.ObjectId storageUsed Int @default(0) @map("storage_used") storageLimit Int @default(524288000) @map("storage_limit") // 500 MB in bytes createdAt DateTime @default(now()) @map("created_at") // Relations - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) folders VaultFolder[] @@map("vaults") } model VaultFolder { - id String @id @default(auto()) @map("_id") @db.ObjectId - vaultId String @map("vault_id") @db.ObjectId - parentId String? @map("parent_id") @db.ObjectId - name String - isSystemFolder Boolean @default(false) @map("is_system_folder") - createdAt DateTime @default(now()) @map("created_at") + id String @id @default(auto()) @map("_id") @db.ObjectId + vaultId String @map("vault_id") @db.ObjectId + parentId String? @map("parent_id") @db.ObjectId + name String + isSystemFolder Boolean @default(false) @map("is_system_folder") + createdAt DateTime @default(now()) @map("created_at") // Relations vault Vault @relation(fields: [vaultId], references: [id], onDelete: Cascade) @@ -498,7 +496,7 @@ model VaultFile { createdAt DateTime @default(now()) @map("created_at") // Relations - folder VaultFolder @relation(fields: [folderId], references: [id], onDelete: Cascade) + folder VaultFolder @relation(fields: [folderId], references: [id], onDelete: Cascade) outputs VaultOutput[] @@map("vault_files") @@ -516,6 +514,7 @@ model VaultOutput { @@map("vault_outputs") } + model Lab { id String @id @default(auto()) @map("_id") @db.ObjectId title String @@ -567,4 +566,4 @@ model LabProgress { lastAccessed DateTime @default(now()) @@unique([labId, userId]) -} \ No newline at end of file +} diff --git a/server/routes/labs.js b/server/routes/labs.js index c5b3d13..eb788d7 100644 --- a/server/routes/labs.js +++ b/server/routes/labs.js @@ -1,15 +1,53 @@ import express from "express"; -import { PrismaClient } from "@prisma/client"; +import prisma from "../db.js"; +import { isValidObjectId } from "../utils/validator.js"; import { authenticateToken } from "../middleware/auth.js"; // existing JWT middleware const router = express.Router(); -const prisma = new PrismaClient(); + + +const ALLOWED_DIFFICULTIES = ["beginner", "intermediate", "advanced"]; +const ALLOWED_VISIBILITY = ["private", "public", "link"]; // --- CREATE Lab --- router.post("/", authenticateToken, async (req, res) => { try { - const { title, description, language, difficulty, content, starterCode, tasks, testCases, solution, visibility } = req.body; + const { + title, + description, + language, + difficulty, + content, + starterCode, + tasks, + testCases, + solution, + visibility, + } = req.body; + + //Required fields validation + if (!title || !language || !difficulty) { + return res.status(400).json({ + error: "title, language, and difficulty are required", + }); + } + + // Difficulty enum validation + if (!ALLOWED_DIFFICULTIES.includes(difficulty)) { + return res.status(400).json({ + error: "Invalid difficulty value", + allowedValues: ALLOWED_DIFFICULTIES, + }); + } + + // Visibility enum validation + if (visibility && !ALLOWED_VISIBILITY.includes(visibility)) { + return res.status(400).json({ + error: "Invalid visibility value", + allowedValues: ALLOWED_VISIBILITY, + }); + } const lab = await prisma.lab.create({ data: { title, @@ -22,7 +60,7 @@ router.post("/", authenticateToken, async (req, res) => { testCases, solution, visibility, - creatorId: req.user.id, // from JWT + creatorId: req.user.id, }, }); @@ -36,13 +74,22 @@ router.post("/", authenticateToken, async (req, res) => { // --- GET ALL Labs --- router.get("/", async (req, res) => { try { + const { language, difficulty, creator } = req.query; + + const where = {}; + if (language) where.language = language; + if (difficulty) where.difficulty = difficulty; + if (creator) where.creatorId = creator; + const labs = await prisma.lab.findMany({ - include: { - creator: true, - shares: true, - progress: true, - }, - }); + where, + include: { + creator: true, + shares: true, + progress: true, + }, +}); + res.json(labs); } catch (err) { console.error(err); @@ -54,6 +101,9 @@ router.get("/", async (req, res) => { router.get("/:id", async (req, res) => { try { const { id } = req.params; + if (!isValidObjectId(id)) { + return res.status(400).json({ error: "Invalid lab ID" }); + } const lab = await prisma.lab.findUnique({ where: { id }, include: { @@ -63,6 +113,7 @@ router.get("/:id", async (req, res) => { }, }); if (!lab) return res.status(404).json({ error: "Lab not found" }); + res.json(lab); } catch (err) { console.error(err); @@ -74,16 +125,50 @@ router.get("/:id", async (req, res) => { router.put("/:id", authenticateToken, async (req, res) => { try { const { id } = req.params; + if (!isValidObjectId(id)) { + return res.status(400).json({ error: "Invalid lab ID" }); + } const existing = await prisma.lab.findUnique({ where: { id } }); if (!existing) return res.status(404).json({ error: "Lab not found" }); if (existing.creatorId !== req.user.id) return res.status(403).json({ error: "Forbidden" }); - const lab = await prisma.lab.update({ - where: { id }, - data: req.body, - }); + const { + title, + description, + language, + difficulty, + content, + starterCode, + tasks, + testCases, + solution, + visibility, +} = req.body; +if (difficulty && !ALLOWED_DIFFICULTIES.includes(difficulty)) { + return res.status(400).json({ error: "Invalid difficulty value" }); +} + +if (visibility && !ALLOWED_VISIBILITY.includes(visibility)) { + return res.status(400).json({ error: "Invalid visibility value" }); +} + +const lab = await prisma.lab.update({ + where: { id }, + data: { + title, + description, + language, + difficulty, + content, + starterCode, + tasks, + testCases, + solution, + visibility, + }, +}); res.json(lab); } catch (err) { console.error(err); @@ -95,6 +180,9 @@ router.put("/:id", authenticateToken, async (req, res) => { router.delete("/:id", authenticateToken, async (req, res) => { try { const { id } = req.params; + if (!isValidObjectId(id)) { + return res.status(400).json({ error: "Invalid lab ID" }); + } const existing = await prisma.lab.findUnique({ where: { id } }); if (!existing) return res.status(404).json({ error: "Lab not found" }); diff --git a/server/utils/validator.js b/server/utils/validator.js new file mode 100644 index 0000000..3c73713 --- /dev/null +++ b/server/utils/validator.js @@ -0,0 +1,4 @@ +// Simple MongoDB ObjectId validator +export function isValidObjectId(id) { + return typeof id === "string" && /^[0-9a-fA-F]{24}$/.test(id); +} From b5b289714dec1acc97ecb9b37bf30342303b2a02 Mon Sep 17 00:00:00 2001 From: Priyamanjare54 <163539431+Priyamanjare54@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:26:47 +0530 Subject: [PATCH 3/3] fix: restore correct package versions --- server/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/package.json b/server/package.json index 0f13244..47df894 100644 --- a/server/package.json +++ b/server/package.json @@ -16,7 +16,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.864.0", "@prisma/client": "^5.22.0", - "@vercel/blob": "^0.0.2", + "@vercel/blob": "^1.1.1", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -34,7 +34,7 @@ "socket.io": "^4.8.1" }, "devDependencies": { - "@flydotio/dockerfile": "^0.7.4", + "@flydotio/dockerfile": "^0.7.10", "prisma": "^5.22.0" }, "keywords": [