├── frontend
├── src
│ ├── index.css
│ ├── utils
│ │ └── date.js
│ ├── main.jsx
│ ├── components
│ │ ├── Input.jsx
│ │ ├── FloatingShape.jsx
│ │ ├── LoadingSpinner.jsx
│ │ └── PasswordStrengthMeter.jsx
│ ├── pages
│ │ ├── LoginPage.jsx
│ │ ├── DashboardPage.jsx
│ │ ├── ResetPasswordPage.jsx
│ │ ├── ForgotPasswordPage.jsx
│ │ ├── SignUpPage.jsx
│ │ └── EmailVerificationPage.jsx
│ ├── store
│ │ └── authStore.js
│ └── App.jsx
├── postcss.config.js
├── vite.config.js
├── tailwind.config.js
├── .gitignore
├── index.html
├── .eslintrc.cjs
├── README.md
├── package.json
└── public
│ └── vite.svg
├── .env.example
├── backend
├── db
│ └── connectDB.js
├── mailtrap
│ ├── mailtrap.config.js
│ ├── emails.js
│ └── emailTemplates.js
├── utils
│ └── generateTokenAndSetCookie.js
├── routes
│ └── auth.route.js
├── middleware
│ └── verifyToken.js
├── models
│ └── user.model.js
├── index.js
└── controllers
│ └── auth.controller.js
├── .gitignore
├── package.json
└── README.md
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
--------------------------------------------------------------------------------
/frontend/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vite.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | MONGO_URI=your_mongo_uri
2 | PORT=5000
3 | JWT_SECRET=your_jwt_secret
4 | NODE_ENV=development
5 | MAILTRAP_TOKEN=your_mailtrap_token
6 | MAILTRAP_ENDPOINT=your_mailtrap_endpoint
7 |
8 | CLIENT_URL=http://localhost:5173
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/frontend/src/utils/date.js:
--------------------------------------------------------------------------------
1 | export const formatDate = (dateString) => {
2 | const date = new Date(dateString);
3 | if (isNaN(date.getTime())) {
4 | return "Invalid Date";
5 | }
6 |
7 | return date.toLocaleString("en-US", {
8 | year: "numeric",
9 | month: "short",
10 | day: "numeric",
11 | hour: "2-digit",
12 | minute: "2-digit",
13 | hour12: true,
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/frontend/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App.jsx";
4 | import "./index.css";
5 | import { BrowserRouter } from "react-router-dom";
6 |
7 | ReactDOM.createRoot(document.getElementById("root")).render(
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/backend/db/connectDB.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | export const connectDB = async () => {
4 | try {
5 | const conn = await mongoose.connect(process.env.MONGO_URI);
6 | console.log(`MongoDB Connected: ${conn.connection.host}`);
7 | } catch (error) {
8 | console.log("Error connection to MongoDB: ", error.message);
9 | process.exit(1); // 1 is failure, 0 status code is success
10 | }
11 | };
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/backend/mailtrap/mailtrap.config.js:
--------------------------------------------------------------------------------
1 | import { MailtrapClient } from "mailtrap";
2 | import dotenv from "dotenv";
3 | dotenv.config();
4 |
5 | const TOKEN = process.env.MAILTRAP_TOKEN;
6 | const ENDPOINT = process.env.MAILTRAP_ENDPOINT;
7 |
8 | export const mailtrapClient = new MailtrapClient({
9 | endpoint: ENDPOINT,
10 | token: TOKEN,
11 | });
12 |
13 | export const sender = {
14 | email: "hello@demomailtrap.co",
15 | name: "MERN_ADVANCED_AUTH",
16 | };
17 |
--------------------------------------------------------------------------------
/backend/utils/generateTokenAndSetCookie.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 |
3 |
4 | export const generateTokenAndSetCookie = (res, userId) => {
5 | const token = jwt.sign({ userId}, process.env.JWT_SECRET, {
6 | expiresIn: "7d",
7 | });
8 |
9 | res.cookie("token", token, {
10 | httpOnly: true,
11 | secure: process.env.NODE_ENV === "production",
12 | sameSite: "strict",
13 | maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
14 | });
15 |
16 | return token;
17 |
18 | }
--------------------------------------------------------------------------------
/frontend/src/components/Input.jsx:
--------------------------------------------------------------------------------
1 | const Input = ({ icon: Icon, ...props }) => {
2 | return (
3 |
12 | );
13 | };
14 | export default Input;
15 |
--------------------------------------------------------------------------------
/frontend/src/components/FloatingShape.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 |
3 | const FloatingShape = ({ color, size, top, left, delay }) => {
4 | return (
5 |
21 | );
22 | };
23 | export default FloatingShape;
24 |
--------------------------------------------------------------------------------
/frontend/src/components/LoadingSpinner.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 |
3 | const LoadingSpinner = () => {
4 | return (
5 |
6 | {/* Simple Loading Spinner */}
7 |
12 |
13 | );
14 | };
15 |
16 | export default LoadingSpinner;
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node.js dependencies
2 | /node_modules
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 |
11 | # Environment variables
12 | .env
13 | .env.local
14 | .env.*.local
15 |
16 | # Build output
17 | /build
18 | /dist
19 | /public/build
20 |
21 | # Cache
22 | .cache
23 | .next
24 | out
25 | temp
26 |
27 | # Editor directories and files
28 | .idea/
29 | .vscode/
30 | *.swp
31 | *.swo
32 |
33 | # OS generated files
34 | .DS_Store
35 | Thumbs.db
36 |
37 | # Coverage directory
38 | coverage
39 |
40 | # Debug files
41 | *.pid
42 | *.seed
43 | *.pid.lock
44 |
45 | # Miscellaneous
46 | *.tgz
47 | *.lock-wscript
--------------------------------------------------------------------------------
/backend/routes/auth.route.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import { login, logout, signup, verifyEmail, forgotPassword, resetPassword, checkAuth } from '../controllers/auth.controller.js';
3 | import { verifyToken } from '../middleware/verifyToken.js';
4 |
5 | const router = express.Router();
6 |
7 | router.get("/check-auth", verifyToken, checkAuth);
8 |
9 | router.post("/signup", signup);
10 | router.post("/login", login);
11 | router.post("/logout", logout);
12 |
13 | router.post("/verify-email", verifyEmail);
14 | router.post("/forgot-password", forgotPassword);
15 |
16 | router.post("/reset-password/:token", resetPassword);
17 |
18 | export default router;
--------------------------------------------------------------------------------
/backend/middleware/verifyToken.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 |
3 | export const verifyToken = (req, res, next) => {
4 | const token = req.cookies.token;
5 | if (!token) return res.status(401).json({ success: false, message: "Unauthorized - no token provided" });
6 | try {
7 | const decoded = jwt.verify(token, process.env.JWT_SECRET);
8 |
9 | if (!decoded) return res.status(401).json({ success: false, message: "Unauthorized - invalid token" });
10 |
11 | req.userId = decoded.userId;
12 | next();
13 | } catch (error) {
14 | console.log("Error in verifyToken ", error);
15 | return res.status(500).json({ success: false, message: "Server error" });
16 | }
17 | };
--------------------------------------------------------------------------------
/frontend/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | "eslint:recommended",
6 | "plugin:react/recommended",
7 | "plugin:react/jsx-runtime",
8 | "plugin:react-hooks/recommended",
9 | ],
10 | ignorePatterns: ["dist", ".eslintrc.cjs"],
11 | parserOptions: { ecmaVersion: "latest", sourceType: "module" },
12 | settings: { react: { version: "18.2" } },
13 | plugins: ["react-refresh"],
14 | rules: {
15 | "react/prop-types": "off",
16 | "react/no-unescaped-entities": "off",
17 | "react/jsx-no-target-blank": "off",
18 | "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mern-advanced-auth",
3 | "version": "1.0.0",
4 | "main": "backend/index.js",
5 | "scripts": {
6 | "dev": "nodemon backend/index.js",
7 | "start": "node backend/index.js"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "type": "module",
12 | "license": "ISC",
13 | "description": "",
14 | "dependencies": {
15 | "bcryptjs": "^3.0.2",
16 | "cookie-parser": "^1.4.7",
17 | "cors": "^2.8.5",
18 | "crypto": "^1.0.1",
19 | "dotenv": "^16.5.0",
20 | "express": "^5.1.0",
21 | "jsonwebtoken": "^9.0.2",
22 | "mailtrap": "^4.1.0",
23 | "mongoose": "^8.14.1"
24 | },
25 | "devDependencies": {
26 | "nodemon": "^3.1.10"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/backend/models/user.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const userSchema = new mongoose.Schema({
4 | email: {
5 | type: String,
6 | required: true,
7 | unique: true,
8 | },
9 | password: {
10 | type: String,
11 | required: true,
12 | },
13 | name: {
14 | type: String,
15 | required: true,
16 | },
17 | lastLogin: {
18 | type: Date,
19 | default: Date.now,
20 | },
21 | isVerified: {
22 | type: Boolean,
23 | default: false,
24 | },
25 | resetPasswordToken: String,
26 | resetPasswordExpiresAt: Date,
27 | verificationToken: String,
28 | verificationTokenExpiresAt: Date,
29 |
30 | }, {timestamps: true,});
31 |
32 |
33 | export const User = mongoose.model("User", userSchema);
--------------------------------------------------------------------------------
/backend/index.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import dotenv from 'dotenv';
3 | import cors from 'cors';
4 | import cookieParser from 'cookie-parser';
5 | import { connectDB } from './db/connectDB.js';
6 | import authRoutes from './routes/auth.route.js';
7 |
8 | dotenv.config();
9 |
10 | const app = express();
11 | const PORT = process.env.PORT || 3000;
12 |
13 |
14 | app.use(cors({ origin: "http://localhost:5173", credentials: true }));
15 |
16 | app.use(express.json()); // allows us to parse incoming requests:req.body
17 | app.use(cookieParser()); // enables cookie parsing for incoming requests
18 |
19 | app.use("/api/auth", authRoutes);
20 |
21 | app.listen(PORT, () => {
22 | connectDB();
23 | console.log(`Server is running on port ${PORT}`);
24 | });
25 |
26 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
13 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "axios": "^1.7.3",
14 | "framer-motion": "^11.3.21",
15 | "lucide-react": "^0.424.0",
16 | "react": "^18.3.1",
17 | "react-dom": "^18.3.1",
18 | "react-hot-toast": "^2.4.1",
19 | "react-router-dom": "^6.26.0",
20 | "zustand": "^4.5.4"
21 | },
22 | "devDependencies": {
23 | "@types/react": "^18.3.3",
24 | "@types/react-dom": "^18.3.0",
25 | "@vitejs/plugin-react": "^4.3.1",
26 | "autoprefixer": "^10.4.20",
27 | "eslint": "^8.57.0",
28 | "eslint-plugin-react": "^7.34.3",
29 | "eslint-plugin-react-hooks": "^4.6.2",
30 | "eslint-plugin-react-refresh": "^0.4.7",
31 | "postcss": "^8.4.40",
32 | "tailwindcss": "^3.4.17",
33 | "vite": "^5.3.4"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/frontend/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/PasswordStrengthMeter.jsx:
--------------------------------------------------------------------------------
1 | import { Check, X } from "lucide-react";
2 |
3 | const PasswordCriteria = ({ password }) => {
4 | const criteria = [
5 | { label: "At least 6 characters", met: password.length >= 6 },
6 | { label: "Contains uppercase letter", met: /[A-Z]/.test(password) },
7 | { label: "Contains lowercase letter", met: /[a-z]/.test(password) },
8 | { label: "Contains a number", met: /\d/.test(password) },
9 | { label: "Contains special character", met: /[^A-Za-z0-9]/.test(password) },
10 | ];
11 |
12 | return (
13 |
14 | {criteria.map((item) => (
15 |
16 | {item.met ? (
17 |
18 | ) : (
19 |
20 | )}
21 | {item.label}
22 |
23 | ))}
24 |
25 | );
26 | };
27 |
28 | const PasswordStrengthMeter = ({ password }) => {
29 | const getStrength = (pass) => {
30 | let strength = 0;
31 | if (pass.length >= 6) strength++;
32 | if (pass.match(/[a-z]/) && pass.match(/[A-Z]/)) strength++;
33 | if (pass.match(/\d/)) strength++;
34 | if (pass.match(/[^a-zA-Z\d]/)) strength++;
35 | return strength;
36 | };
37 | const strength = getStrength(password);
38 |
39 | const getColor = (strength) => {
40 | if (strength === 0) return "bg-red-500";
41 | if (strength === 1) return "bg-red-400";
42 | if (strength === 2) return "bg-yellow-500";
43 | if (strength === 3) return "bg-yellow-400";
44 | return "bg-green-500";
45 | };
46 |
47 | const getStrengthText = (strength) => {
48 | if (strength === 0) return "Very Weak";
49 | if (strength === 1) return "Weak";
50 | if (strength === 2) return "Fair";
51 | if (strength === 3) return "Good";
52 | return "Strong";
53 | };
54 |
55 | return (
56 |
57 |
58 | Password strength
59 | {getStrengthText(strength)}
60 |
61 |
62 |
63 | {[...Array(4)].map((_, index) => (
64 |
70 | ))}
71 |
72 |
73 |
74 | );
75 | };
76 | export default PasswordStrengthMeter;
77 |
--------------------------------------------------------------------------------
/backend/mailtrap/emails.js:
--------------------------------------------------------------------------------
1 | import {
2 | VERIFICATION_EMAIL_TEMPLATE,
3 | PASSWORD_RESET_REQUEST_TEMPLATE,
4 | PASSWORD_RESET_SUCCESS_TEMPLATE,
5 | } from "./emailTemplates.js";
6 | import { mailtrapClient, sender } from "./mailtrap.config.js";
7 |
8 | export const sendVerificationEmail = async (email, verificationToken) => {
9 | const recipient = [{ email }];
10 |
11 | try {
12 | const response = await mailtrapClient.send({
13 | from: sender,
14 | to: recipient,
15 | subject: "Verify your email",
16 | html: VERIFICATION_EMAIL_TEMPLATE.replace("{verificationCode}", verificationToken),
17 | category: "Email Verification",
18 | });
19 |
20 | console.log("Email sent successfully", response);
21 | } catch (error) {
22 | console.error(`Error sending verification`, error);
23 |
24 | throw new Error(`Error sending verification email: ${error}`);
25 | }
26 | };
27 |
28 |
29 |
30 | export const sendWelcomeEmail = async (email, name) => {
31 | const recipient = [{ email }];
32 |
33 | try {
34 | const response = await mailtrapClient.send({
35 | from: sender,
36 | to: recipient,
37 | template_uuid: "229389ee-a527-4855-994d-2255c0a3b482",
38 | template_variables: {
39 | company_info_name: "Auth Company",
40 | name: name,
41 | },
42 | });
43 |
44 | console.log("Welcome email sent successfully", response);
45 | } catch (error) {
46 | console.error(`Error sending welcome email`, error);
47 |
48 | throw new Error(`Error sending welcome email: ${error}`);
49 | }
50 | };
51 |
52 |
53 |
54 | export const sendPasswordResetEmail = async (email, resetURL) => {
55 | const recipient = [{ email }];
56 |
57 | try {
58 | const response = await mailtrapClient.send({
59 | from: sender,
60 | to: recipient,
61 | subject: "Reset your password",
62 | html: PASSWORD_RESET_REQUEST_TEMPLATE.replace("{resetURL}", resetURL),
63 | category: "Password Reset",
64 | });
65 | } catch (error) {
66 | console.error(`Error sending password reset email`, error);
67 |
68 | throw new Error(`Error sending password reset email: ${error}`);
69 | }
70 | };
71 |
72 |
73 | export const sendResetSuccessEmail = async (email) => {
74 | const recipient = [{ email }];
75 |
76 | try {
77 | const response = await mailtrapClient.send({
78 | from: sender,
79 | to: recipient,
80 | subject: "Password Reset Successful",
81 | html: PASSWORD_RESET_SUCCESS_TEMPLATE,
82 | category: "Password Reset",
83 | });
84 |
85 | console.log("Password reset email sent successfully", response);
86 | } catch (error) {
87 | console.error(`Error sending password reset success email`, error);
88 |
89 | throw new Error(`Error sending password reset success email: ${error}`);
90 | }
91 | };
--------------------------------------------------------------------------------
/frontend/src/pages/LoginPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { motion } from "framer-motion";
3 | import { Mail, Lock, Loader } from "lucide-react";
4 | import { Link } from "react-router-dom";
5 | import Input from "../components/Input";
6 | import { useAuthStore } from "../store/authStore";
7 |
8 | const LoginPage = () => {
9 | const [email, setEmail] = useState("");
10 | const [password, setPassword] = useState("");
11 |
12 | const { login, isLoading, error } = useAuthStore();
13 |
14 | const handleLogin = async (e) => {
15 | e.preventDefault();
16 | await login(email, password);
17 | };
18 |
19 | return (
20 |
26 |
27 |
28 | Welcome Back
29 |
30 |
31 |
65 |
66 |
67 |
68 | Don't have an account?{" "}
69 |
70 | Sign up
71 |
72 |
73 |
74 |
75 | );
76 | };
77 | export default LoginPage;
78 |
--------------------------------------------------------------------------------
/frontend/src/pages/DashboardPage.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { useAuthStore } from "../store/authStore";
3 | import { formatDate } from "../utils/date";
4 |
5 | const DashboardPage = () => {
6 | const { user, logout } = useAuthStore();
7 |
8 | const handleLogout = () => {
9 | logout();
10 | };
11 | return (
12 |
19 |
20 | Dashboard
21 |
22 |
23 |
24 |
30 | Profile Information
31 | Name: {user.name}
32 | Email: {user.email}
33 |
34 |
40 | Account Activity
41 |
42 | Joined:
43 | {new Date(user.createdAt).toLocaleDateString("en-US", {
44 | year: "numeric",
45 | month: "long",
46 | day: "numeric",
47 | })}
48 |
49 |
50 | Last Login:
51 |
52 | {formatDate(user.lastLogin)}
53 |
54 |
55 |
56 |
57 |
63 |
71 | Logout
72 |
73 |
74 |
75 | );
76 | };
77 | export default DashboardPage;
78 |
--------------------------------------------------------------------------------
/frontend/src/pages/ResetPasswordPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { motion } from "framer-motion";
3 | import { useAuthStore } from "../store/authStore";
4 | import { useNavigate, useParams } from "react-router-dom";
5 | import Input from "../components/Input";
6 | import { Lock } from "lucide-react";
7 | import toast from "react-hot-toast";
8 |
9 | const ResetPasswordPage = () => {
10 | const [password, setPassword] = useState("");
11 | const [confirmPassword, setConfirmPassword] = useState("");
12 | const { resetPassword, error, isLoading, message } = useAuthStore();
13 |
14 | const { token } = useParams();
15 | const navigate = useNavigate();
16 |
17 | const handleSubmit = async (e) => {
18 | e.preventDefault();
19 |
20 | if (password !== confirmPassword) {
21 | alert("Passwords do not match");
22 | return;
23 | }
24 | try {
25 | await resetPassword(token, password);
26 |
27 | toast.success("Password reset successfully, redirecting to login page...");
28 | setTimeout(() => {
29 | navigate("/login");
30 | }, 2000);
31 | } catch (error) {
32 | console.error(error);
33 | toast.error(error.message || "Error resetting password");
34 | }
35 | };
36 |
37 | return (
38 |
44 |
45 |
46 | Reset Password
47 |
48 | {error &&
{error}
}
49 | {message &&
{message}
}
50 |
51 |
80 |
81 |
82 | );
83 | };
84 | export default ResetPasswordPage;
85 |
--------------------------------------------------------------------------------
/frontend/src/pages/ForgotPasswordPage.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { useState } from "react";
3 | import { useAuthStore } from "../store/authStore";
4 | import Input from "../components/Input";
5 | import { ArrowLeft, Loader, Mail } from "lucide-react";
6 | import { Link } from "react-router-dom";
7 |
8 | const ForgotPasswordPage = () => {
9 | const [email, setEmail] = useState("");
10 | const [isSubmitted, setIsSubmitted] = useState(false);
11 |
12 | const { isLoading, forgotPassword } = useAuthStore();
13 |
14 | const handleSubmit = async (e) => {
15 | e.preventDefault();
16 | await forgotPassword(email);
17 | setIsSubmitted(true);
18 | };
19 |
20 | return (
21 |
27 |
28 |
29 | Forgot Password
30 |
31 |
32 | {!isSubmitted ? (
33 |
54 | ) : (
55 |
56 |
62 |
63 |
64 |
65 | If an account exists for {email}, you will receive a password reset link shortly.
66 |
67 |
68 | )}
69 |
70 |
71 |
72 |
73 |
Back to Login
74 |
75 |
76 |
77 | );
78 | };
79 | export default ForgotPasswordPage;
80 |
--------------------------------------------------------------------------------
/frontend/src/pages/SignUpPage.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import Input from "../components/Input";
3 | import { Loader, Lock, Mail, User } from "lucide-react";
4 | import { useState } from "react";
5 | import { Link, useNavigate } from "react-router-dom";
6 | import PasswordStrengthMeter from "../components/PasswordStrengthMeter";
7 | import { useAuthStore } from "../store/authStore";
8 |
9 | const SignUpPage = () => {
10 | const [name, setName] = useState("");
11 | const [email, setEmail] = useState("");
12 | const [password, setPassword] = useState("");
13 | const navigate = useNavigate();
14 |
15 | const { signup, error, isLoading } = useAuthStore();
16 |
17 | const handleSignUp = async (e) => {
18 | e.preventDefault();
19 |
20 | try {
21 | await signup(email, password, name);
22 | navigate("/verify-email");
23 | } catch (error) {
24 | console.log(error);
25 | }
26 | };
27 | return (
28 |
35 |
36 |
37 | Create Account
38 |
39 |
40 |
78 |
79 |
80 |
81 | Already have an account?{" "}
82 |
83 | Login
84 |
85 |
86 |
87 |
88 | );
89 | };
90 | export default SignUpPage;
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Advanced Authentication with MERN Stack
2 |
3 | This project is an advanced authentication system built using the MERN (MongoDB, Express, React, Node.js) stack. It includes features like email verification, secure password hashing, and token-based authentication.
4 |
5 | ## Project Structure
6 |
7 | The project is organized as follows:
8 |
9 | - **Root**: Contains the main `package.json` file and links to both the backend and frontend folders.
10 | - **backend**: Contains the server-side code, including API endpoints, database models, and authentication logic.
11 | - **frontend**: Contains the client-side code, built with React, for user interaction and UI.
12 |
13 | ## Features
14 |
15 | - User registration and login
16 | - Email verification using Mailtrap
17 | - Secure password hashing with bcrypt
18 | - Token-based authentication with JSON Web Tokens (JWT)
19 | - Protected routes for authenticated users
20 | - Responsive and user-friendly UI
21 |
22 | ## Technologies Used
23 |
24 | - **Frontend**: React, Axios, Bootstrap/Material-UI (optional)
25 | - **Backend**: Node.js, Express.js, MongoDB, Mongoose
26 | - **Authentication**: JWT, bcrypt
27 | - **Email Service**: Mailtrap
28 |
29 | ## Prerequisites
30 |
31 | - Node.js and npm installed
32 | - MongoDB installed and running
33 | - Mailtrap account for email testing
34 |
35 | ## Installation
36 |
37 | 1. Clone the repository:
38 | ```bash
39 | git clone https://github.com/arafatDU/mern-advanced-auth.git
40 | cd mern-advanced-auth
41 | ```
42 |
43 | 2. Install dependencies for the root project:
44 | ```bash
45 | npm install
46 | ```
47 |
48 | 3. Install dependencies for both backend and frontend:
49 | ```bash
50 | cd backend
51 | npm install
52 | cd ../frontend
53 | npm install
54 | ```
55 |
56 | 4. Configure environment variables:
57 | - Create a `.env` file in the `backend` folder with the following:
58 | ```env
59 | MONGO_URI=your_mongodb_connection_string
60 | JWT_SECRET=your_jwt_secret
61 | MAILTRAP_USER=your_mailtrap_username
62 | MAILTRAP_PASS=your_mailtrap_password
63 | ```
64 |
65 | 5. Start the development server:
66 | ```bash
67 | npm run dev
68 | ```
69 | This will start the backend server located at `backend/index.js`.
70 |
71 | ## Usage
72 |
73 | 1. Register a new user.
74 | 2. Check your Mailtrap inbox for the verification email.
75 | 3. Verify your email to activate your account.
76 | 4. Log in to access protected routes.
77 |
78 | ## Folder Structure
79 |
80 | ```
81 | mern-advanced-auth/
82 | ├── backend/
83 | │ ├── controllers/
84 | │ ├── db/
85 | │ ├── routes/
86 | │ ├── index.js
87 | │ └── ...
88 | ├── frontend/
89 | │ ├── src/
90 | │ │ ├── components/
91 | │ │ ├── pages/
92 | │ │ ├── utils/
93 | │ │ └── App.js
94 | ├── package.json
95 | └── README.md
96 | ```
97 |
98 | ## License
99 |
100 | This project is licensed under the [MIT License](LICENSE).
101 |
102 | ## Acknowledgments
103 |
104 | - [Mailtrap](https://mailtrap.io/) for email testing
105 | - [MERN Stack Documentation](https://www.mongodb.com/mern-stack)
106 |
107 | Feel free to contribute to this project by submitting issues or pull requests!
--------------------------------------------------------------------------------
/frontend/src/store/authStore.js:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import axios from "axios";
3 |
4 | const API_URL = import.meta.env.MODE === "development" ? "http://localhost:5000/api/auth" : "/api/auth";
5 |
6 | axios.defaults.withCredentials = true;
7 |
8 | export const useAuthStore = create((set) => ({
9 | user: null,
10 | isAuthenticated: false,
11 | error: null,
12 | isLoading: false,
13 | isCheckingAuth: true,
14 | message: null,
15 |
16 | signup: async (email, password, name) => {
17 | set({ isLoading: true, error: null });
18 | try {
19 | const response = await axios.post(`${API_URL}/signup`, { email, password, name });
20 | set({ user: response.data.user, isAuthenticated: true, isLoading: false });
21 | } catch (error) {
22 | set({ error: error.response.data.message || "Error signing up", isLoading: false });
23 | throw error;
24 | }
25 | },
26 | login: async (email, password) => {
27 | set({ isLoading: true, error: null });
28 | try {
29 | const response = await axios.post(`${API_URL}/login`, { email, password });
30 | set({
31 | isAuthenticated: true,
32 | user: response.data.user,
33 | error: null,
34 | isLoading: false,
35 | });
36 | } catch (error) {
37 | set({ error: error.response?.data?.message || "Error logging in", isLoading: false });
38 | throw error;
39 | }
40 | },
41 |
42 | logout: async () => {
43 | set({ isLoading: true, error: null });
44 | try {
45 | await axios.post(`${API_URL}/logout`);
46 | set({ user: null, isAuthenticated: false, error: null, isLoading: false });
47 | } catch (error) {
48 | set({ error: "Error logging out", isLoading: false });
49 | throw error;
50 | }
51 | },
52 | verifyEmail: async (code) => {
53 | set({ isLoading: true, error: null });
54 | try {
55 | const response = await axios.post(`${API_URL}/verify-email`, { code });
56 | set({ user: response.data.user, isAuthenticated: true, isLoading: false });
57 | return response.data;
58 | } catch (error) {
59 | set({ error: error.response.data.message || "Error verifying email", isLoading: false });
60 | throw error;
61 | }
62 | },
63 | checkAuth: async () => {
64 | set({ isCheckingAuth: true, error: null });
65 | try {
66 | const response = await axios.get(`${API_URL}/check-auth`);
67 | set({ user: response.data.user, isAuthenticated: true, isCheckingAuth: false });
68 | } catch (error) {
69 | set({ error: null, isCheckingAuth: false, isAuthenticated: false });
70 | }
71 | },
72 | forgotPassword: async (email) => {
73 | set({ isLoading: true, error: null });
74 | try {
75 | const response = await axios.post(`${API_URL}/forgot-password`, { email });
76 | set({ message: response.data.message, isLoading: false });
77 | } catch (error) {
78 | set({
79 | isLoading: false,
80 | error: error.response.data.message || "Error sending reset password email",
81 | });
82 | throw error;
83 | }
84 | },
85 | resetPassword: async (token, password) => {
86 | set({ isLoading: true, error: null });
87 | try {
88 | const response = await axios.post(`${API_URL}/reset-password/${token}`, { password });
89 | set({ message: response.data.message, isLoading: false });
90 | } catch (error) {
91 | set({
92 | isLoading: false,
93 | error: error.response.data.message || "Error resetting password",
94 | });
95 | throw error;
96 | }
97 | },
98 | }));
99 |
--------------------------------------------------------------------------------
/frontend/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate, Route, Routes } from "react-router-dom";
2 | import FloatingShape from "./components/FloatingShape";
3 |
4 | import SignUpPage from "./pages/SignUpPage";
5 | import LoginPage from "./pages/LoginPage";
6 | import DashboardPage from "./pages/DashboardPage";
7 |
8 | import LoadingSpinner from "./components/LoadingSpinner";
9 |
10 | import { Toaster } from "react-hot-toast";
11 | import { useAuthStore } from "./store/authStore";
12 | import { useEffect } from "react";
13 | import EmailVerificationPage from "./pages/EmailVerificationPage";
14 | import ForgotPasswordPage from "./pages/ForgotPasswordPage";
15 | import ResetPasswordPage from "./pages/ResetPasswordPage";
16 |
17 | // protect routes that require authentication
18 | const ProtectedRoute = ({ children }) => {
19 | const { isAuthenticated, user } = useAuthStore();
20 |
21 | if (!isAuthenticated) {
22 | return ;
23 | }
24 |
25 | if (!user.isVerified) {
26 | return ;
27 | }
28 |
29 | return children;
30 | };
31 |
32 | // redirect authenticated users to the home page
33 | const RedirectAuthenticatedUser = ({ children }) => {
34 | const { isAuthenticated, user } = useAuthStore();
35 |
36 | if (isAuthenticated && user.isVerified) {
37 | return ;
38 | }
39 |
40 | return children;
41 | };
42 |
43 | function App() {
44 | const { isCheckingAuth, checkAuth } = useAuthStore();
45 |
46 | useEffect(() => {
47 | checkAuth();
48 | }, [checkAuth]);
49 |
50 | if (isCheckingAuth) return ;
51 |
52 | return (
53 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
67 |
68 |
69 | }
70 | />
71 |
75 |
76 |
77 | }
78 | />
79 |
83 |
84 |
85 | }
86 | />
87 | } />
88 |
92 |
93 |
94 | }
95 | />
96 |
97 |
101 |
102 |
103 | }
104 | />
105 |
106 | {/* catch all routes */}
107 | } />
108 |
109 |
110 |
111 | );
112 | }
113 |
114 | export default App;
115 |
--------------------------------------------------------------------------------
/frontend/src/pages/EmailVerificationPage.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import { motion } from "framer-motion";
4 | import { useAuthStore } from "../store/authStore";
5 | import toast from "react-hot-toast";
6 |
7 | const EmailVerificationPage = () => {
8 | const [code, setCode] = useState(["", "", "", "", "", ""]);
9 | const inputRefs = useRef([]);
10 | const navigate = useNavigate();
11 |
12 | const { error, isLoading, verifyEmail } = useAuthStore();
13 |
14 | const handleChange = (index, value) => {
15 | const newCode = [...code];
16 |
17 | // Handle pasted content
18 | if (value.length > 1) {
19 | const pastedCode = value.slice(0, 6).split("");
20 | for (let i = 0; i < 6; i++) {
21 | newCode[i] = pastedCode[i] || "";
22 | }
23 | setCode(newCode);
24 |
25 | // Focus on the last non-empty input or the first empty one
26 | const lastFilledIndex = newCode.findLastIndex((digit) => digit !== "");
27 | const focusIndex = lastFilledIndex < 5 ? lastFilledIndex + 1 : 5;
28 | inputRefs.current[focusIndex].focus();
29 | } else {
30 | newCode[index] = value;
31 | setCode(newCode);
32 |
33 | // Move focus to the next input field if value is entered
34 | if (value && index < 5) {
35 | inputRefs.current[index + 1].focus();
36 | }
37 | }
38 | };
39 |
40 | const handleKeyDown = (index, e) => {
41 | if (e.key === "Backspace" && !code[index] && index > 0) {
42 | inputRefs.current[index - 1].focus();
43 | }
44 | };
45 |
46 | const handleSubmit = async (e) => {
47 | e.preventDefault();
48 | const verificationCode = code.join("");
49 | try {
50 | await verifyEmail(verificationCode);
51 | navigate("/");
52 | toast.success("Email verified successfully");
53 | } catch (error) {
54 | console.log(error);
55 | }
56 | };
57 |
58 | // Auto submit when all fields are filled
59 | useEffect(() => {
60 | if (code.every((digit) => digit !== "")) {
61 | handleSubmit(new Event("submit"));
62 | }
63 | }, [code]);
64 |
65 | return (
66 |
67 |
73 |
74 | Verify Your Email
75 |
76 | Enter the 6-digit code sent to your email address.
77 |
78 |
104 |
105 |
106 | );
107 | };
108 | export default EmailVerificationPage;
109 |
--------------------------------------------------------------------------------
/backend/mailtrap/emailTemplates.js:
--------------------------------------------------------------------------------
1 | export const VERIFICATION_EMAIL_TEMPLATE = `
2 |
3 |
4 |
5 |
6 |
7 | Verify Your Email
8 |
9 |
10 |
11 |
Verify Your Email
12 |
13 |
14 |
Hello,
15 |
Thank you for signing up! Your verification code is:
16 |
17 | {verificationCode}
18 |
19 |
Enter this code on the verification page to complete your registration.
20 |
This code will expire in 15 minutes for security reasons.
21 |
If you didn't create an account with us, please ignore this email.
22 |
Best regards,
Your App Team
23 |
24 |
25 |
This is an automated message, please do not reply to this email.
26 |
27 |
28 |
29 | `;
30 |
31 | export const PASSWORD_RESET_SUCCESS_TEMPLATE = `
32 |
33 |
34 |
35 |
36 |
37 | Password Reset Successful
38 |
39 |
40 |
41 |
Password Reset Successful
42 |
43 |
44 |
Hello,
45 |
We're writing to confirm that your password has been successfully reset.
46 |
51 |
If you did not initiate this password reset, please contact our support team immediately.
52 |
For security reasons, we recommend that you:
53 |
54 | - Use a strong, unique password
55 | - Enable two-factor authentication if available
56 | - Avoid using the same password across multiple sites
57 |
58 |
Thank you for helping us keep your account secure.
59 |
Best regards,
Your App Team
60 |
61 |
62 |
This is an automated message, please do not reply to this email.
63 |
64 |
65 |
66 | `;
67 |
68 | export const PASSWORD_RESET_REQUEST_TEMPLATE = `
69 |
70 |
71 |
72 |
73 |
74 | Reset Your Password
75 |
76 |
77 |
78 |
Password Reset
79 |
80 |
81 |
Hello,
82 |
We received a request to reset your password. If you didn't make this request, please ignore this email.
83 |
To reset your password, click the button below:
84 |
87 |
This link will expire in 1 hour for security reasons.
88 |
Best regards,
Your App Team
89 |
90 |
91 |
This is an automated message, please do not reply to this email.
92 |
93 |
94 |
95 | `;
--------------------------------------------------------------------------------
/backend/controllers/auth.controller.js:
--------------------------------------------------------------------------------
1 | import { User } from "../models/user.model.js";
2 | import bcryptjs from "bcryptjs";
3 | import crypto from "crypto";
4 | import { generateTokenAndSetCookie } from "../utils/generateTokenAndSetCookie.js";
5 | import { sendPasswordResetEmail, sendVerificationEmail, sendWelcomeEmail, sendResetSuccessEmail } from "../mailtrap/emails.js";
6 |
7 | export const signup = async (req, res) => {
8 | const { email, password, name } = req.body;
9 |
10 | try {
11 | if(!email || !password || !name) {
12 | throw new Error("All fields are required");
13 | }
14 |
15 | const userAlreadyExists = await User.findOne({ email });
16 | if(userAlreadyExists) {
17 | return res.status(400).json({success: false, message: "User already exists"});
18 | }
19 |
20 | const hashedPassword = await bcryptjs.hash(password, 10);
21 | const verificationToken = Math.floor(100000 + Math.random() * 900000).toString();
22 |
23 | const user = new User({
24 | email,
25 | password: hashedPassword,
26 | name,
27 | verificationToken,
28 | verificationTokenExpiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
29 | })
30 |
31 | await user.save();
32 |
33 | // send verification email
34 | await sendVerificationEmail(user.email, verificationToken);
35 |
36 | // jwt token
37 | generateTokenAndSetCookie(res, user._id);
38 |
39 |
40 |
41 | res.status(201).json({
42 | success: true,
43 | message: "User created successfully",
44 | user: {
45 | ...user._doc,
46 | password: undefined,
47 | }
48 | });
49 |
50 | } catch (error) {
51 | res.status(400).json({success: false, message: error.message});
52 | }
53 | }
54 |
55 |
56 |
57 | export const verifyEmail = async (req, res) => {
58 | const { code } = req.body;
59 | try {
60 | const user = await User.findOne({
61 | verificationToken: code,
62 | verificationTokenExpiresAt: { $gt: Date.now() },
63 | });
64 |
65 | if (!user) {
66 | return res.status(400).json({ success: false, message: "Invalid or expired verification code" });
67 | }
68 |
69 | user.isVerified = true;
70 | user.verificationToken = undefined;
71 | user.verificationTokenExpiresAt = undefined;
72 | await user.save();
73 |
74 | await sendWelcomeEmail(user.email, user.name);
75 |
76 | res.status(200).json({
77 | success: true,
78 | message: "Email verified successfully",
79 | user: {
80 | ...user._doc,
81 | password: undefined,
82 | },
83 | });
84 | } catch (error) {
85 | console.log("error in verifyEmail ", error);
86 | res.status(500).json({ success: false, message: "Server error" });
87 | }
88 | };
89 |
90 |
91 | export const login = async (req, res) => {
92 | const { email, password } = req.body;
93 | try {
94 | if(!email || !password) {
95 | throw new Error("All fields are required");
96 | }
97 | const user = await User.findOne({ email });
98 | if(!user) {
99 | return res.status(400).json({success: false, message: "User not found"});
100 | }
101 | const isPasswordCorrect = await bcryptjs.compare(password, user.password);
102 | if(!isPasswordCorrect) {
103 | return res.status(400).json({success: false, message: "Invalid credentials"});
104 | }
105 |
106 | generateTokenAndSetCookie(res, user._id);
107 | user.lastLogin = new Date();
108 | await user.save();
109 |
110 | return res.status(200).json({
111 | success: true,
112 | message: "Logged in successfully",
113 | user: {
114 | ...user._doc,
115 | password: undefined,
116 | }
117 | });
118 |
119 | } catch (error) {
120 | console.log("error in login ", error);
121 | res.status(400).json({success: false, message: error.message});
122 | }
123 | }
124 |
125 |
126 | export const logout = async (req, res) => {
127 | res.clearCookie("token");
128 | res.status(200).json({ success: true, message: "Logged out successfully" });
129 | };
130 |
131 |
132 |
133 | export const forgotPassword = async (req, res) => {
134 | const { email } = req.body;
135 |
136 | try {
137 | if(!email) {
138 | throw new Error("Email is required");
139 | }
140 | const user = await User.findOne({email});
141 | if(!user) {
142 | return res.status(400).json({success: false, message: "User not found"});
143 | }
144 |
145 | const resetToken = crypto.randomBytes(20).toString("hex");
146 | const resetTokenExpiresAt = Date.now() + 1 * 60 * 60 * 1000; // 1 hour
147 |
148 | user.resetPasswordToken = resetToken;
149 | user.resetPasswordExpiresAt = resetTokenExpiresAt;
150 | await user.save();
151 |
152 | await sendPasswordResetEmail(user.email, `${process.env.CLIENT_URL}/reset-password/${resetToken}`);
153 |
154 | res.status(200).json({
155 | success: true,
156 | message: "Password reset email sent successfully",
157 | });
158 |
159 |
160 | } catch (error) {
161 | console.log("error in forgotPassword ", error);
162 | res.status(400).json({success: false, message: error.message});
163 | }
164 | }
165 |
166 |
167 |
168 | export const resetPassword = async (req, res) => {
169 | try {
170 | const { token } = req.params;
171 | const { password } = req.body;
172 |
173 | const user = await User.findOne({
174 | resetPasswordToken: token,
175 | resetPasswordExpiresAt: { $gt: Date.now() },
176 | });
177 |
178 | if (!user) {
179 | return res.status(400).json({ success: false, message: "Invalid or expired reset token" });
180 | }
181 |
182 | // update password
183 | const hashedPassword = await bcryptjs.hash(password, 10);
184 |
185 | user.password = hashedPassword;
186 | user.resetPasswordToken = undefined;
187 | user.resetPasswordExpiresAt = undefined;
188 | await user.save();
189 |
190 | await sendResetSuccessEmail(user.email);
191 |
192 | res.status(200).json({ success: true, message: "Password reset successful" });
193 | } catch (error) {
194 | console.log("Error in resetPassword ", error);
195 | res.status(400).json({ success: false, message: error.message });
196 | }
197 | };
198 |
199 |
200 | export const checkAuth = async (req, res) => {
201 | try {
202 | const user = await User.findById(req.userId).select("-password");
203 | if (!user) {
204 | return res.status(400).json({ success: false, message: "User not found" });
205 | }
206 |
207 | res.status(200).json({ success: true, user });
208 | } catch (error) {
209 | console.log("Error in checkAuth ", error);
210 | res.status(400).json({ success: false, message: error.message });
211 | }
212 | };
--------------------------------------------------------------------------------