diff --git a/client/src/app/components/index.tsx b/client/src/app/components/index.tsx index 164517ff..e763c869 100644 --- a/client/src/app/components/index.tsx +++ b/client/src/app/components/index.tsx @@ -9,6 +9,9 @@ import { Routes } from 'react-router-dom'; import getRoutesV1 from '../routes/auth-routes/v1'; export const LoginLazyComponent = lazy(() => import('../../modules/auth/v1')); +export const ResetPasswordLazyComponent = lazy( + () => import('../../modules/auth/v1/reset-password') +); export const HomePageLazyComponent = lazy( () => import('../../modules/home/v1') ); diff --git a/client/src/app/routes/index.tsx b/client/src/app/routes/index.tsx index 1e4be13e..54757862 100644 --- a/client/src/app/routes/index.tsx +++ b/client/src/app/routes/index.tsx @@ -2,11 +2,27 @@ import { Suspense } from 'react'; import { Route, Routes } from 'react-router-dom'; import Loader from '../../shared/components/molecules/loader'; import { LOADING } from './constants'; -import { LoginLazyComponent } from '../components'; +import { LoginLazyComponent, ResetPasswordLazyComponent } from '../components'; export function AppUnProtectedRoutes() { return ( + }> + + + } + /> + }> + + + } + /> ({ paddingTop: theme.spacing(4), @@ -44,6 +45,7 @@ const StyledFooter = styled('p')(({ theme }) => ({ const UserAuthForm = () => { const [formType, setFormType] = useState('login'); const { loading, handleSubmit } = useUserAuthForm({ type: formType }); + const navigate = useNavigate(); return ( @@ -90,6 +92,28 @@ const UserAuthForm = () => { icon={} /> + {/* Forgot Password Link - Only shown on login */} + {formType === 'login' && ( + + navigate('/reset-password'), + sx: { + fontSize: '0.9rem', + textDecoration: 'underline', + color: 'text.secondary', + cursor: 'pointer', + '&:hover': { + opacity: 0.8, + }, + }, + }} + /> + + )} + ({ + paddingTop: theme.spacing(4), + paddingBottom: theme.spacing(4), + paddingLeft: '5vw', + paddingRight: '5vw', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '100vh', +})); + +const StyledForm = styled('form')(() => ({ + width: '80%', + maxWidth: 400, +})); + +const StyledTitle = styled(Typography)(({ theme }) => ({ + fontSize: '2.5rem', + fontFamily: 'Gelasio, serif', + textTransform: 'capitalize', + textAlign: 'center', + marginBottom: theme.spacing(6), +})); + +const StyledFooter = styled('p')(({ theme }) => ({ + marginTop: theme.spacing(6), + color: theme.palette.text.secondary, + fontSize: '1.125rem', + textAlign: 'center', +})); + +const ResetPassword = () => { + const [email, setEmail] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + const navigate = useNavigate(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + setSuccess(false); + + try { + const response = await fetch('http://localhost:8000/api/auth/forgot-password', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email }), + }); + + const data = await response.json(); + + if (response.ok) { + setSuccess(true); + setEmail(''); // Clear email after success + } else { + setError(data.message || 'Failed to send reset link. Please try again.'); + } + } catch (err) { + setError('Something went wrong. Please try again later.'); + console.error('Error:', err); + } finally { + setLoading(false); + } + }; + + const handleEmailChange = (e: React.ChangeEvent) => { + setEmail(e.target.value); + setError(''); // Clear error when user types + setSuccess(false); // Clear success when user types + }; + + return ( + + + Reset Password + + + + Enter your email address to receive a link to reset your password. + + + {success && ( + + Password reset link sent successfully! Check your email. + + )} + + {error && ( + + {error} + + )} + + } + slotProps={{ + input: { + value: email, + onChange: handleEmailChange, + }, + }} + /> + + + Send Reset Link + + + + + Remember your password?{' '} + navigate('/'), + sx: { + fontSize: '1.125rem', + marginLeft: 1, + textDecoration: 'underline', + color: 'inherit', + cursor: 'pointer', + '&:hover': { + opacity: 0.8, + }, + }, + }} + /> + + + + ); +}; + +export default ResetPassword; diff --git a/server/.env.example b/server/.env.example index 06f32422..368a7cc6 100644 --- a/server/.env.example +++ b/server/.env.example @@ -27,3 +27,10 @@ CLOUDINARY_API_SECRET= # Admin Credentials (for sending emails) ADMIN_EMAIL= RESEND_API_KEY= + +# Email Configuration (for password reset) +EMAIL_HOST=smtp.gmail.com +EMAIL_PORT=587 +EMAIL_USER=youremailid +EMAIL_PASSWORD=your-16-char-app-password-here +FRONTEND_URL=http://localhost:5173 \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 00000000..ca2d4525 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,3 @@ +"" +"# Environment variables" +".env" diff --git a/server/package-lock.json b/server/package-lock.json index 47524e38..7edc4b46 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -23,7 +23,7 @@ "morgan": "^1.10.1", "multer": "^1.4.5-lts.1", "nanoid": "^5.1.2", - "nodemailer": "^7.0.5", + "nodemailer": "^7.0.13", "rate-limiter-flexible": "^7.3.0", "resend": "^6.1.2", "sanitize-html": "^2.17.0", @@ -1975,9 +1975,9 @@ } }, "node_modules/nodemailer": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz", - "integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==", + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz", + "integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" diff --git a/server/package.json b/server/package.json index b1d4f016..5544bcc7 100644 --- a/server/package.json +++ b/server/package.json @@ -45,7 +45,7 @@ "morgan": "^1.10.1", "multer": "^1.4.5-lts.1", "nanoid": "^5.1.2", - "nodemailer": "^7.0.5", + "nodemailer": "^7.0.13", "rate-limiter-flexible": "^7.3.0", "resend": "^6.1.2", "sanitize-html": "^2.17.0", diff --git a/server/src/controllers/auth/forgot-password.js b/server/src/controllers/auth/forgot-password.js new file mode 100644 index 00000000..d2a32731 --- /dev/null +++ b/server/src/controllers/auth/forgot-password.js @@ -0,0 +1,80 @@ +import { sendResponse } from '../../utils/response.js'; +import nodemailer from 'nodemailer'; +import crypto from 'crypto'; + +const forgotPassword = async (req, res) => { + try { + const { email } = req.body; + + // Validate email + if (!email) { + return sendResponse(res, 400, 'Email is required'); + } + + // TODO: Check if user exists in database + // const user = await User.findOne({ email }); + // if (!user) { + // return sendResponse(res, 404, 'User not found'); + // } + + // Generate reset token + const resetToken = crypto.randomBytes(32).toString('hex'); + const resetTokenExpiry = Date.now() + 3600000; // 1 hour + + // TODO: Save token to database + // user.passwordResetToken = resetToken; + // user.passwordResetExpires = resetTokenExpiry; + // await user.save(); + + // Create reset URL + const resetUrl = `${process.env.FRONTEND_URL || 'http://localhost:5173'}/reset-password/${resetToken}`; + + // Configure email transporter + const transporter = nodemailer.createTransport({ + host: process.env.EMAIL_HOST || 'smtp.gmail.com', + port: process.env.EMAIL_PORT || 587, + secure: false, + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASSWORD, + }, + }); + + // Email content + const mailOptions = { + from: `"Code A2Z" <${process.env.EMAIL_USER}>`, + to: email, + subject: 'Password Reset Request', + html: ` +
+

Password Reset Request

+

You requested a password reset for your Code A2Z account.

+

Click the link below to reset your password:

+ Reset Password +

This link will expire in 1 hour.

+

If you didn't request this, please ignore this email.

+
+ `, + }; + + // Send email + await transporter.sendMail(mailOptions); + + console.log('Password reset email sent to:', email); + + return sendResponse( + res, + 200, + 'Password reset link sent successfully to your email' + ); + } catch (err) { + console.error('Forgot password error:', err); + return sendResponse( + res, + 500, + err.message || 'Failed to send reset link. Please try again later.' + ); + } +}; + +export default forgotPassword; diff --git a/server/src/routes/api/auth.routes.js b/server/src/routes/api/auth.routes.js index 32564954..de876186 100644 --- a/server/src/routes/api/auth.routes.js +++ b/server/src/routes/api/auth.routes.js @@ -7,6 +7,7 @@ import login from '../../controllers/auth/login.js'; import changePassword from '../../controllers/auth/change-password.js'; import refresh from '../../controllers/auth/refresh.js'; import logout from '../../controllers/auth/logout.js'; +import forgotPassword from '../../controllers/auth/forgot-password.js'; const authRoutes = express.Router(); @@ -15,5 +16,6 @@ authRoutes.post('/login', login); authRoutes.post('/refresh', refresh); authRoutes.post('/logout', logout); authRoutes.patch('/change-password', authenticateUser, changePassword); +authRoutes.post('/forgot-password', forgotPassword); export default authRoutes; diff --git a/server/src/schemas/user.schema.js b/server/src/schemas/user.schema.js index e6001a6a..91b6ed1a 100644 --- a/server/src/schemas/user.schema.js +++ b/server/src/schemas/user.schema.js @@ -44,6 +44,14 @@ const USER_SCHEMA = Schema( return `https://api.dicebear.com/6.x/${profile_imgs_collections_list[Math.floor(Math.random() * profile_imgs_collections_list.length)]}/svg?seed=${profile_imgs_name_list[Math.floor(Math.random() * profile_imgs_name_list.length)]}`; }, }, + resetToken: { + type: String, + default: null, + }, + resetTokenExpiry: { + type: Date, + default: null, + }, }, social_links: { youtube: {