Hotel Booking
This commit is contained in:
488
docs/FORGOT_PASSWORD_COMPLETE.md
Normal file
488
docs/FORGOT_PASSWORD_COMPLETE.md
Normal file
@@ -0,0 +1,488 @@
|
||||
# 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
|
||||
1. **`client/src/pages/auth/ForgotPasswordPage.tsx`** - Component form quên mật khẩu
|
||||
2. **`client/src/pages/auth/index.ts`** - Export ForgotPasswordPage
|
||||
3. **`client/src/App.tsx`** - Route `/forgot-password`
|
||||
|
||||
### Backend
|
||||
4. **`server/src/controllers/authController.js`** - forgotPassword() & resetPassword()
|
||||
5. **`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:
|
||||
```typescript
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [submittedEmail, setSubmittedEmail] = useState('');
|
||||
```
|
||||
|
||||
## 🔧 Features Chi Tiết
|
||||
|
||||
### 1. Validation (Yup Schema)
|
||||
```typescript
|
||||
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
|
||||
```tsx
|
||||
{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
|
||||
```tsx
|
||||
<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:**
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (Success):**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Password reset link has been sent to your email"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
```javascript
|
||||
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:**
|
||||
```json
|
||||
{
|
||||
"token": "reset_token_from_email",
|
||||
"password": "NewPassword123@"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (Success):**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Password has been reset successfully"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
```javascript
|
||||
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
|
||||
```javascript
|
||||
// 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_tokens` table
|
||||
- Hashed (SHA256)
|
||||
- Expires in 1 hour
|
||||
- One token per user (old tokens deleted)
|
||||
|
||||
### 3. Email Enumeration Prevention
|
||||
```javascript
|
||||
// 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
|
||||
```javascript
|
||||
// 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
|
||||
```sql
|
||||
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()
|
||||
```typescript
|
||||
const { forgotPassword, isLoading, error, clearError } =
|
||||
useAuthStore();
|
||||
|
||||
await forgotPassword({ email: data.email });
|
||||
```
|
||||
|
||||
### Success Handling
|
||||
```typescript
|
||||
await forgotPassword({ email: data.email });
|
||||
setIsSuccess(true); // Show success state
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
```typescript
|
||||
try {
|
||||
await forgotPassword({ email });
|
||||
} catch (error) {
|
||||
// Error displayed via store.error
|
||||
console.error('Forgot password error:', error);
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
### Test Frontend
|
||||
```bash
|
||||
URL: http://localhost:5173/forgot-password
|
||||
|
||||
Test Data:
|
||||
Email: admin@hotel.com (from seed data)
|
||||
```
|
||||
|
||||
### Test Backend API
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/forgot-password \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"admin@hotel.com"}'
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
1. Install `nodemailer` in the `server` package and configure
|
||||
SMTP credentials in `server/.env` (`MAIL_HOST`, `MAIL_PORT`,
|
||||
`MAIL_USER`, `MAIL_PASS`, `MAIL_FROM`). Do not commit these
|
||||
credentials to source control.
|
||||
|
||||
2. 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.
|
||||
|
||||
3. Use the mail helper to send the reset email with a link built as
|
||||
`${process.env.CLIENT_URL}/reset-password/${resetToken}`.
|
||||
|
||||
4. Important: never log the raw `resetToken` or the reset URL. If
|
||||
email sending fails, log a generic error and surface a safe
|
||||
message to the user.
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [x] ✅ Create ForgotPasswordPage.tsx
|
||||
- [x] ✅ Implement React Hook Form
|
||||
- [x] ✅ Add Yup validation
|
||||
- [x] ✅ Two-state design (form + success)
|
||||
- [x] ✅ Loading state
|
||||
- [x] ✅ Error display
|
||||
- [x] ✅ Success state with instructions
|
||||
- [x] ✅ Resend email button
|
||||
- [x] ✅ Back to login navigation
|
||||
- [x] ✅ Help section
|
||||
- [x] ✅ Integration with useAuthStore
|
||||
- [x] ✅ Add route to App.tsx
|
||||
- [x] ✅ Backend: forgotPassword() method
|
||||
- [x] ✅ Backend: resetPassword() method
|
||||
- [x] ✅ Backend: Routes added
|
||||
- [x] ✅ Token generation & hashing
|
||||
- [x] ✅ Token expiry (1 hour)
|
||||
- [x] ✅ Security: Email enumeration prevention
|
||||
- [ ] ⏳ TODO: Send actual email (nodemailer)
|
||||
- [ ] ⏳ TODO: Email templates
|
||||
|
||||
## 📚 Related Files
|
||||
|
||||
- `client/src/pages/auth/LoginPage.tsx` - Login form
|
||||
- `client/src/pages/auth/RegisterPage.tsx` - Register form
|
||||
- `client/src/utils/validationSchemas.ts` - Validation schemas
|
||||
- `client/src/store/useAuthStore.ts` - Auth state
|
||||
- `server/src/controllers/authController.js` - Auth logic
|
||||
- `server/src/routes/authRoutes.js` - Auth routes
|
||||
- `server/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
|
||||
175
docs/LAYOUT_IMPLEMENTATION.md
Normal file
175
docs/LAYOUT_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Layout Components - Chức năng 1
|
||||
|
||||
## Tổng quan
|
||||
Đã triển khai thành công **Chức năng 1: Layout cơ bản** bao gồm:
|
||||
|
||||
### Components đã tạo
|
||||
|
||||
#### 1. **Header** (`src/components/layout/Header.tsx`)
|
||||
- Logo và tên ứng dụng
|
||||
- Sticky header với shadow
|
||||
- Responsive design
|
||||
- Links cơ bản (Trang chủ, Phòng, Đặt phòng)
|
||||
|
||||
#### 2. **Footer** (`src/components/layout/Footer.tsx`)
|
||||
- Thông tin công ty
|
||||
- Quick links (Liên kết nhanh)
|
||||
- Support links (Hỗ trợ)
|
||||
- Contact info (Thông tin liên hệ)
|
||||
- Social media icons
|
||||
- Copyright info
|
||||
- Fully responsive (4 columns → 2 → 1)
|
||||
|
||||
#### 3. **Navbar** (`src/components/layout/Navbar.tsx`)
|
||||
- **Trạng thái chưa đăng nhập**:
|
||||
- Hiển thị nút "Đăng nhập" và "Đăng ký"
|
||||
- **Trạng thái đã đăng nhập**:
|
||||
- Hiển thị avatar/tên user
|
||||
- Dropdown menu với "Hồ sơ", "Quản trị" (admin), "Đăng xuất"
|
||||
- Mobile menu với hamburger icon
|
||||
- Responsive cho desktop và mobile
|
||||
|
||||
#### 4. **SidebarAdmin** (`src/components/layout/SidebarAdmin.tsx`)
|
||||
- Chỉ hiển thị cho role = "admin"
|
||||
- Collapsible sidebar (mở/đóng)
|
||||
- Menu items: Dashboard, Users, Rooms, Bookings, Payments, Services, Promotions, Banners, Reports, Settings
|
||||
- Active state highlighting
|
||||
- Responsive design
|
||||
|
||||
#### 5. **LayoutMain** (`src/components/layout/LayoutMain.tsx`)
|
||||
- Tích hợp Header, Navbar, Footer
|
||||
- Sử dụng `<Outlet />` để render nội dung động
|
||||
- Props: `isAuthenticated`, `userInfo`, `onLogout`
|
||||
- Min-height 100vh với flex layout
|
||||
|
||||
### Cấu trúc thư mục
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ └── layout/
|
||||
│ ├── Header.tsx
|
||||
│ ├── Footer.tsx
|
||||
│ ├── Navbar.tsx
|
||||
│ ├── SidebarAdmin.tsx
|
||||
│ ├── LayoutMain.tsx
|
||||
│ └── index.ts
|
||||
├── pages/
|
||||
│ ├── HomePage.tsx
|
||||
│ └── AdminLayout.tsx
|
||||
├── styles/
|
||||
│ └── index.css
|
||||
├── App.tsx
|
||||
└── main.tsx
|
||||
```
|
||||
|
||||
### Cách sử dụng
|
||||
|
||||
#### 1. Import Layout vào App
|
||||
```tsx
|
||||
import LayoutMain from './components/layout/LayoutMain';
|
||||
|
||||
// Trong Routes
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<LayoutMain
|
||||
isAuthenticated={isAuthenticated}
|
||||
userInfo={userInfo}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Route index element={<HomePage />} />
|
||||
{/* Các route con khác */}
|
||||
</Route>
|
||||
```
|
||||
|
||||
#### 2. Sử dụng SidebarAdmin cho trang Admin
|
||||
```tsx
|
||||
import SidebarAdmin from '../components/layout/SidebarAdmin';
|
||||
|
||||
const AdminLayout = () => (
|
||||
<div className="flex h-screen">
|
||||
<SidebarAdmin />
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
### Tính năng đã hoàn thành ✅
|
||||
|
||||
- [x] Tạo thư mục `src/components/layout/`
|
||||
- [x] Header.tsx với logo và navigation
|
||||
- [x] Footer.tsx với thông tin đầy đủ
|
||||
- [x] Navbar.tsx với logic đăng nhập/đăng xuất động
|
||||
- [x] SidebarAdmin.tsx chỉ hiển thị với role admin
|
||||
- [x] LayoutMain.tsx sử dụng `<Outlet />`
|
||||
- [x] Navbar thay đổi theo trạng thái đăng nhập
|
||||
- [x] Giao diện responsive, tương thích desktop/mobile
|
||||
- [x] Tích hợp TailwindCSS cho styling
|
||||
- [x] Export tất cả components qua index.ts
|
||||
|
||||
### Demo Routes đã tạo
|
||||
|
||||
**Public Routes** (với LayoutMain):
|
||||
- `/` - Trang chủ
|
||||
- `/rooms` - Danh sách phòng
|
||||
- `/bookings` - Đặt phòng
|
||||
- `/about` - Giới thiệu
|
||||
|
||||
**Auth Routes** (không có layout):
|
||||
- `/login` - Đăng nhập
|
||||
- `/register` - Đăng ký
|
||||
- `/forgot-password` - Quên mật khẩu
|
||||
|
||||
**Admin Routes** (với SidebarAdmin):
|
||||
- `/admin/dashboard` - Dashboard
|
||||
- `/admin/users` - Quản lý người dùng
|
||||
- `/admin/rooms` - Quản lý phòng
|
||||
- `/admin/bookings` - Quản lý đặt phòng
|
||||
- `/admin/payments` - Quản lý thanh toán
|
||||
- `/admin/services` - Quản lý dịch vụ
|
||||
- `/admin/promotions` - Quản lý khuyến mãi
|
||||
- `/admin/banners` - Quản lý banner
|
||||
|
||||
### Chạy ứng dụng
|
||||
|
||||
```bash
|
||||
# Di chuyển vào thư mục client
|
||||
cd client
|
||||
|
||||
# Cài đặt dependencies (nếu chưa cài)
|
||||
npm install
|
||||
|
||||
# Chạy development server
|
||||
npm run dev
|
||||
|
||||
# Mở trình duyệt tại: http://localhost:5173
|
||||
```
|
||||
|
||||
### Các bước tiếp theo
|
||||
|
||||
**Chức năng 2**: Cấu hình Routing (react-router-dom)
|
||||
- ProtectedRoute component
|
||||
- AdminRoute component
|
||||
- Redirect logic
|
||||
|
||||
**Chức năng 3**: useAuthStore (Zustand Store)
|
||||
- Quản lý authentication state
|
||||
- Login/Logout functions
|
||||
- Persist state trong localStorage
|
||||
|
||||
**Chức năng 4-8**: Auth Forms
|
||||
- LoginPage
|
||||
- RegisterPage
|
||||
- ForgotPasswordPage
|
||||
- ResetPasswordPage
|
||||
|
||||
### Notes
|
||||
- Layout components được thiết kế để tái sử dụng
|
||||
- Props-based design cho flexibility
|
||||
- Sẵn sàng tích hợp với Zustand store
|
||||
- Tailwind classes tuân thủ 80 ký tự/dòng
|
||||
- Icons sử dụng lucide-react (đã có trong dependencies)
|
||||
432
docs/LOGIN_FORM_GUIDE.md
Normal file
432
docs/LOGIN_FORM_GUIDE.md
Normal file
@@ -0,0 +1,432 @@
|
||||
# Chức năng 4: Form Đăng Nhập - Hướng Dẫn Sử Dụng
|
||||
|
||||
## 📋 Tổng Quan
|
||||
|
||||
Form đăng nhập đã được triển khai đầy đủ với:
|
||||
- ✅ Validation form bằng React Hook Form + Yup
|
||||
- ✅ Hiển thị/ẩn mật khẩu
|
||||
- ✅ Checkbox "Nhớ đăng nhập" (7 ngày)
|
||||
- ✅ Loading state trong quá trình đăng nhập
|
||||
- ✅ Hiển thị lỗi từ server
|
||||
- ✅ Redirect sau khi đăng nhập thành công
|
||||
- ✅ UI đẹp với Tailwind CSS và Lucide Icons
|
||||
- ✅ Responsive design
|
||||
|
||||
## 🗂️ Các File Đã Tạo/Cập Nhật
|
||||
|
||||
### 1. **LoginPage.tsx** - Component form đăng nhập
|
||||
**Đường dẫn:** `client/src/pages/auth/LoginPage.tsx`
|
||||
|
||||
```typescript
|
||||
// Các tính năng chính:
|
||||
- React Hook Form với Yup validation
|
||||
- Show/hide password toggle
|
||||
- Remember me checkbox
|
||||
- Loading state với spinner
|
||||
- Error handling
|
||||
- Redirect với location state
|
||||
```
|
||||
|
||||
### 2. **index.ts** - Export module
|
||||
**Đường dẫn:** `client/src/pages/auth/index.ts`
|
||||
|
||||
```typescript
|
||||
export { default as LoginPage } from './LoginPage';
|
||||
```
|
||||
|
||||
### 3. **App.tsx** - Đã cập nhật routing
|
||||
**Đường dẫn:** `client/src/App.tsx`
|
||||
|
||||
```typescript
|
||||
// Đã thêm:
|
||||
import { LoginPage } from './pages/auth';
|
||||
|
||||
// Route:
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
```
|
||||
|
||||
## 🎨 Cấu Trúc UI
|
||||
|
||||
### Layout
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 🏨 Hotel Icon │
|
||||
│ Đăng nhập │
|
||||
│ Chào mừng bạn trở lại... │
|
||||
├─────────────────────────────────────┤
|
||||
│ ┌───────────────────────────────┐ │
|
||||
│ │ [Error message if any] │ │
|
||||
│ ├───────────────────────────────┤ │
|
||||
│ │ Email │ │
|
||||
│ │ [📧 email@example.com ] │ │
|
||||
│ ├───────────────────────────────┤ │
|
||||
│ │ Mật khẩu │ │
|
||||
│ │ [🔒 •••••••• 👁️] │ │
|
||||
│ ├───────────────────────────────┤ │
|
||||
│ │ ☑️ Nhớ đăng nhập │ │
|
||||
│ │ Quên mật khẩu? → │ │
|
||||
│ ├───────────────────────────────┤ │
|
||||
│ │ [🔐 Đăng nhập] │ │
|
||||
│ └───────────────────────────────┘ │
|
||||
│ Chưa có tài khoản? Đăng ký ngay │
|
||||
│ │
|
||||
│ Điều khoản & Chính sách bảo mật │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🔧 Cách Sử Dụng
|
||||
|
||||
### 1. Truy Cập Form
|
||||
|
||||
```bash
|
||||
# URL
|
||||
http://localhost:5173/login
|
||||
```
|
||||
|
||||
### 2. Các Trường Trong Form
|
||||
|
||||
| Trường | Type | Bắt buộc | Validation |
|
||||
|--------|------|----------|------------|
|
||||
| Email | text | ✅ | Email hợp lệ |
|
||||
| Password | password | ✅ | Min 8 ký tự |
|
||||
| Remember Me | checkbox | ❌ | Boolean |
|
||||
|
||||
### 3. Validation Rules
|
||||
|
||||
**Email:**
|
||||
```typescript
|
||||
- Required: "Email là bắt buộc"
|
||||
- Valid email format: "Email không hợp lệ"
|
||||
- Trim whitespace
|
||||
```
|
||||
|
||||
**Password:**
|
||||
```typescript
|
||||
- Required: "Mật khẩu là bắt buộc"
|
||||
- Min 8 characters: "Mật khẩu phải có ít nhất 8 ký tự"
|
||||
```
|
||||
|
||||
### 4. Luồng Đăng Nhập
|
||||
|
||||
```
|
||||
1. User nhập email + password
|
||||
2. Click "Đăng nhập"
|
||||
3. Validation form (client-side)
|
||||
4. Nếu valid:
|
||||
- Button disabled + hiển thị loading
|
||||
- Gọi useAuthStore.login()
|
||||
- API POST /api/auth/login
|
||||
5. Nếu thành công:
|
||||
- Lưu token vào localStorage
|
||||
- Update Zustand state
|
||||
- Redirect đến /dashboard
|
||||
6. Nếu lỗi:
|
||||
- Hiển thị error message
|
||||
- Button enabled lại
|
||||
```
|
||||
|
||||
## 🎯 Tính Năng Chính
|
||||
|
||||
### 1. Show/Hide Password
|
||||
|
||||
```typescript
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
// Toggle button
|
||||
<button onClick={() => setShowPassword(!showPassword)}>
|
||||
{showPassword ? <EyeOff /> : <Eye />}
|
||||
</button>
|
||||
|
||||
// Input type
|
||||
<input type={showPassword ? 'text' : 'password'} />
|
||||
```
|
||||
|
||||
### 2. Remember Me (7 ngày)
|
||||
|
||||
```typescript
|
||||
// Checkbox
|
||||
<input {...register('rememberMe')} type="checkbox" />
|
||||
|
||||
// Logic trong authService.login()
|
||||
if (rememberMe) {
|
||||
// Token sẽ được lưu trong localStorage
|
||||
// và không bị xóa khi đóng trình duyệt
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Loading State
|
||||
|
||||
```typescript
|
||||
// Button disabled khi đang loading
|
||||
<button disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="animate-spin" />
|
||||
Đang xử lý...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LogIn />
|
||||
Đăng nhập
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
```
|
||||
|
||||
### 4. Error Handling
|
||||
|
||||
```typescript
|
||||
// Error từ Zustand store
|
||||
const { error } = useAuthStore();
|
||||
|
||||
// Hiển thị error message
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
### 5. Redirect Logic
|
||||
|
||||
```typescript
|
||||
// Lấy location state từ ProtectedRoute
|
||||
const location = useLocation();
|
||||
|
||||
// Redirect về trang trước đó hoặc dashboard
|
||||
const from = location.state?.from?.pathname || '/dashboard';
|
||||
navigate(from, { replace: true });
|
||||
```
|
||||
|
||||
## 🔗 Integration với Zustand Store
|
||||
|
||||
```typescript
|
||||
// Hook usage
|
||||
const {
|
||||
login, // Function để login
|
||||
isLoading, // Loading state
|
||||
error, // Error message
|
||||
clearError // Clear error
|
||||
} = useAuthStore();
|
||||
|
||||
// Login call
|
||||
await login({
|
||||
email: 'user@example.com',
|
||||
password: 'password123',
|
||||
rememberMe: true
|
||||
});
|
||||
```
|
||||
|
||||
## 🎨 Styling với Tailwind
|
||||
|
||||
### Color Scheme
|
||||
```
|
||||
- Primary: blue-600, blue-700
|
||||
- Background: gradient from-blue-50 to-indigo-100
|
||||
- Error: red-50, red-200, red-600, red-700
|
||||
- Text: gray-600, gray-700, gray-900
|
||||
```
|
||||
|
||||
### Responsive Design
|
||||
```typescript
|
||||
// Container
|
||||
className="max-w-md w-full" // Max width 28rem
|
||||
|
||||
// Grid (nếu có)
|
||||
className="grid grid-cols-1 md:grid-cols-2"
|
||||
```
|
||||
|
||||
## 🧪 Testing Scenarios
|
||||
|
||||
### 1. Validation Testing
|
||||
|
||||
**Test Case 1: Empty form**
|
||||
```
|
||||
- Input: Submit form trống
|
||||
- Expected: Hiển thị lỗi "Email là bắt buộc"
|
||||
```
|
||||
|
||||
**Test Case 2: Invalid email**
|
||||
```
|
||||
- Input: Email = "notanemail"
|
||||
- Expected: "Email không hợp lệ"
|
||||
```
|
||||
|
||||
**Test Case 3: Short password**
|
||||
```
|
||||
- Input: Password = "123"
|
||||
- Expected: "Mật khẩu phải có ít nhất 8 ký tự"
|
||||
```
|
||||
|
||||
### 2. Authentication Testing
|
||||
|
||||
**Test Case 4: Valid credentials**
|
||||
```
|
||||
- Input: Valid email + password
|
||||
- Expected: Redirect to /dashboard
|
||||
```
|
||||
|
||||
**Test Case 5: Invalid credentials**
|
||||
```
|
||||
- Input: Wrong password
|
||||
- Expected: Error message từ server
|
||||
```
|
||||
|
||||
**Test Case 6: Network error**
|
||||
```
|
||||
- Input: Server offline
|
||||
- Expected: Error message "Có lỗi xảy ra"
|
||||
```
|
||||
|
||||
### 3. UX Testing
|
||||
|
||||
**Test Case 7: Show/hide password**
|
||||
```
|
||||
- Action: Click eye icon
|
||||
- Expected: Password text visible/hidden
|
||||
```
|
||||
|
||||
**Test Case 8: Remember me**
|
||||
```
|
||||
- Action: Check "Nhớ đăng nhập"
|
||||
- Expected: Token persist sau khi reload
|
||||
```
|
||||
|
||||
**Test Case 9: Loading state**
|
||||
```
|
||||
- Action: Submit form
|
||||
- Expected: Button disabled, spinner hiển thị
|
||||
```
|
||||
|
||||
## 🔐 Security Features
|
||||
|
||||
### 1. Password Visibility
|
||||
```typescript
|
||||
// Default: password hidden
|
||||
type="password"
|
||||
|
||||
// Toggle: user control
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
```
|
||||
|
||||
### 2. HTTPS Only (Production)
|
||||
```typescript
|
||||
// Trong .env
|
||||
VITE_API_URL=https://api.yourdomain.com
|
||||
```
|
||||
|
||||
### 3. Token Storage
|
||||
```typescript
|
||||
// LocalStorage cho remember me
|
||||
if (rememberMe) {
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
// SessionStorage cho session only
|
||||
else {
|
||||
sessionStorage.setItem('token', token);
|
||||
}
|
||||
```
|
||||
|
||||
## 📝 Best Practices
|
||||
|
||||
### 1. Form Validation
|
||||
```typescript
|
||||
✅ Client-side validation (Yup)
|
||||
✅ Server-side validation (Express validator)
|
||||
✅ Immediate feedback
|
||||
✅ Clear error messages
|
||||
```
|
||||
|
||||
### 2. Error Handling
|
||||
```typescript
|
||||
✅ Try-catch blocks
|
||||
✅ User-friendly messages
|
||||
✅ Clear error state
|
||||
✅ Console logging for debugging
|
||||
```
|
||||
|
||||
### 3. UX
|
||||
```typescript
|
||||
✅ Loading indicators
|
||||
✅ Disabled states
|
||||
✅ Auto-focus first field
|
||||
✅ Enter key submit
|
||||
✅ Remember form state
|
||||
```
|
||||
|
||||
## 🚀 Next Steps (Chức năng 5-7)
|
||||
|
||||
1. **Chức năng 5: Form Register**
|
||||
- Copy structure từ LoginPage
|
||||
- Thêm fields: name, phone, confirmPassword
|
||||
- Use registerSchema
|
||||
- Redirect to /login after success
|
||||
|
||||
2. **Chức năng 6: Forgot Password**
|
||||
- Simple form với email only
|
||||
- Send reset link
|
||||
- Success message
|
||||
|
||||
3. **Chức năng 7: Reset Password**
|
||||
- Form với password + confirmPassword
|
||||
- Token từ URL params
|
||||
- Redirect to /login after success
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Issue 1: "Error: Token expired"
|
||||
```typescript
|
||||
Solution: Check token expiry time
|
||||
- Access token: 1 hour
|
||||
- Refresh token: 7 days
|
||||
```
|
||||
|
||||
### Issue 2: Form không submit
|
||||
```typescript
|
||||
Solution: Check console for validation errors
|
||||
- Open DevTools > Console
|
||||
- Look for Yup validation errors
|
||||
```
|
||||
|
||||
### Issue 3: Redirect không hoạt động
|
||||
```typescript
|
||||
Solution: Check location state
|
||||
console.log(location.state?.from);
|
||||
```
|
||||
|
||||
### Issue 4: Remember me không work
|
||||
```typescript
|
||||
Solution: Check localStorage
|
||||
- Open DevTools > Application > Local Storage
|
||||
- Check "token" và "refreshToken" keys
|
||||
```
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
- [React Hook Form Docs](https://react-hook-form.com/)
|
||||
- [Yup Validation](https://github.com/jquense/yup)
|
||||
- [Zustand Guide](https://github.com/pmndrs/zustand)
|
||||
- [Tailwind CSS](https://tailwindcss.com/)
|
||||
- [Lucide Icons](https://lucide.dev/)
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [x] ✅ Create validationSchemas.ts
|
||||
- [x] ✅ Create LoginPage.tsx
|
||||
- [x] ✅ Add route to App.tsx
|
||||
- [x] ✅ Email validation
|
||||
- [x] ✅ Password validation
|
||||
- [x] ✅ Show/hide password
|
||||
- [x] ✅ Remember me checkbox
|
||||
- [x] ✅ Loading state
|
||||
- [x] ✅ Error display
|
||||
- [x] ✅ Redirect after login
|
||||
- [x] ✅ Responsive design
|
||||
- [x] ✅ Icons integration
|
||||
- [ ] ⏳ E2E testing
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ Chức năng 4 hoàn thành
|
||||
**Next:** Chức năng 5 - Form Register
|
||||
486
docs/REGISTER_FORM_COMPLETE.md
Normal file
486
docs/REGISTER_FORM_COMPLETE.md
Normal file
@@ -0,0 +1,486 @@
|
||||
# Chức năng 5: Form Đăng Ký - Hoàn Thành ✅
|
||||
|
||||
## 📦 Files Đã Tạo/Cập Nhật
|
||||
|
||||
### 1. **RegisterPage.tsx** - Component form đăng ký
|
||||
**Đường dẫn:** `client/src/pages/auth/RegisterPage.tsx`
|
||||
|
||||
### 2. **index.ts** - Export module
|
||||
**Đường dẫn:** `client/src/pages/auth/index.ts`
|
||||
- Đã thêm export RegisterPage
|
||||
|
||||
### 3. **App.tsx** - Cập nhật routing
|
||||
**Đường dẫn:** `client/src/App.tsx`
|
||||
- Đã thêm route `/register`
|
||||
|
||||
## ✨ Tính Năng Chính
|
||||
|
||||
### 1. Form Fields (5 fields)
|
||||
✅ **Họ và tên** (name)
|
||||
- Required, 2-50 ký tự
|
||||
- Icon: User
|
||||
- Placeholder: "Nguyễn Văn A"
|
||||
|
||||
✅ **Email**
|
||||
- Required, valid email format
|
||||
- Icon: Mail
|
||||
- Placeholder: "email@example.com"
|
||||
|
||||
✅ **Số điện thoại** (phone) - Optional
|
||||
- 10-11 chữ số
|
||||
- Icon: Phone
|
||||
- Placeholder: "0123456789"
|
||||
|
||||
✅ **Mật khẩu** (password)
|
||||
- Required, min 8 chars
|
||||
- Must contain: uppercase, lowercase, number, special char
|
||||
- Show/hide toggle với Eye icon
|
||||
- Icon: Lock
|
||||
|
||||
✅ **Xác nhận mật khẩu** (confirmPassword)
|
||||
- Must match password
|
||||
- Show/hide toggle với Eye icon
|
||||
- Icon: Lock
|
||||
|
||||
### 2. Password Strength Indicator
|
||||
✅ **Visual Progress Bar** với 5 levels:
|
||||
1. 🔴 Rất yếu (0/5)
|
||||
2. 🟠 Yếu (1/5)
|
||||
3. 🟡 Trung bình (2/5)
|
||||
4. 🔵 Mạnh (3/5)
|
||||
5. 🟢 Rất mạnh (5/5)
|
||||
|
||||
✅ **Real-time Requirements Checker:**
|
||||
- ✅/❌ Ít nhất 8 ký tự
|
||||
- ✅/❌ Chữ thường (a-z)
|
||||
- ✅/❌ Chữ hoa (A-Z)
|
||||
- ✅/❌ Số (0-9)
|
||||
- ✅/❌ Ký tự đặc biệt (@$!%*?&)
|
||||
|
||||
### 3. Validation Rules (Yup Schema)
|
||||
|
||||
```typescript
|
||||
name:
|
||||
- Required: "Họ tên là bắt buộc"
|
||||
- Min 2 chars: "Họ tên phải có ít nhất 2 ký tự"
|
||||
- Max 50 chars: "Họ tên không được quá 50 ký tự"
|
||||
- Trim whitespace
|
||||
|
||||
email:
|
||||
- Required: "Email là bắt buộc"
|
||||
- Valid format: "Email không hợp lệ"
|
||||
- Trim whitespace
|
||||
|
||||
phone (optional):
|
||||
- Pattern /^[0-9]{10,11}$/
|
||||
- Error: "Số điện thoại không hợp lệ"
|
||||
|
||||
password:
|
||||
- Required: "Mật khẩu là bắt buộc"
|
||||
- Min 8 chars
|
||||
- Pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/
|
||||
- Error: "Mật khẩu phải chứa chữ hoa, chữ thường, số và ký tự đặc biệt"
|
||||
|
||||
confirmPassword:
|
||||
- Required: "Vui lòng xác nhận mật khẩu"
|
||||
- Must match password: "Mật khẩu không khớp"
|
||||
```
|
||||
|
||||
### 4. UX Features
|
||||
|
||||
✅ **Loading State**
|
||||
```tsx
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="animate-spin" />
|
||||
Đang xử lý...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserPlus />
|
||||
Đăng ký
|
||||
</>
|
||||
)}
|
||||
```
|
||||
|
||||
✅ **Show/Hide Password** (2 toggles)
|
||||
- Eye/EyeOff icons
|
||||
- Separate toggle cho password và confirmPassword
|
||||
- Visual feedback khi hover
|
||||
|
||||
✅ **Error Display**
|
||||
- Inline validation errors dưới mỗi field
|
||||
- Global error message ở top của form
|
||||
- Red border cho fields có lỗi
|
||||
|
||||
✅ **Success Flow**
|
||||
```typescript
|
||||
1. Submit form
|
||||
2. Validation passes
|
||||
3. Call useAuthStore.register()
|
||||
4. Show toast: "Đăng ký thành công! Vui lòng đăng nhập."
|
||||
5. Navigate to /login
|
||||
```
|
||||
|
||||
### 5. Design & Styling
|
||||
|
||||
**Color Scheme:**
|
||||
- Primary: purple-600, purple-700
|
||||
- Background: gradient from-purple-50 to-pink-100
|
||||
- Success: green-500, green-600
|
||||
- Error: red-50, red-200, red-600
|
||||
- Text: gray-600, gray-700, gray-900
|
||||
|
||||
**Layout:**
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 🏨 Hotel Icon (Purple) │
|
||||
│ Đăng ký tài khoản │
|
||||
│ Tạo tài khoản mới để đặt phòng... │
|
||||
├─────────────────────────────────────┤
|
||||
│ ┌───────────────────────────────┐ │
|
||||
│ │ [Error message if any] │ │
|
||||
│ ├───────────────────────────────┤ │
|
||||
│ │ Họ và tên │ │
|
||||
│ │ [👤 Nguyễn Văn A ] │ │
|
||||
│ ├───────────────────────────────┤ │
|
||||
│ │ Email │ │
|
||||
│ │ [📧 email@example.com ] │ │
|
||||
│ ├───────────────────────────────┤ │
|
||||
│ │ Số điện thoại (Tùy chọn) │ │
|
||||
│ │ [📱 0123456789 ] │ │
|
||||
│ ├───────────────────────────────┤ │
|
||||
│ │ Mật khẩu │ │
|
||||
│ │ [🔒 •••••••• 👁️] │ │
|
||||
│ │ ▓▓▓▓▓░░░░░ Rất mạnh │ │
|
||||
│ │ ✅ Ít nhất 8 ký tự │ │
|
||||
│ │ ✅ Chữ thường (a-z) │ │
|
||||
│ │ ✅ Chữ hoa (A-Z) │ │
|
||||
│ │ ✅ Số (0-9) │ │
|
||||
│ │ ✅ Ký tự đặc biệt │ │
|
||||
│ ├───────────────────────────────┤ │
|
||||
│ │ Xác nhận mật khẩu │ │
|
||||
│ │ [🔒 •••••••• 👁️] │ │
|
||||
│ ├───────────────────────────────┤ │
|
||||
│ │ [👤 Đăng ký] │ │
|
||||
│ └───────────────────────────────┘ │
|
||||
│ Đã có tài khoản? Đăng nhập ngay │
|
||||
│ │
|
||||
│ Điều khoản & Chính sách bảo mật │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🔗 Integration
|
||||
|
||||
### API Endpoint
|
||||
```
|
||||
POST /api/auth/register
|
||||
```
|
||||
|
||||
### Request Body
|
||||
```typescript
|
||||
{
|
||||
name: string; // "Nguyễn Văn A"
|
||||
email: string; // "user@example.com"
|
||||
password: string; // "Password123@"
|
||||
phone?: string; // "0123456789" (optional)
|
||||
}
|
||||
```
|
||||
|
||||
### Response (Success)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "User registered successfully",
|
||||
"data": {
|
||||
"user": {
|
||||
"id": 1,
|
||||
"name": "Nguyễn Văn A",
|
||||
"email": "user@example.com",
|
||||
"phone": "0123456789",
|
||||
"role": "customer"
|
||||
},
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### useAuthStore Integration
|
||||
```typescript
|
||||
const { register: registerUser, isLoading, error, clearError } =
|
||||
useAuthStore();
|
||||
|
||||
await registerUser({
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
phone: data.phone,
|
||||
});
|
||||
|
||||
// After success:
|
||||
navigate('/login', { replace: true });
|
||||
```
|
||||
|
||||
## 🧪 Test Scenarios
|
||||
|
||||
### 1. Validation Tests
|
||||
|
||||
**Test Case 1: Empty form**
|
||||
```
|
||||
Action: Submit empty form
|
||||
Expected: Show validation errors for name, email, password
|
||||
```
|
||||
|
||||
**Test Case 2: Invalid email**
|
||||
```
|
||||
Input: email = "notanemail"
|
||||
Expected: "Email không hợp lệ"
|
||||
```
|
||||
|
||||
**Test Case 3: Short name**
|
||||
```
|
||||
Input: name = "A"
|
||||
Expected: "Họ tên phải có ít nhất 2 ký tự"
|
||||
```
|
||||
|
||||
**Test Case 4: Weak password**
|
||||
```
|
||||
Input: password = "abc123"
|
||||
Expected: "Mật khẩu phải chứa chữ hoa, chữ thường, số và ký tự đặc biệt"
|
||||
Password strength: Yếu/Trung bình
|
||||
```
|
||||
|
||||
**Test Case 5: Password mismatch**
|
||||
```
|
||||
Input:
|
||||
password = "Password123@"
|
||||
confirmPassword = "Password456@"
|
||||
Expected: "Mật khẩu không khớp"
|
||||
```
|
||||
|
||||
**Test Case 6: Invalid phone**
|
||||
```
|
||||
Input: phone = "123"
|
||||
Expected: "Số điện thoại không hợp lệ"
|
||||
```
|
||||
|
||||
### 2. UX Tests
|
||||
|
||||
**Test Case 7: Password strength indicator**
|
||||
```
|
||||
Input: Type password character by character
|
||||
Expected:
|
||||
- Progress bar animates
|
||||
- Color changes: red → orange → yellow → blue → green
|
||||
- Checkmarks appear as requirements met
|
||||
```
|
||||
|
||||
**Test Case 8: Show/hide password**
|
||||
```
|
||||
Action: Click eye icon on password field
|
||||
Expected: Password text becomes visible/hidden
|
||||
Action: Click eye icon on confirmPassword field
|
||||
Expected: Confirm password text becomes visible/hidden
|
||||
```
|
||||
|
||||
**Test Case 9: Loading state**
|
||||
```
|
||||
Action: Submit valid form
|
||||
Expected:
|
||||
- Button disabled
|
||||
- Spinner shows
|
||||
- Text changes to "Đang xử lý..."
|
||||
```
|
||||
|
||||
### 3. Integration Tests
|
||||
|
||||
**Test Case 10: Successful registration**
|
||||
```
|
||||
Input: All valid data
|
||||
Expected:
|
||||
1. API POST /api/auth/register called
|
||||
2. Toast: "Đăng ký thành công! Vui lòng đăng nhập."
|
||||
3. Redirect to /login
|
||||
```
|
||||
|
||||
**Test Case 11: Email already exists**
|
||||
```
|
||||
Input: email = "existing@example.com"
|
||||
Expected:
|
||||
- Error message: "Email already registered"
|
||||
- Toast error displayed
|
||||
- Form remains on page
|
||||
```
|
||||
|
||||
**Test Case 12: Network error**
|
||||
```
|
||||
Scenario: Server offline
|
||||
Expected:
|
||||
- Error message: "Đăng ký thất bại. Vui lòng thử lại."
|
||||
- Toast error displayed
|
||||
```
|
||||
|
||||
## 📊 Password Strength Algorithm
|
||||
|
||||
```typescript
|
||||
function getPasswordStrength(pwd: string) {
|
||||
let strength = 0;
|
||||
|
||||
if (pwd.length >= 8) strength++; // +1
|
||||
if (/[a-z]/.test(pwd)) strength++; // +1
|
||||
if (/[A-Z]/.test(pwd)) strength++; // +1
|
||||
if (/\d/.test(pwd)) strength++; // +1
|
||||
if (/[@$!%*?&]/.test(pwd)) strength++; // +1
|
||||
|
||||
return {
|
||||
strength: 0-5,
|
||||
label: ['Rất yếu', 'Yếu', 'Trung bình', 'Mạnh', 'Rất mạnh'][strength],
|
||||
color: ['bg-red-500', 'bg-orange-500', 'bg-yellow-500', 'bg-blue-500', 'bg-green-500'][strength]
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 Component Structure
|
||||
|
||||
```tsx
|
||||
RegisterPage/
|
||||
├── Header Section
|
||||
│ ├── Hotel Icon (purple)
|
||||
│ ├── Title: "Đăng ký tài khoản"
|
||||
│ └── Subtitle
|
||||
│
|
||||
├── Form Container (white card)
|
||||
│ ├── Error Alert (conditional)
|
||||
│ ├── Name Input
|
||||
│ ├── Email Input
|
||||
│ ├── Phone Input (optional)
|
||||
│ ├── Password Input
|
||||
│ │ ├── Show/Hide Toggle
|
||||
│ │ ├── Strength Indicator
|
||||
│ │ └── Requirements Checklist
|
||||
│ ├── Confirm Password Input
|
||||
│ │ └── Show/Hide Toggle
|
||||
│ └── Submit Button (with loading)
|
||||
│
|
||||
├── Login Link
|
||||
│ └── "Đã có tài khoản? Đăng nhập ngay"
|
||||
│
|
||||
└── Footer Links
|
||||
├── Terms of Service
|
||||
└── Privacy Policy
|
||||
```
|
||||
|
||||
## 🔐 Security Features
|
||||
|
||||
### 1. Password Validation
|
||||
- Min 8 characters
|
||||
- Requires: uppercase, lowercase, number, special char
|
||||
- Visual feedback for strength
|
||||
|
||||
### 2. Confirm Password
|
||||
- Must match original password
|
||||
- Prevents typos
|
||||
|
||||
### 3. Client-side Validation
|
||||
- Immediate feedback
|
||||
- Prevents invalid API calls
|
||||
- Better UX
|
||||
|
||||
### 4. Server-side Validation
|
||||
- Backend also validates all fields
|
||||
- Checks email uniqueness
|
||||
- Password hashed with bcrypt
|
||||
|
||||
## 📝 Code Quality
|
||||
|
||||
✅ **TypeScript**: Full type safety
|
||||
✅ **React Hook Form**: Optimized re-renders
|
||||
✅ **Yup Validation**: Schema-based validation
|
||||
✅ **Component Composition**: Reusable PasswordRequirement component
|
||||
✅ **Accessibility**: Proper labels, IDs, autocomplete
|
||||
✅ **Error Handling**: Try-catch, user-friendly messages
|
||||
✅ **Loading States**: Visual feedback during async operations
|
||||
✅ **Responsive Design**: Works on mobile and desktop
|
||||
✅ **80 chars/line**: Code formatting standard
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
### Navigate to Register Page
|
||||
```bash
|
||||
http://localhost:5173/register
|
||||
```
|
||||
|
||||
### Example Registration
|
||||
```typescript
|
||||
Name: "Nguyễn Văn A"
|
||||
Email: "nguyenvana@example.com"
|
||||
Phone: "0123456789"
|
||||
Password: "Password123@"
|
||||
Confirm: "Password123@"
|
||||
|
||||
Submit → Success → Redirect to /login
|
||||
```
|
||||
|
||||
## 🔄 Flow Diagram
|
||||
|
||||
```
|
||||
User visits /register
|
||||
↓
|
||||
Fill in form fields
|
||||
↓
|
||||
Real-time validation (Yup)
|
||||
↓
|
||||
Password strength updates live
|
||||
↓
|
||||
Submit button clicked
|
||||
↓
|
||||
Frontend validation passes
|
||||
↓
|
||||
Call useAuthStore.register()
|
||||
↓
|
||||
API POST /api/auth/register
|
||||
↓
|
||||
┌───────┴───────┐
|
||||
↓ ↓
|
||||
Success Failure
|
||||
↓ ↓
|
||||
Toast success Toast error
|
||||
↓ ↓
|
||||
Navigate Stay on page
|
||||
to /login Show errors
|
||||
```
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [x] ✅ Create RegisterPage.tsx component
|
||||
- [x] ✅ Implement React Hook Form
|
||||
- [x] ✅ Add Yup validation schema
|
||||
- [x] ✅ Add 5 form fields (name, email, phone, password, confirmPassword)
|
||||
- [x] ✅ Show/hide password toggle (2 fields)
|
||||
- [x] ✅ Password strength indicator
|
||||
- [x] ✅ Real-time requirements checker
|
||||
- [x] ✅ Loading state
|
||||
- [x] ✅ Error display (inline + global)
|
||||
- [x] ✅ Integration with useAuthStore
|
||||
- [x] ✅ Redirect to /login after success
|
||||
- [x] ✅ Toast notifications
|
||||
- [x] ✅ Add route to App.tsx
|
||||
- [x] ✅ Responsive design
|
||||
- [x] ✅ Purple color scheme
|
||||
- [x] ✅ Icons integration (Lucide React)
|
||||
- [x] ✅ Terms & Privacy links
|
||||
|
||||
## 📚 Related Files
|
||||
|
||||
- `client/src/pages/auth/LoginPage.tsx` - Login form (same design pattern)
|
||||
- `client/src/utils/validationSchemas.ts` - Validation schemas
|
||||
- `client/src/store/useAuthStore.ts` - Auth state management
|
||||
- `client/src/services/api/authService.ts` - API calls
|
||||
- `client/src/App.tsx` - Route configuration
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ Chức năng 5 hoàn thành
|
||||
**Next:** Chức năng 6 - Forgot Password
|
||||
**Test URL:** http://localhost:5173/register
|
||||
484
docs/ROUTE_PROTECTION.md
Normal file
484
docs/ROUTE_PROTECTION.md
Normal file
@@ -0,0 +1,484 @@
|
||||
# Route Protection Documentation
|
||||
|
||||
## Chức năng 8: Phân quyền & Bảo vệ Route
|
||||
|
||||
Hệ thống sử dụng 2 component để bảo vệ các route:
|
||||
- **ProtectedRoute**: Yêu cầu user phải đăng nhập
|
||||
- **AdminRoute**: Yêu cầu user phải là Admin
|
||||
|
||||
---
|
||||
|
||||
## 1. ProtectedRoute
|
||||
|
||||
### Mục đích
|
||||
Bảo vệ các route yêu cầu authentication (đăng nhập).
|
||||
|
||||
### Cách hoạt động
|
||||
```tsx
|
||||
// File: client/src/components/auth/ProtectedRoute.tsx
|
||||
|
||||
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
children
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const { isAuthenticated, isLoading } = useAuthStore();
|
||||
|
||||
// 1. Nếu đang loading → hiển thị spinner
|
||||
if (isLoading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
// 2. Nếu chưa đăng nhập → redirect /login
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<Navigate
|
||||
to="/login"
|
||||
state={{ from: location }} // Lưu location để quay lại
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Đã đăng nhập → cho phép truy cập
|
||||
return <>{children}</>;
|
||||
};
|
||||
```
|
||||
|
||||
### Sử dụng trong App.tsx
|
||||
```tsx
|
||||
import { ProtectedRoute } from './components/auth';
|
||||
|
||||
// Route yêu cầu đăng nhập
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/bookings"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<BookingListPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/profile"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ProfilePage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
### Luồng hoạt động
|
||||
1. User chưa đăng nhập truy cập `/dashboard`
|
||||
2. ProtectedRoute kiểm tra `isAuthenticated === false`
|
||||
3. Redirect về `/login` và lưu `state={{ from: '/dashboard' }}`
|
||||
4. Sau khi login thành công, redirect về `/dashboard`
|
||||
|
||||
---
|
||||
|
||||
## 2. AdminRoute
|
||||
|
||||
### Mục đích
|
||||
Bảo vệ các route chỉ dành cho Admin (role-based access).
|
||||
|
||||
### Cách hoạt động
|
||||
```tsx
|
||||
// File: client/src/components/auth/AdminRoute.tsx
|
||||
|
||||
const AdminRoute: React.FC<AdminRouteProps> = ({
|
||||
children
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const { isAuthenticated, userInfo, isLoading } = useAuthStore();
|
||||
|
||||
// 1. Nếu đang loading → hiển thị spinner
|
||||
if (isLoading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
// 2. Nếu chưa đăng nhập → redirect /login
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<Navigate
|
||||
to="/login"
|
||||
state={{ from: location }}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Nếu không phải admin → redirect /
|
||||
const isAdmin = userInfo?.role === 'admin';
|
||||
if (!isAdmin) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
// 4. Là admin → cho phép truy cập
|
||||
return <>{children}</>;
|
||||
};
|
||||
```
|
||||
|
||||
### Sử dụng trong App.tsx
|
||||
```tsx
|
||||
import { AdminRoute } from './components/auth';
|
||||
|
||||
// Route chỉ dành cho Admin
|
||||
<Route
|
||||
path="/admin"
|
||||
element={
|
||||
<AdminRoute>
|
||||
<AdminLayout />
|
||||
</AdminRoute>
|
||||
}
|
||||
>
|
||||
<Route path="dashboard" element={<AdminDashboard />} />
|
||||
<Route path="users" element={<UserManagement />} />
|
||||
<Route path="rooms" element={<RoomManagement />} />
|
||||
<Route path="bookings" element={<BookingManagement />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Route>
|
||||
```
|
||||
|
||||
### Luồng hoạt động
|
||||
|
||||
#### Case 1: User chưa đăng nhập
|
||||
1. Truy cập `/admin`
|
||||
2. AdminRoute kiểm tra `isAuthenticated === false`
|
||||
3. Redirect về `/login` với `state={{ from: '/admin' }}`
|
||||
4. Sau login thành công → quay lại `/admin`
|
||||
5. AdminRoute kiểm tra lại role
|
||||
|
||||
#### Case 2: User đã đăng nhập nhưng không phải Admin
|
||||
1. Customer (role='customer') truy cập `/admin`
|
||||
2. AdminRoute kiểm tra `isAuthenticated === true`
|
||||
3. AdminRoute kiểm tra `userInfo.role === 'customer'` (không phải 'admin')
|
||||
4. Redirect về `/` (trang chủ)
|
||||
|
||||
#### Case 3: User là Admin
|
||||
1. Admin (role='admin') truy cập `/admin`
|
||||
2. AdminRoute kiểm tra `isAuthenticated === true`
|
||||
3. AdminRoute kiểm tra `userInfo.role === 'admin'` ✅
|
||||
4. Cho phép truy cập
|
||||
|
||||
---
|
||||
|
||||
## 3. Cấu trúc Route trong App.tsx
|
||||
|
||||
```tsx
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/* Public Routes - Không cần đăng nhập */}
|
||||
<Route path="/" element={<LayoutMain />}>
|
||||
<Route index element={<HomePage />} />
|
||||
<Route path="rooms" element={<RoomListPage />} />
|
||||
<Route path="about" element={<AboutPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Auth Routes - Không cần layout */}
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
|
||||
<Route path="/reset-password/:token" element={<ResetPasswordPage />} />
|
||||
|
||||
{/* Protected Routes - Yêu cầu đăng nhập */}
|
||||
<Route path="/" element={<LayoutMain />}>
|
||||
<Route
|
||||
path="dashboard"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="bookings"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<BookingListPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="profile"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ProfilePage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
{/* Admin Routes - Chỉ Admin */}
|
||||
<Route
|
||||
path="/admin"
|
||||
element={
|
||||
<AdminRoute>
|
||||
<AdminLayout />
|
||||
</AdminRoute>
|
||||
}
|
||||
>
|
||||
<Route index element={<Navigate to="dashboard" replace />} />
|
||||
<Route path="dashboard" element={<AdminDashboard />} />
|
||||
<Route path="users" element={<UserManagement />} />
|
||||
<Route path="rooms" element={<RoomManagement />} />
|
||||
<Route path="bookings" element={<BookingManagement />} />
|
||||
<Route path="payments" element={<PaymentManagement />} />
|
||||
<Route path="services" element={<ServiceManagement />} />
|
||||
<Route path="promotions" element={<PromotionManagement />} />
|
||||
<Route path="banners" element={<BannerManagement />} />
|
||||
<Route path="reports" element={<Reports />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Route>
|
||||
|
||||
{/* 404 Route */}
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Tích hợp với Zustand Store
|
||||
|
||||
### useAuthStore State
|
||||
```tsx
|
||||
// File: client/src/store/useAuthStore.ts
|
||||
|
||||
const useAuthStore = create<AuthStore>((set) => ({
|
||||
// State
|
||||
token: localStorage.getItem('token') || null,
|
||||
refreshToken: localStorage.getItem('refreshToken') || null,
|
||||
userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null'),
|
||||
isAuthenticated: !!localStorage.getItem('token'),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
// Actions
|
||||
login: async (credentials) => { ... },
|
||||
logout: () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('userInfo');
|
||||
set({
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
userInfo: null,
|
||||
isAuthenticated: false,
|
||||
error: null
|
||||
});
|
||||
},
|
||||
|
||||
// ... other actions
|
||||
}));
|
||||
```
|
||||
|
||||
### User Roles
|
||||
- **admin**: Quản trị viên (full access)
|
||||
- **staff**: Nhân viên (limited access)
|
||||
- **customer**: Khách hàng (customer features only)
|
||||
|
||||
---
|
||||
|
||||
## 5. Loading State
|
||||
|
||||
Cả 2 component đều xử lý loading state để tránh:
|
||||
- Flash of redirect (nhấp nháy khi chuyển trang)
|
||||
- Race condition (auth state chưa load xong)
|
||||
|
||||
```tsx
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12
|
||||
border-b-2 border-indigo-600 mx-auto"
|
||||
/>
|
||||
<p className="mt-4 text-gray-600">Đang xác thực...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Redirect After Login
|
||||
|
||||
### LoginPage implementation
|
||||
```tsx
|
||||
const LoginPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { login, isLoading } = useAuthStore();
|
||||
|
||||
const from = location.state?.from?.pathname || '/dashboard';
|
||||
|
||||
const onSubmit = async (data: LoginFormData) => {
|
||||
try {
|
||||
await login(data);
|
||||
|
||||
// Redirect về page ban đầu hoặc /dashboard
|
||||
navigate(from, { replace: true });
|
||||
|
||||
toast.success('Đăng nhập thành công!');
|
||||
} catch (error) {
|
||||
toast.error('Đăng nhập thất bại!');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{/* ... form fields */}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Flow
|
||||
1. User truy cập `/bookings` (protected)
|
||||
2. Redirect `/login?from=/bookings`
|
||||
3. Login thành công
|
||||
4. Redirect về `/bookings` (page ban đầu)
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing Route Protection
|
||||
|
||||
### Test Case 1: ProtectedRoute - Unauthenticated
|
||||
**Given**: User chưa đăng nhập
|
||||
**When**: Truy cập `/dashboard`
|
||||
**Then**: Redirect về `/login`
|
||||
**And**: Lưu `from=/dashboard` trong location state
|
||||
|
||||
### Test Case 2: ProtectedRoute - Authenticated
|
||||
**Given**: User đã đăng nhập
|
||||
**When**: Truy cập `/dashboard`
|
||||
**Then**: Hiển thị DashboardPage thành công
|
||||
|
||||
### Test Case 3: AdminRoute - Not Admin
|
||||
**Given**: User có role='customer'
|
||||
**When**: Truy cập `/admin`
|
||||
**Then**: Redirect về `/` (trang chủ)
|
||||
|
||||
### Test Case 4: AdminRoute - Is Admin
|
||||
**Given**: User có role='admin'
|
||||
**When**: Truy cập `/admin`
|
||||
**Then**: Hiển thị AdminLayout thành công
|
||||
|
||||
### Test Case 5: Loading State
|
||||
**Given**: Auth đang initialize
|
||||
**When**: isLoading === true
|
||||
**Then**: Hiển thị loading spinner
|
||||
**And**: Không redirect
|
||||
|
||||
---
|
||||
|
||||
## 8. Security Best Practices
|
||||
|
||||
### ✅ Đã Implement
|
||||
1. **Client-side protection**: ProtectedRoute & AdminRoute
|
||||
2. **Token persistence**: localStorage
|
||||
3. **Role-based access**: Kiểm tra userInfo.role
|
||||
4. **Location state**: Lưu "from" để redirect về đúng page
|
||||
5. **Loading state**: Tránh flash của redirect
|
||||
6. **Replace navigation**: Không lưu lịch sử redirect
|
||||
|
||||
### ⚠️ Lưu Ý
|
||||
- Client-side protection **không đủ** → Phải có backend validation
|
||||
- API endpoints phải kiểm tra JWT + role
|
||||
- Middleware backend: `auth`, `adminOnly`
|
||||
- Never trust client-side role → Always verify on server
|
||||
|
||||
### Backend Middleware Example
|
||||
```javascript
|
||||
// server/src/middlewares/auth.js
|
||||
const auth = async (req, res, next) => {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
message: 'Unauthorized'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = await User.findByPk(decoded.userId);
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ message: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
const adminOnly = (req, res, next) => {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({
|
||||
message: 'Forbidden: Admin only'
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// Usage
|
||||
router.get('/admin/users', auth, adminOnly, getUsers);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Troubleshooting
|
||||
|
||||
### Vấn đề 1: Infinite redirect loop
|
||||
**Nguyên nhân**: ProtectedRoute check sai logic
|
||||
**Giải pháp**: Đảm bảo `replace={true}` trong Navigate
|
||||
|
||||
### Vấn đề 2: Flash of redirect
|
||||
**Nguyên nhân**: Không handle loading state
|
||||
**Giải pháp**: Thêm check `if (isLoading)` trước check auth
|
||||
|
||||
### Vấn đề 3: Lost location state
|
||||
**Nguyên nhân**: Không pass `state={{ from: location }}`
|
||||
**Giải pháp**: Luôn lưu location khi redirect
|
||||
|
||||
### Vấn đề 4: Admin có thể truy cập nhưng API fail
|
||||
**Nguyên nhân**: Backend không verify role
|
||||
**Giải pháp**: Thêm middleware `adminOnly` trên API routes
|
||||
|
||||
---
|
||||
|
||||
## 10. Summary
|
||||
|
||||
### ProtectedRoute
|
||||
- ✅ Kiểm tra `isAuthenticated`
|
||||
- ✅ Redirect `/login` nếu chưa đăng nhập
|
||||
- ✅ Lưu location state để quay lại
|
||||
- ✅ Handle loading state
|
||||
|
||||
### AdminRoute
|
||||
- ✅ Kiểm tra `isAuthenticated` trước
|
||||
- ✅ Kiểm tra `userInfo.role === 'admin'`
|
||||
- ✅ Redirect `/login` nếu chưa đăng nhập
|
||||
- ✅ Redirect `/` nếu không phải admin
|
||||
- ✅ Handle loading state
|
||||
|
||||
### Kết quả
|
||||
- Bảo vệ toàn bộ protected routes
|
||||
- UX mượt mà, không flash
|
||||
- Role-based access hoạt động chính xác
|
||||
- Security tốt (kết hợp backend validation)
|
||||
|
||||
---
|
||||
|
||||
**Chức năng 8 hoàn thành! ✅**
|
||||
363
docs/SERVER_SETUP_COMPLETE.md
Normal file
363
docs/SERVER_SETUP_COMPLETE.md
Normal file
@@ -0,0 +1,363 @@
|
||||
# ✅ Server Backend - Setup Complete
|
||||
|
||||
## 📦 Files Created
|
||||
|
||||
### Core Server Files
|
||||
1. **`.env`** - Environment configuration (với mật khẩu và secrets)
|
||||
2. **`src/server.js`** - Server entry point với database connection
|
||||
3. **`src/app.js`** - Express application setup với middleware
|
||||
|
||||
### Controllers
|
||||
4. **`src/controllers/authController.js`** - Authentication logic
|
||||
- register()
|
||||
- login()
|
||||
- refreshAccessToken()
|
||||
- logout()
|
||||
- getProfile()
|
||||
|
||||
### Routes
|
||||
5. **`src/routes/authRoutes.js`** - Auth endpoints
|
||||
- POST /api/auth/register
|
||||
- POST /api/auth/login
|
||||
- POST /api/auth/refresh-token
|
||||
- POST /api/auth/logout
|
||||
- GET /api/auth/profile
|
||||
|
||||
6. **`src/routes/userRoutes.js`** - User endpoints (placeholder)
|
||||
7. **`src/routes/roomRoutes.js`** - Room endpoints (placeholder)
|
||||
8. **`src/routes/bookingRoutes.js`** - Booking endpoints (placeholder)
|
||||
|
||||
### Middleware
|
||||
9. **`src/middlewares/auth.js`** - JWT authentication
|
||||
- authenticateToken()
|
||||
- authorizeRoles()
|
||||
|
||||
10. **`src/middlewares/errorHandler.js`** - Global error handling
|
||||
11. **`src/middlewares/validate.js`** - Validation middleware
|
||||
|
||||
### Validators
|
||||
12. **`src/validators/authValidator.js`** - Validation rules
|
||||
- registerValidation
|
||||
- loginValidation
|
||||
- refreshTokenValidation
|
||||
|
||||
### Documentation
|
||||
13. **`README.md`** - Server documentation
|
||||
14. **`QUICK_START.md`** - Quick setup guide
|
||||
|
||||
## 🎯 Features Implemented
|
||||
|
||||
### Security
|
||||
- ✅ JWT authentication (access + refresh tokens)
|
||||
- ✅ Password hashing with bcrypt (10 rounds)
|
||||
- ✅ Helmet security headers
|
||||
- ✅ CORS configuration
|
||||
- ✅ Rate limiting (100 req/15min)
|
||||
- ✅ Input validation with express-validator
|
||||
|
||||
### Authentication Flow
|
||||
- ✅ Register with email/password validation
|
||||
- ✅ Login with email/password
|
||||
- ✅ Remember me (7 days vs 1 day token expiry)
|
||||
- ✅ Refresh token mechanism
|
||||
- ✅ Logout with token cleanup
|
||||
- ✅ Protected routes with JWT
|
||||
|
||||
### Error Handling
|
||||
- ✅ Global error handler
|
||||
- ✅ Sequelize error handling
|
||||
- ✅ JWT error handling
|
||||
- ✅ Validation error formatting
|
||||
- ✅ Development vs production error responses
|
||||
|
||||
### Validation Rules
|
||||
|
||||
**Register:**
|
||||
```javascript
|
||||
- name: 2-50 chars, required
|
||||
- email: valid email format, required, unique
|
||||
- password: min 8 chars, uppercase + lowercase + number
|
||||
- phone: 10-11 digits, optional
|
||||
```
|
||||
|
||||
**Login:**
|
||||
```javascript
|
||||
- email: valid email format, required
|
||||
- password: required
|
||||
- rememberMe: boolean, optional
|
||||
```
|
||||
|
||||
## 🗄️ Database Integration
|
||||
|
||||
### Models Used
|
||||
- ✅ User model (full_name, email, password, phone, role_id)
|
||||
- ✅ Role model (for role-based access)
|
||||
- ✅ RefreshToken model (token storage)
|
||||
|
||||
### Associations
|
||||
- User belongsTo Role
|
||||
- User hasMany RefreshToken
|
||||
- User hasMany Booking
|
||||
|
||||
## 🔐 JWT Configuration
|
||||
|
||||
```javascript
|
||||
Access Token:
|
||||
- Secret: JWT_SECRET
|
||||
- Expiry: 1h
|
||||
- Payload: { userId }
|
||||
|
||||
Refresh Token:
|
||||
- Secret: JWT_REFRESH_SECRET
|
||||
- Expiry: 7d (remember me) or 1d (normal)
|
||||
- Payload: { userId }
|
||||
- Stored in database (refresh_tokens table)
|
||||
```
|
||||
|
||||
## 📡 API Endpoints Ready
|
||||
|
||||
### Public Endpoints
|
||||
```
|
||||
✅ GET /health - Health check
|
||||
✅ POST /api/auth/register - User registration
|
||||
✅ POST /api/auth/login - User login
|
||||
✅ POST /api/auth/refresh-token - Refresh access token
|
||||
✅ POST /api/auth/logout - User logout
|
||||
```
|
||||
|
||||
### Protected Endpoints
|
||||
```
|
||||
✅ GET /api/auth/profile - Get user profile (JWT required)
|
||||
🔜 GET /api/users - Get all users (Admin only)
|
||||
🔜 GET /api/rooms - Get rooms (Public)
|
||||
🔜 GET /api/bookings - Get bookings (User's own)
|
||||
```
|
||||
|
||||
## 🧪 Request/Response Examples
|
||||
|
||||
### Login Request
|
||||
```json
|
||||
POST /api/auth/login
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "Password123",
|
||||
"rememberMe": true
|
||||
}
|
||||
```
|
||||
|
||||
### Login Response (Success)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Login successful",
|
||||
"data": {
|
||||
"user": {
|
||||
"id": 1,
|
||||
"name": "John Doe",
|
||||
"email": "user@example.com",
|
||||
"phone": "0123456789",
|
||||
"role": "customer"
|
||||
},
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Login Response (Error)
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Invalid email or password"
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Error
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Validation failed",
|
||||
"errors": [
|
||||
{
|
||||
"field": "email",
|
||||
"message": "Invalid email format"
|
||||
},
|
||||
{
|
||||
"field": "password",
|
||||
"message": "Password must be at least 8 characters"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Middleware Stack
|
||||
|
||||
```javascript
|
||||
1. helmet() - Security headers
|
||||
2. compression() - Response compression
|
||||
3. cors() - CORS handling
|
||||
4. express.json() - JSON body parser
|
||||
5. morgan() - Request logging
|
||||
6. rateLimit() - Rate limiting
|
||||
7. Routes - API routes
|
||||
8. errorHandler() - Global error handler
|
||||
```
|
||||
|
||||
## ⚙️ Environment Variables
|
||||
|
||||
```bash
|
||||
# Server
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_USER=root
|
||||
DB_PASS=
|
||||
DB_NAME=hotel_db
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your-super-secret-jwt-key
|
||||
JWT_EXPIRES_IN=1h
|
||||
JWT_REFRESH_SECRET=your-refresh-token-secret
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
|
||||
# Client
|
||||
CLIENT_URL=http://localhost:5173
|
||||
```
|
||||
|
||||
## 📋 Next Steps (Manual)
|
||||
|
||||
### 1. Database Setup
|
||||
```bash
|
||||
# Tạo database
|
||||
mysql -u root -p
|
||||
CREATE DATABASE hotel_db;
|
||||
|
||||
# Chạy migrations
|
||||
cd d:/hotel-booking/server
|
||||
npm run migrate
|
||||
|
||||
# (Optional) Seed data
|
||||
npm run seed
|
||||
```
|
||||
|
||||
### 2. Start Server
|
||||
```bash
|
||||
cd d:/hotel-booking/server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
✅ Database connection established successfully
|
||||
📊 Database models synced
|
||||
🚀 Server running on port 3000
|
||||
🌐 Environment: development
|
||||
🔗 API: http://localhost:3000/api
|
||||
🏥 Health: http://localhost:3000/health
|
||||
```
|
||||
|
||||
### 3. Test API
|
||||
|
||||
**Health Check:**
|
||||
```bash
|
||||
curl http://localhost:3000/health
|
||||
```
|
||||
|
||||
**Register:**
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Test User",
|
||||
"email": "test@example.com",
|
||||
"password": "Test1234",
|
||||
"phone": "0123456789"
|
||||
}'
|
||||
```
|
||||
|
||||
**Login:**
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "test@example.com",
|
||||
"password": "Test1234",
|
||||
"rememberMe": true
|
||||
}'
|
||||
```
|
||||
|
||||
### 4. Test with Frontend
|
||||
|
||||
1. Make sure client .env has:
|
||||
```
|
||||
VITE_API_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
2. Start frontend:
|
||||
```bash
|
||||
cd d:/hotel-booking/client
|
||||
npm run dev
|
||||
```
|
||||
|
||||
3. Navigate to: http://localhost:5173/login
|
||||
|
||||
4. Try to login with credentials from step 3
|
||||
|
||||
## 🎯 Integration Checklist
|
||||
|
||||
- [ ] MySQL server running
|
||||
- [ ] Database `hotel_db` created
|
||||
- [ ] Migrations executed successfully
|
||||
- [ ] Server running on port 3000
|
||||
- [ ] Health endpoint returns 200
|
||||
- [ ] Frontend .env configured
|
||||
- [ ] Frontend running on port 5173
|
||||
- [ ] Login API working with Postman/curl
|
||||
- [ ] Frontend login form connects to backend
|
||||
- [ ] JWT tokens stored in localStorage
|
||||
- [ ] Protected routes work after login
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Server won't start
|
||||
- Check MySQL is running
|
||||
- Check .env database credentials
|
||||
- Check port 3000 is not in use
|
||||
|
||||
### Login returns 401
|
||||
- Check email/password are correct
|
||||
- Check user exists in database
|
||||
- Check JWT_SECRET in .env
|
||||
|
||||
### CORS errors in frontend
|
||||
- Check CLIENT_URL in server .env
|
||||
- Check frontend is using correct API URL
|
||||
- Check server CORS middleware
|
||||
|
||||
### Token expired immediately
|
||||
- Check JWT_EXPIRES_IN in .env
|
||||
- Check system clock is correct
|
||||
- Check refresh token mechanism
|
||||
|
||||
## 📚 Code Quality
|
||||
|
||||
- ✅ Proper error handling with try-catch
|
||||
- ✅ Async/await pattern
|
||||
- ✅ Input validation before processing
|
||||
- ✅ Password never returned in responses
|
||||
- ✅ Proper HTTP status codes
|
||||
- ✅ Consistent API response format
|
||||
- ✅ Environment-based logging
|
||||
- ✅ Rate limiting for security
|
||||
- ✅ Token expiry management
|
||||
- ✅ Database connection pooling
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ Backend Setup Complete
|
||||
**Next:** Run migrations → Start server → Test login from frontend
|
||||
**Time to complete:** ~5 minutes manual setup
|
||||
|
||||
🎉 **Congratulations!** Backend API is ready for testing!
|
||||
590
docs/TEST_ROUTE_PROTECTION.md
Normal file
590
docs/TEST_ROUTE_PROTECTION.md
Normal file
@@ -0,0 +1,590 @@
|
||||
# Test Scenarios - Route Protection (Chức năng 8)
|
||||
|
||||
## Test Setup
|
||||
|
||||
### Test Users
|
||||
```javascript
|
||||
// Admin user
|
||||
{
|
||||
email: "admin@hotel.com",
|
||||
password: "Admin@123",
|
||||
role: "admin"
|
||||
}
|
||||
|
||||
// Customer user
|
||||
{
|
||||
email: "customer@hotel.com",
|
||||
password: "Customer@123",
|
||||
role: "customer"
|
||||
}
|
||||
|
||||
// Staff user
|
||||
{
|
||||
email: "staff@hotel.com",
|
||||
password: "Staff@123",
|
||||
role: "staff"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Case 1: ProtectedRoute - Unauthenticated User
|
||||
|
||||
### Objective
|
||||
Verify that unauthenticated users cannot access protected routes.
|
||||
|
||||
### Steps
|
||||
1. Open browser (incognito mode)
|
||||
2. Navigate to `http://localhost:5173/dashboard`
|
||||
|
||||
### Expected Result
|
||||
- ✅ Redirected to `/login`
|
||||
- ✅ URL shows `/login`
|
||||
- ✅ Login form displayed
|
||||
- ✅ No error in console
|
||||
- ✅ Location state contains `from: '/dashboard'`
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 2: ProtectedRoute - Authenticated User
|
||||
|
||||
### Objective
|
||||
Verify that authenticated users can access protected routes.
|
||||
|
||||
### Steps
|
||||
1. Login as customer (`customer@hotel.com` / `Customer@123`)
|
||||
2. Navigate to `http://localhost:5173/dashboard`
|
||||
|
||||
### Expected Result
|
||||
- ✅ Dashboard page displayed successfully
|
||||
- ✅ URL shows `/dashboard`
|
||||
- ✅ No redirect to login
|
||||
- ✅ Navbar shows user info
|
||||
- ✅ Logout button visible
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 3: ProtectedRoute - Redirect After Login
|
||||
|
||||
### Objective
|
||||
Verify that users are redirected back to original page after login.
|
||||
|
||||
### Steps
|
||||
1. Logout (if logged in)
|
||||
2. Navigate to `http://localhost:5173/bookings` (protected)
|
||||
3. Should redirect to `/login`
|
||||
4. Login with valid credentials
|
||||
5. Observe redirect behavior
|
||||
|
||||
### Expected Result
|
||||
- ✅ Step 2: Redirected to `/login`
|
||||
- ✅ Step 4: Login successful
|
||||
- ✅ Step 5: Redirected back to `/bookings` (original page)
|
||||
- ✅ URL shows `/bookings`
|
||||
- ✅ BookingListPage displayed
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 4: AdminRoute - Unauthenticated User
|
||||
|
||||
### Objective
|
||||
Verify that unauthenticated users cannot access admin routes.
|
||||
|
||||
### Steps
|
||||
1. Open browser (incognito mode)
|
||||
2. Navigate to `http://localhost:5173/admin`
|
||||
|
||||
### Expected Result
|
||||
- ✅ Redirected to `/login`
|
||||
- ✅ URL shows `/login`
|
||||
- ✅ Login form displayed
|
||||
- ✅ Location state contains `from: '/admin'`
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 5: AdminRoute - Customer User
|
||||
|
||||
### Objective
|
||||
Verify that non-admin users (customer) cannot access admin routes.
|
||||
|
||||
### Steps
|
||||
1. Login as customer (`customer@hotel.com` / `Customer@123`)
|
||||
2. Navigate to `http://localhost:5173/admin`
|
||||
|
||||
### Expected Result
|
||||
- ✅ Redirected to `/` (homepage)
|
||||
- ✅ URL shows `/`
|
||||
- ✅ Homepage displayed
|
||||
- ✅ No admin content visible
|
||||
- ✅ Toast message: "Access denied" (optional)
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 6: AdminRoute - Staff User
|
||||
|
||||
### Objective
|
||||
Verify that staff users cannot access admin routes.
|
||||
|
||||
### Steps
|
||||
1. Login as staff (`staff@hotel.com` / `Staff@123`)
|
||||
2. Navigate to `http://localhost:5173/admin`
|
||||
|
||||
### Expected Result
|
||||
- ✅ Redirected to `/` (homepage)
|
||||
- ✅ URL shows `/`
|
||||
- ✅ Homepage displayed
|
||||
- ✅ No admin content visible
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 7: AdminRoute - Admin User
|
||||
|
||||
### Objective
|
||||
Verify that admin users can access admin routes.
|
||||
|
||||
### Steps
|
||||
1. Login as admin (`admin@hotel.com` / `Admin@123`)
|
||||
2. Navigate to `http://localhost:5173/admin`
|
||||
|
||||
### Expected Result
|
||||
- ✅ Admin dashboard displayed
|
||||
- ✅ URL shows `/admin` or `/admin/dashboard`
|
||||
- ✅ Admin sidebar visible
|
||||
- ✅ Admin navigation menu visible
|
||||
- ✅ Can access all admin sub-routes:
|
||||
- `/admin/users`
|
||||
- `/admin/rooms`
|
||||
- `/admin/bookings`
|
||||
- `/admin/payments`
|
||||
- `/admin/services`
|
||||
- `/admin/promotions`
|
||||
- `/admin/banners`
|
||||
- `/admin/reports`
|
||||
- `/admin/settings`
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 8: Loading State - Slow Network
|
||||
|
||||
### Objective
|
||||
Verify that loading state is displayed during auth check.
|
||||
|
||||
### Steps
|
||||
1. Open DevTools → Network tab
|
||||
2. Set throttling to "Slow 3G"
|
||||
3. Refresh page at protected route
|
||||
4. Observe loading behavior
|
||||
|
||||
### Expected Result
|
||||
- ✅ Loading spinner displayed
|
||||
- ✅ Text "Đang tải..." or "Đang xác thực..." visible
|
||||
- ✅ No flash of redirect
|
||||
- ✅ Smooth transition after loading
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 9: Token Expiration
|
||||
|
||||
### Objective
|
||||
Verify that expired tokens are handled correctly.
|
||||
|
||||
### Steps
|
||||
1. Login successfully
|
||||
2. Manually modify token in localStorage to invalid value:
|
||||
```javascript
|
||||
localStorage.setItem('token', 'invalid-token')
|
||||
```
|
||||
3. Navigate to protected route `/dashboard`
|
||||
4. Observe behavior
|
||||
|
||||
### Expected Result
|
||||
- ✅ Redirected to `/login`
|
||||
- ✅ Toast message: "Session expired" (optional)
|
||||
- ✅ Location state saved
|
||||
- ✅ Can login again successfully
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 10: Direct URL Access - Admin
|
||||
|
||||
### Objective
|
||||
Verify that direct URL access to admin routes is blocked for non-admin.
|
||||
|
||||
### Steps
|
||||
1. Login as customer
|
||||
2. Type in address bar: `http://localhost:5173/admin/users`
|
||||
3. Press Enter
|
||||
|
||||
### Expected Result
|
||||
- ✅ Redirected to `/` (homepage)
|
||||
- ✅ Cannot access admin/users
|
||||
- ✅ No admin content visible
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 11: Nested Admin Routes
|
||||
|
||||
### Objective
|
||||
Verify that all nested admin routes are protected.
|
||||
|
||||
### Steps
|
||||
1. Login as admin
|
||||
2. Navigate to each admin route:
|
||||
- `/admin/dashboard`
|
||||
- `/admin/users`
|
||||
- `/admin/rooms`
|
||||
- `/admin/bookings`
|
||||
- `/admin/payments`
|
||||
- `/admin/services`
|
||||
- `/admin/promotions`
|
||||
- `/admin/banners`
|
||||
- `/admin/reports`
|
||||
- `/admin/settings`
|
||||
|
||||
### Expected Result
|
||||
- ✅ All routes accessible
|
||||
- ✅ Each page displays correctly
|
||||
- ✅ No errors in console
|
||||
- ✅ Admin layout consistent
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 12: Public Routes Access
|
||||
|
||||
### Objective
|
||||
Verify that public routes are accessible without authentication.
|
||||
|
||||
### Steps
|
||||
1. Logout (if logged in)
|
||||
2. Navigate to public routes:
|
||||
- `/` (homepage)
|
||||
- `/rooms`
|
||||
- `/about`
|
||||
- `/login`
|
||||
- `/register`
|
||||
- `/forgot-password`
|
||||
|
||||
### Expected Result
|
||||
- ✅ All public routes accessible
|
||||
- ✅ No redirect to login
|
||||
- ✅ Pages display correctly
|
||||
- ✅ No errors in console
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 13: Logout Behavior
|
||||
|
||||
### Objective
|
||||
Verify that logout clears auth and redirects properly.
|
||||
|
||||
### Steps
|
||||
1. Login as any user
|
||||
2. Navigate to protected route `/dashboard`
|
||||
3. Click logout button
|
||||
4. Observe behavior
|
||||
|
||||
### Expected Result
|
||||
- ✅ User logged out
|
||||
- ✅ Token removed from localStorage
|
||||
- ✅ userInfo removed from localStorage
|
||||
- ✅ Redirected to `/` or `/login`
|
||||
- ✅ Navbar shows "Đăng nhập" button
|
||||
- ✅ Cannot access protected routes anymore
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 14: Multiple Tabs - Logout Sync
|
||||
|
||||
### Objective
|
||||
Verify that logout in one tab affects other tabs.
|
||||
|
||||
### Steps
|
||||
1. Login in Tab 1
|
||||
2. Open Tab 2 with same site
|
||||
3. Logout in Tab 1
|
||||
4. Switch to Tab 2
|
||||
5. Try to access protected route
|
||||
|
||||
### Expected Result
|
||||
- ✅ Tab 2 detects logout
|
||||
- ✅ Redirected to login
|
||||
- ✅ Auth state synced across tabs
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 15: Browser Refresh - Auth Persistence
|
||||
|
||||
### Objective
|
||||
Verify that auth state persists after browser refresh.
|
||||
|
||||
### Steps
|
||||
1. Login successfully
|
||||
2. Navigate to protected route `/dashboard`
|
||||
3. Press F5 (refresh)
|
||||
4. Observe behavior
|
||||
|
||||
### Expected Result
|
||||
- ✅ User still logged in
|
||||
- ✅ Dashboard displays correctly
|
||||
- ✅ No redirect to login
|
||||
- ✅ userInfo still available
|
||||
- ✅ Token still in localStorage
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 16: Role Change Detection
|
||||
|
||||
### Objective
|
||||
Verify that role changes are detected and enforced.
|
||||
|
||||
### Steps
|
||||
1. Login as admin
|
||||
2. Access `/admin/dashboard` successfully
|
||||
3. Manually change role in localStorage:
|
||||
```javascript
|
||||
const user = JSON.parse(localStorage.getItem('userInfo'))
|
||||
user.role = 'customer'
|
||||
localStorage.setItem('userInfo', JSON.stringify(user))
|
||||
```
|
||||
4. Refresh page
|
||||
5. Try to access `/admin`
|
||||
|
||||
### Expected Result
|
||||
- ✅ Redirected to `/` (homepage)
|
||||
- ✅ Cannot access admin routes
|
||||
- ✅ Role validation working
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 17: 404 Route
|
||||
|
||||
### Objective
|
||||
Verify that non-existent routes show 404 page.
|
||||
|
||||
### Steps
|
||||
1. Navigate to `http://localhost:5173/non-existent-route`
|
||||
|
||||
### Expected Result
|
||||
- ✅ 404 page displayed
|
||||
- ✅ "404 - Không tìm thấy trang" message
|
||||
- ✅ URL shows `/non-existent-route`
|
||||
- ✅ No errors in console
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Test Case 18: Remember Me Feature
|
||||
|
||||
### Objective
|
||||
Verify that "Remember Me" extends token duration.
|
||||
|
||||
### Steps
|
||||
1. Login with "Remember Me" checked
|
||||
2. Check token expiry in localStorage
|
||||
3. Close browser
|
||||
4. Reopen browser after 1 day
|
||||
5. Navigate to protected route
|
||||
|
||||
### Expected Result
|
||||
- ✅ User still logged in (if < 7 days)
|
||||
- ✅ No need to login again
|
||||
- ✅ Token valid
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Performance Tests
|
||||
|
||||
### Test Case 19: Route Navigation Speed
|
||||
|
||||
### Steps
|
||||
1. Login as user
|
||||
2. Navigate between routes:
|
||||
- `/dashboard` → `/bookings` → `/profile` → `/rooms`
|
||||
3. Measure navigation time
|
||||
|
||||
### Expected Result
|
||||
- ✅ Navigation < 200ms
|
||||
- ✅ No flickering
|
||||
- ✅ Smooth transitions
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Security Tests
|
||||
|
||||
### Test Case 20: XSS in Route Params
|
||||
|
||||
### Steps
|
||||
1. Navigate to `/reset-password/<script>alert('xss')</script>`
|
||||
2. Observe behavior
|
||||
|
||||
### Expected Result
|
||||
- ✅ No alert popup
|
||||
- ✅ Script not executed
|
||||
- ✅ Token treated as string
|
||||
|
||||
### Actual Result
|
||||
- [ ] Pass
|
||||
- [ ] Fail (describe issue):
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Test Case | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| TC-01: ProtectedRoute - Unauth | ⏳ | |
|
||||
| TC-02: ProtectedRoute - Auth | ⏳ | |
|
||||
| TC-03: Redirect After Login | ⏳ | |
|
||||
| TC-04: AdminRoute - Unauth | ⏳ | |
|
||||
| TC-05: AdminRoute - Customer | ⏳ | |
|
||||
| TC-06: AdminRoute - Staff | ⏳ | |
|
||||
| TC-07: AdminRoute - Admin | ⏳ | |
|
||||
| TC-08: Loading State | ⏳ | |
|
||||
| TC-09: Token Expiration | ⏳ | |
|
||||
| TC-10: Direct URL - Admin | ⏳ | |
|
||||
| TC-11: Nested Admin Routes | ⏳ | |
|
||||
| TC-12: Public Routes | ⏳ | |
|
||||
| TC-13: Logout | ⏳ | |
|
||||
| TC-14: Multi-tab Sync | ⏳ | |
|
||||
| TC-15: Refresh Persistence | ⏳ | |
|
||||
| TC-16: Role Change | ⏳ | |
|
||||
| TC-17: 404 Route | ⏳ | |
|
||||
| TC-18: Remember Me | ⏳ | |
|
||||
| TC-19: Performance | ⏳ | |
|
||||
| TC-20: XSS Security | ⏳ | |
|
||||
|
||||
**Overall Status**: ⏳ Pending Testing
|
||||
|
||||
---
|
||||
|
||||
## How to Run Tests
|
||||
|
||||
### Manual Testing
|
||||
```bash
|
||||
# 1. Start server
|
||||
cd server
|
||||
npm run dev
|
||||
|
||||
# 2. Start client
|
||||
cd client
|
||||
npm run dev
|
||||
|
||||
# 3. Open browser
|
||||
http://localhost:5173
|
||||
|
||||
# 4. Follow test cases above
|
||||
```
|
||||
|
||||
### Automated Testing (Optional - Future)
|
||||
```bash
|
||||
# Install testing dependencies
|
||||
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bug Report Template
|
||||
|
||||
```markdown
|
||||
### Bug: [Short description]
|
||||
|
||||
**Test Case**: TC-XX
|
||||
|
||||
**Steps to Reproduce**:
|
||||
1. Step 1
|
||||
2. Step 2
|
||||
3. Step 3
|
||||
|
||||
**Expected**: [What should happen]
|
||||
|
||||
**Actual**: [What actually happened]
|
||||
|
||||
**Environment**:
|
||||
- Browser: Chrome 120
|
||||
- OS: Windows 11
|
||||
- Node: v18.17.0
|
||||
|
||||
**Screenshots**: [Attach if applicable]
|
||||
|
||||
**Console Errors**: [Copy-paste errors]
|
||||
|
||||
**Priority**: High/Medium/Low
|
||||
```
|
||||
2
docs/VNPAY_QUICK_START.md
Normal file
2
docs/VNPAY_QUICK_START.md
Normal file
@@ -0,0 +1,2 @@
|
||||
VNPay integration docs removed per repository cleanup.
|
||||
This file has been replaced with a short note because VNPay is no longer used in this project.
|
||||
0
docs/VNPAY_SETUP.md
Normal file
0
docs/VNPAY_SETUP.md
Normal file
207
docs/tasks-admin.md
Normal file
207
docs/tasks-admin.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# 🏨 Hotel Management & Booking System
|
||||
## Bản Phân Tích Dành Cho Admin (SRS Admin Analysis)
|
||||
|
||||
---
|
||||
|
||||
## 1. Giới thiệu
|
||||
Tài liệu này phân tích các yêu cầu từ SRS của hệ thống **Hotel Management & Booking Online (e-Hotel)**, tập trung hoàn toàn vào phần **Admin / Manager / Staff** (không bao gồm khách hàng).
|
||||
Mục tiêu là nắm rõ các chức năng quản trị, vận hành và bảo mật của hệ thống.
|
||||
|
||||
---
|
||||
|
||||
# 2. Phân tích chức năng dành cho Admin
|
||||
|
||||
---
|
||||
|
||||
## 2.1 Setup Module (Thiết lập hệ thống)
|
||||
|
||||
### 2.1.1 Setup Rooms (Quản lý phòng)
|
||||
**Vai trò sử dụng:** Manager, Admin
|
||||
|
||||
**Các chức năng:**
|
||||
- Thêm mới phòng
|
||||
- Chỉnh sửa thông tin phòng
|
||||
- Xoá phòng *(chỉ khi phòng chưa có booking)*
|
||||
- Upload hình ảnh phòng
|
||||
|
||||
**Thông tin phòng gồm:**
|
||||
- RoomID
|
||||
- Description
|
||||
- Type (VIP, DELUX, SUITE, …)
|
||||
- Size (Single, Double, …)
|
||||
- Price
|
||||
- Pictures
|
||||
|
||||
**Quy tắc:**
|
||||
- Validate toàn bộ dữ liệu khi thêm/sửa
|
||||
- Không cho xoá phòng đã phát sinh booking
|
||||
|
||||
---
|
||||
|
||||
### 2.1.2 Setup Services (Quản lý dịch vụ)
|
||||
**Vai trò:** Manager, Admin
|
||||
|
||||
**Chức năng:**
|
||||
- Thêm dịch vụ
|
||||
- Chỉnh sửa
|
||||
- Xoá dịch vụ
|
||||
|
||||
**Thông tin dịch vụ:**
|
||||
- Service ID
|
||||
- Service Name
|
||||
- Description
|
||||
- Unit (giờ, suất, lần,…)
|
||||
- Price
|
||||
|
||||
**Quy tắc:**
|
||||
- Validate tất cả dữ liệu nhập
|
||||
|
||||
---
|
||||
|
||||
### 2.1.3 Promotion Management (Quản lý khuyến mãi)
|
||||
**Vai trò:** Manager, Admin
|
||||
|
||||
**Chức năng:**
|
||||
- Add promotion
|
||||
- Edit promotion
|
||||
- Delete promotion
|
||||
- Promotion có thể áp dụng bằng code hoặc tự động trong booking
|
||||
|
||||
**Thông tin:**
|
||||
- ID
|
||||
- Name
|
||||
- Description
|
||||
- Value (phần trăm hoặc số tiền)
|
||||
|
||||
---
|
||||
|
||||
# 2.2 Operation Module (Vận hành khách sạn)
|
||||
|
||||
---
|
||||
|
||||
## 2.2.1 Booking Management
|
||||
**Vai trò:** Staff, Manager, Admin
|
||||
|
||||
**Chức năng:**
|
||||
- Tìm booking theo tên khách, số booking, ngày đặt
|
||||
- Xem chi tiết booking
|
||||
- Xem bill dịch vụ
|
||||
- Xử lý yêu cầu:
|
||||
- Hủy booking
|
||||
- Checkout
|
||||
|
||||
---
|
||||
|
||||
## 2.2.2 Check-in
|
||||
**Vai trò:** Staff, Manager
|
||||
|
||||
**Quy trình check-in:**
|
||||
- Khách xuất trình Booking Number
|
||||
- Nhân viên kiểm tra thông tin booking
|
||||
- Nhập thông tin từng khách trong phòng
|
||||
- Gán số phòng thực tế
|
||||
- Thu thêm phí nếu có trẻ em hoặc extra person
|
||||
|
||||
---
|
||||
|
||||
## 2.2.3 Use Services (Khách đăng ký sử dụng dịch vụ)
|
||||
**Vai trò:** Staff
|
||||
|
||||
**Chức năng:**
|
||||
- Đăng ký dịch vụ cho khách dựa trên Room Number
|
||||
- In ticket nếu có yêu cầu
|
||||
|
||||
---
|
||||
|
||||
## 2.2.4 Check-out
|
||||
**Vai trò:** Staff, Manager
|
||||
|
||||
**Chức năng:**
|
||||
- Tính toán:
|
||||
- Phí phòng
|
||||
- Phí dịch vụ
|
||||
- Phụ phí khác
|
||||
- Tạo hóa đơn (Invoice)
|
||||
- Khấu trừ tiền đã đặt cọc (booking value)
|
||||
- Khách thanh toán phần còn lại
|
||||
|
||||
---
|
||||
|
||||
# 2.3 Report Module (Báo cáo)
|
||||
|
||||
**Vai trò:** Manager, Admin
|
||||
|
||||
**Chức năng:**
|
||||
- Nhập khoảng thời gian From → To
|
||||
- Liệt kê toàn bộ booking trong khoảng thời gian
|
||||
- Tính tổng doanh thu
|
||||
- Xuất báo cáo:
|
||||
- Excel
|
||||
- PDF
|
||||
|
||||
**Nội dung báo cáo:**
|
||||
- Booking ID
|
||||
- Customer Name
|
||||
- Room
|
||||
- Total Amount
|
||||
- Booking Date
|
||||
- Status
|
||||
- Revenue Summary
|
||||
|
||||
---
|
||||
|
||||
# 2.4 System Administration Module (Quản trị hệ thống)
|
||||
|
||||
---
|
||||
|
||||
## 2.4.1 User Management
|
||||
**Vai trò:** Admin
|
||||
|
||||
**Chức năng:**
|
||||
- Add user
|
||||
- Edit user
|
||||
- Delete user
|
||||
- View user detail
|
||||
- List tất cả user
|
||||
- Gán role (Admin, Manager, Staff)
|
||||
|
||||
---
|
||||
|
||||
## 2.4.2 Security
|
||||
**Chức năng bảo mật của hệ thống:**
|
||||
|
||||
### Roles được định nghĩa:
|
||||
| Role | Quyền |
|
||||
|------|-------|
|
||||
| **Customer** | Không cần login |
|
||||
| **Staff (Sale)** | Truy cập Operation Module |
|
||||
| **Manager** | Truy cập Setup Module |
|
||||
| **Admin** | Toàn quyền, bao gồm User & Security |
|
||||
|
||||
### Quy tắc bảo mật:
|
||||
- Nhân viên & admin bắt buộc phải login
|
||||
- Quyền thao tác phụ thuộc vào role
|
||||
- Session timeout sau 30 phút không hoạt động
|
||||
|
||||
---
|
||||
|
||||
# 3. Tóm tắt theo góc nhìn Admin
|
||||
|
||||
| Module | Quyền Admin | Nội dung |
|
||||
|--------|-------------|----------|
|
||||
| Room Setup | Full | CRUD phòng |
|
||||
| Service Setup | Full | CRUD dịch vụ |
|
||||
| Promotion Setup | Full | CRUD khuyến mãi |
|
||||
| Booking Management | Full | Xem, duyệt, hủy booking |
|
||||
| Check-in / Check-out | Full | Quản lý vận hành |
|
||||
| Service Usage | Full | Ghi log dịch vụ |
|
||||
| Reports | Full | Thống kê, xuất file |
|
||||
| User Management | Full | Quản lý nhân viên |
|
||||
| Security | Full | Role, phân quyền |
|
||||
|
||||
---
|
||||
|
||||
# 4. Kết luận
|
||||
Phân tích trên giúp xác định đầy đủ các chức năng cần triển khai cho **Admin / Manager / Staff** trong hệ thống quản lý khách sạn.
|
||||
Tài liệu có thể được sử dụng để xây dựng database, API, UI/UX, và phân quyền hệ thống.
|
||||
|
||||
228
docs/tasks_1_Authentication.md
Normal file
228
docs/tasks_1_Authentication.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# Authentication
|
||||
|
||||
## Chức năng 1: Layout cơ bản (Header, Footer, Navbar, SidebarAdmin)
|
||||
|
||||
### Mục tiêu
|
||||
Tạo layout nền tảng cho toàn bộ hệ thống và cấu trúc render nội dung theo route.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
- Tạo thư mục:
|
||||
```
|
||||
src/components/layouts/
|
||||
```
|
||||
- Bao gồm:
|
||||
+ Header.jsx
|
||||
+ Footer.jsx
|
||||
+ Navbar.jsx
|
||||
+ SidebarAdmin.jsx
|
||||
+ LayoutMain.jsx
|
||||
- Dùng <Outlet /> trong LayoutMain để render nội dung động.
|
||||
- Navbar thay đổi tùy trạng thái đăng nhập:
|
||||
+ Nếu chưa login → hiển thị nút “Đăng nhập / Đăng ký”.
|
||||
+ Nếu đã login → hiển thị avatar, tên user và nút “Đăng xuất”.
|
||||
- SidebarAdmin chỉ hiển thị với role = admin.
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Layout tổng thể hiển thị ổn định.
|
||||
2. Navbar hiển thị nội dung động theo trạng thái người dùng.
|
||||
3. Giao diện responsive, tương thích desktop/mobile.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 2: Cấu hình Routing (react-router-dom)
|
||||
|
||||
### Mục tiêu
|
||||
Thiết lập hệ thống định tuyến chuẩn, có bảo vệ route theo role.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
- Cấu trúc route chính:
|
||||
```
|
||||
<Route path="/" element={<LayoutMain />}>
|
||||
<Route index element={<HomePage />} />
|
||||
<Route path="rooms" element={<RoomListPage />} />
|
||||
<Route path="bookings" element={<BookingListPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
|
||||
<Route path="/reset-password/:token" element={<ResetPasswordPage />} />
|
||||
|
||||
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
|
||||
<Route path="/admin/*" element={<AdminRoute><AdminModule /></AdminRoute>} />
|
||||
```
|
||||
- Dùng ProtectedRoute và AdminRoute để kiểm tra:
|
||||
+ isAuthenticated
|
||||
+ role === "admin"
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Người dùng không đăng nhập bị redirect về /login.
|
||||
2. AdminRoute chỉ cho phép admin truy cập.
|
||||
3. Tất cả route hoạt động mượt, không lỗi vòng lặp redirect.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 3: useAuthStore (Zustand Store)
|
||||
|
||||
### Mục tiêu
|
||||
Quản lý trạng thái xác thực toàn cục (token, userInfo, role).
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
- Tạo src/stores/useAuthStore.js
|
||||
- Cấu trúc:
|
||||
```
|
||||
const useAuthStore = create((set) => ({
|
||||
token: localStorage.getItem("token") || null,
|
||||
userInfo: JSON.parse(localStorage.getItem("userInfo")) || null,
|
||||
isAuthenticated: !!localStorage.getItem("token"),
|
||||
|
||||
login: async (credentials) => { ... },
|
||||
logout: () => { ... },
|
||||
setUser: (user) => { ... },
|
||||
resetPassword: async (payload) => { ... },
|
||||
}));
|
||||
```
|
||||
- Khi đăng nhập thành công:
|
||||
+ Lưu token + userInfo vào localStorage.
|
||||
- Khi logout:
|
||||
+ Xóa localStorage và reset state.
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Toàn bộ thông tin user được quản lý tập trung.
|
||||
2. Duy trì đăng nhập sau khi reload trang.
|
||||
3. Dễ dàng truy cập userInfo trong mọi component.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 4: Form Login
|
||||
|
||||
### Mục tiêu
|
||||
Cho phép người dùng đăng nhập hệ thống.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
- Tạo LoginPage.jsx
|
||||
- Dùng React Hook Form + Yup validate:
|
||||
+ Email hợp lệ
|
||||
+ Mật khẩu ≥ 8 ký tự
|
||||
- API:
|
||||
```
|
||||
POST /api/auth/login
|
||||
```
|
||||
- Sau khi đăng nhập thành công:
|
||||
+ Lưu token vào localStorage.
|
||||
+ Gọi setUser() để cập nhật Zustand.
|
||||
+ Redirect về /dashboard.
|
||||
+ Gửi email POST /api/notify/login-success.
|
||||
- UX nâng cao:
|
||||
+ Nút loading khi đang gửi form.
|
||||
+ “Hiện/Ẩn mật khẩu”.
|
||||
+ “Nhớ đăng nhập” → lưu 7 ngày.
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Đăng nhập hoạt động mượt, hiển thị thông báo lỗi rõ ràng.
|
||||
2. Email được gửi khi login thành công.
|
||||
3. Chuyển hướng đúng theo vai trò user.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 5: Form Register
|
||||
|
||||
### Mục tiêu
|
||||
Cho phép người dùng đăng ký tài khoản mới.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
- Tạo RegisterPage.jsx
|
||||
- Dùng React Hook Form + Yup validate:
|
||||
+ Họ tên không rỗng
|
||||
+ Email hợp lệ
|
||||
+ Mật khẩu ≥ 8 ký tự, có ký tự đặc biệt
|
||||
- API:
|
||||
```
|
||||
POST /api/auth/register
|
||||
```
|
||||
- Sau khi đăng ký thành công:
|
||||
+ Hiển thị toast “Đăng ký thành công, vui lòng đăng nhập”.
|
||||
+ Redirect về /login.
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Người dùng tạo tài khoản mới thành công.
|
||||
2. Validate chặt chẽ, UX mượt mà.
|
||||
3. Giao diện thống nhất với form login.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 6: Quên mật khẩu (Forgot Password)
|
||||
|
||||
### Mục tiêu
|
||||
Cung cấp chức năng gửi email reset mật khẩu.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
- Tạo ForgotPasswordPage.jsx
|
||||
- API:
|
||||
```
|
||||
POST /api/auth/forgot-password
|
||||
```
|
||||
- Sau khi gửi thành công:
|
||||
+ Hiển thị thông báo “Vui lòng kiểm tra email để đặt lại mật khẩu.”
|
||||
+ Backend gửi link reset có token dạng:
|
||||
```
|
||||
https://domain.com/reset-password/:token
|
||||
```
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Gửi email thành công.
|
||||
2. UX rõ ràng, có loading và thông báo lỗi.
|
||||
3. Giao diện thân thiện.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 7: Đặt lại mật khẩu (Reset Password)
|
||||
|
||||
### Mục tiêu
|
||||
Cho phép người dùng đổi mật khẩu thông qua link email.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
- Tạo ResetPasswordPage.jsx
|
||||
- Validate:
|
||||
+ Mật khẩu mới ≥ 8 ký tự, chứa ký tự đặc biệt
|
||||
+ Nhập lại mật khẩu trùng khớp
|
||||
- API:
|
||||
```
|
||||
POST /api/auth/reset-password
|
||||
```
|
||||
- Sau khi đổi mật khẩu thành công:
|
||||
+ Gửi email xác nhận POST /api/notify/reset-success.
|
||||
+ Redirect về /login.
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Mật khẩu được cập nhật thành công.
|
||||
2. Gửi email thông báo thành công.
|
||||
3. Bảo vệ token hết hạn (invalid token → redirect về forgot-password).
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 8: Phân quyền & Bảo vệ route (ProtectedRoute / AdminRoute)
|
||||
|
||||
### Mục tiêu
|
||||
Chặn truy cập trái phép và bảo vệ các route quan trọng.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
- Tạo component ProtectedRoute.jsx:
|
||||
```
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
const location = useLocation();
|
||||
return isAuthenticated ? children : <Navigate to="/login" state={{ from: location }} replace />;
|
||||
};
|
||||
```
|
||||
|
||||
- Tạo AdminRoute.jsx:
|
||||
```
|
||||
const AdminRoute = ({ children }) => {
|
||||
const { userInfo } = useAuthStore();
|
||||
return userInfo?.role === "admin" ? children : <Navigate to="/" replace />;
|
||||
};
|
||||
```
|
||||
### Kết quả mong đợi
|
||||
1. Chỉ người dùng hợp lệ mới truy cập được route quan trọng.
|
||||
2. AdminRoute đảm bảo bảo mật cho module quản trị.
|
||||
182
docs/tasks_2_Home_&_RoomSearch.md
Normal file
182
docs/tasks_2_Home_&_RoomSearch.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Review System
|
||||
|
||||
## Chức năng 1: HomePage – Trang chủ hiển thị phòng nổi bật
|
||||
|
||||
### Mục tiêu
|
||||
Tạo giao diện trang chủ giới thiệu phòng nổi bật, banner và điều hướng đến danh sách phòng.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. Route: /
|
||||
2. Banner:
|
||||
```
|
||||
GET /api/banners?position=home
|
||||
```
|
||||
- Nếu không có banner → hiển thị ảnh mặc định.
|
||||
- Có thể dùng Carousel hoặc ảnh tĩnh.
|
||||
3. Phòng nổi bật:
|
||||
```
|
||||
GET /api/rooms?featured=true
|
||||
```
|
||||
- Hiển thị 4–6 phòng bằng component RoomCard.
|
||||
- Nút “Xem tất cả phòng” → điều hướng /rooms.
|
||||
4. Loading skeleton trong khi chờ dữ liệu.
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Trang chủ hiển thị banner và danh sách phòng nổi bật rõ ràng.
|
||||
2. Khi không có banner → ảnh fallback được hiển thị.
|
||||
3. Phòng nổi bật load từ API, giới hạn 4–6 phòng.
|
||||
4. UX mượt, có skeleton khi load.
|
||||
5. Nút “Xem tất cả phòng” điều hướng chính xác đến /rooms.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 2: RoomListPage – Danh sách & Bộ lọc phòng
|
||||
|
||||
### Mục tiêu
|
||||
Hiển thị danh sách phòng, cho phép người dùng lọc theo loại, giá, số người và phân trang.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. Route: /rooms
|
||||
2. Bộ lọc (component RoomFilter):
|
||||
- Trường lọc: loại phòng, giá min–max, số người.
|
||||
- Khi submit → gọi API:
|
||||
```
|
||||
GET /api/rooms?type=&minPrice=&maxPrice=&capacity=&page=
|
||||
```
|
||||
- Lưu bộ lọc vào URL query.
|
||||
- Nút “Reset” để xóa toàn bộ bộ lọc.
|
||||
3. Phân trang (Pagination component).
|
||||
4. Hiển thị danh sách bằng RoomCard.
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Danh sách phòng hiển thị chính xác theo filter.
|
||||
2. Bộ lọc hoạt động mượt, có thể reset dễ dàng.
|
||||
3. Phân trang hiển thị chính xác số trang.
|
||||
4. Filter được lưu trong URL (giúp reload không mất).
|
||||
5. Giao diện responsive, dễ đọc, không bị vỡ.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 3: RoomDetailPage – Chi tiết phòng & Đánh giá
|
||||
|
||||
### Mục tiêu
|
||||
Tạo trang chi tiết phòng đầy đủ thông tin, hình ảnh, tiện ích và khu vực đánh giá.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. Route: /rooms/:id
|
||||
2. Phần nội dung:
|
||||
-Thông tin phòng (ảnh, mô tả, giá, tiện ích)
|
||||
- RoomGallery: Carousel ảnh
|
||||
- RoomAmenities: danh sách tiện ích
|
||||
- Nút “Đặt ngay” → điều hướng /booking/:roomId
|
||||
3. Review Section:
|
||||
- Lấy danh sách review đã duyệt:
|
||||
```
|
||||
GET /api/rooms/:id/reviews
|
||||
```
|
||||
- Nếu người dùng đã từng đặt phòng:
|
||||
```
|
||||
POST /api/reviews
|
||||
```
|
||||
4. Component RatingStars + ReviewForm.
|
||||
5. Nếu chưa đăng nhập → hiển thị “Vui lòng đăng nhập để đánh giá”.
|
||||
6. Tính trung bình điểm review.
|
||||
7. Loading skeleton khi chờ review.
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Hiển thị đầy đủ ảnh, mô tả, tiện ích phòng.
|
||||
2. Carousel hoạt động mượt mà.
|
||||
3. Review hiển thị đúng, có trung bình số sao.
|
||||
4. Người đã đặt có thể viết review (sau duyệt).
|
||||
5. Nút “Đặt ngay” điều hướng chính xác đến form booking.
|
||||
6. Skeleton hiển thị khi chờ dữ liệu.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 4: SearchRoom – Tìm phòng trống
|
||||
|
||||
### Mục tiêu
|
||||
Cho phép người dùng tìm phòng trống theo ngày và loại phòng.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. Form tìm kiếm (ở HomePage hoặc RoomListPage):
|
||||
- Input: ngày đến (from), ngày đi (to), loại phòng.
|
||||
2. API:
|
||||
```
|
||||
GET /api/rooms/available?from=&to=&type=
|
||||
```
|
||||
3. Validate:
|
||||
- from < to
|
||||
- from không nhỏ hơn hôm nay.
|
||||
4. Kết quả:
|
||||
- Hiển thị danh sách bằng RoomCard.
|
||||
- Nếu không có kết quả → “Không tìm thấy phòng phù hợp”.
|
||||
5. Dùng react-datepicker hoặc react-day-picker.
|
||||
6. Loading spinner khi đang tìm.
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Form tìm phòng hoạt động, validate chính xác.
|
||||
2. Khi bấm tìm → hiển thị danh sách phòng trống.
|
||||
3. Nếu không có kết quả → thông báo thân thiện.
|
||||
4. Loading hiển thị rõ trong lúc chờ.
|
||||
5. Tìm theo ngày & loại phòng chính xác từ backend.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 5: Wishlist – Danh sách yêu thích
|
||||
|
||||
### Mục tiêu
|
||||
Cho phép người dùng thêm, bỏ hoặc xem danh sách phòng yêu thích.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. API:
|
||||
```
|
||||
POST /api/favorites/:roomId # Thêm
|
||||
DELETE /api/favorites/:roomId # Xóa
|
||||
GET /api/favorites # Lấy danh sách yêu thích
|
||||
```
|
||||
2. UI:
|
||||
- FavoriteButton (icon ❤️):
|
||||
+ Nếu yêu thích → tô đỏ
|
||||
+ Nếu chưa → viền xám
|
||||
- Tooltip: “Thêm vào yêu thích” / “Bỏ yêu thích”
|
||||
3. Nếu chưa đăng nhập:
|
||||
- Lưu tạm trong localStorage (guestFavorites)
|
||||
- Khi đăng nhập → đồng bộ với server.
|
||||
4. Toast thông báo khi thêm/bỏ yêu thích.
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Nút ❤️ hoạt động đúng trạng thái (đỏ / xám).
|
||||
2. Người chưa đăng nhập vẫn có thể lưu tạm yêu thích.
|
||||
3. Khi đăng nhập → danh sách đồng bộ với backend.
|
||||
4. Toast hiển thị “Đã thêm vào yêu thích” / “Đã bỏ yêu thích”.
|
||||
5. API hoạt động đúng, không lỗi 401 khi đăng nhập hợp lệ.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 6: Tối ưu UI/UX & Performance
|
||||
|
||||
### Mục tiêu
|
||||
Cải thiện trải nghiệm người dùng, tối ưu tốc độ tải và khả năng hiển thị responsive.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. Loading skeleton khi fetch phòng hoặc review.
|
||||
2. Debounce khi nhập giá để tránh gọi API liên tục.
|
||||
3. Infinite scroll (tùy chọn) thay cho pagination.
|
||||
4. Responsive layout:
|
||||
- Desktop: 3–4 cột
|
||||
- Tablet: 2 cột
|
||||
- Mobile: 1 cột
|
||||
5. Empty states:
|
||||
- Không có phòng → hiển thị ảnh minh họa + dòng “Không tìm thấy phòng phù hợp”.
|
||||
- Không có review → “Hãy là người đầu tiên đánh giá!”.
|
||||
6. Toast thông báo khi thêm yêu thích, gửi review, lỗi mạng.
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Trang hoạt động mượt, có skeleton khi chờ dữ liệu.
|
||||
2. Tốc độ phản hồi nhanh (debounce hoạt động).
|
||||
3. Responsive trên mọi kích thước màn hình.
|
||||
4. Các empty state hiển thị thân thiện.
|
||||
5. Toast thông báo rõ ràng, UX thân thiện.
|
||||
|
||||
---
|
||||
144
docs/tasks_3_Booking_&_Payment.md
Normal file
144
docs/tasks_3_Booking_&_Payment.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Booking & Payment
|
||||
|
||||
## Chức năng 1: BookingPage – Form Đặt phòng
|
||||
|
||||
### Mục tiêu
|
||||
Xây dựng form đặt phòng đầy đủ thông tin, xác thực dữ liệu, tính tổng tiền theo số ngày, và gửi yêu cầu đặt.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. Route:
|
||||
```
|
||||
/booking/:roomId
|
||||
```
|
||||
2. Khi user click “Đặt ngay” ở RoomDetailPage → chuyển sang BookingPage.
|
||||
3. Hiển thị:
|
||||
- Ảnh phòng, tên phòng, giá/đêm
|
||||
- Thông tin người dùng (tự động điền nếu đã login)
|
||||
- Form:
|
||||
+ Ngày check-in / check-out (DateRangePicker)
|
||||
+ Số người
|
||||
+ Ghi chú
|
||||
+ Phương thức thanh toán:
|
||||
1. Thanh toán tại chỗ
|
||||
2. Chuyển khoản (hiển thị QR + hướng dẫn)
|
||||
4. Validate bằng Yup + React Hook Form:
|
||||
- Check-in < Check-out
|
||||
- Không bỏ trống ngày
|
||||
- Có chọn phương thức thanh toán
|
||||
5. Tính tổng tiền:
|
||||
```
|
||||
total = room.price * (số ngày ở)
|
||||
```
|
||||
6. Nút “Đặt phòng”:
|
||||
- Loading spinner
|
||||
- Disable khi đang submit
|
||||
|
||||
7. Nếu chưa đăng nhập → redirect /login.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 2: Booking API (Giao tiếp backend)
|
||||
|
||||
### Mục tiêu
|
||||
Kết nối và xử lý API liên quan đến đặt phòng.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
🔧 Endpoints:
|
||||
```
|
||||
POST /api/bookings → Tạo booking
|
||||
GET /api/bookings/me → Lấy danh sách booking của user
|
||||
PATCH /api/bookings/:id/cancel → Hủy booking
|
||||
GET /api/bookings/:id → Chi tiết booking
|
||||
GET /api/bookings/check/:bookingNumber → Tra cứu booking
|
||||
```
|
||||
🔄 Luồng xử lý:
|
||||
1. Frontend gọi POST /api/bookings
|
||||
2. Backend kiểm tra phòng trống:
|
||||
```
|
||||
GET /api/rooms/available?roomId=...&from=...&to=...
|
||||
```
|
||||
3. Nếu trống → tạo booking
|
||||
- Nếu trùng lịch → trả 409 “Phòng đã được đặt trong thời gian này”
|
||||
4. Gửi email xác nhận booking (nếu cần)
|
||||
5. Trả về dữ liệu booking để hiển thị /booking-success/:id.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 3: BookingSuccess – Trang kết quả sau đặt phòng
|
||||
|
||||
### Mục tiêu
|
||||
Hiển thị kết quả đặt phòng thành công và các hành động tiếp theo.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. Route: /booking-success/:id
|
||||
2. Gọi GET /api/bookings/:id → hiển thị chi tiết
|
||||
3. Nút:
|
||||
- “Xem đơn của tôi” → /my-bookings
|
||||
- “Về trang chủ” → /
|
||||
4. Nếu phương thức là Chuyển khoản:
|
||||
+ Hiển thị QR code ngân hàng
|
||||
+ Cho phép upload ảnh xác nhận
|
||||
+ Gọi POST /api/notify/payment khi người dùng xác nhận đã chuyển khoản.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 4: MyBookingsPage – Danh sách đơn đặt của người
|
||||
|
||||
### Mục tiêu
|
||||
Hiển thị toàn bộ các đơn đặt của user + cho phép hủy đơn.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. Route: /my-bookings
|
||||
2. API: GET /api/bookings/me
|
||||
3. Hiển thị danh sách booking:
|
||||
- Phòng, ngày nhận/trả, tổng tiền
|
||||
- Trạng thái:
|
||||
🟡 pending
|
||||
🟢 confirmed
|
||||
🔴 cancelled
|
||||
4. Nút “Hủy đặt phòng”:
|
||||
1. window.confirm("Bạn có chắc muốn hủy không?")
|
||||
2. Gọi PATCH /api/bookings/:id/cancel (hoặc DELETE /api/bookings/:id tùy implement)
|
||||
3. Logic hủy:
|
||||
- Giữ 20% giá trị đơn
|
||||
- Hoàn 80% còn lại cho user
|
||||
- Cập nhật trạng thái phòng về available
|
||||
4. Hiển thị toast “Đơn đã được hủy thành công”
|
||||
5. Cho phép xem chi tiết booking:
|
||||
- Route: /bookings/:id
|
||||
- Gọi GET /api/bookings/:id
|
||||
- Hiển thị chi tiết phòng, thông tin user, tổng tiền, status.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 5: Thanh toán (Giả lập Payment)
|
||||
|
||||
### Mục tiêu
|
||||
Cho phép người dùng chọn phương thức thanh toán và xác nhận thanh toán.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
- Phương thức:
|
||||
1. Thanh toán tại chỗ
|
||||
- Booking được tạo với status = "pending"
|
||||
2. Chuyển khoản
|
||||
- Hiển thị mã QR ngân hàng (tĩnh hoặc từ API)
|
||||
- Upload ảnh biên lai (image upload)
|
||||
- Sau khi upload → gọi POST /api/notify/payment gửi email xác nhận
|
||||
- Cập nhật status = "confirmed"
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 6: UX & Hiệu năng
|
||||
|
||||
### Mục tiêu
|
||||
Cải thiện trải nghiệm người dùng và tính trực quan.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. Toasts (react-hot-toast hoặc sonner)
|
||||
2. Loading spinner rõ ràng
|
||||
3. DateRangePicker cho chọn ngày
|
||||
4. Form được validate đầy đủ (và báo lỗi chi tiết)
|
||||
5. Focus input đầu tiên
|
||||
6. Tự động redirect khi đặt thành công / hủy đơn
|
||||
|
||||
---
|
||||
140
docs/tasks_5_Review_System.md
Normal file
140
docs/tasks_5_Review_System.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Review System
|
||||
|
||||
## Chức năng 1: ReviewPage – Trang người dùng đánh giá phòng
|
||||
|
||||
### Mục tiêu
|
||||
Cho phép người dùng viết đánh giá cho những phòng họ đã đặt thành công.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. Route: /reviews
|
||||
2. Gọi API:
|
||||
```
|
||||
GET /api/bookings/me → Lấy danh sách phòng người dùng đã đặt.
|
||||
POST /api/reviews → Gửi đánh giá.
|
||||
```
|
||||
3. Giao diện:
|
||||
- Hiển thị danh sách phòng đã đặt (tên, ngày ở, trạng thái)
|
||||
- Nút “Đánh giá” (hiện nếu chưa đánh giá phòng đó)
|
||||
4. Khi nhấn “Đánh giá” → mở Modal:
|
||||
- Input chọn số sao (⭐ 1–5)
|
||||
- Textarea nhập nội dung bình luận
|
||||
- Nút “Gửi đánh giá”
|
||||
5. Validate:
|
||||
- Rating bắt buộc (1–5)
|
||||
- Comment không để trống
|
||||
6. Sau khi gửi thành công → toast thông báo “Đánh giá của bạn đang chờ duyệt”.
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Người dùng chỉ thấy nút “Đánh giá” với phòng đã từng đặt.
|
||||
2. Modal mở ra và validate chính xác.
|
||||
3. Gửi thành công → review có trạng thái "pending".
|
||||
4. Toast hiển thị thông báo hợp lý.
|
||||
5. Giao diện gọn, trực quan, không lỗi khi chưa có phòng nào đặt.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 2: RoomDetailPage – Hiển thị danh sách đánh giá
|
||||
|
||||
### Mục tiêu
|
||||
Hiển thị danh sách các đánh giá đã được admin duyệt cho từng phòng.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. Route: /rooms/:id
|
||||
2. API:
|
||||
```
|
||||
GET /api/reviews?roomId={id}&status=approved
|
||||
```
|
||||
3. Hiển thị danh sách review:
|
||||
- Avatar + tên người dùng
|
||||
- Số sao (⭐)
|
||||
- Nội dung bình luận
|
||||
- Ngày đăng (createdAt)
|
||||
4. Tính và hiển thị điểm trung bình rating (VD: ⭐ 4.2 / 5)
|
||||
5. Nếu chưa có review → hiển thị: “Chưa có đánh giá nào.”
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Danh sách review hiển thị đúng theo phòng.
|
||||
2. Chỉ review có status = approved được render.
|
||||
3. Tính điểm trung bình chính xác (làm tròn 1 chữ số thập phân).
|
||||
4. Hiển thị avatar, tên, sao, và ngày đầy đủ.
|
||||
5. Có thông báo “Chưa có đánh giá” khi danh sách trống.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 3: AdminReviewPage – Trang quản trị đánh giá
|
||||
|
||||
### Mục tiêu
|
||||
Cho phép Admin xem, duyệt hoặc từ chối các đánh giá người dùng gửi lên.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. Route: /admin/reviews
|
||||
2. API:
|
||||
```
|
||||
GET /api/reviews
|
||||
PATCH /api/reviews/:id/approve
|
||||
PATCH /api/reviews/:id/reject
|
||||
```
|
||||
3. Hành động:
|
||||
✅ Duyệt → review chuyển sang approved
|
||||
❌ Từ chối → review chuyển sang rejected
|
||||
4. Sau khi duyệt → cập nhật giao diện và hiển thị toast thông báo.
|
||||
5. Có filter theo trạng thái (pending, approved, rejected).
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Admin thấy đầy đủ danh sách review.
|
||||
2. Duyệt hoặc từ chối hoạt động đúng API.
|
||||
3. Bảng tự cập nhật khi thay đổi trạng thái.
|
||||
4. Toast hiển thị rõ “Đã duyệt” hoặc “Đã từ chối”.
|
||||
5. Chỉ review approved mới hiển thị công khai cho người dùng.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 4: Bảo mật & Logic hiển thị
|
||||
|
||||
### Mục tiêu
|
||||
Đảm bảo chỉ người hợp lệ mới có thể gửi đánh giá và hệ thống hiển thị đúng dữ liệu.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. Kiểm tra quyền:
|
||||
- Người dùng chưa đăng nhập → redirect /login
|
||||
- Người dùng chưa từng đặt phòng → không hiển thị nút “Đánh giá”
|
||||
2. Kiểm tra logic:
|
||||
- Mỗi người chỉ được đánh giá 1 lần / phòng
|
||||
- Review mặc định status = pending
|
||||
3. Phân quyền:
|
||||
- User: chỉ gửi review
|
||||
- Admin: duyệt / từ chối
|
||||
- Staff: chỉ xem
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. Người chưa đăng nhập không thể gửi review.
|
||||
2. Mỗi phòng chỉ được review 1 lần bởi 1 user.
|
||||
3. Dữ liệu hiển thị chính xác theo phân quyền.
|
||||
4. Review chỉ xuất hiện công khai khi được duyệt.
|
||||
5. Không có lỗi logic hoặc hiển thị sai trạng thái.
|
||||
|
||||
---
|
||||
|
||||
## Chức năng 5: UX & Hiển thị tổng quan
|
||||
|
||||
### Mục tiêu
|
||||
Cải thiện trải nghiệm người dùng và giao diện hiển thị đánh giá.
|
||||
|
||||
#### Nhiệm vụ chi tiết
|
||||
1. Dùng component đánh giá sao trực quan (ví dụ react-rating-stars-component).
|
||||
2. Format ngày tạo bằng:
|
||||
```
|
||||
new Date(createdAt).toLocaleDateString('vi-VN')
|
||||
```
|
||||
3. Thêm hiệu ứng hover nhẹ khi hiển thị danh sách review.
|
||||
4. Dùng toast (react-hot-toast) cho thông báo gửi / duyệt / từ chối.
|
||||
5. Loading spinner khi chờ API.
|
||||
|
||||
### Kết quả mong đợi
|
||||
1. UI mượt mà, dễ đọc và thân thiện.
|
||||
2. Loading / toast hiển thị đúng trạng thái.
|
||||
3. Ngày tháng, sao và bình luận được format đẹp.
|
||||
4. Giao diện quản trị và người dùng thống nhất phong cách.
|
||||
5. Trải nghiệm người dùng mượt, không giật lag.
|
||||
|
||||
---
|
||||
2
docs/vnpay-testing-guide.md
Normal file
2
docs/vnpay-testing-guide.md
Normal file
@@ -0,0 +1,2 @@
|
||||
VNPay testing guide removed per repository cleanup.
|
||||
This document was deleted because the project no longer integrates with VNPay.
|
||||
Reference in New Issue
Block a user