diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..5006a5b --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,10 @@ +MONGODB_URI=mongodb://localhost:27017/classsync +PORT=3000 + +JWT_SECRET="yoursecret" +GROQ_API_KEY="" + +GOOGLE_MAILID= +GOOGLE_APP_PASSWORD= + +CORS_ORIGIN=http://localhost:5173 diff --git a/backend/package-lock.json b/backend/package-lock.json index 9e9bd40..8da3c1d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,13 +10,14 @@ "license": "ISC", "dependencies": { "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.15.1", "node-cron": "^4.1.1", - "nodemailer": "^7.0.3", + "nodemailer": "^7.0.6", "openai": "^5.8.2" }, "devDependencies": { @@ -2182,6 +2183,25 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -4825,9 +4845,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz", - "integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.6.tgz", + "integrity": "sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" diff --git a/backend/package.json b/backend/package.json index 1b57385..eb50654 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,13 +13,14 @@ "type": "commonjs", "dependencies": { "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.15.1", "node-cron": "^4.1.1", - "nodemailer": "^7.0.3", + "nodemailer": "^7.0.6", "openai": "^5.8.2" }, "devDependencies": { diff --git a/backend/src/app.js b/backend/src/app.js index 4f0c7d4..39c55e6 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -3,6 +3,7 @@ require('dotenv').config(); const express = require('express'); const cors = require('cors'); const connectDB = require('./config/db'); +const cookieParser = require('cookie-parser'); const app = express(); connectDB(); @@ -16,6 +17,7 @@ const corsOptions = { app.use(cors(corsOptions)); app.use(express.json()); +app.use(cookieParser()); const chatbotRoutes = require('./routes/chatbotRoutes'); app.use('/api/chatbot', chatbotRoutes); diff --git a/backend/src/controllers/authController.js b/backend/src/controllers/authController.js index 1d47ed2..7caa200 100644 --- a/backend/src/controllers/authController.js +++ b/backend/src/controllers/authController.js @@ -1,6 +1,15 @@ -const User = require('../models/User'); -const bcrypt = require('bcrypt'); -const jwt = require('jsonwebtoken'); +const User = require("../models/User"); +const bcrypt = require("bcrypt"); +const jwt = require("jsonwebtoken"); +const nodemailer = require("nodemailer"); + +const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.GOOGLE_MAILID, + pass: process.env.GOOGLE_APP_PASSWORD, + }, +}); // Register new user exports.register = async (req, res) => { @@ -8,12 +17,12 @@ exports.register = async (req, res) => { const { name, email, password, role, schoolId } = req.body; if (!schoolId) { - return res.status(400).json({ message: 'schoolId is required' }); + return res.status(400).json({ message: "schoolId is required" }); } // Check if user exists let user = await User.findOne({ email }); - if (user) return res.status(400).json({ message: 'User already exists' }); + if (user) return res.status(400).json({ message: "User already exists" }); // Hash password const salt = await bcrypt.genSalt(10); @@ -30,13 +39,20 @@ exports.register = async (req, res) => { await user.save(); // Create JWT token - const payload = { userId: user._id, role: user.role, email: user.email, name: user.name }; - const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1d' }); + const payload = { + userId: user._id, + role: user.role, + email: user.email, + name: user.name, + }; + const token = jwt.sign(payload, process.env.JWT_SECRET, { + expiresIn: "1d", + }); res.status(201).json({ token }); } catch (err) { console.error(err.message); - res.status(500).send('Server error'); + res.status(500).send("Server error"); } }; @@ -47,23 +63,129 @@ exports.login = async (req, res) => { // Find user const user = await User.findOne({ email }); - if (!user) return res.status(400).json({ message: 'Invalid credentials' }); + if (!user) return res.status(400).json({ message: "Invalid credentials" }); // Check password const isMatch = await bcrypt.compare(password, user.password); - if (!isMatch) return res.status(400).json({ message: 'Invalid credentials' }); + if (!isMatch) + return res.status(400).json({ message: "Invalid credentials" }); // Create JWT token - const payload = { userId: user._id, role: user.role, email: user.email, name: user.name }; - const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1d' }); + const payload = { + userId: user._id, + role: user.role, + email: user.email, + name: user.name, + }; + const token = jwt.sign(payload, process.env.JWT_SECRET, { + expiresIn: "1d", + }); res.json({ token }); } catch (err) { console.error(err.message); - res.status(500).send('Server error'); + res.status(500).send("Server error"); + } +}; + +exports.sendotp = async (req, res) => { + try { + console.log("triggered"); + const { email } = req.body; + const user = await User.findOne({ email }); + + if (!user) return res.status(400).json({ message: "No such user found." }); + + const otp = Math.floor(100000 + Math.random() * 900000); + + const mailOptions = { + from: process.env.GOOGLE_MAILID, + to: email, + subject: "OTP for password change request", + html: `

Dear User,

+

Your OTP for the password change request is ${otp}.

+

Please do not share this OTP with anyone to keep your account secure.

+

Thank you,
The Support Team,ClassSync

`, + }; + + const otpToken = jwt.sign({ otp, email }, process.env.JWT_SECRET, { + expiresIn: "5m", + }); + + res.cookie('otpToken',otpToken,{ + httpOnly: true, + maxAge: 5 * 60 * 1000 + }); + + await transporter.sendMail(mailOptions); + console.log("sent"); + return res.status(200).json({message:'Otp sent'}); + } catch (err) { + console.error(err.message); + res.status(500).send("Server error"); + } +}; + +exports.verifyotp = async (req, res) => { + try { + const { otp } = req.body; + const otpToken = req.cookies.otpToken; + console.log("otpToken:", otpToken); + console.log("here"); + const decoded = jwt.verify(otpToken, process.env.JWT_SECRET); + console.log("after"); + if (otp == decoded.otp) { + return res.status(200).json({ message: "OTP verified successfully" }); + } else { + return res.status(400).json({ message: "Wrong Otp" }); + } + } catch (error) { + if (error.name === "TokenExpiredError") { + return res + .status(400) + .json({ message: "OTP expired. Please request a new one." }); + } + console.error("yahai pe"); + res.status(500).send("Server error"); } }; +exports.updatepassword = async (req,res) => { + try { + const {email, password, confirmPassword} = req.body; + + let user = await User.findOne({ email }); + if (!user) return res.status(400).json({ message: "User does not exists" }); + + if(password!=confirmPassword) + return res.status(400).json({message:'Passwords do not match.'}); + + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + + user.password=hashedPassword; + await user.save(); + + const mailOptions = { + from: process.env.GOOGLE_MAILID, + to: email, + subject: "OTP for password change request", + html: `

Dear User,

+

Your password has been successfully updated..

+

Please contact if you face any login issues.

+

Thank you,
The Support Team,ClassSync

`, + }; + + await transporter.sendMail(mailOptions); + res.clearCookie('otptoken'); + + return res.status(200).json({message:'Password updated successfully.'}); + } catch (error) { + console.error(error.message); + res.status(500).send("Server Error"); + } +} + //This code defines the authentication controller for user registration and login in an Express application. // It includes functions to register a new user and log in an existing user, handling password hashing and JWT token generation. // The `register` function checks if a user already exists, hashes the password, creates a new user, and returns a JWT token. @@ -71,4 +193,4 @@ exports.login = async (req, res) => { // The controller uses Mongoose for database operations, bcrypt for password hashing, and jsonwebtoken for token generation. // The `register` function handles user registration, including password hashing and JWT token creation. // The `login` function handles user login, verifying credentials and generating a JWT token. -// The controller responds with appropriate status codes and messages for success and error cases. \ No newline at end of file +// The controller responds with appropriate status codes and messages for success and error cases. diff --git a/backend/src/routes/authRoutes.js b/backend/src/routes/authRoutes.js index bef78a4..ee20e4f 100644 --- a/backend/src/routes/authRoutes.js +++ b/backend/src/routes/authRoutes.js @@ -1,10 +1,14 @@ const express = require('express'); const router = express.Router(); -const { register, login } = require('../controllers/authController'); +const { register, login, sendotp, verifyotp, updatepassword } = require('../controllers/authController'); router.post('/register', register); router.post('/login', login); +router.post('/sendotp',sendotp); +router.post('/verifyotp',verifyotp); +router.put('/updatepassword',updatepassword); + module.exports = router; // This code defines the authentication routes for user registration and login. // It imports the necessary modules, sets up the routes, and exports the router for use in the main application file. diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..5317fce --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:3000 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 02911cd..54b857b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,7 +34,7 @@ "globals": "^16.0.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.1", - "vite": "^6.3.5" + "vite": "^6.3.6" } }, "node_modules/@alloc/quick-lru": { @@ -4948,9 +4948,9 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index 829705c..4bd17fe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,6 @@ "globals": "^16.0.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.1", - "vite": "^6.3.5" + "vite": "^6.3.6" } } diff --git a/frontend/src/pages/auth/Login.jsx b/frontend/src/pages/auth/Login.jsx index 2f0f4e5..94db491 100644 --- a/frontend/src/pages/auth/Login.jsx +++ b/frontend/src/pages/auth/Login.jsx @@ -1,10 +1,8 @@ -import React, { useState } from 'react'; -import { Mail, Lock, GraduationCap, Shield } from 'lucide-react'; -import api from '../../utils/api'; -import { useNavigate } from 'react-router-dom'; -import { useAuth } from '../../context/AuthContext'; -import logo from '../../logo.svg'; - +import React, { useState } from "react"; +import { Mail, Lock, GraduationCap, Shield, KeyRound} from "lucide-react"; +import api from "../../utils/api"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "../../context/AuthContext"; const demoUsers = [ { @@ -24,41 +22,119 @@ const demoUsers = [ ]; const Login = () => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); const [isLoading, setIsLoading] = useState(false); const [selectedRole, setSelectedRole] = useState(null); const { login } = useAuth(); const navigate = useNavigate(); + const [page, setPage] = useState(0); + const [otp, setOtp] = useState(); + const [confirmPassword,setConfirmPassword] = useState(""); + const handleSubmit = async (e) => { e.preventDefault(); - setError(''); + setError(""); setIsLoading(true); try { - const response = await api.post('/api/auth/login', { + const response = await api.post("/api/auth/login", { email, password, }); const userRole = login(response.data.token); - if (userRole === 'admin') { - navigate('/admin/dashboard'); + if (userRole === "admin") { + navigate("/admin/dashboard"); } else { - navigate('/teacher/dashboard'); + navigate("/teacher/dashboard"); } - } catch (err) { - console.error('Login failed:', err.response?.data?.message || err.message); + console.error( + "Login failed:", + err.response?.data?.message || err.message + ); setError(err.response?.data?.message || 'An unexpected error occurred. Please try again.'); } finally { setIsLoading(false); } }; + const handleforgotflow = async () => { + setPage(1); + console.log("Opened modal for email"); + }; + + const handleforgotPassword = async () => { + console.log("Sending otp"); + try { + const response = await api.post("/api/auth/sendotp", { email },{ + withCredentials: true + }); + if (response.status == 200) { + setPage(2); + } else setError(response.data.message); + } catch (err) { + console.error( + "Could not send Otp:", + err.response?.data?.message || err.message + ); + setError( + err.response?.data?.message || + "An unexpected error occurred. Please try again." + ); + } + }; + + const handlesubmitotp = async () => { + console.log("verifying"); + try { + const response = await api.post("/api/auth/verifyotp", { otp },{ + withCredentials:true + }); + setError(response.data.message); + if(response.status == 200){ + setPage(3); + console.log("otp verified"); + } + } catch (err) { + console.error( + "Could not verify:", + err.response?.data?.message || err.message + ); + setError( + err.response?.data?.message || + "An unexpected error occurred. Please try again." + ); + } + }; + + const handleUpdatePass = async (req,res) => { + console.log("updating"); + try { + const response = await api.put('/api/auth/updatepassword',{email,password,confirmPassword}); + setError(response.data.message); + if(response.status == 200){ + setTimeout(setPage(0),3000); + setEmail(""); + setPassword(""); + setError(""); + } + } catch (err) { + console.error( + "Could not update password:", + err.response?.data?.message || err.message + ); + setError( + err.response?.data?.message || + "An unexpected error occurred. Please try again." + ); + } + } + const handleRoleSelect = (role) => { setSelectedRole(role); const user = demoUsers.find((u) => u.role === role); @@ -66,7 +142,7 @@ const Login = () => { setEmail(user.email); setPassword(user.password); } - setError(''); // Clear any existing errors when selecting demo user + setError(""); // Clear any existing errors when selecting demo user }; return ( @@ -74,39 +150,49 @@ const Login = () => {
- App Logo -
+ App Logo +
{/* Header */}
-

Welcome Back

-

Sign in to your account

+

+ Welcome Back +

+ {page == 0 && ( +

Sign in to your account

+ )}
{/* Demo User Selection */} -
-

Login as Demo User

-
- {demoUsers.map((user) => ( - - ))} + {page == 0 && ( +
+

+ Login as Demo User +

+
+ {demoUsers.map((user) => ( + + ))} +
-
+ )} {/* Error Message */} {error && ( @@ -116,7 +202,18 @@ const Login = () => { )} {/* Login Form */} -
+ { + e.preventDefault(); + + if (page == 0) handleSubmit(); + else if (page == 1) handleforgotPassword(); + else if (page == 2) handlesubmitotp(); + else handleUpdatePass(); + + }} + className="space-y-4 sm:space-y-5" + > {/* Email Field */}
@@ -128,28 +225,79 @@ const Login = () => { onChange={(e) => setEmail(e.target.value)} className="w-full pl-10 pr-4 h-11 sm:h-12 border border-gray-200 rounded-lg sm:rounded-xl focus:border-indigo-600 focus:ring-2 focus:ring-indigo-100 focus:outline-none transition-all duration-200 text-sm sm:text-base" required - disabled={isLoading} + disabled={isLoading || page==2 || page==3} />

Email

{/* Password Field */} -
-
- - setPassword(e.target.value)} - className="w-full pl-10 pr-4 h-11 sm:h-12 border border-gray-200 rounded-lg sm:rounded-xl focus:border-indigo-600 focus:ring-2 focus:ring-indigo-100 focus:outline-none transition-all duration-200 text-sm sm:text-base" - required - disabled={isLoading} - /> + {(page == 0 || page==3) && ( +
+
+ + setPassword(e.target.value)} + className="w-full pl-10 pr-4 h-11 sm:h-12 border border-gray-200 rounded-lg sm:rounded-xl focus:border-indigo-600 focus:ring-2 focus:ring-indigo-100 focus:outline-none transition-all duration-200 text-sm sm:text-base" + required + disabled={isLoading} + /> +
+

Password

-

Password

-
+ )} + + {page==3 && ( +
+
+ + setConfirmPassword(e.target.value)} + className="w-full pl-10 pr-4 h-11 sm:h-12 border border-gray-200 rounded-lg sm:rounded-xl focus:border-indigo-600 focus:ring-2 focus:ring-indigo-100 focus:outline-none transition-all duration-200 text-sm sm:text-base" + required + disabled={isLoading} + /> +
+

Confirm Password

+
+ )} + + {page == 2 && ( +
+
+ + setOtp(e.target.value)} + className="w-full pl-10 pr-4 h-11 sm:h-12 border border-gray-200 rounded-lg sm:rounded-xl focus:border-indigo-600 focus:ring-2 focus:ring-indigo-100 focus:outline-none transition-all duration-200 text-sm sm:text-base" + required + disabled={isLoading} + /> +
+

Enter OTP

+
+ )} + + {page == 0 && ( +
+ Forgot Password +
+ )} + + {/* {page==0 &&
+ +
} */} {/* Submit Button */}
- ) : ( - "Sign in" - )} + ) : page == 0 ? "Sign in": page == 1 ? "Send OTP": page== 2? "Submit OTP": "Update Password" + }
{/* Additional Info for Demo */} -
-

- Select a demo user above or use your credentials -

-
+ {page == 0 && ( +
+

+ Select a demo user above or use your credentials +

+
+ )}
@@ -203,4 +352,4 @@ const Login = () => { ); }; -export default Login; \ No newline at end of file +export default Login;