feat: reorganize admin settings with tabbed interface and documentation
- Reorganized admin settings into tabbed interface (General, Security, Payment Methods) - Vertical tabs on desktop, horizontal scrollable on mobile - Moved Payment Methods from separate menu to Settings tab - Fixed admin profile reuse and dashboard blocking - Fixed maintenance mode guard to use AppConfig model - Added admin auto-redirect after login (admins → /admin, users → /) - Reorganized documentation into docs/ folder structure - Created comprehensive README and documentation index - Added PWA and Web Push notifications to to-do list
This commit is contained in:
169
ADMIN_TRANSLATION_STATUS.md
Normal file
169
ADMIN_TRANSLATION_STATUS.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# Admin Dashboard Translation Status
|
||||||
|
|
||||||
|
## ✅ Completed Pages
|
||||||
|
|
||||||
|
### 1. AdminDashboard.tsx
|
||||||
|
**Status:** ✅ Fully Translated to English
|
||||||
|
|
||||||
|
**Changes Made:**
|
||||||
|
- "Memuat..." → "Loading..."
|
||||||
|
- "Langganan aktif saat ini" → "Currently active subscriptions"
|
||||||
|
- "Pendapatan 6 bulan terakhir" → "Revenue for the last 6 months"
|
||||||
|
- "Kelola Plans" → "Manage Plans"
|
||||||
|
- "Verifikasi Pembayaran" → "Verify Payments"
|
||||||
|
- "Kelola Users" → "Manage Users"
|
||||||
|
- "Metode Pembayaran" → "Payment Methods"
|
||||||
|
- "Akses cepat ke fitur utama" → "Quick access to main features"
|
||||||
|
- "Status sistem dan statistik" → "System status and statistics"
|
||||||
|
|
||||||
|
### 2. AdminPlans.tsx
|
||||||
|
**Status:** ✅ Fully Translated to English
|
||||||
|
|
||||||
|
**Changes Made:**
|
||||||
|
- "Memuat..." → "Loading..."
|
||||||
|
- "Kelola Plans" → "Manage Plans"
|
||||||
|
- "Kelola paket berlangganan" → "Manage subscription plans"
|
||||||
|
- "Tambah Plan" → "Add Plan"
|
||||||
|
- "Edit Plan" / "Tambah Plan Baru" → "Edit Plan" / "Add New Plan"
|
||||||
|
- "Ubah informasi plan berlangganan" → "Update subscription plan information"
|
||||||
|
- "Buat plan berlangganan baru" → "Create a new subscription plan"
|
||||||
|
- "Nama Plan" → "Plan Name"
|
||||||
|
- "Deskripsi" → "Description"
|
||||||
|
- "Harga" → "Price"
|
||||||
|
- "Tipe Durasi" → "Duration Type"
|
||||||
|
- "Batal" → "Cancel"
|
||||||
|
- "Tambah" / "Update" → "Add" / "Update"
|
||||||
|
- "Hapus Plan?" → "Delete Plan?"
|
||||||
|
- "Apakah Anda yakin..." → "Are you sure..."
|
||||||
|
- "Tampil di halaman pricing" → "Show on pricing page"
|
||||||
|
- All toast messages translated
|
||||||
|
- All button titles translated
|
||||||
|
|
||||||
|
### 3. AdminUsers.tsx
|
||||||
|
**Status:** ✅ Fully Translated to English (Done Previously)
|
||||||
|
|
||||||
|
**Changes Made:**
|
||||||
|
- "Memuat..." → "Loading..."
|
||||||
|
- "Kelola Users" → "Manage Users"
|
||||||
|
- "Kelola akun dan izin pengguna" → "Manage user accounts and permissions"
|
||||||
|
- "Tidak ada user" → "No users found"
|
||||||
|
- All toast messages translated
|
||||||
|
- All dialog text translated
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ All Pages Translated
|
||||||
|
|
||||||
|
### 4. AdminPaymentMethods.tsx
|
||||||
|
**Status:** ✅ Fully Translated to English
|
||||||
|
|
||||||
|
**Changes Made:**
|
||||||
|
- "Metode Pembayaran" → "Payment Methods"
|
||||||
|
- "Kelola metode pembayaran yang tersedia" → "Manage available payment methods"
|
||||||
|
- "Tambah Metode" → "Add Method"
|
||||||
|
- "Edit Metode Pembayaran" / "Tambah Metode Pembayaran" → "Edit Payment Method" / "Add Payment Method"
|
||||||
|
- "Nama Tampilan" → "Display Name"
|
||||||
|
- "Nama Pemilik" → "Account Holder Name"
|
||||||
|
- "Metode pembayaran dapat digunakan" → "Payment method can be used"
|
||||||
|
- "Belum ada metode pembayaran" → "No payment methods yet"
|
||||||
|
- All toast messages translated
|
||||||
|
- All button text translated
|
||||||
|
|
||||||
|
### 5. AdminPayments.tsx
|
||||||
|
**Status:** ✅ Fully Translated to English
|
||||||
|
|
||||||
|
**Changes Made:**
|
||||||
|
- "Verifikasi Pembayaran" → "Payment Verification"
|
||||||
|
- "Kelola dan verifikasi bukti pembayaran dari pengguna" → "Manage and verify payment proofs from users"
|
||||||
|
- "Semua Status" → "All Status"
|
||||||
|
- "Jumlah" → "Amount"
|
||||||
|
- "Metode" → "Method"
|
||||||
|
- "Tanggal" → "Date"
|
||||||
|
- "Lihat Bukti" → "View Proof"
|
||||||
|
- "Verifikasi" → "Verify"
|
||||||
|
- "Tolak" → "Reject"
|
||||||
|
- "Bukti Pembayaran" → "Payment Proof"
|
||||||
|
- "Tidak ada bukti pembayaran" → "No payment proof"
|
||||||
|
- "Catatan:" → "Notes:"
|
||||||
|
- "Catatan verifikasi (opsional):" → "Verification notes (optional):"
|
||||||
|
- "Alasan penolakan:" → "Rejection reason:"
|
||||||
|
- All toast messages translated
|
||||||
|
|
||||||
|
### 6. AdminSettings.tsx
|
||||||
|
**Status:** ✅ Fully Translated to English
|
||||||
|
|
||||||
|
**Changes Made:**
|
||||||
|
- "Pengaturan Aplikasi" → "Application Settings"
|
||||||
|
- "Kelola konfigurasi dan pengaturan sistem" → "Manage system configuration and settings"
|
||||||
|
- "Pengaturan Umum" → "General Settings"
|
||||||
|
- "Informasi dasar aplikasi" → "Basic application information"
|
||||||
|
- "Nama Aplikasi" → "Application Name"
|
||||||
|
- "Fitur & Keamanan" → "Features & Security"
|
||||||
|
- "Aktifkan atau nonaktifkan fitur" → "Enable or disable features"
|
||||||
|
- "Registrasi Pengguna Baru" → "New User Registration"
|
||||||
|
- "Izinkan pengguna baru mendaftar" → "Allow new users to register"
|
||||||
|
- "Verifikasi Email" → "Email Verification"
|
||||||
|
- "Wajibkan verifikasi email untuk pengguna baru" → "Require email verification for new users"
|
||||||
|
- "Verifikasi Pembayaran Manual" → "Manual Payment Verification"
|
||||||
|
- "Aktifkan verifikasi manual untuk pembayaran" → "Enable manual verification for payments"
|
||||||
|
- "Mode Pemeliharaan" → "Maintenance Mode"
|
||||||
|
- "Nonaktifkan akses sementara untuk maintenance" → "Temporarily disable access for maintenance"
|
||||||
|
- "Aktifkan untuk menutup akses sementara" → "Enable to temporarily close access"
|
||||||
|
- "Pesan Pemeliharaan" → "Maintenance Message"
|
||||||
|
- "Menyimpan..." / "Simpan Pengaturan" → "Saving..." / "Save Settings"
|
||||||
|
- "Sistem sedang dalam pemeliharaan..." → "System is under maintenance..."
|
||||||
|
- All toast messages translated
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Translation Progress
|
||||||
|
|
||||||
|
| Page | Status | Progress |
|
||||||
|
|------|--------|----------|
|
||||||
|
| AdminDashboard | ✅ Complete | 100% |
|
||||||
|
| AdminPlans | ✅ Complete | 100% |
|
||||||
|
| AdminUsers | ✅ Complete | 100% |
|
||||||
|
| AdminPaymentMethods | ✅ Complete | 100% |
|
||||||
|
| AdminPayments | ✅ Complete | 100% |
|
||||||
|
| AdminSettings | ✅ Complete | 100% |
|
||||||
|
|
||||||
|
**Overall Progress: 100% (6/6 pages)** ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. ✅ Create TODO list for backend implementations
|
||||||
|
2. ✅ Translate AdminDashboard.tsx
|
||||||
|
3. ✅ Translate AdminPlans.tsx
|
||||||
|
4. ✅ Translate AdminPaymentMethods.tsx
|
||||||
|
5. ✅ Translate AdminPayments.tsx
|
||||||
|
6. ✅ Translate AdminSettings.tsx
|
||||||
|
7. ⚠️ Implement backend logic for:
|
||||||
|
- Suspended user blocking
|
||||||
|
- Email verification enforcement
|
||||||
|
- Maintenance mode
|
||||||
|
- Registration toggle
|
||||||
|
- Pro features & limits
|
||||||
|
- Manual payment verification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. `/apps/web/src/components/admin/pages/AdminDashboard.tsx` ✅
|
||||||
|
2. `/apps/web/src/components/admin/pages/AdminPlans.tsx` ✅
|
||||||
|
3. `/apps/web/src/components/admin/pages/AdminUsers.tsx` ✅
|
||||||
|
4. `/apps/web/src/components/admin/pages/AdminPaymentMethods.tsx` ✅
|
||||||
|
5. `/apps/web/src/components/admin/pages/AdminPayments.tsx` ✅
|
||||||
|
6. `/apps/web/src/components/admin/pages/AdminSettings.tsx` ✅
|
||||||
|
7. `/apps/api/src/admin/admin-users.controller.ts` (added CRUD endpoints)
|
||||||
|
8. `/apps/api/src/admin/admin-users.service.ts` (added CRUD methods)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Translation Complete!
|
||||||
|
|
||||||
|
**All 6 admin pages have been fully translated to English!**
|
||||||
|
|
||||||
|
Total time spent: ~2 hours
|
||||||
294
MAINTENANCE_MODE_IMPLEMENTATION.md
Normal file
294
MAINTENANCE_MODE_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
# Maintenance Mode Implementation
|
||||||
|
|
||||||
|
## ✅ **COMPLETE: Maintenance Mode Feature Fully Implemented**
|
||||||
|
|
||||||
|
### **Overview**
|
||||||
|
Implemented a complete maintenance mode system that allows admins to temporarily disable user access to the application while keeping admin access available. The system is controlled via a toggle in the Admin Settings page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🎯 Features Implemented**
|
||||||
|
|
||||||
|
### **1. Backend (API)**
|
||||||
|
|
||||||
|
#### **MaintenanceGuard** (`/apps/api/src/common/guards/maintenance.guard.ts`)
|
||||||
|
- Global guard that checks maintenance mode status from database
|
||||||
|
- Automatically blocks all requests when maintenance mode is active
|
||||||
|
- **Exceptions:**
|
||||||
|
- Admin users can still access the system
|
||||||
|
- Routes decorated with `@SkipMaintenance()` are exempt
|
||||||
|
- Returns 503 Service Unavailable with custom maintenance message
|
||||||
|
|
||||||
|
#### **SkipMaintenance Decorator** (`/apps/api/src/common/decorators/skip-maintenance.decorator.ts`)
|
||||||
|
- Decorator to mark routes that should bypass maintenance mode
|
||||||
|
- Applied to:
|
||||||
|
- Health check endpoints
|
||||||
|
- Auth endpoints (login, register, OTP, Google OAuth)
|
||||||
|
- All admin endpoints
|
||||||
|
|
||||||
|
#### **Global Guard Registration** (`/apps/api/src/app.module.ts`)
|
||||||
|
- MaintenanceGuard registered as APP_GUARD
|
||||||
|
- Runs on every request automatically
|
||||||
|
|
||||||
|
#### **Protected Routes**
|
||||||
|
Routes that **bypass** maintenance mode:
|
||||||
|
- `/health` - Health check
|
||||||
|
- `/health/db` - Database health check
|
||||||
|
- `/auth/login` - User login
|
||||||
|
- `/auth/register` - User registration
|
||||||
|
- `/auth/verify-otp` - OTP verification
|
||||||
|
- `/auth/google` - Google OAuth
|
||||||
|
- `/auth/google/callback` - Google OAuth callback
|
||||||
|
- `/admin/*` - All admin routes (for admin users only)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2. Frontend (Web)**
|
||||||
|
|
||||||
|
#### **MaintenancePage Component** (`/apps/web/src/components/pages/MaintenancePage.tsx`)
|
||||||
|
- Beautiful, user-friendly maintenance page
|
||||||
|
- Displays custom maintenance message from admin
|
||||||
|
- Includes refresh button to check if maintenance is over
|
||||||
|
- Responsive design with proper styling
|
||||||
|
|
||||||
|
#### **Axios Interceptor** (`/apps/web/src/utils/axiosSetup.ts`)
|
||||||
|
- Intercepts all API responses
|
||||||
|
- Detects 503 status with `maintenanceMode: true`
|
||||||
|
- Triggers maintenance page display
|
||||||
|
- Extracts custom message from response
|
||||||
|
|
||||||
|
#### **App-Level Integration** (`/apps/web/src/App.tsx`)
|
||||||
|
- Maintenance mode state management
|
||||||
|
- Axios interceptor setup on app initialization
|
||||||
|
- Conditional rendering of maintenance page
|
||||||
|
- Preserves theme settings during maintenance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **📝 How It Works**
|
||||||
|
|
||||||
|
### **Activation Flow:**
|
||||||
|
1. Admin navigates to **Admin Settings** page
|
||||||
|
2. Admin toggles **"Maintenance Mode"** switch to ON
|
||||||
|
3. Admin can customize the maintenance message
|
||||||
|
4. Admin clicks **"Save Settings"**
|
||||||
|
5. System updates `maintenance_mode` config in database to `true`
|
||||||
|
|
||||||
|
### **User Experience:**
|
||||||
|
1. User makes any API request (e.g., loading dashboard, creating transaction)
|
||||||
|
2. **MaintenanceGuard** intercepts the request
|
||||||
|
3. Guard checks if user is admin:
|
||||||
|
- **If admin:** Request proceeds normally
|
||||||
|
- **If regular user:** Returns 503 error with maintenance message
|
||||||
|
4. Frontend axios interceptor catches 503 error
|
||||||
|
5. **MaintenancePage** is displayed to user
|
||||||
|
6. User can click "Refresh" to check if maintenance is over
|
||||||
|
|
||||||
|
### **Deactivation Flow:**
|
||||||
|
1. Admin (still has access) navigates to **Admin Settings**
|
||||||
|
2. Admin toggles **"Maintenance Mode"** switch to OFF
|
||||||
|
3. Admin clicks **"Save Settings"**
|
||||||
|
4. System updates `maintenance_mode` config to `false`
|
||||||
|
5. Regular users can now access the application again
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🔧 Configuration**
|
||||||
|
|
||||||
|
### **Database Config Keys:**
|
||||||
|
- **`maintenance_mode`** (system category)
|
||||||
|
- Type: `boolean` (stored as string "true"/"false")
|
||||||
|
- Default: `false`
|
||||||
|
- Controls whether maintenance mode is active
|
||||||
|
|
||||||
|
- **`maintenance_message`** (system category)
|
||||||
|
- Type: `text`
|
||||||
|
- Default: `"System is under maintenance. Please try again later."`
|
||||||
|
- Custom message displayed to users
|
||||||
|
|
||||||
|
### **Admin Settings UI:**
|
||||||
|
Located in: `/admin/settings`
|
||||||
|
|
||||||
|
**Maintenance Mode Section:**
|
||||||
|
- Toggle switch to enable/disable
|
||||||
|
- Text area to customize message
|
||||||
|
- Visual warning (red border) when enabled
|
||||||
|
- Save button to apply changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **📁 Files Created/Modified**
|
||||||
|
|
||||||
|
### **Created Files:**
|
||||||
|
1. `/apps/api/src/common/guards/maintenance.guard.ts` - Maintenance guard
|
||||||
|
2. `/apps/api/src/common/decorators/skip-maintenance.decorator.ts` - Skip decorator
|
||||||
|
3. `/apps/web/src/components/pages/MaintenancePage.tsx` - Maintenance UI
|
||||||
|
4. `/apps/web/src/utils/axiosSetup.ts` - Axios interceptor
|
||||||
|
|
||||||
|
### **Modified Files:**
|
||||||
|
1. `/apps/api/src/app.module.ts` - Registered global guard
|
||||||
|
2. `/apps/api/src/health/health.controller.ts` - Added @SkipMaintenance
|
||||||
|
3. `/apps/api/src/auth/auth.controller.ts` - Added @SkipMaintenance to auth routes
|
||||||
|
4. `/apps/api/src/admin/admin-config.controller.ts` - Added @SkipMaintenance
|
||||||
|
5. `/apps/api/src/admin/admin-users.controller.ts` - Added @SkipMaintenance
|
||||||
|
6. `/apps/api/src/admin/admin-plans.controller.ts` - Added @SkipMaintenance
|
||||||
|
7. `/apps/api/src/admin/admin-payments.controller.ts` - Added @SkipMaintenance
|
||||||
|
8. `/apps/api/src/admin/admin-payment-methods.controller.ts` - Added @SkipMaintenance
|
||||||
|
9. `/apps/web/src/App.tsx` - Added maintenance mode handling
|
||||||
|
10. `/apps/web/src/components/admin/pages/AdminSettings.tsx` - Already has toggle UI
|
||||||
|
11. `/apps/web/src/components/admin/pages/AdminPaymentMethods.tsx` - Fixed Indonesian text
|
||||||
|
12. `/apps/web/src/components/admin/pages/AdminSettings.tsx` - Fixed Indonesian text
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🧪 Testing Instructions**
|
||||||
|
|
||||||
|
### **Test Maintenance Mode Activation:**
|
||||||
|
1. Login as admin
|
||||||
|
2. Navigate to `/admin/settings`
|
||||||
|
3. Enable "Maintenance Mode" toggle
|
||||||
|
4. Optionally change the message
|
||||||
|
5. Click "Save Settings"
|
||||||
|
6. Open a new incognito window
|
||||||
|
7. Try to login as regular user
|
||||||
|
8. After login, you should see the maintenance page
|
||||||
|
9. Try to access any route - should show maintenance page
|
||||||
|
|
||||||
|
### **Test Admin Access During Maintenance:**
|
||||||
|
1. With maintenance mode ON
|
||||||
|
2. Login as admin in a new window
|
||||||
|
3. Verify admin can access all admin routes
|
||||||
|
4. Verify admin dashboard works normally
|
||||||
|
|
||||||
|
### **Test Maintenance Mode Deactivation:**
|
||||||
|
1. As admin, go to `/admin/settings`
|
||||||
|
2. Disable "Maintenance Mode" toggle
|
||||||
|
3. Click "Save Settings"
|
||||||
|
4. In the user's incognito window, click "Refresh"
|
||||||
|
5. User should now be able to access the application
|
||||||
|
|
||||||
|
### **Test Custom Message:**
|
||||||
|
1. Enable maintenance mode
|
||||||
|
2. Set custom message: "We're upgrading our servers. Back in 30 minutes!"
|
||||||
|
3. Save settings
|
||||||
|
4. Verify users see the custom message on maintenance page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🎨 UI/UX Features**
|
||||||
|
|
||||||
|
### **Maintenance Page Design:**
|
||||||
|
- ⚠️ Yellow warning icon
|
||||||
|
- Clear "Under Maintenance" heading
|
||||||
|
- Custom message display
|
||||||
|
- Additional context text
|
||||||
|
- Refresh button with icon
|
||||||
|
- Responsive layout
|
||||||
|
- Dark mode support
|
||||||
|
- Professional appearance
|
||||||
|
|
||||||
|
### **Admin Settings UI:**
|
||||||
|
- Clear toggle switch
|
||||||
|
- Descriptive labels
|
||||||
|
- Red warning styling when enabled
|
||||||
|
- Text area for custom message
|
||||||
|
- Save button with loading state
|
||||||
|
- Success/error toast notifications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🔒 Security Considerations**
|
||||||
|
|
||||||
|
✅ **Admin-only access during maintenance**
|
||||||
|
- Only users with `role: 'admin'` can bypass maintenance mode
|
||||||
|
- Regular users are completely blocked
|
||||||
|
|
||||||
|
✅ **Auth routes remain accessible**
|
||||||
|
- Users can still login (but will see maintenance page after)
|
||||||
|
- Prevents lockout scenarios
|
||||||
|
|
||||||
|
✅ **Health checks remain accessible**
|
||||||
|
- Monitoring systems can still check app health
|
||||||
|
- Database connectivity can be verified
|
||||||
|
|
||||||
|
✅ **No data loss**
|
||||||
|
- Maintenance mode is non-destructive
|
||||||
|
- All user data remains intact
|
||||||
|
- Simply blocks access temporarily
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **📊 Status Codes**
|
||||||
|
|
||||||
|
- **503 Service Unavailable** - Returned when maintenance mode is active
|
||||||
|
- Response includes:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"statusCode": 503,
|
||||||
|
"message": "System is under maintenance. Please try again later.",
|
||||||
|
"maintenanceMode": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🚀 Future Enhancements**
|
||||||
|
|
||||||
|
Potential improvements:
|
||||||
|
1. **Scheduled Maintenance**
|
||||||
|
- Set start/end times for automatic activation/deactivation
|
||||||
|
- Countdown timer on maintenance page
|
||||||
|
|
||||||
|
2. **Partial Maintenance**
|
||||||
|
- Block specific features instead of entire app
|
||||||
|
- Granular control per module
|
||||||
|
|
||||||
|
3. **Notification System**
|
||||||
|
- Email users before maintenance
|
||||||
|
- In-app notifications about upcoming maintenance
|
||||||
|
|
||||||
|
4. **Maintenance History**
|
||||||
|
- Log all maintenance periods
|
||||||
|
- Track admin who activated/deactivated
|
||||||
|
|
||||||
|
5. **Status Page**
|
||||||
|
- Public status page showing system health
|
||||||
|
- Historical uptime data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **✅ Completion Checklist**
|
||||||
|
|
||||||
|
- [x] Backend maintenance guard implemented
|
||||||
|
- [x] Skip maintenance decorator created
|
||||||
|
- [x] Global guard registered
|
||||||
|
- [x] Auth routes exempted
|
||||||
|
- [x] Admin routes exempted
|
||||||
|
- [x] Health check routes exempted
|
||||||
|
- [x] Frontend maintenance page created
|
||||||
|
- [x] Axios interceptor implemented
|
||||||
|
- [x] App-level integration completed
|
||||||
|
- [x] Admin settings toggle working
|
||||||
|
- [x] Custom message support added
|
||||||
|
- [x] All Indonesian text translated
|
||||||
|
- [x] Documentation created
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🎉 Summary**
|
||||||
|
|
||||||
|
**Maintenance mode is now fully functional!**
|
||||||
|
|
||||||
|
Admins can:
|
||||||
|
- ✅ Toggle maintenance mode ON/OFF from Admin Settings
|
||||||
|
- ✅ Customize the maintenance message
|
||||||
|
- ✅ Continue using the admin panel during maintenance
|
||||||
|
- ✅ Monitor and manage the system
|
||||||
|
|
||||||
|
Users will:
|
||||||
|
- ✅ See a professional maintenance page when mode is active
|
||||||
|
- ✅ Receive custom messages from admins
|
||||||
|
- ✅ Be able to refresh to check if maintenance is over
|
||||||
|
- ✅ Have a smooth experience when maintenance ends
|
||||||
|
|
||||||
|
The system is production-ready and can be used for scheduled maintenance, emergency fixes, or system upgrades!
|
||||||
148
README.md
Normal file
148
README.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# Tabungin
|
||||||
|
|
||||||
|
A modern financial tracking application built with React and NestJS.
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
Comprehensive documentation is available in the [`docs/`](./docs) folder:
|
||||||
|
|
||||||
|
- **[Documentation Index](./docs/README.md)** - Start here for all documentation
|
||||||
|
- **[Features](./docs/features/)** - Detailed feature documentation
|
||||||
|
- **[Guides](./docs/guides/)** - How-to guides and tutorials
|
||||||
|
- **[Planning](./docs/planning/)** - Roadmap and to-do list
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js 18+
|
||||||
|
- PostgreSQL 14+
|
||||||
|
- npm or yarn
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd Tabungin
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Setup environment**
|
||||||
|
```bash
|
||||||
|
# Copy environment files
|
||||||
|
cp apps/api/.env.example apps/api/.env
|
||||||
|
cp apps/web/.env.example apps/web/.env
|
||||||
|
|
||||||
|
# Edit .env files with your configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Setup database**
|
||||||
|
```bash
|
||||||
|
cd apps/api
|
||||||
|
npx prisma migrate dev
|
||||||
|
npx prisma db seed
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Run development servers**
|
||||||
|
```bash
|
||||||
|
# Terminal 1: API
|
||||||
|
cd apps/api
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Terminal 2: Web
|
||||||
|
cd apps/web
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Access the application**
|
||||||
|
- Web: http://localhost:5174
|
||||||
|
- API: http://localhost:3001
|
||||||
|
|
||||||
|
## 🏗️ Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Tabungin/
|
||||||
|
├── apps/
|
||||||
|
│ ├── api/ # NestJS backend
|
||||||
|
│ │ ├── src/
|
||||||
|
│ │ │ ├── auth/ # Authentication
|
||||||
|
│ │ │ ├── users/ # User management
|
||||||
|
│ │ │ ├── wallets/ # Wallet management
|
||||||
|
│ │ │ ├── transactions/
|
||||||
|
│ │ │ └── admin/ # Admin features
|
||||||
|
│ │ └── prisma/ # Database schema
|
||||||
|
│ └── web/ # React frontend
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/
|
||||||
|
│ │ ├── contexts/
|
||||||
|
│ │ ├── lib/
|
||||||
|
│ │ └── utils/
|
||||||
|
│ └── public/
|
||||||
|
├── docs/ # Documentation
|
||||||
|
│ ├── features/ # Feature docs
|
||||||
|
│ ├── guides/ # How-to guides
|
||||||
|
│ └── planning/ # Roadmap & to-do
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 💰 **Wallet Management** - Multiple wallets with different currencies
|
||||||
|
- 📊 **Transaction Tracking** - Income and expense tracking
|
||||||
|
- 👤 **User Profiles** - Customizable user profiles with avatars
|
||||||
|
- 🔐 **Multi-Factor Authentication** - Email, WhatsApp, and TOTP
|
||||||
|
- 👥 **Admin Panel** - Comprehensive admin dashboard
|
||||||
|
- 💳 **Subscription Plans** - Flexible subscription management
|
||||||
|
- 💵 **Payment Processing** - Multiple payment methods
|
||||||
|
- 🛡️ **Security** - JWT authentication, role-based access
|
||||||
|
- 🌙 **Dark Mode** - Light and dark theme support
|
||||||
|
- 🌍 **Multi-language** - English and Indonesian
|
||||||
|
|
||||||
|
## 🔧 Tech Stack
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- React 18
|
||||||
|
- TypeScript
|
||||||
|
- Vite
|
||||||
|
- TailwindCSS
|
||||||
|
- shadcn/ui
|
||||||
|
- React Router
|
||||||
|
- Axios
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- NestJS
|
||||||
|
- TypeScript
|
||||||
|
- Prisma ORM
|
||||||
|
- PostgreSQL
|
||||||
|
- JWT Authentication
|
||||||
|
- Passport.js
|
||||||
|
|
||||||
|
## 📖 Documentation
|
||||||
|
|
||||||
|
- [Features Documentation](./docs/features/) - Detailed feature implementations
|
||||||
|
- [Testing Guide](./docs/guides/testing-guide.md) - How to test the application
|
||||||
|
- [To-Do List](./docs/planning/todo.md) - Upcoming features and tasks
|
||||||
|
- [Technical Q&A](./docs/planning/tech-qa.md) - Technical decisions and answers
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## 📝 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License.
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
For questions or issues:
|
||||||
|
- Check the [Documentation](./docs/README.md)
|
||||||
|
- Review [Technical Q&A](./docs/planning/tech-qa.md)
|
||||||
|
- Open an issue on GitHub
|
||||||
482
TODO_ADMIN_FEATURES.md
Normal file
482
TODO_ADMIN_FEATURES.md
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
# Admin Features TODO List
|
||||||
|
|
||||||
|
## 🔴 HIGH PRIORITY - Backend Implementation Required
|
||||||
|
|
||||||
|
### 1. Suspended User Implementation
|
||||||
|
**Status:** ⚠️ UI Only - Backend Logic Missing
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- ✅ UI: Suspend/Unsuspend buttons in Admin Users page
|
||||||
|
- ✅ API: Endpoints exist (`/api/admin/users/:id/suspend`, `/api/admin/users/:id/unsuspend`)
|
||||||
|
- ❌ **Missing:** Middleware to block suspended users from accessing the app
|
||||||
|
|
||||||
|
**Required Implementation:**
|
||||||
|
```typescript
|
||||||
|
// apps/api/src/auth/auth.guard.ts
|
||||||
|
// Add check for suspendedAt field
|
||||||
|
if (user.suspendedAt) {
|
||||||
|
throw new UnauthorizedException('Your account has been suspended. Reason: ' + user.suspendedReason);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Add suspended user check in AuthGuard
|
||||||
|
- [ ] Return proper error message with suspension reason
|
||||||
|
- [ ] Test: Suspended user cannot login
|
||||||
|
- [ ] Test: Suspended user's active sessions are invalidated
|
||||||
|
- [ ] Frontend: Show suspension message on login attempt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Pro Features Implementation
|
||||||
|
**Status:** ⚠️ UI Only - No Pro Features Defined
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- ✅ UI: Grant Pro Access dialog in Admin Users page
|
||||||
|
- ✅ API: Endpoint exists (`/api/admin/users/:id/grant-pro`)
|
||||||
|
- ✅ Database: Subscription model exists
|
||||||
|
- ❌ **Missing:** Actual pro features and restrictions
|
||||||
|
|
||||||
|
**Required Implementation:**
|
||||||
|
|
||||||
|
**A. Define Pro Features:**
|
||||||
|
```typescript
|
||||||
|
// Pro Features List:
|
||||||
|
1. Unlimited wallets (Free: max 5)
|
||||||
|
2. Unlimited transactions (Free: max 100/month)
|
||||||
|
3. Advanced analytics & reports
|
||||||
|
4. Export data (CSV, PDF)
|
||||||
|
5. Multiple currencies
|
||||||
|
6. Recurring transactions
|
||||||
|
7. Budget planning
|
||||||
|
8. Custom categories (Free: limited to default)
|
||||||
|
9. API access
|
||||||
|
10. Priority support
|
||||||
|
```
|
||||||
|
|
||||||
|
**B. Create Pro Guard:**
|
||||||
|
```typescript
|
||||||
|
// apps/api/src/subscription/guards/pro.guard.ts
|
||||||
|
@Injectable()
|
||||||
|
export class ProGuard implements CanActivate {
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const user = request.user;
|
||||||
|
|
||||||
|
// Check if user has active pro subscription
|
||||||
|
const subscription = await this.prisma.subscription.findUnique({
|
||||||
|
where: { userId: user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!subscription || subscription.status !== 'active') {
|
||||||
|
throw new ForbiddenException('This feature requires Pro subscription');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**C. Apply Restrictions:**
|
||||||
|
```typescript
|
||||||
|
// Example: Limit wallets for free users
|
||||||
|
// apps/api/src/wallets/wallets.service.ts
|
||||||
|
async create(userId: string, data: CreateWalletDto) {
|
||||||
|
const subscription = await this.getSubscription(userId);
|
||||||
|
|
||||||
|
if (!subscription || subscription.status !== 'active') {
|
||||||
|
// Free user - check limits
|
||||||
|
const walletCount = await this.prisma.wallet.count({
|
||||||
|
where: { userId, deletedAt: null }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (walletCount >= 5) {
|
||||||
|
throw new ForbiddenException('Free users can only create up to 5 wallets. Upgrade to Pro for unlimited wallets.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create wallet...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Define all Pro features and free limits
|
||||||
|
- [ ] Create ProGuard decorator
|
||||||
|
- [ ] Apply wallet limit (5 for free users)
|
||||||
|
- [ ] Apply transaction limit (100/month for free users)
|
||||||
|
- [ ] Add Pro badge in frontend for Pro users
|
||||||
|
- [ ] Show upgrade prompt when hitting limits
|
||||||
|
- [ ] Create pricing page
|
||||||
|
- [ ] Add "Upgrade to Pro" button in app
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Maintenance Mode Implementation
|
||||||
|
**Status:** ⚠️ UI Only - Backend Logic Missing
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- ✅ UI: Toggle in Admin Settings page
|
||||||
|
- ✅ Database: `Config` table with `maintenanceMode` key
|
||||||
|
- ❌ **Missing:** Middleware to block users during maintenance
|
||||||
|
|
||||||
|
**Required Implementation:**
|
||||||
|
|
||||||
|
**A. Create Maintenance Middleware:**
|
||||||
|
```typescript
|
||||||
|
// apps/api/src/common/middleware/maintenance.middleware.ts
|
||||||
|
@Injectable()
|
||||||
|
export class MaintenanceMiddleware implements NestMiddleware {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async use(req: Request, res: Response, next: NextFunction) {
|
||||||
|
// Skip for admin users
|
||||||
|
if (req.user?.role === 'admin') {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check maintenance mode
|
||||||
|
const config = await this.prisma.config.findUnique({
|
||||||
|
where: { key: 'maintenanceMode' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config?.value === 'true') {
|
||||||
|
throw new ServiceUnavailableException({
|
||||||
|
message: 'System is under maintenance. Please try again later.',
|
||||||
|
maintenanceMode: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**B. Apply Globally:**
|
||||||
|
```typescript
|
||||||
|
// apps/api/src/main.ts
|
||||||
|
app.use(new MaintenanceMiddleware(prisma));
|
||||||
|
```
|
||||||
|
|
||||||
|
**C. Frontend Handling:**
|
||||||
|
```typescript
|
||||||
|
// apps/web/src/App.tsx
|
||||||
|
// Show maintenance page when API returns 503
|
||||||
|
if (error.response?.status === 503 && error.response?.data?.maintenanceMode) {
|
||||||
|
return <MaintenancePage />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Create MaintenanceMiddleware
|
||||||
|
- [ ] Apply middleware globally (except admin routes)
|
||||||
|
- [ ] Create MaintenancePage component
|
||||||
|
- [ ] Test: Regular users blocked during maintenance
|
||||||
|
- [ ] Test: Admin users can still access
|
||||||
|
- [ ] Add maintenance message customization in settings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. New User Registration Toggle Implementation
|
||||||
|
**Status:** ⚠️ UI Only - Backend Logic Missing
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- ✅ UI: Toggle in Admin Settings page
|
||||||
|
- ✅ Database: `Config` table with `allowRegistration` key
|
||||||
|
- ❌ **Missing:** Check in registration endpoint
|
||||||
|
|
||||||
|
**Required Implementation:**
|
||||||
|
|
||||||
|
**A. Add Check in Auth Service:**
|
||||||
|
```typescript
|
||||||
|
// apps/api/src/auth/auth.service.ts
|
||||||
|
async register(email: string, password: string) {
|
||||||
|
// Check if registration is allowed
|
||||||
|
const config = await this.prisma.config.findUnique({
|
||||||
|
where: { key: 'allowRegistration' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config?.value === 'false') {
|
||||||
|
throw new ForbiddenException('New user registration is currently disabled. Please contact administrator.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with registration...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**B. Frontend Handling:**
|
||||||
|
```typescript
|
||||||
|
// apps/web/src/pages/Register.tsx
|
||||||
|
// Show message if registration is disabled
|
||||||
|
if (error.response?.data?.message?.includes('registration is currently disabled')) {
|
||||||
|
return (
|
||||||
|
<Alert>
|
||||||
|
<AlertTitle>Registration Disabled</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
New user registration is currently disabled. Please contact the administrator.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Add registration check in auth.service.ts
|
||||||
|
- [ ] Update register endpoint
|
||||||
|
- [ ] Frontend: Show disabled message
|
||||||
|
- [ ] Test: Registration blocked when disabled
|
||||||
|
- [ ] Test: Admin can still create users manually
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Email Verification Requirement Implementation
|
||||||
|
**Status:** ⚠️ UI Only - Backend Logic Missing
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- ✅ UI: Toggle in Admin Settings page
|
||||||
|
- ✅ Database: `Config` table with `requireEmailVerification` key
|
||||||
|
- ✅ Database: `User.emailVerified` field exists
|
||||||
|
- ❌ **Missing:** Enforcement in AuthGuard
|
||||||
|
|
||||||
|
**Required Implementation:**
|
||||||
|
|
||||||
|
**A. Add Check in AuthGuard:**
|
||||||
|
```typescript
|
||||||
|
// apps/api/src/auth/auth.guard.ts
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
// ... existing auth checks ...
|
||||||
|
|
||||||
|
// Check if email verification is required
|
||||||
|
const config = await this.prisma.config.findUnique({
|
||||||
|
where: { key: 'requireEmailVerification' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config?.value === 'true' && !user.emailVerified) {
|
||||||
|
throw new UnauthorizedException({
|
||||||
|
message: 'Please verify your email address to continue.',
|
||||||
|
emailVerificationRequired: true,
|
||||||
|
email: user.email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**B. Resend Verification Email:**
|
||||||
|
```typescript
|
||||||
|
// apps/api/src/auth/auth.controller.ts
|
||||||
|
@Post('resend-verification')
|
||||||
|
async resendVerification(@Body() body: { email: string }) {
|
||||||
|
return this.authService.resendVerificationEmail(body.email);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**C. Frontend Handling:**
|
||||||
|
```typescript
|
||||||
|
// apps/web/src/components/EmailVerificationRequired.tsx
|
||||||
|
// Show verification required page with resend button
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Add email verification check in AuthGuard
|
||||||
|
- [ ] Create resend verification endpoint
|
||||||
|
- [ ] Create EmailVerificationRequired component
|
||||||
|
- [ ] Test: Unverified users blocked when required
|
||||||
|
- [ ] Test: Resend verification email works
|
||||||
|
- [ ] Add email verification link in emails
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Manual Payment Verification Implementation
|
||||||
|
**Status:** ⚠️ UI Only - Backend Logic Missing
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- ✅ UI: Toggle in Admin Settings page
|
||||||
|
- ✅ Database: `Config` table with `requireManualPaymentVerification` key
|
||||||
|
- ✅ Database: `Payment.status` field exists
|
||||||
|
- ❌ **Missing:** Payment verification workflow
|
||||||
|
|
||||||
|
**Required Implementation:**
|
||||||
|
|
||||||
|
**A. Payment Verification Endpoint:**
|
||||||
|
```typescript
|
||||||
|
// apps/api/src/admin/admin-payments.controller.ts
|
||||||
|
@Post(':id/verify')
|
||||||
|
async verifyPayment(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: { approved: boolean; notes?: string }
|
||||||
|
) {
|
||||||
|
return this.service.verifyPayment(id, body.approved, body.notes);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**B. Payment Verification Service:**
|
||||||
|
```typescript
|
||||||
|
// apps/api/src/admin/admin-payments.service.ts
|
||||||
|
async verifyPayment(paymentId: string, approved: boolean, notes?: string) {
|
||||||
|
const payment = await this.prisma.payment.findUnique({
|
||||||
|
where: { id: paymentId },
|
||||||
|
include: { user: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (approved) {
|
||||||
|
// Approve payment
|
||||||
|
await this.prisma.payment.update({
|
||||||
|
where: { id: paymentId },
|
||||||
|
data: {
|
||||||
|
status: 'completed',
|
||||||
|
verifiedAt: new Date(),
|
||||||
|
verificationNotes: notes
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activate subscription
|
||||||
|
await this.activateSubscription(payment.userId, payment.planId);
|
||||||
|
|
||||||
|
// Send confirmation email
|
||||||
|
await this.emailService.sendPaymentApproved(payment.user.email);
|
||||||
|
} else {
|
||||||
|
// Reject payment
|
||||||
|
await this.prisma.payment.update({
|
||||||
|
where: { id: paymentId },
|
||||||
|
data: {
|
||||||
|
status: 'failed',
|
||||||
|
verifiedAt: new Date(),
|
||||||
|
verificationNotes: notes
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send rejection email
|
||||||
|
await this.emailService.sendPaymentRejected(payment.user.email, notes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**C. Payment Status Check:**
|
||||||
|
```typescript
|
||||||
|
// apps/api/src/payments/payments.service.ts
|
||||||
|
async create(userId: string, data: CreatePaymentDto) {
|
||||||
|
const config = await this.prisma.config.findUnique({
|
||||||
|
where: { key: 'requireManualPaymentVerification' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = config?.value === 'true' ? 'pending' : 'completed';
|
||||||
|
|
||||||
|
const payment = await this.prisma.payment.create({
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
userId,
|
||||||
|
status
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status === 'completed') {
|
||||||
|
// Auto-activate subscription
|
||||||
|
await this.activateSubscription(userId, data.planId);
|
||||||
|
} else {
|
||||||
|
// Notify admin of pending payment
|
||||||
|
await this.notifyAdminNewPayment(payment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payment;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**D. Admin UI for Verification:**
|
||||||
|
```typescript
|
||||||
|
// apps/web/src/components/admin/pages/AdminPayments.tsx
|
||||||
|
// Add Verify/Reject buttons for pending payments
|
||||||
|
<Button onClick={() => openVerifyDialog(payment.id)}>
|
||||||
|
Verify Payment
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Create payment verification endpoints
|
||||||
|
- [ ] Add verification logic in service
|
||||||
|
- [ ] Create admin verification UI
|
||||||
|
- [ ] Send email notifications (approved/rejected)
|
||||||
|
- [ ] Test: Manual verification workflow
|
||||||
|
- [ ] Test: Auto-approval when disabled
|
||||||
|
- [ ] Add payment receipt upload
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 MEDIUM PRIORITY - UI Translation to English
|
||||||
|
|
||||||
|
### Admin Dashboard Pages to Translate:
|
||||||
|
|
||||||
|
#### 1. Dashboard (AdminDashboard.tsx)
|
||||||
|
**Current:** Mixed Indonesian/English
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Convert all headings to English
|
||||||
|
- [ ] Convert all labels to English
|
||||||
|
- [ ] Convert all button text to English
|
||||||
|
- [ ] Convert all chart labels to English
|
||||||
|
- [ ] Convert all toast messages to English
|
||||||
|
|
||||||
|
#### 2. Plans (AdminPlans.tsx)
|
||||||
|
**Current:** Mixed Indonesian/English
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Convert page title and description
|
||||||
|
- [ ] Convert form labels
|
||||||
|
- [ ] Convert button text
|
||||||
|
- [ ] Convert table headers
|
||||||
|
- [ ] Convert dialog text
|
||||||
|
- [ ] Convert toast messages
|
||||||
|
|
||||||
|
#### 3. Payment Methods (AdminPaymentMethods.tsx)
|
||||||
|
**Current:** Mixed Indonesian/English
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Convert page title and description
|
||||||
|
- [ ] Convert form labels
|
||||||
|
- [ ] Convert button text
|
||||||
|
- [ ] Convert table headers
|
||||||
|
- [ ] Convert dialog text
|
||||||
|
- [ ] Convert toast messages
|
||||||
|
|
||||||
|
#### 4. Payments (AdminPayments.tsx)
|
||||||
|
**Current:** Mixed Indonesian/English
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Convert page title and description
|
||||||
|
- [ ] Convert filter labels
|
||||||
|
- [ ] Convert table headers
|
||||||
|
- [ ] Convert status badges
|
||||||
|
- [ ] Convert dialog text
|
||||||
|
- [ ] Convert toast messages
|
||||||
|
|
||||||
|
#### 5. Settings (AdminSettings.tsx)
|
||||||
|
**Current:** Mixed Indonesian/English
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Convert page title and description
|
||||||
|
- [ ] Convert section titles
|
||||||
|
- [ ] Convert toggle labels
|
||||||
|
- [ ] Convert button text
|
||||||
|
- [ ] Convert toast messages
|
||||||
|
- [ ] Convert help text
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Summary
|
||||||
|
|
||||||
|
### Backend Implementation Priority:
|
||||||
|
1. **Suspended User** - Critical security feature
|
||||||
|
2. **Email Verification** - Important for user management
|
||||||
|
3. **Maintenance Mode** - Important for system updates
|
||||||
|
4. **Registration Toggle** - Important for user management
|
||||||
|
5. **Pro Features** - Business logic (most complex)
|
||||||
|
6. **Manual Payment Verification** - Business logic
|
||||||
|
|
||||||
|
### Frontend Translation Priority:
|
||||||
|
1. **Dashboard** - Most visible page
|
||||||
|
2. **Settings** - Admin configuration
|
||||||
|
3. **Payments** - Business critical
|
||||||
|
4. **Plans** - Business critical
|
||||||
|
5. **Payment Methods** - Business critical
|
||||||
|
|
||||||
|
### Estimated Time:
|
||||||
|
- Backend Implementation: **3-5 days**
|
||||||
|
- Frontend Translation: **2-3 hours**
|
||||||
|
- Testing: **1-2 days**
|
||||||
|
|
||||||
|
**Total: ~1 week for complete implementation**
|
||||||
@@ -17,6 +17,7 @@ const common_1 = require("@nestjs/common");
|
|||||||
const auth_guard_1 = require("../auth/auth.guard");
|
const auth_guard_1 = require("../auth/auth.guard");
|
||||||
const admin_guard_1 = require("./guards/admin.guard");
|
const admin_guard_1 = require("./guards/admin.guard");
|
||||||
const admin_config_service_1 = require("./admin-config.service");
|
const admin_config_service_1 = require("./admin-config.service");
|
||||||
|
const skip_maintenance_decorator_1 = require("../common/decorators/skip-maintenance.decorator");
|
||||||
let AdminConfigController = class AdminConfigController {
|
let AdminConfigController = class AdminConfigController {
|
||||||
service;
|
service;
|
||||||
constructor(service) {
|
constructor(service) {
|
||||||
@@ -78,6 +79,7 @@ __decorate([
|
|||||||
exports.AdminConfigController = AdminConfigController = __decorate([
|
exports.AdminConfigController = AdminConfigController = __decorate([
|
||||||
(0, common_1.Controller)('admin/config'),
|
(0, common_1.Controller)('admin/config'),
|
||||||
(0, common_1.UseGuards)(auth_guard_1.AuthGuard, admin_guard_1.AdminGuard),
|
(0, common_1.UseGuards)(auth_guard_1.AuthGuard, admin_guard_1.AdminGuard),
|
||||||
|
(0, skip_maintenance_decorator_1.SkipMaintenance)(),
|
||||||
__metadata("design:paramtypes", [admin_config_service_1.AdminConfigService])
|
__metadata("design:paramtypes", [admin_config_service_1.AdminConfigService])
|
||||||
], AdminConfigController);
|
], AdminConfigController);
|
||||||
//# sourceMappingURL=admin-config.controller.js.map
|
//# sourceMappingURL=admin-config.controller.js.map
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"admin-config.controller.js","sourceRoot":"","sources":["../../src/admin/admin-config.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAUwB;AACxB,mDAA+C;AAC/C,sDAAkD;AAClD,iEAA4D;AAUrD,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;IACH;IAA7B,YAA6B,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;IAAG,CAAC;IAG5D,OAAO,CAAoB,QAAiB;QAC1C,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxC,CAAC;IAGD,aAAa;QACX,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;IACtC,CAAC;IAGD,OAAO,CAAe,GAAW;QAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IAGD,MAAM,CACU,GAAW,EACjB,IAAS,EACV,GAAoB;QAE3B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzD,CAAC;IAGD,MAAM,CAAe,GAAW;QAC9B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAClC,CAAC;CACF,CAAA;AA/BY,sDAAqB;AAIhC;IADC,IAAA,YAAG,GAAE;IACG,WAAA,IAAA,cAAK,EAAC,UAAU,CAAC,CAAA;;;;oDAEzB;AAGD;IADC,IAAA,YAAG,EAAC,aAAa,CAAC;;;;0DAGlB;AAGD;IADC,IAAA,YAAG,EAAC,MAAM,CAAC;IACH,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;;;;oDAEpB;AAGD;IADC,IAAA,aAAI,EAAC,MAAM,CAAC;IAEV,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;IACZ,WAAA,IAAA,aAAI,GAAE,CAAA;IACN,WAAA,IAAA,YAAG,GAAE,CAAA;;;;mDAGP;AAGD;IADC,IAAA,eAAM,EAAC,MAAM,CAAC;IACP,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;;;;mDAEnB;gCA9BU,qBAAqB;IAFjC,IAAA,mBAAU,EAAC,cAAc,CAAC;IAC1B,IAAA,kBAAS,EAAC,sBAAS,EAAE,wBAAU,CAAC;qCAEO,yCAAkB;GAD7C,qBAAqB,CA+BjC"}
|
{"version":3,"file":"admin-config.controller.js","sourceRoot":"","sources":["../../src/admin/admin-config.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAUwB;AACxB,mDAA+C;AAC/C,sDAAkD;AAClD,iEAA4D;AAC5D,gGAAkF;AAW3E,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;IACH;IAA7B,YAA6B,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;IAAG,CAAC;IAG5D,OAAO,CAAoB,QAAiB;QAC1C,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxC,CAAC;IAGD,aAAa;QACX,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;IACtC,CAAC;IAGD,OAAO,CAAe,GAAW;QAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IAGD,MAAM,CACU,GAAW,EACjB,IAAS,EACV,GAAoB;QAE3B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzD,CAAC;IAGD,MAAM,CAAe,GAAW;QAC9B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAClC,CAAC;CACF,CAAA;AA/BY,sDAAqB;AAIhC;IADC,IAAA,YAAG,GAAE;IACG,WAAA,IAAA,cAAK,EAAC,UAAU,CAAC,CAAA;;;;oDAEzB;AAGD;IADC,IAAA,YAAG,EAAC,aAAa,CAAC;;;;0DAGlB;AAGD;IADC,IAAA,YAAG,EAAC,MAAM,CAAC;IACH,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;;;;oDAEpB;AAGD;IADC,IAAA,aAAI,EAAC,MAAM,CAAC;IAEV,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;IACZ,WAAA,IAAA,aAAI,GAAE,CAAA;IACN,WAAA,IAAA,YAAG,GAAE,CAAA;;;;mDAGP;AAGD;IADC,IAAA,eAAM,EAAC,MAAM,CAAC;IACP,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;;;;mDAEnB;gCA9BU,qBAAqB;IAHjC,IAAA,mBAAU,EAAC,cAAc,CAAC;IAC1B,IAAA,kBAAS,EAAC,sBAAS,EAAE,wBAAU,CAAC;IAChC,IAAA,4CAAe,GAAE;qCAEsB,yCAAkB;GAD7C,qBAAqB,CA+BjC"}
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"admin-config.service.js","sourceRoot":"","sources":["../../src/admin/admin-config.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,6DAAyD;AAGlD,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IACA;IAA7B,YAA6B,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAEtD,KAAK,CAAC,OAAO,CAAC,QAAiB;QAC7B,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC;YACpC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,SAAS;YAC1C,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAW;QACvB,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;YACtC,KAAK,EAAE,EAAE,GAAG,EAAE;SACf,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW,EAAE,IAAS,EAAE,SAAiB;QACpD,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;YAClC,KAAK,EAAE,EAAE,GAAG,EAAE;YACd,MAAM,EAAE;gBACN,GAAG,IAAI;gBACP,SAAS;gBACT,SAAS,EAAE,IAAI,IAAI,EAAE;aACtB;YACD,MAAM,EAAE;gBACN,GAAG;gBACH,GAAG,IAAI;gBACP,SAAS;aACV;SACF,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;YAClC,KAAK,EAAE,EAAE,GAAG,EAAE;SACf,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,aAAa;QACjB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC;QAGvD,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YAC7C,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC1B,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YAC5B,CAAC;YACD,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAClC,OAAO,GAAG,CAAC;QACb,CAAC,EAAE,EAA2B,CAAC,CAAC;QAEhC,OAAO,OAAO,CAAC;IACjB,CAAC;CACF,CAAA;AApDY,gDAAkB;6BAAlB,kBAAkB;IAD9B,IAAA,mBAAU,GAAE;qCAE0B,8BAAa;GADvC,kBAAkB,CAoD9B"}
|
{"version":3,"file":"admin-config.service.js","sourceRoot":"","sources":["../../src/admin/admin-config.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,6DAAyD;AAGlD,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IACA;IAA7B,YAA6B,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAEtD,KAAK,CAAC,OAAO,CAAC,QAAiB;QAC7B,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC;YACpC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,SAAS;YAC1C,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAW;QACvB,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;YACtC,KAAK,EAAE,EAAE,GAAG,EAAE;SACf,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW,EAAE,IAAS,EAAE,SAAiB;QACpD,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;YAClC,KAAK,EAAE,EAAE,GAAG,EAAE;YACd,MAAM,EAAE;gBACN,GAAG,IAAI;gBACP,SAAS;gBACT,SAAS,EAAE,IAAI,IAAI,EAAE;aACtB;YACD,MAAM,EAAE;gBACN,GAAG;gBACH,GAAG,IAAI;gBACP,SAAS;aACV;SACF,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;YAClC,KAAK,EAAE,EAAE,GAAG,EAAE;SACf,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,aAAa;QACjB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC;QAGvD,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAC5B,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YACd,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC1B,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YAC5B,CAAC;YACD,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAClC,OAAO,GAAG,CAAC;QACb,CAAC,EACD,EAA2B,CAC5B,CAAC;QAEF,OAAO,OAAO,CAAC;IACjB,CAAC;CACF,CAAA;AAvDY,gDAAkB;6BAAlB,kBAAkB;IAD9B,IAAA,mBAAU,GAAE;qCAE0B,8BAAa;GADvC,kBAAkB,CAuD9B"}
|
||||||
@@ -17,6 +17,7 @@ const common_1 = require("@nestjs/common");
|
|||||||
const auth_guard_1 = require("../auth/auth.guard");
|
const auth_guard_1 = require("../auth/auth.guard");
|
||||||
const admin_guard_1 = require("./guards/admin.guard");
|
const admin_guard_1 = require("./guards/admin.guard");
|
||||||
const admin_payment_methods_service_1 = require("./admin-payment-methods.service");
|
const admin_payment_methods_service_1 = require("./admin-payment-methods.service");
|
||||||
|
const skip_maintenance_decorator_1 = require("../common/decorators/skip-maintenance.decorator");
|
||||||
let AdminPaymentMethodsController = class AdminPaymentMethodsController {
|
let AdminPaymentMethodsController = class AdminPaymentMethodsController {
|
||||||
service;
|
service;
|
||||||
constructor(service) {
|
constructor(service) {
|
||||||
@@ -87,6 +88,7 @@ __decorate([
|
|||||||
exports.AdminPaymentMethodsController = AdminPaymentMethodsController = __decorate([
|
exports.AdminPaymentMethodsController = AdminPaymentMethodsController = __decorate([
|
||||||
(0, common_1.Controller)('admin/payment-methods'),
|
(0, common_1.Controller)('admin/payment-methods'),
|
||||||
(0, common_1.UseGuards)(auth_guard_1.AuthGuard, admin_guard_1.AdminGuard),
|
(0, common_1.UseGuards)(auth_guard_1.AuthGuard, admin_guard_1.AdminGuard),
|
||||||
|
(0, skip_maintenance_decorator_1.SkipMaintenance)(),
|
||||||
__metadata("design:paramtypes", [admin_payment_methods_service_1.AdminPaymentMethodsService])
|
__metadata("design:paramtypes", [admin_payment_methods_service_1.AdminPaymentMethodsService])
|
||||||
], AdminPaymentMethodsController);
|
], AdminPaymentMethodsController);
|
||||||
//# sourceMappingURL=admin-payment-methods.controller.js.map
|
//# sourceMappingURL=admin-payment-methods.controller.js.map
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"admin-payment-methods.controller.js","sourceRoot":"","sources":["../../src/admin/admin-payment-methods.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CASwB;AACxB,mDAA+C;AAC/C,sDAAkD;AAClD,mFAA6E;AAItE,IAAM,6BAA6B,GAAnC,MAAM,6BAA6B;IACX;IAA7B,YAA6B,OAAmC;QAAnC,YAAO,GAAP,OAAO,CAA4B;IAAG,CAAC;IAGpE,OAAO;QACL,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IAChC,CAAC;IAGD,OAAO,CAAc,EAAU;QAC7B,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAClC,CAAC;IAGD,MAAM,CAAS,IAAS;QACtB,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAGD,MAAM,CAAc,EAAU,EAAU,IAAS;QAC/C,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IACvC,CAAC;IAGD,MAAM,CAAc,EAAU;QAC5B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACjC,CAAC;IAGD,OAAO,CAAS,IAA6B;QAC3C,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC9C,CAAC;CACF,CAAA;AAhCY,sEAA6B;AAIxC;IADC,IAAA,YAAG,GAAE;;;;4DAGL;AAGD;IADC,IAAA,YAAG,EAAC,KAAK,CAAC;IACF,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;4DAEnB;AAGD;IADC,IAAA,aAAI,GAAE;IACC,WAAA,IAAA,aAAI,GAAE,CAAA;;;;2DAEb;AAGD;IADC,IAAA,YAAG,EAAC,KAAK,CAAC;IACH,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IAAc,WAAA,IAAA,aAAI,GAAE,CAAA;;;;2DAEtC;AAGD;IADC,IAAA,eAAM,EAAC,KAAK,CAAC;IACN,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;2DAElB;AAGD;IADC,IAAA,aAAI,EAAC,SAAS,CAAC;IACP,WAAA,IAAA,aAAI,GAAE,CAAA;;;;4DAEd;wCA/BU,6BAA6B;IAFzC,IAAA,mBAAU,EAAC,uBAAuB,CAAC;IACnC,IAAA,kBAAS,EAAC,sBAAS,EAAE,wBAAU,CAAC;qCAEO,0DAA0B;GADrD,6BAA6B,CAgCzC"}
|
{"version":3,"file":"admin-payment-methods.controller.js","sourceRoot":"","sources":["../../src/admin/admin-payment-methods.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CASwB;AACxB,mDAA+C;AAC/C,sDAAkD;AAClD,mFAA6E;AAC7E,gGAAkF;AAK3E,IAAM,6BAA6B,GAAnC,MAAM,6BAA6B;IACX;IAA7B,YAA6B,OAAmC;QAAnC,YAAO,GAAP,OAAO,CAA4B;IAAG,CAAC;IAGpE,OAAO;QACL,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IAChC,CAAC;IAGD,OAAO,CAAc,EAAU;QAC7B,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAClC,CAAC;IAGD,MAAM,CAAS,IAAS;QACtB,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAGD,MAAM,CAAc,EAAU,EAAU,IAAS;QAC/C,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IACvC,CAAC;IAGD,MAAM,CAAc,EAAU;QAC5B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACjC,CAAC;IAGD,OAAO,CAAS,IAA6B;QAC3C,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC9C,CAAC;CACF,CAAA;AAhCY,sEAA6B;AAIxC;IADC,IAAA,YAAG,GAAE;;;;4DAGL;AAGD;IADC,IAAA,YAAG,EAAC,KAAK,CAAC;IACF,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;4DAEnB;AAGD;IADC,IAAA,aAAI,GAAE;IACC,WAAA,IAAA,aAAI,GAAE,CAAA;;;;2DAEb;AAGD;IADC,IAAA,YAAG,EAAC,KAAK,CAAC;IACH,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IAAc,WAAA,IAAA,aAAI,GAAE,CAAA;;;;2DAEtC;AAGD;IADC,IAAA,eAAM,EAAC,KAAK,CAAC;IACN,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;2DAElB;AAGD;IADC,IAAA,aAAI,EAAC,SAAS,CAAC;IACP,WAAA,IAAA,aAAI,GAAE,CAAA;;;;4DAEd;wCA/BU,6BAA6B;IAHzC,IAAA,mBAAU,EAAC,uBAAuB,CAAC;IACnC,IAAA,kBAAS,EAAC,sBAAS,EAAE,wBAAU,CAAC;IAChC,IAAA,4CAAe,GAAE;qCAEsB,0DAA0B;GADrD,6BAA6B,CAgCzC"}
|
||||||
@@ -16,10 +16,10 @@ export declare class AdminPaymentsController {
|
|||||||
subscription: ({
|
subscription: ({
|
||||||
plan: {
|
plan: {
|
||||||
id: string;
|
id: string;
|
||||||
currency: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
currency: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: import("@prisma/client/runtime/library").Decimal;
|
price: import("@prisma/client/runtime/library").Decimal;
|
||||||
@@ -42,10 +42,10 @@ export declare class AdminPaymentsController {
|
|||||||
};
|
};
|
||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
status: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
status: string;
|
||||||
|
userId: string;
|
||||||
planId: string;
|
planId: string;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
@@ -56,19 +56,21 @@ export declare class AdminPaymentsController {
|
|||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
status: string;
|
||||||
|
method: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
currency: string;
|
||||||
|
amount: import("@prisma/client/runtime/library").Decimal;
|
||||||
subscriptionId: string | null;
|
subscriptionId: string | null;
|
||||||
invoiceNumber: string;
|
invoiceNumber: string;
|
||||||
amount: import("@prisma/client/runtime/library").Decimal;
|
|
||||||
currency: string;
|
|
||||||
method: string;
|
|
||||||
tripayReference: string | null;
|
tripayReference: string | null;
|
||||||
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
||||||
paymentChannel: string | null;
|
paymentChannel: string | null;
|
||||||
paymentUrl: string | null;
|
paymentUrl: string | null;
|
||||||
qrUrl: string | null;
|
qrUrl: string | null;
|
||||||
status: string;
|
|
||||||
proofImageUrl: string | null;
|
proofImageUrl: string | null;
|
||||||
transferDate: Date | null;
|
transferDate: Date | null;
|
||||||
verifiedBy: string | null;
|
verifiedBy: string | null;
|
||||||
@@ -79,8 +81,6 @@ export declare class AdminPaymentsController {
|
|||||||
notes: string | null;
|
notes: string | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
paidAt: Date | null;
|
paidAt: Date | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
})[]>;
|
})[]>;
|
||||||
getPendingCount(): Promise<number>;
|
getPendingCount(): Promise<number>;
|
||||||
getMonthlyRevenue(): Promise<{
|
getMonthlyRevenue(): Promise<{
|
||||||
@@ -97,10 +97,10 @@ export declare class AdminPaymentsController {
|
|||||||
subscription: ({
|
subscription: ({
|
||||||
plan: {
|
plan: {
|
||||||
id: string;
|
id: string;
|
||||||
currency: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
currency: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: import("@prisma/client/runtime/library").Decimal;
|
price: import("@prisma/client/runtime/library").Decimal;
|
||||||
@@ -123,10 +123,10 @@ export declare class AdminPaymentsController {
|
|||||||
};
|
};
|
||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
status: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
status: string;
|
||||||
|
userId: string;
|
||||||
planId: string;
|
planId: string;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
@@ -137,19 +137,21 @@ export declare class AdminPaymentsController {
|
|||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
status: string;
|
||||||
|
method: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
currency: string;
|
||||||
|
amount: import("@prisma/client/runtime/library").Decimal;
|
||||||
subscriptionId: string | null;
|
subscriptionId: string | null;
|
||||||
invoiceNumber: string;
|
invoiceNumber: string;
|
||||||
amount: import("@prisma/client/runtime/library").Decimal;
|
|
||||||
currency: string;
|
|
||||||
method: string;
|
|
||||||
tripayReference: string | null;
|
tripayReference: string | null;
|
||||||
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
||||||
paymentChannel: string | null;
|
paymentChannel: string | null;
|
||||||
paymentUrl: string | null;
|
paymentUrl: string | null;
|
||||||
qrUrl: string | null;
|
qrUrl: string | null;
|
||||||
status: string;
|
|
||||||
proofImageUrl: string | null;
|
proofImageUrl: string | null;
|
||||||
transferDate: Date | null;
|
transferDate: Date | null;
|
||||||
verifiedBy: string | null;
|
verifiedBy: string | null;
|
||||||
@@ -160,24 +162,24 @@ export declare class AdminPaymentsController {
|
|||||||
notes: string | null;
|
notes: string | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
paidAt: Date | null;
|
paidAt: Date | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}) | null>;
|
}) | null>;
|
||||||
verify(id: string, req: RequestWithUser): Promise<{
|
verify(id: string, req: RequestWithUser): Promise<{
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
status: string;
|
||||||
|
method: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
currency: string;
|
||||||
|
amount: import("@prisma/client/runtime/library").Decimal;
|
||||||
subscriptionId: string | null;
|
subscriptionId: string | null;
|
||||||
invoiceNumber: string;
|
invoiceNumber: string;
|
||||||
amount: import("@prisma/client/runtime/library").Decimal;
|
|
||||||
currency: string;
|
|
||||||
method: string;
|
|
||||||
tripayReference: string | null;
|
tripayReference: string | null;
|
||||||
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
||||||
paymentChannel: string | null;
|
paymentChannel: string | null;
|
||||||
paymentUrl: string | null;
|
paymentUrl: string | null;
|
||||||
qrUrl: string | null;
|
qrUrl: string | null;
|
||||||
status: string;
|
|
||||||
proofImageUrl: string | null;
|
proofImageUrl: string | null;
|
||||||
transferDate: Date | null;
|
transferDate: Date | null;
|
||||||
verifiedBy: string | null;
|
verifiedBy: string | null;
|
||||||
@@ -188,26 +190,26 @@ export declare class AdminPaymentsController {
|
|||||||
notes: string | null;
|
notes: string | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
paidAt: Date | null;
|
paidAt: Date | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}>;
|
}>;
|
||||||
reject(id: string, req: RequestWithUser, body: {
|
reject(id: string, req: RequestWithUser, body: {
|
||||||
reason: string;
|
reason: string;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
status: string;
|
||||||
|
method: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
currency: string;
|
||||||
|
amount: import("@prisma/client/runtime/library").Decimal;
|
||||||
subscriptionId: string | null;
|
subscriptionId: string | null;
|
||||||
invoiceNumber: string;
|
invoiceNumber: string;
|
||||||
amount: import("@prisma/client/runtime/library").Decimal;
|
|
||||||
currency: string;
|
|
||||||
method: string;
|
|
||||||
tripayReference: string | null;
|
tripayReference: string | null;
|
||||||
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
||||||
paymentChannel: string | null;
|
paymentChannel: string | null;
|
||||||
paymentUrl: string | null;
|
paymentUrl: string | null;
|
||||||
qrUrl: string | null;
|
qrUrl: string | null;
|
||||||
status: string;
|
|
||||||
proofImageUrl: string | null;
|
proofImageUrl: string | null;
|
||||||
transferDate: Date | null;
|
transferDate: Date | null;
|
||||||
verifiedBy: string | null;
|
verifiedBy: string | null;
|
||||||
@@ -218,8 +220,6 @@ export declare class AdminPaymentsController {
|
|||||||
notes: string | null;
|
notes: string | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
paidAt: Date | null;
|
paidAt: Date | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const common_1 = require("@nestjs/common");
|
|||||||
const auth_guard_1 = require("../auth/auth.guard");
|
const auth_guard_1 = require("../auth/auth.guard");
|
||||||
const admin_guard_1 = require("./guards/admin.guard");
|
const admin_guard_1 = require("./guards/admin.guard");
|
||||||
const admin_payments_service_1 = require("./admin-payments.service");
|
const admin_payments_service_1 = require("./admin-payments.service");
|
||||||
|
const skip_maintenance_decorator_1 = require("../common/decorators/skip-maintenance.decorator");
|
||||||
let AdminPaymentsController = class AdminPaymentsController {
|
let AdminPaymentsController = class AdminPaymentsController {
|
||||||
service;
|
service;
|
||||||
constructor(service) {
|
constructor(service) {
|
||||||
@@ -88,6 +89,7 @@ __decorate([
|
|||||||
exports.AdminPaymentsController = AdminPaymentsController = __decorate([
|
exports.AdminPaymentsController = AdminPaymentsController = __decorate([
|
||||||
(0, common_1.Controller)('admin/payments'),
|
(0, common_1.Controller)('admin/payments'),
|
||||||
(0, common_1.UseGuards)(auth_guard_1.AuthGuard, admin_guard_1.AdminGuard),
|
(0, common_1.UseGuards)(auth_guard_1.AuthGuard, admin_guard_1.AdminGuard),
|
||||||
|
(0, skip_maintenance_decorator_1.SkipMaintenance)(),
|
||||||
__metadata("design:paramtypes", [admin_payments_service_1.AdminPaymentsService])
|
__metadata("design:paramtypes", [admin_payments_service_1.AdminPaymentsService])
|
||||||
], AdminPaymentsController);
|
], AdminPaymentsController);
|
||||||
//# sourceMappingURL=admin-payments.controller.js.map
|
//# sourceMappingURL=admin-payments.controller.js.map
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"admin-payments.controller.js","sourceRoot":"","sources":["../../src/admin/admin-payments.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CASwB;AACxB,mDAA+C;AAC/C,sDAAkD;AAClD,qEAAgE;AAUzD,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IACL;IAA7B,YAA6B,OAA6B;QAA7B,YAAO,GAAP,OAAO,CAAsB;IAAG,CAAC;IAG9D,OAAO,CAAkB,MAAe;QACtC,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAGD,eAAe;QACb,OAAO,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC;IACxC,CAAC;IAGD,iBAAiB;QACf,OAAO,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAC1C,CAAC;IAGD,OAAO,CAAc,EAAU;QAC7B,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAClC,CAAC;IAGD,MAAM,CAAc,EAAU,EAAS,GAAoB;QACzD,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAClD,CAAC;IAGD,MAAM,CACS,EAAU,EAChB,GAAoB,EACnB,IAAwB;QAEhC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IAC/D,CAAC;CACF,CAAA;AApCY,0DAAuB;AAIlC;IADC,IAAA,YAAG,GAAE;IACG,WAAA,IAAA,cAAK,EAAC,QAAQ,CAAC,CAAA;;;;sDAEvB;AAGD;IADC,IAAA,YAAG,EAAC,eAAe,CAAC;;;;8DAGpB;AAGD;IADC,IAAA,YAAG,EAAC,iBAAiB,CAAC;;;;gEAGtB;AAGD;IADC,IAAA,YAAG,EAAC,KAAK,CAAC;IACF,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;sDAEnB;AAGD;IADC,IAAA,aAAI,EAAC,YAAY,CAAC;IACX,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IAAc,WAAA,IAAA,YAAG,GAAE,CAAA;;;;qDAErC;AAGD;IADC,IAAA,aAAI,EAAC,YAAY,CAAC;IAEhB,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IACX,WAAA,IAAA,YAAG,GAAE,CAAA;IACL,WAAA,IAAA,aAAI,GAAE,CAAA;;;;qDAGR;kCAnCU,uBAAuB;IAFnC,IAAA,mBAAU,EAAC,gBAAgB,CAAC;IAC5B,IAAA,kBAAS,EAAC,sBAAS,EAAE,wBAAU,CAAC;qCAEO,6CAAoB;GAD/C,uBAAuB,CAoCnC"}
|
{"version":3,"file":"admin-payments.controller.js","sourceRoot":"","sources":["../../src/admin/admin-payments.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CASwB;AACxB,mDAA+C;AAC/C,sDAAkD;AAClD,qEAAgE;AAChE,gGAAkF;AAW3E,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IACL;IAA7B,YAA6B,OAA6B;QAA7B,YAAO,GAAP,OAAO,CAAsB;IAAG,CAAC;IAG9D,OAAO,CAAkB,MAAe;QACtC,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAGD,eAAe;QACb,OAAO,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC;IACxC,CAAC;IAGD,iBAAiB;QACf,OAAO,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAC1C,CAAC;IAGD,OAAO,CAAc,EAAU;QAC7B,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAClC,CAAC;IAGD,MAAM,CAAc,EAAU,EAAS,GAAoB;QACzD,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAClD,CAAC;IAGD,MAAM,CACS,EAAU,EAChB,GAAoB,EACnB,IAAwB;QAEhC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IAC/D,CAAC;CACF,CAAA;AApCY,0DAAuB;AAIlC;IADC,IAAA,YAAG,GAAE;IACG,WAAA,IAAA,cAAK,EAAC,QAAQ,CAAC,CAAA;;;;sDAEvB;AAGD;IADC,IAAA,YAAG,EAAC,eAAe,CAAC;;;;8DAGpB;AAGD;IADC,IAAA,YAAG,EAAC,iBAAiB,CAAC;;;;gEAGtB;AAGD;IADC,IAAA,YAAG,EAAC,KAAK,CAAC;IACF,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;sDAEnB;AAGD;IADC,IAAA,aAAI,EAAC,YAAY,CAAC;IACX,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IAAc,WAAA,IAAA,YAAG,GAAE,CAAA;;;;qDAErC;AAGD;IADC,IAAA,aAAI,EAAC,YAAY,CAAC;IAEhB,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IACX,WAAA,IAAA,YAAG,GAAE,CAAA;IACL,WAAA,IAAA,aAAI,GAAE,CAAA;;;;qDAGR;kCAnCU,uBAAuB;IAHnC,IAAA,mBAAU,EAAC,gBAAgB,CAAC;IAC5B,IAAA,kBAAS,EAAC,sBAAS,EAAE,wBAAU,CAAC;IAChC,IAAA,4CAAe,GAAE;qCAEsB,6CAAoB;GAD/C,uBAAuB,CAoCnC"}
|
||||||
60
apps/api/dist/admin/admin-payments.service.d.ts
vendored
60
apps/api/dist/admin/admin-payments.service.d.ts
vendored
@@ -11,10 +11,10 @@ export declare class AdminPaymentsService {
|
|||||||
subscription: ({
|
subscription: ({
|
||||||
plan: {
|
plan: {
|
||||||
id: string;
|
id: string;
|
||||||
currency: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
currency: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: import("@prisma/client/runtime/library").Decimal;
|
price: import("@prisma/client/runtime/library").Decimal;
|
||||||
@@ -37,10 +37,10 @@ export declare class AdminPaymentsService {
|
|||||||
};
|
};
|
||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
status: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
status: string;
|
||||||
|
userId: string;
|
||||||
planId: string;
|
planId: string;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
@@ -51,19 +51,21 @@ export declare class AdminPaymentsService {
|
|||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
status: string;
|
||||||
|
method: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
currency: string;
|
||||||
|
amount: import("@prisma/client/runtime/library").Decimal;
|
||||||
subscriptionId: string | null;
|
subscriptionId: string | null;
|
||||||
invoiceNumber: string;
|
invoiceNumber: string;
|
||||||
amount: import("@prisma/client/runtime/library").Decimal;
|
|
||||||
currency: string;
|
|
||||||
method: string;
|
|
||||||
tripayReference: string | null;
|
tripayReference: string | null;
|
||||||
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
||||||
paymentChannel: string | null;
|
paymentChannel: string | null;
|
||||||
paymentUrl: string | null;
|
paymentUrl: string | null;
|
||||||
qrUrl: string | null;
|
qrUrl: string | null;
|
||||||
status: string;
|
|
||||||
proofImageUrl: string | null;
|
proofImageUrl: string | null;
|
||||||
transferDate: Date | null;
|
transferDate: Date | null;
|
||||||
verifiedBy: string | null;
|
verifiedBy: string | null;
|
||||||
@@ -74,8 +76,6 @@ export declare class AdminPaymentsService {
|
|||||||
notes: string | null;
|
notes: string | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
paidAt: Date | null;
|
paidAt: Date | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
})[]>;
|
})[]>;
|
||||||
findOne(id: string): Promise<({
|
findOne(id: string): Promise<({
|
||||||
user: {
|
user: {
|
||||||
@@ -86,10 +86,10 @@ export declare class AdminPaymentsService {
|
|||||||
subscription: ({
|
subscription: ({
|
||||||
plan: {
|
plan: {
|
||||||
id: string;
|
id: string;
|
||||||
currency: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
currency: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: import("@prisma/client/runtime/library").Decimal;
|
price: import("@prisma/client/runtime/library").Decimal;
|
||||||
@@ -112,10 +112,10 @@ export declare class AdminPaymentsService {
|
|||||||
};
|
};
|
||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
status: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
status: string;
|
||||||
|
userId: string;
|
||||||
planId: string;
|
planId: string;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
@@ -126,19 +126,21 @@ export declare class AdminPaymentsService {
|
|||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
status: string;
|
||||||
|
method: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
currency: string;
|
||||||
|
amount: import("@prisma/client/runtime/library").Decimal;
|
||||||
subscriptionId: string | null;
|
subscriptionId: string | null;
|
||||||
invoiceNumber: string;
|
invoiceNumber: string;
|
||||||
amount: import("@prisma/client/runtime/library").Decimal;
|
|
||||||
currency: string;
|
|
||||||
method: string;
|
|
||||||
tripayReference: string | null;
|
tripayReference: string | null;
|
||||||
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
||||||
paymentChannel: string | null;
|
paymentChannel: string | null;
|
||||||
paymentUrl: string | null;
|
paymentUrl: string | null;
|
||||||
qrUrl: string | null;
|
qrUrl: string | null;
|
||||||
status: string;
|
|
||||||
proofImageUrl: string | null;
|
proofImageUrl: string | null;
|
||||||
transferDate: Date | null;
|
transferDate: Date | null;
|
||||||
verifiedBy: string | null;
|
verifiedBy: string | null;
|
||||||
@@ -149,24 +151,24 @@ export declare class AdminPaymentsService {
|
|||||||
notes: string | null;
|
notes: string | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
paidAt: Date | null;
|
paidAt: Date | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}) | null>;
|
}) | null>;
|
||||||
verify(id: string, adminUserId: string): Promise<{
|
verify(id: string, adminUserId: string): Promise<{
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
status: string;
|
||||||
|
method: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
currency: string;
|
||||||
|
amount: import("@prisma/client/runtime/library").Decimal;
|
||||||
subscriptionId: string | null;
|
subscriptionId: string | null;
|
||||||
invoiceNumber: string;
|
invoiceNumber: string;
|
||||||
amount: import("@prisma/client/runtime/library").Decimal;
|
|
||||||
currency: string;
|
|
||||||
method: string;
|
|
||||||
tripayReference: string | null;
|
tripayReference: string | null;
|
||||||
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
||||||
paymentChannel: string | null;
|
paymentChannel: string | null;
|
||||||
paymentUrl: string | null;
|
paymentUrl: string | null;
|
||||||
qrUrl: string | null;
|
qrUrl: string | null;
|
||||||
status: string;
|
|
||||||
proofImageUrl: string | null;
|
proofImageUrl: string | null;
|
||||||
transferDate: Date | null;
|
transferDate: Date | null;
|
||||||
verifiedBy: string | null;
|
verifiedBy: string | null;
|
||||||
@@ -177,24 +179,24 @@ export declare class AdminPaymentsService {
|
|||||||
notes: string | null;
|
notes: string | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
paidAt: Date | null;
|
paidAt: Date | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}>;
|
}>;
|
||||||
reject(id: string, adminUserId: string, reason: string): Promise<{
|
reject(id: string, adminUserId: string, reason: string): Promise<{
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
status: string;
|
||||||
|
method: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
currency: string;
|
||||||
|
amount: import("@prisma/client/runtime/library").Decimal;
|
||||||
subscriptionId: string | null;
|
subscriptionId: string | null;
|
||||||
invoiceNumber: string;
|
invoiceNumber: string;
|
||||||
amount: import("@prisma/client/runtime/library").Decimal;
|
|
||||||
currency: string;
|
|
||||||
method: string;
|
|
||||||
tripayReference: string | null;
|
tripayReference: string | null;
|
||||||
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
tripayFee: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
totalAmount: import("@prisma/client/runtime/library").Decimal;
|
||||||
paymentChannel: string | null;
|
paymentChannel: string | null;
|
||||||
paymentUrl: string | null;
|
paymentUrl: string | null;
|
||||||
qrUrl: string | null;
|
qrUrl: string | null;
|
||||||
status: string;
|
|
||||||
proofImageUrl: string | null;
|
proofImageUrl: string | null;
|
||||||
transferDate: Date | null;
|
transferDate: Date | null;
|
||||||
verifiedBy: string | null;
|
verifiedBy: string | null;
|
||||||
@@ -205,8 +207,6 @@ export declare class AdminPaymentsService {
|
|||||||
notes: string | null;
|
notes: string | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
paidAt: Date | null;
|
paidAt: Date | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}>;
|
}>;
|
||||||
getPendingCount(): Promise<number>;
|
getPendingCount(): Promise<number>;
|
||||||
getMonthlyRevenue(): Promise<{
|
getMonthlyRevenue(): Promise<{
|
||||||
|
|||||||
15
apps/api/dist/admin/admin-payments.service.js
vendored
15
apps/api/dist/admin/admin-payments.service.js
vendored
@@ -123,7 +123,20 @@ let AdminPaymentsService = class AdminPaymentsService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const monthlyData = {};
|
const monthlyData = {};
|
||||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
const months = [
|
||||||
|
'Jan',
|
||||||
|
'Feb',
|
||||||
|
'Mar',
|
||||||
|
'Apr',
|
||||||
|
'May',
|
||||||
|
'Jun',
|
||||||
|
'Jul',
|
||||||
|
'Aug',
|
||||||
|
'Sep',
|
||||||
|
'Oct',
|
||||||
|
'Nov',
|
||||||
|
'Dec',
|
||||||
|
];
|
||||||
payments.forEach((payment) => {
|
payments.forEach((payment) => {
|
||||||
if (payment.paidAt) {
|
if (payment.paidAt) {
|
||||||
const date = new Date(payment.paidAt);
|
const date = new Date(payment.paidAt);
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"admin-payments.service.js","sourceRoot":"","sources":["../../src/admin/admin-payments.service.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"admin-payments.service.js","sourceRoot":"","sources":["../../src/admin/admin-payments.service.ts"],"names":[],"mappings":"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"}
|
||||||
30
apps/api/dist/admin/admin-plans.controller.d.ts
vendored
30
apps/api/dist/admin/admin-plans.controller.d.ts
vendored
@@ -8,11 +8,13 @@ export declare class AdminPlansController {
|
|||||||
};
|
};
|
||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
currency: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: import("@prisma/client/runtime/library").Decimal;
|
price: import("@prisma/client/runtime/library").Decimal;
|
||||||
currency: string;
|
|
||||||
durationType: string;
|
durationType: string;
|
||||||
durationDays: number | null;
|
durationDays: number | null;
|
||||||
trialDays: number;
|
trialDays: number;
|
||||||
@@ -29,8 +31,6 @@ export declare class AdminPlansController {
|
|||||||
maxTeamMembers: number | null;
|
maxTeamMembers: number | null;
|
||||||
apiEnabled: boolean;
|
apiEnabled: boolean;
|
||||||
apiRateLimit: number | null;
|
apiRateLimit: number | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
})[]>;
|
})[]>;
|
||||||
findOne(id: string): Promise<({
|
findOne(id: string): Promise<({
|
||||||
_count: {
|
_count: {
|
||||||
@@ -38,11 +38,13 @@ export declare class AdminPlansController {
|
|||||||
};
|
};
|
||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
currency: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: import("@prisma/client/runtime/library").Decimal;
|
price: import("@prisma/client/runtime/library").Decimal;
|
||||||
currency: string;
|
|
||||||
durationType: string;
|
durationType: string;
|
||||||
durationDays: number | null;
|
durationDays: number | null;
|
||||||
trialDays: number;
|
trialDays: number;
|
||||||
@@ -59,16 +61,16 @@ export declare class AdminPlansController {
|
|||||||
maxTeamMembers: number | null;
|
maxTeamMembers: number | null;
|
||||||
apiEnabled: boolean;
|
apiEnabled: boolean;
|
||||||
apiRateLimit: number | null;
|
apiRateLimit: number | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}) | null>;
|
}) | null>;
|
||||||
create(data: any): Promise<{
|
create(data: any): Promise<{
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
currency: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: import("@prisma/client/runtime/library").Decimal;
|
price: import("@prisma/client/runtime/library").Decimal;
|
||||||
currency: string;
|
|
||||||
durationType: string;
|
durationType: string;
|
||||||
durationDays: number | null;
|
durationDays: number | null;
|
||||||
trialDays: number;
|
trialDays: number;
|
||||||
@@ -85,16 +87,16 @@ export declare class AdminPlansController {
|
|||||||
maxTeamMembers: number | null;
|
maxTeamMembers: number | null;
|
||||||
apiEnabled: boolean;
|
apiEnabled: boolean;
|
||||||
apiRateLimit: number | null;
|
apiRateLimit: number | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}>;
|
}>;
|
||||||
update(id: string, data: any): Promise<{
|
update(id: string, data: any): Promise<{
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
currency: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: import("@prisma/client/runtime/library").Decimal;
|
price: import("@prisma/client/runtime/library").Decimal;
|
||||||
currency: string;
|
|
||||||
durationType: string;
|
durationType: string;
|
||||||
durationDays: number | null;
|
durationDays: number | null;
|
||||||
trialDays: number;
|
trialDays: number;
|
||||||
@@ -111,8 +113,6 @@ export declare class AdminPlansController {
|
|||||||
maxTeamMembers: number | null;
|
maxTeamMembers: number | null;
|
||||||
apiEnabled: boolean;
|
apiEnabled: boolean;
|
||||||
apiRateLimit: number | null;
|
apiRateLimit: number | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}>;
|
}>;
|
||||||
delete(id: string): Promise<{
|
delete(id: string): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -120,11 +120,13 @@ export declare class AdminPlansController {
|
|||||||
action: string;
|
action: string;
|
||||||
plan: {
|
plan: {
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
currency: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: import("@prisma/client/runtime/library").Decimal;
|
price: import("@prisma/client/runtime/library").Decimal;
|
||||||
currency: string;
|
|
||||||
durationType: string;
|
durationType: string;
|
||||||
durationDays: number | null;
|
durationDays: number | null;
|
||||||
trialDays: number;
|
trialDays: number;
|
||||||
@@ -141,8 +143,6 @@ export declare class AdminPlansController {
|
|||||||
maxTeamMembers: number | null;
|
maxTeamMembers: number | null;
|
||||||
apiEnabled: boolean;
|
apiEnabled: boolean;
|
||||||
apiRateLimit: number | null;
|
apiRateLimit: number | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
};
|
};
|
||||||
} | {
|
} | {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const common_1 = require("@nestjs/common");
|
|||||||
const auth_guard_1 = require("../auth/auth.guard");
|
const auth_guard_1 = require("../auth/auth.guard");
|
||||||
const admin_guard_1 = require("./guards/admin.guard");
|
const admin_guard_1 = require("./guards/admin.guard");
|
||||||
const admin_plans_service_1 = require("./admin-plans.service");
|
const admin_plans_service_1 = require("./admin-plans.service");
|
||||||
|
const skip_maintenance_decorator_1 = require("../common/decorators/skip-maintenance.decorator");
|
||||||
let AdminPlansController = class AdminPlansController {
|
let AdminPlansController = class AdminPlansController {
|
||||||
plansService;
|
plansService;
|
||||||
constructor(plansService) {
|
constructor(plansService) {
|
||||||
@@ -87,6 +88,7 @@ __decorate([
|
|||||||
exports.AdminPlansController = AdminPlansController = __decorate([
|
exports.AdminPlansController = AdminPlansController = __decorate([
|
||||||
(0, common_1.Controller)('admin/plans'),
|
(0, common_1.Controller)('admin/plans'),
|
||||||
(0, common_1.UseGuards)(auth_guard_1.AuthGuard, admin_guard_1.AdminGuard),
|
(0, common_1.UseGuards)(auth_guard_1.AuthGuard, admin_guard_1.AdminGuard),
|
||||||
|
(0, skip_maintenance_decorator_1.SkipMaintenance)(),
|
||||||
__metadata("design:paramtypes", [admin_plans_service_1.AdminPlansService])
|
__metadata("design:paramtypes", [admin_plans_service_1.AdminPlansService])
|
||||||
], AdminPlansController);
|
], AdminPlansController);
|
||||||
//# sourceMappingURL=admin-plans.controller.js.map
|
//# sourceMappingURL=admin-plans.controller.js.map
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"admin-plans.controller.js","sourceRoot":"","sources":["../../src/admin/admin-plans.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CASwB;AACxB,mDAA+C;AAC/C,sDAAkD;AAClD,+DAA0D;AAInD,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACF;IAA7B,YAA6B,YAA+B;QAA/B,iBAAY,GAAZ,YAAY,CAAmB;IAAG,CAAC;IAGhE,OAAO;QACL,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;IACrC,CAAC;IAGD,OAAO,CAAc,EAAU;QAC7B,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACvC,CAAC;IAGD,MAAM,CAAS,IAAS;QACtB,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAGD,MAAM,CAAc,EAAU,EAAU,IAAS;QAC/C,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IAC5C,CAAC;IAGD,MAAM,CAAc,EAAU;QAC5B,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACtC,CAAC;IAGD,OAAO,CAAS,IAA2B;QACzC,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACjD,CAAC;CACF,CAAA;AAhCY,oDAAoB;AAI/B;IADC,IAAA,YAAG,GAAE;;;;mDAGL;AAGD;IADC,IAAA,YAAG,EAAC,KAAK,CAAC;IACF,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;mDAEnB;AAGD;IADC,IAAA,aAAI,GAAE;IACC,WAAA,IAAA,aAAI,GAAE,CAAA;;;;kDAEb;AAGD;IADC,IAAA,YAAG,EAAC,KAAK,CAAC;IACH,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IAAc,WAAA,IAAA,aAAI,GAAE,CAAA;;;;kDAEtC;AAGD;IADC,IAAA,eAAM,EAAC,KAAK,CAAC;IACN,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;kDAElB;AAGD;IADC,IAAA,aAAI,EAAC,SAAS,CAAC;IACP,WAAA,IAAA,aAAI,GAAE,CAAA;;;;mDAEd;+BA/BU,oBAAoB;IAFhC,IAAA,mBAAU,EAAC,aAAa,CAAC;IACzB,IAAA,kBAAS,EAAC,sBAAS,EAAE,wBAAU,CAAC;qCAEY,uCAAiB;GADjD,oBAAoB,CAgChC"}
|
{"version":3,"file":"admin-plans.controller.js","sourceRoot":"","sources":["../../src/admin/admin-plans.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CASwB;AACxB,mDAA+C;AAC/C,sDAAkD;AAClD,+DAA0D;AAC1D,gGAAkF;AAK3E,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACF;IAA7B,YAA6B,YAA+B;QAA/B,iBAAY,GAAZ,YAAY,CAAmB;IAAG,CAAC;IAGhE,OAAO;QACL,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;IACrC,CAAC;IAGD,OAAO,CAAc,EAAU;QAC7B,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACvC,CAAC;IAGD,MAAM,CAAS,IAAS;QACtB,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAGD,MAAM,CAAc,EAAU,EAAU,IAAS;QAC/C,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IAC5C,CAAC;IAGD,MAAM,CAAc,EAAU;QAC5B,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACtC,CAAC;IAGD,OAAO,CAAS,IAA2B;QACzC,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACjD,CAAC;CACF,CAAA;AAhCY,oDAAoB;AAI/B;IADC,IAAA,YAAG,GAAE;;;;mDAGL;AAGD;IADC,IAAA,YAAG,EAAC,KAAK,CAAC;IACF,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;mDAEnB;AAGD;IADC,IAAA,aAAI,GAAE;IACC,WAAA,IAAA,aAAI,GAAE,CAAA;;;;kDAEb;AAGD;IADC,IAAA,YAAG,EAAC,KAAK,CAAC;IACH,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IAAc,WAAA,IAAA,aAAI,GAAE,CAAA;;;;kDAEtC;AAGD;IADC,IAAA,eAAM,EAAC,KAAK,CAAC;IACN,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;kDAElB;AAGD;IADC,IAAA,aAAI,EAAC,SAAS,CAAC;IACP,WAAA,IAAA,aAAI,GAAE,CAAA;;;;mDAEd;+BA/BU,oBAAoB;IAHhC,IAAA,mBAAU,EAAC,aAAa,CAAC;IACzB,IAAA,kBAAS,EAAC,sBAAS,EAAE,wBAAU,CAAC;IAChC,IAAA,4CAAe,GAAE;qCAE2B,uCAAiB;GADjD,oBAAoB,CAgChC"}
|
||||||
30
apps/api/dist/admin/admin-plans.service.d.ts
vendored
30
apps/api/dist/admin/admin-plans.service.d.ts
vendored
@@ -8,11 +8,13 @@ export declare class AdminPlansService {
|
|||||||
};
|
};
|
||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
currency: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: import("@prisma/client/runtime/library").Decimal;
|
price: import("@prisma/client/runtime/library").Decimal;
|
||||||
currency: string;
|
|
||||||
durationType: string;
|
durationType: string;
|
||||||
durationDays: number | null;
|
durationDays: number | null;
|
||||||
trialDays: number;
|
trialDays: number;
|
||||||
@@ -29,8 +31,6 @@ export declare class AdminPlansService {
|
|||||||
maxTeamMembers: number | null;
|
maxTeamMembers: number | null;
|
||||||
apiEnabled: boolean;
|
apiEnabled: boolean;
|
||||||
apiRateLimit: number | null;
|
apiRateLimit: number | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
})[]>;
|
})[]>;
|
||||||
findOne(id: string): Promise<({
|
findOne(id: string): Promise<({
|
||||||
_count: {
|
_count: {
|
||||||
@@ -38,11 +38,13 @@ export declare class AdminPlansService {
|
|||||||
};
|
};
|
||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
currency: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: import("@prisma/client/runtime/library").Decimal;
|
price: import("@prisma/client/runtime/library").Decimal;
|
||||||
currency: string;
|
|
||||||
durationType: string;
|
durationType: string;
|
||||||
durationDays: number | null;
|
durationDays: number | null;
|
||||||
trialDays: number;
|
trialDays: number;
|
||||||
@@ -59,16 +61,16 @@ export declare class AdminPlansService {
|
|||||||
maxTeamMembers: number | null;
|
maxTeamMembers: number | null;
|
||||||
apiEnabled: boolean;
|
apiEnabled: boolean;
|
||||||
apiRateLimit: number | null;
|
apiRateLimit: number | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}) | null>;
|
}) | null>;
|
||||||
create(data: any): Promise<{
|
create(data: any): Promise<{
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
currency: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: import("@prisma/client/runtime/library").Decimal;
|
price: import("@prisma/client/runtime/library").Decimal;
|
||||||
currency: string;
|
|
||||||
durationType: string;
|
durationType: string;
|
||||||
durationDays: number | null;
|
durationDays: number | null;
|
||||||
trialDays: number;
|
trialDays: number;
|
||||||
@@ -85,16 +87,16 @@ export declare class AdminPlansService {
|
|||||||
maxTeamMembers: number | null;
|
maxTeamMembers: number | null;
|
||||||
apiEnabled: boolean;
|
apiEnabled: boolean;
|
||||||
apiRateLimit: number | null;
|
apiRateLimit: number | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}>;
|
}>;
|
||||||
update(id: string, data: any): Promise<{
|
update(id: string, data: any): Promise<{
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
currency: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: import("@prisma/client/runtime/library").Decimal;
|
price: import("@prisma/client/runtime/library").Decimal;
|
||||||
currency: string;
|
|
||||||
durationType: string;
|
durationType: string;
|
||||||
durationDays: number | null;
|
durationDays: number | null;
|
||||||
trialDays: number;
|
trialDays: number;
|
||||||
@@ -111,8 +113,6 @@ export declare class AdminPlansService {
|
|||||||
maxTeamMembers: number | null;
|
maxTeamMembers: number | null;
|
||||||
apiEnabled: boolean;
|
apiEnabled: boolean;
|
||||||
apiRateLimit: number | null;
|
apiRateLimit: number | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}>;
|
}>;
|
||||||
delete(id: string): Promise<{
|
delete(id: string): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -120,11 +120,13 @@ export declare class AdminPlansService {
|
|||||||
action: string;
|
action: string;
|
||||||
plan: {
|
plan: {
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
|
currency: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: import("@prisma/client/runtime/library").Decimal;
|
price: import("@prisma/client/runtime/library").Decimal;
|
||||||
currency: string;
|
|
||||||
durationType: string;
|
durationType: string;
|
||||||
durationDays: number | null;
|
durationDays: number | null;
|
||||||
trialDays: number;
|
trialDays: number;
|
||||||
@@ -141,8 +143,6 @@ export declare class AdminPlansService {
|
|||||||
maxTeamMembers: number | null;
|
maxTeamMembers: number | null;
|
||||||
apiEnabled: boolean;
|
apiEnabled: boolean;
|
||||||
apiRateLimit: number | null;
|
apiRateLimit: number | null;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
};
|
};
|
||||||
} | {
|
} | {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|||||||
28
apps/api/dist/admin/admin-users.controller.d.ts
vendored
28
apps/api/dist/admin/admin-users.controller.d.ts
vendored
@@ -177,4 +177,32 @@ export declare class AdminUsersController {
|
|||||||
cancelledAt: Date | null;
|
cancelledAt: Date | null;
|
||||||
cancellationReason: string | null;
|
cancellationReason: string | null;
|
||||||
}>;
|
}>;
|
||||||
|
create(body: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
name?: string;
|
||||||
|
role?: string;
|
||||||
|
}): Promise<{
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
createdAt: Date;
|
||||||
|
emailVerified: boolean;
|
||||||
|
name: string | null;
|
||||||
|
role: string;
|
||||||
|
}>;
|
||||||
|
update(id: string, body: {
|
||||||
|
email?: string;
|
||||||
|
name?: string;
|
||||||
|
role?: string;
|
||||||
|
}): Promise<{
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
createdAt: Date;
|
||||||
|
emailVerified: boolean;
|
||||||
|
name: string | null;
|
||||||
|
role: string;
|
||||||
|
}>;
|
||||||
|
delete(id: string): Promise<{
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
33
apps/api/dist/admin/admin-users.controller.js
vendored
33
apps/api/dist/admin/admin-users.controller.js
vendored
@@ -17,6 +17,7 @@ const common_1 = require("@nestjs/common");
|
|||||||
const auth_guard_1 = require("../auth/auth.guard");
|
const auth_guard_1 = require("../auth/auth.guard");
|
||||||
const admin_guard_1 = require("./guards/admin.guard");
|
const admin_guard_1 = require("./guards/admin.guard");
|
||||||
const admin_users_service_1 = require("./admin-users.service");
|
const admin_users_service_1 = require("./admin-users.service");
|
||||||
|
const skip_maintenance_decorator_1 = require("../common/decorators/skip-maintenance.decorator");
|
||||||
let AdminUsersController = class AdminUsersController {
|
let AdminUsersController = class AdminUsersController {
|
||||||
service;
|
service;
|
||||||
constructor(service) {
|
constructor(service) {
|
||||||
@@ -43,6 +44,15 @@ let AdminUsersController = class AdminUsersController {
|
|||||||
grantProAccess(id, body) {
|
grantProAccess(id, body) {
|
||||||
return this.service.grantProAccess(id, body.planSlug, body.durationDays);
|
return this.service.grantProAccess(id, body.planSlug, body.durationDays);
|
||||||
}
|
}
|
||||||
|
create(body) {
|
||||||
|
return this.service.create(body);
|
||||||
|
}
|
||||||
|
update(id, body) {
|
||||||
|
return this.service.update(id, body);
|
||||||
|
}
|
||||||
|
delete(id) {
|
||||||
|
return this.service.delete(id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
exports.AdminUsersController = AdminUsersController;
|
exports.AdminUsersController = AdminUsersController;
|
||||||
__decorate([
|
__decorate([
|
||||||
@@ -96,9 +106,32 @@ __decorate([
|
|||||||
__metadata("design:paramtypes", [String, Object]),
|
__metadata("design:paramtypes", [String, Object]),
|
||||||
__metadata("design:returntype", void 0)
|
__metadata("design:returntype", void 0)
|
||||||
], AdminUsersController.prototype, "grantProAccess", null);
|
], AdminUsersController.prototype, "grantProAccess", null);
|
||||||
|
__decorate([
|
||||||
|
(0, common_1.Post)(),
|
||||||
|
__param(0, (0, common_1.Body)()),
|
||||||
|
__metadata("design:type", Function),
|
||||||
|
__metadata("design:paramtypes", [Object]),
|
||||||
|
__metadata("design:returntype", void 0)
|
||||||
|
], AdminUsersController.prototype, "create", null);
|
||||||
|
__decorate([
|
||||||
|
(0, common_1.Put)(':id'),
|
||||||
|
__param(0, (0, common_1.Param)('id')),
|
||||||
|
__param(1, (0, common_1.Body)()),
|
||||||
|
__metadata("design:type", Function),
|
||||||
|
__metadata("design:paramtypes", [String, Object]),
|
||||||
|
__metadata("design:returntype", void 0)
|
||||||
|
], AdminUsersController.prototype, "update", null);
|
||||||
|
__decorate([
|
||||||
|
(0, common_1.Delete)(':id'),
|
||||||
|
__param(0, (0, common_1.Param)('id')),
|
||||||
|
__metadata("design:type", Function),
|
||||||
|
__metadata("design:paramtypes", [String]),
|
||||||
|
__metadata("design:returntype", void 0)
|
||||||
|
], AdminUsersController.prototype, "delete", null);
|
||||||
exports.AdminUsersController = AdminUsersController = __decorate([
|
exports.AdminUsersController = AdminUsersController = __decorate([
|
||||||
(0, common_1.Controller)('admin/users'),
|
(0, common_1.Controller)('admin/users'),
|
||||||
(0, common_1.UseGuards)(auth_guard_1.AuthGuard, admin_guard_1.AdminGuard),
|
(0, common_1.UseGuards)(auth_guard_1.AuthGuard, admin_guard_1.AdminGuard),
|
||||||
|
(0, skip_maintenance_decorator_1.SkipMaintenance)(),
|
||||||
__metadata("design:paramtypes", [admin_users_service_1.AdminUsersService])
|
__metadata("design:paramtypes", [admin_users_service_1.AdminUsersService])
|
||||||
], AdminUsersController);
|
], AdminUsersController);
|
||||||
//# sourceMappingURL=admin-users.controller.js.map
|
//# sourceMappingURL=admin-users.controller.js.map
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"admin-users.controller.js","sourceRoot":"","sources":["../../src/admin/admin-users.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CASwB;AACxB,mDAA+C;AAC/C,sDAAkD;AAClD,+DAA0D;AAInD,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACF;IAA7B,YAA6B,OAA0B;QAA1B,YAAO,GAAP,OAAO,CAAmB;IAAG,CAAC;IAG3D,OAAO,CAAkB,MAAe;QACtC,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAGD,QAAQ;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;IACjC,CAAC;IAGD,OAAO,CAAc,EAAU;QAC7B,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAClC,CAAC;IAGD,UAAU,CAAc,EAAU,EAAU,IAAsB;QAChE,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC;IAGD,OAAO,CAAc,EAAU,EAAU,IAAwB;QAC/D,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IAC/C,CAAC;IAGD,SAAS,CAAc,EAAU;QAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACpC,CAAC;IAGD,cAAc,CACC,EAAU,EACf,IAAgD;QAExD,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IAC3E,CAAC;CACF,CAAA;AAxCY,oDAAoB;AAI/B;IADC,IAAA,YAAG,GAAE;IACG,WAAA,IAAA,cAAK,EAAC,QAAQ,CAAC,CAAA;;;;mDAEvB;AAGD;IADC,IAAA,YAAG,EAAC,OAAO,CAAC;;;;oDAGZ;AAGD;IADC,IAAA,YAAG,EAAC,KAAK,CAAC;IACF,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;mDAEnB;AAGD;IADC,IAAA,YAAG,EAAC,UAAU,CAAC;IACJ,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IAAc,WAAA,IAAA,aAAI,GAAE,CAAA;;;;sDAE1C;AAGD;IADC,IAAA,aAAI,EAAC,aAAa,CAAC;IACX,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IAAc,WAAA,IAAA,aAAI,GAAE,CAAA;;;;mDAEvC;AAGD;IADC,IAAA,aAAI,EAAC,eAAe,CAAC;IACX,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;qDAErB;AAGD;IADC,IAAA,aAAI,EAAC,eAAe,CAAC;IAEnB,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IACX,WAAA,IAAA,aAAI,GAAE,CAAA;;;;0DAGR;+BAvCU,oBAAoB;IAFhC,IAAA,mBAAU,EAAC,aAAa,CAAC;IACzB,IAAA,kBAAS,EAAC,sBAAS,EAAE,wBAAU,CAAC;qCAEO,uCAAiB;GAD5C,oBAAoB,CAwChC"}
|
{"version":3,"file":"admin-users.controller.js","sourceRoot":"","sources":["../../src/admin/admin-users.controller.ts"],"names":[],"mappings":"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"}
|
||||||
28
apps/api/dist/admin/admin-users.service.d.ts
vendored
28
apps/api/dist/admin/admin-users.service.d.ts
vendored
@@ -170,4 +170,32 @@ export declare class AdminUsersService {
|
|||||||
activeSubscriptions: number;
|
activeSubscriptions: number;
|
||||||
suspendedUsers: number;
|
suspendedUsers: number;
|
||||||
}>;
|
}>;
|
||||||
|
create(data: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
name?: string;
|
||||||
|
role?: string;
|
||||||
|
}): Promise<{
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
createdAt: Date;
|
||||||
|
emailVerified: boolean;
|
||||||
|
name: string | null;
|
||||||
|
role: string;
|
||||||
|
}>;
|
||||||
|
update(id: string, data: {
|
||||||
|
email?: string;
|
||||||
|
name?: string;
|
||||||
|
role?: string;
|
||||||
|
}): Promise<{
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
createdAt: Date;
|
||||||
|
emailVerified: boolean;
|
||||||
|
name: string | null;
|
||||||
|
role: string;
|
||||||
|
}>;
|
||||||
|
delete(id: string): Promise<{
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
104
apps/api/dist/admin/admin-users.service.js
vendored
104
apps/api/dist/admin/admin-users.service.js
vendored
@@ -1,10 +1,43 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||||
};
|
};
|
||||||
|
var __importStar = (this && this.__importStar) || (function () {
|
||||||
|
var ownKeys = function(o) {
|
||||||
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||||
|
var ar = [];
|
||||||
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||||
|
return ar;
|
||||||
|
};
|
||||||
|
return ownKeys(o);
|
||||||
|
};
|
||||||
|
return function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||||
};
|
};
|
||||||
@@ -12,6 +45,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||||||
exports.AdminUsersService = void 0;
|
exports.AdminUsersService = void 0;
|
||||||
const common_1 = require("@nestjs/common");
|
const common_1 = require("@nestjs/common");
|
||||||
const prisma_service_1 = require("../prisma/prisma.service");
|
const prisma_service_1 = require("../prisma/prisma.service");
|
||||||
|
const bcrypt = __importStar(require("bcrypt"));
|
||||||
let AdminUsersService = class AdminUsersService {
|
let AdminUsersService = class AdminUsersService {
|
||||||
prisma;
|
prisma;
|
||||||
constructor(prisma) {
|
constructor(prisma) {
|
||||||
@@ -139,6 +173,76 @@ let AdminUsersService = class AdminUsersService {
|
|||||||
suspendedUsers,
|
suspendedUsers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
async create(data) {
|
||||||
|
const existing = await this.prisma.user.findUnique({
|
||||||
|
where: { email: data.email },
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
throw new common_1.ConflictException('User with this email already exists');
|
||||||
|
}
|
||||||
|
const hashedPassword = await bcrypt.hash(data.password, 10);
|
||||||
|
return this.prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: data.email,
|
||||||
|
passwordHash: hashedPassword,
|
||||||
|
name: data.name || null,
|
||||||
|
role: data.role || 'user',
|
||||||
|
emailVerified: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
role: true,
|
||||||
|
emailVerified: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async update(id, data) {
|
||||||
|
const user = await this.prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
throw new common_1.NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
if (data.email && data.email !== user.email) {
|
||||||
|
const existing = await this.prisma.user.findUnique({
|
||||||
|
where: { email: data.email },
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
throw new common_1.ConflictException('Email already in use');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.prisma.user.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
...(data.email && { email: data.email }),
|
||||||
|
...(data.name !== undefined && { name: data.name }),
|
||||||
|
...(data.role && { role: data.role }),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
role: true,
|
||||||
|
emailVerified: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async delete(id) {
|
||||||
|
const user = await this.prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
throw new common_1.NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
await this.prisma.user.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
return { message: 'User deleted successfully' };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
exports.AdminUsersService = AdminUsersService;
|
exports.AdminUsersService = AdminUsersService;
|
||||||
exports.AdminUsersService = AdminUsersService = __decorate([
|
exports.AdminUsersService = AdminUsersService = __decorate([
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
9
apps/api/dist/app.module.js
vendored
9
apps/api/dist/app.module.js
vendored
@@ -42,6 +42,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||||||
exports.AppModule = void 0;
|
exports.AppModule = void 0;
|
||||||
const common_1 = require("@nestjs/common");
|
const common_1 = require("@nestjs/common");
|
||||||
const config_1 = require("@nestjs/config");
|
const config_1 = require("@nestjs/config");
|
||||||
|
const core_1 = require("@nestjs/core");
|
||||||
const path = __importStar(require("path"));
|
const path = __importStar(require("path"));
|
||||||
const prisma_module_1 = require("./prisma/prisma.module");
|
const prisma_module_1 = require("./prisma/prisma.module");
|
||||||
const auth_module_1 = require("./auth/auth.module");
|
const auth_module_1 = require("./auth/auth.module");
|
||||||
@@ -52,6 +53,7 @@ const transactions_module_1 = require("./transactions/transactions.module");
|
|||||||
const categories_module_1 = require("./categories/categories.module");
|
const categories_module_1 = require("./categories/categories.module");
|
||||||
const otp_module_1 = require("./otp/otp.module");
|
const otp_module_1 = require("./otp/otp.module");
|
||||||
const admin_module_1 = require("./admin/admin.module");
|
const admin_module_1 = require("./admin/admin.module");
|
||||||
|
const maintenance_guard_1 = require("./common/guards/maintenance.guard");
|
||||||
let AppModule = class AppModule {
|
let AppModule = class AppModule {
|
||||||
};
|
};
|
||||||
exports.AppModule = AppModule;
|
exports.AppModule = AppModule;
|
||||||
@@ -75,7 +77,12 @@ exports.AppModule = AppModule = __decorate([
|
|||||||
admin_module_1.AdminModule,
|
admin_module_1.AdminModule,
|
||||||
],
|
],
|
||||||
controllers: [health_controller_1.HealthController],
|
controllers: [health_controller_1.HealthController],
|
||||||
providers: [],
|
providers: [
|
||||||
|
{
|
||||||
|
provide: core_1.APP_GUARD,
|
||||||
|
useClass: maintenance_guard_1.MaintenanceGuard,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
], AppModule);
|
], AppModule);
|
||||||
//# sourceMappingURL=app.module.js.map
|
//# sourceMappingURL=app.module.js.map
|
||||||
2
apps/api/dist/app.module.js.map
vendored
2
apps/api/dist/app.module.js.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAwC;AACxC,2CAA8C;AAC9C,2CAA6B;AAC7B,0DAAsD;AACtD,oDAAgD;AAChD,kEAA8D;AAC9D,uDAAmD;AACnD,6DAAyD;AACzD,4EAAwE;AACxE,sEAAkE;AAClE,iDAA6C;AAC7C,uDAAmD;AAuB5C,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IArBrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC;gBACnB,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE;oBACX,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC;oBACnC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC;iBAC1C;aACF,CAAC;YACF,4BAAY;YACZ,wBAAU;YACV,0BAAW;YACX,8BAAa;YACb,wCAAkB;YAClB,oCAAgB;YAChB,sBAAS;YACT,0BAAW;SACZ;QACD,WAAW,EAAE,CAAC,oCAAgB,CAAC;QAC/B,SAAS,EAAE,EAAE;KACd,CAAC;GACW,SAAS,CAAG"}
|
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAwC;AACxC,2CAA8C;AAC9C,uCAAyC;AACzC,2CAA6B;AAC7B,0DAAsD;AACtD,oDAAgD;AAChD,kEAA8D;AAC9D,uDAAmD;AACnD,6DAAyD;AACzD,4EAAwE;AACxE,sEAAkE;AAClE,iDAA6C;AAC7C,uDAAmD;AACnD,yEAAqE;AA4B9D,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IA1BrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC;gBACnB,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE;oBACX,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC;oBACnC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC;iBAC1C;aACF,CAAC;YACF,4BAAY;YACZ,wBAAU;YACV,0BAAW;YACX,8BAAa;YACb,wCAAkB;YAClB,oCAAgB;YAChB,sBAAS;YACT,0BAAW;SACZ;QACD,WAAW,EAAE,CAAC,oCAAgB,CAAC;QAC/B,SAAS,EAAE;YACT;gBACE,OAAO,EAAE,gBAAS;gBAClB,QAAQ,EAAE,oCAAgB;aAC3B;SACF;KACF,CAAC;GACW,SAAS,CAAG"}
|
||||||
6
apps/api/dist/auth/auth.controller.js
vendored
6
apps/api/dist/auth/auth.controller.js
vendored
@@ -17,6 +17,7 @@ const common_1 = require("@nestjs/common");
|
|||||||
const auth_guard_1 = require("./auth.guard");
|
const auth_guard_1 = require("./auth.guard");
|
||||||
const passport_1 = require("@nestjs/passport");
|
const passport_1 = require("@nestjs/passport");
|
||||||
const auth_service_1 = require("./auth.service");
|
const auth_service_1 = require("./auth.service");
|
||||||
|
const skip_maintenance_decorator_1 = require("../common/decorators/skip-maintenance.decorator");
|
||||||
let AuthController = class AuthController {
|
let AuthController = class AuthController {
|
||||||
authService;
|
authService;
|
||||||
constructor(authService) {
|
constructor(authService) {
|
||||||
@@ -53,6 +54,7 @@ let AuthController = class AuthController {
|
|||||||
exports.AuthController = AuthController;
|
exports.AuthController = AuthController;
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.Post)('register'),
|
(0, common_1.Post)('register'),
|
||||||
|
(0, skip_maintenance_decorator_1.SkipMaintenance)(),
|
||||||
__param(0, (0, common_1.Body)()),
|
__param(0, (0, common_1.Body)()),
|
||||||
__metadata("design:type", Function),
|
__metadata("design:type", Function),
|
||||||
__metadata("design:paramtypes", [Object]),
|
__metadata("design:paramtypes", [Object]),
|
||||||
@@ -60,6 +62,7 @@ __decorate([
|
|||||||
], AuthController.prototype, "register", null);
|
], AuthController.prototype, "register", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.Post)('login'),
|
(0, common_1.Post)('login'),
|
||||||
|
(0, skip_maintenance_decorator_1.SkipMaintenance)(),
|
||||||
__param(0, (0, common_1.Body)()),
|
__param(0, (0, common_1.Body)()),
|
||||||
__metadata("design:type", Function),
|
__metadata("design:type", Function),
|
||||||
__metadata("design:paramtypes", [Object]),
|
__metadata("design:paramtypes", [Object]),
|
||||||
@@ -67,6 +70,7 @@ __decorate([
|
|||||||
], AuthController.prototype, "login", null);
|
], AuthController.prototype, "login", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.Post)('verify-otp'),
|
(0, common_1.Post)('verify-otp'),
|
||||||
|
(0, skip_maintenance_decorator_1.SkipMaintenance)(),
|
||||||
__param(0, (0, common_1.Body)()),
|
__param(0, (0, common_1.Body)()),
|
||||||
__metadata("design:type", Function),
|
__metadata("design:type", Function),
|
||||||
__metadata("design:paramtypes", [Object]),
|
__metadata("design:paramtypes", [Object]),
|
||||||
@@ -74,6 +78,7 @@ __decorate([
|
|||||||
], AuthController.prototype, "verifyOtp", null);
|
], AuthController.prototype, "verifyOtp", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.Get)('google'),
|
(0, common_1.Get)('google'),
|
||||||
|
(0, skip_maintenance_decorator_1.SkipMaintenance)(),
|
||||||
(0, common_1.UseGuards)((0, passport_1.AuthGuard)('google')),
|
(0, common_1.UseGuards)((0, passport_1.AuthGuard)('google')),
|
||||||
__metadata("design:type", Function),
|
__metadata("design:type", Function),
|
||||||
__metadata("design:paramtypes", []),
|
__metadata("design:paramtypes", []),
|
||||||
@@ -81,6 +86,7 @@ __decorate([
|
|||||||
], AuthController.prototype, "googleAuth", null);
|
], AuthController.prototype, "googleAuth", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.Get)('google/callback'),
|
(0, common_1.Get)('google/callback'),
|
||||||
|
(0, skip_maintenance_decorator_1.SkipMaintenance)(),
|
||||||
(0, common_1.UseGuards)((0, passport_1.AuthGuard)('google')),
|
(0, common_1.UseGuards)((0, passport_1.AuthGuard)('google')),
|
||||||
__param(0, (0, common_1.Req)()),
|
__param(0, (0, common_1.Req)()),
|
||||||
__param(1, (0, common_1.Res)()),
|
__param(1, (0, common_1.Res)()),
|
||||||
|
|||||||
2
apps/api/dist/auth/auth.controller.js.map
vendored
2
apps/api/dist/auth/auth.controller.js.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"auth.controller.js","sourceRoot":"","sources":["../../src/auth/auth.controller.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"auth.controller.js","sourceRoot":"","sources":["../../src/auth/auth.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAQwB;AACxB,6CAAyD;AACzD,+CAA6C;AAC7C,iDAA6C;AAC7C,gGAAkF;AAW3E,IAAM,cAAc,GAApB,MAAM,cAAc;IACL;IAApB,YAAoB,WAAwB;QAAxB,gBAAW,GAAX,WAAW,CAAa;IAAG,CAAC;IAI1C,AAAN,KAAK,CAAC,QAAQ,CACJ,IAAwD;QAEhE,OAAO,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACzE,CAAC;IAIK,AAAN,KAAK,CAAC,KAAK,CAAS,IAAyC;QAC3D,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC3D,CAAC;IAIK,AAAN,KAAK,CAAC,SAAS,CAEb,IAIC;QAED,OAAO,IAAI,CAAC,WAAW,CAAC,iBAAiB,CACvC,IAAI,CAAC,SAAS,EACd,IAAI,CAAC,OAAO,EACZ,IAAI,CAAC,MAAM,CACZ,CAAC;IACJ,CAAC;IAKK,AAAN,KAAK,CAAC,UAAU;IAEhB,CAAC;IAKK,AAAN,KAAK,CAAC,kBAAkB,CAAQ,GAAQ,EAAS,GAAa;QAE5D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAG5D,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,uBAAuB,CAAC;QAEvE,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;YAEvB,GAAG,CAAC,QAAQ,CACV,GAAG,WAAW,mBAAmB,MAAM,CAAC,SAAS,YAAY,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAAE,CACvG,CAAC;QACJ,CAAC;aAAM,CAAC;YAEN,GAAG,CAAC,QAAQ,CAAC,GAAG,WAAW,wBAAwB,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;IAIK,AAAN,KAAK,CAAC,UAAU,CAAQ,GAAoB;QAC1C,OAAO,IAAI,CAAC,WAAW,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1D,CAAC;IAIK,AAAN,KAAK,CAAC,cAAc,CACX,GAAoB,EAE3B,IAIC;QAED,OAAO,IAAI,CAAC,WAAW,CAAC,cAAc,CACpC,GAAG,CAAC,IAAI,CAAC,MAAM,EACf,IAAI,CAAC,eAAe,EACpB,IAAI,CAAC,WAAW,EAChB,IAAI,CAAC,iBAAiB,CACvB,CAAC;IACJ,CAAC;CACF,CAAA;AAtFY,wCAAc;AAKnB;IAFL,IAAA,aAAI,EAAC,UAAU,CAAC;IAChB,IAAA,4CAAe,GAAE;IAEf,WAAA,IAAA,aAAI,GAAE,CAAA;;;;8CAGR;AAIK;IAFL,IAAA,aAAI,EAAC,OAAO,CAAC;IACb,IAAA,4CAAe,GAAE;IACL,WAAA,IAAA,aAAI,GAAE,CAAA;;;;2CAElB;AAIK;IAFL,IAAA,aAAI,EAAC,YAAY,CAAC;IAClB,IAAA,4CAAe,GAAE;IAEf,WAAA,IAAA,aAAI,GAAE,CAAA;;;;+CAYR;AAKK;IAHL,IAAA,YAAG,EAAC,QAAQ,CAAC;IACb,IAAA,4CAAe,GAAE;IACjB,IAAA,kBAAS,EAAC,IAAA,oBAAS,EAAC,QAAQ,CAAC,CAAC;;;;gDAG9B;AAKK;IAHL,IAAA,YAAG,EAAC,iBAAiB,CAAC;IACtB,IAAA,4CAAe,GAAE;IACjB,IAAA,kBAAS,EAAC,IAAA,oBAAS,EAAC,QAAQ,CAAC,CAAC;IACL,WAAA,IAAA,YAAG,GAAE,CAAA;IAAY,WAAA,IAAA,YAAG,GAAE,CAAA;;;;wDAgB/C;AAIK;IAFL,IAAA,YAAG,EAAC,IAAI,CAAC;IACT,IAAA,kBAAS,EAAC,sBAAY,CAAC;IACN,WAAA,IAAA,YAAG,GAAE,CAAA;;;;gDAEtB;AAIK;IAFL,IAAA,aAAI,EAAC,iBAAiB,CAAC;IACvB,IAAA,kBAAS,EAAC,sBAAY,CAAC;IAErB,WAAA,IAAA,YAAG,GAAE,CAAA;IACL,WAAA,IAAA,aAAI,GAAE,CAAA;;;;oDAaR;yBArFU,cAAc;IAD1B,IAAA,mBAAU,EAAC,MAAM,CAAC;qCAEgB,0BAAW;GADjC,cAAc,CAsF1B"}
|
||||||
2
apps/api/dist/auth/auth.module.js
vendored
2
apps/api/dist/auth/auth.module.js
vendored
@@ -32,7 +32,7 @@ exports.AuthModule = AuthModule = __decorate([
|
|||||||
],
|
],
|
||||||
controllers: [auth_controller_1.AuthController],
|
controllers: [auth_controller_1.AuthController],
|
||||||
providers: [auth_service_1.AuthService, jwt_strategy_1.JwtStrategy, google_strategy_1.GoogleStrategy],
|
providers: [auth_service_1.AuthService, jwt_strategy_1.JwtStrategy, google_strategy_1.GoogleStrategy],
|
||||||
exports: [auth_service_1.AuthService],
|
exports: [auth_service_1.AuthService, jwt_1.JwtModule],
|
||||||
})
|
})
|
||||||
], AuthModule);
|
], AuthModule);
|
||||||
//# sourceMappingURL=auth.module.js.map
|
//# sourceMappingURL=auth.module.js.map
|
||||||
2
apps/api/dist/auth/auth.module.js.map
vendored
2
apps/api/dist/auth/auth.module.js.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"auth.module.js","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAoD;AACpD,qCAAwC;AACxC,+CAAkD;AAClD,uDAAmD;AACnD,iDAA6C;AAC7C,iDAA6C;AAC7C,uDAAmD;AACnD,2DAAuD;AACvD,kDAA8C;AAgBvC,IAAM,UAAU,GAAhB,MAAM,UAAU;CAAG,CAAA;AAAb,gCAAU;qBAAV,UAAU;IAdtB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,4BAAY;YACZ,yBAAc;YACd,IAAA,mBAAU,EAAC,GAAG,EAAE,CAAC,sBAAS,CAAC;YAC3B,eAAS,CAAC,QAAQ,CAAC;gBACjB,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,iBAAiB;gBACnD,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;aACjC,CAAC;SACH;QACD,WAAW,EAAE,CAAC,gCAAc,CAAC;QAC7B,SAAS,EAAE,CAAC,0BAAW,EAAE,0BAAW,EAAE,gCAAc,CAAC;QACrD,OAAO,EAAE,CAAC,0BAAW,CAAC;KACvB,CAAC;GACW,UAAU,CAAG"}
|
{"version":3,"file":"auth.module.js","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAoD;AACpD,qCAAwC;AACxC,+CAAkD;AAClD,uDAAmD;AACnD,iDAA6C;AAC7C,iDAA6C;AAC7C,uDAAmD;AACnD,2DAAuD;AACvD,kDAA8C;AAgBvC,IAAM,UAAU,GAAhB,MAAM,UAAU;CAAG,CAAA;AAAb,gCAAU;qBAAV,UAAU;IAdtB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,4BAAY;YACZ,yBAAc;YACd,IAAA,mBAAU,EAAC,GAAG,EAAE,CAAC,sBAAS,CAAC;YAC3B,eAAS,CAAC,QAAQ,CAAC;gBACjB,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,iBAAiB;gBACnD,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;aACjC,CAAC;SACH;QACD,WAAW,EAAE,CAAC,gCAAc,CAAC;QAC7B,SAAS,EAAE,CAAC,0BAAW,EAAE,0BAAW,EAAE,gCAAc,CAAC;QACrD,OAAO,EAAE,CAAC,0BAAW,EAAE,eAAS,CAAC;KAClC,CAAC;GACW,UAAU,CAAG"}
|
||||||
2
apps/api/dist/auth/jwt.strategy.js
vendored
2
apps/api/dist/auth/jwt.strategy.js
vendored
@@ -25,7 +25,7 @@ let JwtStrategy = class JwtStrategy extends (0, passport_1.PassportStrategy)(pas
|
|||||||
return {
|
return {
|
||||||
userId: payload.sub,
|
userId: payload.sub,
|
||||||
email: payload.email,
|
email: payload.email,
|
||||||
role: payload.role || 'user'
|
role: payload.role || 'user',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"categories.controller.js","sourceRoot":"","sources":["../../src/categories/categories.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAA4F;AAC5F,yEAAqE;AACrE,+EAA0E;AAC1E,mDAA+C;AAUxC,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACF;IAA7B,YAA6B,iBAAoC;QAApC,sBAAiB,GAAjB,iBAAiB,CAAmB;IAAG,CAAC;IAGrE,MAAM,CAAQ,GAAoB,EAAU,iBAAoC;QAC9E,OAAO,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC;YACnC,GAAG,iBAAiB;YACpB,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM;SACxB,CAAC,CAAC;IACL,CAAC;IAGD,OAAO,CAAQ,GAAoB;QACjC,OAAO,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzD,CAAC;IAGD,MAAM,CAAQ,GAAoB,EAAe,EAAU;QACzD,OAAO,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5D,CAAC;CACF,CAAA;AApBY,oDAAoB;AAI/B;IADC,IAAA,aAAI,GAAE;IACC,WAAA,IAAA,YAAG,GAAE,CAAA;IAAwB,WAAA,IAAA,aAAI,GAAE,CAAA;;6CAAoB,uCAAiB;;kDAK/E;AAGD;IADC,IAAA,YAAG,GAAE;IACG,WAAA,IAAA,YAAG,GAAE,CAAA;;;;mDAEb;AAGD;IADC,IAAA,eAAM,EAAC,KAAK,CAAC;IACN,WAAA,IAAA,YAAG,GAAE,CAAA;IAAwB,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;kDAE/C;+BAnBU,oBAAoB;IAFhC,IAAA,mBAAU,EAAC,YAAY,CAAC;IACxB,IAAA,kBAAS,EAAC,sBAAS,CAAC;qCAE6B,sCAAiB;GADtD,oBAAoB,CAoBhC"}
|
{"version":3,"file":"categories.controller.js","sourceRoot":"","sources":["../../src/categories/categories.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CASwB;AACxB,yEAAqE;AACrE,+EAA0E;AAC1E,mDAA+C;AAUxC,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACF;IAA7B,YAA6B,iBAAoC;QAApC,sBAAiB,GAAjB,iBAAiB,CAAmB;IAAG,CAAC;IAGrE,MAAM,CACG,GAAoB,EACnB,iBAAoC;QAE5C,OAAO,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC;YACnC,GAAG,iBAAiB;YACpB,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM;SACxB,CAAC,CAAC;IACL,CAAC;IAGD,OAAO,CAAQ,GAAoB;QACjC,OAAO,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzD,CAAC;IAGD,MAAM,CAAQ,GAAoB,EAAe,EAAU;QACzD,OAAO,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5D,CAAC;CACF,CAAA;AAvBY,oDAAoB;AAI/B;IADC,IAAA,aAAI,GAAE;IAEJ,WAAA,IAAA,YAAG,GAAE,CAAA;IACL,WAAA,IAAA,aAAI,GAAE,CAAA;;6CAAoB,uCAAiB;;kDAM7C;AAGD;IADC,IAAA,YAAG,GAAE;IACG,WAAA,IAAA,YAAG,GAAE,CAAA;;;;mDAEb;AAGD;IADC,IAAA,eAAM,EAAC,KAAK,CAAC;IACN,WAAA,IAAA,YAAG,GAAE,CAAA;IAAwB,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;kDAE/C;+BAtBU,oBAAoB;IAFhC,IAAA,mBAAU,EAAC,YAAY,CAAC;IACxB,IAAA,kBAAS,EAAC,sBAAS,CAAC;qCAE6B,sCAAiB;GADtD,oBAAoB,CAuBhC"}
|
||||||
1
apps/api/dist/common/decorators/skip-maintenance.decorator.d.ts
vendored
Normal file
1
apps/api/dist/common/decorators/skip-maintenance.decorator.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export declare const SkipMaintenance: () => import("@nestjs/common").CustomDecorator<string>;
|
||||||
7
apps/api/dist/common/decorators/skip-maintenance.decorator.js
vendored
Normal file
7
apps/api/dist/common/decorators/skip-maintenance.decorator.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.SkipMaintenance = void 0;
|
||||||
|
const common_1 = require("@nestjs/common");
|
||||||
|
const SkipMaintenance = () => (0, common_1.SetMetadata)('skipMaintenance', true);
|
||||||
|
exports.SkipMaintenance = SkipMaintenance;
|
||||||
|
//# sourceMappingURL=skip-maintenance.decorator.js.map
|
||||||
1
apps/api/dist/common/decorators/skip-maintenance.decorator.js.map
vendored
Normal file
1
apps/api/dist/common/decorators/skip-maintenance.decorator.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"skip-maintenance.decorator.js","sourceRoot":"","sources":["../../../src/common/decorators/skip-maintenance.decorator.ts"],"names":[],"mappings":";;;AAAA,2CAA6C;AAEtC,MAAM,eAAe,GAAG,GAAG,EAAE,CAAC,IAAA,oBAAW,EAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC;AAA7D,QAAA,eAAe,mBAA8C"}
|
||||||
11
apps/api/dist/common/guards/maintenance.guard.d.ts
vendored
Normal file
11
apps/api/dist/common/guards/maintenance.guard.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { PrismaService } from '../../prisma/prisma.service';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
export declare class MaintenanceGuard implements CanActivate {
|
||||||
|
private reflector;
|
||||||
|
private prisma;
|
||||||
|
private jwtService;
|
||||||
|
constructor(reflector: Reflector, prisma: PrismaService, jwtService: JwtService);
|
||||||
|
canActivate(context: ExecutionContext): Promise<boolean>;
|
||||||
|
}
|
||||||
71
apps/api/dist/common/guards/maintenance.guard.js
vendored
Normal file
71
apps/api/dist/common/guards/maintenance.guard.js
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
"use strict";
|
||||||
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||||
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||||
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||||
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||||
|
};
|
||||||
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.MaintenanceGuard = void 0;
|
||||||
|
const common_1 = require("@nestjs/common");
|
||||||
|
const core_1 = require("@nestjs/core");
|
||||||
|
const prisma_service_1 = require("../../prisma/prisma.service");
|
||||||
|
const jwt_1 = require("@nestjs/jwt");
|
||||||
|
let MaintenanceGuard = class MaintenanceGuard {
|
||||||
|
reflector;
|
||||||
|
prisma;
|
||||||
|
jwtService;
|
||||||
|
constructor(reflector, prisma, jwtService) {
|
||||||
|
this.reflector = reflector;
|
||||||
|
this.prisma = prisma;
|
||||||
|
this.jwtService = jwtService;
|
||||||
|
}
|
||||||
|
async canActivate(context) {
|
||||||
|
const isExempt = this.reflector.get('skipMaintenance', context.getHandler());
|
||||||
|
const isControllerExempt = this.reflector.get('skipMaintenance', context.getClass());
|
||||||
|
if (isExempt || isControllerExempt) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const maintenanceConfig = await this.prisma.appConfig.findUnique({
|
||||||
|
where: { key: 'maintenance_mode' },
|
||||||
|
});
|
||||||
|
const isMaintenanceMode = maintenanceConfig?.value === 'true';
|
||||||
|
if (!isMaintenanceMode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const authHeader = request.headers.authorization;
|
||||||
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||||
|
try {
|
||||||
|
const token = authHeader.substring(7);
|
||||||
|
const payload = this.jwtService.verify(token);
|
||||||
|
if (payload.role === 'admin') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const messageConfig = await this.prisma.appConfig.findUnique({
|
||||||
|
where: { key: 'maintenance_message' },
|
||||||
|
});
|
||||||
|
const message = messageConfig?.value ||
|
||||||
|
'System is under maintenance. Please try again later.';
|
||||||
|
throw new common_1.ServiceUnavailableException({
|
||||||
|
statusCode: 503,
|
||||||
|
message: message,
|
||||||
|
maintenanceMode: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exports.MaintenanceGuard = MaintenanceGuard;
|
||||||
|
exports.MaintenanceGuard = MaintenanceGuard = __decorate([
|
||||||
|
(0, common_1.Injectable)(),
|
||||||
|
__metadata("design:paramtypes", [core_1.Reflector,
|
||||||
|
prisma_service_1.PrismaService,
|
||||||
|
jwt_1.JwtService])
|
||||||
|
], MaintenanceGuard);
|
||||||
|
//# sourceMappingURL=maintenance.guard.js.map
|
||||||
1
apps/api/dist/common/guards/maintenance.guard.js.map
vendored
Normal file
1
apps/api/dist/common/guards/maintenance.guard.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"maintenance.guard.js","sourceRoot":"","sources":["../../../src/common/guards/maintenance.guard.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAKwB;AACxB,uCAAyC;AACzC,gEAA4D;AAC5D,qCAAyC;AAGlC,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IAEjB;IACA;IACA;IAHV,YACU,SAAoB,EACpB,MAAqB,EACrB,UAAsB;QAFtB,cAAS,GAAT,SAAS,CAAW;QACpB,WAAM,GAAN,MAAM,CAAe;QACrB,eAAU,GAAV,UAAU,CAAY;IAC7B,CAAC;IAEJ,KAAK,CAAC,WAAW,CAAC,OAAyB;QAEzC,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAU,iBAAiB,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;QACtF,MAAM,kBAAkB,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAU,iBAAiB,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;QAE9F,IAAI,QAAQ,IAAI,kBAAkB,EAAE,CAAC;YACnC,OAAO,IAAI,CAAC;QACd,CAAC;QAGD,MAAM,iBAAiB,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;YAC/D,KAAK,EAAE,EAAE,GAAG,EAAE,kBAAkB,EAAE;SACnC,CAAC,CAAC;QAEH,MAAM,iBAAiB,GAAG,iBAAiB,EAAE,KAAK,KAAK,MAAM,CAAC;QAE9D,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QAGD,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAE,CAAC;QACpD,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC;QAEjD,IAAI,UAAU,IAAI,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACnD,IAAI,CAAC;gBACH,MAAM,KAAK,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;gBACtC,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAG9C,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;oBAC7B,OAAO,IAAI,CAAC;gBACd,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;YAEjB,CAAC;QACH,CAAC;QAGD,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;YAC3D,KAAK,EAAE,EAAE,GAAG,EAAE,qBAAqB,EAAE;SACtC,CAAC,CAAC;QAEH,MAAM,OAAO,GACX,aAAa,EAAE,KAAK;YACpB,sDAAsD,CAAC;QAEzD,MAAM,IAAI,oCAA2B,CAAC;YACpC,UAAU,EAAE,GAAG;YACf,OAAO,EAAE,OAAO;YAChB,eAAe,EAAE,IAAI;SACtB,CAAC,CAAC;IACL,CAAC;CACF,CAAA;AA5DY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,mBAAU,GAAE;qCAGU,gBAAS;QACZ,8BAAa;QACT,gBAAU;GAJrB,gBAAgB,CA4D5B"}
|
||||||
2
apps/api/dist/health/health.controller.js
vendored
2
apps/api/dist/health/health.controller.js
vendored
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||||||
exports.HealthController = void 0;
|
exports.HealthController = void 0;
|
||||||
const common_1 = require("@nestjs/common");
|
const common_1 = require("@nestjs/common");
|
||||||
const prisma_service_1 = require("../prisma/prisma.service");
|
const prisma_service_1 = require("../prisma/prisma.service");
|
||||||
|
const skip_maintenance_decorator_1 = require("../common/decorators/skip-maintenance.decorator");
|
||||||
let HealthController = class HealthController {
|
let HealthController = class HealthController {
|
||||||
prisma;
|
prisma;
|
||||||
constructor(prisma) {
|
constructor(prisma) {
|
||||||
@@ -40,6 +41,7 @@ __decorate([
|
|||||||
], HealthController.prototype, "db", null);
|
], HealthController.prototype, "db", null);
|
||||||
exports.HealthController = HealthController = __decorate([
|
exports.HealthController = HealthController = __decorate([
|
||||||
(0, common_1.Controller)('health'),
|
(0, common_1.Controller)('health'),
|
||||||
|
(0, skip_maintenance_decorator_1.SkipMaintenance)(),
|
||||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||||
], HealthController);
|
], HealthController);
|
||||||
//# sourceMappingURL=health.controller.js.map
|
//# sourceMappingURL=health.controller.js.map
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"health.controller.js","sourceRoot":"","sources":["../../src/health/health.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAiD;AACjD,6DAAyD;AAGlD,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IACE;IAA7B,YAA6B,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAGtD,EAAE;QACA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC1B,CAAC;IAGK,AAAN,KAAK,CAAC,EAAE;QAEN,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAA,UAAU,CAAC;QACtC,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC;IAC7B,CAAC;CACF,CAAA;AAdY,4CAAgB;AAI3B;IADC,IAAA,YAAG,GAAE;;;;0CAGL;AAGK;IADL,IAAA,YAAG,EAAC,IAAI,CAAC;;;;0CAKT;2BAbU,gBAAgB;IAD5B,IAAA,mBAAU,EAAC,QAAQ,CAAC;qCAEkB,8BAAa;GADvC,gBAAgB,CAc5B"}
|
{"version":3,"file":"health.controller.js","sourceRoot":"","sources":["../../src/health/health.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAiD;AACjD,6DAAyD;AACzD,gGAAkF;AAI3E,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IACE;IAA7B,YAA6B,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAGtD,EAAE;QACA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC1B,CAAC;IAGK,AAAN,KAAK,CAAC,EAAE;QAEN,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAA,UAAU,CAAC;QACtC,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC;IAC7B,CAAC;CACF,CAAA;AAdY,4CAAgB;AAI3B;IADC,IAAA,YAAG,GAAE;;;;0CAGL;AAGK;IADL,IAAA,YAAG,EAAC,IAAI,CAAC;;;;0CAKT;2BAbU,gBAAgB;IAF5B,IAAA,mBAAU,EAAC,QAAQ,CAAC;IACpB,IAAA,4CAAe,GAAE;qCAEqB,8BAAa;GADvC,gBAAgB,CAc5B"}
|
||||||
2
apps/api/dist/otp/otp.service.js.map
vendored
2
apps/api/dist/otp/otp.service.js.map
vendored
File diff suppressed because one or more lines are too long
54
apps/api/dist/seed.js
vendored
54
apps/api/dist/seed.js
vendored
@@ -107,10 +107,22 @@ async function main() {
|
|||||||
features: {
|
features: {
|
||||||
wallets: { limit: null, label: 'Unlimited wallets' },
|
wallets: { limit: null, label: 'Unlimited wallets' },
|
||||||
goals: { limit: null, label: 'Unlimited goals' },
|
goals: { limit: null, label: 'Unlimited goals' },
|
||||||
team: { enabled: true, maxMembers: 10, label: 'Team feature (10 members)' },
|
team: {
|
||||||
api: { enabled: true, rateLimit: 1000, label: 'API access (1000 req/hr)' },
|
enabled: true,
|
||||||
|
maxMembers: 10,
|
||||||
|
label: 'Team feature (10 members)',
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
enabled: true,
|
||||||
|
rateLimit: 1000,
|
||||||
|
label: 'API access (1000 req/hr)',
|
||||||
|
},
|
||||||
support: { level: 'priority', label: 'Priority support' },
|
support: { level: 'priority', label: 'Priority support' },
|
||||||
export: { enabled: true, formats: ['csv', 'excel', 'pdf'], label: 'All export formats' },
|
export: {
|
||||||
|
enabled: true,
|
||||||
|
formats: ['csv', 'excel', 'pdf'],
|
||||||
|
label: 'All export formats',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
badge: 'Popular',
|
badge: 'Popular',
|
||||||
badgeColor: 'blue',
|
badgeColor: 'blue',
|
||||||
@@ -141,10 +153,22 @@ async function main() {
|
|||||||
features: {
|
features: {
|
||||||
wallets: { limit: null, label: 'Unlimited wallets' },
|
wallets: { limit: null, label: 'Unlimited wallets' },
|
||||||
goals: { limit: null, label: 'Unlimited goals' },
|
goals: { limit: null, label: 'Unlimited goals' },
|
||||||
team: { enabled: true, maxMembers: 10, label: 'Team feature (10 members)' },
|
team: {
|
||||||
api: { enabled: true, rateLimit: 1000, label: 'API access (1000 req/hr)' },
|
enabled: true,
|
||||||
|
maxMembers: 10,
|
||||||
|
label: 'Team feature (10 members)',
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
enabled: true,
|
||||||
|
rateLimit: 1000,
|
||||||
|
label: 'API access (1000 req/hr)',
|
||||||
|
},
|
||||||
support: { level: 'priority', label: 'Priority support' },
|
support: { level: 'priority', label: 'Priority support' },
|
||||||
export: { enabled: true, formats: ['csv', 'excel', 'pdf'], label: 'All export formats' },
|
export: {
|
||||||
|
enabled: true,
|
||||||
|
formats: ['csv', 'excel', 'pdf'],
|
||||||
|
label: 'All export formats',
|
||||||
|
},
|
||||||
discount: { value: '17%', label: 'Save 17% (2 months free)' },
|
discount: { value: '17%', label: 'Save 17% (2 months free)' },
|
||||||
},
|
},
|
||||||
badge: 'Best Value',
|
badge: 'Best Value',
|
||||||
@@ -161,7 +185,11 @@ async function main() {
|
|||||||
apiRateLimit: 1000,
|
apiRateLimit: 1000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log('✅ Plans created:', [freePlan.name, proMonthly.name, proYearly.name]);
|
console.log('✅ Plans created:', [
|
||||||
|
freePlan.name,
|
||||||
|
proMonthly.name,
|
||||||
|
proYearly.name,
|
||||||
|
]);
|
||||||
console.log('\n💳 Creating default payment methods...');
|
console.log('\n💳 Creating default payment methods...');
|
||||||
const bcaMethod = await prisma.paymentMethod.upsert({
|
const bcaMethod = await prisma.paymentMethod.upsert({
|
||||||
where: { id: 'bca-method' },
|
where: { id: 'bca-method' },
|
||||||
@@ -211,7 +239,11 @@ async function main() {
|
|||||||
sortOrder: 3,
|
sortOrder: 3,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log('✅ Payment methods created:', [bcaMethod.displayName, mandiriMethod.displayName, gopayMethod.displayName]);
|
console.log('✅ Payment methods created:', [
|
||||||
|
bcaMethod.displayName,
|
||||||
|
mandiriMethod.displayName,
|
||||||
|
gopayMethod.displayName,
|
||||||
|
]);
|
||||||
console.log('\n⚙️ Creating app config...');
|
console.log('\n⚙️ Creating app config...');
|
||||||
await prisma.appConfig.upsert({
|
await prisma.appConfig.upsert({
|
||||||
where: { key: 'MAINTENANCE_MODE' },
|
where: { key: 'MAINTENANCE_MODE' },
|
||||||
@@ -253,7 +285,11 @@ async function main() {
|
|||||||
console.log(' Admin Email:', ADMIN_EMAIL);
|
console.log(' Admin Email:', ADMIN_EMAIL);
|
||||||
console.log(' Admin Password:', ADMIN_PASSWORD);
|
console.log(' Admin Password:', ADMIN_PASSWORD);
|
||||||
console.log(' Plans:', [freePlan.name, proMonthly.name, proYearly.name].join(', '));
|
console.log(' Plans:', [freePlan.name, proMonthly.name, proYearly.name].join(', '));
|
||||||
console.log(' Payment Methods:', [bcaMethod.displayName, mandiriMethod.displayName, gopayMethod.displayName].join(', '));
|
console.log(' Payment Methods:', [
|
||||||
|
bcaMethod.displayName,
|
||||||
|
mandiriMethod.displayName,
|
||||||
|
gopayMethod.displayName,
|
||||||
|
].join(', '));
|
||||||
console.log('\n⚠️ IMPORTANT: Change admin password after first login!');
|
console.log('\n⚠️ IMPORTANT: Change admin password after first login!');
|
||||||
console.log('\n🔗 Login at: http://localhost:5174/auth/login');
|
console.log('\n🔗 Login at: http://localhost:5174/auth/login');
|
||||||
}
|
}
|
||||||
|
|||||||
2
apps/api/dist/seed.js.map
vendored
2
apps/api/dist/seed.js.map
vendored
File diff suppressed because one or more lines are too long
2
apps/api/dist/tsconfig.build.tsbuildinfo
vendored
2
apps/api/dist/tsconfig.build.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
2
apps/api/dist/users/users.controller.js.map
vendored
2
apps/api/dist/users/users.controller.js.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"users.controller.js","sourceRoot":"","sources":["../../src/users/users.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAoF;AACpF,mDAA+C;AAC/C,mDAA+C;AAWxC,IAAM,eAAe,GAArB,MAAM,eAAe;IACG;IAA7B,YAA6B,KAAmB;QAAnB,UAAK,GAAL,KAAK,CAAc;IAAG,CAAC;IAGpD,EAAE;QACA,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;IACzB,CAAC;IAGK,AAAN,KAAK,CAAC,aAAa,CACV,GAAoB,EACnB,IAAuC;QAE/C,OAAO,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACzD,CAAC;IAGK,AAAN,KAAK,CAAC,WAAW,CAAQ,GAAoB;QAC3C,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACjD,CAAC;IAGK,AAAN,KAAK,CAAC,aAAa,CACV,GAAoB,EACnB,IAA0B;QAElC,OAAO,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClE,CAAC;CACF,CAAA;AA5BY,0CAAe;AAI1B;IADC,IAAA,YAAG,EAAC,IAAI,CAAC;;;;yCAGT;AAGK;IADL,IAAA,YAAG,EAAC,SAAS,CAAC;IAEZ,WAAA,IAAA,YAAG,GAAE,CAAA;IACL,WAAA,IAAA,aAAI,GAAE,CAAA;;;;oDAGR;AAGK;IADL,IAAA,YAAG,EAAC,WAAW,CAAC;IACE,WAAA,IAAA,YAAG,GAAE,CAAA;;;;kDAEvB;AAGK;IADL,IAAA,eAAM,EAAC,SAAS,CAAC;IAEf,WAAA,IAAA,YAAG,GAAE,CAAA;IACL,WAAA,IAAA,aAAI,GAAE,CAAA;;;;oDAGR;0BA3BU,eAAe;IAF3B,IAAA,mBAAU,EAAC,OAAO,CAAC;IACnB,IAAA,kBAAS,EAAC,sBAAS,CAAC;qCAEiB,4BAAY;GADrC,eAAe,CA4B3B"}
|
{"version":3,"file":"users.controller.js","sourceRoot":"","sources":["../../src/users/users.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAQwB;AACxB,mDAA+C;AAC/C,mDAA+C;AAWxC,IAAM,eAAe,GAArB,MAAM,eAAe;IACG;IAA7B,YAA6B,KAAmB;QAAnB,UAAK,GAAL,KAAK,CAAc;IAAG,CAAC;IAGpD,EAAE;QACA,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;IACzB,CAAC;IAGK,AAAN,KAAK,CAAC,aAAa,CACV,GAAoB,EACnB,IAAuC;QAE/C,OAAO,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACzD,CAAC;IAGK,AAAN,KAAK,CAAC,WAAW,CAAQ,GAAoB;QAC3C,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACjD,CAAC;IAGK,AAAN,KAAK,CAAC,aAAa,CACV,GAAoB,EACnB,IAA0B;QAElC,OAAO,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClE,CAAC;CACF,CAAA;AA5BY,0CAAe;AAI1B;IADC,IAAA,YAAG,EAAC,IAAI,CAAC;;;;yCAGT;AAGK;IADL,IAAA,YAAG,EAAC,SAAS,CAAC;IAEZ,WAAA,IAAA,YAAG,GAAE,CAAA;IACL,WAAA,IAAA,aAAI,GAAE,CAAA;;;;oDAGR;AAGK;IADL,IAAA,YAAG,EAAC,WAAW,CAAC;IACE,WAAA,IAAA,YAAG,GAAE,CAAA;;;;kDAEvB;AAGK;IADL,IAAA,eAAM,EAAC,SAAS,CAAC;IAEf,WAAA,IAAA,YAAG,GAAE,CAAA;IACL,WAAA,IAAA,aAAI,GAAE,CAAA;;;;oDAGR;0BA3BU,eAAe;IAF3B,IAAA,mBAAU,EAAC,OAAO,CAAC;IACnB,IAAA,kBAAS,EAAC,sBAAS,CAAC;qCAEiB,4BAAY;GADrC,eAAe,CA4B3B"}
|
||||||
2
apps/api/dist/users/users.service.js.map
vendored
2
apps/api/dist/users/users.service.js.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"users.service.js","sourceRoot":"","sources":["../../src/users/users.service.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"users.service.js","sourceRoot":"","sources":["../../src/users/users.service.ts"],"names":[],"mappings":"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"}
|
||||||
28
apps/api/dist/wallets/wallets.controller.d.ts
vendored
28
apps/api/dist/wallets/wallets.controller.d.ts
vendored
@@ -11,11 +11,11 @@ export declare class WalletsController {
|
|||||||
constructor(wallets: WalletsService, transactions: TransactionsService);
|
constructor(wallets: WalletsService, transactions: TransactionsService);
|
||||||
list(req: RequestWithUser): import("@prisma/client").Prisma.PrismaPromise<{
|
list(req: RequestWithUser): import("@prisma/client").Prisma.PrismaPromise<{
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
kind: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
kind: string;
|
||||||
currency: string | null;
|
currency: string | null;
|
||||||
unit: string | null;
|
unit: string | null;
|
||||||
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
@@ -23,15 +23,15 @@ export declare class WalletsController {
|
|||||||
deletedAt: Date | null;
|
deletedAt: Date | null;
|
||||||
}[]>;
|
}[]>;
|
||||||
getAllTransactions(req: RequestWithUser): Promise<{
|
getAllTransactions(req: RequestWithUser): Promise<{
|
||||||
|
category: string | null;
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
walletId: string;
|
userId: string;
|
||||||
date: Date;
|
|
||||||
amount: import("@prisma/client/runtime/library").Decimal;
|
amount: import("@prisma/client/runtime/library").Decimal;
|
||||||
direction: string;
|
direction: string;
|
||||||
category: string | null;
|
date: Date;
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
|
walletId: string;
|
||||||
recurrenceId: string | null;
|
recurrenceId: string | null;
|
||||||
}[]>;
|
}[]>;
|
||||||
create(req: RequestWithUser, body: {
|
create(req: RequestWithUser, body: {
|
||||||
@@ -43,11 +43,11 @@ export declare class WalletsController {
|
|||||||
pricePerUnit?: number;
|
pricePerUnit?: number;
|
||||||
}): import("@prisma/client").Prisma.Prisma__WalletClient<{
|
}): import("@prisma/client").Prisma.Prisma__WalletClient<{
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
kind: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
kind: string;
|
||||||
currency: string | null;
|
currency: string | null;
|
||||||
unit: string | null;
|
unit: string | null;
|
||||||
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
@@ -65,11 +65,11 @@ export declare class WalletsController {
|
|||||||
pricePerUnit?: number;
|
pricePerUnit?: number;
|
||||||
}): import("@prisma/client").Prisma.Prisma__WalletClient<{
|
}): import("@prisma/client").Prisma.Prisma__WalletClient<{
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
kind: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
kind: string;
|
||||||
currency: string | null;
|
currency: string | null;
|
||||||
unit: string | null;
|
unit: string | null;
|
||||||
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
@@ -86,11 +86,11 @@ export declare class WalletsController {
|
|||||||
updated: number;
|
updated: number;
|
||||||
wallets: {
|
wallets: {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
kind: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
kind: string;
|
||||||
currency: string | null;
|
currency: string | null;
|
||||||
unit: string | null;
|
unit: string | null;
|
||||||
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
@@ -102,11 +102,11 @@ export declare class WalletsController {
|
|||||||
};
|
};
|
||||||
delete(req: RequestWithUser, id: string): import("@prisma/client").Prisma.Prisma__WalletClient<{
|
delete(req: RequestWithUser, id: string): import("@prisma/client").Prisma.Prisma__WalletClient<{
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
kind: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
kind: string;
|
||||||
currency: string | null;
|
currency: string | null;
|
||||||
unit: string | null;
|
unit: string | null;
|
||||||
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
|
|||||||
20
apps/api/dist/wallets/wallets.service.d.ts
vendored
20
apps/api/dist/wallets/wallets.service.d.ts
vendored
@@ -4,11 +4,11 @@ export declare class WalletsService {
|
|||||||
constructor(prisma: PrismaService);
|
constructor(prisma: PrismaService);
|
||||||
list(userId: string): import("@prisma/client").Prisma.PrismaPromise<{
|
list(userId: string): import("@prisma/client").Prisma.PrismaPromise<{
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
kind: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
kind: string;
|
||||||
currency: string | null;
|
currency: string | null;
|
||||||
unit: string | null;
|
unit: string | null;
|
||||||
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
@@ -24,11 +24,11 @@ export declare class WalletsService {
|
|||||||
pricePerUnit?: number;
|
pricePerUnit?: number;
|
||||||
}): import("@prisma/client").Prisma.Prisma__WalletClient<{
|
}): import("@prisma/client").Prisma.Prisma__WalletClient<{
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
kind: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
kind: string;
|
||||||
currency: string | null;
|
currency: string | null;
|
||||||
unit: string | null;
|
unit: string | null;
|
||||||
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
@@ -44,11 +44,11 @@ export declare class WalletsService {
|
|||||||
pricePerUnit?: number;
|
pricePerUnit?: number;
|
||||||
}): import("@prisma/client").Prisma.Prisma__WalletClient<{
|
}): import("@prisma/client").Prisma.Prisma__WalletClient<{
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
kind: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
kind: string;
|
||||||
currency: string | null;
|
currency: string | null;
|
||||||
unit: string | null;
|
unit: string | null;
|
||||||
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
@@ -63,11 +63,11 @@ export declare class WalletsService {
|
|||||||
updated: number;
|
updated: number;
|
||||||
wallets: {
|
wallets: {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
kind: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
kind: string;
|
||||||
currency: string | null;
|
currency: string | null;
|
||||||
unit: string | null;
|
unit: string | null;
|
||||||
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
@@ -77,11 +77,11 @@ export declare class WalletsService {
|
|||||||
}>;
|
}>;
|
||||||
delete(userId: string, id: string): import("@prisma/client").Prisma.Prisma__WalletClient<{
|
delete(userId: string, id: string): import("@prisma/client").Prisma.Prisma__WalletClient<{
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
kind: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
kind: string;
|
||||||
currency: string | null;
|
currency: string | null;
|
||||||
unit: string | null;
|
unit: string | null;
|
||||||
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
initialAmount: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { AuthGuard } from '../auth/auth.guard';
|
import { AuthGuard } from '../auth/auth.guard';
|
||||||
import { AdminGuard } from './guards/admin.guard';
|
import { AdminGuard } from './guards/admin.guard';
|
||||||
import { AdminConfigService } from './admin-config.service';
|
import { AdminConfigService } from './admin-config.service';
|
||||||
|
import { SkipMaintenance } from '../common/decorators/skip-maintenance.decorator';
|
||||||
|
|
||||||
interface RequestWithUser {
|
interface RequestWithUser {
|
||||||
user: {
|
user: {
|
||||||
@@ -21,6 +22,7 @@ interface RequestWithUser {
|
|||||||
|
|
||||||
@Controller('admin/config')
|
@Controller('admin/config')
|
||||||
@UseGuards(AuthGuard, AdminGuard)
|
@UseGuards(AuthGuard, AdminGuard)
|
||||||
|
@SkipMaintenance()
|
||||||
export class AdminConfigController {
|
export class AdminConfigController {
|
||||||
constructor(private readonly service: AdminConfigService) {}
|
constructor(private readonly service: AdminConfigService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -42,15 +42,18 @@ export class AdminConfigService {
|
|||||||
|
|
||||||
async getByCategory() {
|
async getByCategory() {
|
||||||
const configs = await this.prisma.appConfig.findMany();
|
const configs = await this.prisma.appConfig.findMany();
|
||||||
|
|
||||||
// Group by category
|
// Group by category
|
||||||
const grouped = configs.reduce((acc, config) => {
|
const grouped = configs.reduce(
|
||||||
if (!acc[config.category]) {
|
(acc, config) => {
|
||||||
acc[config.category] = [];
|
if (!acc[config.category]) {
|
||||||
}
|
acc[config.category] = [];
|
||||||
acc[config.category].push(config);
|
}
|
||||||
return acc;
|
acc[config.category].push(config);
|
||||||
}, {} as Record<string, any[]>);
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, any[]>,
|
||||||
|
);
|
||||||
|
|
||||||
return grouped;
|
return grouped;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import {
|
|||||||
import { AuthGuard } from '../auth/auth.guard';
|
import { AuthGuard } from '../auth/auth.guard';
|
||||||
import { AdminGuard } from './guards/admin.guard';
|
import { AdminGuard } from './guards/admin.guard';
|
||||||
import { AdminPaymentMethodsService } from './admin-payment-methods.service';
|
import { AdminPaymentMethodsService } from './admin-payment-methods.service';
|
||||||
|
import { SkipMaintenance } from '../common/decorators/skip-maintenance.decorator';
|
||||||
|
|
||||||
@Controller('admin/payment-methods')
|
@Controller('admin/payment-methods')
|
||||||
@UseGuards(AuthGuard, AdminGuard)
|
@UseGuards(AuthGuard, AdminGuard)
|
||||||
|
@SkipMaintenance()
|
||||||
export class AdminPaymentMethodsController {
|
export class AdminPaymentMethodsController {
|
||||||
constructor(private readonly service: AdminPaymentMethodsService) {}
|
constructor(private readonly service: AdminPaymentMethodsService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ export class AdminPaymentMethodsService {
|
|||||||
this.prisma.paymentMethod.update({
|
this.prisma.paymentMethod.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { sortOrder: index + 1 },
|
data: { sortOrder: index + 1 },
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.prisma.$transaction(updates);
|
await this.prisma.$transaction(updates);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { AuthGuard } from '../auth/auth.guard';
|
import { AuthGuard } from '../auth/auth.guard';
|
||||||
import { AdminGuard } from './guards/admin.guard';
|
import { AdminGuard } from './guards/admin.guard';
|
||||||
import { AdminPaymentsService } from './admin-payments.service';
|
import { AdminPaymentsService } from './admin-payments.service';
|
||||||
|
import { SkipMaintenance } from '../common/decorators/skip-maintenance.decorator';
|
||||||
|
|
||||||
interface RequestWithUser {
|
interface RequestWithUser {
|
||||||
user: {
|
user: {
|
||||||
@@ -20,6 +21,7 @@ interface RequestWithUser {
|
|||||||
|
|
||||||
@Controller('admin/payments')
|
@Controller('admin/payments')
|
||||||
@UseGuards(AuthGuard, AdminGuard)
|
@UseGuards(AuthGuard, AdminGuard)
|
||||||
|
@SkipMaintenance()
|
||||||
export class AdminPaymentsController {
|
export class AdminPaymentsController {
|
||||||
constructor(private readonly service: AdminPaymentsService) {}
|
constructor(private readonly service: AdminPaymentsService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export class AdminPaymentsService {
|
|||||||
const plan = payment.subscription.plan;
|
const plan = payment.subscription.plan;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const endDate = new Date(now);
|
const endDate = new Date(now);
|
||||||
|
|
||||||
if (plan.durationDays) {
|
if (plan.durationDays) {
|
||||||
endDate.setDate(endDate.getDate() + plan.durationDays);
|
endDate.setDate(endDate.getDate() + plan.durationDays);
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,8 @@ export class AdminPaymentsService {
|
|||||||
data: {
|
data: {
|
||||||
status: 'active',
|
status: 'active',
|
||||||
startDate: now,
|
startDate: now,
|
||||||
endDate: plan.durationType === 'lifetime' ? new Date('2099-12-31') : endDate,
|
endDate:
|
||||||
|
plan.durationType === 'lifetime' ? new Date('2099-12-31') : endDate,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -127,18 +128,32 @@ export class AdminPaymentsService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Group by month
|
// Group by month
|
||||||
const monthlyData: { [key: string]: { revenue: number; count: number } } = {};
|
const monthlyData: { [key: string]: { revenue: number; count: number } } =
|
||||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
{};
|
||||||
|
const months = [
|
||||||
|
'Jan',
|
||||||
|
'Feb',
|
||||||
|
'Mar',
|
||||||
|
'Apr',
|
||||||
|
'May',
|
||||||
|
'Jun',
|
||||||
|
'Jul',
|
||||||
|
'Aug',
|
||||||
|
'Sep',
|
||||||
|
'Oct',
|
||||||
|
'Nov',
|
||||||
|
'Dec',
|
||||||
|
];
|
||||||
|
|
||||||
payments.forEach((payment) => {
|
payments.forEach((payment) => {
|
||||||
if (payment.paidAt) {
|
if (payment.paidAt) {
|
||||||
const date = new Date(payment.paidAt);
|
const date = new Date(payment.paidAt);
|
||||||
const monthKey = `${months[date.getMonth()]} ${date.getFullYear()}`;
|
const monthKey = `${months[date.getMonth()]} ${date.getFullYear()}`;
|
||||||
|
|
||||||
if (!monthlyData[monthKey]) {
|
if (!monthlyData[monthKey]) {
|
||||||
monthlyData[monthKey] = { revenue: 0, count: 0 };
|
monthlyData[monthKey] = { revenue: 0, count: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
monthlyData[monthKey].revenue += Number(payment.amount);
|
monthlyData[monthKey].revenue += Number(payment.amount);
|
||||||
monthlyData[monthKey].count += 1;
|
monthlyData[monthKey].count += 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import {
|
|||||||
import { AuthGuard } from '../auth/auth.guard';
|
import { AuthGuard } from '../auth/auth.guard';
|
||||||
import { AdminGuard } from './guards/admin.guard';
|
import { AdminGuard } from './guards/admin.guard';
|
||||||
import { AdminPlansService } from './admin-plans.service';
|
import { AdminPlansService } from './admin-plans.service';
|
||||||
|
import { SkipMaintenance } from '../common/decorators/skip-maintenance.decorator';
|
||||||
|
|
||||||
@Controller('admin/plans')
|
@Controller('admin/plans')
|
||||||
@UseGuards(AuthGuard, AdminGuard)
|
@UseGuards(AuthGuard, AdminGuard)
|
||||||
|
@SkipMaintenance()
|
||||||
export class AdminPlansController {
|
export class AdminPlansController {
|
||||||
constructor(private readonly plansService: AdminPlansService) {}
|
constructor(private readonly plansService: AdminPlansService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -86,9 +86,9 @@ export class AdminPlansService {
|
|||||||
this.prisma.plan.update({
|
this.prisma.plan.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { sortOrder: index + 1 },
|
data: { sortOrder: index + 1 },
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.prisma.$transaction(updates);
|
await this.prisma.$transaction(updates);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
|
Delete,
|
||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
Query,
|
Query,
|
||||||
@@ -11,9 +12,11 @@ import {
|
|||||||
import { AuthGuard } from '../auth/auth.guard';
|
import { AuthGuard } from '../auth/auth.guard';
|
||||||
import { AdminGuard } from './guards/admin.guard';
|
import { AdminGuard } from './guards/admin.guard';
|
||||||
import { AdminUsersService } from './admin-users.service';
|
import { AdminUsersService } from './admin-users.service';
|
||||||
|
import { SkipMaintenance } from '../common/decorators/skip-maintenance.decorator';
|
||||||
|
|
||||||
@Controller('admin/users')
|
@Controller('admin/users')
|
||||||
@UseGuards(AuthGuard, AdminGuard)
|
@UseGuards(AuthGuard, AdminGuard)
|
||||||
|
@SkipMaintenance()
|
||||||
export class AdminUsersController {
|
export class AdminUsersController {
|
||||||
constructor(private readonly service: AdminUsersService) {}
|
constructor(private readonly service: AdminUsersService) {}
|
||||||
|
|
||||||
@@ -54,4 +57,30 @@ export class AdminUsersController {
|
|||||||
) {
|
) {
|
||||||
return this.service.grantProAccess(id, body.planSlug, body.durationDays);
|
return this.service.grantProAccess(id, body.planSlug, body.durationDays);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(
|
||||||
|
@Body()
|
||||||
|
body: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
name?: string;
|
||||||
|
role?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return this.service.create(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
update(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: { email?: string; name?: string; role?: string },
|
||||||
|
) {
|
||||||
|
return this.service.update(id, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
delete(@Param('id') id: string) {
|
||||||
|
return this.service.delete(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import {
|
||||||
|
Injectable,
|
||||||
|
ConflictException,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminUsersService {
|
export class AdminUsersService {
|
||||||
@@ -140,4 +145,102 @@ export class AdminUsersService {
|
|||||||
suspendedUsers,
|
suspendedUsers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async create(data: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
name?: string;
|
||||||
|
role?: string;
|
||||||
|
}) {
|
||||||
|
// Check if user already exists
|
||||||
|
const existing = await this.prisma.user.findUnique({
|
||||||
|
where: { email: data.email },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
throw new ConflictException('User with this email already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
const hashedPassword = await bcrypt.hash(data.password, 10);
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
return this.prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: data.email,
|
||||||
|
passwordHash: hashedPassword,
|
||||||
|
name: data.name || null,
|
||||||
|
role: data.role || 'user',
|
||||||
|
emailVerified: true, // Admin-created users are auto-verified
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
role: true,
|
||||||
|
emailVerified: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
id: string,
|
||||||
|
data: { email?: string; name?: string; role?: string },
|
||||||
|
) {
|
||||||
|
// Check if user exists
|
||||||
|
const user = await this.prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If email is being updated, check if it's already taken
|
||||||
|
if (data.email && data.email !== user.email) {
|
||||||
|
const existing = await this.prisma.user.findUnique({
|
||||||
|
where: { email: data.email },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
throw new ConflictException('Email already in use');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.user.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
...(data.email && { email: data.email }),
|
||||||
|
...(data.name !== undefined && { name: data.name }),
|
||||||
|
...(data.role && { role: data.role }),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
role: true,
|
||||||
|
emailVerified: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
// Check if user exists
|
||||||
|
const user = await this.prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete user (cascade will handle related records)
|
||||||
|
await this.prisma.user.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { message: 'User deleted successfully' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { PrismaModule } from './prisma/prisma.module';
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
@@ -10,6 +11,7 @@ import { TransactionsModule } from './transactions/transactions.module';
|
|||||||
import { CategoriesModule } from './categories/categories.module';
|
import { CategoriesModule } from './categories/categories.module';
|
||||||
import { OtpModule } from './otp/otp.module';
|
import { OtpModule } from './otp/otp.module';
|
||||||
import { AdminModule } from './admin/admin.module';
|
import { AdminModule } from './admin/admin.module';
|
||||||
|
import { MaintenanceGuard } from './common/guards/maintenance.guard';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -30,6 +32,11 @@ import { AdminModule } from './admin/admin.module';
|
|||||||
AdminModule,
|
AdminModule,
|
||||||
],
|
],
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
providers: [],
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_GUARD,
|
||||||
|
useClass: MaintenanceGuard,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { AuthGuard as JwtAuthGuard } from './auth.guard';
|
import { AuthGuard as JwtAuthGuard } from './auth.guard';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
|
import { SkipMaintenance } from '../common/decorators/skip-maintenance.decorator';
|
||||||
import type { Response } from 'express';
|
import type { Response } from 'express';
|
||||||
|
|
||||||
interface RequestWithUser {
|
interface RequestWithUser {
|
||||||
@@ -24,6 +25,7 @@ export class AuthController {
|
|||||||
constructor(private authService: AuthService) {}
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
@Post('register')
|
@Post('register')
|
||||||
|
@SkipMaintenance()
|
||||||
async register(
|
async register(
|
||||||
@Body() body: { email: string; password: string; name?: string },
|
@Body() body: { email: string; password: string; name?: string },
|
||||||
) {
|
) {
|
||||||
@@ -31,11 +33,13 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
|
@SkipMaintenance()
|
||||||
async login(@Body() body: { email: string; password: string }) {
|
async login(@Body() body: { email: string; password: string }) {
|
||||||
return this.authService.login(body.email, body.password);
|
return this.authService.login(body.email, body.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('verify-otp')
|
@Post('verify-otp')
|
||||||
|
@SkipMaintenance()
|
||||||
async verifyOtp(
|
async verifyOtp(
|
||||||
@Body()
|
@Body()
|
||||||
body: {
|
body: {
|
||||||
@@ -52,12 +56,14 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('google')
|
@Get('google')
|
||||||
|
@SkipMaintenance()
|
||||||
@UseGuards(AuthGuard('google'))
|
@UseGuards(AuthGuard('google'))
|
||||||
async googleAuth() {
|
async googleAuth() {
|
||||||
// Initiates Google OAuth flow
|
// Initiates Google OAuth flow
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('google/callback')
|
@Get('google/callback')
|
||||||
|
@SkipMaintenance()
|
||||||
@UseGuards(AuthGuard('google'))
|
@UseGuards(AuthGuard('google'))
|
||||||
async googleAuthCallback(@Req() req: any, @Res() res: Response) {
|
async googleAuthCallback(@Req() req: any, @Res() res: Response) {
|
||||||
// Handle Google OAuth callback
|
// Handle Google OAuth callback
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ import { OtpModule } from '../otp/otp.module';
|
|||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [AuthService, JwtStrategy, GoogleStrategy],
|
providers: [AuthService, JwtStrategy, GoogleStrategy],
|
||||||
exports: [AuthService],
|
exports: [AuthService, JwtModule],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ export class AuthService {
|
|||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
select: { role: true },
|
select: { role: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.jwtService.sign({
|
return this.jwtService.sign({
|
||||||
sub: userId,
|
sub: userId,
|
||||||
email,
|
email,
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async validate(payload: JwtPayload) {
|
async validate(payload: JwtPayload) {
|
||||||
return {
|
return {
|
||||||
userId: payload.sub,
|
userId: payload.sub,
|
||||||
email: payload.email,
|
email: payload.email,
|
||||||
role: payload.role || 'user'
|
role: payload.role || 'user',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { Controller, Get, Post, Body, Param, Delete, Req, UseGuards } from '@nestjs/common';
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
Req,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { CategoriesService } from '../categories/categories.service';
|
import { CategoriesService } from '../categories/categories.service';
|
||||||
import { CreateCategoryDto } from '../categories/dto/create-category.dto';
|
import { CreateCategoryDto } from '../categories/dto/create-category.dto';
|
||||||
import { AuthGuard } from '../auth/auth.guard';
|
import { AuthGuard } from '../auth/auth.guard';
|
||||||
@@ -15,7 +24,10 @@ export class CategoriesController {
|
|||||||
constructor(private readonly categoriesService: CategoriesService) {}
|
constructor(private readonly categoriesService: CategoriesService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
create(@Req() req: RequestWithUser, @Body() createCategoryDto: CreateCategoryDto) {
|
create(
|
||||||
|
@Req() req: RequestWithUser,
|
||||||
|
@Body() createCategoryDto: CreateCategoryDto,
|
||||||
|
) {
|
||||||
return this.categoriesService.create({
|
return this.categoriesService.create({
|
||||||
...createCategoryDto,
|
...createCategoryDto,
|
||||||
userId: req.user.userId,
|
userId: req.user.userId,
|
||||||
@@ -31,4 +43,4 @@ export class CategoriesController {
|
|||||||
remove(@Req() req: RequestWithUser, @Param('id') id: string) {
|
remove(@Req() req: RequestWithUser, @Param('id') id: string) {
|
||||||
return this.categoriesService.remove(id, req.user.userId);
|
return this.categoriesService.remove(id, req.user.userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const SkipMaintenance = () => SetMetadata('skipMaintenance', true);
|
||||||
72
apps/api/src/common/guards/maintenance.guard.ts
Normal file
72
apps/api/src/common/guards/maintenance.guard.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
ServiceUnavailableException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { PrismaService } from '../../prisma/prisma.service';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MaintenanceGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private reflector: Reflector,
|
||||||
|
private prisma: PrismaService,
|
||||||
|
private jwtService: JwtService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
// Check if route is exempt from maintenance mode (auth, health, admin routes)
|
||||||
|
const isExempt = this.reflector.get<boolean>('skipMaintenance', context.getHandler());
|
||||||
|
const isControllerExempt = this.reflector.get<boolean>('skipMaintenance', context.getClass());
|
||||||
|
|
||||||
|
if (isExempt || isControllerExempt) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get maintenance mode status from config
|
||||||
|
const maintenanceConfig = await this.prisma.appConfig.findUnique({
|
||||||
|
where: { key: 'maintenance_mode' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const isMaintenanceMode = maintenanceConfig?.value === 'true';
|
||||||
|
|
||||||
|
if (!isMaintenanceMode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract user from JWT token (if exists)
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const authHeader = request.headers.authorization;
|
||||||
|
|
||||||
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||||
|
try {
|
||||||
|
const token = authHeader.substring(7);
|
||||||
|
const payload = this.jwtService.verify(token);
|
||||||
|
|
||||||
|
// If user is admin, allow access
|
||||||
|
if (payload.role === 'admin') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Invalid token, continue to block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get maintenance message
|
||||||
|
const messageConfig = await this.prisma.appConfig.findUnique({
|
||||||
|
where: { key: 'maintenance_message' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const message =
|
||||||
|
messageConfig?.value ||
|
||||||
|
'System is under maintenance. Please try again later.';
|
||||||
|
|
||||||
|
throw new ServiceUnavailableException({
|
||||||
|
statusCode: 503,
|
||||||
|
message: message,
|
||||||
|
maintenanceMode: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { SkipMaintenance } from '../common/decorators/skip-maintenance.decorator';
|
||||||
|
|
||||||
@Controller('health')
|
@Controller('health')
|
||||||
|
@SkipMaintenance()
|
||||||
export class HealthController {
|
export class HealthController {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -404,7 +404,9 @@ export class OtpService {
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error('Failed to check WhatsApp number:', error);
|
console.error('Failed to check WhatsApp number:', error);
|
||||||
// Return false if webhook fails - safer approach
|
// Return false if webhook fails - safer approach
|
||||||
console.log(`📱 Failed to check WhatsApp number: ${phone} - Webhook error`);
|
console.log(
|
||||||
|
`📱 Failed to check WhatsApp number: ${phone} - Webhook error`,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
isRegistered: false,
|
isRegistered: false,
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ async function main() {
|
|||||||
// 1. CREATE ADMIN USER
|
// 1. CREATE ADMIN USER
|
||||||
// ============================================
|
// ============================================
|
||||||
console.log('\n👤 Creating admin user...');
|
console.log('\n👤 Creating admin user...');
|
||||||
|
|
||||||
const passwordHash = await bcrypt.hash(ADMIN_PASSWORD, 10);
|
const passwordHash = await bcrypt.hash(ADMIN_PASSWORD, 10);
|
||||||
|
|
||||||
const admin = await prisma.user.upsert({
|
const admin = await prisma.user.upsert({
|
||||||
where: { email: ADMIN_EMAIL },
|
where: { email: ADMIN_EMAIL },
|
||||||
update: {
|
update: {
|
||||||
@@ -34,14 +34,14 @@ async function main() {
|
|||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✅ Admin user created:', admin.email);
|
console.log('✅ Admin user created:', admin.email);
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 2. CREATE DEFAULT PLANS
|
// 2. CREATE DEFAULT PLANS
|
||||||
// ============================================
|
// ============================================
|
||||||
console.log('\n💰 Creating default plans...');
|
console.log('\n💰 Creating default plans...');
|
||||||
|
|
||||||
const freePlan = await prisma.plan.upsert({
|
const freePlan = await prisma.plan.upsert({
|
||||||
where: { slug: 'free' },
|
where: { slug: 'free' },
|
||||||
update: {},
|
update: {},
|
||||||
@@ -74,7 +74,7 @@ async function main() {
|
|||||||
apiRateLimit: null,
|
apiRateLimit: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const proMonthly = await prisma.plan.upsert({
|
const proMonthly = await prisma.plan.upsert({
|
||||||
where: { slug: 'pro-monthly' },
|
where: { slug: 'pro-monthly' },
|
||||||
update: {},
|
update: {},
|
||||||
@@ -90,10 +90,22 @@ async function main() {
|
|||||||
features: {
|
features: {
|
||||||
wallets: { limit: null, label: 'Unlimited wallets' },
|
wallets: { limit: null, label: 'Unlimited wallets' },
|
||||||
goals: { limit: null, label: 'Unlimited goals' },
|
goals: { limit: null, label: 'Unlimited goals' },
|
||||||
team: { enabled: true, maxMembers: 10, label: 'Team feature (10 members)' },
|
team: {
|
||||||
api: { enabled: true, rateLimit: 1000, label: 'API access (1000 req/hr)' },
|
enabled: true,
|
||||||
|
maxMembers: 10,
|
||||||
|
label: 'Team feature (10 members)',
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
enabled: true,
|
||||||
|
rateLimit: 1000,
|
||||||
|
label: 'API access (1000 req/hr)',
|
||||||
|
},
|
||||||
support: { level: 'priority', label: 'Priority support' },
|
support: { level: 'priority', label: 'Priority support' },
|
||||||
export: { enabled: true, formats: ['csv', 'excel', 'pdf'], label: 'All export formats' },
|
export: {
|
||||||
|
enabled: true,
|
||||||
|
formats: ['csv', 'excel', 'pdf'],
|
||||||
|
label: 'All export formats',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
badge: 'Popular',
|
badge: 'Popular',
|
||||||
badgeColor: 'blue',
|
badgeColor: 'blue',
|
||||||
@@ -109,7 +121,7 @@ async function main() {
|
|||||||
apiRateLimit: 1000,
|
apiRateLimit: 1000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const proYearly = await prisma.plan.upsert({
|
const proYearly = await prisma.plan.upsert({
|
||||||
where: { slug: 'pro-yearly' },
|
where: { slug: 'pro-yearly' },
|
||||||
update: {},
|
update: {},
|
||||||
@@ -125,10 +137,22 @@ async function main() {
|
|||||||
features: {
|
features: {
|
||||||
wallets: { limit: null, label: 'Unlimited wallets' },
|
wallets: { limit: null, label: 'Unlimited wallets' },
|
||||||
goals: { limit: null, label: 'Unlimited goals' },
|
goals: { limit: null, label: 'Unlimited goals' },
|
||||||
team: { enabled: true, maxMembers: 10, label: 'Team feature (10 members)' },
|
team: {
|
||||||
api: { enabled: true, rateLimit: 1000, label: 'API access (1000 req/hr)' },
|
enabled: true,
|
||||||
|
maxMembers: 10,
|
||||||
|
label: 'Team feature (10 members)',
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
enabled: true,
|
||||||
|
rateLimit: 1000,
|
||||||
|
label: 'API access (1000 req/hr)',
|
||||||
|
},
|
||||||
support: { level: 'priority', label: 'Priority support' },
|
support: { level: 'priority', label: 'Priority support' },
|
||||||
export: { enabled: true, formats: ['csv', 'excel', 'pdf'], label: 'All export formats' },
|
export: {
|
||||||
|
enabled: true,
|
||||||
|
formats: ['csv', 'excel', 'pdf'],
|
||||||
|
label: 'All export formats',
|
||||||
|
},
|
||||||
discount: { value: '17%', label: 'Save 17% (2 months free)' },
|
discount: { value: '17%', label: 'Save 17% (2 months free)' },
|
||||||
},
|
},
|
||||||
badge: 'Best Value',
|
badge: 'Best Value',
|
||||||
@@ -145,14 +169,18 @@ async function main() {
|
|||||||
apiRateLimit: 1000,
|
apiRateLimit: 1000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✅ Plans created:', [freePlan.name, proMonthly.name, proYearly.name]);
|
console.log('✅ Plans created:', [
|
||||||
|
freePlan.name,
|
||||||
|
proMonthly.name,
|
||||||
|
proYearly.name,
|
||||||
|
]);
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 3. CREATE DEFAULT PAYMENT METHODS
|
// 3. CREATE DEFAULT PAYMENT METHODS
|
||||||
// ============================================
|
// ============================================
|
||||||
console.log('\n💳 Creating default payment methods...');
|
console.log('\n💳 Creating default payment methods...');
|
||||||
|
|
||||||
const bcaMethod = await prisma.paymentMethod.upsert({
|
const bcaMethod = await prisma.paymentMethod.upsert({
|
||||||
where: { id: 'bca-method' },
|
where: { id: 'bca-method' },
|
||||||
update: {},
|
update: {},
|
||||||
@@ -164,12 +192,13 @@ async function main() {
|
|||||||
accountNumber: '1234567890',
|
accountNumber: '1234567890',
|
||||||
displayName: 'BCA Virtual Account',
|
displayName: 'BCA Virtual Account',
|
||||||
logoUrl: '/logos/bca.png',
|
logoUrl: '/logos/bca.png',
|
||||||
instructions: 'Transfer to the account above and upload proof of payment.',
|
instructions:
|
||||||
|
'Transfer to the account above and upload proof of payment.',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
sortOrder: 1,
|
sortOrder: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mandiriMethod = await prisma.paymentMethod.upsert({
|
const mandiriMethod = await prisma.paymentMethod.upsert({
|
||||||
where: { id: 'mandiri-method' },
|
where: { id: 'mandiri-method' },
|
||||||
update: {},
|
update: {},
|
||||||
@@ -181,12 +210,13 @@ async function main() {
|
|||||||
accountNumber: '9876543210',
|
accountNumber: '9876543210',
|
||||||
displayName: 'Mandiri Virtual Account',
|
displayName: 'Mandiri Virtual Account',
|
||||||
logoUrl: '/logos/mandiri.png',
|
logoUrl: '/logos/mandiri.png',
|
||||||
instructions: 'Transfer to the account above and upload proof of payment.',
|
instructions:
|
||||||
|
'Transfer to the account above and upload proof of payment.',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
sortOrder: 2,
|
sortOrder: 2,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const gopayMethod = await prisma.paymentMethod.upsert({
|
const gopayMethod = await prisma.paymentMethod.upsert({
|
||||||
where: { id: 'gopay-method' },
|
where: { id: 'gopay-method' },
|
||||||
update: {},
|
update: {},
|
||||||
@@ -203,14 +233,18 @@ async function main() {
|
|||||||
sortOrder: 3,
|
sortOrder: 3,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✅ Payment methods created:', [bcaMethod.displayName, mandiriMethod.displayName, gopayMethod.displayName]);
|
console.log('✅ Payment methods created:', [
|
||||||
|
bcaMethod.displayName,
|
||||||
|
mandiriMethod.displayName,
|
||||||
|
gopayMethod.displayName,
|
||||||
|
]);
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 4. CREATE APP CONFIG (Optional)
|
// 4. CREATE APP CONFIG (Optional)
|
||||||
// ============================================
|
// ============================================
|
||||||
console.log('\n⚙️ Creating app config...');
|
console.log('\n⚙️ Creating app config...');
|
||||||
|
|
||||||
await prisma.appConfig.upsert({
|
await prisma.appConfig.upsert({
|
||||||
where: { key: 'MAINTENANCE_MODE' },
|
where: { key: 'MAINTENANCE_MODE' },
|
||||||
update: {},
|
update: {},
|
||||||
@@ -224,14 +258,14 @@ async function main() {
|
|||||||
isSecret: false,
|
isSecret: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✅ App config created');
|
console.log('✅ App config created');
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 5. CREATE TEMP USER & WALLET (Legacy)
|
// 5. CREATE TEMP USER & WALLET (Legacy)
|
||||||
// ============================================
|
// ============================================
|
||||||
console.log('\n🔧 Creating temp user (legacy)...');
|
console.log('\n🔧 Creating temp user (legacy)...');
|
||||||
|
|
||||||
const user = await prisma.user.upsert({
|
const user = await prisma.user.upsert({
|
||||||
where: { id: TEMP_USER_ID },
|
where: { id: TEMP_USER_ID },
|
||||||
update: {},
|
update: {},
|
||||||
@@ -252,7 +286,7 @@ async function main() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ Temp user created:', user.id);
|
console.log('✅ Temp user created:', user.id);
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -262,8 +296,18 @@ async function main() {
|
|||||||
console.log('\n📋 Summary:');
|
console.log('\n📋 Summary:');
|
||||||
console.log(' Admin Email:', ADMIN_EMAIL);
|
console.log(' Admin Email:', ADMIN_EMAIL);
|
||||||
console.log(' Admin Password:', ADMIN_PASSWORD);
|
console.log(' Admin Password:', ADMIN_PASSWORD);
|
||||||
console.log(' Plans:', [freePlan.name, proMonthly.name, proYearly.name].join(', '));
|
console.log(
|
||||||
console.log(' Payment Methods:', [bcaMethod.displayName, mandiriMethod.displayName, gopayMethod.displayName].join(', '));
|
' Plans:',
|
||||||
|
[freePlan.name, proMonthly.name, proYearly.name].join(', '),
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
' Payment Methods:',
|
||||||
|
[
|
||||||
|
bcaMethod.displayName,
|
||||||
|
mandiriMethod.displayName,
|
||||||
|
gopayMethod.displayName,
|
||||||
|
].join(', '),
|
||||||
|
);
|
||||||
console.log('\n⚠️ IMPORTANT: Change admin password after first login!');
|
console.log('\n⚠️ IMPORTANT: Change admin password after first login!');
|
||||||
console.log('\n🔗 Login at: http://localhost:5174/auth/login');
|
console.log('\n🔗 Login at: http://localhost:5174/auth/login');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { Controller, Get, Put, Delete, Body, Req, UseGuards } from '@nestjs/common';
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Put,
|
||||||
|
Delete,
|
||||||
|
Body,
|
||||||
|
Req,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { AuthGuard } from '../auth/auth.guard';
|
import { AuthGuard } from '../auth/auth.guard';
|
||||||
import { UsersService } from './users.service';
|
import { UsersService } from './users.service';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { Injectable, BadRequestException, UnauthorizedException } from '@nestjs/common';
|
import {
|
||||||
|
Injectable,
|
||||||
|
BadRequestException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { getTempUserId } from '../common/user.util';
|
import { getTempUserId } from '../common/user.util';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
||||||
import { LanguageProvider } from './contexts/LanguageContext'
|
import { LanguageProvider } from './contexts/LanguageContext'
|
||||||
import { ThemeProvider } from './components/ThemeProvider'
|
import { ThemeProvider } from './components/ThemeProvider'
|
||||||
@@ -8,14 +9,16 @@ import { Login } from './components/pages/Login'
|
|||||||
import { Register } from './components/pages/Register'
|
import { Register } from './components/pages/Register'
|
||||||
import { OtpVerification } from './components/pages/OtpVerification'
|
import { OtpVerification } from './components/pages/OtpVerification'
|
||||||
import { AuthCallback } from './components/pages/AuthCallback'
|
import { AuthCallback } from './components/pages/AuthCallback'
|
||||||
|
import { MaintenancePage } from './components/pages/MaintenancePage'
|
||||||
import { AdminLayout } from './components/admin/AdminLayout'
|
import { AdminLayout } from './components/admin/AdminLayout'
|
||||||
import { AdminDashboard } from './components/admin/pages/AdminDashboard'
|
import { AdminDashboard } from './components/admin/pages/AdminDashboard'
|
||||||
import { AdminPlans } from './components/admin/pages/AdminPlans'
|
import { AdminPlans } from './components/admin/pages/AdminPlans'
|
||||||
import { AdminPaymentMethods } from './components/admin/pages/AdminPaymentMethods'
|
|
||||||
import { AdminPayments } from './components/admin/pages/AdminPayments'
|
import { AdminPayments } from './components/admin/pages/AdminPayments'
|
||||||
import { AdminUsers } from './components/admin/pages/AdminUsers'
|
import { AdminUsers } from './components/admin/pages/AdminUsers'
|
||||||
import { AdminSettings } from './components/admin/pages/AdminSettings'
|
import { AdminSettings } from './components/admin/pages/AdminSettingsNew'
|
||||||
|
import { Profile } from './components/pages/Profile'
|
||||||
import { Loader2 } from 'lucide-react'
|
import { Loader2 } from 'lucide-react'
|
||||||
|
import { setupAxiosInterceptors } from './utils/axiosSetup'
|
||||||
|
|
||||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||||
const { user, loading } = useAuth()
|
const { user, loading } = useAuth()
|
||||||
@@ -50,13 +53,35 @@ function PublicRoute({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
return <Navigate to="/" replace />
|
// Redirect based on role
|
||||||
|
const redirectTo = user.role === 'admin' ? '/admin' : '/'
|
||||||
|
return <Navigate to={redirectTo} replace />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>
|
return <>{children}</>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const [maintenanceMode, setMaintenanceMode] = useState(false)
|
||||||
|
const [maintenanceMessage, setMaintenanceMessage] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Setup axios interceptor for maintenance mode
|
||||||
|
setupAxiosInterceptors((message) => {
|
||||||
|
setMaintenanceMessage(message)
|
||||||
|
setMaintenanceMode(true)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Show maintenance page if maintenance mode is active
|
||||||
|
if (maintenanceMode) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider defaultTheme="light" storageKey="tabungin-ui-theme">
|
||||||
|
<MaintenancePage message={maintenanceMessage} />
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ThemeProvider defaultTheme="light" storageKey="tabungin-ui-theme">
|
<ThemeProvider defaultTheme="light" storageKey="tabungin-ui-theme">
|
||||||
@@ -74,9 +99,9 @@ export default function App() {
|
|||||||
<Route path="/admin" element={<ProtectedRoute><AdminLayout /></ProtectedRoute>}>
|
<Route path="/admin" element={<ProtectedRoute><AdminLayout /></ProtectedRoute>}>
|
||||||
<Route index element={<AdminDashboard />} />
|
<Route index element={<AdminDashboard />} />
|
||||||
<Route path="plans" element={<AdminPlans />} />
|
<Route path="plans" element={<AdminPlans />} />
|
||||||
<Route path="payment-methods" element={<AdminPaymentMethods />} />
|
|
||||||
<Route path="payments" element={<AdminPayments />} />
|
<Route path="payments" element={<AdminPayments />} />
|
||||||
<Route path="users" element={<AdminUsers />} />
|
<Route path="users" element={<AdminUsers />} />
|
||||||
|
<Route path="profile" element={<Profile />} />
|
||||||
<Route path="settings" element={<AdminSettings />} />
|
<Route path="settings" element={<AdminSettings />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useCallback } from "react"
|
import { useState, useCallback } from "react"
|
||||||
import { Routes, Route, useLocation, useNavigate } from "react-router-dom"
|
import { Routes, Route, useLocation, useNavigate, Navigate } from "react-router-dom"
|
||||||
|
import { useAuth } from "@/contexts/AuthContext"
|
||||||
import { DashboardLayout } from "./layout/DashboardLayout"
|
import { DashboardLayout } from "./layout/DashboardLayout"
|
||||||
import { Overview } from "./pages/Overview"
|
import { Overview } from "./pages/Overview"
|
||||||
import { Wallets } from "./pages/Wallets"
|
import { Wallets } from "./pages/Wallets"
|
||||||
@@ -7,8 +8,14 @@ import { Transactions } from "./pages/Transactions"
|
|||||||
import { Profile } from "./pages/Profile"
|
import { Profile } from "./pages/Profile"
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
|
const { user } = useAuth()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
// Block admins from accessing member dashboard
|
||||||
|
if (user?.role === 'admin') {
|
||||||
|
return <Navigate to="/admin" replace />
|
||||||
|
}
|
||||||
const [fabWalletDialogOpen, setFabWalletDialogOpen] = useState(false)
|
const [fabWalletDialogOpen, setFabWalletDialogOpen] = useState(false)
|
||||||
const [fabTransactionDialogOpen, setFabTransactionDialogOpen] = useState(false)
|
const [fabTransactionDialogOpen, setFabTransactionDialogOpen] = useState(false)
|
||||||
|
|
||||||
|
|||||||
@@ -16,16 +16,16 @@ export function AdminLayout() {
|
|||||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-2xl font-bold text-foreground mb-2">
|
<h1 className="text-2xl font-bold text-foreground mb-2">
|
||||||
Akses Ditolak
|
Access Denied
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Anda tidak memiliki izin untuk mengakses panel admin.
|
You don't have permission to access the admin panel.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="mt-4 inline-block text-primary hover:text-primary/90"
|
className="mt-4 inline-block text-primary hover:text-primary/90"
|
||||||
>
|
>
|
||||||
Kembali ke Dashboard
|
Back to Dashboard
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LayoutDashboard, CreditCard, Wallet, Users, Settings, LogOut } from 'lucide-react'
|
import { LayoutDashboard, CreditCard, Wallet, Users, Settings, LogOut, UserCircle } from 'lucide-react'
|
||||||
import { Logo } from '../Logo'
|
import { Logo } from '../Logo'
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
@@ -26,21 +26,21 @@ const items = [
|
|||||||
url: '/admin/plans',
|
url: '/admin/plans',
|
||||||
icon: CreditCard,
|
icon: CreditCard,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Payment Methods',
|
|
||||||
url: '/admin/payment-methods',
|
|
||||||
icon: Wallet,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Payments',
|
title: 'Payments',
|
||||||
url: '/admin/payments',
|
url: '/admin/payments',
|
||||||
icon: CreditCard,
|
icon: Wallet,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Users',
|
title: 'Users',
|
||||||
url: '/admin/users',
|
url: '/admin/users',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Profile',
|
||||||
|
url: '/admin/profile',
|
||||||
|
icon: UserCircle,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
url: '/admin/settings',
|
url: '/admin/settings',
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export function AdminDashboard() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-muted-foreground">Memuat...</div>
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ export function AdminDashboard() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-foreground">Dashboard</h1>
|
<h1 className="text-3xl font-bold text-foreground">Dashboard</h1>
|
||||||
<p className="mt-2 text-muted-foreground">
|
<p className="mt-2 text-muted-foreground">
|
||||||
Selamat datang di panel admin - Overview performa aplikasi
|
Welcome to admin panel - Application performance overview
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ export function AdminDashboard() {
|
|||||||
<p className="text-xs text-muted-foreground flex items-center mt-1">
|
<p className="text-xs text-muted-foreground flex items-center mt-1">
|
||||||
<ArrowUpRight className="h-3 w-3 text-green-600 mr-1" />
|
<ArrowUpRight className="h-3 w-3 text-green-600 mr-1" />
|
||||||
<span className="text-green-600">+{stats?.userGrowth || 0}%</span>
|
<span className="text-green-600">+{stats?.userGrowth || 0}%</span>
|
||||||
<span className="ml-1">dari bulan lalu</span>
|
<span className="ml-1">from last month</span>
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -156,7 +156,7 @@ export function AdminDashboard() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{stats?.activeSubscriptions || 0}</div>
|
<div className="text-2xl font-bold">{stats?.activeSubscriptions || 0}</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Langganan aktif saat ini
|
Currently active subscriptions
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -171,7 +171,7 @@ export function AdminDashboard() {
|
|||||||
<p className="text-xs text-muted-foreground flex items-center mt-1">
|
<p className="text-xs text-muted-foreground flex items-center mt-1">
|
||||||
<ArrowUpRight className="h-3 w-3 text-green-600 mr-1" />
|
<ArrowUpRight className="h-3 w-3 text-green-600 mr-1" />
|
||||||
<span className="text-green-600">+{stats?.revenueGrowth || 0}%</span>
|
<span className="text-green-600">+{stats?.revenueGrowth || 0}%</span>
|
||||||
<span className="ml-1">dari bulan lalu</span>
|
<span className="ml-1">from last month</span>
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -184,7 +184,7 @@ export function AdminDashboard() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{pendingPayments}</div>
|
<div className="text-2xl font-bold">{pendingPayments}</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Menunggu verifikasi
|
Awaiting verification
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -196,7 +196,7 @@ export function AdminDashboard() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Revenue Overview</CardTitle>
|
<CardTitle>Revenue Overview</CardTitle>
|
||||||
<CardDescription>Pendapatan 6 bulan terakhir</CardDescription>
|
<CardDescription>Revenue for the last 6 months</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
@@ -228,7 +228,7 @@ export function AdminDashboard() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Subscription Distribution</CardTitle>
|
<CardTitle>Subscription Distribution</CardTitle>
|
||||||
<CardDescription>Distribusi pengguna per plan</CardDescription>
|
<CardDescription>User distribution by plan</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
@@ -256,19 +256,19 @@ export function AdminDashboard() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Quick Actions</CardTitle>
|
<CardTitle>Quick Actions</CardTitle>
|
||||||
<CardDescription>Akses cepat ke fitur utama</CardDescription>
|
<CardDescription>Quick access to main features</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-2">
|
<CardContent className="grid gap-2">
|
||||||
<Button variant="outline" className="justify-start" asChild>
|
<Button variant="outline" className="justify-start" asChild>
|
||||||
<a href="/admin/plans">
|
<a href="/admin/plans">
|
||||||
<CreditCard className="h-4 w-4 mr-2" />
|
<CreditCard className="h-4 w-4 mr-2" />
|
||||||
Kelola Plans
|
Manage Plans
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" className="justify-start" asChild>
|
<Button variant="outline" className="justify-start" asChild>
|
||||||
<a href="/admin/payments">
|
<a href="/admin/payments">
|
||||||
<DollarSign className="h-4 w-4 mr-2" />
|
<DollarSign className="h-4 w-4 mr-2" />
|
||||||
Verifikasi Pembayaran
|
Verify Payments
|
||||||
{pendingPayments > 0 && (
|
{pendingPayments > 0 && (
|
||||||
<Badge variant="destructive" className="ml-auto">{pendingPayments}</Badge>
|
<Badge variant="destructive" className="ml-auto">{pendingPayments}</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -277,13 +277,13 @@ export function AdminDashboard() {
|
|||||||
<Button variant="outline" className="justify-start" asChild>
|
<Button variant="outline" className="justify-start" asChild>
|
||||||
<a href="/admin/users">
|
<a href="/admin/users">
|
||||||
<Users className="h-4 w-4 mr-2" />
|
<Users className="h-4 w-4 mr-2" />
|
||||||
Kelola Users
|
Manage Users
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" className="justify-start" asChild>
|
<Button variant="outline" className="justify-start" asChild>
|
||||||
<a href="/admin/payment-methods">
|
<a href="/admin/payment-methods">
|
||||||
<Wallet className="h-4 w-4 mr-2" />
|
<Wallet className="h-4 w-4 mr-2" />
|
||||||
Metode Pembayaran
|
Payment Methods
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -293,7 +293,7 @@ export function AdminDashboard() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>System Status</CardTitle>
|
<CardTitle>System Status</CardTitle>
|
||||||
<CardDescription>Status sistem dan statistik</CardDescription>
|
<CardDescription>System status and statistics</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ function SortableMethodCard({ method, onEdit, onDelete, onToggleActive, getTypeI
|
|||||||
{/* Instructions */}
|
{/* Instructions */}
|
||||||
{method.instructions && (
|
{method.instructions && (
|
||||||
<div className="mb-6 p-4 rounded-lg bg-muted/50 border border-border/50">
|
<div className="mb-6 p-4 rounded-lg bg-muted/50 border border-border/50">
|
||||||
<p className="text-xs text-muted-foreground mb-1">Instruksi:</p>
|
<p className="text-xs text-muted-foreground mb-1">Instruction:</p>
|
||||||
<p className="text-sm text-foreground line-clamp-3">
|
<p className="text-sm text-foreground line-clamp-3">
|
||||||
{method.instructions}
|
{method.instructions}
|
||||||
</p>
|
</p>
|
||||||
@@ -230,7 +230,7 @@ function SortableMethodCard({ method, onEdit, onDelete, onToggleActive, getTypeI
|
|||||||
<button
|
<button
|
||||||
onClick={() => onDelete(method.id)}
|
onClick={() => onDelete(method.id)}
|
||||||
className="p-2.5 rounded-lg bg-destructive/10 text-destructive hover:bg-destructive hover:text-destructive-foreground transition-all"
|
className="p-2.5 rounded-lg bg-destructive/10 text-destructive hover:bg-destructive hover:text-destructive-foreground transition-all"
|
||||||
title="Hapus"
|
title="Delete"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -301,10 +301,10 @@ export function AdminPaymentMethods() {
|
|||||||
{ methodIds: newMethods.map((m) => m.id) },
|
{ methodIds: newMethods.map((m) => m.id) },
|
||||||
{ headers: { Authorization: `Bearer ${token}` } }
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
)
|
)
|
||||||
toast.success('Urutan metode pembayaran berhasil diubah')
|
toast.success('Payment method order updated successfully')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to reorder methods:', error)
|
console.error('Failed to reorder methods:', error)
|
||||||
toast.error('Gagal mengubah urutan metode pembayaran')
|
toast.error('Failed to update payment method order')
|
||||||
fetchMethods() // Revert on error
|
fetchMethods() // Revert on error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,11 +319,11 @@ export function AdminPaymentMethods() {
|
|||||||
{ ...method, isActive: !method.isActive },
|
{ ...method, isActive: !method.isActive },
|
||||||
{ headers: { Authorization: `Bearer ${token}` } }
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
)
|
)
|
||||||
toast.success(method.isActive ? 'Metode pembayaran dinonaktifkan' : 'Metode pembayaran diaktifkan')
|
toast.success(method.isActive ? 'Payment method deactivated' : 'Payment method activated')
|
||||||
fetchMethods()
|
fetchMethods()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to toggle status:', error)
|
console.error('Failed to toggle status:', error)
|
||||||
toast.error('Gagal mengubah status')
|
toast.error('Failed to change status')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,12 +337,12 @@ export function AdminPaymentMethods() {
|
|||||||
await axios.delete(`${API_URL}/api/admin/payment-methods/${deleteDialog.methodId}`, {
|
await axios.delete(`${API_URL}/api/admin/payment-methods/${deleteDialog.methodId}`, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
toast.success('Metode pembayaran berhasil dihapus')
|
toast.success('Payment method deleted successfully')
|
||||||
fetchMethods()
|
fetchMethods()
|
||||||
setDeleteDialog({ open: false, methodId: '' })
|
setDeleteDialog({ open: false, methodId: '' })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete payment method:', error)
|
console.error('Failed to delete payment method:', error)
|
||||||
toast.error('Gagal menghapus metode pembayaran')
|
toast.error('Failed to delete payment method')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,13 +391,13 @@ export function AdminPaymentMethods() {
|
|||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
toast.success(editingMethod ? 'Metode pembayaran berhasil diupdate' : 'Metode pembayaran berhasil ditambahkan')
|
toast.success(editingMethod ? 'Payment method updated successfully' : 'Payment method added successfully')
|
||||||
fetchMethods()
|
fetchMethods()
|
||||||
setShowModal(false)
|
setShowModal(false)
|
||||||
setEditingMethod(null)
|
setEditingMethod(null)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save payment method:', error)
|
console.error('Failed to save payment method:', error)
|
||||||
toast.error('Gagal menyimpan metode pembayaran')
|
toast.error('Failed to save payment method')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +443,7 @@ export function AdminPaymentMethods() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-muted-foreground">Memuat...</div>
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -452,16 +452,16 @@ export function AdminPaymentMethods() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-8">
|
<div className="flex items-center justify-between mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-foreground">
|
<h4 className="text-xl font-bold text-foreground">
|
||||||
Metode Pembayaran
|
Payment Methods
|
||||||
</h1>
|
</h4>
|
||||||
<p className="mt-2 text-muted-foreground">
|
<p className="mt-2 text-muted-foreground">
|
||||||
Kelola metode pembayaran yang tersedia
|
Manage available payment methods
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => handleOpenModal()}>
|
<Button onClick={() => handleOpenModal()}>
|
||||||
<Plus className="h-5 w-5 mr-2" />
|
<Plus className="h-5 w-5 mr-2" />
|
||||||
Tambah Metode
|
Add Method
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -487,7 +487,7 @@ export function AdminPaymentMethods() {
|
|||||||
|
|
||||||
{methods.length === 0 && (
|
{methods.length === 0 && (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<p className="text-muted-foreground">Belum ada metode pembayaran</p>
|
<p className="text-muted-foreground">No payment methods yet</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -495,21 +495,21 @@ export function AdminPaymentMethods() {
|
|||||||
<Dialog open={showModal} onOpenChange={setShowModal}>
|
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{editingMethod ? 'Edit Metode Pembayaran' : 'Tambah Metode Pembayaran'}</DialogTitle>
|
<DialogTitle>{editingMethod ? 'Edit Payment Method' : 'Add Payment Method'}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{editingMethod ? 'Ubah informasi metode pembayaran' : 'Tambah metode pembayaran baru'}
|
{editingMethod ? 'Update payment method information' : 'Add a new payment method'}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="displayName">Nama Tampilan</Label>
|
<Label htmlFor="displayName">Display Name</Label>
|
||||||
<Input
|
<Input
|
||||||
id="displayName"
|
id="displayName"
|
||||||
required
|
required
|
||||||
value={formData.displayName}
|
value={formData.displayName}
|
||||||
onChange={(e) => setFormData({ ...formData, displayName: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, displayName: e.target.value })}
|
||||||
placeholder="BCA, GoPay, QRIS, dll"
|
placeholder="BCA, GoPay, QRIS, etc"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -520,28 +520,28 @@ export function AdminPaymentMethods() {
|
|||||||
required
|
required
|
||||||
value={formData.provider}
|
value={formData.provider}
|
||||||
onChange={(e) => setFormData({ ...formData, provider: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, provider: e.target.value })}
|
||||||
placeholder="BCA, Gopay, OVO, dll"
|
placeholder="BCA, Gopay, OVO, etc"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="type">Tipe</Label>
|
<Label htmlFor="type">Type</Label>
|
||||||
<Select value={formData.type} onValueChange={(value) => setFormData({ ...formData, type: value })}>
|
<Select value={formData.type} onValueChange={(value) => setFormData({ ...formData, type: value })}>
|
||||||
<SelectTrigger id="type">
|
<SelectTrigger id="type">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="bank_transfer">Transfer Bank</SelectItem>
|
<SelectItem value="bank_transfer">Bank Transfer</SelectItem>
|
||||||
<SelectItem value="ewallet">E-Wallet</SelectItem>
|
<SelectItem value="ewallet">E-Wallet</SelectItem>
|
||||||
<SelectItem value="qris">QRIS</SelectItem>
|
<SelectItem value="qris">QRIS</SelectItem>
|
||||||
<SelectItem value="other">Lainnya</SelectItem>
|
<SelectItem value="other">Other</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="accountNumber">Nomor Rekening / Akun</Label>
|
<Label htmlFor="accountNumber">Account Number</Label>
|
||||||
<Input
|
<Input
|
||||||
id="accountNumber"
|
id="accountNumber"
|
||||||
value={formData.accountNumber}
|
value={formData.accountNumber}
|
||||||
@@ -550,7 +550,7 @@ export function AdminPaymentMethods() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="accountName">Nama Pemilik</Label>
|
<Label htmlFor="accountName">Account Holder Name</Label>
|
||||||
<Input
|
<Input
|
||||||
id="accountName"
|
id="accountName"
|
||||||
value={formData.accountName}
|
value={formData.accountName}
|
||||||
@@ -561,13 +561,13 @@ export function AdminPaymentMethods() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="instructions">Instruksi Pembayaran</Label>
|
<Label htmlFor="instructions">Payment Instruction</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="instructions"
|
id="instructions"
|
||||||
value={formData.instructions}
|
value={formData.instructions}
|
||||||
onChange={(e) => setFormData({ ...formData, instructions: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, instructions: e.target.value })}
|
||||||
rows={4}
|
rows={4}
|
||||||
placeholder="Petunjuk cara melakukan pembayaran..."
|
placeholder="Detail payment instruction, step by step to make payment..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -591,17 +591,17 @@ export function AdminPaymentMethods() {
|
|||||||
/>
|
/>
|
||||||
<Label htmlFor="isActive" className="cursor-pointer flex justify-between w-full">
|
<Label htmlFor="isActive" className="cursor-pointer flex justify-between w-full">
|
||||||
<div className="font-semibold text-foreground">Active</div>
|
<div className="font-semibold text-foreground">Active</div>
|
||||||
<div className="text-xs text-muted-foreground">Metode pembayaran dapat digunakan</div>
|
<div className="text-xs text-muted-foreground">Payment method can be used</div>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="outline" onClick={() => setShowModal(false)}>
|
<Button type="button" variant="outline" onClick={() => setShowModal(false)}>
|
||||||
Batal
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit">
|
<Button type="submit">
|
||||||
{editingMethod ? 'Update' : 'Tambah'}
|
{editingMethod ? 'Update' : 'Add'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
@@ -612,15 +612,15 @@ export function AdminPaymentMethods() {
|
|||||||
<AlertDialog open={deleteDialog.open} onOpenChange={(open) => setDeleteDialog({ ...deleteDialog, open })}>
|
<AlertDialog open={deleteDialog.open} onOpenChange={(open) => setDeleteDialog({ ...deleteDialog, open })}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Hapus Metode Pembayaran?</AlertDialogTitle>
|
<AlertDialogTitle>Delete Payment Method?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Apakah Anda yakin ingin menghapus metode pembayaran ini? Tindakan ini tidak dapat dibatalkan.
|
Are you sure you want to delete this payment method? This action cannot be undone.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Batal</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
<AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||||
Hapus
|
Delete
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function AdminPayments() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleVerify = async (paymentId: string) => {
|
const handleVerify = async (paymentId: string) => {
|
||||||
const notes = prompt('Catatan verifikasi (opsional):')
|
const notes = prompt('Verification notes (optional):')
|
||||||
if (notes === null) return
|
if (notes === null) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -78,16 +78,16 @@ export function AdminPayments() {
|
|||||||
{ notes },
|
{ notes },
|
||||||
{ headers: { Authorization: `Bearer ${token}` } }
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
)
|
)
|
||||||
toast.success('Pembayaran berhasil diverifikasi')
|
toast.success('Payment verified successfully')
|
||||||
fetchPayments()
|
fetchPayments()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to verify payment:', error)
|
console.error('Failed to verify payment:', error)
|
||||||
toast.error('Gagal memverifikasi pembayaran')
|
toast.error('Failed to verify payment')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReject = async (paymentId: string) => {
|
const handleReject = async (paymentId: string) => {
|
||||||
const reason = prompt('Alasan penolakan:')
|
const reason = prompt('Rejection reason:')
|
||||||
if (!reason) return
|
if (!reason) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -97,11 +97,11 @@ export function AdminPayments() {
|
|||||||
{ reason },
|
{ reason },
|
||||||
{ headers: { Authorization: `Bearer ${token}` } }
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
)
|
)
|
||||||
toast.success('Pembayaran berhasil ditolak')
|
toast.success('Payment rejected successfully')
|
||||||
fetchPayments()
|
fetchPayments()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to reject payment:', error)
|
console.error('Failed to reject payment:', error)
|
||||||
toast.error('Gagal menolak pembayaran')
|
toast.error('Failed to reject payment')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ export function AdminPayments() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-muted-foreground">Memuat...</div>
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -162,9 +162,9 @@ export function AdminPayments() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-foreground">Verifikasi Pembayaran</h1>
|
<h1 className="text-3xl font-bold text-foreground">Payment Verification</h1>
|
||||||
<p className="mt-2 text-muted-foreground">
|
<p className="mt-2 text-muted-foreground">
|
||||||
Kelola dan verifikasi bukti pembayaran dari pengguna
|
Manage and verify payment proofs from users
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ export function AdminPayments() {
|
|||||||
onChange={(e) => setFilter(e.target.value as FilterStatus)}
|
onChange={(e) => setFilter(e.target.value as FilterStatus)}
|
||||||
className="px-4 py-2 border border-input rounded-lg bg-background text-foreground focus:ring-2 focus:ring-ring focus:border-transparent"
|
className="px-4 py-2 border border-input rounded-lg bg-background text-foreground focus:ring-2 focus:ring-ring focus:border-transparent"
|
||||||
>
|
>
|
||||||
<option value="all">Semua Status</option>
|
<option value="all">All Status</option>
|
||||||
<option value="pending">Pending</option>
|
<option value="pending">Pending</option>
|
||||||
<option value="verified">Verified</option>
|
<option value="verified">Verified</option>
|
||||||
<option value="rejected">Rejected</option>
|
<option value="rejected">Rejected</option>
|
||||||
@@ -211,16 +211,16 @@ export function AdminPayments() {
|
|||||||
Plan
|
Plan
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
Jumlah
|
Amount
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
Metode
|
Method
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
Tanggal
|
Date
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
Actions
|
Actions
|
||||||
@@ -272,7 +272,7 @@ export function AdminPayments() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setSelectedPayment(payment)}
|
onClick={() => setSelectedPayment(payment)}
|
||||||
className="p-2 rounded-lg text-primary hover:bg-primary/10 transition-colors"
|
className="p-2 rounded-lg text-primary hover:bg-primary/10 transition-colors"
|
||||||
title="Lihat Bukti"
|
title="View Proof"
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -282,14 +282,14 @@ export function AdminPayments() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleVerify(payment.id)}
|
onClick={() => handleVerify(payment.id)}
|
||||||
className="p-2 rounded-lg text-green-600 hover:bg-green-500/10 transition-colors"
|
className="p-2 rounded-lg text-green-600 hover:bg-green-500/10 transition-colors"
|
||||||
title="Verifikasi"
|
title="Verify"
|
||||||
>
|
>
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleReject(payment.id)}
|
onClick={() => handleReject(payment.id)}
|
||||||
className="p-2 rounded-lg text-destructive hover:bg-destructive/10 transition-colors"
|
className="p-2 rounded-lg text-destructive hover:bg-destructive/10 transition-colors"
|
||||||
title="Tolak"
|
title="Reject"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -321,7 +321,7 @@ export function AdminPayments() {
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className="p-6 border-b border-border">
|
<div className="p-6 border-b border-border">
|
||||||
<h2 className="text-xl font-bold text-foreground">Bukti Pembayaran</h2>
|
<h2 className="text-xl font-bold text-foreground">Payment Proof</h2>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
{selectedPayment.user.name} - {selectedPayment.plan.name}
|
{selectedPayment.user.name} - {selectedPayment.plan.name}
|
||||||
</p>
|
</p>
|
||||||
@@ -330,15 +330,15 @@ export function AdminPayments() {
|
|||||||
{selectedPayment.proofUrl ? (
|
{selectedPayment.proofUrl ? (
|
||||||
<img
|
<img
|
||||||
src={selectedPayment.proofUrl}
|
src={selectedPayment.proofUrl}
|
||||||
alt="Bukti Pembayaran"
|
alt="Payment Proof"
|
||||||
className="w-full rounded-lg"
|
className="w-full rounded-lg"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground">Tidak ada bukti pembayaran</p>
|
<p className="text-muted-foreground">No payment proof</p>
|
||||||
)}
|
)}
|
||||||
{selectedPayment.notes && (
|
{selectedPayment.notes && (
|
||||||
<div className="mt-4 p-4 bg-muted rounded-lg">
|
<div className="mt-4 p-4 bg-muted rounded-lg">
|
||||||
<p className="text-sm font-medium text-foreground">Catatan:</p>
|
<p className="text-sm font-medium text-foreground">Notes:</p>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
{selectedPayment.notes}
|
{selectedPayment.notes}
|
||||||
</p>
|
</p>
|
||||||
@@ -361,7 +361,7 @@ export function AdminPayments() {
|
|||||||
}}
|
}}
|
||||||
className="px-4 py-2 rounded-lg bg-green-500 text-white hover:bg-green-600 transition-colors"
|
className="px-4 py-2 rounded-lg bg-green-500 text-white hover:bg-green-600 transition-colors"
|
||||||
>
|
>
|
||||||
Verifikasi
|
Verify
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -370,7 +370,7 @@ export function AdminPayments() {
|
|||||||
}}
|
}}
|
||||||
className="px-4 py-2 rounded-lg bg-destructive text-destructive-foreground hover:bg-destructive/90 transition-colors"
|
className="px-4 py-2 rounded-lg bg-destructive text-destructive-foreground hover:bg-destructive/90 transition-colors"
|
||||||
>
|
>
|
||||||
Tolak
|
Reject
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ function SortablePlanCard({ plan, onEdit, onDelete, onToggleVisibility, formatPr
|
|||||||
<button
|
<button
|
||||||
onClick={() => onDelete(plan.id)}
|
onClick={() => onDelete(plan.id)}
|
||||||
className="p-2.5 rounded-lg bg-destructive/10 text-destructive hover:bg-destructive hover:text-destructive-foreground transition-all"
|
className="p-2.5 rounded-lg bg-destructive/10 text-destructive hover:bg-destructive hover:text-destructive-foreground transition-all"
|
||||||
title="Hapus"
|
title="Delete"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -317,10 +317,10 @@ export function AdminPlans() {
|
|||||||
{ planIds: newPlans.map((p) => p.id) },
|
{ planIds: newPlans.map((p) => p.id) },
|
||||||
{ headers: { Authorization: `Bearer ${token}` } }
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
)
|
)
|
||||||
toast.success('Urutan plan berhasil diubah')
|
toast.success('Plan order updated successfully')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to reorder plans:', error)
|
console.error('Failed to reorder plans:', error)
|
||||||
toast.error('Gagal mengubah urutan plan')
|
toast.error('Failed to update plan order')
|
||||||
fetchPlans() // Revert on error
|
fetchPlans() // Revert on error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,14 +341,14 @@ export function AdminPlans() {
|
|||||||
if (response.data.action === 'deactivated') {
|
if (response.data.action === 'deactivated') {
|
||||||
toast.warning(response.data.message)
|
toast.warning(response.data.message)
|
||||||
} else {
|
} else {
|
||||||
toast.success('Plan berhasil dihapus')
|
toast.success('Plan deleted successfully')
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchPlans()
|
fetchPlans()
|
||||||
setDeleteDialog({ open: false, planId: '' })
|
setDeleteDialog({ open: false, planId: '' })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete plan:', error)
|
console.error('Failed to delete plan:', error)
|
||||||
toast.error('Gagal menghapus plan')
|
toast.error('Failed to delete plan')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,11 +360,11 @@ export function AdminPlans() {
|
|||||||
{ isVisible: !plan.isVisible },
|
{ isVisible: !plan.isVisible },
|
||||||
{ headers: { Authorization: `Bearer ${token}` } }
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
)
|
)
|
||||||
toast.success(plan.isVisible ? 'Plan berhasil disembunyikan' : 'Plan berhasil ditampilkan')
|
toast.success(plan.isVisible ? 'Plan hidden successfully' : 'Plan shown successfully')
|
||||||
fetchPlans()
|
fetchPlans()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update plan:', error)
|
console.error('Failed to update plan:', error)
|
||||||
toast.error('Gagal mengubah visibilitas plan')
|
toast.error('Failed to change plan visibility')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,13 +423,13 @@ export function AdminPlans() {
|
|||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
toast.success(editingPlan ? 'Plan berhasil diupdate' : 'Plan berhasil ditambahkan')
|
toast.success(editingPlan ? 'Plan updated successfully' : 'Plan added successfully')
|
||||||
fetchPlans()
|
fetchPlans()
|
||||||
setShowModal(false)
|
setShowModal(false)
|
||||||
setEditingPlan(null)
|
setEditingPlan(null)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save plan:', error)
|
console.error('Failed to save plan:', error)
|
||||||
toast.error('Gagal menyimpan plan')
|
toast.error('Failed to save plan')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,7 +446,7 @@ export function AdminPlans() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-muted-foreground">Memuat...</div>
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -455,12 +455,12 @@ export function AdminPlans() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-8">
|
<div className="flex items-center justify-between mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-foreground">Kelola Plans</h1>
|
<h1 className="text-3xl font-bold text-foreground">Manage Plans</h1>
|
||||||
<p className="mt-2 text-muted-foreground">Kelola paket berlangganan</p>
|
<p className="mt-2 text-muted-foreground">Manage subscription plans</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => handleOpenModal()}>
|
<Button onClick={() => handleOpenModal()}>
|
||||||
<Plus className="h-5 w-5 mr-2" />
|
<Plus className="h-5 w-5 mr-2" />
|
||||||
Tambah Plan
|
Add Plan
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -486,16 +486,16 @@ export function AdminPlans() {
|
|||||||
<Dialog open={showModal} onOpenChange={setShowModal}>
|
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{editingPlan ? 'Edit Plan' : 'Tambah Plan Baru'}</DialogTitle>
|
<DialogTitle>{editingPlan ? 'Edit Plan' : 'Add New Plan'}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{editingPlan ? 'Ubah informasi plan berlangganan' : 'Buat plan berlangganan baru'}
|
{editingPlan ? 'Update subscription plan information' : 'Create a new subscription plan'}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="name">Nama Plan</Label>
|
<Label htmlFor="name">Plan Name</Label>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
required
|
required
|
||||||
@@ -515,7 +515,7 @@ export function AdminPlans() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="description">Deskripsi</Label>
|
<Label htmlFor="description">Description</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
required
|
required
|
||||||
@@ -527,7 +527,7 @@ export function AdminPlans() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="price">Harga</Label>
|
<Label htmlFor="price">Price</Label>
|
||||||
<Input
|
<Input
|
||||||
id="price"
|
id="price"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -552,7 +552,7 @@ export function AdminPlans() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="durationType">Tipe Durasi</Label>
|
<Label htmlFor="durationType">Duration Type</Label>
|
||||||
<Select value={formData.durationType} onValueChange={(value) => setFormData({ ...formData, durationType: value })}>
|
<Select value={formData.durationType} onValueChange={(value) => setFormData({ ...formData, durationType: value })}>
|
||||||
<SelectTrigger id="durationType">
|
<SelectTrigger id="durationType">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
@@ -576,7 +576,7 @@ export function AdminPlans() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="features">Features (satu per baris)</Label>
|
<Label htmlFor="features">Features (one per line)</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="features"
|
id="features"
|
||||||
value={formData.features.join('\n')}
|
value={formData.features.join('\n')}
|
||||||
@@ -636,7 +636,7 @@ export function AdminPlans() {
|
|||||||
/>
|
/>
|
||||||
<Label htmlFor="isVisible" className="cursor-pointer flex justify-between w-full">
|
<Label htmlFor="isVisible" className="cursor-pointer flex justify-between w-full">
|
||||||
<div className="font-semibold text-foreground">Visible</div>
|
<div className="font-semibold text-foreground">Visible</div>
|
||||||
<div className="text-xs text-muted-foreground">Tampil di halaman pricing</div>
|
<div className="text-xs text-muted-foreground">Show on pricing page</div>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -644,10 +644,10 @@ export function AdminPlans() {
|
|||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="outline" onClick={() => setShowModal(false)}>
|
<Button type="button" variant="outline" onClick={() => setShowModal(false)}>
|
||||||
Batal
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit">
|
<Button type="submit">
|
||||||
{editingPlan ? 'Update' : 'Tambah'}
|
{editingPlan ? 'Update' : 'Add'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
@@ -658,15 +658,15 @@ export function AdminPlans() {
|
|||||||
<AlertDialog open={deleteDialog.open} onOpenChange={(open) => setDeleteDialog({ ...deleteDialog, open })}>
|
<AlertDialog open={deleteDialog.open} onOpenChange={(open) => setDeleteDialog({ ...deleteDialog, open })}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Hapus Plan?</AlertDialogTitle>
|
<AlertDialogTitle>Delete Plan?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Apakah Anda yakin ingin menghapus plan ini? Tindakan ini tidak dapat dibatalkan.
|
Are you sure you want to delete this plan? This action cannot be undone.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Batal</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
<AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||||
Hapus
|
Delete
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function AdminSettings() {
|
|||||||
enableEmailVerification: true,
|
enableEmailVerification: true,
|
||||||
enablePaymentVerification: true,
|
enablePaymentVerification: true,
|
||||||
maintenanceMode: false,
|
maintenanceMode: false,
|
||||||
maintenanceMessage: 'Sistem sedang dalam pemeliharaan. Mohon coba lagi nanti.',
|
maintenanceMessage: 'System is under maintenance. Please try again later.',
|
||||||
})
|
})
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
@@ -65,7 +65,7 @@ export function AdminSettings() {
|
|||||||
enableEmailVerification: configData.features?.find((c) => c.key === 'enable_email_verification')?.value === 'true',
|
enableEmailVerification: configData.features?.find((c) => c.key === 'enable_email_verification')?.value === 'true',
|
||||||
enablePaymentVerification: configData.features?.find((c) => c.key === 'enable_payment_verification')?.value === 'true',
|
enablePaymentVerification: configData.features?.find((c) => c.key === 'enable_payment_verification')?.value === 'true',
|
||||||
maintenanceMode: configData.system?.find((c) => c.key === 'maintenance_mode')?.value === 'true',
|
maintenanceMode: configData.system?.find((c) => c.key === 'maintenance_mode')?.value === 'true',
|
||||||
maintenanceMessage: configData.system?.find((c) => c.key === 'maintenance_message')?.value || 'Sistem sedang dalam pemeliharaan. Mohon coba lagi nanti.',
|
maintenanceMessage: configData.system?.find((c) => c.key === 'maintenance_message')?.value || 'System is under maintenance. Please try again later.',
|
||||||
}
|
}
|
||||||
setSettings(settingsObj)
|
setSettings(settingsObj)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -82,14 +82,14 @@ export function AdminSettings() {
|
|||||||
|
|
||||||
// Save each setting individually
|
// Save each setting individually
|
||||||
const configUpdates = [
|
const configUpdates = [
|
||||||
{ key: 'app_name', value: settings.appName, category: 'general', label: 'Nama Aplikasi', type: 'text' },
|
{ key: 'app_name', value: settings.appName, category: 'general', label: 'Application Name', type: 'text' },
|
||||||
{ key: 'app_url', value: settings.appUrl, category: 'general', label: 'URL Aplikasi', type: 'text' },
|
{ key: 'app_url', value: settings.appUrl, category: 'general', label: 'Application URL', type: 'text' },
|
||||||
{ key: 'support_email', value: settings.supportEmail, category: 'general', label: 'Email Support', type: 'email' },
|
{ key: 'support_email', value: settings.supportEmail, category: 'general', label: 'Support Email', type: 'email' },
|
||||||
{ key: 'enable_registration', value: String(settings.enableRegistration), category: 'features', label: 'Registrasi Pengguna Baru', type: 'boolean' },
|
{ key: 'enable_registration', value: String(settings.enableRegistration), category: 'features', label: 'New User Registration', type: 'boolean' },
|
||||||
{ key: 'enable_email_verification', value: String(settings.enableEmailVerification), category: 'features', label: 'Verifikasi Email', type: 'boolean' },
|
{ key: 'enable_email_verification', value: String(settings.enableEmailVerification), category: 'features', label: 'Email Verification', type: 'boolean' },
|
||||||
{ key: 'enable_payment_verification', value: String(settings.enablePaymentVerification), category: 'features', label: 'Verifikasi Pembayaran', type: 'boolean' },
|
{ key: 'enable_payment_verification', value: String(settings.enablePaymentVerification), category: 'features', label: 'Payment Verification', type: 'boolean' },
|
||||||
{ key: 'maintenance_mode', value: String(settings.maintenanceMode), category: 'system', label: 'Mode Pemeliharaan', type: 'boolean' },
|
{ key: 'maintenance_mode', value: String(settings.maintenanceMode), category: 'system', label: 'Maintenance Mode', type: 'boolean' },
|
||||||
{ key: 'maintenance_message', value: settings.maintenanceMessage, category: 'system', label: 'Pesan Pemeliharaan', type: 'text' },
|
{ key: 'maintenance_message', value: settings.maintenanceMessage, category: 'system', label: 'Maintenance Message', type: 'text' },
|
||||||
]
|
]
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -100,11 +100,11 @@ export function AdminSettings() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
toast.success('Pengaturan berhasil disimpan')
|
toast.success('Settings saved successfully')
|
||||||
fetchSettings() // Refresh
|
fetchSettings() // Refresh
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save settings:', error)
|
console.error('Failed to save settings:', error)
|
||||||
toast.error('Gagal menyimpan pengaturan')
|
toast.error('Failed to save settings')
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@ export function AdminSettings() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-muted-foreground">Memuat...</div>
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -125,9 +125,9 @@ export function AdminSettings() {
|
|||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-foreground">Pengaturan Aplikasi</h1>
|
<h1 className="text-3xl font-bold text-foreground">Application Settings</h1>
|
||||||
<p className="mt-2 text-muted-foreground">
|
<p className="mt-2 text-muted-foreground">
|
||||||
Kelola konfigurasi dan pengaturan sistem
|
Manage system configuration and settings
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -139,15 +139,15 @@ export function AdminSettings() {
|
|||||||
<Globe className="h-5 w-5 text-primary" />
|
<Globe className="h-5 w-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-foreground">Pengaturan Umum</h2>
|
<h2 className="text-lg font-semibold text-foreground">General Settings</h2>
|
||||||
<p className="text-sm text-muted-foreground">Informasi dasar aplikasi</p>
|
<p className="text-sm text-muted-foreground">Basic application information</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
Nama Aplikasi
|
Application Name
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -158,7 +158,7 @@ export function AdminSettings() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
URL Aplikasi
|
Application URL
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
type="url"
|
type="url"
|
||||||
@@ -187,17 +187,17 @@ export function AdminSettings() {
|
|||||||
<Shield className="h-5 w-5 text-primary" />
|
<Shield className="h-5 w-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-foreground">Fitur & Keamanan</h2>
|
<h2 className="text-lg font-semibold text-foreground">Features & Security</h2>
|
||||||
<p className="text-sm text-muted-foreground">Aktifkan atau nonaktifkan fitur</p>
|
<p className="text-sm text-muted-foreground">Enable or disable features</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-foreground">Registrasi Pengguna Baru</p>
|
<p className="font-medium text-foreground">New User Registration</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Izinkan pengguna baru mendaftar
|
Allow new users to register
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -208,9 +208,9 @@ export function AdminSettings() {
|
|||||||
|
|
||||||
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-foreground">Verifikasi Email</p>
|
<p className="font-medium text-foreground">Email Verification</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Wajibkan verifikasi email untuk pengguna baru
|
Require email verification for new users
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -221,9 +221,9 @@ export function AdminSettings() {
|
|||||||
|
|
||||||
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-foreground">Verifikasi Pembayaran Manual</p>
|
<p className="font-medium text-foreground">Manual Payment Verification</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Aktifkan verifikasi manual untuk pembayaran
|
Enable manual verification for payments
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -241,9 +241,9 @@ export function AdminSettings() {
|
|||||||
<Database className="h-5 w-5 text-primary" />
|
<Database className="h-5 w-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-foreground">Mode Pemeliharaan</h2>
|
<h2 className="text-lg font-semibold text-foreground">Maintenance Mode</h2>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Nonaktifkan akses sementara untuk maintenance
|
Temporarily disable access for maintenance
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -251,9 +251,9 @@ export function AdminSettings() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between p-4 rounded-lg bg-destructive/10 border border-destructive/20">
|
<div className="flex items-center justify-between p-4 rounded-lg bg-destructive/10 border border-destructive/20">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-foreground">Mode Pemeliharaan</p>
|
<p className="font-medium text-foreground">Maintenance Mode</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Aktifkan untuk menutup akses sementara
|
Enable to temporarily close access
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -266,7 +266,7 @@ export function AdminSettings() {
|
|||||||
{settings.maintenanceMode && (
|
{settings.maintenanceMode && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
Pesan Pemeliharaan
|
Maintenance Message
|
||||||
</label>
|
</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
value={settings.maintenanceMessage}
|
value={settings.maintenanceMessage}
|
||||||
@@ -286,7 +286,7 @@ export function AdminSettings() {
|
|||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Save className="h-5 w-5" />
|
<Save className="h-5 w-5" />
|
||||||
{saving ? 'Menyimpan...' : 'Simpan Pengaturan'}
|
{saving ? 'Saving...' : 'Save Settings'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
63
apps/web/src/components/admin/pages/AdminSettingsNew.tsx
Normal file
63
apps/web/src/components/admin/pages/AdminSettingsNew.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import { Globe, Shield, Wallet } from 'lucide-react'
|
||||||
|
import { AdminSettingsGeneral } from './settings/AdminSettingsGeneral'
|
||||||
|
import { AdminSettingsSecurity } from './settings/AdminSettingsSecurity'
|
||||||
|
import { AdminSettingsPaymentMethods } from './settings/AdminSettingsPaymentMethods'
|
||||||
|
|
||||||
|
export function AdminSettings() {
|
||||||
|
const [activeTab, setActiveTab] = useState('general')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-foreground">Settings</h1>
|
||||||
|
<p className="mt-2 text-muted-foreground">
|
||||||
|
Manage system configuration and settings
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex flex-col md:flex-row gap-6">
|
||||||
|
{/* Vertical Tabs (Horizontal scrollable on mobile) */}
|
||||||
|
<TabsList className="flex md:flex-col h-auto md:w-64 flex-shrink-0 bg-muted/50 p-1 overflow-x-auto md:overflow-x-visible justify-start md:h-fit">
|
||||||
|
<TabsTrigger
|
||||||
|
value="general"
|
||||||
|
className="w-full justify-start gap-3 px-4 py-3 data-[state=active]:bg-background whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<Globe className="h-5 w-5" />
|
||||||
|
<span className="inline">General</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="security"
|
||||||
|
className="w-full justify-start gap-3 px-4 py-3 data-[state=active]:bg-background whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<Shield className="h-5 w-5" />
|
||||||
|
<span className="inline">Security</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="payment-methods"
|
||||||
|
className="w-full justify-start gap-3 px-4 py-3 data-[state=active]:bg-background whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<Wallet className="h-5 w-5" />
|
||||||
|
<span className="inline">Payment Methods</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* Tab Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<TabsContent value="general" className="mt-0">
|
||||||
|
<AdminSettingsGeneral />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="security" className="mt-0">
|
||||||
|
<AdminSettingsSecurity />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="payment-methods" className="mt-0">
|
||||||
|
<AdminSettingsPaymentMethods />
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { Search, UserX, UserCheck, Crown } from 'lucide-react'
|
import { Search, UserX, UserCheck, Crown, Plus, Edit, Trash2 } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -39,6 +46,9 @@ export function AdminUsers() {
|
|||||||
const [suspendReason, setSuspendReason] = useState('')
|
const [suspendReason, setSuspendReason] = useState('')
|
||||||
const [grantProDialog, setGrantProDialog] = useState<{ open: boolean; userId: string }>({ open: false, userId: '' })
|
const [grantProDialog, setGrantProDialog] = useState<{ open: boolean; userId: string }>({ open: false, userId: '' })
|
||||||
const [proDays, setProDays] = useState('')
|
const [proDays, setProDays] = useState('')
|
||||||
|
const [userDialog, setUserDialog] = useState<{ open: boolean; mode: 'create' | 'edit'; user?: User }>({ open: false, mode: 'create' })
|
||||||
|
const [formData, setFormData] = useState({ email: '', password: '', name: '', role: 'user' })
|
||||||
|
const [deleteDialog, setDeleteDialog] = useState<{ open: boolean; userId: string; userName: string }>({ open: false, userId: '', userName: '' })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers()
|
fetchUsers()
|
||||||
@@ -76,12 +86,12 @@ export function AdminUsers() {
|
|||||||
suspendDialog.suspend ? { reason: suspendReason } : {},
|
suspendDialog.suspend ? { reason: suspendReason } : {},
|
||||||
{ headers: { Authorization: `Bearer ${token}` } }
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
)
|
)
|
||||||
toast.success(suspendDialog.suspend ? 'User berhasil disuspend' : 'User berhasil diaktifkan kembali')
|
toast.success(suspendDialog.suspend ? 'User suspended successfully' : 'User unsuspended successfully')
|
||||||
fetchUsers()
|
fetchUsers()
|
||||||
setSuspendDialog({ open: false, userId: '', suspend: false })
|
setSuspendDialog({ open: false, userId: '', suspend: false })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update user:', error)
|
console.error('Failed to update user:', error)
|
||||||
toast.error('Gagal mengupdate user')
|
toast.error('Failed to update user')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,32 +113,116 @@ export function AdminUsers() {
|
|||||||
},
|
},
|
||||||
{ headers: { Authorization: `Bearer ${token}` } }
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
)
|
)
|
||||||
toast.success('Akses Pro berhasil diberikan!')
|
toast.success('Pro access granted successfully!')
|
||||||
fetchUsers()
|
fetchUsers()
|
||||||
setGrantProDialog({ open: false, userId: '' })
|
setGrantProDialog({ open: false, userId: '' })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to grant pro access:', error)
|
console.error('Failed to grant pro access:', error)
|
||||||
toast.error('Gagal memberikan akses Pro')
|
toast.error('Failed to grant Pro access')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openUserDialog = (mode: 'create' | 'edit', user?: User) => {
|
||||||
|
setUserDialog({ open: true, mode, user })
|
||||||
|
if (mode === 'edit' && user) {
|
||||||
|
setFormData({
|
||||||
|
email: user.email,
|
||||||
|
password: '',
|
||||||
|
name: user.name || '',
|
||||||
|
role: user.role,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setFormData({ email: '', password: '', name: '', role: 'user' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveUser = async () => {
|
||||||
|
if (!formData.email) {
|
||||||
|
toast.error('Email is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userDialog.mode === 'create' && !formData.password) {
|
||||||
|
toast.error('Password is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
|
||||||
|
if (userDialog.mode === 'create') {
|
||||||
|
await axios.post(
|
||||||
|
`${API_URL}/api/admin/users`,
|
||||||
|
formData,
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
)
|
||||||
|
toast.success('User created successfully!')
|
||||||
|
} else {
|
||||||
|
const updateData: any = {
|
||||||
|
email: formData.email,
|
||||||
|
name: formData.name || null,
|
||||||
|
role: formData.role,
|
||||||
|
}
|
||||||
|
await axios.put(
|
||||||
|
`${API_URL}/api/admin/users/${userDialog.user?.id}`,
|
||||||
|
updateData,
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
)
|
||||||
|
toast.success('User updated successfully!')
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchUsers()
|
||||||
|
setUserDialog({ open: false, mode: 'create' })
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to save user:', error)
|
||||||
|
const message = error.response?.data?.message || 'Failed to save user'
|
||||||
|
toast.error(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDeleteDialog = (userId: string, userName: string) => {
|
||||||
|
setDeleteDialog({ open: true, userId, userName })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
await axios.delete(
|
||||||
|
`${API_URL}/api/admin/users/${deleteDialog.userId}`,
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
)
|
||||||
|
toast.success('User deleted successfully!')
|
||||||
|
fetchUsers()
|
||||||
|
setDeleteDialog({ open: false, userId: '', userName: '' })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete user:', error)
|
||||||
|
toast.error('Failed to delete user')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-muted-foreground">Memuat...</div>
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-8">
|
<div className="mb-8 flex items-center justify-between">
|
||||||
<h1 className="text-3xl font-bold text-foreground">
|
<div>
|
||||||
Kelola Users
|
<h1 className="text-3xl font-bold text-foreground">
|
||||||
</h1>
|
Manage Users
|
||||||
<p className="mt-2 text-muted-foreground">
|
</h1>
|
||||||
Kelola akun dan izin pengguna
|
<p className="mt-2 text-muted-foreground">
|
||||||
</p>
|
Manage user accounts and permissions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => openUserDialog('create')}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Add User
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
@@ -217,6 +311,13 @@ export function AdminUsers() {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => openUserDialog('edit', user)}
|
||||||
|
className="text-primary hover:text-primary/80"
|
||||||
|
title="Edit User"
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4 inline" />
|
||||||
|
</button>
|
||||||
{user.suspendedAt ? (
|
{user.suspendedAt ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => openSuspendDialog(user.id, false)}
|
onClick={() => openSuspendDialog(user.id, false)}
|
||||||
@@ -241,6 +342,13 @@ export function AdminUsers() {
|
|||||||
>
|
>
|
||||||
<Crown className="h-4 w-4 inline" />
|
<Crown className="h-4 w-4 inline" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => openDeleteDialog(user.id, user.name || user.email)}
|
||||||
|
className="text-destructive hover:text-destructive/80"
|
||||||
|
title="Delete User"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 inline" />
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -251,7 +359,7 @@ export function AdminUsers() {
|
|||||||
|
|
||||||
{users.length === 0 && (
|
{users.length === 0 && (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<p className="text-muted-foreground">Tidak ada user</p>
|
<p className="text-muted-foreground">No users found</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -317,6 +425,93 @@ export function AdminUsers() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Create/Edit User Dialog */}
|
||||||
|
<Dialog open={userDialog.open} onOpenChange={(open) => setUserDialog({ ...userDialog, open })}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{userDialog.mode === 'create' ? 'Create New User' : 'Edit User'}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{userDialog.mode === 'create'
|
||||||
|
? 'Add a new user to the system.'
|
||||||
|
: 'Update user information.'}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">Email *</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
|
placeholder="user@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{userDialog.mode === 'create' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password">Password *</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||||
|
placeholder="Enter password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
placeholder="Full name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="role">Role</Label>
|
||||||
|
<Select value={formData.role} onValueChange={(value) => setFormData({ ...formData, role: value })}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="user">User</SelectItem>
|
||||||
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setUserDialog({ open: false, mode: 'create' })}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSaveUser}>
|
||||||
|
{userDialog.mode === 'create' ? 'Create User' : 'Update User'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
<Dialog open={deleteDialog.open} onOpenChange={(open) => setDeleteDialog({ ...deleteDialog, open })}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete User</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete <strong>{deleteDialog.userName}</strong>? This action cannot be undone and will delete all associated data including wallets and transactions.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setDeleteDialog({ open: false, userId: '', userName: '' })}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={handleDelete}>
|
||||||
|
Delete User
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Globe, Database, Save } from 'lucide-react'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'
|
||||||
|
|
||||||
|
interface GeneralSettings {
|
||||||
|
appName: string
|
||||||
|
appUrl: string
|
||||||
|
supportEmail: string
|
||||||
|
maintenanceMode: boolean
|
||||||
|
maintenanceMessage: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdminSettingsGeneral() {
|
||||||
|
const [settings, setSettings] = useState<GeneralSettings>({
|
||||||
|
appName: 'Tabungin',
|
||||||
|
appUrl: 'https://tabungin.app',
|
||||||
|
supportEmail: 'support@tabungin.app',
|
||||||
|
maintenanceMode: false,
|
||||||
|
maintenanceMessage: 'System is under maintenance. Please try again later.',
|
||||||
|
})
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSettings()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchSettings = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await axios.get(`${API_URL}/api/admin/config/by-category`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
|
||||||
|
const configData = response.data
|
||||||
|
const settingsObj: GeneralSettings = {
|
||||||
|
appName: configData.general?.find((c: any) => c.key === 'app_name')?.value || 'Tabungin',
|
||||||
|
appUrl: configData.general?.find((c: any) => c.key === 'app_url')?.value || 'https://tabungin.app',
|
||||||
|
supportEmail: configData.general?.find((c: any) => c.key === 'support_email')?.value || 'support@tabungin.app',
|
||||||
|
maintenanceMode: configData.system?.find((c: any) => c.key === 'maintenance_mode')?.value === 'true',
|
||||||
|
maintenanceMessage: configData.system?.find((c: any) => c.key === 'maintenance_message')?.value || 'System is under maintenance. Please try again later.',
|
||||||
|
}
|
||||||
|
setSettings(settingsObj)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch settings:', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
setSaving(true)
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
|
||||||
|
const configUpdates = [
|
||||||
|
{ key: 'app_name', value: settings.appName, category: 'general', label: 'Application Name', type: 'text' },
|
||||||
|
{ key: 'app_url', value: settings.appUrl, category: 'general', label: 'Application URL', type: 'text' },
|
||||||
|
{ key: 'support_email', value: settings.supportEmail, category: 'general', label: 'Support Email', type: 'email' },
|
||||||
|
{ key: 'maintenance_mode', value: String(settings.maintenanceMode), category: 'system', label: 'Maintenance Mode', type: 'boolean' },
|
||||||
|
{ key: 'maintenance_message', value: settings.maintenanceMessage, category: 'system', label: 'Maintenance Message', type: 'text' },
|
||||||
|
]
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
configUpdates.map((config) =>
|
||||||
|
axios.post(`${API_URL}/api/admin/config/${config.key}`, config, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
toast.success('Settings saved successfully')
|
||||||
|
fetchSettings()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save settings:', error)
|
||||||
|
toast.error('Failed to save settings')
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (field: keyof GeneralSettings, value: string | boolean) => {
|
||||||
|
setSettings((prev) => ({ ...prev, [field]: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* General Settings Card */}
|
||||||
|
<div className="bg-card rounded-xl border border-border p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="p-2 rounded-lg bg-primary/10">
|
||||||
|
<Globe className="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground">General Settings</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">Basic application information</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
|
Application Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={settings.appName}
|
||||||
|
onChange={(e) => handleChange('appName', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
|
Application URL
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="url"
|
||||||
|
value={settings.appUrl}
|
||||||
|
onChange={(e) => handleChange('appUrl', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
|
Support Email
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
value={settings.supportEmail}
|
||||||
|
onChange={(e) => handleChange('supportEmail', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Maintenance Mode Card */}
|
||||||
|
<div className="bg-card rounded-xl border border-border p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="p-2 rounded-lg bg-primary/10">
|
||||||
|
<Database className="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground">Maintenance Mode</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Temporarily disable access for maintenance
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between p-4 rounded-lg bg-destructive/10 border border-destructive/20">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-foreground">Maintenance Mode</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Enable to temporarily close access
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={settings.maintenanceMode}
|
||||||
|
onCheckedChange={(checked) => handleChange('maintenanceMode', checked)}
|
||||||
|
className="data-[state=checked]:bg-destructive"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{settings.maintenanceMode && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
|
Maintenance Message
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
value={settings.maintenanceMessage}
|
||||||
|
onChange={(e) => handleChange('maintenanceMessage', e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Save className="h-5 w-5" />
|
||||||
|
{saving ? 'Saving...' : 'Save Settings'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// This component wraps the existing AdminPaymentMethods component
|
||||||
|
// to be used as a tab content in the Settings page
|
||||||
|
|
||||||
|
import { AdminPaymentMethods } from '../AdminPaymentMethods'
|
||||||
|
|
||||||
|
export function AdminSettingsPaymentMethods() {
|
||||||
|
return <AdminPaymentMethods />
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Shield, Save } from 'lucide-react'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'
|
||||||
|
|
||||||
|
interface SecuritySettings {
|
||||||
|
enableRegistration: boolean
|
||||||
|
enableEmailVerification: boolean
|
||||||
|
enablePaymentVerification: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdminSettingsSecurity() {
|
||||||
|
const [settings, setSettings] = useState<SecuritySettings>({
|
||||||
|
enableRegistration: true,
|
||||||
|
enableEmailVerification: true,
|
||||||
|
enablePaymentVerification: true,
|
||||||
|
})
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSettings()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchSettings = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await axios.get(`${API_URL}/api/admin/config/by-category`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
|
||||||
|
const configData = response.data
|
||||||
|
const settingsObj: SecuritySettings = {
|
||||||
|
enableRegistration: configData.features?.find((c: any) => c.key === 'enable_registration')?.value === 'true',
|
||||||
|
enableEmailVerification: configData.features?.find((c: any) => c.key === 'enable_email_verification')?.value === 'true',
|
||||||
|
enablePaymentVerification: configData.features?.find((c: any) => c.key === 'enable_payment_verification')?.value === 'true',
|
||||||
|
}
|
||||||
|
setSettings(settingsObj)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch settings:', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
setSaving(true)
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
|
||||||
|
const configUpdates = [
|
||||||
|
{ key: 'enable_registration', value: String(settings.enableRegistration), category: 'features', label: 'New User Registration', type: 'boolean' },
|
||||||
|
{ key: 'enable_email_verification', value: String(settings.enableEmailVerification), category: 'features', label: 'Email Verification', type: 'boolean' },
|
||||||
|
{ key: 'enable_payment_verification', value: String(settings.enablePaymentVerification), category: 'features', label: 'Payment Verification', type: 'boolean' },
|
||||||
|
]
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
configUpdates.map((config) =>
|
||||||
|
axios.post(`${API_URL}/api/admin/config/${config.key}`, config, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
toast.success('Settings saved successfully')
|
||||||
|
fetchSettings()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save settings:', error)
|
||||||
|
toast.error('Failed to save settings')
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (field: keyof SecuritySettings, value: boolean) => {
|
||||||
|
setSettings((prev) => ({ ...prev, [field]: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Feature Toggles Card */}
|
||||||
|
<div className="bg-card rounded-xl border border-border p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="p-2 rounded-lg bg-primary/10">
|
||||||
|
<Shield className="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground">Features & Security</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">Enable or disable features</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-foreground">New User Registration</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Allow new users to register
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={settings.enableRegistration}
|
||||||
|
onCheckedChange={(checked) => handleChange('enableRegistration', checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-foreground">Email Verification</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Require email verification for new users
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={settings.enableEmailVerification}
|
||||||
|
onCheckedChange={(checked) => handleChange('enableEmailVerification', checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-foreground">Manual Payment Verification</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Enable manual verification for payments
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={settings.enablePaymentVerification}
|
||||||
|
onCheckedChange={(checked) => handleChange('enablePaymentVerification', checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Save className="h-5 w-5" />
|
||||||
|
{saving ? 'Saving...' : 'Save Settings'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,32 +1,64 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { useAuth } from '@/contexts/AuthContext'
|
|
||||||
import { Loader2 } from 'lucide-react'
|
import { Loader2 } from 'lucide-react'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'
|
||||||
|
|
||||||
export function AuthCallback() {
|
export function AuthCallback() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const { updateUser } = useAuth()
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = searchParams.get('token')
|
const handleCallback = async () => {
|
||||||
|
const token = searchParams.get('token')
|
||||||
if (token) {
|
|
||||||
// Store token and redirect to dashboard
|
if (!token) {
|
||||||
localStorage.setItem('token', token)
|
navigate('/auth/login')
|
||||||
// Force reload to trigger auth context
|
return
|
||||||
window.location.href = '/'
|
}
|
||||||
} else {
|
|
||||||
// No token, redirect to login
|
try {
|
||||||
navigate('/auth/login')
|
// Store token
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
|
||||||
|
// Fetch user to check role
|
||||||
|
const response = await axios.get(`${API_URL}/api/auth/me`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Redirect based on role
|
||||||
|
if (response.data.role === 'admin') {
|
||||||
|
window.location.href = '/admin'
|
||||||
|
} else {
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch user:', err)
|
||||||
|
setError('Failed to complete sign in')
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
setTimeout(() => navigate('/auth/login'), 2000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [searchParams, navigate, updateUser])
|
|
||||||
|
handleCallback()
|
||||||
|
}, [searchParams, navigate])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" />
|
{error ? (
|
||||||
<p className="text-gray-600">Completing sign in...</p>
|
<>
|
||||||
|
<p className="text-red-600 mb-4">{error}</p>
|
||||||
|
<p className="text-gray-600">Redirecting to login...</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" />
|
||||||
|
<p className="text-gray-600">Completing sign in...</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
60
apps/web/src/components/pages/MaintenancePage.tsx
Normal file
60
apps/web/src/components/pages/MaintenancePage.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { AlertTriangle, RefreshCw } from 'lucide-react'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
interface MaintenancePageProps {
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MaintenancePage({ message }: MaintenancePageProps) {
|
||||||
|
const defaultMessage = 'System is under maintenance. Please try again later.'
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
||||||
|
<div className="max-w-md w-full text-center space-y-6">
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="p-6 rounded-full bg-yellow-500/10">
|
||||||
|
<AlertTriangle className="h-16 w-16 text-yellow-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h1 className="text-3xl font-bold text-foreground">
|
||||||
|
Under Maintenance
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground text-lg">
|
||||||
|
{message || defaultMessage}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div className="p-4 rounded-lg bg-muted/50 border border-border">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
We're currently performing scheduled maintenance to improve your experience.
|
||||||
|
We'll be back online shortly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Refresh Button */}
|
||||||
|
<Button
|
||||||
|
onClick={handleRefresh}
|
||||||
|
className="w-full"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-5 w-5 mr-2" />
|
||||||
|
Refresh Page
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Thank you for your patience
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -57,9 +57,13 @@ export function OtpVerification() {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await verifyOtp(tempToken, code, method)
|
const result = await verifyOtp(tempToken, code, method)
|
||||||
// Verification successful, redirect to dashboard
|
// Verification successful, redirect based on role
|
||||||
navigate('/')
|
if (result.user?.role === 'admin') {
|
||||||
|
navigate('/admin')
|
||||||
|
} else {
|
||||||
|
navigate('/')
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as { response?: { data?: { message?: string } } }
|
const error = err as { response?: { data?: { message?: string } } }
|
||||||
setError(error.response?.data?.message || 'Invalid OTP code. Please try again.')
|
setError(error.response?.data?.message || 'Invalid OTP code. Please try again.')
|
||||||
|
|||||||
@@ -498,14 +498,14 @@ export function Profile() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="mx-auto">
|
||||||
<h1 className="text-3xl font-bold">{t.profile.title}</h1>
|
<h1 className="text-3xl font-bold">{t.profile.title}</h1>
|
||||||
<p className="text-muted-foreground">{t.profile.description}</p>
|
<p className="text-muted-foreground">{t.profile.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="mx-auto">
|
||||||
<Tabs defaultValue="profile" className="w-full">
|
<Tabs defaultValue="profile" className="w-full">
|
||||||
<TabsList className="grid w-[50%] grid-cols-2 h-auto p-1">
|
<TabsList className="grid md:w-[40%] grid-cols-2 h-auto p-1">
|
||||||
<TabsTrigger value="profile" className="h-11 md:h-9 text-base md:text-sm data-[state=active]:bg-background">
|
<TabsTrigger value="profile" className="h-11 md:h-9 text-base md:text-sm data-[state=active]:bg-background">
|
||||||
{t.profile.editProfile}
|
{t.profile.editProfile}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -516,6 +516,7 @@ export function Profile() {
|
|||||||
|
|
||||||
{/* Edit Profile Tab */}
|
{/* Edit Profile Tab */}
|
||||||
<TabsContent value="profile" className="w-full space-y-6">
|
<TabsContent value="profile" className="w-full space-y-6">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t.profile.personalInfo}</CardTitle>
|
<CardTitle>{t.profile.personalInfo}</CardTitle>
|
||||||
@@ -679,6 +680,7 @@ export function Profile() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* Security Tab */}
|
{/* Security Tab */}
|
||||||
@@ -720,7 +722,7 @@ export function Profile() {
|
|||||||
<Input
|
<Input
|
||||||
id="current-password"
|
id="current-password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="******"
|
placeholder="••••••"
|
||||||
value={currentPassword}
|
value={currentPassword}
|
||||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
disabled={passwordLoading}
|
disabled={passwordLoading}
|
||||||
@@ -733,7 +735,7 @@ export function Profile() {
|
|||||||
<Input
|
<Input
|
||||||
id="new-password"
|
id="new-password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="******"
|
placeholder="••••••"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
disabled={passwordLoading}
|
disabled={passwordLoading}
|
||||||
@@ -745,7 +747,7 @@ export function Profile() {
|
|||||||
<Input
|
<Input
|
||||||
id="confirm-password"
|
id="confirm-password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="******"
|
placeholder="••••••"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
disabled={passwordLoading}
|
disabled={passwordLoading}
|
||||||
@@ -782,6 +784,8 @@ export function Profile() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
{/* WhatsApp OTP */}
|
{/* WhatsApp OTP */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -794,7 +798,7 @@ export function Profile() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={otpStatus.whatsappEnabled ? "default" : "secondary"}>
|
<Badge variant={otpStatus.whatsappEnabled ? "default" : "secondary"} className="text-nowrap">
|
||||||
{otpStatus.whatsappEnabled ? t.profile.enabled : t.profile.disabled}
|
{otpStatus.whatsappEnabled ? t.profile.enabled : t.profile.disabled}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -895,7 +899,7 @@ export function Profile() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={otpStatus.emailEnabled ? "default" : "secondary"}>
|
<Badge variant={otpStatus.emailEnabled ? "default" : "secondary"} className="text-nowrap">
|
||||||
{otpStatus.emailEnabled ? t.profile.enabled : t.profile.disabled}
|
{otpStatus.emailEnabled ? t.profile.enabled : t.profile.disabled}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -974,7 +978,7 @@ export function Profile() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={otpStatus.totpEnabled ? "default" : "secondary"}>
|
<Badge variant={otpStatus.totpEnabled ? "default" : "secondary"} className="text-nowrap">
|
||||||
{otpStatus.totpEnabled ? t.profile.enabled : t.profile.disabled}
|
{otpStatus.totpEnabled ? t.profile.enabled : t.profile.disabled}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
24
apps/web/src/utils/axiosSetup.ts
Normal file
24
apps/web/src/utils/axiosSetup.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
let maintenanceCallback: ((message: string) => void) | null = null
|
||||||
|
|
||||||
|
export function setupAxiosInterceptors(onMaintenance: (message: string) => void) {
|
||||||
|
maintenanceCallback = onMaintenance
|
||||||
|
|
||||||
|
// Response interceptor to handle maintenance mode
|
||||||
|
axios.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
// Check if it's a maintenance mode error (503)
|
||||||
|
if (error.response?.status === 503 && error.response?.data?.maintenanceMode) {
|
||||||
|
const message = error.response.data.message || 'System is under maintenance. Please try again later.'
|
||||||
|
if (maintenanceCallback) {
|
||||||
|
maintenanceCallback(message)
|
||||||
|
}
|
||||||
|
// Prevent further error handling
|
||||||
|
return Promise.reject({ maintenanceMode: true, message })
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
86
docs/README.md
Normal file
86
docs/README.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Tabungin Documentation
|
||||||
|
|
||||||
|
Welcome to the Tabungin documentation! This guide will help you understand the project structure, features, and development workflow.
|
||||||
|
|
||||||
|
## 📚 Documentation Structure
|
||||||
|
|
||||||
|
### Features
|
||||||
|
Detailed documentation for each feature implementation:
|
||||||
|
- [Maintenance Mode](./features/maintenance-mode.md) - System maintenance mode with admin bypass
|
||||||
|
- [Admin Auto-Redirect](./features/admin-auto-redirect.md) - Automatic admin routing after login
|
||||||
|
- [Admin Profile & Dashboard Blocking](./features/admin-profile-reuse.md) - Profile page reuse and access control
|
||||||
|
- [Admin Settings Reorganization](./features/admin-settings-tabs.md) - Tabbed settings interface
|
||||||
|
|
||||||
|
### Guides
|
||||||
|
Step-by-step guides for common tasks:
|
||||||
|
- [Testing Guide](./guides/testing-guide.md) - How to test features
|
||||||
|
- [Development Setup](./guides/development-setup.md) - Getting started with development
|
||||||
|
|
||||||
|
### Planning
|
||||||
|
Project planning and roadmap:
|
||||||
|
- [To-Do List](./planning/todo.md) - Upcoming tasks and features
|
||||||
|
- [Technical Q&A](./planning/tech-qa.md) - Technical decisions and answers
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd Tabungin
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Setup environment**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run development servers**
|
||||||
|
```bash
|
||||||
|
# Terminal 1: API
|
||||||
|
cd apps/api
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Terminal 2: Web
|
||||||
|
cd apps/web
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Tabungin/
|
||||||
|
├── apps/
|
||||||
|
│ ├── api/ # NestJS backend
|
||||||
|
│ └── web/ # React frontend
|
||||||
|
├── docs/ # Documentation
|
||||||
|
│ ├── features/ # Feature documentation
|
||||||
|
│ ├── guides/ # How-to guides
|
||||||
|
│ └── planning/ # Project planning
|
||||||
|
└── README.md # Project overview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔗 Useful Links
|
||||||
|
|
||||||
|
- [Main README](../README.md) - Project overview
|
||||||
|
- [API Documentation](../apps/api/README.md) - Backend API docs
|
||||||
|
- [Web Documentation](../apps/web/README.md) - Frontend docs
|
||||||
|
|
||||||
|
## 📝 Contributing
|
||||||
|
|
||||||
|
When adding new features:
|
||||||
|
1. Create feature documentation in `docs/features/`
|
||||||
|
2. Update testing guide if needed
|
||||||
|
3. Add to to-do list or mark as complete
|
||||||
|
4. Update this index
|
||||||
|
|
||||||
|
## 🆘 Need Help?
|
||||||
|
|
||||||
|
- Check the [Testing Guide](./guides/testing-guide.md)
|
||||||
|
- Review [Technical Q&A](./planning/tech-qa.md)
|
||||||
|
- See feature-specific documentation in `docs/features/`
|
||||||
248
docs/features/admin-auto-redirect.md
Normal file
248
docs/features/admin-auto-redirect.md
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
# Admin Auto-Redirect Implementation
|
||||||
|
|
||||||
|
## ✅ **COMPLETE: Admins Now Redirect to /admin Automatically**
|
||||||
|
|
||||||
|
### **Overview**
|
||||||
|
Modified all authentication flows to automatically redirect admin users to `/admin` instead of the member dashboard (`/`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **📝 Changes Made**
|
||||||
|
|
||||||
|
### **1. Login Page** (`/apps/web/src/components/pages/Login.tsx`)
|
||||||
|
**Already Implemented** ✅
|
||||||
|
- Lines 42-46: Checks `result.user?.role === 'admin'`
|
||||||
|
- Redirects admins to `/admin`
|
||||||
|
- Redirects regular users to `/`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (result.user?.role === 'admin') {
|
||||||
|
navigate('/admin')
|
||||||
|
} else {
|
||||||
|
navigate('/')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2. OTP Verification** (`/apps/web/src/components/pages/OtpVerification.tsx`)
|
||||||
|
**Fixed** ✅
|
||||||
|
- Lines 60-66: Now checks user role after OTP verification
|
||||||
|
- Redirects based on role
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```typescript
|
||||||
|
await verifyOtp(tempToken, code, method)
|
||||||
|
navigate('/') // Always went to member dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```typescript
|
||||||
|
const result = await verifyOtp(tempToken, code, method)
|
||||||
|
if (result.user?.role === 'admin') {
|
||||||
|
navigate('/admin')
|
||||||
|
} else {
|
||||||
|
navigate('/')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **3. Auth Callback (Google OAuth)** (`/apps/web/src/components/pages/AuthCallback.tsx`)
|
||||||
|
**Fixed** ✅
|
||||||
|
- Lines 22-36: Fetches user data to check role
|
||||||
|
- Redirects based on role
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```typescript
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
window.location.href = '/' // Always went to member dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```typescript
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
|
||||||
|
// Fetch user to check role
|
||||||
|
const response = await axios.get(`${API_URL}/api/auth/me`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Redirect based on role
|
||||||
|
if (response.data.role === 'admin') {
|
||||||
|
window.location.href = '/admin'
|
||||||
|
} else {
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **4. Public Route Guard** (`/apps/web/src/App.tsx`)
|
||||||
|
**Fixed** ✅
|
||||||
|
- Lines 55-58: Prevents logged-in users from accessing login/register
|
||||||
|
- Now redirects admins to `/admin` instead of `/`
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```typescript
|
||||||
|
if (user) {
|
||||||
|
return <Navigate to="/" replace /> // Always went to member dashboard
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```typescript
|
||||||
|
if (user) {
|
||||||
|
// Redirect based on role
|
||||||
|
const redirectTo = user.role === 'admin' ? '/admin' : '/'
|
||||||
|
return <Navigate to={redirectTo} replace />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🎯 User Flows**
|
||||||
|
|
||||||
|
### **Admin Login Flow:**
|
||||||
|
```
|
||||||
|
1. Admin goes to /auth/login
|
||||||
|
2. Enters credentials
|
||||||
|
3. Clicks "Sign In"
|
||||||
|
↓
|
||||||
|
4a. If NO OTP required:
|
||||||
|
→ Redirected to /admin ✅
|
||||||
|
|
||||||
|
4b. If OTP required:
|
||||||
|
→ Goes to /auth/otp
|
||||||
|
→ Enters OTP code
|
||||||
|
→ Redirected to /admin ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Admin Google OAuth Flow:**
|
||||||
|
```
|
||||||
|
1. Admin clicks "Continue with Google"
|
||||||
|
2. Completes Google authentication
|
||||||
|
3. Redirected to /auth/callback
|
||||||
|
↓
|
||||||
|
4a. If NO OTP required:
|
||||||
|
→ Redirected to /admin ✅
|
||||||
|
|
||||||
|
4b. If OTP required:
|
||||||
|
→ Goes to /auth/otp
|
||||||
|
→ Enters OTP code
|
||||||
|
→ Redirected to /admin ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Admin Already Logged In:**
|
||||||
|
```
|
||||||
|
1. Admin is already logged in
|
||||||
|
2. Admin tries to access /auth/login or /auth/register
|
||||||
|
↓
|
||||||
|
3. PublicRoute guard intercepts
|
||||||
|
4. Redirected to /admin ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Regular User Flows:**
|
||||||
|
All regular users continue to be redirected to `/` (member dashboard) as before.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **📊 Redirect Matrix**
|
||||||
|
|
||||||
|
| Scenario | User Type | Destination |
|
||||||
|
|----------|-----------|-------------|
|
||||||
|
| Login (no OTP) | Admin | `/admin` ✅ |
|
||||||
|
| Login (no OTP) | User | `/` |
|
||||||
|
| Login → OTP | Admin | `/admin` ✅ |
|
||||||
|
| Login → OTP | User | `/` |
|
||||||
|
| Google OAuth (no OTP) | Admin | `/admin` ✅ |
|
||||||
|
| Google OAuth (no OTP) | User | `/` |
|
||||||
|
| Google OAuth → OTP | Admin | `/admin` ✅ |
|
||||||
|
| Google OAuth → OTP | User | `/` |
|
||||||
|
| Already logged in → /auth/login | Admin | `/admin` ✅ |
|
||||||
|
| Already logged in → /auth/login | User | `/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🧪 Testing Instructions**
|
||||||
|
|
||||||
|
### **Test 1: Admin Email Login**
|
||||||
|
```
|
||||||
|
1. Go to http://localhost:5174/auth/login
|
||||||
|
2. Login with admin credentials
|
||||||
|
3. Verify redirected to /admin ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Test 2: Admin Google Login**
|
||||||
|
```
|
||||||
|
1. Go to http://localhost:5174/auth/login
|
||||||
|
2. Click "Continue with Google"
|
||||||
|
3. Complete Google authentication
|
||||||
|
4. Verify redirected to /admin ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Test 3: Admin with OTP**
|
||||||
|
```
|
||||||
|
1. Login as admin (with OTP enabled)
|
||||||
|
2. Enter OTP code
|
||||||
|
3. Verify redirected to /admin ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Test 4: Admin Already Logged In**
|
||||||
|
```
|
||||||
|
1. Login as admin
|
||||||
|
2. Manually navigate to /auth/login
|
||||||
|
3. Verify redirected to /admin ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Test 5: Regular User Login**
|
||||||
|
```
|
||||||
|
1. Login as regular user
|
||||||
|
2. Verify redirected to / (member dashboard) ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **📁 Files Modified**
|
||||||
|
|
||||||
|
1. `/apps/web/src/components/pages/OtpVerification.tsx`
|
||||||
|
- Added role check after OTP verification
|
||||||
|
- Conditional redirect based on role
|
||||||
|
|
||||||
|
2. `/apps/web/src/components/pages/AuthCallback.tsx`
|
||||||
|
- Fetches user data to determine role
|
||||||
|
- Conditional redirect based on role
|
||||||
|
- Added error handling
|
||||||
|
|
||||||
|
3. `/apps/web/src/App.tsx`
|
||||||
|
- Updated PublicRoute to redirect admins to /admin
|
||||||
|
- Regular users still go to /
|
||||||
|
|
||||||
|
4. `/apps/web/src/components/pages/Login.tsx`
|
||||||
|
- Already had role-based redirect (no changes needed)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **✅ Verification Checklist**
|
||||||
|
|
||||||
|
- [x] Admin email login → /admin
|
||||||
|
- [x] Admin Google login → /admin
|
||||||
|
- [x] Admin with OTP → /admin
|
||||||
|
- [x] Admin already logged in → /admin (when accessing login page)
|
||||||
|
- [x] Regular user login → /
|
||||||
|
- [x] Regular user with OTP → /
|
||||||
|
- [x] Regular user already logged in → / (when accessing login page)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🎉 Summary**
|
||||||
|
|
||||||
|
**All authentication flows now correctly redirect admins to `/admin`!**
|
||||||
|
|
||||||
|
- ✅ Email/password login
|
||||||
|
- ✅ Google OAuth login
|
||||||
|
- ✅ OTP verification
|
||||||
|
- ✅ Already logged-in guard
|
||||||
|
- ✅ No changes to regular user flows
|
||||||
|
|
||||||
|
Admins will now land on the admin dashboard immediately after login, providing a better UX and clearer separation between admin and member interfaces.
|
||||||
292
docs/features/admin-profile-reuse.md
Normal file
292
docs/features/admin-profile-reuse.md
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
# Admin Profile Page & Dashboard Blocking
|
||||||
|
|
||||||
|
## ✅ **COMPLETE: Profile Page Reused for Admin + Member Dashboard Blocked**
|
||||||
|
|
||||||
|
### **Overview**
|
||||||
|
- Reused the existing Profile page for admin users
|
||||||
|
- Added Profile menu item to admin sidebar
|
||||||
|
- Blocked admins from accessing the member dashboard
|
||||||
|
- Admins are automatically redirected to `/admin` if they try to access member routes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **📝 Changes Made**
|
||||||
|
|
||||||
|
### **1. Added Profile to Admin Sidebar** (`AdminSidebar.tsx`)
|
||||||
|
**Changes:**
|
||||||
|
- Added `UserCircle` icon import
|
||||||
|
- Added Profile menu item between Users and Settings
|
||||||
|
- Profile route: `/admin/profile`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
title: 'Profile',
|
||||||
|
url: '/admin/profile',
|
||||||
|
icon: UserCircle,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2. Added Profile Route to Admin Panel** (`App.tsx`)
|
||||||
|
**Changes:**
|
||||||
|
- Imported Profile component
|
||||||
|
- Added route: `/admin/profile`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<Route path="/admin" element={<ProtectedRoute><AdminLayout /></ProtectedRoute>}>
|
||||||
|
<Route index element={<AdminDashboard />} />
|
||||||
|
<Route path="plans" element={<AdminPlans />} />
|
||||||
|
<Route path="payment-methods" element={<AdminPaymentMethods />} />
|
||||||
|
<Route path="payments" element={<AdminPayments />} />
|
||||||
|
<Route path="users" element={<AdminUsers />} />
|
||||||
|
<Route path="profile" element={<Profile />} /> ✅ NEW
|
||||||
|
<Route path="settings" element={<AdminSettings />} />
|
||||||
|
</Route>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **3. Blocked Admins from Member Dashboard** (`Dashboard.tsx`)
|
||||||
|
**Changes:**
|
||||||
|
- Added auth check at the top of Dashboard component
|
||||||
|
- Redirects admins to `/admin` if they try to access member routes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function Dashboard() {
|
||||||
|
const { user } = useAuth()
|
||||||
|
|
||||||
|
// Block admins from accessing member dashboard
|
||||||
|
if (user?.role === 'admin') {
|
||||||
|
return <Navigate to="/admin" replace />
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... rest of component
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **4. Translated AdminLayout** (`AdminLayout.tsx`)
|
||||||
|
**Changes:**
|
||||||
|
- "Akses Ditolak" → "Access Denied"
|
||||||
|
- "Anda tidak memiliki izin..." → "You don't have permission..."
|
||||||
|
- "Kembali ke Dashboard" → "Back to Dashboard"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🎯 User Flows**
|
||||||
|
|
||||||
|
### **Admin Accessing Profile:**
|
||||||
|
```
|
||||||
|
Admin logged in
|
||||||
|
↓
|
||||||
|
Click "Profile" in sidebar
|
||||||
|
↓
|
||||||
|
Navigate to /admin/profile ✅
|
||||||
|
↓
|
||||||
|
See Profile page with all settings
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Admin Trying to Access Member Dashboard:**
|
||||||
|
```
|
||||||
|
Admin logged in
|
||||||
|
↓
|
||||||
|
Try to navigate to / (member dashboard)
|
||||||
|
↓
|
||||||
|
Dashboard component checks role
|
||||||
|
↓
|
||||||
|
Redirected to /admin ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Admin Trying Member Routes:**
|
||||||
|
```
|
||||||
|
Admin tries:
|
||||||
|
- / → Redirected to /admin ✅
|
||||||
|
- /wallets → Redirected to /admin ✅
|
||||||
|
- /transactions → Redirected to /admin ✅
|
||||||
|
- /profile → Redirected to /admin ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Regular User:**
|
||||||
|
```
|
||||||
|
User logged in
|
||||||
|
↓
|
||||||
|
Access / → Works normally ✅
|
||||||
|
Access /wallets → Works normally ✅
|
||||||
|
Access /transactions → Works normally ✅
|
||||||
|
Access /profile → Works normally ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **📊 Route Access Matrix**
|
||||||
|
|
||||||
|
| Route | Admin Access | User Access |
|
||||||
|
|-------|--------------|-------------|
|
||||||
|
| `/` | ❌ Redirect to /admin | ✅ Member Dashboard |
|
||||||
|
| `/wallets` | ❌ Redirect to /admin | ✅ Wallets Page |
|
||||||
|
| `/transactions` | ❌ Redirect to /admin | ✅ Transactions Page |
|
||||||
|
| `/profile` | ❌ Redirect to /admin | ✅ Profile Page |
|
||||||
|
| `/admin` | ✅ Admin Dashboard | ❌ Access Denied |
|
||||||
|
| `/admin/profile` | ✅ Profile Page | ❌ Access Denied |
|
||||||
|
| `/admin/*` | ✅ All admin routes | ❌ Access Denied |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🎨 Admin Sidebar Menu Order**
|
||||||
|
|
||||||
|
1. 📊 Dashboard (`/admin`)
|
||||||
|
2. 💳 Plans (`/admin/plans`)
|
||||||
|
3. 💰 Payment Methods (`/admin/payment-methods`)
|
||||||
|
4. 💵 Payments (`/admin/payments`)
|
||||||
|
5. 👥 Users (`/admin/users`)
|
||||||
|
6. 👤 **Profile** (`/admin/profile`) ✅ NEW
|
||||||
|
7. ⚙️ Settings (`/admin/settings`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **✨ Benefits**
|
||||||
|
|
||||||
|
### **1. Code Reuse**
|
||||||
|
- ✅ No need to duplicate Profile component
|
||||||
|
- ✅ Same profile settings for both admin and users
|
||||||
|
- ✅ Consistent UI/UX across roles
|
||||||
|
|
||||||
|
### **2. Clear Separation**
|
||||||
|
- ✅ Admins can't access member dashboard
|
||||||
|
- ✅ Users can't access admin panel
|
||||||
|
- ✅ Automatic redirects prevent confusion
|
||||||
|
|
||||||
|
### **3. Better UX**
|
||||||
|
- ✅ Admins have dedicated profile page in admin panel
|
||||||
|
- ✅ No need to switch between interfaces
|
||||||
|
- ✅ All admin features in one place
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🧪 Testing Instructions**
|
||||||
|
|
||||||
|
### **Test 1: Admin Profile Access**
|
||||||
|
```
|
||||||
|
1. Login as admin
|
||||||
|
2. Navigate to /admin
|
||||||
|
3. Click "Profile" in sidebar
|
||||||
|
4. Verify you see the profile page ✅
|
||||||
|
5. Test all profile features (edit, OTP, etc.)
|
||||||
|
6. Verify everything works ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Test 2: Admin Dashboard Blocking**
|
||||||
|
```
|
||||||
|
1. Login as admin
|
||||||
|
2. Manually navigate to / (member dashboard)
|
||||||
|
3. Verify redirected to /admin ✅
|
||||||
|
4. Try /wallets
|
||||||
|
5. Verify redirected to /admin ✅
|
||||||
|
6. Try /transactions
|
||||||
|
7. Verify redirected to /admin ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Test 3: Regular User Access**
|
||||||
|
```
|
||||||
|
1. Login as regular user
|
||||||
|
2. Navigate to / (member dashboard)
|
||||||
|
3. Verify dashboard loads ✅
|
||||||
|
4. Navigate to /wallets
|
||||||
|
5. Verify wallets page loads ✅
|
||||||
|
6. Navigate to /profile
|
||||||
|
7. Verify profile page loads ✅
|
||||||
|
8. Try to access /admin
|
||||||
|
9. Verify "Access Denied" message ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Test 4: Profile Functionality**
|
||||||
|
```
|
||||||
|
As Admin (/admin/profile):
|
||||||
|
- ✅ Edit profile information
|
||||||
|
- ✅ Change password
|
||||||
|
- ✅ Enable/disable OTP methods
|
||||||
|
- ✅ Setup TOTP
|
||||||
|
- ✅ All features work
|
||||||
|
|
||||||
|
As User (/profile):
|
||||||
|
- ✅ Same features work
|
||||||
|
- ✅ No differences in functionality
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **📁 Files Modified**
|
||||||
|
|
||||||
|
1. **`/apps/web/src/components/admin/AdminSidebar.tsx`**
|
||||||
|
- Added UserCircle icon import
|
||||||
|
- Added Profile menu item
|
||||||
|
|
||||||
|
2. **`/apps/web/src/App.tsx`**
|
||||||
|
- Imported Profile component
|
||||||
|
- Added `/admin/profile` route
|
||||||
|
|
||||||
|
3. **`/apps/web/src/components/Dashboard.tsx`**
|
||||||
|
- Added auth check
|
||||||
|
- Blocks admins from accessing member dashboard
|
||||||
|
|
||||||
|
4. **`/apps/web/src/components/admin/AdminLayout.tsx`**
|
||||||
|
- Translated access denied message to English
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🔒 Security Notes**
|
||||||
|
|
||||||
|
### **Frontend Guards:**
|
||||||
|
- ✅ Dashboard component blocks admins
|
||||||
|
- ✅ AdminLayout blocks non-admins
|
||||||
|
- ✅ Automatic redirects prevent access
|
||||||
|
|
||||||
|
### **Backend Protection:**
|
||||||
|
- ✅ API endpoints already have role-based guards
|
||||||
|
- ✅ Admin routes require admin role
|
||||||
|
- ✅ User routes work for all authenticated users
|
||||||
|
|
||||||
|
### **Important:**
|
||||||
|
Frontend guards are for UX only. Backend API guards provide actual security. The frontend redirects just prevent confusion and provide better user experience.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **✅ Verification Checklist**
|
||||||
|
|
||||||
|
- [x] Profile menu item added to admin sidebar
|
||||||
|
- [x] Profile route added to admin panel
|
||||||
|
- [x] Profile page accessible at /admin/profile
|
||||||
|
- [x] Admins blocked from / (member dashboard)
|
||||||
|
- [x] Admins blocked from /wallets
|
||||||
|
- [x] Admins blocked from /transactions
|
||||||
|
- [x] Admins blocked from /profile (member route)
|
||||||
|
- [x] Regular users can still access all member routes
|
||||||
|
- [x] Regular users blocked from /admin routes
|
||||||
|
- [x] AdminLayout access denied message translated
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🎉 Summary**
|
||||||
|
|
||||||
|
**Profile page successfully reused for admins!**
|
||||||
|
|
||||||
|
✅ **Admins can:**
|
||||||
|
- Access profile at `/admin/profile`
|
||||||
|
- Edit their profile information
|
||||||
|
- Manage OTP settings
|
||||||
|
- Change password
|
||||||
|
- All from within the admin panel
|
||||||
|
|
||||||
|
❌ **Admins cannot:**
|
||||||
|
- Access member dashboard (`/`)
|
||||||
|
- Access member routes (`/wallets`, `/transactions`, etc.)
|
||||||
|
- They are automatically redirected to `/admin`
|
||||||
|
|
||||||
|
✅ **Regular users:**
|
||||||
|
- Continue to use profile at `/profile`
|
||||||
|
- Full access to member dashboard
|
||||||
|
- Cannot access admin panel
|
||||||
|
|
||||||
|
**Clean separation of concerns with maximum code reuse!** 🎊
|
||||||
131
docs/features/admin-settings-tabs.md
Normal file
131
docs/features/admin-settings-tabs.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# Admin Settings - Tabbed Interface
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Reorganized admin settings page into a clean tabbed interface with vertical tabs on desktop and horizontal scrollable tabs on mobile.
|
||||||
|
|
||||||
|
## Implementation Date
|
||||||
|
October 13, 2025
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
### Tab Layout
|
||||||
|
```
|
||||||
|
Settings Page
|
||||||
|
├── Tabs (Vertical on Desktop, Horizontal on Mobile)
|
||||||
|
│ ├── 🌐 General
|
||||||
|
│ ├── 🛡️ Security
|
||||||
|
│ └── 💰 Payment Methods
|
||||||
|
│
|
||||||
|
└── Content Area (Dynamic based on active tab)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tab Contents
|
||||||
|
|
||||||
|
#### 1. General Tab
|
||||||
|
- **General Settings Card**
|
||||||
|
- Application Name
|
||||||
|
- Application URL
|
||||||
|
- Support Email
|
||||||
|
|
||||||
|
- **Maintenance Mode Card**
|
||||||
|
- Maintenance Mode Toggle
|
||||||
|
- Maintenance Message (when enabled)
|
||||||
|
|
||||||
|
#### 2. Security Tab
|
||||||
|
- **Features & Security Card**
|
||||||
|
- New User Registration Toggle
|
||||||
|
- Email Verification Toggle
|
||||||
|
- Manual Payment Verification Toggle
|
||||||
|
|
||||||
|
#### 3. Payment Methods Tab
|
||||||
|
- Full payment methods management
|
||||||
|
- Drag-and-drop reordering
|
||||||
|
- Add/Edit/Delete payment methods
|
||||||
|
- Active/Inactive status toggle
|
||||||
|
|
||||||
|
## Files Created
|
||||||
|
|
||||||
|
1. **`AdminSettingsNew.tsx`**
|
||||||
|
- Main settings page with tab navigation
|
||||||
|
- Responsive layout (vertical/horizontal)
|
||||||
|
|
||||||
|
2. **`settings/AdminSettingsGeneral.tsx`**
|
||||||
|
- General settings + Maintenance mode
|
||||||
|
- Handles app configuration
|
||||||
|
|
||||||
|
3. **`settings/AdminSettingsSecurity.tsx`**
|
||||||
|
- Feature toggles
|
||||||
|
- Security settings
|
||||||
|
|
||||||
|
4. **`settings/AdminSettingsPaymentMethods.tsx`**
|
||||||
|
- Wrapper for existing AdminPaymentMethods component
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. **`App.tsx`**
|
||||||
|
- Removed `/admin/payment-methods` route
|
||||||
|
- Updated import to use `AdminSettingsNew`
|
||||||
|
|
||||||
|
2. **`AdminSidebar.tsx`**
|
||||||
|
- Removed "Payment Methods" menu item
|
||||||
|
- Consolidated under Settings
|
||||||
|
|
||||||
|
3. **`AdminPaymentMethods.tsx`**
|
||||||
|
- Changed heading from h1 to h4 for tab context
|
||||||
|
|
||||||
|
## UI/UX Improvements
|
||||||
|
|
||||||
|
### Desktop
|
||||||
|
- Vertical tabs on the left (fixed width: 256px)
|
||||||
|
- Large content area on the right
|
||||||
|
- Clear visual separation
|
||||||
|
- Icons + text labels
|
||||||
|
|
||||||
|
### Mobile
|
||||||
|
- Horizontal scrollable tabs at top
|
||||||
|
- Full-width content below
|
||||||
|
- Icons + text labels visible
|
||||||
|
- Touch-friendly tap targets
|
||||||
|
|
||||||
|
### Responsive Breakpoint
|
||||||
|
- Mobile: `< 768px` (md breakpoint)
|
||||||
|
- Desktop: `≥ 768px`
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
1. **Cleaner Navigation**
|
||||||
|
- Reduced sidebar clutter
|
||||||
|
- Grouped related settings
|
||||||
|
- Better information architecture
|
||||||
|
|
||||||
|
2. **Better UX**
|
||||||
|
- All settings in one place
|
||||||
|
- No page navigation needed
|
||||||
|
- Faster access to configuration
|
||||||
|
|
||||||
|
3. **Scalability**
|
||||||
|
- Easy to add new tabs
|
||||||
|
- Modular component structure
|
||||||
|
- Clear separation of concerns
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Desktop
|
||||||
|
- [x] Vertical tabs display correctly
|
||||||
|
- [x] Tab switching works
|
||||||
|
- [x] All settings save properly
|
||||||
|
- [x] Icons and labels visible
|
||||||
|
|
||||||
|
### Mobile
|
||||||
|
- [x] Horizontal scrollable tabs
|
||||||
|
- [x] Tab switching works
|
||||||
|
- [x] Content responsive
|
||||||
|
- [x] Touch targets adequate
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential additions:
|
||||||
|
- Notifications tab (Email/Push settings)
|
||||||
|
- Integrations tab (Third-party services)
|
||||||
|
- Appearance tab (Theme, branding)
|
||||||
|
- Advanced tab (Developer settings)
|
||||||
293
docs/features/maintenance-mode.md
Normal file
293
docs/features/maintenance-mode.md
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# Maintenance Mode - Visual Flow Diagram
|
||||||
|
|
||||||
|
## 🎯 **Where Does the Maintenance Page Appear?**
|
||||||
|
|
||||||
|
### **Answer: It appears AFTER successful login, when trying to access protected routes**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **📊 Complete User Journey**
|
||||||
|
|
||||||
|
### **Scenario 1: Non-Logged-In User (Maintenance ON)**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ User opens: http://localhost:5174 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ App.tsx checks: Is user logged in? │
|
||||||
|
│ Answer: NO │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Redirect to: /auth/login │
|
||||||
|
│ (Login page loads normally - @SkipMaintenance) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ User enters credentials and clicks Login │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ POST /auth/login (Works - @SkipMaintenance) │
|
||||||
|
│ Returns: { token: "...", user: {...} } │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Token saved to localStorage │
|
||||||
|
│ Redirect to: / │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Dashboard tries to load │
|
||||||
|
│ Makes API call: GET /api/users/me │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ MaintenanceGuard intercepts request │
|
||||||
|
│ - Checks: Is maintenance ON? YES │
|
||||||
|
│ - Checks: Is user admin? NO (role: 'user') │
|
||||||
|
│ - Returns: 503 Service Unavailable │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Axios interceptor catches 503 error │
|
||||||
|
│ Calls: setMaintenanceMode(true) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ ⚠️ MAINTENANCE PAGE DISPLAYED │
|
||||||
|
│ │
|
||||||
|
│ 🔧 Under Maintenance │
|
||||||
|
│ Your custom message here │
|
||||||
|
│ [Refresh Page] │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Scenario 2: Logged-In User (Maintenance Turns ON)**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ User is already logged in, using the app normally │
|
||||||
|
│ Currently on: /wallets │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Admin enables maintenance mode in /admin/settings │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ User clicks on "Transactions" menu │
|
||||||
|
│ OR tries to create a new transaction │
|
||||||
|
│ OR any action that makes an API call │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ API call: POST /api/transactions │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ MaintenanceGuard intercepts │
|
||||||
|
│ - Maintenance is ON │
|
||||||
|
│ - User is not admin │
|
||||||
|
│ - Returns: 503 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Axios interceptor triggers │
|
||||||
|
│ setMaintenanceMode(true) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ ⚠️ MAINTENANCE PAGE REPLACES CURRENT VIEW │
|
||||||
|
│ │
|
||||||
|
│ 🔧 Under Maintenance │
|
||||||
|
│ System is being upgraded... │
|
||||||
|
│ [Refresh Page] │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Scenario 3: Admin User (Maintenance ON)**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Admin logs in at: /auth/login │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Redirected to: / │
|
||||||
|
│ Makes API call: GET /api/users/me │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ MaintenanceGuard intercepts │
|
||||||
|
│ - Maintenance is ON │
|
||||||
|
│ - Extracts JWT token │
|
||||||
|
│ - Checks role: 'admin' │
|
||||||
|
│ - Returns: 503 (Even for admin on main app!) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ ⚠️ MAINTENANCE PAGE SHOWN │
|
||||||
|
│ │
|
||||||
|
│ (Admin sees maintenance on main app) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Admin navigates to: /admin │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Makes API call: GET /api/admin/users/stats │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ MaintenanceGuard intercepts │
|
||||||
|
│ - Route has @SkipMaintenance decorator │
|
||||||
|
│ - Returns: ALLOW (200 OK) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ ✅ ADMIN PANEL WORKS NORMALLY │
|
||||||
|
│ │
|
||||||
|
│ Admin can: │
|
||||||
|
│ - View dashboard │
|
||||||
|
│ - Manage users │
|
||||||
|
│ - Disable maintenance mode │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🎨 Visual Representation**
|
||||||
|
|
||||||
|
### **App Structure During Maintenance:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ TABUNGIN APP │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ PUBLIC ROUTES (Always Work) │
|
||||||
|
│ ✅ /auth/login │
|
||||||
|
│ ✅ /auth/register │
|
||||||
|
│ ✅ /auth/otp │
|
||||||
|
│ ✅ /health │
|
||||||
|
│ │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ADMIN ROUTES (Work for Admins Only) │
|
||||||
|
│ ✅ /admin/* │
|
||||||
|
│ ✅ /api/admin/* │
|
||||||
|
│ │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ MAIN APP ROUTES (BLOCKED - Shows Maintenance) │
|
||||||
|
│ ❌ / │
|
||||||
|
│ ❌ /wallets │
|
||||||
|
│ ❌ /transactions │
|
||||||
|
│ ❌ /api/users/me │
|
||||||
|
│ ❌ /api/wallets │
|
||||||
|
│ ❌ /api/transactions │
|
||||||
|
│ │
|
||||||
|
│ 👉 All show: MAINTENANCE PAGE │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🔍 Code Flow in App.tsx**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default function App() {
|
||||||
|
const [maintenanceMode, setMaintenanceMode] = useState(false)
|
||||||
|
const [maintenanceMessage, setMaintenanceMessage] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Setup interceptor on app load
|
||||||
|
setupAxiosInterceptors((message) => {
|
||||||
|
setMaintenanceMessage(message)
|
||||||
|
setMaintenanceMode(true) // 👈 This triggers maintenance page
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 👇 When maintenance mode is true, show maintenance page
|
||||||
|
if (maintenanceMode) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<MaintenancePage message={maintenanceMessage} />
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 👇 Otherwise, show normal app
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<ThemeProvider>
|
||||||
|
<LanguageProvider>
|
||||||
|
<AuthProvider>
|
||||||
|
<Routes>
|
||||||
|
{/* All routes here */}
|
||||||
|
</Routes>
|
||||||
|
</AuthProvider>
|
||||||
|
</LanguageProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🎯 Key Points**
|
||||||
|
|
||||||
|
### **1. Maintenance Page is NOT a Route**
|
||||||
|
- It's not `/maintenance` or any URL
|
||||||
|
- It's a **state-based replacement** of the entire app
|
||||||
|
- When `maintenanceMode` state is `true`, it replaces everything
|
||||||
|
|
||||||
|
### **2. Trigger is API Response**
|
||||||
|
- Maintenance page appears when **any API call returns 503**
|
||||||
|
- The axios interceptor catches this and sets state
|
||||||
|
- This can happen on:
|
||||||
|
- Initial page load (GET /api/users/me)
|
||||||
|
- Any user action (creating transaction, loading wallets, etc.)
|
||||||
|
- Navigation between pages
|
||||||
|
|
||||||
|
### **3. Admin Panel is Separate**
|
||||||
|
- Admin routes (`/admin/*`) have `@SkipMaintenance`
|
||||||
|
- API calls to `/api/admin/*` bypass the guard
|
||||||
|
- Admins can still manage the system
|
||||||
|
- But if admin tries to access main app (`/`), they also see maintenance
|
||||||
|
|
||||||
|
### **4. Login Always Works**
|
||||||
|
- `/auth/login` has `@SkipMaintenance`
|
||||||
|
- Users can still login during maintenance
|
||||||
|
- But after login, they immediately see maintenance page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **📝 Summary**
|
||||||
|
|
||||||
|
**Where does maintenance page appear?**
|
||||||
|
- ✅ After any API call that returns 503
|
||||||
|
- ✅ For regular users trying to access protected routes
|
||||||
|
- ✅ Even for admins trying to access main app (not admin panel)
|
||||||
|
- ✅ Replaces the entire app UI (not a specific route)
|
||||||
|
|
||||||
|
**Where does it NOT appear?**
|
||||||
|
- ❌ On login/register pages (before API calls)
|
||||||
|
- ❌ On admin panel routes (for admins)
|
||||||
|
- ❌ On health check endpoints
|
||||||
|
|
||||||
|
**How to see it?**
|
||||||
|
1. Enable maintenance mode as admin
|
||||||
|
2. Login as regular user (or stay logged in)
|
||||||
|
3. Try to access any page or make any action
|
||||||
|
4. Maintenance page will appear immediately
|
||||||
267
docs/guides/testing-guide.md
Normal file
267
docs/guides/testing-guide.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# Maintenance Mode - Testing Guide
|
||||||
|
|
||||||
|
## ✅ **Fixed Implementation**
|
||||||
|
|
||||||
|
### **What Was Wrong:**
|
||||||
|
The original implementation had a critical flaw - it checked `request.user` which doesn't exist until AFTER the JWT guard runs. This meant:
|
||||||
|
- Admins would also be blocked
|
||||||
|
- The guard couldn't distinguish between admin and regular users
|
||||||
|
|
||||||
|
### **What Was Fixed:**
|
||||||
|
- MaintenanceGuard now manually verifies JWT tokens using `JwtService`
|
||||||
|
- Extracts user role from token payload
|
||||||
|
- Allows admins through, blocks everyone else
|
||||||
|
- Works independently of the JWT guard
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🧪 How to Test Maintenance Mode**
|
||||||
|
|
||||||
|
### **Prerequisites:**
|
||||||
|
1. Have at least 2 user accounts:
|
||||||
|
- One admin account (role: 'admin')
|
||||||
|
- One regular user account (role: 'user')
|
||||||
|
2. Both API and Web servers running
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Test Scenario 1: Enable Maintenance Mode**
|
||||||
|
|
||||||
|
#### **Step 1: Login as Admin**
|
||||||
|
```
|
||||||
|
1. Go to http://localhost:5174/auth/login
|
||||||
|
2. Login with admin credentials
|
||||||
|
3. Navigate to http://localhost:5174/admin/settings
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Step 2: Enable Maintenance**
|
||||||
|
```
|
||||||
|
1. Find "Maintenance Mode" section
|
||||||
|
2. Toggle the switch to ON
|
||||||
|
3. Optionally change the message to: "We're upgrading! Back in 10 minutes."
|
||||||
|
4. Click "Save Settings"
|
||||||
|
5. You should see success toast
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Step 3: Verify Admin Still Has Access**
|
||||||
|
```
|
||||||
|
1. Stay logged in as admin
|
||||||
|
2. Navigate to different admin pages:
|
||||||
|
- /admin (Dashboard)
|
||||||
|
- /admin/users
|
||||||
|
- /admin/plans
|
||||||
|
3. All should work normally ✅
|
||||||
|
4. Try accessing main app: http://localhost:5174/
|
||||||
|
5. Admin should see maintenance page ❌ (this is expected - only admin panel works)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Test Scenario 2: Regular User Experience**
|
||||||
|
|
||||||
|
#### **Step 1: Open Incognito/Private Window**
|
||||||
|
```
|
||||||
|
1. Open new incognito window
|
||||||
|
2. Go to http://localhost:5174
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Step 2: Try to Access Without Login**
|
||||||
|
```
|
||||||
|
1. You'll be redirected to login page
|
||||||
|
2. Try to login with regular user credentials
|
||||||
|
3. After successful login, you should see MAINTENANCE PAGE ✅
|
||||||
|
4. The page should display your custom message
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Step 3: Verify User is Blocked**
|
||||||
|
```
|
||||||
|
1. Try to navigate to any route:
|
||||||
|
- /
|
||||||
|
- /wallets
|
||||||
|
- /transactions
|
||||||
|
2. All should show maintenance page ✅
|
||||||
|
3. Click "Refresh Page" button
|
||||||
|
4. Should still show maintenance (because it's still ON)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Test Scenario 3: Disable Maintenance Mode**
|
||||||
|
|
||||||
|
#### **Step 1: As Admin, Disable Maintenance**
|
||||||
|
```
|
||||||
|
1. Go back to admin window (still logged in)
|
||||||
|
2. Navigate to /admin/settings
|
||||||
|
3. Toggle "Maintenance Mode" to OFF
|
||||||
|
4. Click "Save Settings"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Step 2: Verify User Can Access Again**
|
||||||
|
```
|
||||||
|
1. Go to incognito window (with regular user)
|
||||||
|
2. Click "Refresh Page" button
|
||||||
|
3. User should now see the normal app ✅
|
||||||
|
4. Try navigating to different pages - all should work
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **📊 Expected Behavior Matrix**
|
||||||
|
|
||||||
|
| User Type | Maintenance OFF | Maintenance ON |
|
||||||
|
|-----------|----------------|----------------|
|
||||||
|
| **Not Logged In** | Can login | Can login, then see maintenance page |
|
||||||
|
| **Regular User** | Full access | Maintenance page on all routes |
|
||||||
|
| **Admin User** | Full access | Admin panel works, main app shows maintenance |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🔍 How to Verify It's Working**
|
||||||
|
|
||||||
|
### **Check 1: API Returns 503**
|
||||||
|
Open browser DevTools (F12) → Network tab:
|
||||||
|
```
|
||||||
|
1. With maintenance ON, as regular user
|
||||||
|
2. Try to access any protected route
|
||||||
|
3. Look for API calls
|
||||||
|
4. Should see 503 status code
|
||||||
|
5. Response body should have:
|
||||||
|
{
|
||||||
|
"statusCode": 503,
|
||||||
|
"message": "Your custom message",
|
||||||
|
"maintenanceMode": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Check 2: Admin Bypasses Maintenance**
|
||||||
|
```
|
||||||
|
1. With maintenance ON, as admin
|
||||||
|
2. Open DevTools → Network tab
|
||||||
|
3. Navigate to /admin/users
|
||||||
|
4. API calls should return 200 OK (not 503)
|
||||||
|
5. Admin panel should work normally
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Check 3: Database Config**
|
||||||
|
Check the database directly:
|
||||||
|
```sql
|
||||||
|
SELECT * FROM "Config" WHERE key = 'maintenance_mode';
|
||||||
|
-- Should show: value = 'true' when ON, 'false' when OFF
|
||||||
|
|
||||||
|
SELECT * FROM "Config" WHERE key = 'maintenance_message';
|
||||||
|
-- Should show your custom message
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🐛 Troubleshooting**
|
||||||
|
|
||||||
|
### **Problem: Admin Also Sees Maintenance Page**
|
||||||
|
**Cause:** JWT token might be invalid or expired
|
||||||
|
**Solution:**
|
||||||
|
1. Logout and login again as admin
|
||||||
|
2. Check browser console for errors
|
||||||
|
3. Verify JWT_SECRET in .env matches between sessions
|
||||||
|
|
||||||
|
### **Problem: Regular User Can Still Access**
|
||||||
|
**Cause:** Maintenance mode not actually enabled in DB
|
||||||
|
**Solution:**
|
||||||
|
1. Check database: `SELECT * FROM "Config" WHERE key = 'maintenance_mode'`
|
||||||
|
2. If value is not 'true', manually update:
|
||||||
|
```sql
|
||||||
|
UPDATE "Config" SET value = 'true' WHERE key = 'maintenance_mode';
|
||||||
|
```
|
||||||
|
3. Or toggle it again in Admin Settings
|
||||||
|
|
||||||
|
### **Problem: Maintenance Page Never Shows**
|
||||||
|
**Cause:** Axios interceptor not set up properly
|
||||||
|
**Solution:**
|
||||||
|
1. Check browser console for errors
|
||||||
|
2. Verify App.tsx has `setupAxiosInterceptors` call
|
||||||
|
3. Hard refresh (Ctrl+Shift+R or Cmd+Shift+R)
|
||||||
|
|
||||||
|
### **Problem: Auth Routes Also Blocked**
|
||||||
|
**Cause:** @SkipMaintenance decorator missing
|
||||||
|
**Solution:**
|
||||||
|
1. Verify auth.controller.ts has @SkipMaintenance on login/register
|
||||||
|
2. Restart API server
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🎯 Quick Test Checklist**
|
||||||
|
|
||||||
|
- [ ] Admin can enable maintenance mode
|
||||||
|
- [ ] Admin can customize maintenance message
|
||||||
|
- [ ] Admin can still access admin panel when maintenance is ON
|
||||||
|
- [ ] Regular user sees maintenance page when maintenance is ON
|
||||||
|
- [ ] Maintenance page displays custom message
|
||||||
|
- [ ] Refresh button works on maintenance page
|
||||||
|
- [ ] Admin can disable maintenance mode
|
||||||
|
- [ ] Regular user can access app after maintenance is OFF
|
||||||
|
- [ ] Login/register still work during maintenance
|
||||||
|
- [ ] Health check endpoints still work during maintenance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **📝 Notes**
|
||||||
|
|
||||||
|
### **Routes That Always Work (Even During Maintenance):**
|
||||||
|
- `/health` - Health check
|
||||||
|
- `/health/db` - Database check
|
||||||
|
- `/auth/login` - Login
|
||||||
|
- `/auth/register` - Register
|
||||||
|
- `/auth/verify-otp` - OTP verification
|
||||||
|
- `/auth/google` - Google OAuth
|
||||||
|
- `/admin/*` - All admin routes (for admins only)
|
||||||
|
|
||||||
|
### **Routes That Get Blocked:**
|
||||||
|
- `/` - Main dashboard
|
||||||
|
- `/wallets` - Wallets page
|
||||||
|
- `/transactions` - Transactions page
|
||||||
|
- `/api/users/me` - User profile
|
||||||
|
- `/api/wallets` - Wallet API
|
||||||
|
- `/api/transactions` - Transaction API
|
||||||
|
- Any other non-admin, non-auth route
|
||||||
|
|
||||||
|
### **How Admins Are Identified:**
|
||||||
|
1. MaintenanceGuard extracts JWT token from `Authorization` header
|
||||||
|
2. Verifies token using JwtService
|
||||||
|
3. Checks if `payload.role === 'admin'`
|
||||||
|
4. If true, allows access
|
||||||
|
5. If false or no token, blocks with 503
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **🚀 Production Deployment Notes**
|
||||||
|
|
||||||
|
### **Before Enabling Maintenance:**
|
||||||
|
1. Notify users via email/notification
|
||||||
|
2. Set a clear maintenance message with expected duration
|
||||||
|
3. Schedule during low-traffic hours
|
||||||
|
4. Have rollback plan ready
|
||||||
|
|
||||||
|
### **During Maintenance:**
|
||||||
|
1. Monitor admin panel access
|
||||||
|
2. Check logs for any issues
|
||||||
|
3. Test critical features before re-enabling
|
||||||
|
|
||||||
|
### **After Maintenance:**
|
||||||
|
1. Disable maintenance mode
|
||||||
|
2. Monitor for any issues
|
||||||
|
3. Verify all features work
|
||||||
|
4. Notify users that system is back online
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **✅ Summary**
|
||||||
|
|
||||||
|
Maintenance mode is now **fully functional** with proper admin bypass. The system:
|
||||||
|
- ✅ Blocks regular users when enabled
|
||||||
|
- ✅ Allows admins to continue working
|
||||||
|
- ✅ Shows professional maintenance page
|
||||||
|
- ✅ Displays custom admin messages
|
||||||
|
- ✅ Can be toggled on/off from Admin Settings
|
||||||
|
- ✅ Preserves auth functionality
|
||||||
|
- ✅ Keeps health checks working
|
||||||
|
|
||||||
|
**Ready for production use!**
|
||||||
394
docs/planning/tech-qa.md
Normal file
394
docs/planning/tech-qa.md
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
# Technical Questions & Answers
|
||||||
|
|
||||||
|
## 📱 **Q1: Mobile App - Native vs PWA?**
|
||||||
|
|
||||||
|
### **Recommendation: PWA (Progressive Web App)**
|
||||||
|
|
||||||
|
#### **Why PWA is Better for Tabungin:**
|
||||||
|
|
||||||
|
**✅ Advantages:**
|
||||||
|
1. **Single Codebase**
|
||||||
|
- Same React/TypeScript code for web, mobile, and desktop
|
||||||
|
- No need to learn Swift (iOS) or Kotlin (Android)
|
||||||
|
- Faster development and easier maintenance
|
||||||
|
|
||||||
|
2. **Instant Updates**
|
||||||
|
- No app store approval delays
|
||||||
|
- Users always get the latest version
|
||||||
|
- Fix bugs immediately
|
||||||
|
|
||||||
|
3. **Lower Cost**
|
||||||
|
- No Apple Developer ($99/year) or Google Play ($25 one-time) fees
|
||||||
|
- No separate mobile development team needed
|
||||||
|
- Shared backend API
|
||||||
|
|
||||||
|
4. **Easy Distribution**
|
||||||
|
- Users just visit the URL
|
||||||
|
- "Add to Home Screen" prompt
|
||||||
|
- No app store submission process
|
||||||
|
|
||||||
|
5. **Cross-Platform**
|
||||||
|
- Works on iOS, Android, Windows, Mac, Linux
|
||||||
|
- Same experience everywhere
|
||||||
|
- Responsive design handles all screen sizes
|
||||||
|
|
||||||
|
**❌ Native App Disadvantages:**
|
||||||
|
- Need to build 2 separate apps (iOS + Android)
|
||||||
|
- Different programming languages (Swift + Kotlin)
|
||||||
|
- App store approval process (can take days/weeks)
|
||||||
|
- Higher development cost (2-3x more expensive)
|
||||||
|
- Slower iteration cycle
|
||||||
|
|
||||||
|
#### **When to Consider Native:**
|
||||||
|
- Need advanced device features (Bluetooth, NFC, advanced camera)
|
||||||
|
- Performance-critical 3D graphics or games
|
||||||
|
- Offline-first with complex local database
|
||||||
|
- Very large user base willing to download apps
|
||||||
|
|
||||||
|
#### **For Tabungin Specifically:**
|
||||||
|
PWA is perfect because:
|
||||||
|
- ✅ Financial tracking doesn't need native features
|
||||||
|
- ✅ Web APIs cover all needs (storage, notifications, camera)
|
||||||
|
- ✅ Users prefer quick access without app download
|
||||||
|
- ✅ Easier to maintain with small team
|
||||||
|
- ✅ Can always add native wrapper later (Capacitor/Cordova)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔔 **Q2: Push Notifications in PWA?**
|
||||||
|
|
||||||
|
### **Yes! PWA supports push notifications via Web Push API**
|
||||||
|
|
||||||
|
#### **How It Works:**
|
||||||
|
|
||||||
|
**1. Browser Support:**
|
||||||
|
- ✅ Chrome (Android & Desktop)
|
||||||
|
- ✅ Firefox (Android & Desktop)
|
||||||
|
- ✅ Edge (Android & Desktop)
|
||||||
|
- ✅ Safari (iOS 16.4+, macOS)
|
||||||
|
- ✅ Opera (Android & Desktop)
|
||||||
|
|
||||||
|
**2. Implementation:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Frontend: Request permission
|
||||||
|
async function requestNotificationPermission() {
|
||||||
|
const permission = await Notification.requestPermission()
|
||||||
|
|
||||||
|
if (permission === 'granted') {
|
||||||
|
// Subscribe to push notifications
|
||||||
|
const registration = await navigator.serviceWorker.ready
|
||||||
|
const subscription = await registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: 'YOUR_VAPID_PUBLIC_KEY'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Send subscription to backend
|
||||||
|
await fetch('/api/notifications/subscribe', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(subscription),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service Worker: Receive push
|
||||||
|
self.addEventListener('push', (event) => {
|
||||||
|
const data = event.data.json()
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification(data.title, {
|
||||||
|
body: data.body,
|
||||||
|
icon: '/icon-192.png',
|
||||||
|
badge: '/badge-72.png',
|
||||||
|
data: data.url
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle notification click
|
||||||
|
self.addEventListener('notificationclick', (event) => {
|
||||||
|
event.notification.close()
|
||||||
|
event.waitUntil(
|
||||||
|
clients.openWindow(event.notification.data)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Backend (NestJS):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Install: npm install web-push
|
||||||
|
import * as webpush from 'web-push'
|
||||||
|
|
||||||
|
// Setup VAPID keys (one-time)
|
||||||
|
const vapidKeys = webpush.generateVAPIDKeys()
|
||||||
|
// Store these in .env:
|
||||||
|
// VAPID_PUBLIC_KEY=...
|
||||||
|
// VAPID_PRIVATE_KEY=...
|
||||||
|
|
||||||
|
webpush.setVapidDetails(
|
||||||
|
'mailto:support@tabungin.app',
|
||||||
|
process.env.VAPID_PUBLIC_KEY,
|
||||||
|
process.env.VAPID_PRIVATE_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send notification
|
||||||
|
async sendPushNotification(subscription: any, payload: any) {
|
||||||
|
try {
|
||||||
|
await webpush.sendNotification(subscription, JSON.stringify(payload))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Push notification failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Database Schema (Prisma):**
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model PushSubscription {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
endpoint String @unique
|
||||||
|
keys Json // { p256dh, auth }
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Use Cases for Tabungin:**
|
||||||
|
|
||||||
|
1. **Transaction Reminders**
|
||||||
|
- "Don't forget to log today's expenses!"
|
||||||
|
- Scheduled daily/weekly
|
||||||
|
|
||||||
|
2. **Budget Alerts**
|
||||||
|
- "You've spent 80% of your monthly budget"
|
||||||
|
- Real-time when threshold reached
|
||||||
|
|
||||||
|
3. **Payment Reminders**
|
||||||
|
- "Subscription payment due in 3 days"
|
||||||
|
- Scheduled based on payment dates
|
||||||
|
|
||||||
|
4. **Goal Progress**
|
||||||
|
- "You're 50% towards your savings goal! 🎉"
|
||||||
|
- Milestone notifications
|
||||||
|
|
||||||
|
5. **Admin Notifications**
|
||||||
|
- "New payment verification request"
|
||||||
|
- "New user registered"
|
||||||
|
|
||||||
|
#### **Notification Channels Summary:**
|
||||||
|
|
||||||
|
| Channel | Use Case | Delivery Speed | Cost |
|
||||||
|
|---------|----------|----------------|------|
|
||||||
|
| **Web Push** | App updates, reminders | Instant | Free |
|
||||||
|
| **Email** | Receipts, reports, OTP | 1-5 seconds | Free (SendGrid free tier) |
|
||||||
|
| **WhatsApp** | OTP, urgent alerts | 1-3 seconds | ~$0.005/message |
|
||||||
|
| **SMS** | OTP (fallback) | 1-5 seconds | ~$0.01/message |
|
||||||
|
|
||||||
|
**Recommendation:**
|
||||||
|
- ✅ Use Web Push for in-app notifications (free, instant)
|
||||||
|
- ✅ Use Email for important communications (free, reliable)
|
||||||
|
- ✅ Use WhatsApp for OTP (cheap, high delivery rate)
|
||||||
|
- ❌ Avoid SMS unless required (expensive)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 **Q3: Why So Many .md Documentation Files?**
|
||||||
|
|
||||||
|
### **Great Question! Let me explain the documentation strategy:**
|
||||||
|
|
||||||
|
#### **Current Documentation Files:**
|
||||||
|
|
||||||
|
1. `MAINTENANCE_MODE_FLOW.md` - Visual flow diagrams
|
||||||
|
2. `MAINTENANCE_MODE_TESTING_GUIDE.md` - Testing instructions
|
||||||
|
3. `ADMIN_AUTO_REDIRECT.md` - Login redirect implementation
|
||||||
|
4. `ADMIN_PROFILE_REUSE.md` - Profile page reuse
|
||||||
|
5. `TECH_QUESTIONS_ANSWERED.md` - This file
|
||||||
|
|
||||||
|
#### **Why Separate Files?**
|
||||||
|
|
||||||
|
**❌ Problem with Single Large Document:**
|
||||||
|
```
|
||||||
|
MASTER_DOCUMENTATION.md (5000+ lines)
|
||||||
|
├── Done Tasks (500 lines)
|
||||||
|
├── To-Do List (200 lines)
|
||||||
|
├── Requirements (800 lines)
|
||||||
|
├── UI/UX Guidelines (600 lines)
|
||||||
|
├── Feature 1 (300 lines)
|
||||||
|
├── Feature 2 (400 lines)
|
||||||
|
├── Feature 3 (350 lines)
|
||||||
|
└── ... (becomes unmanageable)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issues:**
|
||||||
|
- Hard to find specific information
|
||||||
|
- Merge conflicts when updating
|
||||||
|
- Overwhelming to read
|
||||||
|
- Difficult to share specific sections
|
||||||
|
- No clear separation of concerns
|
||||||
|
|
||||||
|
**✅ Benefits of Separate Files:**
|
||||||
|
|
||||||
|
1. **Focused Context**
|
||||||
|
- Each file covers ONE topic
|
||||||
|
- Easy to find what you need
|
||||||
|
- Clear purpose and scope
|
||||||
|
|
||||||
|
2. **Better Organization**
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── features/
|
||||||
|
│ ├── maintenance-mode.md
|
||||||
|
│ ├── admin-redirect.md
|
||||||
|
│ └── profile-reuse.md
|
||||||
|
├── guides/
|
||||||
|
│ ├── testing-guide.md
|
||||||
|
│ └── deployment-guide.md
|
||||||
|
└── architecture/
|
||||||
|
├── tech-stack.md
|
||||||
|
└── api-design.md
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Easier Collaboration**
|
||||||
|
- Multiple people can edit different files
|
||||||
|
- No merge conflicts
|
||||||
|
- Clear ownership
|
||||||
|
|
||||||
|
4. **Version Control**
|
||||||
|
- See what changed per feature
|
||||||
|
- Easier to review changes
|
||||||
|
- Better git history
|
||||||
|
|
||||||
|
#### **Recommended Structure:**
|
||||||
|
|
||||||
|
```
|
||||||
|
/docs
|
||||||
|
├── README.md # Main index
|
||||||
|
├── /features # Feature documentation
|
||||||
|
│ ├── maintenance-mode.md
|
||||||
|
│ ├── admin-redirect.md
|
||||||
|
│ └── profile-reuse.md
|
||||||
|
├── /guides # How-to guides
|
||||||
|
│ ├── testing.md
|
||||||
|
│ ├── deployment.md
|
||||||
|
│ └── development.md
|
||||||
|
├── /architecture # Technical decisions
|
||||||
|
│ ├── tech-stack.md
|
||||||
|
│ ├── database-schema.md
|
||||||
|
│ └── api-design.md
|
||||||
|
├── /planning # Project management
|
||||||
|
│ ├── roadmap.md
|
||||||
|
│ ├── requirements.md
|
||||||
|
│ └── changelog.md
|
||||||
|
└── /ui-ux # Design system
|
||||||
|
├── design-principles.md
|
||||||
|
├── components.md
|
||||||
|
└── style-guide.md
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **What I Can Do:**
|
||||||
|
|
||||||
|
**Option 1: Keep Current Structure** (Recommended)
|
||||||
|
- Separate files for each feature/topic
|
||||||
|
- Easy to navigate and maintain
|
||||||
|
- Better for version control
|
||||||
|
|
||||||
|
**Option 2: Create Index File**
|
||||||
|
```markdown
|
||||||
|
# Tabungin Documentation Index
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- [Maintenance Mode](./features/maintenance-mode.md)
|
||||||
|
- [Admin Auto-Redirect](./features/admin-redirect.md)
|
||||||
|
- [Profile Page Reuse](./features/profile-reuse.md)
|
||||||
|
|
||||||
|
## Guides
|
||||||
|
- [Testing Guide](./guides/testing.md)
|
||||||
|
- [Deployment Guide](./guides/deployment.md)
|
||||||
|
|
||||||
|
## Planning
|
||||||
|
- [Roadmap](./planning/roadmap.md)
|
||||||
|
- [Requirements](./planning/requirements.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 3: Consolidate by Category**
|
||||||
|
- `FEATURES.md` - All feature documentation
|
||||||
|
- `GUIDES.md` - All how-to guides
|
||||||
|
- `PLANNING.md` - Roadmap, requirements, to-dos
|
||||||
|
- `ARCHITECTURE.md` - Technical decisions
|
||||||
|
|
||||||
|
#### **My Recommendation:**
|
||||||
|
|
||||||
|
**Keep separate files BUT organize them better:**
|
||||||
|
|
||||||
|
1. **Move to `/docs` folder:**
|
||||||
|
```bash
|
||||||
|
mkdir -p docs/{features,guides,planning,architecture}
|
||||||
|
mv MAINTENANCE_MODE_*.md docs/features/
|
||||||
|
mv ADMIN_*.md docs/features/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create `docs/README.md` as index:**
|
||||||
|
- Links to all documentation
|
||||||
|
- Quick navigation
|
||||||
|
- Overview of project
|
||||||
|
|
||||||
|
3. **Add to root `README.md`:**
|
||||||
|
- Link to documentation folder
|
||||||
|
- Quick start guide
|
||||||
|
- Project overview
|
||||||
|
|
||||||
|
#### **Why I Create These Docs:**
|
||||||
|
|
||||||
|
1. **Knowledge Transfer**
|
||||||
|
- You can understand what was done
|
||||||
|
- Future developers can onboard faster
|
||||||
|
- Clear implementation details
|
||||||
|
|
||||||
|
2. **Testing Reference**
|
||||||
|
- Step-by-step testing instructions
|
||||||
|
- Expected behavior documented
|
||||||
|
- Edge cases covered
|
||||||
|
|
||||||
|
3. **Decision Record**
|
||||||
|
- Why certain approaches were chosen
|
||||||
|
- Trade-offs considered
|
||||||
|
- Future reference
|
||||||
|
|
||||||
|
4. **Maintenance**
|
||||||
|
- Easy to modify features later
|
||||||
|
- Understand dependencies
|
||||||
|
- Avoid breaking changes
|
||||||
|
|
||||||
|
**Note:** I don't actually "read" these files later in the traditional sense. Each conversation is independent. However, these docs are valuable for:
|
||||||
|
- **You** (the developer) - Reference and knowledge base
|
||||||
|
- **Team members** - Onboarding and collaboration
|
||||||
|
- **Future maintenance** - Understanding the system
|
||||||
|
- **AI assistants** - Can be provided as context in future sessions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **Summary & Recommendations**
|
||||||
|
|
||||||
|
### **Mobile App:**
|
||||||
|
✅ **Use PWA** - Perfect for Tabungin's needs, lower cost, faster development
|
||||||
|
|
||||||
|
### **Push Notifications:**
|
||||||
|
✅ **Use Web Push API** - Free, instant, works on all platforms
|
||||||
|
- Combine with Email for important messages
|
||||||
|
- WhatsApp for OTP/urgent alerts
|
||||||
|
|
||||||
|
### **Documentation:**
|
||||||
|
✅ **Keep separate files** but organize better
|
||||||
|
- Create `/docs` folder structure
|
||||||
|
- Add index/README for navigation
|
||||||
|
- Group by category (features, guides, planning)
|
||||||
|
|
||||||
|
**Would you like me to:**
|
||||||
|
1. Reorganize the documentation into proper folder structure?
|
||||||
|
2. Implement Web Push notifications?
|
||||||
|
3. Add PWA manifest and service worker for mobile installation?
|
||||||
221
docs/planning/todo.md
Normal file
221
docs/planning/todo.md
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
# To-Do List
|
||||||
|
|
||||||
|
## 🚀 High Priority
|
||||||
|
|
||||||
|
### 3. PWA Implementation
|
||||||
|
**Status:** Pending
|
||||||
|
**Priority:** High
|
||||||
|
**Estimated Time:** 4-6 hours
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Create `manifest.json` for PWA
|
||||||
|
- App name, icons, theme colors
|
||||||
|
- Display mode (standalone)
|
||||||
|
- Start URL and scope
|
||||||
|
|
||||||
|
- [ ] Add app icons
|
||||||
|
- 192x192 icon
|
||||||
|
- 512x512 icon
|
||||||
|
- Maskable icons for Android
|
||||||
|
- Apple touch icons for iOS
|
||||||
|
|
||||||
|
- [ ] Create Service Worker
|
||||||
|
- Cache strategy (network-first for API, cache-first for assets)
|
||||||
|
- Offline fallback page
|
||||||
|
- Background sync for transactions
|
||||||
|
|
||||||
|
- [ ] Add "Add to Home Screen" prompt
|
||||||
|
- Detect installation capability
|
||||||
|
- Show custom install prompt
|
||||||
|
- Handle installation events
|
||||||
|
|
||||||
|
- [ ] Test PWA features
|
||||||
|
- Install on Android
|
||||||
|
- Install on iOS (Safari)
|
||||||
|
- Test offline functionality
|
||||||
|
- Verify caching works
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Users can install app on home screen
|
||||||
|
- Works offline
|
||||||
|
- Faster load times
|
||||||
|
- Native app-like experience
|
||||||
|
|
||||||
|
**Resources:**
|
||||||
|
- [PWA Documentation](https://web.dev/progressive-web-apps/)
|
||||||
|
- [Workbox (Service Worker library)](https://developers.google.com/web/tools/workbox)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Web Push Notifications
|
||||||
|
**Status:** Pending
|
||||||
|
**Priority:** High
|
||||||
|
**Estimated Time:** 6-8 hours
|
||||||
|
|
||||||
|
**Backend Tasks:**
|
||||||
|
- [ ] Install `web-push` library
|
||||||
|
```bash
|
||||||
|
cd apps/api
|
||||||
|
npm install web-push
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Generate VAPID keys
|
||||||
|
```typescript
|
||||||
|
const vapidKeys = webpush.generateVAPIDKeys()
|
||||||
|
// Add to .env:
|
||||||
|
// VAPID_PUBLIC_KEY=...
|
||||||
|
// VAPID_PRIVATE_KEY=...
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Create PushSubscription model (Prisma)
|
||||||
|
```prisma
|
||||||
|
model PushSubscription {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
endpoint String @unique
|
||||||
|
keys Json
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Create NotificationService
|
||||||
|
- Subscribe endpoint
|
||||||
|
- Unsubscribe endpoint
|
||||||
|
- Send notification method
|
||||||
|
- Batch send for multiple users
|
||||||
|
|
||||||
|
- [ ] Add notification triggers
|
||||||
|
- Transaction reminders (scheduled)
|
||||||
|
- Budget alerts (real-time)
|
||||||
|
- Payment reminders (scheduled)
|
||||||
|
- Admin alerts (real-time)
|
||||||
|
|
||||||
|
**Frontend Tasks:**
|
||||||
|
- [ ] Request notification permission
|
||||||
|
- Show permission prompt
|
||||||
|
- Handle user response
|
||||||
|
- Store permission status
|
||||||
|
|
||||||
|
- [ ] Subscribe to push notifications
|
||||||
|
- Get service worker registration
|
||||||
|
- Subscribe with VAPID public key
|
||||||
|
- Send subscription to backend
|
||||||
|
|
||||||
|
- [ ] Handle push events in Service Worker
|
||||||
|
- Show notification
|
||||||
|
- Handle notification click
|
||||||
|
- Open relevant page
|
||||||
|
|
||||||
|
- [ ] Add notification preferences UI
|
||||||
|
- Enable/disable notifications
|
||||||
|
- Choose notification types
|
||||||
|
- Set quiet hours
|
||||||
|
|
||||||
|
- [ ] Test notifications
|
||||||
|
- Test on Chrome (Android & Desktop)
|
||||||
|
- Test on Firefox
|
||||||
|
- Test on Safari (iOS 16.4+)
|
||||||
|
- Test notification click actions
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
1. **Transaction Reminders**
|
||||||
|
- "Don't forget to log today's expenses!"
|
||||||
|
- Daily/weekly schedule
|
||||||
|
|
||||||
|
2. **Budget Alerts**
|
||||||
|
- "You've spent 80% of your monthly budget"
|
||||||
|
- Real-time threshold alerts
|
||||||
|
|
||||||
|
3. **Payment Reminders**
|
||||||
|
- "Subscription payment due in 3 days"
|
||||||
|
- Scheduled based on payment dates
|
||||||
|
|
||||||
|
4. **Goal Progress**
|
||||||
|
- "You're 50% towards your savings goal! 🎉"
|
||||||
|
- Milestone notifications
|
||||||
|
|
||||||
|
5. **Admin Notifications**
|
||||||
|
- "New payment verification request"
|
||||||
|
- "New user registered"
|
||||||
|
|
||||||
|
**Resources:**
|
||||||
|
- [Web Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API)
|
||||||
|
- [web-push library](https://github.com/web-push-libs/web-push)
|
||||||
|
- [Notification API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Medium Priority
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- [ ] Fix ESLint warnings (87 issues)
|
||||||
|
- Review and fix auth-related warnings
|
||||||
|
- Fix OTP component warnings
|
||||||
|
- Fix transaction/wallet warnings
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Add unit tests for critical functions
|
||||||
|
- [ ] Add integration tests for API endpoints
|
||||||
|
- [ ] Add E2E tests for main user flows
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- [ ] API endpoint documentation (Swagger/OpenAPI)
|
||||||
|
- [ ] Component documentation (Storybook)
|
||||||
|
- [ ] Database schema documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔮 Future Enhancements
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- [ ] Export transactions (CSV, PDF)
|
||||||
|
- [ ] Recurring transactions
|
||||||
|
- [ ] Budget categories
|
||||||
|
- [ ] Multi-currency support
|
||||||
|
- [ ] Team/family sharing
|
||||||
|
- [ ] Financial reports and charts
|
||||||
|
|
||||||
|
### Admin Features
|
||||||
|
- [ ] Analytics dashboard
|
||||||
|
- [ ] User activity logs
|
||||||
|
- [ ] System health monitoring
|
||||||
|
- [ ] Backup and restore
|
||||||
|
|
||||||
|
### Integrations
|
||||||
|
- [ ] Bank account sync
|
||||||
|
- [ ] Receipt scanning (OCR)
|
||||||
|
- [ ] Cryptocurrency tracking
|
||||||
|
- [ ] Investment portfolio
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Completed
|
||||||
|
|
||||||
|
- [x] Maintenance mode implementation
|
||||||
|
- [x] Admin auto-redirect after login
|
||||||
|
- [x] Profile page reuse for admin
|
||||||
|
- [x] Admin dashboard blocking for regular users
|
||||||
|
- [x] Admin settings tabbed interface
|
||||||
|
- [x] Documentation reorganization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
**Priority Levels:**
|
||||||
|
- **High:** Should be done soon (within 1-2 weeks)
|
||||||
|
- **Medium:** Important but not urgent (within 1 month)
|
||||||
|
- **Low:** Nice to have (when time permits)
|
||||||
|
|
||||||
|
**Time Estimates:**
|
||||||
|
- Based on single developer working full-time
|
||||||
|
- May vary based on experience and complexity
|
||||||
|
- Include testing and documentation time
|
||||||
|
|
||||||
|
**Update Frequency:**
|
||||||
|
- Review weekly
|
||||||
|
- Mark completed items
|
||||||
|
- Add new items as needed
|
||||||
|
- Reprioritize based on user feedback
|
||||||
Reference in New Issue
Block a user