├── .gitignore ├── LICENSE ├── README.md ├── backend ├── controllers │ └── auth.controller.js ├── db │ └── connectDB.js ├── index.js ├── mailtrap │ ├── emailTemplates.js │ ├── emails.js │ └── mailtrap.config.js ├── middleware │ └── verifyToken.js ├── models │ └── user.model.js ├── routes │ └── auth.route.js └── utils │ └── generateTokenAndSetCookie.js ├── frontend ├── .eslintrc.cjs ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── screenshot-for-readme.png │ └── vite.svg ├── src │ ├── App.jsx │ ├── components │ │ ├── FloatingShape.jsx │ │ ├── Input.jsx │ │ ├── LoadingSpinner.jsx │ │ └── PasswordStrengthMeter.jsx │ ├── index.css │ ├── main.jsx │ ├── pages │ │ ├── DashboardPage.jsx │ │ ├── EmailVerificationPage.jsx │ │ ├── ForgotPasswordPage.jsx │ │ ├── LoginPage.jsx │ │ ├── ResetPasswordPage.jsx │ │ └── SignUpPage.jsx │ ├── store │ │ └── authStore.js │ └── utils │ │ └── date.js ├── tailwind.config.js └── vite.config.js ├── package-lock.json └── package.json /.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 | .env 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Burak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Advanced Auth Tutorial 🔒

2 | 3 | ![Demo App](/frontend/public/screenshot-for-readme.png) 4 | 5 | [Video Tutorial on Youtube](https://youtu.be/pmvEgZC55Cg) 6 | 7 | About This Course: 8 | 9 | - 🔧 Backend Setup 10 | - 🗄️ Database Setup 11 | - 🔐 Signup Endpoint 12 | - 📧 Sending Verify Account Email 13 | - 🔍 Verify Email Endpoint 14 | - 📄 Building a Welcome Email Template 15 | - 🚪 Logout Endpoint 16 | - 🔑 Login Endpoint 17 | - 🔄 Forgot Password Endpoint 18 | - 🔁 Reset Password Endpoint 19 | - ✔️ Check Auth Endpoint 20 | - 🌐 Frontend Setup 21 | - 📋 Signup Page UI 22 | - 🔓 Login Page UI 23 | - ✅ Email Verification Page UI 24 | - 📤 Implementing Signup 25 | - 📧 Implementing Email Verification 26 | - 🔒 Protecting Our Routes 27 | - 🔑 Implementing Login 28 | - 🏠 Dashboard Page 29 | - 🔄 Implementing Forgot Password 30 | - 🚀 Super Detailed Deployment 31 | - ✅ This is a lot of work. Support my work by subscribing to the [Channel](https://www.youtube.com/@asaprogrammer_) 32 | 33 | ### Setup .env file 34 | 35 | ```bash 36 | MONGO_URI=your_mongo_uri 37 | PORT=5000 38 | JWT_SECRET=your_secret_key 39 | NODE_ENV=development 40 | 41 | MAILTRAP_TOKEN=your_mailtrap_token 42 | MAILTRAP_ENDPOINT=https://send.api.mailtrap.io/ 43 | 44 | CLIENT_URL= http://localhost:5173 45 | ``` 46 | 47 | ### Run this app locally 48 | 49 | ```shell 50 | npm run build 51 | ``` 52 | 53 | ### Start the app 54 | 55 | ```shell 56 | npm run start 57 | ``` 58 | 59 | ### I'll see you in the next one! 🚀 60 | -------------------------------------------------------------------------------- /backend/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | import bcryptjs from "bcryptjs"; 2 | import crypto from "crypto"; 3 | 4 | import { generateTokenAndSetCookie } from "../utils/generateTokenAndSetCookie.js"; 5 | import { 6 | sendPasswordResetEmail, 7 | sendResetSuccessEmail, 8 | sendVerificationEmail, 9 | sendWelcomeEmail, 10 | } from "../mailtrap/emails.js"; 11 | import { User } from "../models/user.model.js"; 12 | 13 | export const signup = async (req, res) => { 14 | const { email, password, name } = req.body; 15 | 16 | try { 17 | if (!email || !password || !name) { 18 | throw new Error("All fields are required"); 19 | } 20 | 21 | const userAlreadyExists = await User.findOne({ email }); 22 | console.log("userAlreadyExists", userAlreadyExists); 23 | 24 | if (userAlreadyExists) { 25 | return res.status(400).json({ success: false, message: "User already exists" }); 26 | } 27 | 28 | const hashedPassword = await bcryptjs.hash(password, 10); 29 | const verificationToken = Math.floor(100000 + Math.random() * 900000).toString(); 30 | 31 | const user = new User({ 32 | email, 33 | password: hashedPassword, 34 | name, 35 | verificationToken, 36 | verificationTokenExpiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours 37 | }); 38 | 39 | await user.save(); 40 | 41 | // jwt 42 | generateTokenAndSetCookie(res, user._id); 43 | 44 | await sendVerificationEmail(user.email, verificationToken); 45 | 46 | res.status(201).json({ 47 | success: true, 48 | message: "User created successfully", 49 | user: { 50 | ...user._doc, 51 | password: undefined, 52 | }, 53 | }); 54 | } catch (error) { 55 | res.status(400).json({ success: false, message: error.message }); 56 | } 57 | }; 58 | 59 | export const verifyEmail = async (req, res) => { 60 | const { code } = req.body; 61 | try { 62 | const user = await User.findOne({ 63 | verificationToken: code, 64 | verificationTokenExpiresAt: { $gt: Date.now() }, 65 | }); 66 | 67 | if (!user) { 68 | return res.status(400).json({ success: false, message: "Invalid or expired verification code" }); 69 | } 70 | 71 | user.isVerified = true; 72 | user.verificationToken = undefined; 73 | user.verificationTokenExpiresAt = undefined; 74 | await user.save(); 75 | 76 | await sendWelcomeEmail(user.email, user.name); 77 | 78 | res.status(200).json({ 79 | success: true, 80 | message: "Email verified successfully", 81 | user: { 82 | ...user._doc, 83 | password: undefined, 84 | }, 85 | }); 86 | } catch (error) { 87 | console.log("error in verifyEmail ", error); 88 | res.status(500).json({ success: false, message: "Server error" }); 89 | } 90 | }; 91 | 92 | export const login = async (req, res) => { 93 | const { email, password } = req.body; 94 | try { 95 | const user = await User.findOne({ email }); 96 | if (!user) { 97 | return res.status(400).json({ success: false, message: "Invalid credentials" }); 98 | } 99 | const isPasswordValid = await bcryptjs.compare(password, user.password); 100 | if (!isPasswordValid) { 101 | return res.status(400).json({ success: false, message: "Invalid credentials" }); 102 | } 103 | 104 | generateTokenAndSetCookie(res, user._id); 105 | 106 | user.lastLogin = new Date(); 107 | await user.save(); 108 | 109 | res.status(200).json({ 110 | success: true, 111 | message: "Logged in successfully", 112 | user: { 113 | ...user._doc, 114 | password: undefined, 115 | }, 116 | }); 117 | } catch (error) { 118 | console.log("Error in login ", error); 119 | res.status(400).json({ success: false, message: error.message }); 120 | } 121 | }; 122 | 123 | export const logout = async (req, res) => { 124 | res.clearCookie("token"); 125 | res.status(200).json({ success: true, message: "Logged out successfully" }); 126 | }; 127 | 128 | export const forgotPassword = async (req, res) => { 129 | const { email } = req.body; 130 | try { 131 | const user = await User.findOne({ email }); 132 | 133 | if (!user) { 134 | return res.status(400).json({ success: false, message: "User not found" }); 135 | } 136 | 137 | // Generate reset token 138 | const resetToken = crypto.randomBytes(20).toString("hex"); 139 | const resetTokenExpiresAt = Date.now() + 1 * 60 * 60 * 1000; // 1 hour 140 | 141 | user.resetPasswordToken = resetToken; 142 | user.resetPasswordExpiresAt = resetTokenExpiresAt; 143 | 144 | await user.save(); 145 | 146 | // send email 147 | await sendPasswordResetEmail(user.email, `${process.env.CLIENT_URL}/reset-password/${resetToken}`); 148 | 149 | res.status(200).json({ success: true, message: "Password reset link sent to your email" }); 150 | } catch (error) { 151 | console.log("Error in forgotPassword ", error); 152 | res.status(400).json({ success: false, message: error.message }); 153 | } 154 | }; 155 | 156 | export const resetPassword = async (req, res) => { 157 | try { 158 | const { token } = req.params; 159 | const { password } = req.body; 160 | 161 | const user = await User.findOne({ 162 | resetPasswordToken: token, 163 | resetPasswordExpiresAt: { $gt: Date.now() }, 164 | }); 165 | 166 | if (!user) { 167 | return res.status(400).json({ success: false, message: "Invalid or expired reset token" }); 168 | } 169 | 170 | // update password 171 | const hashedPassword = await bcryptjs.hash(password, 10); 172 | 173 | user.password = hashedPassword; 174 | user.resetPasswordToken = undefined; 175 | user.resetPasswordExpiresAt = undefined; 176 | await user.save(); 177 | 178 | await sendResetSuccessEmail(user.email); 179 | 180 | res.status(200).json({ success: true, message: "Password reset successful" }); 181 | } catch (error) { 182 | console.log("Error in resetPassword ", error); 183 | res.status(400).json({ success: false, message: error.message }); 184 | } 185 | }; 186 | 187 | export const checkAuth = async (req, res) => { 188 | try { 189 | const user = await User.findById(req.userId).select("-password"); 190 | if (!user) { 191 | return res.status(400).json({ success: false, message: "User not found" }); 192 | } 193 | 194 | res.status(200).json({ success: true, user }); 195 | } catch (error) { 196 | console.log("Error in checkAuth ", error); 197 | res.status(400).json({ success: false, message: error.message }); 198 | } 199 | }; 200 | -------------------------------------------------------------------------------- /backend/db/connectDB.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | export const connectDB = async () => { 4 | try { 5 | console.log("mongo_uri: ", process.env.MONGO_URI); 6 | const conn = await mongoose.connect(process.env.MONGO_URI); 7 | console.log(`MongoDB Connected: ${conn.connection.host}`); 8 | } catch (error) { 9 | console.log("Error connection to MongoDB: ", error.message); 10 | process.exit(1); // 1 is failure, 0 status code is success 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /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 path from "path"; 6 | 7 | import { connectDB } from "./db/connectDB.js"; 8 | 9 | import authRoutes from "./routes/auth.route.js"; 10 | 11 | dotenv.config(); 12 | 13 | const app = express(); 14 | const PORT = process.env.PORT || 5000; 15 | const __dirname = path.resolve(); 16 | 17 | app.use(cors({ origin: "http://localhost:5173", credentials: true })); 18 | 19 | app.use(express.json()); // allows us to parse incoming requests:req.body 20 | app.use(cookieParser()); // allows us to parse incoming cookies 21 | 22 | app.use("/api/auth", authRoutes); 23 | 24 | if (process.env.NODE_ENV === "production") { 25 | app.use(express.static(path.join(__dirname, "/frontend/dist"))); 26 | 27 | app.get("*", (req, res) => { 28 | res.sendFile(path.resolve(__dirname, "frontend", "dist", "index.html")); 29 | }); 30 | } 31 | 32 | app.listen(PORT, () => { 33 | connectDB(); 34 | console.log("Server is running on port: ", PORT); 35 | }); 36 | -------------------------------------------------------------------------------- /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 |
47 |
48 | ✓ 49 |
50 |
51 |

If you did not initiate this password reset, please contact our support team immediately.

52 |

For security reasons, we recommend that you:

53 | 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 |
85 | Reset Password 86 |
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 | `; 96 | -------------------------------------------------------------------------------- /backend/mailtrap/emails.js: -------------------------------------------------------------------------------- 1 | import { 2 | PASSWORD_RESET_REQUEST_TEMPLATE, 3 | PASSWORD_RESET_SUCCESS_TEMPLATE, 4 | VERIFICATION_EMAIL_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 | export const sendWelcomeEmail = async (email, name) => { 29 | const recipient = [{ email }]; 30 | 31 | try { 32 | const response = await mailtrapClient.send({ 33 | from: sender, 34 | to: recipient, 35 | template_uuid: "e65925d1-a9d1-4a40-ae7c-d92b37d593df", 36 | template_variables: { 37 | company_info_name: "Auth Company", 38 | name: name, 39 | }, 40 | }); 41 | 42 | console.log("Welcome email sent successfully", response); 43 | } catch (error) { 44 | console.error(`Error sending welcome email`, error); 45 | 46 | throw new Error(`Error sending welcome email: ${error}`); 47 | } 48 | }; 49 | 50 | export const sendPasswordResetEmail = async (email, resetURL) => { 51 | const recipient = [{ email }]; 52 | 53 | try { 54 | const response = await mailtrapClient.send({ 55 | from: sender, 56 | to: recipient, 57 | subject: "Reset your password", 58 | html: PASSWORD_RESET_REQUEST_TEMPLATE.replace("{resetURL}", resetURL), 59 | category: "Password Reset", 60 | }); 61 | } catch (error) { 62 | console.error(`Error sending password reset email`, error); 63 | 64 | throw new Error(`Error sending password reset email: ${error}`); 65 | } 66 | }; 67 | 68 | export const sendResetSuccessEmail = async (email) => { 69 | const recipient = [{ email }]; 70 | 71 | try { 72 | const response = await mailtrapClient.send({ 73 | from: sender, 74 | to: recipient, 75 | subject: "Password Reset Successful", 76 | html: PASSWORD_RESET_SUCCESS_TEMPLATE, 77 | category: "Password Reset", 78 | }); 79 | 80 | console.log("Password reset email sent successfully", response); 81 | } catch (error) { 82 | console.error(`Error sending password reset success email`, error); 83 | 84 | throw new Error(`Error sending password reset success email: ${error}`); 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /backend/mailtrap/mailtrap.config.js: -------------------------------------------------------------------------------- 1 | import { MailtrapClient } from "mailtrap"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | export const mailtrapClient = new MailtrapClient({ 7 | endpoint: process.env.MAILTRAP_ENDPOINT, 8 | token: process.env.MAILTRAP_TOKEN, 9 | }); 10 | 11 | export const sender = { 12 | email: "mailtrap@demomailtrap.com", 13 | name: "Burak", 14 | }; 15 | -------------------------------------------------------------------------------- /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 | }; 18 | -------------------------------------------------------------------------------- /backend/models/user.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const userSchema = new mongoose.Schema( 4 | { 5 | email: { 6 | type: String, 7 | required: true, 8 | unique: true, 9 | }, 10 | password: { 11 | type: String, 12 | required: true, 13 | }, 14 | name: { 15 | type: String, 16 | required: true, 17 | }, 18 | lastLogin: { 19 | type: Date, 20 | default: Date.now, 21 | }, 22 | isVerified: { 23 | type: Boolean, 24 | default: false, 25 | }, 26 | resetPasswordToken: String, 27 | resetPasswordExpiresAt: Date, 28 | verificationToken: String, 29 | verificationTokenExpiresAt: Date, 30 | }, 31 | { timestamps: true } 32 | ); 33 | 34 | export const User = mongoose.model("User", userSchema); 35 | -------------------------------------------------------------------------------- /backend/routes/auth.route.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | login, 4 | logout, 5 | signup, 6 | verifyEmail, 7 | forgotPassword, 8 | resetPassword, 9 | checkAuth, 10 | } from "../controllers/auth.controller.js"; 11 | import { verifyToken } from "../middleware/verifyToken.js"; 12 | 13 | const router = express.Router(); 14 | 15 | router.get("/check-auth", verifyToken, checkAuth); 16 | 17 | router.post("/signup", signup); 18 | router.post("/login", login); 19 | router.post("/logout", logout); 20 | 21 | router.post("/verify-email", verifyEmail); 22 | router.post("/forgot-password", forgotPassword); 23 | 24 | router.post("/reset-password/:token", resetPassword); 25 | 26 | export default router; 27 | -------------------------------------------------------------------------------- /backend/utils/generateTokenAndSetCookie.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | export const generateTokenAndSetCookie = (res, userId) => { 4 | const token = jwt.sign({ userId }, process.env.JWT_SECRET, { 5 | expiresIn: "7d", 6 | }); 7 | 8 | res.cookie("token", token, { 9 | httpOnly: true, 10 | secure: process.env.NODE_ENV === "production", 11 | sameSite: "strict", 12 | maxAge: 7 * 24 * 60 * 60 * 1000, 13 | }); 14 | 15 | return token; 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 | -------------------------------------------------------------------------------- /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/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 | "auth-tutorial": "file:..", 14 | "axios": "^1.7.3", 15 | "framer-motion": "^11.3.21", 16 | "lucide-react": "^0.424.0", 17 | "react": "^18.3.1", 18 | "react-dom": "^18.3.1", 19 | "react-hot-toast": "^2.4.1", 20 | "react-router-dom": "^6.26.0", 21 | "zustand": "^4.5.4" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^18.3.3", 25 | "@types/react-dom": "^18.3.0", 26 | "@vitejs/plugin-react": "^4.3.1", 27 | "autoprefixer": "^10.4.20", 28 | "eslint": "^8.57.0", 29 | "eslint-plugin-react": "^7.34.3", 30 | "eslint-plugin-react-hooks": "^4.6.2", 31 | "eslint-plugin-react-refresh": "^0.4.7", 32 | "postcss": "^8.4.40", 33 | "tailwindcss": "^3.4.7", 34 | "vite": "^5.3.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/screenshot-for-readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/mern-advanced-auth/fe0f65940913f8a0a067124369b290750c296208/frontend/public/screenshot-for-readme.png -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 EmailVerificationPage from "./pages/EmailVerificationPage"; 7 | import DashboardPage from "./pages/DashboardPage"; 8 | import ForgotPasswordPage from "./pages/ForgotPasswordPage"; 9 | import ResetPasswordPage from "./pages/ResetPasswordPage"; 10 | 11 | import LoadingSpinner from "./components/LoadingSpinner"; 12 | 13 | import { Toaster } from "react-hot-toast"; 14 | import { useAuthStore } from "./store/authStore"; 15 | import { useEffect } from "react"; 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 | 66 | 67 | 68 | } 69 | /> 70 | 74 | 75 | 76 | } 77 | /> 78 | 82 | 83 | 84 | } 85 | /> 86 | } /> 87 | 91 | 92 | 93 | } 94 | /> 95 | 96 | 100 | 101 | 102 | } 103 | /> 104 | {/* catch all routes */} 105 | } /> 106 | 107 | 108 |
109 | ); 110 | } 111 | 112 | export default App; 113 | -------------------------------------------------------------------------------- /frontend/src/components/FloatingShape.jsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | 3 | const FloatingShape = ({ color, size, top, left, delay }) => { 4 | return ( 5 |