From 967800a9814077a4f73dda8809f8f5753f7462be Mon Sep 17 00:00:00 2001 From: Shubham-Lal Date: Wed, 17 Jul 2024 23:02:32 +0530 Subject: [PATCH 1/4] client: stop refresh on swipe --- client/src/globals.css | 5 +++++ client/src/pages/create-debate/style.css | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/client/src/globals.css b/client/src/globals.css index 2aedb9b..31c9848 100644 --- a/client/src/globals.css +++ b/client/src/globals.css @@ -89,6 +89,11 @@ --modal_header_color: rgb(60 130 246); } +html, +body { + overscroll-behavior-y: contain; +} + * { margin: 0; padding: 0; diff --git a/client/src/pages/create-debate/style.css b/client/src/pages/create-debate/style.css index 23afb15..3d215ef 100644 --- a/client/src/pages/create-debate/style.css +++ b/client/src/pages/create-debate/style.css @@ -17,7 +17,6 @@ #create { position: relative; overflow: hidden; - overscroll-behavior: contain; } #create-debate { From 9dc6f4ea1fac1f5a4a6e2fbb34b0e8fa78b968af Mon Sep 17 00:00:00 2001 From: Shubham-Lal Date: Wed, 17 Jul 2024 23:15:02 +0530 Subject: [PATCH 2/4] client: modify bg color for small screen --- client/src/App.css | 6 +++++- client/src/components/sidebar/right-sidebar.css | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index 6c47df1..203c1c5 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -145,6 +145,10 @@ } @media screen and (max-width: 480px) { + body { + background: var(--nav-foot__background); + } + #left-sidebar { position: fixed; top: unset; @@ -152,7 +156,7 @@ width: 100%; bottom: 0; height: 60px; - background: var(--nav-foot__background); + background: var(--body_background); border-top: 1px solid var(--nav_border); } diff --git a/client/src/components/sidebar/right-sidebar.css b/client/src/components/sidebar/right-sidebar.css index abb9d5c..16c174d 100644 --- a/client/src/components/sidebar/right-sidebar.css +++ b/client/src/components/sidebar/right-sidebar.css @@ -130,7 +130,6 @@ padding: 0 10px; display: flex; align-items: center; - background-color: var(--nav-foot__background); } #right-sidebar .profile-theme__container { From cb0e3e1375dd4e886c5603128eb412a52a07709d Mon Sep 17 00:00:00 2001 From: Shubham-Lal Date: Wed, 17 Jul 2024 23:49:34 +0530 Subject: [PATCH 3/4] client: match bg color --- client/src/App.css | 7 ++++--- client/src/components/modal/auth/index.css | 5 +++++ client/src/components/sidebar/profile.css | 1 + client/src/globals.css | 4 ++-- client/src/pages/create-debate/style.css | 1 + 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index 203c1c5..490c1dc 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1,4 +1,5 @@ #app { + position: relative; display: flex; } @@ -146,7 +147,7 @@ @media screen and (max-width: 480px) { body { - background: var(--nav-foot__background); + background-color: var(--nav-foot__background); } #left-sidebar { @@ -156,7 +157,7 @@ width: 100%; bottom: 0; height: 60px; - background: var(--body_background); + background-color: var(--body_background); border-top: 1px solid var(--nav_border); } @@ -165,7 +166,7 @@ position: fixed; width: 100%; height: 60px; - background: var(--body_background); + background-color: var(--body_background); } #main { diff --git a/client/src/components/modal/auth/index.css b/client/src/components/modal/auth/index.css index e692c9b..68c5c36 100644 --- a/client/src/components/modal/auth/index.css +++ b/client/src/components/modal/auth/index.css @@ -324,6 +324,7 @@ height: 100%; flex-direction: column; gap: 10px; + background-color: var(--body_background); border: none; border-radius: unset; } @@ -375,4 +376,8 @@ gap: 10px; flex-direction: column; } + + #auth-modal .form__container .input__container p { + background-color: var(--body_background); + } } \ No newline at end of file diff --git a/client/src/components/sidebar/profile.css b/client/src/components/sidebar/profile.css index 914e743..2e7be3b 100644 --- a/client/src/components/sidebar/profile.css +++ b/client/src/components/sidebar/profile.css @@ -201,6 +201,7 @@ padding-bottom: 15px; width: 100%; left: 0; + background-color: var(--body_background); border: 0; border-bottom: 1px solid var(--explore_input_bg); border-radius: 0; diff --git a/client/src/globals.css b/client/src/globals.css index 31c9848..db23c4e 100644 --- a/client/src/globals.css +++ b/client/src/globals.css @@ -53,9 +53,9 @@ } :root { - --body_background: #fafafa; + --body_background: #ffffff; --body_color: rgb(1, 1, 1); - --nav-foot__background: #ffffff; + --nav-foot__background: #fafafa; --nav_border: #ebebeb; --nav_profile_username: #a6a6a6; --explore_input_bg: #dee2e6; diff --git a/client/src/pages/create-debate/style.css b/client/src/pages/create-debate/style.css index 3d215ef..93b7aae 100644 --- a/client/src/pages/create-debate/style.css +++ b/client/src/pages/create-debate/style.css @@ -181,6 +181,7 @@ .debate-btns { width: 100%; padding: 10px; + background-color: var(--body_background); } .debate-btns.reveal { From 6a0c679148b99a79231f97537447d7dd0527ddd2 Mon Sep 17 00:00:00 2001 From: Shubham-Lal Date: Thu, 18 Jul 2024 11:53:45 +0530 Subject: [PATCH 4/4] add reset password --- client/src/App.css | 1 - client/src/App.tsx | 1 + .../components/modal/auth/forgot-password.tsx | 47 ++++--- client/src/components/modal/auth/index.css | 8 +- client/src/components/modal/auth/index.tsx | 5 +- .../components/modal/auth/reset-password.tsx | 127 ++++++++++++++++++ client/src/pages/auth/index.tsx | 7 +- client/src/store/useAuthStore.ts | 1 + server/.env.example | 11 +- server/controllers/auth.js | 120 ++++++++++++----- server/db/README.md | 2 + server/middleware/verifyToken.js | 11 +- server/package-lock.json | 29 ++++ server/package.json | 1 + server/routes/auth.js | 63 ++++++++- server/server.js | 13 ++ server/utils/sendMail.js | 8 ++ 17 files changed, 391 insertions(+), 64 deletions(-) create mode 100644 client/src/components/modal/auth/reset-password.tsx create mode 100644 server/utils/sendMail.js diff --git a/client/src/App.css b/client/src/App.css index 490c1dc..d3aad0a 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1,5 +1,4 @@ #app { - position: relative; display: flex; } diff --git a/client/src/App.tsx b/client/src/App.tsx index 837654e..51e399d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -64,6 +64,7 @@ export default function App() { } />isScrollingUp } /> } /> + } /> } /> } /> } /> diff --git a/client/src/components/modal/auth/forgot-password.tsx b/client/src/components/modal/auth/forgot-password.tsx index af33e14..104ed2b 100644 --- a/client/src/components/modal/auth/forgot-password.tsx +++ b/client/src/components/modal/auth/forgot-password.tsx @@ -40,33 +40,42 @@ const ForgotPassword = () => { setValidationState(!!trimmedEmail || !!trimmedUsername); - if (!emailRegex.test(trimmedEmail)) { + if (trimmedEmail && !emailRegex.test(trimmedEmail)) { setIsSubmitted(false); return toast.warning('Invalid email address'); } if (trimmedEmail || trimmedUsername) { - await fetch(`${import.meta.env.VITE_SERVER_URL}/api/auth/recover-account`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(trimmedEmail ? { email: trimmedEmail } : { username: trimmedUsername }) - }) - .then(res => res.json()) - .then(response => { - if (response.success) toast.success(response.message); - else { - if (response.message === 'Validation failed') { - return toast.error(`${response.errors[0].message.charAt(0).toUpperCase() + response.errors[0].message.slice(1)}`) - } - toast.error(response.message) - }; - }).finally(() => setIsSubmitted(false)); - } else setTimeout(() => setIsSubmitted(false), 500); + try { + const res = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/auth/recover-account`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(trimmedEmail ? { email: trimmedEmail } : { username: trimmedUsername }) + }); + + const response = await res.json(); + if (response.success) { + toast.success(response.message); + } else { + if (response.message === 'Validation failed') { + toast.error(`${response.errors[0].message.charAt(0).toUpperCase() + response.errors[0].message.slice(1)}`); + } else { + toast.error(response.message); + } + } + } catch (error) { + toast.error('Something went wrong'); + } finally { + setIsSubmitted(false); + } + } else { + setTimeout(() => setIsSubmitted(false), 500); + } }; return (
-

Account Recover

+

Recover Account

Email

@@ -76,7 +85,6 @@ const ForgotPassword = () => { onChange={handleInputChange} className={`${isSubmitted && !validationState ? "shake" : ""}`} style={{ borderColor: isSubmitted && !validationState ? "red" : "" }} - placeholder='Enter your email' />
@@ -92,7 +100,6 @@ const ForgotPassword = () => { onChange={handleInputChange} className={`${isSubmitted && !validationState ? "shake" : ""}`} style={{ borderColor: isSubmitted && !validationState ? "red" : "" }} - placeholder='Enter your username' />
<> diff --git a/client/src/components/modal/auth/reset-password.tsx b/client/src/components/modal/auth/reset-password.tsx new file mode 100644 index 0000000..eeb5308 --- /dev/null +++ b/client/src/components/modal/auth/reset-password.tsx @@ -0,0 +1,127 @@ +import { useCallback, useState } from "react" +import { useNavigate } from "react-router-dom" +import { toast } from "sonner" +import { LoadingSVG } from "../../loading/svg" + +const ResetPassword = () => { + const navigate = useNavigate(); + + const [resetData, setResetData] = useState({ + new: "", + confirm: "" + }); + + const [isSubmitted, setIsSubmitted] = useState(false); + const [validationState, setValidationState] = useState({ + isNewValid: true, + isConfirmValid: true + }); + + const handleInputChange = useCallback((e: React.ChangeEvent) => { + const { name, value } = e.target; + setResetData(prevState => ({ + ...prevState, + [name]: value + })); + + setValidationState(prevState => ({ + ...prevState, + [`is${name.charAt(0).toUpperCase() + name.slice(1)}Valid`]: !!value + })); + }, []); + + const handleResetSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitted(true); + + const trimmedNew = resetData.new.trim(); + const trimmedConfirm = resetData.confirm.trim(); + + setResetData(prevState => ({ + ...prevState, + new: trimmedNew, + confirm: trimmedConfirm + })); + + setValidationState({ + isNewValid: !!trimmedNew, + isConfirmValid: !!trimmedConfirm + }); + + if (trimmedNew !== trimmedConfirm) { + setIsSubmitted(false); + return toast.error("Password doesn't match"); + } + else { + if (trimmedNew.length < 6) { + setIsSubmitted(false); + return toast.warning('Password should be atleast 6 digits'); + } + + await fetch(`${import.meta.env.VITE_SERVER_URL}/api/auth/reset-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token: new URLSearchParams(location.search).get('token'), password: trimmedNew }) + }).then(res => res.json()) + .then(response => { + if (response.success) { + navigate('/auth?type=login'); + toast.success(response.message); + } + else { + if (response.message === 'Validation failed') { + return toast.error(`${response.errors[0].field.charAt(0).toUpperCase() + response.errors[0].field.slice(1)} ${response.errors[0].message}`) + } + toast.error(response.message) + } + }) + .finally(() => setIsSubmitted(false)); + } + } + + return ( +
+

Reset Password

+ +
+

New Password

+ +
+
+

Confirm Password

+ +
+ +
+

+ navigate('/auth?type=login')}> + Go Back + +

+
+ +
+ ) +} + +export default ResetPassword \ No newline at end of file diff --git a/client/src/pages/auth/index.tsx b/client/src/pages/auth/index.tsx index 7fb8c33..a5a5c44 100644 --- a/client/src/pages/auth/index.tsx +++ b/client/src/pages/auth/index.tsx @@ -19,7 +19,12 @@ export default function AuthPage() { navigate(route === '/auth' || route === '/login' || route === '/signup' ? '/' : route, { replace: true }); } else if (isAuthenticated === AuthStatus.Failed) { - setAuthTab(type === 'login' ? AuthTab.Login : type === 'signup' ? AuthTab.Signup : AuthTab.Login); + setAuthTab( + type === 'login' ? AuthTab.Login + : type === 'signup' ? AuthTab.Signup + : type === 'forgot' ? AuthTab.Forgot + : type === 'reset' && new URLSearchParams(location.search).get('token') ? AuthTab.Reset + : AuthTab.Login); } const userData = params.get('user'); diff --git a/client/src/store/useAuthStore.ts b/client/src/store/useAuthStore.ts index b479e3c..69eef12 100644 --- a/client/src/store/useAuthStore.ts +++ b/client/src/store/useAuthStore.ts @@ -6,6 +6,7 @@ export enum AuthTab { Signup = 'signup', Info = 'info', Forgot = 'forgot', + Reset = 'reset' } export enum AuthStatus { diff --git a/server/.env.example b/server/.env.example index 5ff98fa..496e6df 100644 --- a/server/.env.example +++ b/server/.env.example @@ -3,13 +3,18 @@ FRONTEND_URL=http://localhost:5173 SERVER_URL=http://localhost:5000 JWT_SECRET= -# MySQL Secrets +# MySQL DB_HOST= DB_USER= DB_PASSWORD= DB_DATABASE= -# Google Cloud OAuth +# Google Cloud GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret -GOOGLE_REDIRECT_URI=http://localhost:5000/api/auth/google/callback \ No newline at end of file +GOOGLE_REDIRECT_URI=http://localhost:5000/api/auth/google/callback + +#Email +EMAIL_USER= +EMAIL_ADDRESS= +EMAIL_PASSWORD= \ No newline at end of file diff --git a/server/controllers/auth.js b/server/controllers/auth.js index b4411f6..40c548e 100644 --- a/server/controllers/auth.js +++ b/server/controllers/auth.js @@ -1,17 +1,23 @@ const bcrypt = require('bcrypt'); -const jwt = require('jsonwebtoken'); +const { sign } = require('jsonwebtoken'); const { ErrorHandler, catchError } = require('../utils/ErrorHandler'); +const crypto = require('crypto'); +const sendMail = require('../utils/sendMail'); exports.handleGoogleAuth = async function (fastify, request, reply) { try { const { token } = await fastify.googleOAuth2.getAccessTokenFromAuthorizationCodeFlow(request, reply); const userinfo = await fastify.googleOAuth2.userinfo(token.access_token); - const [user] = await fastify.mysql.query('SELECT * FROM users WHERE email = ?', [userinfo.email]); + const [user] = await fastify.mysql.query('SELECT * FROM users WHERE email=?', [userinfo.email]); if (user.length > 0) { - const token = jwt.sign({ userId: user[0].username }, process.env.JWT_SECRET, { expiresIn: '12h' }); - + const token = await new Promise((resolve, reject) => { + sign({ userId: user[0].username }, process.env.JWT_SECRET, { expiresIn: '12h' }, (err, token) => { + if (err) reject(err); + else resolve(token) + }); + }); reply.redirect(`${process.env.FRONTEND_URL}/auth?type=login&token=${token}`); } else { reply.redirect(`${process.env.FRONTEND_URL}/auth?type=signup&user=${encodeURIComponent(JSON.stringify(userinfo))}`); @@ -29,11 +35,11 @@ exports.register = async function (fastify, request, reply) { if (request.body.avatar) avatar = request.body.avatar; else if (request.file) avatar = null; - const [emailExists] = await fastify.mysql.query('SELECT * FROM users WHERE email = ?', [email]); - if (emailExists.length > 0) throw new ErrorHandler(400, false, 'Account already exists.'); + const [emailExists] = await fastify.mysql.query('SELECT * FROM users WHERE email=?', [email]); + if (emailExists.length > 0) throw new ErrorHandler(400, false, 'Account already exists'); - const [usernameExists] = await fastify.mysql.query('SELECT * FROM users WHERE username = ?', [username]); - if (usernameExists.length > 0) throw new ErrorHandler(400, false, 'Username already exists.'); + const [usernameExists] = await fastify.mysql.query('SELECT * FROM users WHERE username=?', [username]); + if (usernameExists.length > 0) throw new ErrorHandler(400, false, 'Username already exists'); const hashedPassword = await bcrypt.hash(password, 10); @@ -43,7 +49,12 @@ exports.register = async function (fastify, request, reply) { ); if (result.affectedRows > 0) { - const token = jwt.sign({ userId: username }, process.env.JWT_SECRET, { expiresIn: '12h' }); + const token = await new Promise((resolve, reject) => { + sign({ userId: username }, process.env.JWT_SECRET, { expiresIn: '12h' }, (err, token) => { + if (err) reject(err); + else resolve(token) + }); + }); return reply.code(201).send({ success: true, message: 'Account created successfully', @@ -52,7 +63,7 @@ exports.register = async function (fastify, request, reply) { token } }); - } else throw new ErrorHandler(500, false, 'Failed to create user account.'); + } else throw new ErrorHandler(400, false, 'Failed to create user account'); } catch (err) { return catchError(reply, err); } @@ -62,26 +73,26 @@ exports.login = async function (fastify, request, reply) { try { const { id, password } = request.body; - const isEmail = id.includes('@'); - const query = isEmail ? 'SELECT * FROM users WHERE email = ?' : 'SELECT * FROM users WHERE username = ?'; + const query = `SELECT * FROM users WHERE ${id.includes('@') ? 'email=?' : 'username=?'}`; - const [user] = await fastify.mysql.query(query, [id]); - if (!user.length) throw new ErrorHandler(400, false, 'Invalid credentials.'); + const [user] = await fastify.mysql.query(query, [id, id]); + if (!user.length) throw new ErrorHandler(400, false, "Account doesn't exists"); - const userData = user[0]; - const isPasswordValid = await bcrypt.compare(password, userData.password); + const isPasswordValid = await bcrypt.compare(password, user[0].password); + if (!isPasswordValid) throw new ErrorHandler(400, false, 'Incorrect password'); - if (!isPasswordValid) throw new ErrorHandler(400, false, 'Incorrect password.'); - - const token = jwt.sign({ userId: userData.username }, process.env.JWT_SECRET, { expiresIn: '12h' }); + const token = await new Promise((resolve, reject) => { + sign({ userId: user[0].username }, process.env.JWT_SECRET, { expiresIn: '12h' }, (err, token) => { + if (err) reject(err); + else resolve(token) + }); + }); return reply.code(200).send({ success: true, message: 'Login successful', data: { - user: { - ...(({ password, ...rest }) => rest)(userData) - }, + user: { ...(({ password, ...rest }) => rest)(user[0]) }, token } }); @@ -93,19 +104,17 @@ exports.login = async function (fastify, request, reply) { exports.autoLogin = async function (fastify, request, reply) { try { const id = request.user.userId; - const query = 'SELECT * FROM users WHERE username = ?'; + const query = 'SELECT * FROM users WHERE username=?'; const [user] = await fastify.mysql.query(query, [id]); - if (!user.length) throw new ErrorHandler(400, false, 'Invalid credentials.'); - - const userData = user[0]; + if (!user.length) throw new ErrorHandler(400, false, 'Invalid credentials'); return reply.code(200).send({ success: true, message: 'Login successful', data: { user: { - ...(({ password, ...rest }) => rest)(userData) + ...(({ password, ...rest }) => rest)(user[0]) } } }); @@ -117,11 +126,11 @@ exports.autoLogin = async function (fastify, request, reply) { exports.checkUsername = async function (fastify, request, reply) { try { const { username } = request.body; - const query = 'SELECT * FROM users WHERE username =?'; + const query = 'SELECT * FROM users WHERE username=?'; const [user] = await fastify.mysql.query(query, [username]); if (user.length) { - throw new ErrorHandler(400, false, 'Username already taken.'); + throw new ErrorHandler(400, false, 'Username already taken'); } return reply.code(200).send({ @@ -131,4 +140,55 @@ exports.checkUsername = async function (fastify, request, reply) { } catch (err) { return catchError(reply, err); } -} \ No newline at end of file +} + +exports.recoverAccount = async function (fastify, request, reply) { + try { + const { email, username } = request.body; + const { mailer } = fastify; + + const [user] = await fastify.mysql.query('SELECT * FROM users WHERE email=? OR username=?', [email, username]); + if (!user.length) throw new ErrorHandler(400, false, "Account doesn't exist"); + + const token = crypto.randomBytes(32).toString('hex'); + const tokenExpiry = new Date(Date.now() + 3600000); + + await fastify.mysql.query('UPDATE users SET reset_token=?, reset_token_expiry=? WHERE username=?', [token, tokenExpiry, user[0].username]); + + await sendMail(mailer, { + to: user[0].email, + subject: 'DebateMe', + html: `

You requested to reset your password. Click the link to reset:

${`${process.env.FRONTEND_URL}/auth?type=reset&token=${token}`}

` + }) + .then(info => { + reply.status(200).send({ + success: true, + message: `Activation link sent to your email${username ? (': ' + user[0].email) : ''}` + }); + }) + .catch(errors => { throw new ErrorHandler(400, false, 'Something went wrong') }); + + } catch (err) { + return catchError(reply, err); + } +}; + +exports.resetPassword = async (fastify, request, reply) => { + try { + const { token, password } = request.body; + + const [user] = await fastify.mysql.query('SELECT * FROM users WHERE reset_token=? AND reset_token_expiry>?', [token, new Date()]); + if (!user.length) throw new ErrorHandler(400, false, "Invalid or expired token"); + + const hashedPassword = await bcrypt.hash(password, 10); + + await fastify.mysql.query('UPDATE users SET password=?, reset_token=NULL, reset_token_expiry=NULL WHERE username=?', [hashedPassword, user[0].username]); + + reply.status(200).send({ + success: true, + message: 'Password reset successfully' + }); + } catch (err) { + return catchError(reply, err); + } +}; \ No newline at end of file diff --git a/server/db/README.md b/server/db/README.md index f565f30..6a8346a 100644 --- a/server/db/README.md +++ b/server/db/README.md @@ -7,6 +7,8 @@ CREATE TABLE users ( first_name VARCHAR(30) NOT NULL, last_name VARCHAR(30) NOT NULL, avatar VARCHAR(255), + reset_token VARCHAR(255), + reset_token_expiry DATETIME, PRIMARY KEY (username) ); ``` \ No newline at end of file diff --git a/server/middleware/verifyToken.js b/server/middleware/verifyToken.js index 13405ba..0b8ce6b 100644 --- a/server/middleware/verifyToken.js +++ b/server/middleware/verifyToken.js @@ -1,4 +1,4 @@ -const jwt = require('jsonwebtoken'); +const { verify } = require('jsonwebtoken'); async function verifyToken(request, reply) { const authHeader = request.headers.authorization; @@ -8,8 +8,13 @@ async function verifyToken(request, reply) { const token = authHeader.split(' ')[1]; try { - const decoded = jwt.verify(token, process.env.JWT_SECRET); - request.user = decoded; + const data = await new Promise((resolve, reject) => { + verify(token, process.env.JWT_SECRET, (err, payload) => { + if (err) reject(err); + else resolve(payload); + }); + }); + request.user = data; } catch (err) { throw new Error('Invalid token'); } diff --git a/server/package-lock.json b/server/package-lock.json index ae87710..055b1ab 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -14,6 +14,7 @@ "bcrypt": "^5.1.1", "dotenv": "^16.4.5", "fastify": "^4.28.1", + "fastify-mailer": "^2.3.1", "fastify-multer": "^2.0.3", "jsonwebtoken": "^9.0.2", "mysql2": "^3.10.2" @@ -683,6 +684,25 @@ "toad-cache": "^3.3.0" } }, + "node_modules/fastify-mailer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/fastify-mailer/-/fastify-mailer-2.3.1.tgz", + "integrity": "sha512-SKMkgws+nYXLW1wwZuStxfRZ/y+QFfAfIakv70VSyqxm8uHqelbDX8p8d7bFUNOTH8gg5VeQnXymrKjKD6hOJw==", + "dependencies": { + "fastify-plugin": "^3.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "nodemailer": ">=6.0.0" + } + }, + "node_modules/fastify-mailer/node_modules/fastify-plugin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.1.tgz", + "integrity": "sha512-qKcDXmuZadJqdTm6vlCqioEbyewF60b/0LOFCcYN1B6BIZGlYJumWWOYs70SFYLDAH4YqdE1cxH/RKMG7rFxgA==" + }, "node_modules/fastify-multer": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/fastify-multer/-/fastify-multer-2.0.3.tgz", @@ -1297,6 +1317,15 @@ } } }, + "node_modules/nodemailer": { + "version": "6.9.14", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", + "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", diff --git a/server/package.json b/server/package.json index 4314e24..a9f1cad 100644 --- a/server/package.json +++ b/server/package.json @@ -13,6 +13,7 @@ "bcrypt": "^5.1.1", "dotenv": "^16.4.5", "fastify": "^4.28.1", + "fastify-mailer": "^2.3.1", "fastify-multer": "^2.0.3", "jsonwebtoken": "^9.0.2", "mysql2": "^3.10.2" diff --git a/server/routes/auth.js b/server/routes/auth.js index 8ae1e09..6a8801b 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -1,13 +1,15 @@ -const { handleGoogleAuth, register, login, autoLogin, checkUsername } = require('../controllers/auth'); +const { handleGoogleAuth, register, login, autoLogin, checkUsername, recoverAccount, resetPassword } = require('../controllers/auth'); const verifyToken = require('../middleware/verifyToken'); module.exports = async function (fastify, opts) { + const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+(com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|museum|in|space)))$/; + const registerSchema = { consumes: ['multipart/form-data'], body: { type: 'object', properties: { - email: { type: 'string', format: 'email', minLength: 1 }, + email: { type: 'string', minLength: 1, pattern: emailRegex.source }, password: { type: 'string', minLength: 6 }, username: { type: 'string', minLength: 1 }, first_name: { type: 'string', minLength: 1 }, @@ -38,6 +40,31 @@ module.exports = async function (fastify, opts) { } }; + const recoverSchema = { + body: { + type: 'object', + properties: { + email: { type: 'string', minLength: 1, pattern: emailRegex.source }, + username: { type: 'string', minLength: 1 } + }, + anyOf: [ + { required: ['email'] }, + { required: ['username'] } + ] + } + }; + + const resetSchema = { + body: { + type: 'object', + properties: { + token: { type: 'string', minLength: 1 }, + password: { type: 'string', minLength: 1 }, + }, + required: ['token', 'password'] + } + }; + fastify.get('/google/callback', async (request, reply) => { return handleGoogleAuth(fastify, request, reply); }); @@ -104,4 +131,36 @@ module.exports = async function (fastify, opts) { } return checkUsername(fastify, request, reply); }); + + fastify.post('/recover-account', { + schema: recoverSchema, + attachValidation: true + }, async (request, reply) => { + if (request.validationError) { + const errors = request.validationError.validation.map(error => { + return { + field: error.params.missingProperty || error.instancePath.substring(1), + message: error.message + }; + }); + return reply.code(400).send({ success: false, message: 'Validation failed', errors }); + } + return recoverAccount(fastify, request, reply); + }); + + fastify.post('/reset-password', { + schema: resetSchema, + attachValidation: true + }, async (request, reply) => { + if (request.validationError) { + const errors = request.validationError.validation.map(error => { + return { + field: error.params.missingProperty || error.instancePath.substring(1), + message: error.message + }; + }); + return reply.code(400).send({ success: false, message: 'Validation failed', errors }); + } + return resetPassword(fastify, request, reply); + }); } \ No newline at end of file diff --git a/server/server.js b/server/server.js index 88980ac..6c4d3b1 100644 --- a/server/server.js +++ b/server/server.js @@ -38,6 +38,19 @@ fastify.register(require('@fastify/oauth2'), { } }) +fastify.register(require('fastify-mailer'), { + defaults: { from: `${process.env.EMAIL_USER} <${process.env.EMAIL_ADDRESS}>` }, + transport: { + host: 'smtp.gmail.com', + port: 465, + secure: true, + auth: { + user: process.env.EMAIL_ADDRESS, + pass: process.env.EMAIL_PASSWORD + } + } +}) + fastify.register(require('./routes/auth'), { prefix: '/api/auth' }) fastify.get('/test', (request, reply) => reply.code(200).send({ success: true, message: 'Server running...' })) diff --git a/server/utils/sendMail.js b/server/utils/sendMail.js new file mode 100644 index 0000000..dbeb9c1 --- /dev/null +++ b/server/utils/sendMail.js @@ -0,0 +1,8 @@ +module.exports = sendMail = (mailer, mailOptions) => { + return new Promise((resolve, reject) => { + mailer.sendMail(mailOptions, (error, info) => { + if (error) return reject(error); + resolve(info); + }); + }); +} \ No newline at end of file