Add these variables to your .env file:
# Frontend URL (used in reset email links)
FRONTEND_URL=http://localhost:3000
# Email Service Configuration (choose one option below)
# Option A: Gmail with App Password
SMTP_SERVICE=gmail
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-16-char-app-password
SMTP_FROM=noreply@localmind.com
# Option B: Custom SMTP Server
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-smtp-user
SMTP_PASSWORD=your-smtp-password
SMTP_FROM=noreply@localmind.com- Go to https://myaccount.google.com/apppasswords
- Select "Mail" and "Windows Computer" (or your device)
- Copy the generated 16-character password
- Paste into
.envasSMTP_PASSWORD
All dependencies are already installed:
- ✅
nodemailer- Email sending - ✅
bcrypt- Password hashing - ✅
crypto- Token generation (Node.js built-in)
The User schema is automatically updated with:
resetPasswordToken: String | null
resetPasswordExpire: Date | nullNo database migration needed if using MongoDB with Mongoose.
<form id="forgotPasswordForm">
<input type="email" id="email" placeholder="Enter your email" required />
<button type="submit">Send Reset Link</button>
</form>
<script>
document.getElementById('forgotPasswordForm').addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('email').value;
try {
const response = await fetch('/api/v1/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
const data = await response.json();
// Always shows success message - doesn't reveal if email exists
alert('If the email exists, a reset link has been sent.');
} catch (error) {
console.error('Error:', error);
alert('An error occurred. Please try again.');
}
});
</script><form id="resetPasswordForm">
<input
type="password"
id="password"
placeholder="New Password (min 8 chars, 1 uppercase, 1 number, 1 special char)"
required
/>
<button type="submit">Reset Password</button>
</form>
<script>
// Extract token from URL
const urlParams = new URLSearchParams(window.location.search);
const token = new URL(window.location).pathname.split('/').pop(); // or from URL
document.getElementById('resetPasswordForm').addEventListener('submit', async (e) => {
e.preventDefault();
const password = document.getElementById('password').value;
try {
const response = await fetch(`/api/v1/auth/reset-password/${token}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password })
});
const data = await response.json();
if (data.success) {
alert('Password reset successful! You can now login with your new password.');
window.location.href = '/login';
} else {
alert(data.message); // Show error message
}
} catch (error) {
console.error('Error:', error);
alert('An error occurred. Please try again.');
}
});
</script>curl -X POST http://localhost:5000/api/v1/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com"}'
# Response:
# {
# "success": true,
# "message": "If the email exists, a reset link has been sent.",
# "data": {}
# }# Use the token from the email link you received
TOKEN="your-token-from-email"
curl -X POST http://localhost:5000/api/v1/auth/reset-password/$TOKEN \
-H "Content-Type: application/json" \
-d '{"password": "NewPassword123@"}'
# Response:
# {
# "success": true,
# "message": "Password reset successful",
# "data": {}
# }curl -X POST http://localhost:5000/api/v1/user/login \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "password": "NewPassword123@"}'
# Response should contain token and user data-
Create Forgot Password Request
- Method: POST
- URL:
http://localhost:5000/api/v1/auth/forgot-password - Body (raw JSON):
{"email": "test@example.com"}
-
Create Reset Password Request
- Method: POST
- URL:
http://localhost:5000/api/v1/auth/reset-password/{{token}} - Body (raw JSON):
{"password": "NewPassword123@"} - Replace
{{token}}with the token from email
Before deploying to production:
- Set strong
JWT_SECRETin.env - Set strong
SERVER_HMAC_SECRETin.env - Configure SMTP with real email service
- Set
FRONTEND_URLto your production domain - Enable HTTPS (required for production)
- Set
NODE_ENV=production - Enable CORS only for your frontend domain
- Regularly rotate SMTP credentials
- Monitor failed reset attempts
- Set up email rate limiting (recommended)
LocalMind-Backend/
├── src/
│ ├── api/
│ │ └── v1/
│ │ └── user/
│ │ ├── user.model.ts (✏️ MODIFIED - added reset fields)
│ │ ├── user.type.ts (✏️ MODIFIED - added interface)
│ │ ├── user.constant.ts (✏️ MODIFIED - added messages)
│ │ ├── user.controller.ts (✏️ MODIFIED - added methods)
│ │ ├── user.routes.ts (✏️ MODIFIED - added endpoints)
│ │ └── user.validator.ts (✏️ MODIFIED - added schemas)
│ ├── services/
│ │ └── password-reset.service.ts (✨ NEW)
│ ├── utils/
│ │ ├── email.utils.ts (✨ NEW)
│ │ └── SendResponse.utils.ts
│ ├── constant/
│ │ └── env.constant.ts
│ ├── validator/
│ │ └── env.ts (✏️ MODIFIED - added email vars)
│ └── ...
├── tsconfig.json (✏️ MODIFIED - added Node types)
├── PASSWORD_RESET_API.md (✨ NEW - Full documentation)
└── ...
- Check email configuration in
.env - For Gmail: Use App Password, not regular password
- Check server logs:
npm run dev
- Verify email service is accessible from your network
- Check email in spam/junk folder
- Token expires after 15 minutes
- User must complete reset within this time
- Token can only be used once
Password must have:
- 8-20 characters
- At least 1 uppercase letter (A-Z)
- At least 1 lowercase letter (a-z)
- At least 1 number (0-9)
- At least 1 special character (@$!%*?&)
Edit src/api/v1/user/user.constant.ts:
export const ResetPasswordConfig = {
tokenLength: 32,
expiryMinutes: 15, // ← Change this value
}Edit src/utils/email.utils.ts, modify the htmlContent in sendPasswordResetEmail() method.
// In user.routes.ts
import rateLimit from 'express-rate-limit';
const forgotPasswordLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 3, // 3 requests per 15 minutes
message: 'Too many password reset requests, please try again later.'
});
router.post('/v1/auth/forgot-password', forgotPasswordLimiter, userController.forgotPassword);{
"success": true,
"message": "Password reset successful",
"data": {}
}{
"success": false,
"message": "Invalid or expired reset token"
}For issues or questions:
- Check
PASSWORD_RESET_API.mdfor detailed documentation - Review error messages in server logs
- Verify environment variables are set correctly
- Test with cURL before integrating into frontend
Version: 1.0
Last Updated: January 2025