12 KiB
Chức năng 6: Quên Mật Khẩu (Forgot Password) - Hoàn Thành ✅
📦 Files Đã Tạo/Cập Nhật
Frontend
client/src/pages/auth/ForgotPasswordPage.tsx- Component form quên mật khẩuclient/src/pages/auth/index.ts- Export ForgotPasswordPageclient/src/App.tsx- Route/forgot-password
Backend
server/src/controllers/authController.js- forgotPassword() & resetPassword()server/src/routes/authRoutes.js- Routes cho forgot/reset password
✨ Tính Năng Chính
1. Form State (Initial)
┌─────────────────────────────────────┐
│ 🏨 Hotel Icon (Blue) │
│ Quên mật khẩu? │
│ Nhập email để nhận link... │
├─────────────────────────────────────┤
│ ┌───────────────────────────────┐ │
│ │ Email │ │
│ │ [📧 email@example.com ] │ │
│ ├───────────────────────────────┤ │
│ │ [📤 Gửi link đặt lại MK] │ │
│ ├───────────────────────────────┤ │
│ │ ← Quay lại đăng nhập │ │
│ └───────────────────────────────┘ │
│ Chưa có tài khoản? Đăng ký ngay │
└─────────────────────────────────────┘
2. Success State (After Submit)
┌─────────────────────────────────────┐
│ ✅ Success Icon │
│ │
│ Email đã được gửi! │
│ Chúng tôi đã gửi link đến │
│ user@example.com │
├─────────────────────────────────────┤
│ ℹ️ Lưu ý: │
│ • Link có hiệu lực trong 1 giờ │
│ • Kiểm tra cả thư mục Spam/Junk │
│ • Nếu không nhận được, thử lại │
├─────────────────────────────────────┤
│ [📧 Gửi lại email] │
│ [← Quay lại đăng nhập] │
└─────────────────────────────────────┘
3. Two-State Design Pattern
✅ Form State - Nhập email ✅ Success State - Hiển thị xác nhận & hướng dẫn
State management:
const [isSuccess, setIsSuccess] = useState(false);
const [submittedEmail, setSubmittedEmail] = useState('');
🔧 Features Chi Tiết
1. Validation (Yup Schema)
email:
- Required: "Email là bắt buộc"
- Valid format: "Email không hợp lệ"
- Trim whitespace
2. Form Field
- Email input với Mail icon
- Auto-focus khi load page
- Validation real-time
- Error message inline
3. Submit Button States
{isLoading ? (
<>
<Loader2 className="animate-spin" />
Đang xử lý...
</>
) : (
<>
<Send />
Gửi link đặt lại mật khẩu
</>
)}
4. Success Features
✅ Visual Confirmation
- Green checkmark icon
- Success message
- Display submitted email
✅ User Instructions
- Link expires in 1 hour
- Check spam folder
- Can resend email
✅ Action Buttons
- "Gửi lại email" - Reset to form state
- "Quay lại đăng nhập" - Navigate to /login
5. Help Section
<div className="bg-white rounded-lg shadow-sm border">
<h3>Cần trợ giúp?</h3>
<p>
Liên hệ: support@hotel.com hoặc 1900-xxxx
</p>
</div>
🔗 Backend Integration
API Endpoint: Forgot Password
POST /api/auth/forgot-password
Request Body:
{
"email": "user@example.com"
}
Response (Success):
{
"status": "success",
"message": "Password reset link has been sent to your email"
}
Implementation:
const forgotPassword = async (req, res, next) => {
// 1. Find user by email
// 2. Generate crypto reset token (32 bytes)
// 3. Hash token with SHA256
// 4. Save to password_reset_tokens table
// 5. Expires in 1 hour
// 6. TODO: Send email with reset link
// 7. Return success (prevent email enumeration)
};
API Endpoint: Reset Password
POST /api/auth/reset-password
Request Body:
{
"token": "reset_token_from_email",
"password": "NewPassword123@"
}
Response (Success):
{
"status": "success",
"message": "Password has been reset successfully"
}
Implementation:
const resetPassword = async (req, res, next) => {
// 1. Hash incoming token
// 2. Find valid token in DB (not expired)
// 3. Find user by user_id
// 4. Hash new password with bcrypt
// 5. Update user password
// 6. Delete used token
// 7. TODO: Send confirmation email
// 8. Return success
};
🔐 Security Features
1. Token Generation
// Generate random 32-byte token
const resetToken = crypto.randomBytes(32).toString('hex');
// Hash with SHA256 before storing
const hashedToken = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex');
2. Token Storage
- Stored in
password_reset_tokenstable - Hashed (SHA256)
- Expires in 1 hour
- One token per user (old tokens deleted)
3. Email Enumeration Prevention
// Always return success, even if email not found
if (!user) {
return res.status(200).json({
status: 'success',
message: 'If email exists, reset link has been sent'
});
}
4. Token Validation
// Check token exists and not expired
const resetToken = await PasswordResetToken.findOne({
where: {
token: hashedToken,
expires_at: { [Op.gt]: new Date() }
}
});
📊 Database Schema
password_reset_tokens Table
CREATE TABLE password_reset_tokens (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
token VARCHAR(255) NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
🎨 Design & Styling
Color Scheme:
- Primary: blue-600, blue-700
- Background: gradient from-blue-50 to-indigo-100
- Success: green-100, green-600
- Info: blue-50, blue-200
- Text: gray-600, gray-700, gray-900
Icons:
- 🏨 Hotel - Main logo
- 📧 Mail - Email input
- 📤 Send - Submit button
- ⏳ Loader2 - Loading spinner
- ✅ CheckCircle - Success state
- ← ArrowLeft - Back navigation
🔄 User Flow
1. User visits /forgot-password
↓
2. Enter email address
↓
3. Click "Gửi link đặt lại mật khẩu"
↓
4. Frontend validation (Yup)
↓
5. Call useAuthStore.forgotPassword()
↓
6. API POST /api/auth/forgot-password
↓
7. Backend:
- Generate token
- Save to DB
- TODO: Send email
↓
8. Frontend shows success state
↓
9. User receives email (TODO)
↓
10. Click link → /reset-password/:token
↓
11. Enter new password (Chức năng 7)
🧪 Test Scenarios
Test Case 1: Valid email
Input: email = "user@example.com"
Expected:
- API called successfully
- Success state shown
- Email displayed correctly
- Instructions visible
Test Case 2: Invalid email format
Input: email = "notanemail"
Expected:
- Validation error: "Email không hợp lệ"
- Form not submitted
Test Case 3: Empty email
Input: email = ""
Expected:
- Validation error: "Email là bắt buộc"
- Form not submitted
Test Case 4: Loading state
Action: Submit form
Expected:
- Button disabled
- Spinner shows
- Text: "Đang xử lý..."
Test Case 5: Resend email
Action: Click "Gửi lại email" in success state
Expected:
- Return to form state
- Email field cleared
- Error cleared
Test Case 6: Back to login
Action: Click "Quay lại đăng nhập"
Expected:
- Navigate to /login
Test Case 7: Email not found (Backend)
Input: email = "nonexistent@example.com"
Expected:
- Still return success (security)
- No error shown to user
Test Case 8: Token expiry (Backend)
Scenario: Token created 2 hours ago
Expected:
- Reset fails
- Error: "Invalid or expired reset token"
📝 Code Quality
✅ TypeScript: Full type safety ✅ React Hook Form: Optimized performance ✅ Yup Validation: Schema-based validation ✅ State Management: Local state for success toggle ✅ Error Handling: Try-catch blocks ✅ Security: Token hashing, email enumeration prevention ✅ UX: Clear instructions, help section ✅ Accessibility: Labels, autocomplete, focus management ✅ Responsive: Mobile-friendly ✅ 80 chars/line: Code formatting
🔗 Integration Points
useAuthStore.forgotPassword()
const { forgotPassword, isLoading, error, clearError } =
useAuthStore();
await forgotPassword({ email: data.email });
Success Handling
await forgotPassword({ email: data.email });
setIsSuccess(true); // Show success state
Error Handling
try {
await forgotPassword({ email });
} catch (error) {
// Error displayed via store.error
console.error('Forgot password error:', error);
}
🚀 Usage
Test Frontend
URL: http://localhost:5173/forgot-password
Test Data:
Email: admin@hotel.com (from seed data)
Test Backend API
curl -X POST http://localhost:3000/api/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{"email":"admin@hotel.com"}'
Expected response:
{
"status": "success",
"message": "Password reset link has been sent to your email"
}
Ensure SMTP is configured in server/.env and that the server
is able to send emails. The application must never log raw reset
tokens or reset URLs in production.
⚠️ TODO: Email Service
The project must send real emails via SMTP in production and must never expose raw reset tokens in logs. To enable email sending:
-
Install
nodemailerin theserverpackage and configure SMTP credentials inserver/.env(MAIL_HOST,MAIL_PORT,MAIL_USER,MAIL_PASS,MAIL_FROM). Do not commit these credentials to source control. -
Implement a mail helper (e.g.
server/src/utils/mailer.js) that uses the SMTP settings. Ensure it throws or fails when SMTP credentials are missing so that emails are not silently dropped. -
Use the mail helper to send the reset email with a link built as
${process.env.CLIENT_URL}/reset-password/${resetToken}. -
Important: never log the raw
resetTokenor the reset URL. If email sending fails, log a generic error and surface a safe message to the user.
✅ Checklist
- ✅ Create ForgotPasswordPage.tsx
- ✅ Implement React Hook Form
- ✅ Add Yup validation
- ✅ Two-state design (form + success)
- ✅ Loading state
- ✅ Error display
- ✅ Success state with instructions
- ✅ Resend email button
- ✅ Back to login navigation
- ✅ Help section
- ✅ Integration with useAuthStore
- ✅ Add route to App.tsx
- ✅ Backend: forgotPassword() method
- ✅ Backend: resetPassword() method
- ✅ Backend: Routes added
- ✅ Token generation & hashing
- ✅ Token expiry (1 hour)
- ✅ Security: Email enumeration prevention
- ⏳ TODO: Send actual email (nodemailer)
- ⏳ TODO: Email templates
📚 Related Files
client/src/pages/auth/LoginPage.tsx- Login formclient/src/pages/auth/RegisterPage.tsx- Register formclient/src/utils/validationSchemas.ts- Validation schemasclient/src/store/useAuthStore.ts- Auth stateserver/src/controllers/authController.js- Auth logicserver/src/routes/authRoutes.js- Auth routesserver/src/databases/models/PasswordResetToken.js- Token model
Status: ✅ Chức năng 6 hoàn thành Next: Chức năng 7 - Reset Password (form to change password with token) Test URL: http://localhost:5173/forgot-password API: POST /api/auth/forgot-password