From ccc2dae493ec47ada4407935efeade6709677fa0 Mon Sep 17 00:00:00 2001 From: BruArtist <60303299@udst.edu.qa> Date: Sat, 11 Apr 2026 20:25:21 +0300 Subject: [PATCH 1/2] Add logout button to landing page --- src/pages/LandingPage.jsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index fbc2b53..e798f06 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -3,6 +3,11 @@ import { useNavigate } from "react-router-dom"; function LandingPage() { const navigate = useNavigate(); + const handleLogout = () => { + localStorage.removeItem("quizeyUser"); + navigate("/"); + }; + return (
@@ -15,7 +20,13 @@ function LandingPage() { AI Chatbot -
Profile
+
+ Logout +
@@ -34,7 +45,10 @@ function LandingPage() {

- @@ -45,7 +59,9 @@ function LandingPage() {
AI Learning Interface
-
98% success boosting quiz mastery
+
+ 98% success boosting quiz mastery +
From a0ddce1149e66dfd7c03f1846066c3448f33fb41 Mon Sep 17 00:00:00 2001 From: BruArtist <60303299@udst.edu.qa> Date: Sat, 11 Apr 2026 21:11:54 +0300 Subject: [PATCH 2/2] Fixed the dashboard and useless buttons that aint workind and fixed the hashing and the salt to make it more secure for our security implentation --- backend/models/Result.js | 11 +- backend/models/User.js | 6 +- backend/routes/authRoutes.js | 38 ++++-- backend/routes/quizRoutes.js | 116 +++++++++++------ src/pages/DashboardPage.jsx | 226 +++++++++++++++++++++++++++++---- src/pages/FeedbackPage.jsx | 21 ++- src/pages/GenerateQuizPage.jsx | 31 ++--- src/pages/LandingPage.jsx | 18 ++- src/pages/QuizPage.jsx | 60 ++++++--- src/services/api.js | 44 ++++++- 10 files changed, 445 insertions(+), 126 deletions(-) diff --git a/backend/models/Result.js b/backend/models/Result.js index 335e321..b583076 100644 --- a/backend/models/Result.js +++ b/backend/models/Result.js @@ -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: { @@ -31,4 +36,4 @@ const resultSchema = new mongoose.Schema( { timestamps: true } ); -module.exports = mongoose.model('Result', resultSchema); \ No newline at end of file +module.exports = mongoose.model("Result", resultSchema); \ No newline at end of file diff --git a/backend/models/User.js b/backend/models/User.js index 867d708..73e1fc5 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -15,11 +15,7 @@ const userSchema = new mongoose.Schema( trim: true, lowercase: true, }, - passwordHash: { - type: String, - required: true, - }, - passwordSalt: { + password: { type: String, required: true, }, diff --git a/backend/routes/authRoutes.js b/backend/routes/authRoutes.js index ce8df14..9417415 100644 --- a/backend/routes/authRoutes.js +++ b/backend/routes/authRoutes.js @@ -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, @@ -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; @@ -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({ @@ -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; @@ -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.", diff --git a/backend/routes/quizRoutes.js b/backend/routes/quizRoutes.js index 270923a..a613fa8 100644 --- a/backend/routes/quizRoutes.js +++ b/backend/routes/quizRoutes.js @@ -1,70 +1,97 @@ -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, @@ -72,8 +99,8 @@ router.post('/submit', async (req, res) => { wrongAnswers: scoreResult.wrongAnswers, }; - // Save result to MongoDB const result = await Result.create({ + userId, quizId: quizId || null, ...normalizedResult, answers, @@ -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, @@ -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; \ No newline at end of file diff --git a/src/pages/DashboardPage.jsx b/src/pages/DashboardPage.jsx index bd7ed36..a9aa61e 100644 --- a/src/pages/DashboardPage.jsx +++ b/src/pages/DashboardPage.jsx @@ -1,8 +1,18 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; +import { + clearStoredUser, + fetchDashboardData, + getStoredUser, + getUserInitials, +} from "../services/api"; function DashboardPage() { const navigate = useNavigate(); + const currentUser = useMemo(() => getStoredUser(), []); + const firstName = currentUser?.fullName?.trim()?.split(" ")[0] || "Learner"; + const settingsRef = useRef(null); + const [dashboardData, setDashboardData] = useState({ completed: 0, avgScore: 0, @@ -10,22 +20,60 @@ function DashboardPage() { chartData: [], latestQuiz: null, }); + const [loading, setLoading] = useState(true); const [error, setError] = useState(""); + const [settings, setSettings] = useState(() => { + const saved = localStorage.getItem("quizeySettings"); + if (saved) { + try { + return JSON.parse(saved); + } catch { + return { + preferredDifficulty: "medium", + preferredQuestions: 10, + studyGoal: 5, + emailTips: true, + }; + } + } + + return { + preferredDifficulty: "medium", + preferredQuestions: 10, + studyGoal: 5, + emailTips: true, + }; + }); + + const handleLogout = () => { + clearStoredUser(); + navigate("/"); + }; + + const handleSettingChange = (field, value) => { + const updated = { ...settings, [field]: value }; + setSettings(updated); + localStorage.setItem("quizeySettings", JSON.stringify(updated)); + }; + + const scrollToSettings = () => { + settingsRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); + }; + useEffect(() => { - const fetchDashboardData = async () => { + if (!currentUser?.id) { + navigate("/"); + return; + } + + const loadDashboard = async () => { try { setLoading(true); setError(""); - const response = await fetch("http://localhost:5000/api/quiz/dashboard"); - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.error || "Failed to fetch dashboard data."); - } - + const data = await fetchDashboardData(currentUser.id); setDashboardData(data); } catch (err) { setError(err.message || "Failed to load dashboard."); @@ -34,16 +82,22 @@ function DashboardPage() { } }; - fetchDashboardData(); - }, []); + loadDashboard(); + }, [currentUser, navigate]); - const { completed, avgScore, recentQuizzes, chartData, latestQuiz } = dashboardData; + const { completed, avgScore, recentQuizzes, chartData, latestQuiz } = + dashboardData; const safeChartData = [...chartData]; while (safeChartData.length < 7) { safeChartData.unshift({ percentage: 0 }); } + const highestPercentage = Math.max( + ...safeChartData.map((x) => x.percentage), + 0 + ); + if (loading) { return (
@@ -80,11 +134,15 @@ function DashboardPage() { Generate - - + - @@ -92,14 +150,22 @@ function DashboardPage() {
-

Welcome back, Alex.

+

Welcome back, {firstName}.

{completed > 0 - ? `You have completed ${completed} quiz${completed > 1 ? "zes" : ""} so far.` + ? `You have completed ${completed} quiz${ + completed > 1 ? "zes" : "" + } so far.` : "Start your first quiz to see your progress here."}

-
Profile
+
+ Logout +
@@ -114,10 +180,13 @@ function DashboardPage() {

{latestQuiz ? `You scored ${latestQuiz.score}/${latestQuiz.total} on your latest quiz.` - : "Generate and complete a quiz to populate your dashboard with real data."} + : "Generate and complete a quiz to populate your dashboard with your own data."}

-
@@ -128,9 +197,11 @@ function DashboardPage() {
-
A
-

Alex Thorne

-

Expert Polymath • Level 42

+
+ {getUserInitials(currentUser?.fullName)} +
+

{currentUser?.fullName || "Learner"}

+

{currentUser?.email || "No email available"}

@@ -146,15 +217,14 @@ function DashboardPage() {

Score History

-

Performance over recent attempts

+

Performance over your recent attempts

{safeChartData.map((item, index) => ( x.percentage)) && - item.percentage > 0 + item.percentage === highestPercentage && item.percentage > 0 ? "active-bar" : "" } @@ -176,7 +246,7 @@ function DashboardPage() {
No quizzes yet -

Complete a quiz to see history here.

+

Complete a quiz to see your history here.

0/0
@@ -194,6 +264,108 @@ function DashboardPage() { )) )}
+ +
+

Quick Settings

+

+ Personalize your learning flow. These settings are saved on your + device and used as your default study preferences. +

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
Saved Preferences
+

+ Difficulty: {settings.preferredDifficulty} | Questions:{" "} + {settings.preferredQuestions} | Goal:{" "} + {settings.studyGoal} quizzes/week | Tips:{" "} + {settings.emailTips ? "On" : "Off"} +

+
+