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
32 changes: 32 additions & 0 deletions backend/models/User.js
Original file line number Diff line number Diff line change
@@ -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);
136 changes: 136 additions & 0 deletions backend/routes/authRoutes.js
Original file line number Diff line number Diff line change
@@ -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;
54 changes: 28 additions & 26 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -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,
});
});
Expand Down
30 changes: 26 additions & 4 deletions src/pages/LoginPage.jsx
Original file line number Diff line number Diff line change
@@ -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 = {};
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -82,8 +102,10 @@ function LoginPage() {
/>
{errors.password && <p className="field-error">{errors.password}</p>}

<button type="submit" className="primary-btn full-width">
Login
{serverError && <p className="field-error">{serverError}</p>}

<button type="submit" className="primary-btn full-width" disabled={isSubmitting}>
{isSubmitting ? "Logging In..." : "Login"}
</button>
</form>

Expand Down
30 changes: 26 additions & 4 deletions src/pages/RegisterPage.jsx
Original file line number Diff line number Diff line change
@@ -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: "",
Expand All @@ -11,6 +13,8 @@ function RegisterPage() {
});

const [errors, setErrors] = useState({});
const [serverError, setServerError] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);

const validateForm = () => {
const newErrors = {};
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -118,8 +138,10 @@ function RegisterPage() {
<p className="field-error">{errors.confirmPassword}</p>
)}

<button type="submit" className="primary-btn full-width">
Register
{serverError && <p className="field-error">{serverError}</p>}

<button type="submit" className="primary-btn full-width" disabled={isSubmitting}>
{isSubmitting ? "Creating Account..." : "Register"}
</button>
</form>

Expand Down
Loading
Loading