From bbe5013987dd9279e056866a30971b9e04dc7a44 Mon Sep 17 00:00:00 2001 From: BruArtist <60303299@udst.edu.qa> Date: Sat, 11 Apr 2026 19:59:50 +0300 Subject: [PATCH] Added mongoDB authentication for register and login --- backend/models/User.js | 32 +++++++++ backend/routes/authRoutes.js | 136 +++++++++++++++++++++++++++++++++++ backend/server.js | 54 +++++++------- src/pages/LoginPage.jsx | 30 ++++++-- src/pages/RegisterPage.jsx | 30 ++++++-- src/services/api.js | 41 +++++++++-- 6 files changed, 283 insertions(+), 40 deletions(-) create mode 100644 backend/models/User.js create mode 100644 backend/routes/authRoutes.js diff --git a/backend/models/User.js b/backend/models/User.js new file mode 100644 index 0000000..867d708 --- /dev/null +++ b/backend/models/User.js @@ -0,0 +1,32 @@ +const mongoose = require("mongoose"); + +const userSchema = new mongoose.Schema( + { + fullName: { + type: String, + required: true, + trim: true, + minlength: 3, + }, + email: { + type: String, + required: true, + unique: true, + trim: true, + lowercase: true, + }, + passwordHash: { + type: String, + required: true, + }, + passwordSalt: { + type: String, + required: true, + }, + }, + { + timestamps: true, + } +); + +module.exports = mongoose.model("User", userSchema); \ No newline at end of file diff --git a/backend/routes/authRoutes.js b/backend/routes/authRoutes.js new file mode 100644 index 0000000..ce8df14 --- /dev/null +++ b/backend/routes/authRoutes.js @@ -0,0 +1,136 @@ +const express = require("express"); +const crypto = require("crypto"); +const User = require("../models/User"); + +const router = express.Router(); + +function hashPassword(password, salt) { + return crypto.scryptSync(password, salt, 64).toString("hex"); +} + +function sanitizeUser(user) { + return { + id: user._id, + fullName: user.fullName, + email: user.email, + }; +} + +// POST /api/auth/register +router.post("/register", async (req, res) => { + try { + const { fullName, email, password, confirmPassword } = req.body; + + if (!fullName || !email || !password || !confirmPassword) { + return res.status(400).json({ + error: "Validation Error", + details: "All fields are required.", + }); + } + + if (fullName.trim().length < 3) { + return res.status(400).json({ + error: "Validation Error", + details: "Full name must be at least 3 characters.", + }); + } + + const normalizedEmail = email.trim().toLowerCase(); + + if (!/\S+@\S+\.\S+/.test(normalizedEmail)) { + return res.status(400).json({ + error: "Validation Error", + details: "Please enter a valid email address.", + }); + } + + if (password.length < 6) { + return res.status(400).json({ + error: "Validation Error", + details: "Password must be at least 6 characters.", + }); + } + + if (password !== confirmPassword) { + return res.status(400).json({ + error: "Validation Error", + details: "Passwords do not match.", + }); + } + + const existingUser = await User.findOne({ email: normalizedEmail }); + + if (existingUser) { + return res.status(409).json({ + error: "Conflict", + details: "An account with this email already exists.", + }); + } + + const salt = crypto.randomBytes(16).toString("hex"); + const passwordHash = hashPassword(password, salt); + + const newUser = await User.create({ + fullName: fullName.trim(), + email: normalizedEmail, + passwordHash, + passwordSalt: salt, + }); + + return res.status(201).json({ + message: "Account created successfully.", + user: sanitizeUser(newUser), + }); + } catch (error) { + return res.status(500).json({ + error: "Internal Server Error", + details: error.message, + }); + } +}); + +// POST /api/auth/login +router.post("/login", async (req, res) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ + error: "Validation Error", + details: "Email and password are required.", + }); + } + + const normalizedEmail = email.trim().toLowerCase(); + + const user = await User.findOne({ email: normalizedEmail }); + + if (!user) { + return res.status(401).json({ + error: "Authentication Error", + details: "Invalid email or password.", + }); + } + + const attemptedHash = hashPassword(password, user.passwordSalt); + + if (attemptedHash !== user.passwordHash) { + return res.status(401).json({ + error: "Authentication Error", + details: "Invalid email or password.", + }); + } + + return res.status(200).json({ + message: "Login successful.", + user: sanitizeUser(user), + }); + } catch (error) { + return res.status(500).json({ + error: "Internal Server Error", + details: error.message, + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index a8844cc..b54935b 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,48 +1,50 @@ -require('dotenv').config({ path: require('path').resolve(__dirname, '../.env') }); -const path = require('path'); -const express = require('express'); -const cors = require('cors'); -const mongoose = require('mongoose'); -const quizRoutes = require('./routes/quizRoutes'); +require("dotenv").config({ path: require("path").resolve(__dirname, "../.env") }); +const path = require("path"); +const express = require("express"); +const cors = require("cors"); +const mongoose = require("mongoose"); +const quizRoutes = require("./routes/quizRoutes"); +const authRoutes = require("./routes/authRoutes"); const app = express(); const PORT = process.env.PORT || 5000; -// Connect to MongoDB -mongoose.connect(process.env.MONGODB_URI) - .then(() => console.log('Connected to MongoDB Atlas')) - .catch((err) => console.error('MongoDB connection error:', err)); +mongoose + .connect(process.env.MONGODB_URI) + .then(() => console.log("Connected to MongoDB Atlas")) + .catch((err) => console.error("MongoDB connection error:", err)); -// Middleware app.use(cors()); -app.use(express.json({ limit: '50mb' })); -app.use(express.urlencoded({ limit: '50mb', extended: true })); -app.use(express.text({ limit: '50mb' })); +app.use(express.json({ limit: "50mb" })); +app.use(express.urlencoded({ limit: "50mb", extended: true })); +app.use(express.text({ limit: "50mb" })); -// API routes -app.use('/api/quiz', quizRoutes); +app.get("/api/health", (req, res) => { + res.status(200).json({ message: "API is running." }); +}); + +app.use("/api/auth", authRoutes); +app.use("/api/quiz", quizRoutes); -// Only serve frontend build in production -if (process.env.NODE_ENV === 'production') { - const distPath = path.resolve(__dirname, '../dist'); +if (process.env.NODE_ENV === "production") { + const distPath = path.resolve(__dirname, "../dist"); app.use(express.static(distPath)); - app.get('*', (req, res) => { - res.sendFile(path.join(distPath, 'index.html')); + app.get("*", (req, res) => { + res.sendFile(path.join(distPath, "index.html")); }); } -// Error handler app.use((err, req, res, next) => { - if (err.status === 413 || err.message === 'request entity too large') { + if (err.status === 413 || err.message === "request entity too large") { return res.status(413).json({ - error: 'Payload Too Large', - details: 'File size exceeds the 50MB limit. Please upload a smaller document.', + error: "Payload Too Large", + details: "File size exceeds the 50MB limit. Please upload a smaller document.", }); } res.status(500).json({ - error: 'Internal Server Error', + error: "Internal Server Error", details: err.message, }); }); diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index 6c5d512..8d3eb42 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -1,14 +1,18 @@ import { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; +import { loginUser } from "../services/api"; function LoginPage() { const navigate = useNavigate(); + const [formData, setFormData] = useState({ email: "", password: "", }); const [errors, setErrors] = useState({}); + const [serverError, setServerError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); const validateForm = () => { const newErrors = {}; @@ -38,18 +42,34 @@ function LoginPage() { ...prev, [e.target.name]: "", })); + + setServerError(""); }; - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); const validationErrors = validateForm(); + if (Object.keys(validationErrors).length > 0) { setErrors(validationErrors); return; } - navigate("/landing"); + try { + setIsSubmitting(true); + setServerError(""); + + const data = await loginUser(formData); + + localStorage.setItem("quizeyUser", JSON.stringify(data.user)); + + navigate("/landing"); + } catch (error) { + setServerError(error.message || "Login failed."); + } finally { + setIsSubmitting(false); + } }; return ( @@ -82,8 +102,10 @@ function LoginPage() { /> {errors.password &&

{errors.password}

} - diff --git a/src/pages/RegisterPage.jsx b/src/pages/RegisterPage.jsx index 832f97b..1fed563 100644 --- a/src/pages/RegisterPage.jsx +++ b/src/pages/RegisterPage.jsx @@ -1,8 +1,10 @@ import { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; +import { registerUser } from "../services/api"; function RegisterPage() { const navigate = useNavigate(); + const [formData, setFormData] = useState({ fullName: "", email: "", @@ -11,6 +13,8 @@ function RegisterPage() { }); const [errors, setErrors] = useState({}); + const [serverError, setServerError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); const validateForm = () => { const newErrors = {}; @@ -52,18 +56,34 @@ function RegisterPage() { ...prev, [e.target.name]: "", })); + + setServerError(""); }; - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); const validationErrors = validateForm(); + if (Object.keys(validationErrors).length > 0) { setErrors(validationErrors); return; } - navigate("/landing"); + try { + setIsSubmitting(true); + setServerError(""); + + const data = await registerUser(formData); + + localStorage.setItem("quizeyUser", JSON.stringify(data.user)); + + navigate("/landing"); + } catch (error) { + setServerError(error.message || "Registration failed."); + } finally { + setIsSubmitting(false); + } }; return ( @@ -118,8 +138,10 @@ function RegisterPage() {

{errors.confirmPassword}

)} - diff --git a/src/services/api.js b/src/services/api.js index 56cf559..f8288f4 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,5 +1,39 @@ const API_BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:5000"; +async function handleResponse(response, fallbackMessage) { + const data = await response.json().catch(() => ({})); + + if (!response.ok) { + throw new Error(data.details || fallbackMessage); + } + + return data; +} + +export async function registerUser(payload) { + const response = await fetch(`${API_BASE_URL}/api/auth/register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + return handleResponse(response, "Failed to register user."); +} + +export async function loginUser(payload) { + const response = await fetch(`${API_BASE_URL}/api/auth/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + return handleResponse(response, "Failed to login."); +} + export async function generateQuiz(payload) { const response = await fetch(`${API_BASE_URL}/api/quiz/generate`, { method: "POST", @@ -9,10 +43,5 @@ export async function generateQuiz(payload) { body: JSON.stringify(payload), }); - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.details || "Failed to generate quiz."); - } - - return response.json(); + return handleResponse(response, "Failed to generate quiz."); } \ No newline at end of file