diff --git a/initdb/schema.sql b/initdb/schema.sql index 21ea3825609294f1dac2eaf526906aea8eddc050..802ca77450d8e48191400cb451534013f58d7e04 100644 --- a/initdb/schema.sql +++ b/initdb/schema.sql @@ -9,6 +9,7 @@ CREATE TABLE users ( role VARCHAR(10), isVerified BOOLEAN DEFAULT FALSE, verificationToken VARCHAR(255), -- This column stores the verification token + recoveryToken VARCHAR(255), -- This column stores the password recovery token dateCreated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE videos( diff --git a/login-server.js b/login-server.js index 9e529bb35ce8391ca5ddb497d738df13658bf30f..7693b3899b421adaf7cfc8a330cd93c401330b81 100644 --- a/login-server.js +++ b/login-server.js @@ -14,7 +14,7 @@ if (process.env.DATABASE_HOST) { dbHost = process.env.DATABASE_HOST; } -let frontendUrl = "http://localhost:8081"; // Default for development +let frontendUrl = "http://localhost:5173"; // Default for development if (process.env.VITE_FRONTEND_URL) { frontendUrl = process.env.VITE_FRONTEND_URL; // Use environment variable in production } @@ -111,7 +111,7 @@ export const signup = async (req, res) => { .json({ message: "Database error", error: err }); } // Send verification email - const verificationLink = `${frontendUrl}/verify-email?token=${verificationToken}`; // Change to your frontend URL when deploying + const verificationLink = `${frontendUrl}/verify-email/${verificationToken}`; // Change to your frontend URL when deploying const mailOptions = { from: emailUser, // your email to: email, @@ -145,6 +145,100 @@ export const signup = async (req, res) => { return res.status(500).json({ message: "Database error", error }); }); }; + +// Recover Account Route +app.get("/recover-account", (req, res) => { + const db = dbRequest(dbHost); + const { token } = req.query; + + if (!token) { + db.destroy(); + return res.status(400).json({ message: "Recovery token is required" }); + } + + jwt.verify(token, "secretkey", (err, decoded) => { + if (err) { + db.destroy(); + return res.status(400).json({ message: "Invalid or expired token" }); + } + + const email = decoded.email; + + const updateQuery = + "UPDATE users SET recoveryToken = NULL WHERE email = ?"; + db.query(updateQuery, [email], (err, result) => { + if (err) { + db.destroy(); + return res.status(500).json({ message: "Database error" }); + } + db.destroy(); + return res + .status(200) + .json({ message: email }); + }); + }); +}); + +// Send Recovery Link Route +app.post("/send-recovery-link", (req, res) => { + const db = dbRequest(dbHost); + const { email } = req.body; + + if (!email) { + db.destroy(); + return res.status(400).json({ message: "Email is required" }); + } + + const findUserQuery = "SELECT * FROM users WHERE email = ?"; + db.query(findUserQuery, [email], (err, results) => { + if (err) { + console.error("Database error:", err); + db.destroy(); + return res.status(500).json({ message: "Database error" }); + } + + if (results.length === 0) { + db.destroy(); + return res.status(404).json({ message: "Email does not exist" }); + } + + const user = results[0]; + const recoveryToken = jwt.sign({ email: user.email }, "secretkey", { + expiresIn: "1h", + }); + + const recoveryLink = `${frontendUrl}/recover-account/${recoveryToken}`; + const mailOptions = { + from: emailUser, + to: email, + subject: "Password Recovery", + text: `The link will expire in 1 hour. Click this link to reset your password: ${recoveryLink}`, + }; + const attachTokenQuery = "UPDATE users SET recoveryToken = ? WHERE email = ?"; + db.query(attachTokenQuery, [recoveryToken, email], (err) => { + if (err) { + console.error("Database error:", err); + db.destroy(); + return res.status(500).json({ message: "Database error" }); + } + }); + + transporter.sendMail(mailOptions, (error, info) => { + if (error) { + console.error("Error sending email:", error); + db.destroy(); + return res.status(500).json({ message: "Error sending email" }); + } + + db.destroy(); + return res.status(200).json({ + message: "Recovery link sent successfully. Please check your email.", + }); + }); + }); +}); + + // Email Verification Route app.get("/verify-email", (req, res) => { const db = dbRequest(dbHost); diff --git a/src/App.tsx b/src/App.tsx index 66f388f32bce4002ff5feea9fa45c7a0a3b280ac..5bd2214905c68dfad96aa3a50409a03edea4b2af 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,6 +19,7 @@ import axios from "axios"; import Terms from "./terms.tsx"; import LikeButton from "./components/likeButton.tsx"; import TopBar from "./components/TopBar.tsx"; +import RecoverAccount from "./recoverAccount.tsx"; // import { createContext, useContext } from 'react'; // import VideoPlayer from './components/VideoPlayerUser.tsx'; @@ -460,7 +461,8 @@ function App() { <Route path="/signup" element={<Signup />} /> <Route path="/terms" element={<Terms />} /> <Route path="/reset-password" element={<ResetPassword />} /> - <Route path="/verify-email" element={<VerifyEmail />} /> + <Route path="/verify-email/:token" element={<VerifyEmail />} /> + <Route path="/recover-account/:token" element={<RecoverAccount />} /> {/* User Page Route */} {/* Protected Route for Dashboard and Video Player */} diff --git a/src/VerifyEmail.tsx b/src/VerifyEmail.tsx index e838a0aade5f285676b248fcdde3e620afc07d20..0d9f3104a00d263bbd2114c7bff7b3415f2db301 100644 --- a/src/VerifyEmail.tsx +++ b/src/VerifyEmail.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; -import { useNavigate, useLocation } from "react-router-dom"; +import { useNavigate, useLocation, useParams } from "react-router-dom"; import axios from "axios"; +import "./styles/auth.scss"; let loginServer = "http://localhost:8081"; if (import.meta.env.VITE_LOGIN_SERVER !== undefined) { @@ -8,13 +9,17 @@ if (import.meta.env.VITE_LOGIN_SERVER !== undefined) { loginServer = import.meta.env.VITE_LOGIN_SERVER; } + + const VerifyEmail: React.FC = () => { const [message, setMessage] = useState<string>("Verifying..."); const navigate = useNavigate(); const location = useLocation(); + const token = useParams().token; useEffect(() => { - const token = localStorage.getItem("authToken"); + + // const token = localStorage.setItem('token', token); if (token) { axios diff --git a/src/recoverAccount.tsx b/src/recoverAccount.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1644de36e7a3cbfd9e5d7636049807d2d58b1a01 --- /dev/null +++ b/src/recoverAccount.tsx @@ -0,0 +1,143 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import "./styles/auth.scss"; +import { Link, useNavigate, useParams } from "react-router-dom"; + +// let uploadServer = "http://localhost:3001"; +// if (import.meta.env.VITE_UPLOAD_SERVER !== undefined) { +// // console.log(import.meta.env.VITE_UPLOAD_SERVER); +// uploadServer = import.meta.env.VITE_UPLOAD_SERVER; +// } +let loginServer = "http://localhost:8081" + +if (import.meta.env.VITE_LOGIN_SERVER !== undefined) { + // console.log(import.meta.env.VITE_UPLOAD_SERVER); + loginServer = import.meta.env.VITE_LOGIN_SERVER; +} + + + +const RecoverAccount: React.FC = () => { + const [email, setEmail] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [message, setMessage] = useState<string | null>(null); + + + const [validToken, setValidToken] = useState(false); + const [error, setError] = useState<string | null>(null); + const navigate = useNavigate(); + const token = useParams().token; + + useEffect(() => { + + // const token = localStorage.setItem('token', token); + + if (token) { + axios + .get(`${loginServer}/recover-account?token=${token}`) + .then((res) => { + setEmail(res.data.message); + setValidToken(true); + }) + .catch((err) => { + setMessage( + err.response?.data?.message || "Invalid or expired token." + ); + }); + } else { + setMessage("Invalid request."); + } + }, [location, navigate]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setMessage(null); + setError(null); + + // Frontend validation for matching passwords + if (newPassword !== confirmPassword) { + setError("Passwords do not match"); + return; + } + + try { + const response = await axios.post( + `${loginServer}/reset-password`, + { + email, + newPassword, + } + ); + setMessage(response.data.message); + setTimeout(() => { + navigate("/login"); // Redirect to Login after success message + }, 1500); // Redirect after 1.5 seconds + } catch (err: any) { + setError(err.response?.data?.message || "An error occurred"); + } + }; + + return ( + <div className="auth__body"> + <div className="auth__form"> + <h2>Reset Password</h2> + {/* {!validToken ? ( + <div className="auth__error"></div> + ) : null} */} + {message && <div className="auth__success">{message}</div>} + {error && <div className="auth__error">{error}</div>} + {validToken && ( + + <form onSubmit={handleSubmit}> + + +<div className="auth__container"> + + <label> + <strong>New Password:</strong> + </label> + <input + type="password" + value={newPassword} + onChange={(e) => setNewPassword(e.target.value)} + placeholder="Enter new password" + required + className="auth__form-control" + /> +</div> + +<div className="auth__container"> + + <label> + <strong>Confirm Password:</strong> + </label> + <input + type="password" + value={confirmPassword} + onChange={(e) => setConfirmPassword(e.target.value)} + placeholder="Confirm new password" + required + className="auth__form-control" + /> +</div> + + + <button type="submit" className="button danger">Reset Password</button> + <br /> <br /> + <Link to="/login"> + <button className="button primary">Go to Login</button> + </Link> + + </form> + )} + + <div> + + </div> + </div> + </div> + ); +}; + +export default RecoverAccount; diff --git a/src/resetPassword.tsx b/src/resetPassword.tsx index 264ab8c9b06f2dd8921afbaee5394b140bf48b79..ac11f96762c80b43b521aba01262b3a1fc75e96b 100644 --- a/src/resetPassword.tsx +++ b/src/resetPassword.tsx @@ -19,35 +19,22 @@ if (import.meta.env.VITE_LOGIN_SERVER !== undefined) { const ResetPassword: React.FC = () => { const [email, setEmail] = useState(""); - const [newPassword, setNewPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); const [message, setMessage] = useState<string | null>(null); const [error, setError] = useState<string | null>(null); - const navigate = useNavigate(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setMessage(null); setError(null); - // Frontend validation for matching passwords - if (newPassword !== confirmPassword) { - setError("Passwords do not match"); - return; - } - try { const response = await axios.post( - `${loginServer}/reset-password`, - { - email, - newPassword, - } + `${loginServer}/send-recovery-link`, + { + email, + } ); - setMessage(response.data.message); - setTimeout(() => { - navigate("/login"); // Redirect to Login after success message - }, 1500); // Redirect after 1.5 seconds + setMessage(response.data.message || "Recovery link sent successfully"); } catch (err: any) { setError(err.response?.data?.message || "An error occurred"); } @@ -75,39 +62,8 @@ const ResetPassword: React.FC = () => { className="auth__form-control" /> </div> - -<div className="auth__container"> - - <label> - <strong>New Password:</strong> - </label> - <input - type="password" - value={newPassword} - onChange={(e) => setNewPassword(e.target.value)} - placeholder="Enter new password" - required - className="auth__form-control" - /> -</div> - -<div className="auth__container"> - - <label> - <strong>Confirm Password:</strong> - </label> - <input - type="password" - value={confirmPassword} - onChange={(e) => setConfirmPassword(e.target.value)} - placeholder="Confirm new password" - required - className="auth__form-control" - /> -</div> - - <button type="submit" className="button danger">Reset Password</button> + <button type="submit" className="button warning">Send Recovery Email</button> <br /> <br /> <Link to="/login"> <button className="button primary">Go to Login</button> diff --git a/src/styles/auth.scss b/src/styles/auth.scss index e8d14b1b51e0e8f8624d889838c7ce803cc42a0a..9117e1c17d179913f94b2b1f28f3f47fef5e4f49 100644 --- a/src/styles/auth.scss +++ b/src/styles/auth.scss @@ -154,3 +154,18 @@ text-align: left; } +.verify-email__container{ + color: #ddd; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background-position: top; + background-repeat: no-repeat; + padding: 20px; + max-height: 40vh; + +} + +// http://localhost:5173/verify-email/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Impvc2gzQGpvc2hyYW5kYWxsLm5ldCIsImlhdCI6MTc0MjM0NDc2MiwiZXhwIjoxNzQyNDMxMTYyfQ.HAVUd4k3iFVUfqh4Ek4PnqHnKZV_0iiIgVCZ90qskD8