diff --git a/backend/.env.example b/backend/.env.example index 7b62781..f9e24b6 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -9,3 +9,5 @@ GOOGLE_CLIENT_ID=your_google_client_id VAPID_PUBLIC_KEY=your_vapid_public_key VAPID_PRIVATE_KEY=your_vapid_private_key VAPID_SUBJECT=mailto:your@email.com +EMAIL=your_email@gmail.com +EMAIL_PASSWORD=your_google_email_password \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 1ff3988..1f6adb4 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -20,6 +20,7 @@ "helmet": "^8.1.0", "jsonwebtoken": "^9.0.3", "mongoose": "^9.6.1", + "nodemailer": "^8.0.11", "socket.io": "^4.8.3", "web-push": "^3.6.7", "xss": "^1.0.15" @@ -1470,6 +1471,15 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/nodemailer": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.11.tgz", + "integrity": "sha512-nrO/pDAUKl+wXX+lx16tDLbnm0fW6sK/x8mgohaCpg+CdCEl482bD4tCuAZk2DyliruiNTIZxRCoWkDqJEnAiA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.14", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", diff --git a/backend/package.json b/backend/package.json index 4996ced..a5dc163 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,6 +23,7 @@ "helmet": "^8.1.0", "jsonwebtoken": "^9.0.3", "mongoose": "^9.6.1", + "nodemailer": "^8.0.11", "socket.io": "^4.8.3", "web-push": "^3.6.7", "xss": "^1.0.15" diff --git a/backend/src/controllers/auth.controller.js b/backend/src/controllers/auth.controller.js index 9abeb65..f43d5f6 100644 --- a/backend/src/controllers/auth.controller.js +++ b/backend/src/controllers/auth.controller.js @@ -3,6 +3,7 @@ import { OAuth2Client } from "google-auth-library"; import User from "../models/user.model.js"; import { generateTokenAndSetCookie, catchAsync } from "../lib/utils.js"; import cloudinary from "../lib/cloudinary.js"; +import transporter from "../utils/nodemailer.js" let googleClient; @@ -247,4 +248,95 @@ export const subscribeToPush = catchAsync(async (req, res) => { }).select("-password -__v"); res.status(200).json({ message: "Push subscription saved" }); -}); \ No newline at end of file + } catch (err) { + console.error("subscribeToPush:", err.message); + res.status(500).json({ message: "Could not save push subscription" }); + } +} + +export const emailVerification = async (req, res) => { + try { + const user = await User.findById(req.userId); + + if (!user) { + return res.status(404).json({ + message: "User not found", + }); + } + + if (user.isVerified) { + return res.status(400).json({ + message: "Email already verified", + }); + } + + const otp = Math.floor( + 100000 + Math.random() * 900000 + ).toString(); + + user.otp = otp; + user.otpExpires = Date.now() + 5 * 60 * 1000; + + await user.save(); + + await transporter.sendMail({ + from: process.env.EMAIL, + to: user.email, + subject: "Email Verification OTP", + html: `
Your OTP is: ${otp}
`, + }); + + res.status(200).json({ + message: "OTP sent successfully", + }); + + } catch (error) { + console.log(error); + res.status(500).json({ + message: "Failed to send OTP", + }); + } +}; + +export const verifyOtp = async (req, res) => { + try { + const { otp } = req.body; + + const user = await User.findById(req.userId); + + if (!user) { + return res.status(404).json({ + message: "User not found", + }); + } + + if (user.otp !== otp) { + return res.status(400).json({ + message: "Invalid OTP", + }); + } + + if (user.otpExpires < Date.now()) { + return res.status(400).json({ + message: "OTP expired", + }); + } + + user.isVerified = true; + user.otp = null; + user.otpExpires = null; + + await user.save(); + + res.status(200).json({ + message: "Email verified successfully", + }); + + } catch (error) { + res.status(500).json({ + message: "Verification failed", + }); + } +}; +}); diff --git a/backend/src/models/user.model.js b/backend/src/models/user.model.js index 5f6aead..11b7a7c 100644 --- a/backend/src/models/user.model.js +++ b/backend/src/models/user.model.js @@ -33,25 +33,16 @@ const userSchema = new mongoose.Schema({ type: mongoose.Schema.Types.Mixed, default: null, }, + isVerified: { + type: Boolean, + default: false + }, lastSeen: { type: Date, default: Date.now, }, - statusMood: { - type: String, - enum: [ - "coding", - "coffee_break", - "studying", - "gaming", - "working", - "sleeping", - "music", - "away", - ], - default: null, - trim: true, - }, + otp: String, + otpExpires: Date }, { timestamps: true }); userSchema.index({ name: "text" }); diff --git a/backend/src/routes/auth.route.js b/backend/src/routes/auth.route.js index 3db8be9..2dc89a9 100644 --- a/backend/src/routes/auth.route.js +++ b/backend/src/routes/auth.route.js @@ -1,7 +1,7 @@ import express from "express"; import protectRoute from "../middleware/auth.middleware.js"; import { validateSignup, validateLogin } from "../middleware/validate.js"; -import { signup, login, logout, googleAuth, updateProfile, updateProfilePicture, checkAuth, subscribeToPush } from "../controllers/auth.controller.js"; +import { signup, login, logout, googleAuth, updateProfile, updateProfilePicture, checkAuth, subscribeToPush, emailVerification, verifyOtp } from "../controllers/auth.controller.js"; const router = express.Router(); @@ -13,5 +13,7 @@ router.put("/update-profile", protectRoute, updateProfile); router.put("/update-profile-picture", protectRoute, updateProfilePicture); router.get("/check", protectRoute, checkAuth); router.post("/push-subscribe", protectRoute, subscribeToPush); +router.post("/send-verification-email-otp",protectRoute, emailVerification); +router.post("/Verified-account",protectRoute, verifyOtp); export default router; \ No newline at end of file diff --git a/backend/src/utils/nodemailer.js b/backend/src/utils/nodemailer.js new file mode 100644 index 0000000..9977241 --- /dev/null +++ b/backend/src/utils/nodemailer.js @@ -0,0 +1,11 @@ +import nodemailer from 'nodemailer' + +const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASSWORD + } +}); + +export default transporter \ No newline at end of file diff --git a/frontend/pages/ProfilePage.jsx b/frontend/pages/ProfilePage.jsx index 1166e20..1334533 100644 --- a/frontend/pages/ProfilePage.jsx +++ b/frontend/pages/ProfilePage.jsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react" import { Camera, Pencil } from "lucide-react" import toast from "react-hot-toast" import useAuthStore from "../src/store/useAuthStore" -import StatusMoodSelector from "../components/StatusMoodSelector" +import { useNavigate } from "react-router-dom"; export default function ProfilePage() { const { authUser: user, updateProfile, updateProfilePicture, isLoading } = useAuthStore() @@ -12,21 +12,8 @@ export default function ProfilePage() { const [previewImage, setPreviewImage] = useState(user?.profilePicture || null) const [selectedFile, setSelectedFile] = useState(null) const [isEditing, setIsEditing] = useState(false) - const [selectedMood, setSelectedMood] = useState(user?.statusMood || null) - - useEffect(() => { - setSelectedMood(user?.statusMood || null) - }, [user?.statusMood]) - - const handleMoodChange = async (mood) => { - const previousMood = selectedMood - setSelectedMood(mood) - try { - await updateStatusMood(mood) - } catch { - setSelectedMood(previousMood) - } - } + const [isEmailOtp, SetIsEmailOtp] = useState(false); + const navigate = useNavigate(); const handleFileChange = (e) => { const file = e.target.files[0] @@ -121,6 +108,14 @@ export default function ProfilePage() { Edit ) : null} + {!user?.isVerified && ( + +)} diff --git a/frontend/pages/SendVerifyEmailOtp.jsx b/frontend/pages/SendVerifyEmailOtp.jsx new file mode 100644 index 0000000..3a16d86 --- /dev/null +++ b/frontend/pages/SendVerifyEmailOtp.jsx @@ -0,0 +1,47 @@ +import { useState } from "react"; +import axios from "axios"; +import toast from "react-hot-toast"; +import { useNavigate } from "react-router-dom"; + +function SendVerifyEmailOtp() { +const [loading, setLoading] = useState(false); +const navigate = useNavigate(); + +const handleSendOtp = async () => { +try { +setLoading(true); + const res = await axios.post( + "http://localhost:5001/api/auth/send-verification-email-otp", + {}, + { withCredentials: true } + ); + + toast.success(res.data.message); + + navigate("/verify-email"); +} catch (error) { + toast.error( + error.response?.data?.message || + "Failed to send OTP" + ); +} finally { + setLoading(false); +} + +}; + +return (+ Enter the OTP sent to +
+ + + + +