diff --git a/backend/.env.example b/backend/.env.example index 63ad8ae..25370b7 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -14,3 +14,4 @@ SMTP_PASS=your_app_password EMAIL_FROM="MailMERN no-reply@example.com" EMAIL_TEST_TO=reciever@example.com NODE_ENV=development +JWT_SECRET=mailmern2323 diff --git a/backend/src/controllers/userController.js b/backend/src/controllers/userController.js index b1dabb5..1da9780 100644 --- a/backend/src/controllers/userController.js +++ b/backend/src/controllers/userController.js @@ -39,9 +39,10 @@ exports.login = async (req, res) => { return res.status(401).json({ success: false, message: "Invalid email or password" }); } + const jwtSecret = process.env.JWT_SECRET || 'mailmern-secret'; const token = jwt.sign( { id: user._id, email: user.email }, - process.env.JWT_SECRET, + jwtSecret, { expiresIn: "1d" } ); @@ -55,37 +56,47 @@ exports.login = async (req, res) => { res.status(400).json({ success: false, error: err.message }); } }; -exports.sendOtp = async (req,res) => { +exports.sendOtp = async (req, res) => { try { - const {email} = req.body; - const user = await User.findOne({email}); - if (!user) return res.status(400).json({message:"User does not exist."}); + const { email } = req.body; + if (!email) { + return res.status(400).json({ success: false, message: "Email is required" }); + } + const user = await User.findOne({ email }); + if (!user) { + return res.status(400).json({ success: false, message: "User does not exist." }); + } const otp = Math.floor(1000 + Math.random() * 9000).toString(); // 4-digit OTP user.resetOtp = otp; - user.otpExpires = Date.now() + 5*60*1000; // 5 minutes + user.otpExpires = Date.now() + 5*60*1000; user.isOtpVerified = false; await user.save(); - await sendEmail - - + await sendEmail({ to: email, otp }); - - ({ to: email, otp }); // ✅ Correct usage - return res.status(200).json({message:"OTP sent successfully"}); + return res.status(200).json({ success: true, message: "OTP sent successfully" }); } catch (error) { - return res.status(500).json({message: `send otp error ${error}`}); - } + console.error("Send OTP error:", error); + return res.status(500).json({ success: false, message: `Failed to send OTP: ${error.message}` }); + } }; - -exports.verifyOtp = async (req,res) => { +exports.verifyOtp = async (req, res) => { try { - const {email, otp} = req.body; - const user = await User.findOne({email}); - if (!user || user.resetOtp !== otp || user.otpExpires < Date.now()) { - return res.status(400).json({message:"Invalid/expired OTP"}); + const { email, otp } = req.body; + if (!email || !otp) { + return res.status(400).json({ success: false, message: "Email and OTP are required" }); + } + const user = await User.findOne({ email }); + if (!user) { + return res.status(400).json({ success: false, message: "User does not exist" }); + } + if (!user.resetOtp || user.resetOtp !== otp) { + return res.status(400).json({ success: false, message: "Invalid OTP" }); + } + if (user.otpExpires < Date.now()) { + return res.status(400).json({ success: false, message: "OTP has expired" }); } user.isOtpVerified = true; @@ -93,28 +104,40 @@ exports.verifyOtp = async (req,res) => { user.otpExpires = undefined; await user.save(); - return res.status(200).json({message:"OTP verified successfully"}); + return res.status(200).json({ success: true, message: "OTP verified successfully" }); } catch (error) { - return res.status(500).json({message: `verify otp error ${error}`}); + console.error("Verify OTP error:", error); + return res.status(500).json({ success: false, message: `Failed to verify OTP: ${error.message}` }); } }; -exports.resetPassword = async (req,res) => { +exports.resetPassword = async (req, res) => { try { - const {email, newPassword} = req.body; - const user = await User.findOne({email}); - if (!user || !user.isOtpVerified) { - return res.status(400).json({message:"OTP verification required"}); + const { email, newPassword } = req.body; + if (!email || !newPassword) { + return res.status(400).json({ success: false, message: "Email and new password are required" }); + } + if (newPassword.length < 6) { + return res.status(400).json({ success: false, message: "Password must be at least 6 characters" }); + } + const user = await User.findOne({ email }); + if (!user) { + return res.status(400).json({ success: false, message: "User does not exist" }); + } + if (!user.isOtpVerified) { + return res.status(400).json({ success: false, message: "OTP verification required. Please verify OTP first." }); } const hashedPassword = await bcrypt.hash(newPassword, 10); user.password = hashedPassword; user.isOtpVerified = false; // reset verification + user.resetOtp = undefined; + user.otpExpires = undefined; await user.save(); - return res.status(200).json({message:"Password reset successfully"}); + return res.status(200).json({ success: true, message: "Password reset successfully" }); } catch (error) { - return res.status(500).json({message: `reset password error ${error}`}); + console.error("Reset password error:", error); + return res.status(500).json({ success: false, message: `Failed to reset password: ${error.message}` }); } -}; - +}; \ No newline at end of file diff --git a/backend/src/middlewares/authMiddleware.js b/backend/src/middlewares/authMiddleware.js index 5fe4386..e923d49 100644 --- a/backend/src/middlewares/authMiddleware.js +++ b/backend/src/middlewares/authMiddleware.js @@ -1,5 +1,56 @@ -// Simple auth middleware placeholder -module.exports = (req, res, next) => { - // In real app, validate JWT or session here - next(); +const jwt = require('jsonwebtoken'); +const User = require('../models/User'); + +//JWT Auth Middleware +const authMiddleware = async (req, res, next) => { + try { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + success: false, + message: 'No token provided. Authorization denied.' + }); + } + const token = authHeader.substring(7); + if (!token) { + return res.status(401).json({ + success: false, + message: 'No token provided. Authorization denied.' + }); + } + + try { + const jwtSecret = process.env.JWT_SECRET || 'mailmern-secret'; + const decoded = jwt.verify(token, jwtSecret); + const user = await User.findById(decoded.id).select('-password'); + + if (!user) { + return res.status(401).json({ + success: false, + message: 'Token is not valid. User not found.' + }); + } + req.user = user; + next(); + } catch (err) { + if (err.name === 'TokenExpiredError') { + return res.status(401).json({ + success: false, + message: 'Token expired. Please login again.' + }); + } + return res.status(401).json({ + success: false, + message: 'Token is not valid.' + }); + } + } catch (err) { + return res.status(500).json({ + success: false, + message: 'Server error in authentication' + }); + } }; + +module.exports = authMiddleware; \ No newline at end of file diff --git a/backend/src/middlewares/errorMiddleware.js b/backend/src/middlewares/errorMiddleware.js index 9243880..13d3889 100644 --- a/backend/src/middlewares/errorMiddleware.js +++ b/backend/src/middlewares/errorMiddleware.js @@ -1,14 +1,11 @@ - -export function errorMiddleware(err, req, res, next) { - - const statusCode = err.statusCode || 500; - const message = err.message || 'Internal Server Error'; - res.status(statusCode).json({ - success: false, - message, - stack: process.env.NODE_ENV === 'production' ? null : err.stack - - }); - next(); -} - +const errorMiddleware = (err, req, res, next) => { + const statusCode = err.statusCode || 500; + const message = err.message || 'Internal Server Error'; + + res.status(statusCode).json({ + success: false, + message, + ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }) + }); +}; +module.exports = { errorMiddleware }; diff --git a/backend/src/server.js b/backend/src/server.js index 012778e..dfe99cc 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -6,12 +6,16 @@ const userRoutes = require('./routes/userRoutes'); const { errorMiddleware } = require('./middlewares/errorMiddleware'); const chatbotRoutes = require('./routes/chatbotRoutes'); const emailRoutes = require('./routes/emailRoutes'); -const googleRoutes = require('./routes/googleRoute'); const trackRoutes = require('./routes/trackRoutes'); -const { configDotenv } = require('dotenv'); const contactRoutes = require('./routes/contactRoutes'); const googleRoutes = require('./routes/googleRoute'); const app = express(); + +if (!process.env.JWT_SECRET) { + process.env.JWT_SECRET = 'mailmern-dev-secret-key-change-in-production-' + Date.now(); + console.warn('WARNING: JWT_SECRET not found in environment variables. Using default development secret.'); + console.warn('Please set JWT_SECRET in your .env file for production use!'); +} app.use( cors({ origin: ["http://localhost:5173"], @@ -31,12 +35,13 @@ app.use('/api/contacts',contactRoutes); app.use("/api/google-calendar", googleRoutes); app.use('/api/track', trackRoutes); -const PORT = process.env.PORT || 5000; - +// 404 handler app.use('*', (req, res) => { res.status(404).json({ success: false, message: 'Route not found' }); }); +app.use(errorMiddleware); +const PORT = process.env.PORT || 5000; const start = async () => { try { await connectDB(); @@ -47,5 +52,4 @@ const start = async () => { }; start(); -app.use(errorMiddleware); module.exports = app; diff --git a/backend/src/services/emailService.js b/backend/src/services/emailService.js index e9e6453..217f835 100644 --- a/backend/src/services/emailService.js +++ b/backend/src/services/emailService.js @@ -3,11 +3,11 @@ const EmailLog = require('../models/EmailLog'); // import your model const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST || 'smtp.gmail.com', - port: process.env.SMTP_PORT || 465, - secure: true, + port: parseInt(process.env.SMTP_PORT || '587'), + secure: process.env.SMTP_SECURE === 'true' || false, auth: { - user: process.env.EMAIL, - pass: process.env.PASS, + user: process.env.SMTP_USER || process.env.EMAIL_USER || process.env.EMAIL, + pass: process.env.SMTP_PASS || process.env.EMAIL_PASS || process.env.PASS, }, }); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f4a5c42..6a6ae19 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -15,7 +15,8 @@ import TemplateBuilder from "./pages/Campaign"; import { AuthProvider } from "./context/AuthContext"; import ForgotPassword from "./pages/Forgotpassword"; import Contacts from "./pages/Contact"; -import BulkEmail from "./pages/BulkEmail"; +import BulkEmail from "./pages/BulkEmail"; +import ProtectedRoute from "./components/ProtectedRoute"; export default function App() { return ( @@ -27,14 +28,50 @@ export default function App() { } /> - } /> - } /> } /> } /> - } /> - }/> - }/> - }/> + } /> + {/* Protected Routes */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> } /> diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 52fe941..606c8d8 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -1,23 +1,37 @@ import React, { useState } from "react"; -import { Link, useLocation } from "react-router-dom"; -import { Menu, X } from "lucide-react"; +import { Link, useLocation, useNavigate } from "react-router-dom"; +import { Menu, X, LogOut } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; import { Mail } from "lucide-react"; +import { useAuth } from "../context/AuthContext"; +import toast from "react-hot-toast"; + export default function Navbar() { const [isOpen, setIsOpen] = useState(false); const location = useLocation(); + const { isAuthenticated, user, logout } = useAuth(); + const navigate = useNavigate(); + const handleLogout = () => { + logout(); + toast.success("Logged out successfully"); + navigate("/"); + }; + const publicLinks = [ + { name: "Home", path: "/" }, + { name: "Login", path: "/login" }, + { name: "Register", path: "/register" }, + ]; - const links = [ + const authenticatedLinks = [ { name: "Home", path: "/" }, { name: "Dashboard", path: "/dashboard" }, { name: "Chatbot", path: "/chatbot" }, { name: "Bulk Email", path: "/bulk-email" }, - { name: "Login", path: "/login" }, - { name: "Register", path: "/register" }, - { name:"Email Builder", path:"/builder"}, - {name:"Contacts", path:"/contacts"} + { name: "Email Builder", path: "/builder" }, + { name: "Contacts", path: "/contacts" }, ]; + const links = isAuthenticated ? authenticatedLinks : publicLinks; return ( {/* Desktop Links */} -
+
{links.map((link) => ( ))} + {isAuthenticated && ( + <> + {user?.name || user?.email} + + + )}
{/* Mobile Toggle */} @@ -88,6 +114,21 @@ export default function Navbar() { {link.name} ))} + {isAuthenticated && ( + <> +
{user?.name || user?.email}
+ + + )}
)} diff --git a/frontend/src/components/ProtectedRoute.jsx b/frontend/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..a3305b8 --- /dev/null +++ b/frontend/src/components/ProtectedRoute.jsx @@ -0,0 +1,20 @@ +import { Navigate } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import { ClipLoader } from "react-spinners"; + +const ProtectedRoute = ({ children }) => { + const { isAuthenticated, loading } = useAuth(); + if (loading) { + return ( +
+ +
+ ); + } + if (!isAuthenticated) { + return ; + } + return children; +}; +export default ProtectedRoute; + diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index cba22b3..34c16a5 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -1,15 +1,45 @@ -import React, { createContext, useState, useContext } from "react"; +import React, { createContext, useState, useContext, useEffect } from "react"; const AuthContext = createContext(); export function AuthProvider({ children }) { const [user, setUser] = useState(null); + const [token, setToken] = useState(null); + const [loading, setLoading] = useState(true); - const login = (email) => setUser({ email }); - const logout = () => setUser(null); + useEffect(() => { + const storedToken = localStorage.getItem("token"); + const storedUser = localStorage.getItem("user"); + + if (storedToken && storedUser) { + try { + setToken(storedToken); + setUser(JSON.parse(storedUser)); + } catch (error) { + console.error("Error parsing stored user data:", error); + localStorage.removeItem("token"); + localStorage.removeItem("user"); + } + } + setLoading(false); + }, []); + + const login = (token, userData) => { + setToken(token); + setUser(userData); + localStorage.setItem("token", token); + localStorage.setItem("user", JSON.stringify(userData)); + }; + const logout = () => { + setToken(null); + setUser(null); + localStorage.removeItem("token"); + localStorage.removeItem("user"); + }; + const isAuthenticated = !!token && !!user; return ( - + {children} ); diff --git a/frontend/src/pages/Forgotpassword.jsx b/frontend/src/pages/Forgotpassword.jsx index 8e8417a..39e0632 100644 --- a/frontend/src/pages/Forgotpassword.jsx +++ b/frontend/src/pages/Forgotpassword.jsx @@ -3,6 +3,7 @@ import { IoIosArrowRoundBack } from "react-icons/io"; import { useNavigate } from "react-router-dom"; import { ClipLoader } from "react-spinners"; import { sendOtp, verifyOtp, resetPassword } from "../services/authService"; +import toast from "react-hot-toast"; const ForgotPassword = () => { const [step, setStep] = useState(1); @@ -15,45 +16,85 @@ const ForgotPassword = () => { const navigate = useNavigate(); - const handleSendOtp = async () => { + const handleSendOtp = async (e) => { + e?.preventDefault(); + if (!email) { + setErr("Email is required"); + return; + } setLoading(true); + setErr(""); try { - await sendOtp(email); - setErr(""); - setStep(2); + const result = await sendOtp(email); + if (result.success || result.message) { + toast.success("OTP sent to your email!"); + setStep(2); + } else { + setErr(result.message || "Failed to send OTP"); + } } catch (error) { - setErr(error?.response?.data?.message || "Failed to send OTP"); + const errorMessage = error?.response?.data?.message || "Failed to send OTP"; + setErr(errorMessage); + toast.error(errorMessage); } finally { setLoading(false); } }; - const handleVerifyOtp = async () => { + const handleVerifyOtp = async (e) => { + e?.preventDefault(); + if (!otp) { + setErr("OTP is required"); + return; + } setLoading(true); + setErr(""); try { - await verifyOtp(email, otp); - setErr(""); - setStep(3); + const result = await verifyOtp(email, otp); + if (result.success || result.message) { + toast.success("OTP verified successfully!"); + setStep(3); + } else { + setErr(result.message || "OTP verification failed"); + } } catch (error) { - setErr(error?.response?.data?.message || "OTP verification failed"); + const errorMessage = error?.response?.data?.message || "OTP verification failed"; + setErr(errorMessage); + toast.error(errorMessage); } finally { setLoading(false); } }; - const handleResetPassword = async () => { + const handleResetPassword = async (e) => { + e?.preventDefault(); + if (!newPassword || !confirmPassword) { + setErr("Both password fields are required"); + return; + } + if (newPassword.length < 6) { + setErr("Password must be at least 6 characters"); + return; + } if (newPassword !== confirmPassword) { setErr("Passwords do not match"); return; } setLoading(true); + setErr(""); try { - await resetPassword(email, newPassword); - setErr(""); - navigate("/signin"); + const result = await resetPassword(email, newPassword); + if (result.success || result.message) { + toast.success("Password reset successfully! Please login."); + navigate("/login"); + } else { + setErr(result.message || "Password reset failed"); + } } catch (error) { - setErr(error?.response?.data?.message || "Password reset failed"); + const errorMessage = error?.response?.data?.message || "Password reset failed"; + setErr(errorMessage); + toast.error(errorMessage); } finally { setLoading(false); } @@ -72,74 +113,92 @@ const ForgotPassword = () => { {step === 1 && ( -
+
setEmail(e.target.value)} + required /> {err &&

{err}

} -
+ )} {step === 2 && ( -
+
setOtp(e.target.value)} + onChange={(e) => setOtp(e.target.value.replace(/\D/g, ''))} + required /> {err &&

{err}

} -
+ + )} {step === 3 && ( -
+
setNewPassword(e.target.value)} + required + minLength="6" /> setConfirmPassword(e.target.value)} + required + minLength="6" /> {err &&

{err}

} -
+ )} diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 60b1a7d..2856096 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -1,8 +1,10 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { FaRegEye, FaRegEyeSlash } from "react-icons/fa"; import { useNavigate } from "react-router-dom"; import { ClipLoader } from "react-spinners"; import { loginUser } from "../services/authService"; +import { useAuth } from "../context/AuthContext"; +import toast from "react-hot-toast"; const Login = () => { const [email, setEmail] = useState(""); @@ -11,6 +13,19 @@ const Login = () => { const [err, setErr] = useState(""); const [loading, setLoading] = useState(false); const navigate = useNavigate(); + const { login, isAuthenticated, loading: authLoading } = useAuth(); + useEffect(() => { + if (!authLoading && isAuthenticated) { + navigate("/dashboard"); + } + }, [isAuthenticated, authLoading, navigate]); + if (authLoading) { + return ( +
+ +
+ ); + } const handleLogin = async (e) => { e.preventDefault(); @@ -18,11 +33,18 @@ const Login = () => { setLoading(true); try { const result = await loginUser({ email, password }); - console.log(result.data); + if (result.success && result.token && result.user) { + login(result.token, result.user); + toast.success("Login successful!"); + navigate("/dashboard"); + } else { + setErr(result.message || "Login failed. Please try again."); + } setLoading(false); - navigate("/"); // Redirect on success } catch (error) { - setErr(error?.response?.data?.message || error.message || "Login failed."); + const errorMessage = error?.response?.data?.message || error?.response?.data?.error || error.message || "Login failed."; + setErr(errorMessage); + toast.error(errorMessage); setLoading(false); } }; diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx index 9b3865f..43f9bbc 100644 --- a/frontend/src/pages/Register.jsx +++ b/frontend/src/pages/Register.jsx @@ -1,9 +1,11 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { FaRegEye, FaRegEyeSlash } from "react-icons/fa"; import { useNavigate } from "react-router-dom"; import { ClipLoader } from "react-spinners"; import { registerUser } from "../services/authService"; +import toast from "react-hot-toast"; +import { useAuth } from "../context/AuthContext"; const Register = () => { const [fullname, setFullname] = useState(""); @@ -13,6 +15,19 @@ const Register = () => { const [err, setErr] = useState(""); const [loading, setLoading] = useState(false); const navigate = useNavigate(); + const { isAuthenticated, loading: authLoading } = useAuth(); + useEffect(() => { + if (!authLoading && isAuthenticated) { + navigate("/dashboard"); + } + }, [isAuthenticated, authLoading, navigate]); + if (authLoading) { + return ( +
+ +
+ ); + } const handleSubmit = async (e) => { e.preventDefault(); @@ -21,14 +36,25 @@ const Register = () => { setErr("All fields are required"); return; } + if (password.length < 6) { + setErr("Password must be at least 6 characters"); + return; + } setLoading(true); try { - await registerUser({ fullname, email, password }); + const result = await registerUser({ fullname, email, password }); + if (result.success || result.data) { + toast.success("Registration successful! Please login."); + navigate("/login"); + } else { + setErr(result.message || "Registration failed. Please try again."); + } setLoading(false); - navigate("/login"); } catch (error) { - setErr(error?.response?.data?.message || error.message || "Registration failed."); + const errorMessage = error?.response?.data?.message || error?.response?.data?.error || error.message || "Registration failed."; + setErr(errorMessage); + toast.error(errorMessage); setLoading(false); } }; diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 5d0eec6..ce93a94 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -71,10 +71,31 @@ const api = axios.create({ }, }); +// Request interceptor +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem("token"); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); +// Response interceptor api.interceptors.response.use( (response) => response, (error) => { console.error("API Error:", error.response || error); + if (error.response?.status === 401) { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + if (window.location.pathname !== "/login") { + window.location.href = "/login"; + } + } return Promise.reject(error); } );