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:
dwindown
2025-10-13 09:28:12 +07:00
parent 49d60676d0
commit 89f881e7cf
99 changed files with 4884 additions and 392 deletions

169
ADMIN_TRANSLATION_STATUS.md Normal file
View 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

View 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
View 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
View 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**

View File

@@ -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

View File

@@ -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"}

View File

@@ -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"}

View File

@@ -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

View File

@@ -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"}

View File

@@ -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 {};

View File

@@ -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

View File

@@ -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"}

View File

@@ -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<{

View File

@@ -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);

View File

@@ -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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"}

View File

@@ -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;

View File

@@ -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

View File

@@ -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"}

View File

@@ -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;

View File

@@ -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;
}>;
} }

View File

@@ -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

View File

@@ -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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"}

View File

@@ -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;
}>;
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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"}

View File

@@ -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)()),

View File

@@ -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":"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"}

View File

@@ -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

View File

@@ -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"}

View File

@@ -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',
}; };
} }
}; };

View File

@@ -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"}

View File

@@ -0,0 +1 @@
export declare const SkipMaintenance: () => import("@nestjs/common").CustomDecorator<string>;

View 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

View 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"}

View 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>;
}

View 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

View 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"}

View File

@@ -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

View File

@@ -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"}

File diff suppressed because one or more lines are too long

54
apps/api/dist/seed.js vendored
View File

@@ -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');
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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"}

View File

@@ -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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"}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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) {}

View File

@@ -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;
} }

View File

@@ -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) {}

View File

@@ -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 };
} }

View File

@@ -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) {}

View File

@@ -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;
} }

View File

@@ -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) {}

View File

@@ -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 };
} }

View File

@@ -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);
}
} }

View File

@@ -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' };
}
} }

View File

@@ -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 {}

View File

@@ -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

View File

@@ -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 {}

View File

@@ -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,

View File

@@ -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',
}; };
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -0,0 +1,3 @@
import { SetMetadata } from '@nestjs/common';
export const SkipMaintenance = () => SetMetadata('skipMaintenance', true);

View 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,
});
}
}

View File

@@ -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) {}

View File

@@ -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,

View File

@@ -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');
} }

View File

@@ -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';

View File

@@ -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';

View File

@@ -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>

View File

@@ -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)

View File

@@ -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>

View File

@@ -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',

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>
</> </>
)} )}

View File

@@ -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>

View File

@@ -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>

View 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>
)
}

View File

@@ -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>
) )
} }

View File

@@ -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>
)
}

View File

@@ -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 />
}

View File

@@ -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>
)
}

View File

@@ -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>
) )

View 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>
)
}

View File

@@ -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.')

View File

@@ -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>

View 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
View 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/`

View 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.

View 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!** 🎊

View 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)

View 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

View 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
View 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
View 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