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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions backend/models/Result.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
const mongoose = require('mongoose');
const mongoose = require("mongoose");

const resultSchema = new mongoose.Schema(
{
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
quizId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Quiz',
ref: "Quiz",
default: null,
},
score: {
Expand All @@ -31,4 +36,4 @@ const resultSchema = new mongoose.Schema(
{ timestamps: true }
);

module.exports = mongoose.model('Result', resultSchema);
module.exports = mongoose.model("Result", resultSchema);
6 changes: 1 addition & 5 deletions backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ const userSchema = new mongoose.Schema(
trim: true,
lowercase: true,
},
passwordHash: {
type: String,
required: true,
},
passwordSalt: {
password: {
type: String,
required: true,
},
Expand Down
38 changes: 30 additions & 8 deletions backend/routes/authRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@ function hashPassword(password, salt) {
return crypto.scryptSync(password, salt, 64).toString("hex");
}

function createStoredPassword(password) {
const salt = crypto.randomBytes(16).toString("hex");
const hash = hashPassword(password, salt);
return `${salt}:${hash}`;
}

function verifyStoredPassword(password, storedPassword) {
if (!storedPassword || typeof storedPassword !== "string") {
return false;
}

const parts = storedPassword.split(":");

if (parts.length !== 2) {
return false;
}

const [salt, originalHash] = parts;
const attemptHash = hashPassword(password, salt);

return crypto.timingSafeEqual(
Buffer.from(originalHash, "hex"),
Buffer.from(attemptHash, "hex")
);
}

function sanitizeUser(user) {
return {
id: user._id,
Expand All @@ -16,7 +42,6 @@ function sanitizeUser(user) {
};
}

// POST /api/auth/register
router.post("/register", async (req, res) => {
try {
const { fullName, email, password, confirmPassword } = req.body;
Expand Down Expand Up @@ -67,14 +92,12 @@ router.post("/register", async (req, res) => {
});
}

const salt = crypto.randomBytes(16).toString("hex");
const passwordHash = hashPassword(password, salt);
const storedPassword = createStoredPassword(password);

const newUser = await User.create({
fullName: fullName.trim(),
email: normalizedEmail,
passwordHash,
passwordSalt: salt,
password: storedPassword,
});

return res.status(201).json({
Expand All @@ -89,7 +112,6 @@ router.post("/register", async (req, res) => {
}
});

// POST /api/auth/login
router.post("/login", async (req, res) => {
try {
const { email, password } = req.body;
Expand All @@ -112,9 +134,9 @@ router.post("/login", async (req, res) => {
});
}

const attemptedHash = hashPassword(password, user.passwordSalt);
const isValidPassword = verifyStoredPassword(password, user.password);

if (attemptedHash !== user.passwordHash) {
if (!isValidPassword) {
return res.status(401).json({
error: "Authentication Error",
details: "Invalid email or password.",
Expand Down
116 changes: 80 additions & 36 deletions backend/routes/quizRoutes.js
Original file line number Diff line number Diff line change
@@ -1,79 +1,106 @@
const express = require('express');
const { generateQuiz } = require('../services/geminiService');
const { calculateScore } = require('../services/scoreService');
const Quiz = require('../models/Quiz');
const Result = require('../models/Result');
const express = require("express");
const { generateQuiz } = require("../services/geminiService");
const { calculateScore } = require("../services/scoreService");
const Quiz = require("../models/Quiz");
const Result = require("../models/Result");

const router = express.Router();

router.post('/generate', async (req, res) => {
router.post("/generate", async (req, res) => {
try {
const { topic, text } = req.body;

if (!topic && !text) {
return res.status(400).json({ error: 'Bad Request: Either "topic" or "text" must be provided' });
return res
.status(400)
.json({ error: 'Bad Request: Either "topic" or "text" must be provided' });
}
if (topic && typeof topic !== 'string') {

if (topic && typeof topic !== "string") {
return res.status(400).json({ error: 'Bad Request: "topic" must be a string' });
}
if (text && typeof text !== 'string') {

if (text && typeof text !== "string") {
return res.status(400).json({ error: 'Bad Request: "text" must be a string' });
}

const trimmedTopic = topic ? topic.trim() : null;
const trimmedText = text ? text.trim() : null;

if ((!trimmedTopic || trimmedTopic.length === 0) && (!trimmedText || trimmedText.length === 0)) {
return res.status(400).json({ error: 'Bad Request: "topic" or "text" cannot be empty' });
if (
(!trimmedTopic || trimmedTopic.length === 0) &&
(!trimmedText || trimmedText.length === 0)
) {
return res
.status(400)
.json({ error: 'Bad Request: "topic" or "text" cannot be empty' });
}

const questions = await generateQuiz({ topic: trimmedTopic, text: trimmedText });

// Save quiz to MongoDB
const quiz = await Quiz.create({
topic: trimmedTopic,
sourceText: trimmedText,
questions,
});

return res.status(200).json({ quizId: quiz._id, questions: quiz.questions });
return res.status(200).json({
quizId: quiz._id,
questions: quiz.questions,
});
} catch (error) {
return res.status(500).json({ error: 'Failed to generate quiz', details: error.message });
return res.status(500).json({
error: "Failed to generate quiz",
details: error.message,
});
}
});

router.post('/submit', async (req, res) => {
router.post("/submit", async (req, res) => {
try {
const { quizId, questions, answers } = req.body;
const { userId, quizId, questions, answers } = req.body;

if (!userId) {
return res.status(400).json({
error: 'Bad Request: "userId" is required',
});
}

if (!questions || !Array.isArray(questions)) {
return res.status(400).json({ error: 'Bad Request: "questions" must be an array' });
return res.status(400).json({
error: 'Bad Request: "questions" must be an array',
});
}

if (!answers || !Array.isArray(answers)) {
return res.status(400).json({ error: 'Bad Request: "answers" must be an array' });
return res.status(400).json({
error: 'Bad Request: "answers" must be an array',
});
}

if (questions.length === 0) {
return res.status(400).json({ error: 'Bad Request: "questions" cannot be empty' });
return res.status(400).json({
error: 'Bad Request: "questions" cannot be empty',
});
}

if (questions.length !== answers.length) {
return res.status(400).json({ error: 'Mismatch between questions and answers length' });
return res.status(400).json({
error: "Mismatch between questions and answers length",
});
}

const scoreResult = calculateScore(questions, answers);

// Ensure all required fields exist
const normalizedResult = {
score: scoreResult.score,
total: scoreResult.total,
correctAnswers: scoreResult.correctAnswers,
wrongAnswers: scoreResult.wrongAnswers,
};

// Save result to MongoDB
const result = await Result.create({
userId,
quizId: quizId || null,
...normalizedResult,
answers,
Expand All @@ -83,47 +110,64 @@ router.post('/submit', async (req, res) => {
resultId: result._id,
...normalizedResult,
});

} catch (error) {
return res.status(500).json({
error: 'Failed to calculate score',
error: "Failed to calculate score",
details: error.message,
});
}
});

router.get('/results', async (req, res) => {
router.get("/results", async (req, res) => {
try {
const results = await Result.find()
.populate('quizId')
const { userId } = req.query;

if (!userId) {
return res.status(400).json({
error: 'Bad Request: "userId" is required',
});
}

const results = await Result.find({ userId })
.populate("quizId")
.sort({ createdAt: -1 });

return res.status(200).json(results);
} catch (error) {
return res.status(500).json({
error: 'Failed to fetch results',
error: "Failed to fetch results",
details: error.message,
});
}
});

router.get('/dashboard', async (req, res) => {
router.get("/dashboard", async (req, res) => {
try {
const results = await Result.find()
.populate('quizId')
const { userId } = req.query;

if (!userId) {
return res.status(400).json({
error: 'Bad Request: "userId" is required',
});
}

const results = await Result.find({ userId })
.populate("quizId")
.sort({ createdAt: -1 });

const completed = results.length;

const avgScore = completed
? Math.round(
results.reduce((sum, item) => sum + (item.total ? (item.score / item.total) * 100 : 0), 0) / completed
results.reduce((sum, item) => {
return sum + (item.total ? (item.score / item.total) * 100 : 0);
}, 0) / completed
)
: 0;

const recentQuizzes = results.slice(0, 5).map((item) => ({
id: item._id,
topic: item.quizId?.topic || 'Untitled Quiz',
topic: item.quizId?.topic || "Untitled Quiz",
score: item.score,
total: item.total,
percentage: item.total ? Math.round((item.score / item.total) * 100) : 0,
Expand All @@ -147,14 +191,14 @@ router.get('/dashboard', async (req, res) => {
});
} catch (error) {
return res.status(500).json({
error: 'Failed to fetch dashboard data',
error: "Failed to fetch dashboard data",
details: error.message,
});
}
});

router.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
router.get("/health", (req, res) => {
res.status(200).json({ status: "ok" });
});

module.exports = router;
Loading
Loading