refactor(services): Reorganize API layer and consolidate documentation
This commit is contained in:
parent
6021d83385
commit
a1c9b689d5
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
Admin dashboard for Yaltopia Ticket management system built with React, TypeScript, and Vite.
|
Admin dashboard for Yaltopia Ticket management system built with React, TypeScript, and Vite.
|
||||||
|
|
||||||
|
> 📚 **For detailed documentation, see [dev-docs/](./dev-docs/README.md)**
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- User Management
|
- User Management
|
||||||
|
|
|
||||||
983
dev-docs/API_GUIDE.md
Normal file
983
dev-docs/API_GUIDE.md
Normal file
|
|
@ -0,0 +1,983 @@
|
||||||
|
# API & Service Layer Guide
|
||||||
|
|
||||||
|
Complete guide for making API calls in the Yaltopia Ticket Admin application.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Architecture Overview](#architecture-overview)
|
||||||
|
- [Available Services](#available-services)
|
||||||
|
- [Basic Usage](#basic-usage)
|
||||||
|
- [Common Patterns](#common-patterns)
|
||||||
|
- [Service Methods Reference](#service-methods-reference)
|
||||||
|
- [Error Handling](#error-handling)
|
||||||
|
- [Best Practices](#best-practices)
|
||||||
|
- [Examples](#examples)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### The Service Layer Pattern
|
||||||
|
|
||||||
|
All API calls flow through a centralized service layer:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Component │ "I need user data"
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Service Layer │ userService.getUsers()
|
||||||
|
│ (Business │ • Type-safe methods
|
||||||
|
│ Logic) │ • Error handling
|
||||||
|
└────────┬────────┘ • Data transformation
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ API Client │ axios.get('/admin/users')
|
||||||
|
│ (HTTP Layer) │ • Auth token injection
|
||||||
|
└────────┬────────┘ • Token refresh
|
||||||
|
│ • Request/response interceptors
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Backend API │ Returns JSON data
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why Service Layer?
|
||||||
|
|
||||||
|
**Before (Bad):**
|
||||||
|
```typescript
|
||||||
|
// Direct API calls - scattered everywhere
|
||||||
|
const response = await axios.get('/api/users')
|
||||||
|
const users = response.data
|
||||||
|
|
||||||
|
// Different patterns in different files
|
||||||
|
fetch('/api/users').then(r => r.json())
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Good):**
|
||||||
|
```typescript
|
||||||
|
// Centralized, typed, consistent
|
||||||
|
import { userService } from '@/services'
|
||||||
|
const users = await userService.getUsers()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ Single source of truth
|
||||||
|
- ✅ Type safety (TypeScript)
|
||||||
|
- ✅ Automatic authentication
|
||||||
|
- ✅ Consistent error handling
|
||||||
|
- ✅ Easy to test
|
||||||
|
- ✅ Easy to maintain
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Available Services
|
||||||
|
|
||||||
|
### Import All Services
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
authService, // Authentication & authorization
|
||||||
|
userService, // User management
|
||||||
|
analyticsService, // Dashboard analytics & metrics
|
||||||
|
securityService, // Security monitoring & logs
|
||||||
|
systemService, // System health & maintenance
|
||||||
|
announcementService,// Announcements management
|
||||||
|
auditService, // Audit logs & history
|
||||||
|
settingsService // System settings
|
||||||
|
} from '@/services'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Locations
|
||||||
|
|
||||||
|
```
|
||||||
|
src/services/
|
||||||
|
├── index.ts # Central export (import from here)
|
||||||
|
├── api/
|
||||||
|
│ └── client.ts # Shared axios instance
|
||||||
|
├── auth.service.ts # authService
|
||||||
|
├── user.service.ts # userService
|
||||||
|
├── analytics.service.ts # analyticsService
|
||||||
|
├── security.service.ts # securityService
|
||||||
|
├── system.service.ts # systemService
|
||||||
|
├── announcement.service.ts # announcementService
|
||||||
|
├── audit.service.ts # auditService
|
||||||
|
└── settings.service.ts # settingsService
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### 1. Simple API Call
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { userService } from '@/services'
|
||||||
|
|
||||||
|
// Async/await
|
||||||
|
async function loadUsers() {
|
||||||
|
const users = await userService.getUsers()
|
||||||
|
console.log(users) // Typed response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Promise
|
||||||
|
userService.getUsers()
|
||||||
|
.then(users => console.log(users))
|
||||||
|
.catch(error => console.error(error))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. With React Query (Recommended)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { userService } from '@/services'
|
||||||
|
|
||||||
|
function UsersPage() {
|
||||||
|
const { data, isLoading, error } = useQuery({
|
||||||
|
queryKey: ['users'],
|
||||||
|
queryFn: () => userService.getUsers()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>
|
||||||
|
if (error) return <div>Error: {error.message}</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{data?.data.map(user => (
|
||||||
|
<div key={user.id}>{user.email}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. With Parameters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { userService } from '@/services'
|
||||||
|
|
||||||
|
// Fetch with filters
|
||||||
|
const users = await userService.getUsers({
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
search: 'john',
|
||||||
|
role: 'ADMIN',
|
||||||
|
isActive: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Single user
|
||||||
|
const user = await userService.getUser('user-id-123')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Mutations (Create/Update/Delete)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { userService } from '@/services'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
function DeleteUserButton({ userId }) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: () => userService.deleteUser(userId),
|
||||||
|
onSuccess: () => {
|
||||||
|
// Refresh the users list
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||||
|
toast.success('User deleted successfully')
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.message || 'Failed to delete user')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => mutation.mutate()}
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
>
|
||||||
|
{mutation.isPending ? 'Deleting...' : 'Delete User'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Fetching List Data
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { userService } from '@/services'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
function UsersPage() {
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ['users', page, search],
|
||||||
|
queryFn: () => userService.getUsers({ page, limit: 20, search })
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
placeholder="Search users..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div>Loading...</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{data?.data.map(user => (
|
||||||
|
<UserCard key={user.id} user={user} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
page={page}
|
||||||
|
totalPages={data?.totalPages}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Creating New Records
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { userService } from '@/services'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
function CreateUserForm() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
email: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
role: 'USER'
|
||||||
|
})
|
||||||
|
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: (data) => userService.createUser(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||||
|
toast.success('User created successfully')
|
||||||
|
setFormData({ email: '', firstName: '', lastName: '', role: 'USER' })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.message || 'Failed to create user')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
createMutation.mutate(formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<input
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
|
placeholder="Email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{/* More fields... */}
|
||||||
|
|
||||||
|
<button type="submit" disabled={createMutation.isPending}>
|
||||||
|
{createMutation.isPending ? 'Creating...' : 'Create User'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Updating Records
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { userService } from '@/services'
|
||||||
|
|
||||||
|
function UpdateUserButton({ userId, updates }) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: () => userService.updateUser(userId, updates),
|
||||||
|
onSuccess: () => {
|
||||||
|
// Refresh both the list and the single user
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users', userId] })
|
||||||
|
toast.success('User updated successfully')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={() => updateMutation.mutate()}>
|
||||||
|
Update User
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: File Upload
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { userService } from '@/services'
|
||||||
|
|
||||||
|
function ImportUsersButton() {
|
||||||
|
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await userService.importUsers(file)
|
||||||
|
toast.success(`Imported ${result.imported} users. ${result.failed} failed.`)
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.response?.data?.message || 'Import failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".csv"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 5: File Download
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { userService } from '@/services'
|
||||||
|
|
||||||
|
function ExportUsersButton() {
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
const blob = await userService.exportUsers('csv')
|
||||||
|
|
||||||
|
// Create download link
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `users-${new Date().toISOString()}.csv`
|
||||||
|
a.click()
|
||||||
|
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
toast.success('Users exported successfully')
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error('Export failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={handleExport}>
|
||||||
|
Export Users
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Service Methods Reference
|
||||||
|
|
||||||
|
### AuthService
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { authService } from '@/services'
|
||||||
|
|
||||||
|
// Login
|
||||||
|
const response = await authService.login({
|
||||||
|
email: 'admin@example.com',
|
||||||
|
password: 'password'
|
||||||
|
})
|
||||||
|
// Returns: { accessToken, refreshToken, user }
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
await authService.logout()
|
||||||
|
|
||||||
|
// Refresh token
|
||||||
|
await authService.refreshToken()
|
||||||
|
|
||||||
|
// Get current user (from localStorage)
|
||||||
|
const user = authService.getCurrentUser()
|
||||||
|
|
||||||
|
// Check if authenticated
|
||||||
|
const isAuth = authService.isAuthenticated()
|
||||||
|
|
||||||
|
// Check if admin
|
||||||
|
const isAdmin = authService.isAdmin()
|
||||||
|
```
|
||||||
|
|
||||||
|
### UserService
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { userService } from '@/services'
|
||||||
|
|
||||||
|
// Get paginated users
|
||||||
|
const users = await userService.getUsers({
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
search: 'john',
|
||||||
|
role: 'ADMIN',
|
||||||
|
isActive: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get single user
|
||||||
|
const user = await userService.getUser('user-id')
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
const newUser = await userService.createUser({
|
||||||
|
email: 'user@example.com',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
role: 'USER'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update user
|
||||||
|
const updated = await userService.updateUser('user-id', {
|
||||||
|
isActive: false,
|
||||||
|
role: 'ADMIN'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete user
|
||||||
|
await userService.deleteUser('user-id', hard = false)
|
||||||
|
|
||||||
|
// Reset password
|
||||||
|
const result = await userService.resetPassword('user-id')
|
||||||
|
// Returns: { temporaryPassword: 'abc123' }
|
||||||
|
|
||||||
|
// Get user activity
|
||||||
|
const activity = await userService.getUserActivity('user-id', days = 30)
|
||||||
|
|
||||||
|
// Import users from CSV
|
||||||
|
const result = await userService.importUsers(file)
|
||||||
|
// Returns: { imported: 10, failed: 2 }
|
||||||
|
|
||||||
|
// Export users to CSV
|
||||||
|
const blob = await userService.exportUsers('csv')
|
||||||
|
```
|
||||||
|
|
||||||
|
### AnalyticsService
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { analyticsService } from '@/services'
|
||||||
|
|
||||||
|
// Get overview statistics
|
||||||
|
const stats = await analyticsService.getOverview()
|
||||||
|
// Returns: { totalUsers, activeUsers, totalRevenue, ... }
|
||||||
|
|
||||||
|
// Get user growth data
|
||||||
|
const growth = await analyticsService.getUserGrowth(days = 30)
|
||||||
|
// Returns: [{ date: '2024-01-01', users: 100, ... }]
|
||||||
|
|
||||||
|
// Get revenue data
|
||||||
|
const revenue = await analyticsService.getRevenue('30days')
|
||||||
|
// Options: '7days', '30days', '90days'
|
||||||
|
|
||||||
|
// Get API usage
|
||||||
|
const apiUsage = await analyticsService.getApiUsage(days = 7)
|
||||||
|
|
||||||
|
// Get error rate
|
||||||
|
const errorRate = await analyticsService.getErrorRate(days = 7)
|
||||||
|
|
||||||
|
// Get storage analytics
|
||||||
|
const storage = await analyticsService.getStorageAnalytics()
|
||||||
|
```
|
||||||
|
|
||||||
|
### SecurityService
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { securityService } from '@/services'
|
||||||
|
|
||||||
|
// Get suspicious activity
|
||||||
|
const suspicious = await securityService.getSuspiciousActivity()
|
||||||
|
// Returns: { suspiciousIPs: [...], suspiciousEmails: [...] }
|
||||||
|
|
||||||
|
// Get active sessions
|
||||||
|
const sessions = await securityService.getActiveSessions()
|
||||||
|
|
||||||
|
// Terminate session
|
||||||
|
await securityService.terminateSession('session-id')
|
||||||
|
|
||||||
|
// Get failed login attempts
|
||||||
|
const failedLogins = await securityService.getFailedLogins({
|
||||||
|
page: 1,
|
||||||
|
limit: 50,
|
||||||
|
email: 'user@example.com'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get rate limit violations
|
||||||
|
const violations = await securityService.getRateLimitViolations(days = 7)
|
||||||
|
|
||||||
|
// Get all API keys
|
||||||
|
const apiKeys = await securityService.getAllApiKeys()
|
||||||
|
|
||||||
|
// Revoke API key
|
||||||
|
await securityService.revokeApiKey('key-id')
|
||||||
|
|
||||||
|
// Ban IP address
|
||||||
|
await securityService.banIpAddress('192.168.1.1', 'Suspicious activity')
|
||||||
|
```
|
||||||
|
|
||||||
|
### SystemService
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { systemService } from '@/services'
|
||||||
|
|
||||||
|
// Get system health
|
||||||
|
const health = await systemService.getHealth()
|
||||||
|
// Returns: { status: 'healthy', database: 'connected', ... }
|
||||||
|
|
||||||
|
// Get system info
|
||||||
|
const info = await systemService.getSystemInfo()
|
||||||
|
// Returns: { version, environment, memory, cpu, ... }
|
||||||
|
|
||||||
|
// Get maintenance status
|
||||||
|
const maintenance = await systemService.getMaintenanceStatus()
|
||||||
|
|
||||||
|
// Enable maintenance mode
|
||||||
|
await systemService.enableMaintenance('System upgrade in progress')
|
||||||
|
|
||||||
|
// Disable maintenance mode
|
||||||
|
await systemService.disableMaintenance()
|
||||||
|
|
||||||
|
// Clear cache
|
||||||
|
await systemService.clearCache()
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
const result = await systemService.runMigrations()
|
||||||
|
```
|
||||||
|
|
||||||
|
### AnnouncementService
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { announcementService } from '@/services'
|
||||||
|
|
||||||
|
// Get all announcements
|
||||||
|
const announcements = await announcementService.getAnnouncements(activeOnly = false)
|
||||||
|
|
||||||
|
// Get single announcement
|
||||||
|
const announcement = await announcementService.getAnnouncement('announcement-id')
|
||||||
|
|
||||||
|
// Create announcement
|
||||||
|
const newAnnouncement = await announcementService.createAnnouncement({
|
||||||
|
title: 'Maintenance Notice',
|
||||||
|
message: 'System will be down for maintenance',
|
||||||
|
type: 'warning',
|
||||||
|
priority: 1,
|
||||||
|
targetAudience: 'all'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update announcement
|
||||||
|
const updated = await announcementService.updateAnnouncement('announcement-id', {
|
||||||
|
title: 'Updated Title'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Toggle announcement active status
|
||||||
|
await announcementService.toggleAnnouncement('announcement-id')
|
||||||
|
|
||||||
|
// Delete announcement
|
||||||
|
await announcementService.deleteAnnouncement('announcement-id')
|
||||||
|
```
|
||||||
|
|
||||||
|
### AuditService
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { auditService } from '@/services'
|
||||||
|
|
||||||
|
// Get audit logs
|
||||||
|
const logs = await auditService.getAuditLogs({
|
||||||
|
page: 1,
|
||||||
|
limit: 50,
|
||||||
|
userId: 'user-id',
|
||||||
|
action: 'DELETE',
|
||||||
|
resourceType: 'user',
|
||||||
|
startDate: '2024-01-01',
|
||||||
|
endDate: '2024-12-31'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get audit log by ID
|
||||||
|
const log = await auditService.getAuditLog('log-id')
|
||||||
|
|
||||||
|
// Get user audit activity
|
||||||
|
const activity = await auditService.getUserAuditActivity('user-id', days = 30)
|
||||||
|
|
||||||
|
// Get resource history
|
||||||
|
const history = await auditService.getResourceHistory('user', 'resource-id')
|
||||||
|
|
||||||
|
// Get audit statistics
|
||||||
|
const stats = await auditService.getAuditStats(startDate, endDate)
|
||||||
|
|
||||||
|
// Export audit logs
|
||||||
|
const blob = await auditService.exportAuditLogs({
|
||||||
|
format: 'csv',
|
||||||
|
startDate: '2024-01-01',
|
||||||
|
endDate: '2024-12-31'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### SettingsService
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { settingsService } from '@/services'
|
||||||
|
|
||||||
|
// Get all settings
|
||||||
|
const settings = await settingsService.getSettings(category = 'GENERAL')
|
||||||
|
|
||||||
|
// Get single setting
|
||||||
|
const setting = await settingsService.getSetting('feature_flag')
|
||||||
|
|
||||||
|
// Create setting
|
||||||
|
const newSetting = await settingsService.createSetting({
|
||||||
|
key: 'feature_flag',
|
||||||
|
value: 'true',
|
||||||
|
category: 'FEATURES',
|
||||||
|
description: 'Enable new feature',
|
||||||
|
isPublic: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update setting
|
||||||
|
const updated = await settingsService.updateSetting('feature_flag', {
|
||||||
|
value: 'false'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete setting
|
||||||
|
await settingsService.deleteSetting('feature_flag')
|
||||||
|
|
||||||
|
// Get public settings (for frontend use)
|
||||||
|
const publicSettings = await settingsService.getPublicSettings()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Standard Error Pattern
|
||||||
|
|
||||||
|
All services throw errors with consistent structure:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
await userService.deleteUser(userId)
|
||||||
|
toast.success('User deleted')
|
||||||
|
} catch (error: any) {
|
||||||
|
// Error structure:
|
||||||
|
// error.response.status - HTTP status code
|
||||||
|
// error.response.data.message - Error message from backend
|
||||||
|
|
||||||
|
const message = error.response?.data?.message || 'Operation failed'
|
||||||
|
toast.error(message)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common HTTP Status Codes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 400 - Bad Request (validation error)
|
||||||
|
catch (error: any) {
|
||||||
|
if (error.response?.status === 400) {
|
||||||
|
toast.error('Invalid input: ' + error.response.data.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 401 - Unauthorized (handled automatically by interceptor)
|
||||||
|
// Token refresh attempted automatically
|
||||||
|
// If refresh fails, user redirected to login
|
||||||
|
|
||||||
|
// 403 - Forbidden (no permission)
|
||||||
|
catch (error: any) {
|
||||||
|
if (error.response?.status === 403) {
|
||||||
|
toast.error('You do not have permission to perform this action')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 404 - Not Found
|
||||||
|
catch (error: any) {
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
toast.error('Resource not found')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 500 - Server Error
|
||||||
|
catch (error: any) {
|
||||||
|
if (error.response?.status === 500) {
|
||||||
|
toast.error('Server error. Please try again later.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### React Query Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = useQuery({
|
||||||
|
queryKey: ['users'],
|
||||||
|
queryFn: () => userService.getUsers(),
|
||||||
|
retry: 3, // Retry failed requests
|
||||||
|
retryDelay: 1000, // Wait 1s between retries
|
||||||
|
})
|
||||||
|
|
||||||
|
// Display error
|
||||||
|
if (error) {
|
||||||
|
return <div>Error: {error.message}</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### ✅ DO: Use Services
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Good
|
||||||
|
import { userService } from '@/services'
|
||||||
|
const users = await userService.getUsers()
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ DON'T: Direct API Calls
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad - don't do this
|
||||||
|
import axios from 'axios'
|
||||||
|
const response = await axios.get('/api/users')
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ DO: Use React Query
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Good - caching, loading states, error handling
|
||||||
|
const { data, isLoading, error } = useQuery({
|
||||||
|
queryKey: ['users'],
|
||||||
|
queryFn: () => userService.getUsers()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ DON'T: Manual State Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad - manual state management
|
||||||
|
const [users, setUsers] = useState([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true)
|
||||||
|
userService.getUsers()
|
||||||
|
.then(setUsers)
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [])
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ DO: Specific Query Keys
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Good - specific, cacheable
|
||||||
|
queryKey: ['users', page, limit, search]
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ DON'T: Generic Query Keys
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad - too generic
|
||||||
|
queryKey: ['data']
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ DO: Invalidate After Mutations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Good - refresh data after changes
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: userService.deleteUser,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ DO: Handle Errors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Good - user feedback
|
||||||
|
try {
|
||||||
|
await userService.deleteUser(id)
|
||||||
|
toast.success('User deleted')
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.response?.data?.message)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ DON'T: Ignore Errors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad - no error handling
|
||||||
|
await userService.deleteUser(id)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Complete CRUD Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { userService } from '@/services'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
function UsersManagement() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [editingUser, setEditingUser] = useState(null)
|
||||||
|
|
||||||
|
// READ - Fetch users
|
||||||
|
const { data: users, isLoading } = useQuery({
|
||||||
|
queryKey: ['users', page],
|
||||||
|
queryFn: () => userService.getUsers({ page, limit: 20 })
|
||||||
|
})
|
||||||
|
|
||||||
|
// CREATE - Add new user
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: (data) => userService.createUser(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||||
|
toast.success('User created')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// UPDATE - Edit user
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: ({ id, data }) => userService.updateUser(id, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||||
|
setEditingUser(null)
|
||||||
|
toast.success('User updated')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// DELETE - Remove user
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: (id) => userService.deleteUser(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||||
|
toast.success('User deleted')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Users Management</h1>
|
||||||
|
|
||||||
|
{/* User List */}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users?.data.map(user => (
|
||||||
|
<tr key={user.id}>
|
||||||
|
<td>{user.email}</td>
|
||||||
|
<td>{user.firstName} {user.lastName}</td>
|
||||||
|
<td>{user.role}</td>
|
||||||
|
<td>
|
||||||
|
<button onClick={() => setEditingUser(user)}>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button onClick={() => deleteMutation.mutate(user.id)}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => setPage(p => p - 1)}
|
||||||
|
disabled={page === 1}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<span>Page {page} of {users?.totalPages}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setPage(p => p + 1)}
|
||||||
|
disabled={page === users?.totalPages}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
### Key Takeaways
|
||||||
|
|
||||||
|
1. **Always use services** - Never make direct API calls
|
||||||
|
2. **Use React Query** - For data fetching and caching
|
||||||
|
3. **Handle errors** - Provide user feedback
|
||||||
|
4. **Invalidate queries** - After mutations
|
||||||
|
5. **Use specific query keys** - For better caching
|
||||||
|
|
||||||
|
### Quick Reference
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Import
|
||||||
|
import { userService } from '@/services'
|
||||||
|
|
||||||
|
// Fetch
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ['users'],
|
||||||
|
queryFn: () => userService.getUsers()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mutate
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (id) => userService.deleteUser(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||||
|
toast.success('Success')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
try {
|
||||||
|
await userService.deleteUser(id)
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.response?.data?.message)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**For more information:**
|
||||||
|
- [Development Guide](./DEVELOPMENT.md) - General development practices
|
||||||
|
- [Testing Guide](./TESTING_GUIDE.md) - How to test services
|
||||||
|
- [Security Guide](./SECURITY.md) - Security best practices
|
||||||
|
|
@ -1,476 +0,0 @@
|
||||||
# API Client Standards
|
|
||||||
|
|
||||||
## Industry Best Practices Implemented
|
|
||||||
|
|
||||||
### 1. Separation of Concerns
|
|
||||||
- **Public API Instance**: Used for unauthenticated endpoints (login, register, forgot password)
|
|
||||||
- **Authenticated API Instance**: Used for protected endpoints requiring authentication
|
|
||||||
- This prevents unnecessary token attachment to public endpoints
|
|
||||||
|
|
||||||
### 2. Cookie-Based Authentication (Recommended)
|
|
||||||
|
|
||||||
#### Configuration
|
|
||||||
```typescript
|
|
||||||
withCredentials: true // Enables sending/receiving cookies
|
|
||||||
```
|
|
||||||
|
|
||||||
This allows the backend to set httpOnly cookies which are:
|
|
||||||
- **Secure**: Not accessible via JavaScript (prevents XSS attacks)
|
|
||||||
- **Automatic**: Browser automatically sends with each request
|
|
||||||
- **Industry Standard**: Used by major platforms (Google, Facebook, etc.)
|
|
||||||
|
|
||||||
#### Backend Requirements for Cookie-Based Auth
|
|
||||||
|
|
||||||
**Login Response:**
|
|
||||||
```http
|
|
||||||
POST /auth/login
|
|
||||||
Set-Cookie: access_token=<jwt>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
|
|
||||||
Set-Cookie: refresh_token=<jwt>; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=604800
|
|
||||||
|
|
||||||
Response Body:
|
|
||||||
{
|
|
||||||
"user": {
|
|
||||||
"id": "user-id",
|
|
||||||
"email": "user@example.com",
|
|
||||||
"role": "ADMIN"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cookie Attributes Explained:**
|
|
||||||
- `HttpOnly`: Prevents JavaScript access (XSS protection)
|
|
||||||
- `Secure`: Only sent over HTTPS (production)
|
|
||||||
- `SameSite=Strict`: CSRF protection
|
|
||||||
- `Path=/`: Cookie scope
|
|
||||||
- `Max-Age`: Expiration time in seconds
|
|
||||||
|
|
||||||
**Logout:**
|
|
||||||
```http
|
|
||||||
POST /auth/logout
|
|
||||||
Set-Cookie: access_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0
|
|
||||||
Set-Cookie: refresh_token=; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=0
|
|
||||||
```
|
|
||||||
|
|
||||||
**Token Refresh:**
|
|
||||||
```http
|
|
||||||
POST /auth/refresh
|
|
||||||
Cookie: refresh_token=<jwt>
|
|
||||||
|
|
||||||
Response:
|
|
||||||
Set-Cookie: access_token=<new_jwt>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Fallback: localStorage (Current Implementation)
|
|
||||||
|
|
||||||
For backends that don't support httpOnly cookies, the system falls back to localStorage:
|
|
||||||
- Token stored in `localStorage.access_token`
|
|
||||||
- Automatically added to Authorization header
|
|
||||||
- Less secure than cookies (vulnerable to XSS)
|
|
||||||
|
|
||||||
### 4. Authentication Flow
|
|
||||||
|
|
||||||
#### Login
|
|
||||||
```typescript
|
|
||||||
// Uses publicApi (no token required)
|
|
||||||
adminApiHelpers.login({ email, password })
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response Expected (Cookie-based):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"user": {
|
|
||||||
"id": "user-id",
|
|
||||||
"email": "user@example.com",
|
|
||||||
"role": "ADMIN",
|
|
||||||
"firstName": "John",
|
|
||||||
"lastName": "Doe"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// + Set-Cookie headers
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response Expected (localStorage fallback):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"access_token": "jwt-token",
|
|
||||||
"refresh_token": "refresh-token",
|
|
||||||
"user": {
|
|
||||||
"id": "user-id",
|
|
||||||
"email": "user@example.com",
|
|
||||||
"role": "ADMIN"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Logout
|
|
||||||
```typescript
|
|
||||||
// Centralized logout handling
|
|
||||||
await adminApiHelpers.logout()
|
|
||||||
```
|
|
||||||
- Calls backend `/auth/logout` to clear httpOnly cookies
|
|
||||||
- Clears localStorage (access_token, user)
|
|
||||||
- Prevents duplicate logout logic across components
|
|
||||||
|
|
||||||
#### Token Refresh (Automatic)
|
|
||||||
```typescript
|
|
||||||
// Automatically called on 401 response
|
|
||||||
adminApiHelpers.refreshToken()
|
|
||||||
```
|
|
||||||
- Refreshes expired access token using refresh token
|
|
||||||
- Retries failed request with new token
|
|
||||||
- If refresh fails, logs out user
|
|
||||||
|
|
||||||
#### Get Current User
|
|
||||||
```typescript
|
|
||||||
adminApiHelpers.getCurrentUser()
|
|
||||||
```
|
|
||||||
- Validates token and fetches current user data
|
|
||||||
- Useful for session validation
|
|
||||||
|
|
||||||
### 5. Interceptor Improvements
|
|
||||||
|
|
||||||
#### Request Interceptor
|
|
||||||
- Adds `withCredentials: true` to send cookies
|
|
||||||
- Adds Authorization header if localStorage token exists (fallback)
|
|
||||||
- Bearer token format: `Authorization: Bearer <token>`
|
|
||||||
|
|
||||||
#### Response Interceptor
|
|
||||||
- **401 Unauthorized**:
|
|
||||||
- Attempts automatic token refresh
|
|
||||||
- Retries original request
|
|
||||||
- If refresh fails, auto-logout and redirect
|
|
||||||
- Prevents infinite loops on login page
|
|
||||||
- **403 Forbidden**: Shows permission error toast
|
|
||||||
- **404 Not Found**: Shows resource not found toast
|
|
||||||
- **500 Server Error**: Shows server error toast
|
|
||||||
- **Network Error**: Shows connection error toast
|
|
||||||
|
|
||||||
### 6. Security Best Practices
|
|
||||||
|
|
||||||
✅ **Implemented:**
|
|
||||||
- Separate public/private API instances
|
|
||||||
- Cookie support with `withCredentials: true`
|
|
||||||
- Bearer token authentication (fallback)
|
|
||||||
- Automatic token injection
|
|
||||||
- Automatic token refresh on 401
|
|
||||||
- Centralized logout with backend call
|
|
||||||
- Auto-redirect on 401 (with login page check)
|
|
||||||
- Retry mechanism for failed requests
|
|
||||||
|
|
||||||
✅ **Backend Should Implement:**
|
|
||||||
- httpOnly cookies for tokens
|
|
||||||
- Secure flag (HTTPS only)
|
|
||||||
- SameSite=Strict (CSRF protection)
|
|
||||||
- Short-lived access tokens (15 min)
|
|
||||||
- Long-lived refresh tokens (7 days)
|
|
||||||
- Token rotation on refresh
|
|
||||||
- Logout endpoint to clear cookies
|
|
||||||
|
|
||||||
⚠️ **Additional Production Recommendations:**
|
|
||||||
- Rate limiting on login endpoint
|
|
||||||
- Account lockout after failed attempts
|
|
||||||
- Two-factor authentication (2FA)
|
|
||||||
- IP whitelisting for admin access
|
|
||||||
- Audit logging for all admin actions
|
|
||||||
- Content Security Policy (CSP) headers
|
|
||||||
- CORS configuration
|
|
||||||
- Request/response encryption for sensitive data
|
|
||||||
|
|
||||||
### 7. Security Comparison
|
|
||||||
|
|
||||||
| Feature | localStorage | httpOnly Cookies |
|
|
||||||
|---------|-------------|------------------|
|
|
||||||
| XSS Protection | ❌ Vulnerable | ✅ Protected |
|
|
||||||
| CSRF Protection | ✅ Not vulnerable | ⚠️ Needs SameSite |
|
|
||||||
| Automatic Sending | ❌ Manual | ✅ Automatic |
|
|
||||||
| Cross-domain | ✅ Easy | ⚠️ Complex |
|
|
||||||
| Mobile Apps | ✅ Works | ❌ Limited |
|
|
||||||
| Industry Standard | ⚠️ Common | ✅ Recommended |
|
|
||||||
|
|
||||||
### 8. Error Handling
|
|
||||||
|
|
||||||
All API errors are consistently handled:
|
|
||||||
- User-friendly error messages
|
|
||||||
- Toast notifications for feedback
|
|
||||||
- Proper error propagation for component-level handling
|
|
||||||
- Automatic retry on token expiration
|
|
||||||
|
|
||||||
### 9. Type Safety
|
|
||||||
|
|
||||||
All API methods have TypeScript types for:
|
|
||||||
- Request parameters
|
|
||||||
- Request body
|
|
||||||
- Response data (can be improved with response types)
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Login Flow (Cookie-based)
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
const response = await adminApiHelpers.login({ email, password })
|
|
||||||
const { user } = response.data // No access_token in response
|
|
||||||
|
|
||||||
// Verify admin role
|
|
||||||
if (user.role !== 'ADMIN') {
|
|
||||||
throw new Error('Admin access required')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store user data only (token is in httpOnly cookie)
|
|
||||||
localStorage.setItem('user', JSON.stringify(user))
|
|
||||||
|
|
||||||
// Navigate to dashboard
|
|
||||||
navigate('/admin/dashboard')
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('Login failed')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Authenticated Request
|
|
||||||
```typescript
|
|
||||||
// Token automatically sent via cookie or Authorization header
|
|
||||||
const response = await adminApiHelpers.getUsers({ page: 1, limit: 20 })
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logout
|
|
||||||
```typescript
|
|
||||||
// Centralized logout (clears cookies and localStorage)
|
|
||||||
await adminApiHelpers.logout()
|
|
||||||
navigate('/login')
|
|
||||||
```
|
|
||||||
|
|
||||||
### Automatic Token Refresh
|
|
||||||
```typescript
|
|
||||||
// Happens automatically on 401 response
|
|
||||||
// No manual intervention needed
|
|
||||||
const response = await adminApiHelpers.getUsers()
|
|
||||||
// If token expired, it's automatically refreshed and request retried
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Endpoint Requirements
|
|
||||||
|
|
||||||
### Authentication Endpoints
|
|
||||||
|
|
||||||
#### POST /auth/login
|
|
||||||
- **Public endpoint** (no authentication required)
|
|
||||||
- Validates credentials
|
|
||||||
- **Cookie-based**: Sets httpOnly cookies in response headers
|
|
||||||
- **localStorage fallback**: Returns access_token in response body
|
|
||||||
- Returns user data
|
|
||||||
- Should verify user role on backend
|
|
||||||
|
|
||||||
**Request:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"password": "password123"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response (Cookie-based):**
|
|
||||||
```http
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Set-Cookie: access_token=<jwt>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900
|
|
||||||
Set-Cookie: refresh_token=<jwt>; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=604800
|
|
||||||
|
|
||||||
{
|
|
||||||
"user": {
|
|
||||||
"id": "123",
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"role": "ADMIN",
|
|
||||||
"firstName": "John",
|
|
||||||
"lastName": "Doe"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response (localStorage fallback):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"access_token": "eyJhbGc...",
|
|
||||||
"refresh_token": "eyJhbGc...",
|
|
||||||
"user": {
|
|
||||||
"id": "123",
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"role": "ADMIN"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### POST /auth/logout
|
|
||||||
- **Protected endpoint**
|
|
||||||
- Clears httpOnly cookies
|
|
||||||
- Invalidates tokens on server
|
|
||||||
- Clears server-side session
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```http
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Set-Cookie: access_token=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0
|
|
||||||
Set-Cookie: refresh_token=; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=0
|
|
||||||
|
|
||||||
{
|
|
||||||
"message": "Logged out successfully"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### POST /auth/refresh
|
|
||||||
- **Protected endpoint**
|
|
||||||
- Reads refresh_token from httpOnly cookie
|
|
||||||
- Returns new access token
|
|
||||||
- Implements token rotation (optional)
|
|
||||||
|
|
||||||
**Request:**
|
|
||||||
```http
|
|
||||||
Cookie: refresh_token=<jwt>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```http
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Set-Cookie: access_token=<new_jwt>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900
|
|
||||||
Set-Cookie: refresh_token=<new_jwt>; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=604800
|
|
||||||
|
|
||||||
{
|
|
||||||
"message": "Token refreshed"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### GET /auth/me
|
|
||||||
- **Protected endpoint**
|
|
||||||
- Returns current authenticated user
|
|
||||||
- Useful for session validation
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "123",
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"role": "ADMIN",
|
|
||||||
"firstName": "John",
|
|
||||||
"lastName": "Doe"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Backend Implementation Guide
|
|
||||||
|
|
||||||
### Node.js/Express Example
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Login endpoint
|
|
||||||
app.post('/auth/login', async (req, res) => {
|
|
||||||
const { email, password } = req.body
|
|
||||||
|
|
||||||
// Validate credentials
|
|
||||||
const user = await validateUser(email, password)
|
|
||||||
if (!user) {
|
|
||||||
return res.status(401).json({ message: 'Invalid credentials' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate tokens
|
|
||||||
const accessToken = generateAccessToken(user)
|
|
||||||
const refreshToken = generateRefreshToken(user)
|
|
||||||
|
|
||||||
// Set httpOnly cookies
|
|
||||||
res.cookie('access_token', accessToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'strict',
|
|
||||||
maxAge: 15 * 60 * 1000 // 15 minutes
|
|
||||||
})
|
|
||||||
|
|
||||||
res.cookie('refresh_token', refreshToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'strict',
|
|
||||||
path: '/auth/refresh',
|
|
||||||
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return user data (no tokens in body)
|
|
||||||
res.json({ user: sanitizeUser(user) })
|
|
||||||
})
|
|
||||||
|
|
||||||
// Logout endpoint
|
|
||||||
app.post('/auth/logout', (req, res) => {
|
|
||||||
res.clearCookie('access_token')
|
|
||||||
res.clearCookie('refresh_token', { path: '/auth/refresh' })
|
|
||||||
res.json({ message: 'Logged out successfully' })
|
|
||||||
})
|
|
||||||
|
|
||||||
// Refresh endpoint
|
|
||||||
app.post('/auth/refresh', async (req, res) => {
|
|
||||||
const refreshToken = req.cookies.refresh_token
|
|
||||||
|
|
||||||
if (!refreshToken) {
|
|
||||||
return res.status(401).json({ message: 'No refresh token' })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const decoded = verifyRefreshToken(refreshToken)
|
|
||||||
const newAccessToken = generateAccessToken(decoded)
|
|
||||||
|
|
||||||
res.cookie('access_token', newAccessToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'strict',
|
|
||||||
maxAge: 15 * 60 * 1000
|
|
||||||
})
|
|
||||||
|
|
||||||
res.json({ message: 'Token refreshed' })
|
|
||||||
} catch (error) {
|
|
||||||
res.status(401).json({ message: 'Invalid refresh token' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Auth middleware
|
|
||||||
const authMiddleware = (req, res, next) => {
|
|
||||||
const token = req.cookies.access_token ||
|
|
||||||
req.headers.authorization?.replace('Bearer ', '')
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return res.status(401).json({ message: 'No token provided' })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const decoded = verifyAccessToken(token)
|
|
||||||
req.user = decoded
|
|
||||||
next()
|
|
||||||
} catch (error) {
|
|
||||||
res.status(401).json({ message: 'Invalid token' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### CORS Configuration
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
app.use(cors({
|
|
||||||
origin: 'http://localhost:5173', // Your frontend URL
|
|
||||||
credentials: true // Important: Allow cookies
|
|
||||||
}))
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Notes
|
|
||||||
|
|
||||||
If migrating from the old implementation:
|
|
||||||
1. Login now uses `publicApi` instead of `adminApi`
|
|
||||||
2. Added `withCredentials: true` for cookie support
|
|
||||||
3. Logout is centralized and calls backend endpoint
|
|
||||||
4. Automatic token refresh on 401 responses
|
|
||||||
5. Backend should set httpOnly cookies instead of returning tokens
|
|
||||||
6. Frontend stores only user data, not tokens (if using cookies)
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Test Cookie-based Auth
|
|
||||||
1. Login and check browser DevTools > Application > Cookies
|
|
||||||
2. Should see `access_token` and `refresh_token` cookies
|
|
||||||
3. Cookies should have HttpOnly, Secure, SameSite flags
|
|
||||||
4. Make authenticated request - cookie sent automatically
|
|
||||||
5. Logout - cookies should be cleared
|
|
||||||
|
|
||||||
### Test localStorage Fallback
|
|
||||||
1. Backend returns `access_token` in response body
|
|
||||||
2. Token stored in localStorage
|
|
||||||
3. Token added to Authorization header automatically
|
|
||||||
4. Works for backends without cookie support
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
# Authentication Setup
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
The admin dashboard now requires authentication before accessing any admin routes and follows industry-standard security practices.
|
|
||||||
|
|
||||||
## Security Status
|
|
||||||
|
|
||||||
### ✅ Frontend Security (Implemented)
|
|
||||||
- Protected routes with authentication check
|
|
||||||
- Role-based access control (ADMIN only)
|
|
||||||
- httpOnly cookie support (`withCredentials: true`)
|
|
||||||
- Automatic token refresh on expiration
|
|
||||||
- Centralized logout with backend call
|
|
||||||
- localStorage fallback for compatibility
|
|
||||||
- Secure error handling
|
|
||||||
- CSRF protection ready (via SameSite cookies)
|
|
||||||
|
|
||||||
### ⚠️ Backend Security (Required)
|
|
||||||
The backend MUST implement these critical security measures:
|
|
||||||
1. **httpOnly Cookies**: Store tokens in httpOnly cookies (not response body)
|
|
||||||
2. **Password Hashing**: Use bcrypt with salt rounds >= 12
|
|
||||||
3. **Rate Limiting**: Limit login attempts (5 per 15 minutes)
|
|
||||||
4. **HTTPS**: Enable HTTPS in production
|
|
||||||
5. **Token Refresh**: Implement refresh token endpoint
|
|
||||||
6. **Input Validation**: Sanitize and validate all inputs
|
|
||||||
7. **CORS**: Configure with specific origin and credentials
|
|
||||||
8. **Security Headers**: Use helmet.js for security headers
|
|
||||||
9. **Audit Logging**: Log all admin actions
|
|
||||||
10. **SQL Injection Prevention**: Use parameterized queries
|
|
||||||
|
|
||||||
See `SECURITY_CHECKLIST.md` for complete requirements.
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
### 1. Protected Routes
|
|
||||||
All admin routes are wrapped with `ProtectedRoute` component that checks for a valid access token.
|
|
||||||
|
|
||||||
### 2. Login Flow
|
|
||||||
- User visits any admin route without authentication → Redirected to `/login`
|
|
||||||
- User enters credentials → API validates and returns user data
|
|
||||||
- Backend sets httpOnly cookies (recommended) OR returns token (fallback)
|
|
||||||
- Token/cookies stored, user redirected to originally requested page
|
|
||||||
|
|
||||||
### 3. Token Management
|
|
||||||
- **Recommended**: Tokens stored in httpOnly cookies (XSS protection)
|
|
||||||
- **Fallback**: Access token in localStorage, automatically added to requests
|
|
||||||
- Token automatically sent with all API requests
|
|
||||||
- On 401 response, automatically attempts token refresh
|
|
||||||
- If refresh fails, user is logged out and redirected to login
|
|
||||||
|
|
||||||
### 4. Logout
|
|
||||||
- Calls backend `/auth/logout` to clear httpOnly cookies
|
|
||||||
- Clears localStorage (access_token, user)
|
|
||||||
- Redirects to `/login` page
|
|
||||||
|
|
||||||
## API Endpoints Required
|
|
||||||
|
|
||||||
### POST /auth/login
|
|
||||||
**Request:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"password": "password123"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response (Cookie-based - Recommended):**
|
|
||||||
```http
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Set-Cookie: access_token=<jwt>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900
|
|
||||||
Set-Cookie: refresh_token=<jwt>; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=604800
|
|
||||||
|
|
||||||
{
|
|
||||||
"user": {
|
|
||||||
"id": "user-id",
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"firstName": "Admin",
|
|
||||||
"lastName": "User",
|
|
||||||
"role": "ADMIN"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response (localStorage fallback):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
||||||
"user": {
|
|
||||||
"id": "user-id",
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"role": "ADMIN"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### POST /auth/logout
|
|
||||||
Clears httpOnly cookies and invalidates tokens.
|
|
||||||
|
|
||||||
### POST /auth/refresh
|
|
||||||
Refreshes expired access token using refresh token from cookie.
|
|
||||||
|
|
||||||
### GET /auth/me (Optional)
|
|
||||||
Returns current authenticated user for session validation.
|
|
||||||
|
|
||||||
## Files Modified/Created
|
|
||||||
|
|
||||||
### Created:
|
|
||||||
- `src/pages/login/index.tsx` - Login page with show/hide password
|
|
||||||
- `src/components/ProtectedRoute.tsx` - Route protection wrapper
|
|
||||||
- `dev-docs/AUTHENTICATION.md` - This documentation
|
|
||||||
- `dev-docs/API_STANDARDS.md` - Detailed API standards
|
|
||||||
- `dev-docs/SECURITY_CHECKLIST.md` - Complete security checklist
|
|
||||||
|
|
||||||
### Modified:
|
|
||||||
- `src/App.tsx` - Added login route and protected admin routes
|
|
||||||
- `src/layouts/app-shell.tsx` - User state management and logout
|
|
||||||
- `src/lib/api-client.ts` - Cookie support, token refresh, centralized auth
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
1. Start the application
|
|
||||||
2. Navigate to any admin route (e.g., `/admin/dashboard`)
|
|
||||||
3. Should be redirected to `/login`
|
|
||||||
4. Enter valid admin credentials
|
|
||||||
5. Should be redirected back to the dashboard
|
|
||||||
6. Check browser DevTools > Application > Cookies (if backend uses cookies)
|
|
||||||
7. Click logout to clear session
|
|
||||||
|
|
||||||
## Security Comparison
|
|
||||||
|
|
||||||
| Feature | Current (Frontend) | With Backend Implementation |
|
|
||||||
|---------|-------------------|----------------------------|
|
|
||||||
| XSS Protection | ⚠️ Partial (localStorage) | ✅ Full (httpOnly cookies) |
|
|
||||||
| CSRF Protection | ✅ Ready | ✅ Full (SameSite cookies) |
|
|
||||||
| Token Refresh | ✅ Automatic | ✅ Automatic |
|
|
||||||
| Rate Limiting | ❌ None | ✅ Required |
|
|
||||||
| Password Hashing | ❌ Backend only | ✅ Required |
|
|
||||||
| Audit Logging | ❌ Backend only | ✅ Required |
|
|
||||||
| HTTPS | ⚠️ Production | ✅ Required |
|
|
||||||
|
|
||||||
## Production Deployment Checklist
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- ✅ Build with production environment variables
|
|
||||||
- ✅ Enable HTTPS
|
|
||||||
- ✅ Configure CSP headers
|
|
||||||
- ✅ Set secure cookie flags
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- ⚠️ Implement httpOnly cookies
|
|
||||||
- ⚠️ Enable HTTPS with valid SSL certificate
|
|
||||||
- ⚠️ Configure CORS with specific origin
|
|
||||||
- ⚠️ Add rate limiting
|
|
||||||
- ⚠️ Implement password hashing
|
|
||||||
- ⚠️ Add security headers (helmet.js)
|
|
||||||
- ⚠️ Set up audit logging
|
|
||||||
- ⚠️ Configure environment variables
|
|
||||||
- ⚠️ Enable database encryption
|
|
||||||
- ⚠️ Set up monitoring and alerting
|
|
||||||
|
|
||||||
## Security Notes
|
|
||||||
|
|
||||||
### Current Implementation
|
|
||||||
- Frontend follows industry standards
|
|
||||||
- Supports both cookie-based and localStorage authentication
|
|
||||||
- Automatic token refresh prevents session interruption
|
|
||||||
- Centralized error handling and logout
|
|
||||||
|
|
||||||
### Backend Requirements
|
|
||||||
- **Critical**: Backend must implement security measures in `SECURITY_CHECKLIST.md`
|
|
||||||
- **Recommended**: Use httpOnly cookies instead of localStorage
|
|
||||||
- **Required**: Implement rate limiting, password hashing, HTTPS
|
|
||||||
- **Important**: Regular security audits and updates
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For detailed security requirements, see:
|
|
||||||
- `dev-docs/SECURITY_CHECKLIST.md` - Complete security checklist
|
|
||||||
- `dev-docs/API_STANDARDS.md` - API implementation guide
|
|
||||||
- `dev-docs/DEPLOYMENT.md` - Deployment instructions
|
|
||||||
|
|
@ -1,209 +0,0 @@
|
||||||
# CI/CD Setup Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This project uses **GitHub Actions** for continuous integration and deployment.
|
|
||||||
|
|
||||||
## Workflows
|
|
||||||
|
|
||||||
### 1. CI Workflow (`.github/workflows/ci.yml`)
|
|
||||||
|
|
||||||
Runs on every push and pull request to main/develop branches.
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
1. Checkout code
|
|
||||||
2. Setup Node.js (18.x, 20.x matrix)
|
|
||||||
3. Install dependencies
|
|
||||||
4. Run linter
|
|
||||||
5. Run type check
|
|
||||||
6. Run tests
|
|
||||||
7. Generate coverage report
|
|
||||||
8. Upload coverage to Codecov
|
|
||||||
9. Build application
|
|
||||||
10. Upload build artifacts
|
|
||||||
11. Security audit
|
|
||||||
|
|
||||||
### 2. Deploy Workflow (`.github/workflows/deploy.yml`)
|
|
||||||
|
|
||||||
Runs on push to main branch or manual trigger.
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
1. Checkout code
|
|
||||||
2. Setup Node.js
|
|
||||||
3. Install dependencies
|
|
||||||
4. Run tests
|
|
||||||
5. Build for production
|
|
||||||
6. Deploy to Netlify/Vercel
|
|
||||||
7. Notify deployment status
|
|
||||||
|
|
||||||
## Required Secrets
|
|
||||||
|
|
||||||
Configure these in GitHub Settings > Secrets and variables > Actions:
|
|
||||||
|
|
||||||
### For CI
|
|
||||||
- `CODECOV_TOKEN` - Codecov upload token (optional)
|
|
||||||
- `SNYK_TOKEN` - Snyk security scanning token (optional)
|
|
||||||
- `VITE_BACKEND_API_URL` - API URL for build
|
|
||||||
|
|
||||||
### For Deployment
|
|
||||||
|
|
||||||
#### Netlify
|
|
||||||
- `NETLIFY_AUTH_TOKEN` - Netlify authentication token
|
|
||||||
- `NETLIFY_SITE_ID` - Netlify site ID
|
|
||||||
- `VITE_BACKEND_API_URL_PROD` - Production API URL
|
|
||||||
- `VITE_SENTRY_DSN` - Sentry DSN for error tracking
|
|
||||||
|
|
||||||
#### Vercel (Alternative)
|
|
||||||
- `VERCEL_TOKEN` - Vercel authentication token
|
|
||||||
- `VERCEL_ORG_ID` - Vercel organization ID
|
|
||||||
- `VERCEL_PROJECT_ID` - Vercel project ID
|
|
||||||
- `VITE_BACKEND_API_URL_PROD` - Production API URL
|
|
||||||
- `VITE_SENTRY_DSN` - Sentry DSN
|
|
||||||
|
|
||||||
## Setup Instructions
|
|
||||||
|
|
||||||
### 1. Enable GitHub Actions
|
|
||||||
GitHub Actions is enabled by default for all repositories.
|
|
||||||
|
|
||||||
### 2. Configure Secrets
|
|
||||||
|
|
||||||
Go to your repository:
|
|
||||||
```
|
|
||||||
Settings > Secrets and variables > Actions > New repository secret
|
|
||||||
```
|
|
||||||
|
|
||||||
Add all required secrets listed above.
|
|
||||||
|
|
||||||
### 3. Configure Codecov (Optional)
|
|
||||||
|
|
||||||
1. Sign up at [codecov.io](https://codecov.io)
|
|
||||||
2. Add your repository
|
|
||||||
3. Copy the upload token
|
|
||||||
4. Add as `CODECOV_TOKEN` secret
|
|
||||||
|
|
||||||
### 4. Configure Netlify
|
|
||||||
|
|
||||||
1. Sign up at [netlify.com](https://netlify.com)
|
|
||||||
2. Create a new site
|
|
||||||
3. Get your Site ID from Site settings
|
|
||||||
4. Generate a Personal Access Token
|
|
||||||
5. Add both as secrets
|
|
||||||
|
|
||||||
### 5. Configure Vercel (Alternative)
|
|
||||||
|
|
||||||
1. Sign up at [vercel.com](https://vercel.com)
|
|
||||||
2. Install Vercel CLI: `npm i -g vercel`
|
|
||||||
3. Run `vercel login`
|
|
||||||
4. Run `vercel link` in your project
|
|
||||||
5. Get tokens from Vercel dashboard
|
|
||||||
6. Add as secrets
|
|
||||||
|
|
||||||
## Manual Deployment
|
|
||||||
|
|
||||||
### Trigger via GitHub UI
|
|
||||||
1. Go to Actions tab
|
|
||||||
2. Select "Deploy to Production"
|
|
||||||
3. Click "Run workflow"
|
|
||||||
4. Select branch
|
|
||||||
5. Click "Run workflow"
|
|
||||||
|
|
||||||
### Trigger via CLI
|
|
||||||
```bash
|
|
||||||
gh workflow run deploy.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
|
|
||||||
### View Workflow Runs
|
|
||||||
```
|
|
||||||
Repository > Actions tab
|
|
||||||
```
|
|
||||||
|
|
||||||
### View Logs
|
|
||||||
Click on any workflow run to see detailed logs.
|
|
||||||
|
|
||||||
### Notifications
|
|
||||||
Configure notifications in:
|
|
||||||
```
|
|
||||||
Settings > Notifications > Actions
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Build Fails
|
|
||||||
1. Check logs in Actions tab
|
|
||||||
2. Verify all secrets are set correctly
|
|
||||||
3. Test build locally: `npm run build`
|
|
||||||
|
|
||||||
### Tests Fail
|
|
||||||
1. Run tests locally: `npm run test:run`
|
|
||||||
2. Check for environment-specific issues
|
|
||||||
3. Verify test setup is correct
|
|
||||||
|
|
||||||
### Deployment Fails
|
|
||||||
1. Check deployment logs
|
|
||||||
2. Verify API URL is correct
|
|
||||||
3. Check Netlify/Vercel dashboard for errors
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Always run tests before merging**
|
|
||||||
2. **Use pull requests for code review**
|
|
||||||
3. **Keep secrets secure** - never commit them
|
|
||||||
4. **Monitor build times** - optimize if needed
|
|
||||||
5. **Review security audit results**
|
|
||||||
6. **Keep dependencies updated**
|
|
||||||
|
|
||||||
## Advanced Configuration
|
|
||||||
|
|
||||||
### Branch Protection Rules
|
|
||||||
|
|
||||||
Recommended settings:
|
|
||||||
```
|
|
||||||
Settings > Branches > Add rule
|
|
||||||
|
|
||||||
Branch name pattern: main
|
|
||||||
☑ Require a pull request before merging
|
|
||||||
☑ Require status checks to pass before merging
|
|
||||||
- test
|
|
||||||
- build
|
|
||||||
☑ Require branches to be up to date before merging
|
|
||||||
☑ Do not allow bypassing the above settings
|
|
||||||
```
|
|
||||||
|
|
||||||
### Caching
|
|
||||||
|
|
||||||
The workflows use npm caching to speed up builds:
|
|
||||||
```yaml
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
cache: 'npm'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Matrix Testing
|
|
||||||
|
|
||||||
Tests run on multiple Node.js versions:
|
|
||||||
```yaml
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [18.x, 20.x]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cost Optimization
|
|
||||||
|
|
||||||
GitHub Actions is free for public repositories and includes:
|
|
||||||
- 2,000 minutes/month for private repos (free tier)
|
|
||||||
- Unlimited for public repos
|
|
||||||
|
|
||||||
Tips to reduce usage:
|
|
||||||
1. Use caching
|
|
||||||
2. Run tests only on changed files
|
|
||||||
3. Skip redundant jobs
|
|
||||||
4. Use self-hosted runners for heavy workloads
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [GitHub Actions Documentation](https://docs.github.com/en/actions)
|
|
||||||
- [Netlify Deploy Action](https://github.com/nwtgck/actions-netlify)
|
|
||||||
- [Vercel Deploy Action](https://github.com/amondnet/vercel-action)
|
|
||||||
- [Codecov Action](https://github.com/codecov/codecov-action)
|
|
||||||
|
|
||||||
|
|
@ -1,393 +0,0 @@
|
||||||
# Deployment Options - Industry Standard
|
|
||||||
|
|
||||||
## ✅ Your Project Has All Major Deployment Configurations!
|
|
||||||
|
|
||||||
Your project includes deployment configs for:
|
|
||||||
1. **Vercel** (vercel.json)
|
|
||||||
2. **Netlify** (netlify.toml)
|
|
||||||
3. **Docker** (Dockerfile + nginx.conf)
|
|
||||||
4. **GitHub Actions** (CI/CD workflows)
|
|
||||||
|
|
||||||
This makes your project **deployment-ready** for any platform!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Vercel Deployment ⚡
|
|
||||||
|
|
||||||
**File:** `vercel.json`
|
|
||||||
|
|
||||||
### Features:
|
|
||||||
- ✅ Production build command
|
|
||||||
- ✅ SPA routing (rewrites)
|
|
||||||
- ✅ Security headers
|
|
||||||
- ✅ Asset caching (1 year)
|
|
||||||
- ✅ XSS protection
|
|
||||||
- ✅ Clickjacking protection
|
|
||||||
|
|
||||||
### Deploy:
|
|
||||||
```bash
|
|
||||||
# Install Vercel CLI
|
|
||||||
npm i -g vercel
|
|
||||||
|
|
||||||
# Deploy
|
|
||||||
vercel
|
|
||||||
|
|
||||||
# Deploy to production
|
|
||||||
vercel --prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### Or via GitHub:
|
|
||||||
1. Connect repository to Vercel
|
|
||||||
2. Auto-deploys on push to main
|
|
||||||
3. Preview deployments for PRs
|
|
||||||
|
|
||||||
### Environment Variables:
|
|
||||||
Set in Vercel dashboard:
|
|
||||||
- `VITE_API_URL` - Your production API URL
|
|
||||||
- `VITE_SENTRY_DSN` - Sentry error tracking
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Netlify Deployment 🌐
|
|
||||||
|
|
||||||
**File:** `netlify.toml`
|
|
||||||
|
|
||||||
### Features:
|
|
||||||
- ✅ Production build command
|
|
||||||
- ✅ SPA routing (redirects)
|
|
||||||
- ✅ Security headers
|
|
||||||
- ✅ Asset caching
|
|
||||||
- ✅ Node.js 18 environment
|
|
||||||
|
|
||||||
### Deploy:
|
|
||||||
```bash
|
|
||||||
# Install Netlify CLI
|
|
||||||
npm i -g netlify-cli
|
|
||||||
|
|
||||||
# Deploy
|
|
||||||
netlify deploy
|
|
||||||
|
|
||||||
# Deploy to production
|
|
||||||
netlify deploy --prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### Or via GitHub:
|
|
||||||
1. Connect repository to Netlify
|
|
||||||
2. Auto-deploys on push to main
|
|
||||||
3. Deploy previews for PRs
|
|
||||||
|
|
||||||
### Environment Variables:
|
|
||||||
Set in Netlify dashboard:
|
|
||||||
- `VITE_API_URL`
|
|
||||||
- `VITE_SENTRY_DSN`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Docker Deployment 🐳
|
|
||||||
|
|
||||||
**Files:** `Dockerfile` + `nginx.conf`
|
|
||||||
|
|
||||||
### Features:
|
|
||||||
- ✅ Multi-stage build (optimized)
|
|
||||||
- ✅ Nginx web server
|
|
||||||
- ✅ Gzip compression
|
|
||||||
- ✅ Security headers
|
|
||||||
- ✅ Health checks
|
|
||||||
- ✅ Asset caching
|
|
||||||
- ✅ Production-ready
|
|
||||||
|
|
||||||
### Build & Run:
|
|
||||||
```bash
|
|
||||||
# Build image
|
|
||||||
docker build -t yaltopia-admin .
|
|
||||||
|
|
||||||
# Run container
|
|
||||||
docker run -p 80:80 yaltopia-admin
|
|
||||||
|
|
||||||
# Or with environment variables
|
|
||||||
docker run -p 80:80 \
|
|
||||||
-e VITE_API_URL=https://api.yourdomain.com/api/v1 \
|
|
||||||
yaltopia-admin
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploy to Cloud:
|
|
||||||
- **AWS ECS/Fargate**
|
|
||||||
- **Google Cloud Run**
|
|
||||||
- **Azure Container Instances**
|
|
||||||
- **DigitalOcean App Platform**
|
|
||||||
- **Kubernetes**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. GitHub Actions CI/CD 🚀
|
|
||||||
|
|
||||||
**Files:** `.github/workflows/ci.yml` + `.github/workflows/deploy.yml`
|
|
||||||
|
|
||||||
### Features:
|
|
||||||
- ✅ Automated testing
|
|
||||||
- ✅ Linting & type checking
|
|
||||||
- ✅ Security scanning
|
|
||||||
- ✅ Code coverage
|
|
||||||
- ✅ Automated deployment
|
|
||||||
- ✅ Multi-node testing (18.x, 20.x)
|
|
||||||
|
|
||||||
### Triggers:
|
|
||||||
- Push to main/develop
|
|
||||||
- Pull requests
|
|
||||||
- Manual workflow dispatch
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Headers Comparison
|
|
||||||
|
|
||||||
All deployment configs include these security headers:
|
|
||||||
|
|
||||||
| Header | Purpose | Included |
|
|
||||||
|--------|---------|----------|
|
|
||||||
| X-Frame-Options | Prevent clickjacking | ✅ |
|
|
||||||
| X-Content-Type-Options | Prevent MIME sniffing | ✅ |
|
|
||||||
| X-XSS-Protection | XSS protection | ✅ |
|
|
||||||
| Referrer-Policy | Control referrer info | ✅ |
|
|
||||||
| Cache-Control | Asset caching | ✅ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance Optimizations
|
|
||||||
|
|
||||||
### All Configs Include:
|
|
||||||
1. **Gzip Compression** - Reduce file sizes
|
|
||||||
2. **Asset Caching** - 1 year cache for static files
|
|
||||||
3. **Production Build** - Minified, optimized code
|
|
||||||
4. **Code Splitting** - Vendor chunks separated
|
|
||||||
5. **Tree Shaking** - Remove unused code
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Comparison: Which to Use?
|
|
||||||
|
|
||||||
### Vercel ⚡
|
|
||||||
**Best for:**
|
|
||||||
- Fastest deployment
|
|
||||||
- Automatic HTTPS
|
|
||||||
- Edge network (CDN)
|
|
||||||
- Serverless functions
|
|
||||||
- Preview deployments
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Zero config needed
|
|
||||||
- Excellent DX
|
|
||||||
- Fast global CDN
|
|
||||||
- Free tier generous
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Vendor lock-in
|
|
||||||
- Limited customization
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Netlify 🌐
|
|
||||||
**Best for:**
|
|
||||||
- Static sites
|
|
||||||
- Form handling
|
|
||||||
- Split testing
|
|
||||||
- Identity/Auth
|
|
||||||
- Functions
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Easy to use
|
|
||||||
- Great free tier
|
|
||||||
- Built-in forms
|
|
||||||
- Deploy previews
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Slower than Vercel
|
|
||||||
- Limited compute
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Docker 🐳
|
|
||||||
**Best for:**
|
|
||||||
- Full control
|
|
||||||
- Any cloud provider
|
|
||||||
- Kubernetes
|
|
||||||
- On-premise
|
|
||||||
- Complex setups
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Complete control
|
|
||||||
- Portable
|
|
||||||
- Scalable
|
|
||||||
- No vendor lock-in
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- More complex
|
|
||||||
- Need to manage infra
|
|
||||||
- Requires DevOps knowledge
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Industry Standards Checklist
|
|
||||||
|
|
||||||
Your project has:
|
|
||||||
|
|
||||||
### Deployment ✅
|
|
||||||
- [x] Multiple deployment options
|
|
||||||
- [x] Vercel configuration
|
|
||||||
- [x] Netlify configuration
|
|
||||||
- [x] Docker support
|
|
||||||
- [x] CI/CD pipelines
|
|
||||||
|
|
||||||
### Security ✅
|
|
||||||
- [x] Security headers
|
|
||||||
- [x] XSS protection
|
|
||||||
- [x] Clickjacking protection
|
|
||||||
- [x] MIME sniffing prevention
|
|
||||||
- [x] Referrer policy
|
|
||||||
|
|
||||||
### Performance ✅
|
|
||||||
- [x] Gzip compression
|
|
||||||
- [x] Asset caching
|
|
||||||
- [x] Code splitting
|
|
||||||
- [x] Production builds
|
|
||||||
- [x] Optimized images
|
|
||||||
|
|
||||||
### DevOps ✅
|
|
||||||
- [x] Automated testing
|
|
||||||
- [x] Automated deployment
|
|
||||||
- [x] Environment variables
|
|
||||||
- [x] Health checks (Docker)
|
|
||||||
- [x] Multi-stage builds
|
|
||||||
|
|
||||||
### Documentation ✅
|
|
||||||
- [x] Deployment guides
|
|
||||||
- [x] Environment setup
|
|
||||||
- [x] API documentation
|
|
||||||
- [x] Security checklist
|
|
||||||
- [x] Troubleshooting
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Start Deployment
|
|
||||||
|
|
||||||
### Option 1: Vercel (Fastest)
|
|
||||||
```bash
|
|
||||||
npm i -g vercel
|
|
||||||
vercel login
|
|
||||||
vercel
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 2: Netlify
|
|
||||||
```bash
|
|
||||||
npm i -g netlify-cli
|
|
||||||
netlify login
|
|
||||||
netlify deploy --prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 3: Docker
|
|
||||||
```bash
|
|
||||||
docker build -t yaltopia-admin .
|
|
||||||
docker run -p 80:80 yaltopia-admin
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
All platforms need these:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Required
|
|
||||||
VITE_API_URL=https://api.yourdomain.com/api/v1
|
|
||||||
|
|
||||||
# Optional
|
|
||||||
VITE_SENTRY_DSN=https://your-sentry-dsn
|
|
||||||
VITE_ENV=production
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cost Comparison
|
|
||||||
|
|
||||||
### Vercel
|
|
||||||
- **Free:** Hobby projects
|
|
||||||
- **Pro:** $20/month
|
|
||||||
- **Enterprise:** Custom
|
|
||||||
|
|
||||||
### Netlify
|
|
||||||
- **Free:** Personal projects
|
|
||||||
- **Pro:** $19/month
|
|
||||||
- **Business:** $99/month
|
|
||||||
|
|
||||||
### Docker (AWS)
|
|
||||||
- **ECS Fargate:** ~$15-50/month
|
|
||||||
- **EC2:** ~$10-100/month
|
|
||||||
- **Depends on:** Traffic, resources
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recommendation
|
|
||||||
|
|
||||||
### For This Project:
|
|
||||||
1. **Development:** Local + GitHub Actions
|
|
||||||
2. **Staging:** Vercel/Netlify (free tier)
|
|
||||||
3. **Production:**
|
|
||||||
- Small scale: Vercel/Netlify
|
|
||||||
- Large scale: Docker + AWS/GCP
|
|
||||||
- Enterprise: Kubernetes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What Makes This Industry Standard?
|
|
||||||
|
|
||||||
✅ **Multiple Deployment Options**
|
|
||||||
- Not locked to one platform
|
|
||||||
- Can deploy anywhere
|
|
||||||
|
|
||||||
✅ **Security First**
|
|
||||||
- All security headers configured
|
|
||||||
- XSS, clickjacking protection
|
|
||||||
- HTTPS ready
|
|
||||||
|
|
||||||
✅ **Performance Optimized**
|
|
||||||
- Caching strategies
|
|
||||||
- Compression enabled
|
|
||||||
- CDN ready
|
|
||||||
|
|
||||||
✅ **CI/CD Ready**
|
|
||||||
- Automated testing
|
|
||||||
- Automated deployment
|
|
||||||
- Quality gates
|
|
||||||
|
|
||||||
✅ **Production Ready**
|
|
||||||
- Health checks
|
|
||||||
- Error monitoring
|
|
||||||
- Logging ready
|
|
||||||
|
|
||||||
✅ **Well Documented**
|
|
||||||
- Clear instructions
|
|
||||||
- Multiple options
|
|
||||||
- Troubleshooting guides
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. **Choose Platform:** Vercel, Netlify, or Docker
|
|
||||||
2. **Set Environment Variables**
|
|
||||||
3. **Deploy:** Follow quick start above
|
|
||||||
4. **Configure Domain:** Point to deployment
|
|
||||||
5. **Enable Monitoring:** Sentry, analytics
|
|
||||||
6. **Set Up Alerts:** Error notifications
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
- [Vercel Docs](https://vercel.com/docs)
|
|
||||||
- [Netlify Docs](https://docs.netlify.com)
|
|
||||||
- [Docker Docs](https://docs.docker.com)
|
|
||||||
- [GitHub Actions Docs](https://docs.github.com/en/actions)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Your project is deployment-ready for any platform!** 🚀
|
|
||||||
294
dev-docs/DEVELOPMENT.md
Normal file
294
dev-docs/DEVELOPMENT.md
Normal file
|
|
@ -0,0 +1,294 @@
|
||||||
|
# Development Guide
|
||||||
|
|
||||||
|
Complete guide for developing the Yaltopia Ticket Admin application.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Frontend**: React 18 + TypeScript + Vite
|
||||||
|
- **UI**: TailwindCSS + shadcn/ui
|
||||||
|
- **State**: React Query (TanStack Query)
|
||||||
|
- **Routing**: React Router v6
|
||||||
|
- **HTTP Client**: Axios
|
||||||
|
- **Forms**: React Hook Form + Zod
|
||||||
|
- **Charts**: Recharts
|
||||||
|
- **Notifications**: Sonner
|
||||||
|
- **Error Tracking**: Sentry
|
||||||
|
- **Testing**: Vitest + Testing Library
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Set up environment
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your backend URL
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/ # Reusable UI components
|
||||||
|
├── pages/ # Page components
|
||||||
|
├── services/ # API service layer
|
||||||
|
│ ├── api/
|
||||||
|
│ │ └── client.ts # Axios instance
|
||||||
|
│ ├── auth.service.ts
|
||||||
|
│ ├── user.service.ts
|
||||||
|
│ └── ...
|
||||||
|
├── layouts/ # Layout components
|
||||||
|
├── lib/ # Utilities
|
||||||
|
└── test/ # Test utilities
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Architecture
|
||||||
|
|
||||||
|
### Service Layer Pattern
|
||||||
|
|
||||||
|
All API calls go through typed service classes:
|
||||||
|
|
||||||
|
```
|
||||||
|
Component → Service → API Client → Backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Services
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
authService, // Authentication
|
||||||
|
userService, // User management
|
||||||
|
analyticsService, // Analytics
|
||||||
|
securityService, // Security
|
||||||
|
systemService, // System health
|
||||||
|
announcementService,// Announcements
|
||||||
|
auditService, // Audit logs
|
||||||
|
settingsService // Settings
|
||||||
|
} from '@/services'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage Examples
|
||||||
|
|
||||||
|
**Fetching Data:**
|
||||||
|
```typescript
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { userService } from '@/services'
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ['users'],
|
||||||
|
queryFn: () => userService.getUsers({ page: 1, limit: 20 })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mutations:**
|
||||||
|
```typescript
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { userService } from '@/services'
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (id: string) => userService.deleteUser(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||||
|
toast.success('User deleted')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Direct Calls:**
|
||||||
|
```typescript
|
||||||
|
import { authService } from '@/services'
|
||||||
|
|
||||||
|
const response = await authService.login({ email, password })
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Backend must return tokens on login
|
||||||
|
2. Frontend stores in httpOnly cookies (recommended) or localStorage
|
||||||
|
3. All requests automatically include auth token
|
||||||
|
4. 401 errors trigger automatic token refresh
|
||||||
|
|
||||||
|
### Login Flow
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// User logs in
|
||||||
|
const response = await authService.login({ email, password })
|
||||||
|
|
||||||
|
// Token stored automatically
|
||||||
|
// User redirected to dashboard
|
||||||
|
|
||||||
|
// All subsequent requests include token
|
||||||
|
await userService.getUsers() // Token added automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protected Routes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<Route element={<ProtectedRoute />}>
|
||||||
|
<Route path="/admin/*" element={<AdminLayout />} />
|
||||||
|
</Route>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logout
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await authService.logout() // Clears tokens & cookies
|
||||||
|
navigate('/login')
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Standards
|
||||||
|
|
||||||
|
### Service Methods
|
||||||
|
|
||||||
|
All service methods:
|
||||||
|
- Return typed data (no `response.data` unwrapping needed)
|
||||||
|
- Throw errors with `error.response.data.message`
|
||||||
|
- Use consistent naming (get, create, update, delete)
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
await userService.deleteUser(id)
|
||||||
|
toast.success('User deleted')
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.response?.data?.message || 'Operation failed')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Safety
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// All responses are typed
|
||||||
|
const users: PaginatedResponse<User> = await userService.getUsers()
|
||||||
|
const stats: OverviewStats = await analyticsService.getOverview()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required
|
||||||
|
VITE_BACKEND_API_URL=http://localhost:3001/api/v1
|
||||||
|
|
||||||
|
# Optional (Sentry)
|
||||||
|
VITE_SENTRY_DSN=your-sentry-dsn
|
||||||
|
VITE_SENTRY_ENVIRONMENT=development
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Adding a New Service Method
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/services/user.service.ts
|
||||||
|
async exportUserData(userId: string): Promise<Blob> {
|
||||||
|
const response = await apiClient.get(`/admin/users/${userId}/export`, {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding a New Page
|
||||||
|
|
||||||
|
1. Create page component in `src/pages/`
|
||||||
|
2. Add route in `src/App.tsx`
|
||||||
|
3. Import required services
|
||||||
|
4. Use React Query for data fetching
|
||||||
|
|
||||||
|
### Adding a New Component
|
||||||
|
|
||||||
|
1. Create in `src/components/`
|
||||||
|
2. Use TypeScript for props
|
||||||
|
3. Follow existing patterns
|
||||||
|
4. Add to component exports if reusable
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### React Query
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Good - specific query keys
|
||||||
|
queryKey: ['users', page, limit, search]
|
||||||
|
|
||||||
|
// Bad - too generic
|
||||||
|
queryKey: ['data']
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Layer
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Good - use services
|
||||||
|
import { userService } from '@/services'
|
||||||
|
await userService.getUsers()
|
||||||
|
|
||||||
|
// Bad - direct axios
|
||||||
|
import axios from 'axios'
|
||||||
|
await axios.get('/api/users')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Good - handle errors
|
||||||
|
try {
|
||||||
|
await userService.deleteUser(id)
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.response?.data?.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad - no error handling
|
||||||
|
await userService.deleteUser(id)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Safety
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Good - use types
|
||||||
|
const users: PaginatedResponse<User> = await userService.getUsers()
|
||||||
|
|
||||||
|
// Bad - any type
|
||||||
|
const users: any = await userService.getUsers()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### CORS Errors
|
||||||
|
- Ensure backend has CORS configured
|
||||||
|
- Check `withCredentials: true` in API client
|
||||||
|
- Verify `VITE_BACKEND_API_URL` is correct
|
||||||
|
|
||||||
|
### 401 Errors
|
||||||
|
- Check token is being sent
|
||||||
|
- Verify backend token validation
|
||||||
|
- Check token expiration
|
||||||
|
|
||||||
|
### Build Errors
|
||||||
|
- Run `npm run build` to check TypeScript errors
|
||||||
|
- Fix any type errors
|
||||||
|
- Ensure all imports are correct
|
||||||
|
|
||||||
|
### Test Failures
|
||||||
|
- Run `npm run test` to see failures
|
||||||
|
- Check mock implementations
|
||||||
|
- Verify test data matches types
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [Testing Guide](./TESTING.md)
|
||||||
|
- [Deployment Guide](./DEPLOYMENT.md)
|
||||||
|
- [Security Guide](./SECURITY.md)
|
||||||
|
- [Troubleshooting](./TROUBLESHOOTING.md)
|
||||||
|
|
@ -1,231 +0,0 @@
|
||||||
# Error Monitoring with Sentry
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This project uses **Sentry** for error tracking and performance monitoring.
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
### 1. Create Sentry Account
|
|
||||||
1. Sign up at [sentry.io](https://sentry.io)
|
|
||||||
2. Create a new project
|
|
||||||
3. Select "React" as the platform
|
|
||||||
4. Copy your DSN
|
|
||||||
|
|
||||||
### 2. Configure Environment Variables
|
|
||||||
|
|
||||||
Add to `.env.production`:
|
|
||||||
```env
|
|
||||||
VITE_SENTRY_DSN=https://your-key@sentry.io/your-project-id
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Sentry is Already Integrated
|
|
||||||
|
|
||||||
The following files have Sentry integration:
|
|
||||||
- `src/lib/sentry.ts` - Sentry initialization
|
|
||||||
- `src/main.tsx` - Sentry init on app start
|
|
||||||
- `src/components/ErrorBoundary.tsx` - Error boundary with Sentry
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### 1. Error Tracking
|
|
||||||
All uncaught errors are automatically sent to Sentry:
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
// Your code
|
|
||||||
} catch (error) {
|
|
||||||
Sentry.captureException(error)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Performance Monitoring
|
|
||||||
Tracks page load times and API calls:
|
|
||||||
```typescript
|
|
||||||
tracesSampleRate: 0.1 // 10% of transactions
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Session Replay
|
|
||||||
Records user sessions when errors occur:
|
|
||||||
```typescript
|
|
||||||
replaysOnErrorSampleRate: 1.0 // 100% on errors
|
|
||||||
replaysSessionSampleRate: 0.1 // 10% of normal sessions
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Error Filtering
|
|
||||||
Filters out browser extension errors:
|
|
||||||
```typescript
|
|
||||||
beforeSend(event, hint) {
|
|
||||||
// Filter logic
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Manual Error Logging
|
|
||||||
|
|
||||||
### Capture Exception
|
|
||||||
```typescript
|
|
||||||
import { Sentry } from '@/lib/sentry'
|
|
||||||
|
|
||||||
try {
|
|
||||||
// risky operation
|
|
||||||
} catch (error) {
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: {
|
|
||||||
section: 'user-management',
|
|
||||||
},
|
|
||||||
extra: {
|
|
||||||
userId: user.id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Capture Message
|
|
||||||
```typescript
|
|
||||||
Sentry.captureMessage('Something important happened', 'info')
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add Breadcrumbs
|
|
||||||
```typescript
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
category: 'auth',
|
|
||||||
message: 'User logged in',
|
|
||||||
level: 'info',
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Set User Context
|
|
||||||
```typescript
|
|
||||||
Sentry.setUser({
|
|
||||||
id: user.id,
|
|
||||||
email: user.email,
|
|
||||||
username: user.name,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dashboard Features
|
|
||||||
|
|
||||||
### 1. Issues
|
|
||||||
View all errors with:
|
|
||||||
- Stack traces
|
|
||||||
- User context
|
|
||||||
- Breadcrumbs
|
|
||||||
- Session replays
|
|
||||||
|
|
||||||
### 2. Performance
|
|
||||||
Monitor:
|
|
||||||
- Page load times
|
|
||||||
- API response times
|
|
||||||
- Slow transactions
|
|
||||||
|
|
||||||
### 3. Releases
|
|
||||||
Track errors by release version:
|
|
||||||
```bash
|
|
||||||
# Set release in build
|
|
||||||
VITE_SENTRY_RELEASE=1.0.0 npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Alerts
|
|
||||||
Configure alerts for:
|
|
||||||
- New issues
|
|
||||||
- Spike in errors
|
|
||||||
- Performance degradation
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### 1. Environment Configuration
|
|
||||||
```typescript
|
|
||||||
// Only enable in production
|
|
||||||
if (environment !== 'development') {
|
|
||||||
Sentry.init({ ... })
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Sample Rates
|
|
||||||
```typescript
|
|
||||||
// Production
|
|
||||||
tracesSampleRate: 0.1 // 10%
|
|
||||||
replaysSessionSampleRate: 0.1 // 10%
|
|
||||||
|
|
||||||
// Staging
|
|
||||||
tracesSampleRate: 1.0 // 100%
|
|
||||||
replaysSessionSampleRate: 0.5 // 50%
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. PII Protection
|
|
||||||
```typescript
|
|
||||||
replaysIntegration({
|
|
||||||
maskAllText: true,
|
|
||||||
blockAllMedia: true,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Error Grouping
|
|
||||||
Use fingerprinting for better grouping:
|
|
||||||
```typescript
|
|
||||||
beforeSend(event) {
|
|
||||||
event.fingerprint = ['{{ default }}', event.message]
|
|
||||||
return event
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Errors Not Appearing
|
|
||||||
1. Check DSN is correct
|
|
||||||
2. Verify environment is not 'development'
|
|
||||||
3. Check browser console for Sentry errors
|
|
||||||
4. Verify network requests to Sentry
|
|
||||||
|
|
||||||
### Too Many Events
|
|
||||||
1. Reduce sample rates
|
|
||||||
2. Add more filters in beforeSend
|
|
||||||
3. Set up rate limiting in Sentry dashboard
|
|
||||||
|
|
||||||
### Missing Context
|
|
||||||
1. Add more breadcrumbs
|
|
||||||
2. Set user context after login
|
|
||||||
3. Add custom tags and extra data
|
|
||||||
|
|
||||||
## Cost Management
|
|
||||||
|
|
||||||
Sentry pricing is based on:
|
|
||||||
- Number of events
|
|
||||||
- Number of replays
|
|
||||||
- Data retention
|
|
||||||
|
|
||||||
Tips to reduce costs:
|
|
||||||
1. Lower sample rates in production
|
|
||||||
2. Filter out noisy errors
|
|
||||||
3. Use error grouping effectively
|
|
||||||
4. Set up spike protection
|
|
||||||
|
|
||||||
## Integration with CI/CD
|
|
||||||
|
|
||||||
### Upload Source Maps
|
|
||||||
```yaml
|
|
||||||
# In .github/workflows/deploy.yml
|
|
||||||
- name: Upload source maps to Sentry
|
|
||||||
run: |
|
|
||||||
npm install -g @sentry/cli
|
|
||||||
sentry-cli releases new ${{ github.sha }}
|
|
||||||
sentry-cli releases files ${{ github.sha }} upload-sourcemaps ./dist
|
|
||||||
sentry-cli releases finalize ${{ github.sha }}
|
|
||||||
env:
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
SENTRY_ORG: your-org
|
|
||||||
SENTRY_PROJECT: your-project
|
|
||||||
```
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [Sentry React Documentation](https://docs.sentry.io/platforms/javascript/guides/react/)
|
|
||||||
- [Sentry Performance Monitoring](https://docs.sentry.io/product/performance/)
|
|
||||||
- [Sentry Session Replay](https://docs.sentry.io/product/session-replay/)
|
|
||||||
- [Sentry Best Practices](https://docs.sentry.io/product/best-practices/)
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For issues with Sentry integration:
|
|
||||||
1. Check Sentry documentation
|
|
||||||
2. Review browser console
|
|
||||||
3. Check Sentry dashboard
|
|
||||||
4. Contact Sentry support
|
|
||||||
|
|
@ -1,392 +0,0 @@
|
||||||
# Error Tracking with Fallback System
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This project uses a **dual error tracking system**:
|
|
||||||
1. **Primary**: Sentry (cloud-based)
|
|
||||||
2. **Fallback**: Custom backend logging (if Sentry fails)
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
Error Occurs
|
|
||||||
↓
|
|
||||||
Try Sentry First
|
|
||||||
↓
|
|
||||||
If Sentry Fails → Queue for Backend
|
|
||||||
↓
|
|
||||||
Send to Backend API: POST /api/v1/errors/log
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Basic Error Tracking
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { errorTracker } from '@/lib/error-tracker'
|
|
||||||
|
|
||||||
try {
|
|
||||||
await riskyOperation()
|
|
||||||
} catch (error) {
|
|
||||||
errorTracker.trackError(error, {
|
|
||||||
tags: { section: 'payment' },
|
|
||||||
extra: { orderId: '123' },
|
|
||||||
userId: user.id
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Track Messages
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
errorTracker.trackMessage('Payment processed successfully', 'info', {
|
|
||||||
amount: 100,
|
|
||||||
currency: 'USD'
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Set User Context
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// After login
|
|
||||||
errorTracker.setUser({
|
|
||||||
id: user.id,
|
|
||||||
email: user.email,
|
|
||||||
name: user.name
|
|
||||||
})
|
|
||||||
|
|
||||||
// On logout
|
|
||||||
errorTracker.clearUser()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add Breadcrumbs
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
errorTracker.addBreadcrumb('navigation', 'User clicked checkout button', 'info')
|
|
||||||
```
|
|
||||||
|
|
||||||
## Backend API Required
|
|
||||||
|
|
||||||
Your backend needs to implement this endpoint:
|
|
||||||
|
|
||||||
### POST /api/v1/errors/log
|
|
||||||
|
|
||||||
**Request Body:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Error message",
|
|
||||||
"stack": "Error stack trace",
|
|
||||||
"url": "https://app.example.com/dashboard",
|
|
||||||
"userAgent": "Mozilla/5.0...",
|
|
||||||
"timestamp": "2024-02-24T10:30:00.000Z",
|
|
||||||
"userId": "user-123",
|
|
||||||
"extra": {
|
|
||||||
"section": "payment",
|
|
||||||
"orderId": "123"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"logId": "log-456"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backend Implementation Example (Node.js/Express)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// routes/errors.js
|
|
||||||
router.post('/errors/log', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { message, stack, url, userAgent, timestamp, userId, extra } = req.body
|
|
||||||
|
|
||||||
// Save to database
|
|
||||||
await ErrorLog.create({
|
|
||||||
message,
|
|
||||||
stack,
|
|
||||||
url,
|
|
||||||
userAgent,
|
|
||||||
timestamp: new Date(timestamp),
|
|
||||||
userId,
|
|
||||||
extra: JSON.stringify(extra)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Optional: Send alert for critical errors
|
|
||||||
if (message.includes('payment') || message.includes('auth')) {
|
|
||||||
await sendSlackAlert(message, stack)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ success: true })
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to log error:', error)
|
|
||||||
res.status(500).json({ success: false })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Schema
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE error_logs (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
stack TEXT,
|
|
||||||
url TEXT NOT NULL,
|
|
||||||
user_agent TEXT,
|
|
||||||
timestamp TIMESTAMP NOT NULL,
|
|
||||||
user_id UUID,
|
|
||||||
extra JSONB,
|
|
||||||
created_at TIMESTAMP DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_error_logs_timestamp ON error_logs(timestamp DESC);
|
|
||||||
CREATE INDEX idx_error_logs_user_id ON error_logs(user_id);
|
|
||||||
```
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
### 1. Automatic Global Error Handling
|
|
||||||
|
|
||||||
All uncaught errors are automatically tracked:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Automatically catches all errors
|
|
||||||
window.addEventListener('error', (event) => {
|
|
||||||
errorTracker.trackError(new Error(event.message))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Catches unhandled promise rejections
|
|
||||||
window.addEventListener('unhandledrejection', (event) => {
|
|
||||||
errorTracker.trackError(event.reason)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Queue System
|
|
||||||
|
|
||||||
Errors are queued and sent to backend:
|
|
||||||
- Max queue size: 50 errors
|
|
||||||
- Automatic retry on failure
|
|
||||||
- Prevents memory leaks
|
|
||||||
|
|
||||||
### 3. Dual Tracking
|
|
||||||
|
|
||||||
Every error is sent to:
|
|
||||||
1. **Sentry** (if available) - Rich debugging features
|
|
||||||
2. **Backend** (always) - Your own database for compliance/analysis
|
|
||||||
|
|
||||||
## Sentry Alternatives
|
|
||||||
|
|
||||||
If you want to replace Sentry entirely:
|
|
||||||
|
|
||||||
### 1. LogRocket
|
|
||||||
```bash
|
|
||||||
npm install logrocket
|
|
||||||
```
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import LogRocket from 'logrocket'
|
|
||||||
|
|
||||||
LogRocket.init('your-app-id')
|
|
||||||
|
|
||||||
// Track errors
|
|
||||||
LogRocket.captureException(error)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Rollbar
|
|
||||||
```bash
|
|
||||||
npm install rollbar
|
|
||||||
```
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import Rollbar from 'rollbar'
|
|
||||||
|
|
||||||
const rollbar = new Rollbar({
|
|
||||||
accessToken: 'your-token',
|
|
||||||
environment: 'production'
|
|
||||||
})
|
|
||||||
|
|
||||||
rollbar.error(error)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Bugsnag
|
|
||||||
```bash
|
|
||||||
npm install @bugsnag/js @bugsnag/plugin-react
|
|
||||||
```
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import Bugsnag from '@bugsnag/js'
|
|
||||||
import BugsnagPluginReact from '@bugsnag/plugin-react'
|
|
||||||
|
|
||||||
Bugsnag.start({
|
|
||||||
apiKey: 'your-api-key',
|
|
||||||
plugins: [new BugsnagPluginReact()]
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Self-Hosted GlitchTip
|
|
||||||
```bash
|
|
||||||
# Docker Compose
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Free, open-source, Sentry-compatible API.
|
|
||||||
|
|
||||||
## Benefits of Fallback System
|
|
||||||
|
|
||||||
### 1. Reliability
|
|
||||||
- Never lose error data if Sentry is down
|
|
||||||
- Backend always receives errors
|
|
||||||
|
|
||||||
### 2. Compliance
|
|
||||||
- Keep error logs in your own database
|
|
||||||
- Meet data residency requirements
|
|
||||||
- Full control over sensitive data
|
|
||||||
|
|
||||||
### 3. Cost Control
|
|
||||||
- Reduce Sentry event count
|
|
||||||
- Use backend for high-volume errors
|
|
||||||
- Keep Sentry for detailed debugging
|
|
||||||
|
|
||||||
### 4. Custom Analysis
|
|
||||||
- Query errors with SQL
|
|
||||||
- Build custom dashboards
|
|
||||||
- Integrate with your alerting system
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Enable/Disable Fallback
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/lib/error-tracker.ts
|
|
||||||
|
|
||||||
// Disable backend logging (Sentry only)
|
|
||||||
const ENABLE_BACKEND_LOGGING = false
|
|
||||||
|
|
||||||
private queueError(errorLog: ErrorLog) {
|
|
||||||
if (!ENABLE_BACKEND_LOGGING) return
|
|
||||||
// ... rest of code
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Adjust Queue Size
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
private maxQueueSize = 100 // Increase for high-traffic apps
|
|
||||||
```
|
|
||||||
|
|
||||||
### Change Backend Endpoint
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
await apiClient.post('/custom/error-endpoint', errorLog)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring Dashboard
|
|
||||||
|
|
||||||
Build a simple error dashboard:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Backend endpoint
|
|
||||||
router.get('/errors/stats', async (req, res) => {
|
|
||||||
const stats = await db.query(`
|
|
||||||
SELECT
|
|
||||||
DATE(timestamp) as date,
|
|
||||||
COUNT(*) as count,
|
|
||||||
COUNT(DISTINCT user_id) as affected_users
|
|
||||||
FROM error_logs
|
|
||||||
WHERE timestamp > NOW() - INTERVAL '7 days'
|
|
||||||
GROUP BY DATE(timestamp)
|
|
||||||
ORDER BY date DESC
|
|
||||||
`)
|
|
||||||
|
|
||||||
res.json(stats)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### 1. Don't Log Everything
|
|
||||||
```typescript
|
|
||||||
// Bad: Logging expected errors
|
|
||||||
if (!user) {
|
|
||||||
errorTracker.trackError(new Error('User not found'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Good: Only log unexpected errors
|
|
||||||
try {
|
|
||||||
await criticalOperation()
|
|
||||||
} catch (error) {
|
|
||||||
errorTracker.trackError(error)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Add Context
|
|
||||||
```typescript
|
|
||||||
errorTracker.trackError(error, {
|
|
||||||
extra: {
|
|
||||||
action: 'checkout',
|
|
||||||
step: 'payment',
|
|
||||||
amount: 100
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Set User Context Early
|
|
||||||
```typescript
|
|
||||||
// In your auth flow
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
errorTracker.setUser({
|
|
||||||
id: user.id,
|
|
||||||
email: user.email,
|
|
||||||
name: user.name
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [user])
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Clean Up on Logout
|
|
||||||
```typescript
|
|
||||||
const handleLogout = () => {
|
|
||||||
errorTracker.clearUser()
|
|
||||||
// ... rest of logout
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Errors Not Reaching Backend
|
|
||||||
|
|
||||||
1. Check network tab for failed requests
|
|
||||||
2. Verify backend endpoint exists
|
|
||||||
3. Check CORS configuration
|
|
||||||
4. Review backend logs
|
|
||||||
|
|
||||||
### Queue Growing Too Large
|
|
||||||
|
|
||||||
1. Increase `maxQueueSize`
|
|
||||||
2. Check backend availability
|
|
||||||
3. Add retry logic with exponential backoff
|
|
||||||
|
|
||||||
### Duplicate Errors
|
|
||||||
|
|
||||||
This is intentional - errors go to both Sentry and backend. To disable:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Only send to backend if Sentry fails
|
|
||||||
try {
|
|
||||||
Sentry.captureException(error)
|
|
||||||
} catch (sentryError) {
|
|
||||||
this.queueError(errorLog) // Only fallback
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [Sentry Documentation](https://docs.sentry.io/)
|
|
||||||
- [LogRocket Documentation](https://docs.logrocket.com/)
|
|
||||||
- [Rollbar Documentation](https://docs.rollbar.com/)
|
|
||||||
- [GlitchTip (Self-hosted)](https://glitchtip.com/)
|
|
||||||
|
|
||||||
|
|
@ -1,357 +0,0 @@
|
||||||
# Login API Documentation
|
|
||||||
|
|
||||||
## Endpoint
|
|
||||||
```
|
|
||||||
POST /api/v1/auth/login
|
|
||||||
```
|
|
||||||
|
|
||||||
## Description
|
|
||||||
Login user with email or phone number. This endpoint authenticates users using either email address or phone number along with password.
|
|
||||||
|
|
||||||
## Current Implementation
|
|
||||||
|
|
||||||
### Frontend Code
|
|
||||||
|
|
||||||
**API Client** (`src/lib/api-client.ts`):
|
|
||||||
```typescript
|
|
||||||
export const adminApiHelpers = {
|
|
||||||
// Auth - uses publicApi (no token required)
|
|
||||||
login: (data: { email: string; password: string }) =>
|
|
||||||
publicApi.post('/auth/login', data),
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Login Page** (`src/pages/login/index.tsx`):
|
|
||||||
```typescript
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await adminApiHelpers.login({ email, password })
|
|
||||||
const { access_token, user } = response.data
|
|
||||||
|
|
||||||
// Check if user is admin
|
|
||||||
if (user.role !== 'ADMIN') {
|
|
||||||
toast.error("Access denied. Admin privileges required.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store credentials
|
|
||||||
if (access_token) {
|
|
||||||
localStorage.setItem('access_token', access_token)
|
|
||||||
}
|
|
||||||
localStorage.setItem('user', JSON.stringify(user))
|
|
||||||
|
|
||||||
toast.success("Login successful!")
|
|
||||||
navigate(from, { replace: true })
|
|
||||||
} catch (error: any) {
|
|
||||||
const message = error.response?.data?.message || "Invalid email or password"
|
|
||||||
toast.error(message)
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Request
|
|
||||||
|
|
||||||
### Headers
|
|
||||||
```
|
|
||||||
Content-Type: application/json
|
|
||||||
```
|
|
||||||
|
|
||||||
### Body (JSON)
|
|
||||||
|
|
||||||
**Option 1: Email + Password**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"password": "your-password"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 2: Phone + Password** (if backend supports)
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"phone": "+1234567890",
|
|
||||||
"password": "your-password"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Request
|
|
||||||
```bash
|
|
||||||
curl -X POST https://api.yourdomain.com/api/v1/auth/login \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"password": "password123"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Response
|
|
||||||
|
|
||||||
### Success Response (200 OK)
|
|
||||||
|
|
||||||
**Option 1: With Access Token in Body** (localStorage fallback)
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
||||||
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
||||||
"user": {
|
|
||||||
"id": "user-id-123",
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"firstName": "John",
|
|
||||||
"lastName": "Doe",
|
|
||||||
"role": "ADMIN",
|
|
||||||
"isActive": true,
|
|
||||||
"createdAt": "2024-01-01T00:00:00.000Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 2: With httpOnly Cookies** (recommended)
|
|
||||||
```http
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Set-Cookie: access_token=<jwt>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900
|
|
||||||
Set-Cookie: refresh_token=<jwt>; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=604800
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"user": {
|
|
||||||
"id": "user-id-123",
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"firstName": "John",
|
|
||||||
"lastName": "Doe",
|
|
||||||
"role": "ADMIN",
|
|
||||||
"isActive": true,
|
|
||||||
"createdAt": "2024-01-01T00:00:00.000Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Responses
|
|
||||||
|
|
||||||
**401 Unauthorized** - Invalid credentials
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Invalid email or password",
|
|
||||||
"statusCode": 401
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**403 Forbidden** - Account inactive or not admin
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Account is inactive",
|
|
||||||
"statusCode": 403
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**400 Bad Request** - Validation error
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Validation failed",
|
|
||||||
"errors": [
|
|
||||||
{
|
|
||||||
"field": "email",
|
|
||||||
"message": "Invalid email format"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"statusCode": 400
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**429 Too Many Requests** - Rate limit exceeded
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Too many login attempts. Please try again later.",
|
|
||||||
"statusCode": 429,
|
|
||||||
"retryAfter": 900
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**500 Internal Server Error** - Server error
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Internal server error",
|
|
||||||
"statusCode": 500
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Frontend Behavior
|
|
||||||
|
|
||||||
### 1. Form Validation
|
|
||||||
- Email: Required, valid email format
|
|
||||||
- Password: Required, minimum 8 characters
|
|
||||||
- Show/hide password toggle
|
|
||||||
|
|
||||||
### 2. Loading State
|
|
||||||
- Disable form during submission
|
|
||||||
- Show "Logging in..." button text
|
|
||||||
- Prevent multiple submissions
|
|
||||||
|
|
||||||
### 3. Success Flow
|
|
||||||
1. Validate response contains user data
|
|
||||||
2. Check if user.role === 'ADMIN'
|
|
||||||
3. Store access_token (if provided)
|
|
||||||
4. Store user data in localStorage
|
|
||||||
5. Show success toast
|
|
||||||
6. Redirect to dashboard or original destination
|
|
||||||
|
|
||||||
### 4. Error Handling
|
|
||||||
- Display user-friendly error messages
|
|
||||||
- Show toast notification
|
|
||||||
- Keep form enabled for retry
|
|
||||||
- Don't expose sensitive error details
|
|
||||||
|
|
||||||
### 5. Security Features
|
|
||||||
- HTTPS only in production
|
|
||||||
- httpOnly cookies support
|
|
||||||
- CSRF protection (SameSite cookies)
|
|
||||||
- Automatic token refresh
|
|
||||||
- Role-based access control
|
|
||||||
|
|
||||||
## Backend Requirements
|
|
||||||
|
|
||||||
### Must Implement
|
|
||||||
1. **Password Hashing**: bcrypt with salt rounds >= 12
|
|
||||||
2. **Rate Limiting**: 5 attempts per 15 minutes per IP
|
|
||||||
3. **Account Lockout**: Lock after 5 failed attempts
|
|
||||||
4. **Role Verification**: Ensure user.role === 'ADMIN'
|
|
||||||
5. **Active Status Check**: Verify user.isActive === true
|
|
||||||
6. **Token Generation**: JWT with proper expiration
|
|
||||||
7. **Audit Logging**: Log all login attempts
|
|
||||||
|
|
||||||
### Recommended
|
|
||||||
1. **httpOnly Cookies**: Store tokens in cookies, not response body
|
|
||||||
2. **Refresh Tokens**: Long-lived tokens for session renewal
|
|
||||||
3. **2FA Support**: Two-factor authentication
|
|
||||||
4. **IP Whitelisting**: Restrict admin access by IP
|
|
||||||
5. **Session Management**: Track active sessions
|
|
||||||
6. **Email Notifications**: Alert on new login
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Manual Testing
|
|
||||||
```bash
|
|
||||||
# Test with valid credentials
|
|
||||||
curl -X POST http://localhost:3000/api/v1/auth/login \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"email":"admin@example.com","password":"password123"}'
|
|
||||||
|
|
||||||
# Test with invalid credentials
|
|
||||||
curl -X POST http://localhost:3000/api/v1/auth/login \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"email":"admin@example.com","password":"wrong"}'
|
|
||||||
|
|
||||||
# Test with non-admin user
|
|
||||||
curl -X POST http://localhost:3000/api/v1/auth/login \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"email":"user@example.com","password":"password123"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Automated Testing
|
|
||||||
See `src/pages/login/__tests__/index.test.tsx` for component tests.
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
### Development (`.env`)
|
|
||||||
```env
|
|
||||||
VITE_BACKEND_API_URL=http://localhost:3000/api/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production (`.env.production`)
|
|
||||||
```env
|
|
||||||
VITE_BACKEND_API_URL=https://api.yourdomain.com/api/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Client Configuration
|
|
||||||
|
|
||||||
The login endpoint uses the `publicApi` instance which:
|
|
||||||
- Does NOT require authentication
|
|
||||||
- Does NOT send Authorization header
|
|
||||||
- DOES send cookies (`withCredentials: true`)
|
|
||||||
- DOES handle CORS properly
|
|
||||||
|
|
||||||
## Flow Diagram
|
|
||||||
|
|
||||||
```
|
|
||||||
User enters credentials
|
|
||||||
↓
|
|
||||||
Form validation
|
|
||||||
↓
|
|
||||||
POST /api/v1/auth/login
|
|
||||||
↓
|
|
||||||
Backend validates credentials
|
|
||||||
↓
|
|
||||||
Backend checks role === 'ADMIN'
|
|
||||||
↓
|
|
||||||
Backend generates tokens
|
|
||||||
↓
|
|
||||||
Backend returns user + tokens
|
|
||||||
↓
|
|
||||||
Frontend checks role === 'ADMIN'
|
|
||||||
↓
|
|
||||||
Frontend stores tokens
|
|
||||||
↓
|
|
||||||
Frontend redirects to dashboard
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Checklist
|
|
||||||
|
|
||||||
Backend:
|
|
||||||
- [ ] Passwords hashed with bcrypt
|
|
||||||
- [ ] Rate limiting enabled
|
|
||||||
- [ ] Account lockout implemented
|
|
||||||
- [ ] HTTPS enforced
|
|
||||||
- [ ] CORS configured properly
|
|
||||||
- [ ] httpOnly cookies used
|
|
||||||
- [ ] Audit logging enabled
|
|
||||||
- [ ] Input validation
|
|
||||||
- [ ] SQL injection prevention
|
|
||||||
|
|
||||||
Frontend:
|
|
||||||
- [x] HTTPS only in production
|
|
||||||
- [x] Cookie support enabled
|
|
||||||
- [x] Role verification
|
|
||||||
- [x] Error handling
|
|
||||||
- [x] Loading states
|
|
||||||
- [x] Form validation
|
|
||||||
- [x] Token storage
|
|
||||||
- [x] Automatic token refresh
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Issue: "Network Error"
|
|
||||||
**Solution:** Check API URL in environment variables
|
|
||||||
|
|
||||||
### Issue: "CORS Error"
|
|
||||||
**Solution:** Backend must allow credentials and specific origin
|
|
||||||
|
|
||||||
### Issue: "Invalid credentials" for valid user
|
|
||||||
**Solution:** Check backend password hashing and comparison
|
|
||||||
|
|
||||||
### Issue: "Access denied" for admin user
|
|
||||||
**Solution:** Verify user.role === 'ADMIN' in database
|
|
||||||
|
|
||||||
### Issue: Token not persisting
|
|
||||||
**Solution:** Check if backend is setting httpOnly cookies or returning access_token
|
|
||||||
|
|
||||||
## Related Documentation
|
|
||||||
|
|
||||||
- [Authentication Setup](./AUTHENTICATION.md)
|
|
||||||
- [API Standards](./API_STANDARDS.md)
|
|
||||||
- [Security Checklist](./SECURITY_CHECKLIST.md)
|
|
||||||
- [Backend Requirements](./SECURITY_CHECKLIST.md#backend-security)
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For issues with login:
|
|
||||||
1. Check browser console for errors
|
|
||||||
2. Check network tab for API response
|
|
||||||
3. Verify environment variables
|
|
||||||
4. Check backend logs
|
|
||||||
5. Test with curl/Postman
|
|
||||||
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
# Pre-Deployment Checklist
|
|
||||||
|
|
||||||
Use this checklist before deploying to production.
|
|
||||||
|
|
||||||
## ✅ Code Quality
|
|
||||||
|
|
||||||
- [x] All TypeScript errors resolved
|
|
||||||
- [x] Build completes successfully (`npm run build`)
|
|
||||||
- [x] Type checking passes (`npm run type-check`)
|
|
||||||
- [ ] ESLint warnings addressed (`npm run lint`)
|
|
||||||
- [ ] No console.log statements in production code
|
|
||||||
- [ ] All TODO comments resolved or documented
|
|
||||||
|
|
||||||
## ✅ Environment Setup
|
|
||||||
|
|
||||||
- [ ] `.env.production` file created
|
|
||||||
- [ ] `VITE_API_URL` set to production API endpoint
|
|
||||||
- [ ] Backend API is accessible from production domain
|
|
||||||
- [ ] CORS configured on backend for production domain
|
|
||||||
- [ ] All required environment variables documented
|
|
||||||
|
|
||||||
## ✅ Security
|
|
||||||
|
|
||||||
- [ ] HTTPS/SSL certificate obtained and configured
|
|
||||||
- [ ] Security headers configured (see nginx.conf or hosting config)
|
|
||||||
- [ ] API endpoints secured with authentication
|
|
||||||
- [ ] Sensitive data not exposed in client code
|
|
||||||
- [ ] Rate limiting configured on backend
|
|
||||||
- [ ] Error messages don't expose sensitive information
|
|
||||||
- [ ] Dependencies audited (`npm audit`)
|
|
||||||
|
|
||||||
## ✅ Testing
|
|
||||||
|
|
||||||
- [ ] Application tested in development mode
|
|
||||||
- [ ] Production build tested locally (`npm run preview`)
|
|
||||||
- [ ] Login/logout flow tested
|
|
||||||
- [ ] All main routes tested
|
|
||||||
- [ ] API calls tested and working
|
|
||||||
- [ ] Error handling tested (network errors, 401, 403, 404, 500)
|
|
||||||
- [ ] Mobile responsiveness verified
|
|
||||||
- [ ] Cross-browser testing completed:
|
|
||||||
- [ ] Chrome
|
|
||||||
- [ ] Firefox
|
|
||||||
- [ ] Safari
|
|
||||||
- [ ] Edge
|
|
||||||
|
|
||||||
## ✅ Performance
|
|
||||||
|
|
||||||
- [ ] Bundle size reviewed (should be ~970 KB uncompressed)
|
|
||||||
- [ ] Lighthouse performance score checked (aim for >80)
|
|
||||||
- [ ] Images optimized (if any)
|
|
||||||
- [ ] Code splitting configured (already done in vite.config.ts)
|
|
||||||
- [ ] Compression enabled on server (gzip/brotli)
|
|
||||||
|
|
||||||
## ✅ Monitoring & Analytics
|
|
||||||
|
|
||||||
- [ ] Error tracking service configured (Sentry, LogRocket, etc.)
|
|
||||||
- [ ] Analytics configured (Google Analytics, Plausible, etc.)
|
|
||||||
- [ ] Uptime monitoring set up
|
|
||||||
- [ ] Alert notifications configured
|
|
||||||
- [ ] Logging strategy defined
|
|
||||||
|
|
||||||
## ✅ Documentation
|
|
||||||
|
|
||||||
- [x] README.md updated with project info
|
|
||||||
- [x] Environment variables documented
|
|
||||||
- [x] Deployment instructions clear
|
|
||||||
- [ ] API documentation available
|
|
||||||
- [ ] Team trained on deployment process
|
|
||||||
|
|
||||||
## ✅ Deployment Configuration
|
|
||||||
|
|
||||||
Choose your deployment method and complete the relevant section:
|
|
||||||
|
|
||||||
### For Vercel
|
|
||||||
- [ ] Vercel account created
|
|
||||||
- [ ] Project connected to repository
|
|
||||||
- [ ] Environment variables set in Vercel dashboard
|
|
||||||
- [ ] Custom domain configured (if applicable)
|
|
||||||
- [ ] Build command: `npm run build:prod`
|
|
||||||
- [ ] Output directory: `dist`
|
|
||||||
|
|
||||||
### For Netlify
|
|
||||||
- [ ] Netlify account created
|
|
||||||
- [ ] Project connected to repository
|
|
||||||
- [ ] Environment variables set in Netlify dashboard
|
|
||||||
- [ ] Custom domain configured (if applicable)
|
|
||||||
- [ ] Build command: `npm run build:prod`
|
|
||||||
- [ ] Publish directory: `dist`
|
|
||||||
|
|
||||||
### For Docker
|
|
||||||
- [ ] Docker image built successfully
|
|
||||||
- [ ] Container tested locally
|
|
||||||
- [ ] Image pushed to container registry
|
|
||||||
- [ ] Deployment platform configured (ECS, Cloud Run, etc.)
|
|
||||||
- [ ] Environment variables configured in platform
|
|
||||||
- [ ] Health checks configured
|
|
||||||
|
|
||||||
### For VPS/Traditional Server
|
|
||||||
- [ ] Server provisioned and accessible
|
|
||||||
- [ ] Node.js 18+ installed
|
|
||||||
- [ ] Nginx installed and configured
|
|
||||||
- [ ] SSL certificate installed
|
|
||||||
- [ ] Firewall configured
|
|
||||||
- [ ] Automatic deployment script created
|
|
||||||
|
|
||||||
## ✅ Post-Deployment
|
|
||||||
|
|
||||||
After deploying, verify:
|
|
||||||
|
|
||||||
- [ ] Application loads at production URL
|
|
||||||
- [ ] HTTPS working (no mixed content warnings)
|
|
||||||
- [ ] All routes accessible (test deep links)
|
|
||||||
- [ ] Login/authentication working
|
|
||||||
- [ ] API calls successful
|
|
||||||
- [ ] No console errors
|
|
||||||
- [ ] Error tracking receiving data
|
|
||||||
- [ ] Analytics tracking pageviews
|
|
||||||
- [ ] Performance acceptable (run Lighthouse)
|
|
||||||
|
|
||||||
## ✅ Backup & Recovery
|
|
||||||
|
|
||||||
- [ ] Previous version tagged in git
|
|
||||||
- [ ] Rollback procedure documented
|
|
||||||
- [ ] Database backup completed (if applicable)
|
|
||||||
- [ ] Configuration backed up
|
|
||||||
|
|
||||||
## ✅ Communication
|
|
||||||
|
|
||||||
- [ ] Stakeholders notified of deployment
|
|
||||||
- [ ] Maintenance window communicated (if applicable)
|
|
||||||
- [ ] Support team briefed
|
|
||||||
- [ ] Documentation shared with team
|
|
||||||
|
|
||||||
## 🚨 Emergency Contacts
|
|
||||||
|
|
||||||
Document your emergency contacts:
|
|
||||||
|
|
||||||
- **Backend Team:** _________________
|
|
||||||
- **DevOps/Infrastructure:** _________________
|
|
||||||
- **Security Team:** _________________
|
|
||||||
- **On-Call Engineer:** _________________
|
|
||||||
|
|
||||||
## 📋 Deployment Steps
|
|
||||||
|
|
||||||
1. **Pre-deployment**
|
|
||||||
- [ ] Complete this checklist
|
|
||||||
- [ ] Create git tag: `git tag v1.0.0`
|
|
||||||
- [ ] Push tag: `git push origin v1.0.0`
|
|
||||||
|
|
||||||
2. **Deployment**
|
|
||||||
- [ ] Deploy to staging first (if available)
|
|
||||||
- [ ] Test on staging
|
|
||||||
- [ ] Deploy to production
|
|
||||||
- [ ] Monitor for 15-30 minutes
|
|
||||||
|
|
||||||
3. **Post-deployment**
|
|
||||||
- [ ] Verify application working
|
|
||||||
- [ ] Check error logs
|
|
||||||
- [ ] Monitor performance
|
|
||||||
- [ ] Notify stakeholders
|
|
||||||
|
|
||||||
4. **If issues occur**
|
|
||||||
- [ ] Check error tracking service
|
|
||||||
- [ ] Review server logs
|
|
||||||
- [ ] Rollback if necessary
|
|
||||||
- [ ] Document issue for post-mortem
|
|
||||||
|
|
||||||
## 📝 Deployment Log
|
|
||||||
|
|
||||||
Keep a record of deployments:
|
|
||||||
|
|
||||||
| Date | Version | Deployed By | Status | Notes |
|
|
||||||
|------|---------|-------------|--------|-------|
|
|
||||||
| YYYY-MM-DD | v1.0.0 | Name | ✅/❌ | Initial production release |
|
|
||||||
|
|
||||||
## 🎯 Success Criteria
|
|
||||||
|
|
||||||
Deployment is successful when:
|
|
||||||
|
|
||||||
- ✅ Application loads without errors
|
|
||||||
- ✅ All critical features working
|
|
||||||
- ✅ No increase in error rate
|
|
||||||
- ✅ Performance within acceptable range
|
|
||||||
- ✅ No security vulnerabilities detected
|
|
||||||
- ✅ Monitoring and alerts active
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
If you encounter issues:
|
|
||||||
|
|
||||||
1. Check `DEPLOYMENT.md` troubleshooting section
|
|
||||||
2. Review error logs in monitoring service
|
|
||||||
3. Check browser console for client-side errors
|
|
||||||
4. Verify API connectivity
|
|
||||||
5. Contact backend team if API issues
|
|
||||||
6. Rollback if critical issues persist
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Remember:** It's better to delay deployment than to deploy with known issues. Take your time and verify each step.
|
|
||||||
|
|
||||||
**Good luck with your deployment! 🚀**
|
|
||||||
|
|
@ -1,233 +0,0 @@
|
||||||
# Production Ready Summary
|
|
||||||
|
|
||||||
## ✅ Issues Fixed
|
|
||||||
|
|
||||||
### 1. Build Errors (27 TypeScript errors) - FIXED
|
|
||||||
- Removed all unused imports across the codebase
|
|
||||||
- Fixed type safety issues in api-client.ts
|
|
||||||
- Added proper type annotations for error responses
|
|
||||||
- Fixed undefined variable references
|
|
||||||
- All files now compile successfully
|
|
||||||
|
|
||||||
### 2. Environment Configuration - COMPLETED
|
|
||||||
- ✅ Created `.env.example` with all required variables
|
|
||||||
- ✅ Created `.env.production.example` for production setup
|
|
||||||
- ✅ Updated `.gitignore` to exclude environment files
|
|
||||||
- ✅ Documented all environment variables in README
|
|
||||||
|
|
||||||
### 3. Documentation - COMPLETED
|
|
||||||
- ✅ Comprehensive README.md with:
|
|
||||||
- Project overview and features
|
|
||||||
- Installation instructions
|
|
||||||
- Development and production build steps
|
|
||||||
- Deployment guides for multiple platforms
|
|
||||||
- Environment variable documentation
|
|
||||||
- ✅ DEPLOYMENT.md with detailed deployment checklist
|
|
||||||
- ✅ SECURITY.md with security best practices
|
|
||||||
- ✅ This summary document
|
|
||||||
|
|
||||||
### 4. Production Optimizations - COMPLETED
|
|
||||||
- ✅ Error boundary component for graceful error handling
|
|
||||||
- ✅ Code splitting configuration in vite.config.ts
|
|
||||||
- ✅ Manual chunks for better caching (react, ui, charts, query)
|
|
||||||
- ✅ Build optimization settings
|
|
||||||
- ✅ Version updated to 1.0.0
|
|
||||||
|
|
||||||
### 5. Deployment Configuration - COMPLETED
|
|
||||||
- ✅ Dockerfile for containerized deployment
|
|
||||||
- ✅ nginx.conf with security headers and SPA routing
|
|
||||||
- ✅ vercel.json for Vercel deployment
|
|
||||||
- ✅ netlify.toml for Netlify deployment
|
|
||||||
- ✅ .dockerignore for efficient Docker builds
|
|
||||||
- ✅ GitHub Actions CI workflow
|
|
||||||
|
|
||||||
### 6. Security Improvements - COMPLETED
|
|
||||||
- ✅ Security headers configured (X-Frame-Options, CSP, etc.)
|
|
||||||
- ✅ Error boundary prevents app crashes
|
|
||||||
- ✅ Comprehensive security documentation
|
|
||||||
- ✅ Security best practices guide
|
|
||||||
- ⚠️ Token storage still uses localStorage (documented for improvement)
|
|
||||||
|
|
||||||
## 📊 Build Status
|
|
||||||
|
|
||||||
```
|
|
||||||
✓ TypeScript compilation: SUCCESS
|
|
||||||
✓ Vite build: SUCCESS
|
|
||||||
✓ Bundle size: Optimized with code splitting
|
|
||||||
✓ No critical warnings
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build Output
|
|
||||||
- Total bundle size: ~970 KB (before gzip)
|
|
||||||
- Gzipped size: ~288 KB
|
|
||||||
- Code split into 6 chunks for optimal caching
|
|
||||||
|
|
||||||
## 📁 New Files Created
|
|
||||||
|
|
||||||
### Configuration Files
|
|
||||||
- `.env.example` - Development environment template
|
|
||||||
- `.env.production.example` - Production environment template
|
|
||||||
- `vite.config.ts` - Updated with production optimizations
|
|
||||||
- `vercel.json` - Vercel deployment configuration
|
|
||||||
- `netlify.toml` - Netlify deployment configuration
|
|
||||||
- `Dockerfile` - Docker containerization
|
|
||||||
- `nginx.conf` - Nginx server configuration
|
|
||||||
- `.dockerignore` - Docker build optimization
|
|
||||||
- `.github/workflows/ci.yml` - CI/CD pipeline
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- `README.md` - Comprehensive project documentation
|
|
||||||
- `DEPLOYMENT.md` - Deployment guide and checklist
|
|
||||||
- `SECURITY.md` - Security best practices
|
|
||||||
- `PRODUCTION_READY_SUMMARY.md` - This file
|
|
||||||
|
|
||||||
### Components
|
|
||||||
- `src/components/ErrorBoundary.tsx` - Error boundary component
|
|
||||||
|
|
||||||
## 🚀 Quick Start for Production
|
|
||||||
|
|
||||||
### 1. Set Up Environment
|
|
||||||
```bash
|
|
||||||
cp .env.production.example .env.production
|
|
||||||
# Edit .env.production with your production API URL
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Build
|
|
||||||
```bash
|
|
||||||
npm run build:prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Test Locally
|
|
||||||
```bash
|
|
||||||
npm run preview
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Deploy
|
|
||||||
Choose your platform:
|
|
||||||
- **Vercel:** `vercel --prod`
|
|
||||||
- **Netlify:** `netlify deploy --prod`
|
|
||||||
- **Docker:** `docker build -t yaltopia-admin . && docker run -p 80:80 yaltopia-admin`
|
|
||||||
|
|
||||||
## ⚠️ Important Notes Before Production
|
|
||||||
|
|
||||||
### Must Do
|
|
||||||
1. **Set up HTTPS** - Never deploy without SSL/TLS
|
|
||||||
2. **Configure environment variables** - Set VITE_API_URL to production API
|
|
||||||
3. **Test authentication flow** - Ensure login/logout works
|
|
||||||
4. **Verify API connectivity** - Test all API endpoints
|
|
||||||
5. **Configure CORS** - Backend must allow your production domain
|
|
||||||
|
|
||||||
### Should Do
|
|
||||||
1. **Set up error tracking** - Sentry, LogRocket, or similar
|
|
||||||
2. **Configure analytics** - Google Analytics, Plausible, etc.
|
|
||||||
3. **Set up monitoring** - Uptime monitoring and alerts
|
|
||||||
4. **Review security checklist** - See SECURITY.md
|
|
||||||
5. **Test on multiple browsers** - Chrome, Firefox, Safari, Edge
|
|
||||||
|
|
||||||
### Consider Doing
|
|
||||||
1. **Implement httpOnly cookies** - More secure than localStorage
|
|
||||||
2. **Add rate limiting** - Protect against abuse
|
|
||||||
3. **Set up CDN** - Cloudflare, AWS CloudFront, etc.
|
|
||||||
4. **Enable compression** - Gzip/Brotli on server
|
|
||||||
5. **Add CSP headers** - Content Security Policy
|
|
||||||
|
|
||||||
## 🔒 Security Status
|
|
||||||
|
|
||||||
### Implemented ✅
|
|
||||||
- Error boundary for graceful failures
|
|
||||||
- Security headers in deployment configs
|
|
||||||
- HTTPS enforcement in configs
|
|
||||||
- Input validation on forms
|
|
||||||
- Error handling for API calls
|
|
||||||
- Environment variable management
|
|
||||||
|
|
||||||
### Recommended Improvements ⚠️
|
|
||||||
- Move from localStorage to httpOnly cookies for tokens
|
|
||||||
- Implement Content Security Policy (CSP)
|
|
||||||
- Add rate limiting on backend
|
|
||||||
- Set up error tracking service
|
|
||||||
- Implement session timeout
|
|
||||||
- Add security monitoring
|
|
||||||
|
|
||||||
See `SECURITY.md` for detailed security recommendations.
|
|
||||||
|
|
||||||
## 📈 Performance
|
|
||||||
|
|
||||||
### Current Status
|
|
||||||
- Bundle split into 6 optimized chunks
|
|
||||||
- React vendor: 47 KB (gzipped: 17 KB)
|
|
||||||
- UI vendor: 107 KB (gzipped: 32 KB)
|
|
||||||
- Chart vendor: 383 KB (gzipped: 112 KB)
|
|
||||||
- Main app: 396 KB (gzipped: 117 KB)
|
|
||||||
|
|
||||||
### Optimization Opportunities
|
|
||||||
- Lazy load routes (if needed)
|
|
||||||
- Optimize images (if any large images added)
|
|
||||||
- Consider removing unused Radix UI components
|
|
||||||
- Implement virtual scrolling for large tables
|
|
||||||
|
|
||||||
## 🧪 Testing Checklist
|
|
||||||
|
|
||||||
Before deploying to production:
|
|
||||||
|
|
||||||
- [ ] Build completes without errors
|
|
||||||
- [ ] Application loads in browser
|
|
||||||
- [ ] Login/authentication works
|
|
||||||
- [ ] All routes accessible
|
|
||||||
- [ ] API calls successful
|
|
||||||
- [ ] Error handling works
|
|
||||||
- [ ] No console errors
|
|
||||||
- [ ] Mobile responsive
|
|
||||||
- [ ] Cross-browser compatible
|
|
||||||
- [ ] Performance acceptable (Lighthouse score)
|
|
||||||
|
|
||||||
## 📞 Support & Maintenance
|
|
||||||
|
|
||||||
### Regular Tasks
|
|
||||||
- **Daily:** Monitor error logs
|
|
||||||
- **Weekly:** Review security alerts, check for updates
|
|
||||||
- **Monthly:** Run `npm audit`, update dependencies
|
|
||||||
- **Quarterly:** Security review, performance audit
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
See `DEPLOYMENT.md` for common issues and solutions.
|
|
||||||
|
|
||||||
## 🎯 Next Steps
|
|
||||||
|
|
||||||
1. **Immediate:**
|
|
||||||
- Set up production environment variables
|
|
||||||
- Deploy to staging environment
|
|
||||||
- Run full test suite
|
|
||||||
- Deploy to production
|
|
||||||
|
|
||||||
2. **Short-term (1-2 weeks):**
|
|
||||||
- Set up error tracking (Sentry)
|
|
||||||
- Configure analytics
|
|
||||||
- Set up monitoring and alerts
|
|
||||||
- Implement security improvements
|
|
||||||
|
|
||||||
3. **Long-term (1-3 months):**
|
|
||||||
- Add automated testing
|
|
||||||
- Implement CI/CD pipeline
|
|
||||||
- Performance optimization
|
|
||||||
- Security audit
|
|
||||||
|
|
||||||
## ✨ Summary
|
|
||||||
|
|
||||||
Your Yaltopia Ticket Admin application is now **production-ready** with:
|
|
||||||
|
|
||||||
- ✅ All TypeScript errors fixed
|
|
||||||
- ✅ Build successfully compiling
|
|
||||||
- ✅ Comprehensive documentation
|
|
||||||
- ✅ Multiple deployment options configured
|
|
||||||
- ✅ Security best practices documented
|
|
||||||
- ✅ Error handling implemented
|
|
||||||
- ✅ Production optimizations applied
|
|
||||||
|
|
||||||
**The application can be deployed to production**, but review the security recommendations and complete the pre-deployment checklist in `DEPLOYMENT.md` for best results.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Version:** 1.0.0
|
|
||||||
**Last Updated:** February 24, 2026
|
|
||||||
**Status:** ✅ Production Ready
|
|
||||||
|
|
@ -1,206 +0,0 @@
|
||||||
# Quick Reference Guide
|
|
||||||
|
|
||||||
## Common Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
npm run dev # Start dev server (http://localhost:5173)
|
|
||||||
npm run build # Build for production
|
|
||||||
npm run build:prod # Build with production env
|
|
||||||
npm run preview # Preview production build
|
|
||||||
npm run lint # Run ESLint
|
|
||||||
npm run lint:fix # Fix ESLint errors
|
|
||||||
npm run type-check # TypeScript type checking
|
|
||||||
|
|
||||||
# Deployment
|
|
||||||
vercel --prod # Deploy to Vercel
|
|
||||||
netlify deploy --prod # Deploy to Netlify
|
|
||||||
docker build -t app . # Build Docker image
|
|
||||||
docker run -p 80:80 app # Run Docker container
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Required
|
|
||||||
VITE_API_URL=http://localhost:3000/api/v1
|
|
||||||
|
|
||||||
# Optional
|
|
||||||
VITE_ENV=development
|
|
||||||
VITE_ANALYTICS_ID=
|
|
||||||
VITE_SENTRY_DSN=
|
|
||||||
```
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
├── src/
|
|
||||||
│ ├── app/ # App config (query client)
|
|
||||||
│ ├── components/ # Reusable components
|
|
||||||
│ │ └── ui/ # UI components
|
|
||||||
│ ├── layouts/ # Layout components
|
|
||||||
│ ├── lib/ # Utils & API client
|
|
||||||
│ ├── pages/ # Page components
|
|
||||||
│ │ └── admin/ # Admin pages
|
|
||||||
│ ├── App.tsx # Main app
|
|
||||||
│ └── main.tsx # Entry point
|
|
||||||
├── .env.example # Env template
|
|
||||||
├── vite.config.ts # Vite config
|
|
||||||
├── package.json # Dependencies
|
|
||||||
└── README.md # Documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `src/lib/api-client.ts` | API configuration & helpers |
|
|
||||||
| `src/app/query-client.ts` | React Query setup |
|
|
||||||
| `src/components/ErrorBoundary.tsx` | Error handling |
|
|
||||||
| `vite.config.ts` | Build configuration |
|
|
||||||
| `.env.example` | Environment template |
|
|
||||||
|
|
||||||
## API Client Usage
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { adminApiHelpers } from '@/lib/api-client';
|
|
||||||
|
|
||||||
// Get users
|
|
||||||
const response = await adminApiHelpers.getUsers({ page: 1, limit: 20 });
|
|
||||||
|
|
||||||
// Get user by ID
|
|
||||||
const user = await adminApiHelpers.getUser(userId);
|
|
||||||
|
|
||||||
// Update user
|
|
||||||
await adminApiHelpers.updateUser(userId, { isActive: false });
|
|
||||||
|
|
||||||
// Delete user
|
|
||||||
await adminApiHelpers.deleteUser(userId);
|
|
||||||
```
|
|
||||||
|
|
||||||
## React Query Usage
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
// Fetch data
|
|
||||||
const { data, isLoading, error } = useQuery({
|
|
||||||
queryKey: ['users', page],
|
|
||||||
queryFn: async () => {
|
|
||||||
const response = await adminApiHelpers.getUsers({ page });
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mutate data
|
|
||||||
const mutation = useMutation({
|
|
||||||
mutationFn: async (data) => {
|
|
||||||
await adminApiHelpers.updateUser(id, data);
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Routing
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
// Navigate
|
|
||||||
const navigate = useNavigate();
|
|
||||||
navigate('/admin/users');
|
|
||||||
|
|
||||||
// Get params
|
|
||||||
const { id } = useParams();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Toast Notifications
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
toast.success('Success message');
|
|
||||||
toast.error('Error message');
|
|
||||||
toast.info('Info message');
|
|
||||||
toast.warning('Warning message');
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment Quick Start
|
|
||||||
|
|
||||||
### Vercel
|
|
||||||
```bash
|
|
||||||
npm i -g vercel
|
|
||||||
vercel login
|
|
||||||
vercel --prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### Netlify
|
|
||||||
```bash
|
|
||||||
npm i -g netlify-cli
|
|
||||||
netlify login
|
|
||||||
netlify deploy --prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
```bash
|
|
||||||
docker build -t yaltopia-admin .
|
|
||||||
docker run -p 80:80 yaltopia-admin
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Build fails
|
|
||||||
```bash
|
|
||||||
rm -rf node_modules package-lock.json
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type errors
|
|
||||||
```bash
|
|
||||||
npm run type-check
|
|
||||||
```
|
|
||||||
|
|
||||||
### Blank page after deploy
|
|
||||||
- Check browser console
|
|
||||||
- Verify API URL in env vars
|
|
||||||
- Check server config for SPA routing
|
|
||||||
|
|
||||||
### API calls failing
|
|
||||||
- Check CORS on backend
|
|
||||||
- Verify API URL
|
|
||||||
- Check network tab in DevTools
|
|
||||||
|
|
||||||
## Security Checklist
|
|
||||||
|
|
||||||
- [ ] HTTPS enabled
|
|
||||||
- [ ] Environment variables set
|
|
||||||
- [ ] CORS configured
|
|
||||||
- [ ] Security headers added
|
|
||||||
- [ ] Error tracking set up
|
|
||||||
- [ ] Monitoring configured
|
|
||||||
|
|
||||||
## Performance Tips
|
|
||||||
|
|
||||||
- Use code splitting for large routes
|
|
||||||
- Lazy load heavy components
|
|
||||||
- Optimize images
|
|
||||||
- Enable compression (gzip/brotli)
|
|
||||||
- Use CDN for static assets
|
|
||||||
|
|
||||||
## Useful Links
|
|
||||||
|
|
||||||
- [React Query Docs](https://tanstack.com/query/latest)
|
|
||||||
- [React Router Docs](https://reactrouter.com/)
|
|
||||||
- [Vite Docs](https://vitejs.dev/)
|
|
||||||
- [Tailwind CSS Docs](https://tailwindcss.com/)
|
|
||||||
- [Radix UI Docs](https://www.radix-ui.com/)
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
- Check `README.md` for detailed docs
|
|
||||||
- See `DEPLOYMENT.md` for deployment guide
|
|
||||||
- Review `SECURITY.md` for security best practices
|
|
||||||
- Read `PRODUCTION_READY_SUMMARY.md` for status
|
|
||||||
|
|
@ -1,89 +1,99 @@
|
||||||
# Developer Documentation
|
# Developer Documentation
|
||||||
|
|
||||||
This directory contains comprehensive documentation for the Yaltopia Ticket Admin project.
|
Essential documentation for the Yaltopia Ticket Admin project.
|
||||||
|
|
||||||
## 📚 Documentation Index
|
## 📚 Documentation
|
||||||
|
|
||||||
### Getting Started
|
### [Development Guide](./DEVELOPMENT.md)
|
||||||
- **[Quick Reference](./QUICK_REFERENCE.md)** - Quick start guide and common commands
|
Complete development guide including:
|
||||||
- **[Tech Stack](./TECH_STACK.md)** - Technologies and frameworks used
|
- Tech stack & project structure
|
||||||
|
- Quick start & setup
|
||||||
|
- Common tasks & best practices
|
||||||
|
- Troubleshooting
|
||||||
|
|
||||||
### Development
|
### [API & Service Layer Guide](./API_GUIDE.md) ⭐
|
||||||
- **[Authentication](./AUTHENTICATION.md)** - Authentication setup and flow
|
**Essential reading for making API calls:**
|
||||||
- **[API Standards](./API_STANDARDS.md)** - API client implementation and best practices
|
- Service layer architecture
|
||||||
- **[Login API Documentation](./LOGIN_API_DOCUMENTATION.md)** - Login endpoint specifications
|
- All available services & methods
|
||||||
- **[Troubleshooting](./TROUBLESHOOTING.md)** - Common issues and solutions
|
- Common patterns & examples
|
||||||
|
- Error handling
|
||||||
|
- Best practices
|
||||||
|
|
||||||
### Testing & Quality
|
### [Testing Guide](./TESTING_GUIDE.md)
|
||||||
- **[Testing Guide](./TESTING_GUIDE.md)** - Testing setup and best practices
|
Testing setup and practices:
|
||||||
- **[CI/CD Setup](./CI_CD_SETUP.md)** - Continuous integration and deployment
|
- Unit testing with Vitest
|
||||||
- **[Error Monitoring](./ERROR_MONITORING.md)** - Sentry integration and error tracking
|
- Component testing
|
||||||
|
- Integration testing
|
||||||
|
- Test utilities & mocks
|
||||||
|
|
||||||
### Security
|
### [Deployment Guide](./DEPLOYMENT.md)
|
||||||
- **[Security Checklist](./SECURITY_CHECKLIST.md)** - Comprehensive security requirements
|
Production deployment:
|
||||||
- **[Security](./SECURITY.md)** - Security best practices and guidelines
|
- Pre-deployment checklist
|
||||||
|
- Deployment options (Vercel, Netlify, Docker)
|
||||||
|
- Environment configuration
|
||||||
|
- CI/CD setup
|
||||||
|
|
||||||
### Deployment
|
### [Security Guide](./SECURITY.md)
|
||||||
- **[Deployment Options](./DEPLOYMENT_OPTIONS.md)** - All deployment configurations (Vercel, Netlify, Docker)
|
Security best practices:
|
||||||
- **[Deployment Guide](./DEPLOYMENT.md)** - Step-by-step deployment instructions
|
- Authentication & authorization
|
||||||
- **[Pre-Deployment Checklist](./PRE_DEPLOYMENT_CHECKLIST.md)** - Checklist before going live
|
- Data protection
|
||||||
- **[Production Ready Summary](./PRODUCTION_READY_SUMMARY.md)** - Production readiness overview
|
- Security headers
|
||||||
|
- CORS configuration
|
||||||
|
- Input validation
|
||||||
|
|
||||||
## 🎯 Quick Links
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### For Developers
|
```bash
|
||||||
1. Start with [Quick Reference](./QUICK_REFERENCE.md)
|
# Install
|
||||||
2. Understand [Tech Stack](./TECH_STACK.md)
|
npm install
|
||||||
3. Set up [Authentication](./AUTHENTICATION.md)
|
|
||||||
4. Review [API Standards](./API_STANDARDS.md)
|
|
||||||
|
|
||||||
### For DevOps
|
# Configure
|
||||||
1. Review [CI/CD Setup](./CI_CD_SETUP.md)
|
cp .env.example .env
|
||||||
2. Choose deployment from [Deployment Options](./DEPLOYMENT_OPTIONS.md)
|
# Edit .env with your backend URL
|
||||||
3. Follow [Deployment Guide](./DEPLOYMENT.md)
|
|
||||||
4. Complete [Pre-Deployment Checklist](./PRE_DEPLOYMENT_CHECKLIST.md)
|
|
||||||
|
|
||||||
### For Security Review
|
# Develop
|
||||||
1. Review [Security Checklist](./SECURITY_CHECKLIST.md)
|
npm run dev
|
||||||
2. Check [Security](./SECURITY.md) guidelines
|
|
||||||
3. Verify [API Standards](./API_STANDARDS.md) compliance
|
|
||||||
|
|
||||||
### For Troubleshooting
|
# Test
|
||||||
1. Check [Troubleshooting](./TROUBLESHOOTING.md) guide
|
npm run test
|
||||||
2. Review [Error Monitoring](./ERROR_MONITORING.md) setup
|
|
||||||
3. Consult [API Standards](./API_STANDARDS.md) for API issues
|
|
||||||
|
|
||||||
## 📖 Documentation Standards
|
# Build
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
All documentation follows these principles:
|
## 📖 Key Concepts
|
||||||
- **Clear and Concise** - Easy to understand
|
|
||||||
- **Actionable** - Includes examples and commands
|
|
||||||
- **Up-to-date** - Reflects current implementation
|
|
||||||
- **Professional** - Industry-standard practices
|
|
||||||
|
|
||||||
## 🔄 Keeping Documentation Updated
|
### Service Layer
|
||||||
|
All API calls go through typed service classes:
|
||||||
|
```typescript
|
||||||
|
import { userService } from '@/services'
|
||||||
|
const users = await userService.getUsers()
|
||||||
|
```
|
||||||
|
|
||||||
When making changes to the project:
|
### React Query
|
||||||
1. Update relevant documentation
|
Data fetching with caching:
|
||||||
2. Add new sections if needed
|
```typescript
|
||||||
3. Remove outdated information
|
const { data } = useQuery({
|
||||||
4. Keep examples current
|
queryKey: ['users'],
|
||||||
|
queryFn: () => userService.getUsers()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
## 📝 Contributing to Documentation
|
### Protected Routes
|
||||||
|
Authentication required for admin routes:
|
||||||
To improve documentation:
|
```typescript
|
||||||
1. Identify gaps or unclear sections
|
<Route element={<ProtectedRoute />}>
|
||||||
2. Add examples and use cases
|
<Route path="/admin/*" element={<AdminLayout />} />
|
||||||
3. Include troubleshooting tips
|
</Route>
|
||||||
4. Keep formatting consistent
|
```
|
||||||
|
|
||||||
## 🆘 Need Help?
|
## 🆘 Need Help?
|
||||||
|
|
||||||
If documentation is unclear or missing:
|
1. **Making API calls?** → [API & Service Layer Guide](./API_GUIDE.md)
|
||||||
1. Check [Troubleshooting](./TROUBLESHOOTING.md)
|
2. **General development?** → [Development Guide](./DEVELOPMENT.md)
|
||||||
2. Review related documentation
|
3. **Writing tests?** → [Testing Guide](./TESTING_GUIDE.md)
|
||||||
3. Check code comments
|
4. **Deploying?** → [Deployment Guide](./DEPLOYMENT.md)
|
||||||
4. Consult team members
|
5. **Security questions?** → [Security Guide](./SECURITY.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,406 +0,0 @@
|
||||||
# Security Checklist
|
|
||||||
|
|
||||||
## Frontend Security (✅ Implemented)
|
|
||||||
|
|
||||||
### Authentication & Authorization
|
|
||||||
- ✅ **Protected Routes**: All admin routes require authentication
|
|
||||||
- ✅ **Role-Based Access**: Checks for ADMIN role before granting access
|
|
||||||
- ✅ **Cookie Support**: `withCredentials: true` for httpOnly cookies
|
|
||||||
- ✅ **Token Refresh**: Automatic token refresh on 401 errors
|
|
||||||
- ✅ **Centralized Logout**: Calls backend to clear cookies
|
|
||||||
- ✅ **Secure Redirects**: Prevents redirect loops on login page
|
|
||||||
- ✅ **localStorage Fallback**: Works with backends without cookie support
|
|
||||||
|
|
||||||
### API Security
|
|
||||||
- ✅ **Separate API Instances**: Public vs authenticated endpoints
|
|
||||||
- ✅ **Bearer Token**: Proper Authorization header format
|
|
||||||
- ✅ **Error Handling**: Consistent error responses with user feedback
|
|
||||||
- ✅ **Request Retry**: Automatic retry after token refresh
|
|
||||||
- ✅ **CORS Credentials**: Enabled for cross-origin cookie sharing
|
|
||||||
|
|
||||||
### Code Security
|
|
||||||
- ✅ **TypeScript**: Type safety throughout the application
|
|
||||||
- ✅ **Input Validation**: Form validation on login
|
|
||||||
- ✅ **Error Messages**: Generic error messages (no sensitive info leak)
|
|
||||||
- ✅ **No Hardcoded Secrets**: Uses environment variables
|
|
||||||
|
|
||||||
## Backend Security (⚠️ Must Implement)
|
|
||||||
|
|
||||||
### Critical Requirements
|
|
||||||
|
|
||||||
#### 1. httpOnly Cookies (Recommended)
|
|
||||||
```javascript
|
|
||||||
// ⚠️ BACKEND MUST IMPLEMENT
|
|
||||||
res.cookie('access_token', token, {
|
|
||||||
httpOnly: true, // ✅ Prevents XSS attacks
|
|
||||||
secure: true, // ✅ HTTPS only (production)
|
|
||||||
sameSite: 'strict', // ✅ CSRF protection
|
|
||||||
maxAge: 900000, // ✅ 15 minutes
|
|
||||||
path: '/'
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why httpOnly?**
|
|
||||||
- Prevents JavaScript access to tokens
|
|
||||||
- Protects against XSS (Cross-Site Scripting) attacks
|
|
||||||
- Industry standard for authentication
|
|
||||||
|
|
||||||
#### 2. Token Management
|
|
||||||
- ⚠️ **Short-lived Access Tokens**: 15 minutes max
|
|
||||||
- ⚠️ **Long-lived Refresh Tokens**: 7 days max
|
|
||||||
- ⚠️ **Token Rotation**: Generate new refresh token on each refresh
|
|
||||||
- ⚠️ **Token Revocation**: Invalidate tokens on logout
|
|
||||||
- ⚠️ **Token Blacklist**: Store revoked tokens (Redis recommended)
|
|
||||||
|
|
||||||
#### 3. Password Security
|
|
||||||
- ⚠️ **Hashing**: Use bcrypt/argon2 (NOT MD5/SHA1)
|
|
||||||
- ⚠️ **Salt**: Unique salt per password
|
|
||||||
- ⚠️ **Cost Factor**: bcrypt rounds >= 12
|
|
||||||
- ⚠️ **Password Policy**: Min 8 chars, complexity requirements
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ⚠️ BACKEND MUST IMPLEMENT
|
|
||||||
const bcrypt = require('bcrypt')
|
|
||||||
const saltRounds = 12
|
|
||||||
const hashedPassword = await bcrypt.hash(password, saltRounds)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. Rate Limiting
|
|
||||||
- ⚠️ **Login Endpoint**: 5 attempts per 15 minutes per IP
|
|
||||||
- ⚠️ **API Endpoints**: 100 requests per minute per user
|
|
||||||
- ⚠️ **Account Lockout**: Lock after 5 failed login attempts
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ⚠️ BACKEND MUST IMPLEMENT
|
|
||||||
const rateLimit = require('express-rate-limit')
|
|
||||||
|
|
||||||
const loginLimiter = rateLimit({
|
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
||||||
max: 5, // 5 requests per window
|
|
||||||
message: 'Too many login attempts, please try again later'
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post('/auth/login', loginLimiter, loginHandler)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5. CORS Configuration
|
|
||||||
```javascript
|
|
||||||
// ⚠️ BACKEND MUST IMPLEMENT
|
|
||||||
app.use(cors({
|
|
||||||
origin: process.env.FRONTEND_URL, // Specific origin, not '*'
|
|
||||||
credentials: true, // Allow cookies
|
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
|
||||||
allowedHeaders: ['Content-Type', 'Authorization']
|
|
||||||
}))
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 6. Input Validation
|
|
||||||
- ⚠️ **Sanitize Inputs**: Prevent SQL injection, XSS
|
|
||||||
- ⚠️ **Validate Email**: Proper email format
|
|
||||||
- ⚠️ **Validate Types**: Check data types
|
|
||||||
- ⚠️ **Limit Payload Size**: Prevent DoS attacks
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ⚠️ BACKEND MUST IMPLEMENT
|
|
||||||
const { body, validationResult } = require('express-validator')
|
|
||||||
|
|
||||||
app.post('/auth/login', [
|
|
||||||
body('email').isEmail().normalizeEmail(),
|
|
||||||
body('password').isLength({ min: 8 })
|
|
||||||
], (req, res) => {
|
|
||||||
const errors = validationResult(req)
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
return res.status(400).json({ errors: errors.array() })
|
|
||||||
}
|
|
||||||
// Process login
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 7. SQL Injection Prevention
|
|
||||||
- ⚠️ **Parameterized Queries**: Use prepared statements
|
|
||||||
- ⚠️ **ORM**: Use Prisma, TypeORM, Sequelize
|
|
||||||
- ⚠️ **Never**: Concatenate user input into SQL
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ❌ VULNERABLE
|
|
||||||
const query = `SELECT * FROM users WHERE email = '${email}'`
|
|
||||||
|
|
||||||
// ✅ SAFE
|
|
||||||
const query = 'SELECT * FROM users WHERE email = ?'
|
|
||||||
db.query(query, [email])
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 8. XSS Prevention
|
|
||||||
- ⚠️ **Escape Output**: Sanitize data before rendering
|
|
||||||
- ⚠️ **Content Security Policy**: Set CSP headers
|
|
||||||
- ⚠️ **httpOnly Cookies**: Prevent JavaScript access to tokens
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ⚠️ BACKEND MUST IMPLEMENT
|
|
||||||
const helmet = require('helmet')
|
|
||||||
app.use(helmet.contentSecurityPolicy({
|
|
||||||
directives: {
|
|
||||||
defaultSrc: ["'self'"],
|
|
||||||
scriptSrc: ["'self'"],
|
|
||||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
||||||
imgSrc: ["'self'", "data:", "https:"],
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 9. HTTPS/TLS
|
|
||||||
- ⚠️ **Production**: HTTPS only (no HTTP)
|
|
||||||
- ⚠️ **TLS 1.2+**: Disable older versions
|
|
||||||
- ⚠️ **HSTS Header**: Force HTTPS
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ⚠️ BACKEND MUST IMPLEMENT
|
|
||||||
app.use(helmet.hsts({
|
|
||||||
maxAge: 31536000, // 1 year
|
|
||||||
includeSubDomains: true,
|
|
||||||
preload: true
|
|
||||||
}))
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 10. Security Headers
|
|
||||||
```javascript
|
|
||||||
// ⚠️ BACKEND MUST IMPLEMENT
|
|
||||||
const helmet = require('helmet')
|
|
||||||
app.use(helmet()) // Sets multiple security headers
|
|
||||||
|
|
||||||
// Or manually:
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
||||||
res.setHeader('X-Frame-Options', 'DENY')
|
|
||||||
res.setHeader('X-XSS-Protection', '1; mode=block')
|
|
||||||
res.setHeader('Strict-Transport-Security', 'max-age=31536000')
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 11. Audit Logging
|
|
||||||
- ⚠️ **Log All Admin Actions**: Who, what, when, where
|
|
||||||
- ⚠️ **Log Failed Logins**: Track suspicious activity
|
|
||||||
- ⚠️ **Log Sensitive Operations**: User deletion, role changes
|
|
||||||
- ⚠️ **Secure Logs**: Store in separate database/service
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ⚠️ BACKEND MUST IMPLEMENT
|
|
||||||
const auditLog = async (userId, action, resource, details) => {
|
|
||||||
await db.auditLogs.create({
|
|
||||||
userId,
|
|
||||||
action,
|
|
||||||
resource,
|
|
||||||
details,
|
|
||||||
ipAddress: req.ip,
|
|
||||||
userAgent: req.headers['user-agent'],
|
|
||||||
timestamp: new Date()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 12. Database Security
|
|
||||||
- ⚠️ **Least Privilege**: Database user with minimal permissions
|
|
||||||
- ⚠️ **Encrypted Connections**: Use SSL/TLS for database
|
|
||||||
- ⚠️ **Backup Encryption**: Encrypt database backups
|
|
||||||
- ⚠️ **Sensitive Data**: Encrypt PII at rest
|
|
||||||
|
|
||||||
#### 13. Environment Variables
|
|
||||||
- ⚠️ **Never Commit**: .env files in .gitignore
|
|
||||||
- ⚠️ **Secrets Management**: Use vault (AWS Secrets Manager, etc.)
|
|
||||||
- ⚠️ **Rotate Secrets**: Regular rotation of API keys, tokens
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# ⚠️ BACKEND MUST CONFIGURE
|
|
||||||
JWT_SECRET=<strong-random-secret-256-bits>
|
|
||||||
JWT_REFRESH_SECRET=<different-strong-secret>
|
|
||||||
DATABASE_URL=<encrypted-connection-string>
|
|
||||||
FRONTEND_URL=https://admin.yourdomain.com
|
|
||||||
NODE_ENV=production
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 14. Session Management
|
|
||||||
- ⚠️ **Session Timeout**: Auto-logout after inactivity
|
|
||||||
- ⚠️ **Concurrent Sessions**: Limit or track multiple sessions
|
|
||||||
- ⚠️ **Session Invalidation**: Clear on logout, password change
|
|
||||||
|
|
||||||
## Additional Security Measures
|
|
||||||
|
|
||||||
### Frontend (Optional Improvements)
|
|
||||||
|
|
||||||
#### 1. Content Security Policy (CSP)
|
|
||||||
```html
|
|
||||||
<!-- Add to index.html -->
|
|
||||||
<meta http-equiv="Content-Security-Policy"
|
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Subresource Integrity (SRI)
|
|
||||||
```html
|
|
||||||
<!-- For CDN resources -->
|
|
||||||
<script src="https://cdn.example.com/lib.js"
|
|
||||||
integrity="sha384-..."
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Input Sanitization
|
|
||||||
```typescript
|
|
||||||
// Install DOMPurify
|
|
||||||
import DOMPurify from 'dompurify'
|
|
||||||
|
|
||||||
const sanitizedInput = DOMPurify.sanitize(userInput)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. Two-Factor Authentication (2FA)
|
|
||||||
- Add TOTP support (Google Authenticator)
|
|
||||||
- SMS verification
|
|
||||||
- Backup codes
|
|
||||||
|
|
||||||
#### 5. Password Strength Meter
|
|
||||||
```typescript
|
|
||||||
// Install zxcvbn
|
|
||||||
import zxcvbn from 'zxcvbn'
|
|
||||||
|
|
||||||
const strength = zxcvbn(password)
|
|
||||||
// Show strength indicator to user
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backend (Additional)
|
|
||||||
|
|
||||||
#### 1. API Versioning
|
|
||||||
```javascript
|
|
||||||
app.use('/api/v1', v1Routes)
|
|
||||||
app.use('/api/v2', v2Routes)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Request Signing
|
|
||||||
- Sign requests with HMAC
|
|
||||||
- Verify signature on backend
|
|
||||||
- Prevents request tampering
|
|
||||||
|
|
||||||
#### 3. IP Whitelisting (Admin Panel)
|
|
||||||
```javascript
|
|
||||||
const adminIpWhitelist = ['192.168.1.1', '10.0.0.1']
|
|
||||||
|
|
||||||
const ipWhitelistMiddleware = (req, res, next) => {
|
|
||||||
if (!adminIpWhitelist.includes(req.ip)) {
|
|
||||||
return res.status(403).json({ message: 'Access denied' })
|
|
||||||
}
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use('/admin', ipWhitelistMiddleware)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. Geo-blocking
|
|
||||||
- Block requests from certain countries
|
|
||||||
- Use CloudFlare or similar service
|
|
||||||
|
|
||||||
#### 5. DDoS Protection
|
|
||||||
- Use CloudFlare, AWS Shield
|
|
||||||
- Rate limiting at infrastructure level
|
|
||||||
- CDN for static assets
|
|
||||||
|
|
||||||
## Security Testing
|
|
||||||
|
|
||||||
### Automated Testing
|
|
||||||
- ⚠️ **OWASP ZAP**: Automated security scanning
|
|
||||||
- ⚠️ **npm audit**: Check for vulnerable dependencies
|
|
||||||
- ⚠️ **Snyk**: Continuous security monitoring
|
|
||||||
- ⚠️ **SonarQube**: Code quality and security
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run security audit
|
|
||||||
npm audit
|
|
||||||
npm audit fix
|
|
||||||
|
|
||||||
# Check for outdated packages
|
|
||||||
npm outdated
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual Testing
|
|
||||||
- ⚠️ **Penetration Testing**: Hire security experts
|
|
||||||
- ⚠️ **Code Review**: Security-focused code reviews
|
|
||||||
- ⚠️ **Vulnerability Scanning**: Regular scans
|
|
||||||
|
|
||||||
## Compliance
|
|
||||||
|
|
||||||
### GDPR (EU)
|
|
||||||
- ⚠️ **Data Minimization**: Collect only necessary data
|
|
||||||
- ⚠️ **Right to Erasure**: Allow users to delete their data
|
|
||||||
- ⚠️ **Data Portability**: Export user data
|
|
||||||
- ⚠️ **Consent**: Explicit consent for data processing
|
|
||||||
- ⚠️ **Privacy Policy**: Clear privacy policy
|
|
||||||
|
|
||||||
### HIPAA (Healthcare - US)
|
|
||||||
- ⚠️ **Encryption**: Encrypt PHI at rest and in transit
|
|
||||||
- ⚠️ **Access Controls**: Role-based access
|
|
||||||
- ⚠️ **Audit Logs**: Track all PHI access
|
|
||||||
- ⚠️ **Business Associate Agreement**: With third parties
|
|
||||||
|
|
||||||
### PCI DSS (Payment Cards)
|
|
||||||
- ⚠️ **Never Store**: CVV, full card numbers
|
|
||||||
- ⚠️ **Tokenization**: Use payment gateway tokens
|
|
||||||
- ⚠️ **Encryption**: Encrypt cardholder data
|
|
||||||
|
|
||||||
## Monitoring & Alerting
|
|
||||||
|
|
||||||
### What to Monitor
|
|
||||||
- ⚠️ **Failed Login Attempts**: Alert on threshold
|
|
||||||
- ⚠️ **Unusual Activity**: Large data exports, bulk deletions
|
|
||||||
- ⚠️ **API Errors**: Spike in 401/403/500 errors
|
|
||||||
- ⚠️ **Performance**: Slow queries, high CPU
|
|
||||||
- ⚠️ **Security Events**: Unauthorized access attempts
|
|
||||||
|
|
||||||
### Tools
|
|
||||||
- **Sentry**: Error tracking
|
|
||||||
- **DataDog**: Application monitoring
|
|
||||||
- **CloudWatch**: AWS monitoring
|
|
||||||
- **Prometheus + Grafana**: Metrics and dashboards
|
|
||||||
|
|
||||||
## Incident Response Plan
|
|
||||||
|
|
||||||
### Steps
|
|
||||||
1. **Detect**: Identify security incident
|
|
||||||
2. **Contain**: Isolate affected systems
|
|
||||||
3. **Investigate**: Determine scope and impact
|
|
||||||
4. **Remediate**: Fix vulnerability
|
|
||||||
5. **Recover**: Restore normal operations
|
|
||||||
6. **Review**: Post-incident analysis
|
|
||||||
|
|
||||||
### Contacts
|
|
||||||
- Security team email
|
|
||||||
- On-call engineer
|
|
||||||
- Legal team (for data breaches)
|
|
||||||
- PR team (for public disclosure)
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
### Current Status
|
|
||||||
✅ **Frontend**: Implements industry-standard security patterns
|
|
||||||
⚠️ **Backend**: Must implement security measures listed above
|
|
||||||
|
|
||||||
### Priority Actions (Backend)
|
|
||||||
1. 🔴 **Critical**: Implement httpOnly cookies
|
|
||||||
2. 🔴 **Critical**: Hash passwords with bcrypt
|
|
||||||
3. 🔴 **Critical**: Add rate limiting
|
|
||||||
4. 🔴 **Critical**: Enable HTTPS in production
|
|
||||||
5. 🟡 **High**: Implement token refresh
|
|
||||||
6. 🟡 **High**: Add input validation
|
|
||||||
7. 🟡 **High**: Configure CORS properly
|
|
||||||
8. 🟡 **High**: Add security headers
|
|
||||||
9. 🟢 **Medium**: Implement audit logging
|
|
||||||
10. 🟢 **Medium**: Add 2FA support
|
|
||||||
|
|
||||||
### Security Score
|
|
||||||
- **Frontend**: 9/10 ✅
|
|
||||||
- **Backend**: Depends on implementation ⚠️
|
|
||||||
- **Overall**: Requires backend security implementation
|
|
||||||
|
|
||||||
### Next Steps
|
|
||||||
1. Review this checklist with backend team
|
|
||||||
2. Implement critical security measures
|
|
||||||
3. Conduct security audit
|
|
||||||
4. Set up monitoring and alerting
|
|
||||||
5. Create incident response plan
|
|
||||||
6. Regular security reviews and updates
|
|
||||||
|
|
@ -1,437 +0,0 @@
|
||||||
# Tech Stack & Frameworks
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
**Yaltopia Ticket Admin** - Admin dashboard for ticket management system
|
|
||||||
|
|
||||||
## Core Technologies
|
|
||||||
|
|
||||||
### Frontend Framework
|
|
||||||
- **React 19.2.0** - Latest version with modern features
|
|
||||||
- Component-based architecture
|
|
||||||
- Hooks for state management
|
|
||||||
- Concurrent rendering
|
|
||||||
- Automatic batching
|
|
||||||
|
|
||||||
### Language
|
|
||||||
- **TypeScript 5.9.3** - Type-safe JavaScript
|
|
||||||
- Static type checking
|
|
||||||
- Enhanced IDE support
|
|
||||||
- Better code documentation
|
|
||||||
- Reduced runtime errors
|
|
||||||
|
|
||||||
### Build Tool
|
|
||||||
- **Vite 7.2.4** - Next-generation frontend tooling
|
|
||||||
- Lightning-fast HMR (Hot Module Replacement)
|
|
||||||
- Optimized production builds
|
|
||||||
- Native ES modules
|
|
||||||
- Plugin ecosystem
|
|
||||||
- Code splitting and lazy loading
|
|
||||||
|
|
||||||
## UI & Styling
|
|
||||||
|
|
||||||
### CSS Framework
|
|
||||||
- **Tailwind CSS 3.4.17** - Utility-first CSS framework
|
|
||||||
- Rapid UI development
|
|
||||||
- Consistent design system
|
|
||||||
- Responsive design utilities
|
|
||||||
- Dark mode support
|
|
||||||
- Custom theme configuration
|
|
||||||
|
|
||||||
### Component Library
|
|
||||||
- **Radix UI** - Unstyled, accessible component primitives
|
|
||||||
- `@radix-ui/react-avatar` - Avatar component
|
|
||||||
- `@radix-ui/react-dialog` - Modal dialogs
|
|
||||||
- `@radix-ui/react-dropdown-menu` - Dropdown menus
|
|
||||||
- `@radix-ui/react-label` - Form labels
|
|
||||||
- `@radix-ui/react-scroll-area` - Custom scrollbars
|
|
||||||
- `@radix-ui/react-select` - Select dropdowns
|
|
||||||
- `@radix-ui/react-separator` - Visual separators
|
|
||||||
- `@radix-ui/react-slot` - Composition utility
|
|
||||||
- `@radix-ui/react-switch` - Toggle switches
|
|
||||||
- `@radix-ui/react-tabs` - Tab navigation
|
|
||||||
- `@radix-ui/react-toast` - Toast notifications
|
|
||||||
|
|
||||||
**Why Radix UI?**
|
|
||||||
- Fully accessible (WCAG compliant)
|
|
||||||
- Unstyled (full design control)
|
|
||||||
- Keyboard navigation
|
|
||||||
- Focus management
|
|
||||||
- Screen reader support
|
|
||||||
|
|
||||||
### UI Utilities
|
|
||||||
- **class-variance-authority (CVA)** - Component variant management
|
|
||||||
- **clsx** - Conditional className utility
|
|
||||||
- **tailwind-merge** - Merge Tailwind classes intelligently
|
|
||||||
- **tailwindcss-animate** - Animation utilities
|
|
||||||
|
|
||||||
### Icons
|
|
||||||
- **Lucide React 0.561.0** - Beautiful, consistent icon set
|
|
||||||
- 1000+ icons
|
|
||||||
- Tree-shakeable
|
|
||||||
- Customizable size and color
|
|
||||||
- Accessible
|
|
||||||
|
|
||||||
## Routing
|
|
||||||
|
|
||||||
### Router
|
|
||||||
- **React Router v7.11.0** - Declarative routing
|
|
||||||
- Nested routes
|
|
||||||
- Protected routes
|
|
||||||
- Dynamic routing
|
|
||||||
- Navigation guards
|
|
||||||
- Location state management
|
|
||||||
|
|
||||||
## State Management
|
|
||||||
|
|
||||||
### Server State
|
|
||||||
- **TanStack Query (React Query) 5.90.12** - Powerful data synchronization
|
|
||||||
- Automatic caching
|
|
||||||
- Background refetching
|
|
||||||
- Optimistic updates
|
|
||||||
- Pagination support
|
|
||||||
- Infinite queries
|
|
||||||
- Devtools for debugging
|
|
||||||
|
|
||||||
**Why React Query?**
|
|
||||||
- Eliminates boilerplate for API calls
|
|
||||||
- Automatic loading/error states
|
|
||||||
- Smart caching and invalidation
|
|
||||||
- Reduces global state complexity
|
|
||||||
|
|
||||||
### Local State
|
|
||||||
- **React Hooks** - Built-in state management
|
|
||||||
- `useState` - Component state
|
|
||||||
- `useEffect` - Side effects
|
|
||||||
- `useContext` - Context API
|
|
||||||
- Custom hooks for reusability
|
|
||||||
|
|
||||||
## Data Fetching
|
|
||||||
|
|
||||||
### HTTP Client
|
|
||||||
- **Axios 1.13.2** - Promise-based HTTP client
|
|
||||||
- Request/response interceptors
|
|
||||||
- Automatic JSON transformation
|
|
||||||
- Request cancellation
|
|
||||||
- Progress tracking
|
|
||||||
- Error handling
|
|
||||||
- TypeScript support
|
|
||||||
|
|
||||||
**Features Implemented:**
|
|
||||||
- Automatic token injection
|
|
||||||
- Cookie support (`withCredentials`)
|
|
||||||
- Centralized error handling
|
|
||||||
- Automatic token refresh
|
|
||||||
- Request retry logic
|
|
||||||
|
|
||||||
## Data Visualization
|
|
||||||
|
|
||||||
### Charts
|
|
||||||
- **Recharts 3.6.0** - Composable charting library
|
|
||||||
- Line charts
|
|
||||||
- Bar charts
|
|
||||||
- Area charts
|
|
||||||
- Pie charts
|
|
||||||
- Responsive design
|
|
||||||
- Customizable styling
|
|
||||||
|
|
||||||
**Used For:**
|
|
||||||
- User growth analytics
|
|
||||||
- Revenue trends
|
|
||||||
- API usage statistics
|
|
||||||
- Error rate monitoring
|
|
||||||
- Storage analytics
|
|
||||||
|
|
||||||
## Utilities
|
|
||||||
|
|
||||||
### Date Handling
|
|
||||||
- **date-fns 4.1.0** - Modern date utility library
|
|
||||||
- Lightweight (tree-shakeable)
|
|
||||||
- Immutable
|
|
||||||
- TypeScript support
|
|
||||||
- Timezone support
|
|
||||||
- Formatting and parsing
|
|
||||||
|
|
||||||
### Notifications
|
|
||||||
- **Sonner 2.0.7** - Toast notification system
|
|
||||||
- Beautiful default styling
|
|
||||||
- Promise-based toasts
|
|
||||||
- Custom positioning
|
|
||||||
- Dismissible
|
|
||||||
- Accessible
|
|
||||||
|
|
||||||
## Development Tools
|
|
||||||
|
|
||||||
### Linting
|
|
||||||
- **ESLint 9.39.1** - JavaScript/TypeScript linter
|
|
||||||
- Code quality enforcement
|
|
||||||
- Best practices
|
|
||||||
- Error prevention
|
|
||||||
- Custom rules
|
|
||||||
|
|
||||||
**Plugins:**
|
|
||||||
- `eslint-plugin-react-hooks` - React Hooks rules
|
|
||||||
- `eslint-plugin-react-refresh` - Fast Refresh rules
|
|
||||||
- `typescript-eslint` - TypeScript-specific rules
|
|
||||||
|
|
||||||
### Build Tools
|
|
||||||
- **PostCSS 8.5.6** - CSS transformation
|
|
||||||
- **Autoprefixer 10.4.23** - Automatic vendor prefixes
|
|
||||||
- **TypeScript Compiler** - Type checking and transpilation
|
|
||||||
|
|
||||||
### Type Definitions
|
|
||||||
- `@types/node` - Node.js types
|
|
||||||
- `@types/react` - React types
|
|
||||||
- `@types/react-dom` - React DOM types
|
|
||||||
|
|
||||||
## Architecture Patterns
|
|
||||||
|
|
||||||
### Design Patterns Used
|
|
||||||
|
|
||||||
1. **Component Composition**
|
|
||||||
- Reusable UI components
|
|
||||||
- Props-based customization
|
|
||||||
- Compound components
|
|
||||||
|
|
||||||
2. **Custom Hooks**
|
|
||||||
- Reusable logic extraction
|
|
||||||
- State management
|
|
||||||
- Side effects handling
|
|
||||||
|
|
||||||
3. **Higher-Order Components (HOC)**
|
|
||||||
- `ProtectedRoute` for authentication
|
|
||||||
- Route guards
|
|
||||||
|
|
||||||
4. **Render Props**
|
|
||||||
- Flexible component APIs
|
|
||||||
- Logic sharing
|
|
||||||
|
|
||||||
5. **Container/Presentational Pattern**
|
|
||||||
- Separation of concerns
|
|
||||||
- Logic vs UI separation
|
|
||||||
|
|
||||||
6. **API Client Pattern**
|
|
||||||
- Centralized API calls
|
|
||||||
- Consistent error handling
|
|
||||||
- Interceptor-based auth
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
yaltopia-ticket-admin/
|
|
||||||
├── src/
|
|
||||||
│ ├── app/ # App configuration
|
|
||||||
│ │ └── query-client.ts # React Query setup
|
|
||||||
│ ├── assets/ # Static assets
|
|
||||||
│ ├── components/ # Reusable components
|
|
||||||
│ │ ├── ui/ # Radix UI components
|
|
||||||
│ │ ├── ErrorBoundary.tsx # Error handling
|
|
||||||
│ │ └── ProtectedRoute.tsx # Auth guard
|
|
||||||
│ ├── layouts/ # Layout components
|
|
||||||
│ │ └── app-shell.tsx # Main layout
|
|
||||||
│ ├── lib/ # Utilities
|
|
||||||
│ │ ├── api-client.ts # Axios configuration
|
|
||||||
│ │ └── utils.ts # Helper functions
|
|
||||||
│ ├── pages/ # Page components
|
|
||||||
│ │ ├── admin/ # Admin pages
|
|
||||||
│ │ ├── login/ # Login page
|
|
||||||
│ │ └── ...
|
|
||||||
│ ├── App.tsx # Root component
|
|
||||||
│ ├── main.tsx # Entry point
|
|
||||||
│ └── index.css # Global styles
|
|
||||||
├── public/ # Public assets
|
|
||||||
├── dev-docs/ # Documentation
|
|
||||||
├── .env.example # Environment template
|
|
||||||
├── vite.config.ts # Vite configuration
|
|
||||||
├── tailwind.config.js # Tailwind configuration
|
|
||||||
├── tsconfig.json # TypeScript configuration
|
|
||||||
└── package.json # Dependencies
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Optimizations
|
|
||||||
|
|
||||||
### Code Splitting
|
|
||||||
- **Manual Chunks** - Vendor code separation
|
|
||||||
- `react-vendor` - React core libraries
|
|
||||||
- `ui-vendor` - Radix UI components
|
|
||||||
- `chart-vendor` - Recharts library
|
|
||||||
- `query-vendor` - TanStack Query
|
|
||||||
|
|
||||||
### Build Optimizations
|
|
||||||
- Tree shaking (unused code removal)
|
|
||||||
- Minification
|
|
||||||
- Compression
|
|
||||||
- Source map generation (disabled in production)
|
|
||||||
- Chunk size optimization (1000kb limit)
|
|
||||||
|
|
||||||
### Runtime Optimizations
|
|
||||||
- React Query caching
|
|
||||||
- Lazy loading routes
|
|
||||||
- Image optimization
|
|
||||||
- Debounced search inputs
|
|
||||||
- Memoization where needed
|
|
||||||
|
|
||||||
## Browser Support
|
|
||||||
|
|
||||||
- Chrome (latest)
|
|
||||||
- Firefox (latest)
|
|
||||||
- Safari (latest)
|
|
||||||
- Edge (latest)
|
|
||||||
|
|
||||||
**Minimum Versions:**
|
|
||||||
- Chrome 90+
|
|
||||||
- Firefox 88+
|
|
||||||
- Safari 14+
|
|
||||||
- Edge 90+
|
|
||||||
|
|
||||||
## Development Environment
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
- **Node.js**: 18+ (LTS recommended)
|
|
||||||
- **npm**: 9+ or **yarn**: 1.22+
|
|
||||||
- **Git**: 2.0+
|
|
||||||
|
|
||||||
### Recommended IDE
|
|
||||||
- **VS Code** with extensions:
|
|
||||||
- ESLint
|
|
||||||
- Prettier
|
|
||||||
- Tailwind CSS IntelliSense
|
|
||||||
- TypeScript and JavaScript Language Features
|
|
||||||
- Auto Rename Tag
|
|
||||||
- Path Intellisense
|
|
||||||
|
|
||||||
### Development Server
|
|
||||||
- **Port**: 5173 (configurable)
|
|
||||||
- **Hot Module Replacement**: Enabled
|
|
||||||
- **Host**: 0.0.0.0 (accessible from network)
|
|
||||||
|
|
||||||
## Deployment Options
|
|
||||||
|
|
||||||
### Static Hosting
|
|
||||||
- **Netlify** - Recommended
|
|
||||||
- **Vercel** - Recommended
|
|
||||||
- **AWS S3 + CloudFront**
|
|
||||||
- **Azure Static Web Apps**
|
|
||||||
- **GitHub Pages**
|
|
||||||
|
|
||||||
### Container Deployment
|
|
||||||
- **Docker** - Nginx-based container
|
|
||||||
- **Kubernetes** - Scalable deployment
|
|
||||||
- **AWS ECS/Fargate**
|
|
||||||
- **Google Cloud Run**
|
|
||||||
|
|
||||||
### CDN
|
|
||||||
- **CloudFlare** - Recommended for caching and security
|
|
||||||
- **AWS CloudFront**
|
|
||||||
- **Fastly**
|
|
||||||
|
|
||||||
## Monitoring & Analytics (Optional)
|
|
||||||
|
|
||||||
### Error Tracking
|
|
||||||
- **Sentry** - Error monitoring
|
|
||||||
- **LogRocket** - Session replay
|
|
||||||
- **Rollbar** - Error tracking
|
|
||||||
|
|
||||||
### Analytics
|
|
||||||
- **Google Analytics 4**
|
|
||||||
- **Mixpanel** - Product analytics
|
|
||||||
- **Amplitude** - User behavior
|
|
||||||
|
|
||||||
### Performance Monitoring
|
|
||||||
- **Lighthouse** - Performance audits
|
|
||||||
- **Web Vitals** - Core metrics
|
|
||||||
- **New Relic** - APM
|
|
||||||
|
|
||||||
## Security Tools
|
|
||||||
|
|
||||||
### Dependency Scanning
|
|
||||||
- `npm audit` - Vulnerability scanning
|
|
||||||
- **Snyk** - Continuous security monitoring
|
|
||||||
- **Dependabot** - Automated updates
|
|
||||||
|
|
||||||
### Code Quality
|
|
||||||
- **SonarQube** - Code quality and security
|
|
||||||
- **CodeQL** - Security analysis
|
|
||||||
|
|
||||||
## Testing (Not Yet Implemented)
|
|
||||||
|
|
||||||
### Recommended Testing Stack
|
|
||||||
- **Vitest** - Unit testing (Vite-native)
|
|
||||||
- **React Testing Library** - Component testing
|
|
||||||
- **Playwright** - E2E testing
|
|
||||||
- **MSW** - API mocking
|
|
||||||
|
|
||||||
## Comparison with Alternatives
|
|
||||||
|
|
||||||
### Why React over Vue/Angular?
|
|
||||||
- Larger ecosystem
|
|
||||||
- Better TypeScript support
|
|
||||||
- More job opportunities
|
|
||||||
- Flexible architecture
|
|
||||||
- Strong community
|
|
||||||
|
|
||||||
### Why Vite over Webpack/CRA?
|
|
||||||
- 10-100x faster HMR
|
|
||||||
- Faster cold starts
|
|
||||||
- Better developer experience
|
|
||||||
- Modern ES modules
|
|
||||||
- Smaller bundle sizes
|
|
||||||
|
|
||||||
### Why Tailwind over CSS-in-JS?
|
|
||||||
- Better performance (no runtime)
|
|
||||||
- Smaller bundle size
|
|
||||||
- Easier to maintain
|
|
||||||
- Better IDE support
|
|
||||||
- Consistent design system
|
|
||||||
|
|
||||||
### Why React Query over Redux?
|
|
||||||
- Less boilerplate
|
|
||||||
- Automatic caching
|
|
||||||
- Better for server state
|
|
||||||
- Simpler API
|
|
||||||
- Built-in loading/error states
|
|
||||||
|
|
||||||
## Version History
|
|
||||||
|
|
||||||
| Package | Current | Latest Stable | Notes |
|
|
||||||
|---------|---------|---------------|-------|
|
|
||||||
| React | 19.2.0 | 19.2.0 | ✅ Latest |
|
|
||||||
| TypeScript | 5.9.3 | 5.9.x | ✅ Latest |
|
|
||||||
| Vite | 7.2.4 | 7.x | ✅ Latest |
|
|
||||||
| React Router | 7.11.0 | 7.x | ✅ Latest |
|
|
||||||
| TanStack Query | 5.90.12 | 5.x | ✅ Latest |
|
|
||||||
| Tailwind CSS | 3.4.17 | 3.x | ✅ Latest |
|
|
||||||
|
|
||||||
## Future Considerations
|
|
||||||
|
|
||||||
### Potential Additions
|
|
||||||
- **React Hook Form** - Form management
|
|
||||||
- **Zod** - Schema validation
|
|
||||||
- **Zustand** - Lightweight state management
|
|
||||||
- **Framer Motion** - Advanced animations
|
|
||||||
- **i18next** - Internationalization
|
|
||||||
- **React Helmet** - SEO management
|
|
||||||
|
|
||||||
### Potential Upgrades
|
|
||||||
- **React 19 Features** - Use new concurrent features
|
|
||||||
- **Vite 6** - When stable
|
|
||||||
- **TypeScript 5.10** - When released
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- [React Docs](https://react.dev)
|
|
||||||
- [TypeScript Docs](https://www.typescriptlang.org/docs)
|
|
||||||
- [Vite Docs](https://vitejs.dev)
|
|
||||||
- [Tailwind CSS Docs](https://tailwindcss.com/docs)
|
|
||||||
- [React Router Docs](https://reactrouter.com)
|
|
||||||
- [TanStack Query Docs](https://tanstack.com/query)
|
|
||||||
- [Radix UI Docs](https://www.radix-ui.com)
|
|
||||||
|
|
||||||
### Learning Resources
|
|
||||||
- [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app)
|
|
||||||
- [Tailwind CSS Best Practices](https://tailwindcss.com/docs/reusing-styles)
|
|
||||||
- [React Query Tutorial](https://tanstack.com/query/latest/docs/framework/react/overview)
|
|
||||||
|
|
||||||
## License
|
|
||||||
Proprietary - All rights reserved
|
|
||||||
|
|
@ -1,484 +0,0 @@
|
||||||
# Troubleshooting Guide
|
|
||||||
|
|
||||||
## Common Issues and Solutions
|
|
||||||
|
|
||||||
### 1. ERR_CONNECTION_REFUSED
|
|
||||||
|
|
||||||
**Error:**
|
|
||||||
```
|
|
||||||
POST http://localhost:3000/api/v1/auth/login net::ERR_CONNECTION_REFUSED
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cause:** Backend server is not running or running on a different port.
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
|
|
||||||
#### A. Start Your Backend Server
|
|
||||||
```bash
|
|
||||||
# Navigate to backend directory
|
|
||||||
cd path/to/backend
|
|
||||||
|
|
||||||
# Start the server
|
|
||||||
npm run dev
|
|
||||||
# or
|
|
||||||
npm start
|
|
||||||
# or
|
|
||||||
node server.js
|
|
||||||
# or
|
|
||||||
python manage.py runserver # Django
|
|
||||||
# or
|
|
||||||
php artisan serve # Laravel
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Check Backend Port
|
|
||||||
1. Find which port your backend is running on
|
|
||||||
2. Update `.env` file:
|
|
||||||
```env
|
|
||||||
# If backend is on port 3001
|
|
||||||
VITE_API_URL=http://localhost:3001/api/v1
|
|
||||||
|
|
||||||
# If backend is on port 8000
|
|
||||||
VITE_API_URL=http://localhost:8000/api/v1
|
|
||||||
|
|
||||||
# If backend is on port 5000
|
|
||||||
VITE_API_URL=http://localhost:5000/api/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Restart your frontend:
|
|
||||||
```bash
|
|
||||||
# Stop the dev server (Ctrl+C)
|
|
||||||
# Start again
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
#### C. Verify Backend is Running
|
|
||||||
```bash
|
|
||||||
# Test if backend is accessible
|
|
||||||
curl http://localhost:3000/api/v1/auth/login
|
|
||||||
|
|
||||||
# Or open in browser
|
|
||||||
http://localhost:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
#### D. Check for Port Conflicts
|
|
||||||
```bash
|
|
||||||
# Windows - Check what's using port 3000
|
|
||||||
netstat -ano | findstr :3000
|
|
||||||
|
|
||||||
# Kill process if needed (replace PID)
|
|
||||||
taskkill /PID <PID> /F
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. CORS Error
|
|
||||||
|
|
||||||
**Error:**
|
|
||||||
```
|
|
||||||
Access to XMLHttpRequest at 'http://localhost:3000/api/v1/auth/login'
|
|
||||||
from origin 'http://localhost:5173' has been blocked by CORS policy
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cause:** Backend not configured to allow requests from frontend.
|
|
||||||
|
|
||||||
**Solution:** Configure CORS on backend
|
|
||||||
|
|
||||||
**Node.js/Express:**
|
|
||||||
```javascript
|
|
||||||
const cors = require('cors')
|
|
||||||
|
|
||||||
app.use(cors({
|
|
||||||
origin: 'http://localhost:5173', // Your frontend URL
|
|
||||||
credentials: true
|
|
||||||
}))
|
|
||||||
```
|
|
||||||
|
|
||||||
**Django:**
|
|
||||||
```python
|
|
||||||
# settings.py
|
|
||||||
CORS_ALLOWED_ORIGINS = [
|
|
||||||
"http://localhost:5173",
|
|
||||||
]
|
|
||||||
CORS_ALLOW_CREDENTIALS = True
|
|
||||||
```
|
|
||||||
|
|
||||||
**Laravel:**
|
|
||||||
```php
|
|
||||||
// config/cors.php
|
|
||||||
'allowed_origins' => ['http://localhost:5173'],
|
|
||||||
'supports_credentials' => true,
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. 404 Not Found
|
|
||||||
|
|
||||||
**Error:**
|
|
||||||
```
|
|
||||||
POST http://localhost:3000/api/v1/auth/login 404 (Not Found)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cause:** Backend endpoint doesn't exist or path is wrong.
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
|
|
||||||
#### A. Verify Backend Route
|
|
||||||
Check if your backend has the login route:
|
|
||||||
```javascript
|
|
||||||
// Should have something like:
|
|
||||||
app.post('/api/v1/auth/login', loginController)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Check API Path
|
|
||||||
Your backend might use a different path:
|
|
||||||
```env
|
|
||||||
# If backend uses /api/auth/login
|
|
||||||
VITE_API_URL=http://localhost:3000/api
|
|
||||||
|
|
||||||
# If backend uses /auth/login
|
|
||||||
VITE_API_URL=http://localhost:3000
|
|
||||||
|
|
||||||
# If backend uses /v1/auth/login
|
|
||||||
VITE_API_URL=http://localhost:3000/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
#### C. Test Backend Directly
|
|
||||||
```bash
|
|
||||||
# Test with curl
|
|
||||||
curl -X POST http://localhost:3000/api/v1/auth/login \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"email":"test@example.com","password":"test123"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. 401 Unauthorized
|
|
||||||
|
|
||||||
**Error:**
|
|
||||||
```
|
|
||||||
POST http://localhost:3000/api/v1/auth/login 401 (Unauthorized)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cause:** Invalid credentials or backend authentication issue.
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
|
|
||||||
#### A. Check Credentials
|
|
||||||
- Verify email/password are correct
|
|
||||||
- Check if user exists in database
|
|
||||||
- Verify user is active
|
|
||||||
|
|
||||||
#### B. Check Backend Password Hashing
|
|
||||||
```javascript
|
|
||||||
// Backend should compare hashed passwords
|
|
||||||
const isValid = await bcrypt.compare(password, user.hashedPassword)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### C. Check Database
|
|
||||||
```sql
|
|
||||||
-- Verify user exists
|
|
||||||
SELECT * FROM users WHERE email = 'admin@example.com';
|
|
||||||
|
|
||||||
-- Check if password is hashed
|
|
||||||
SELECT password FROM users WHERE email = 'admin@example.com';
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. 403 Forbidden
|
|
||||||
|
|
||||||
**Error:**
|
|
||||||
```
|
|
||||||
POST http://localhost:3000/api/v1/auth/login 403 (Forbidden)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cause:** User doesn't have admin role or account is inactive.
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
|
|
||||||
#### A. Check User Role
|
|
||||||
```sql
|
|
||||||
-- Update user role to ADMIN
|
|
||||||
UPDATE users SET role = 'ADMIN' WHERE email = 'admin@example.com';
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Check Active Status
|
|
||||||
```sql
|
|
||||||
-- Activate user account
|
|
||||||
UPDATE users SET is_active = true WHERE email = 'admin@example.com';
|
|
||||||
```
|
|
||||||
|
|
||||||
#### C. Frontend Validation
|
|
||||||
The frontend checks `user.role === 'ADMIN'`. Make sure backend returns correct role.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. Network Error (No Response)
|
|
||||||
|
|
||||||
**Error:**
|
|
||||||
```
|
|
||||||
Network Error
|
|
||||||
```
|
|
||||||
|
|
||||||
**Causes & Solutions:**
|
|
||||||
|
|
||||||
#### A. Backend Crashed
|
|
||||||
Check backend console for errors and restart.
|
|
||||||
|
|
||||||
#### B. Firewall Blocking
|
|
||||||
Temporarily disable firewall or add exception.
|
|
||||||
|
|
||||||
#### C. Wrong Protocol
|
|
||||||
```env
|
|
||||||
# Use http for local development
|
|
||||||
VITE_API_URL=http://localhost:3000/api/v1
|
|
||||||
|
|
||||||
# NOT https
|
|
||||||
# VITE_API_URL=https://localhost:3000/api/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. Environment Variables Not Loading
|
|
||||||
|
|
||||||
**Error:**
|
|
||||||
API calls go to wrong URL or undefined.
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
|
|
||||||
#### A. Create .env File
|
|
||||||
```bash
|
|
||||||
# Copy example file
|
|
||||||
cp .env.example .env
|
|
||||||
|
|
||||||
# Edit with your values
|
|
||||||
VITE_API_URL=http://localhost:3000/api/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Restart Dev Server
|
|
||||||
```bash
|
|
||||||
# Stop server (Ctrl+C)
|
|
||||||
# Start again
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
#### C. Check Variable Name
|
|
||||||
Must start with `VITE_`:
|
|
||||||
```env
|
|
||||||
# ✅ Correct
|
|
||||||
VITE_API_URL=http://localhost:3000/api/v1
|
|
||||||
|
|
||||||
# ❌ Wrong (won't work)
|
|
||||||
API_URL=http://localhost:3000/api/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
#### D. Access in Code
|
|
||||||
```typescript
|
|
||||||
// ✅ Correct
|
|
||||||
import.meta.env.VITE_API_URL
|
|
||||||
|
|
||||||
// ❌ Wrong
|
|
||||||
process.env.VITE_API_URL
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 8. Token Not Persisting
|
|
||||||
|
|
||||||
**Error:**
|
|
||||||
User logged out after page refresh.
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
|
|
||||||
#### A. Check localStorage
|
|
||||||
```javascript
|
|
||||||
// Open browser console
|
|
||||||
localStorage.getItem('access_token')
|
|
||||||
localStorage.getItem('user')
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Check Cookie Settings
|
|
||||||
If using httpOnly cookies, check browser DevTools > Application > Cookies.
|
|
||||||
|
|
||||||
#### C. Backend Must Return Token
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"access_token": "...",
|
|
||||||
"user": { ... }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 9. Infinite Redirect Loop
|
|
||||||
|
|
||||||
**Error:**
|
|
||||||
Page keeps redirecting between login and dashboard.
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
|
|
||||||
#### A. Check ProtectedRoute Logic
|
|
||||||
```typescript
|
|
||||||
// Should check for token
|
|
||||||
const token = localStorage.getItem('access_token')
|
|
||||||
if (!token) {
|
|
||||||
return <Navigate to="/login" />
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Clear localStorage
|
|
||||||
```javascript
|
|
||||||
// Browser console
|
|
||||||
localStorage.clear()
|
|
||||||
// Then try logging in again
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 10. Tests Hanging
|
|
||||||
|
|
||||||
**Error:**
|
|
||||||
Tests run forever without completing.
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
|
|
||||||
#### A. Add Timeout
|
|
||||||
```typescript
|
|
||||||
// In test file
|
|
||||||
import { vi } from 'vitest'
|
|
||||||
|
|
||||||
vi.setConfig({ testTimeout: 10000 })
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Mock Timers
|
|
||||||
```typescript
|
|
||||||
vi.useFakeTimers()
|
|
||||||
// ... test code
|
|
||||||
vi.useRealTimers()
|
|
||||||
```
|
|
||||||
|
|
||||||
#### C. Check for Unresolved Promises
|
|
||||||
Make sure all async operations complete.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Debugging Tips
|
|
||||||
|
|
||||||
### 1. Check Browser Console
|
|
||||||
Press F12 and look for errors in Console tab.
|
|
||||||
|
|
||||||
### 2. Check Network Tab
|
|
||||||
1. Press F12
|
|
||||||
2. Go to Network tab
|
|
||||||
3. Try logging in
|
|
||||||
4. Click on the failed request
|
|
||||||
5. Check:
|
|
||||||
- Request URL
|
|
||||||
- Request Headers
|
|
||||||
- Request Payload
|
|
||||||
- Response
|
|
||||||
|
|
||||||
### 3. Check Backend Logs
|
|
||||||
Look at your backend console for error messages.
|
|
||||||
|
|
||||||
### 4. Test Backend Independently
|
|
||||||
Use curl or Postman to test backend without frontend.
|
|
||||||
|
|
||||||
### 5. Verify Environment Variables
|
|
||||||
```bash
|
|
||||||
# Check if .env file exists
|
|
||||||
ls -la .env
|
|
||||||
|
|
||||||
# Check contents
|
|
||||||
cat .env
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Clear Browser Cache
|
|
||||||
Sometimes old cached files cause issues:
|
|
||||||
1. Press Ctrl+Shift+Delete
|
|
||||||
2. Clear cache and cookies
|
|
||||||
3. Restart browser
|
|
||||||
|
|
||||||
### 7. Check Node Version
|
|
||||||
```bash
|
|
||||||
node --version
|
|
||||||
# Should be 18.x or 20.x
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Checklist
|
|
||||||
|
|
||||||
Before asking for help, verify:
|
|
||||||
|
|
||||||
- [ ] Backend server is running
|
|
||||||
- [ ] Backend is on correct port
|
|
||||||
- [ ] `.env` file exists with correct API URL
|
|
||||||
- [ ] Frontend dev server restarted after .env changes
|
|
||||||
- [ ] CORS configured on backend
|
|
||||||
- [ ] Login endpoint exists on backend
|
|
||||||
- [ ] Test user exists in database
|
|
||||||
- [ ] User has ADMIN role
|
|
||||||
- [ ] User account is active
|
|
||||||
- [ ] Browser console shows no errors
|
|
||||||
- [ ] Network tab shows request details
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Getting Help
|
|
||||||
|
|
||||||
If still stuck:
|
|
||||||
|
|
||||||
1. **Check Documentation**
|
|
||||||
- [Authentication Setup](./AUTHENTICATION.md)
|
|
||||||
- [Login API Documentation](./LOGIN_API_DOCUMENTATION.md)
|
|
||||||
- [API Standards](./API_STANDARDS.md)
|
|
||||||
|
|
||||||
2. **Gather Information**
|
|
||||||
- Error message
|
|
||||||
- Browser console logs
|
|
||||||
- Network tab details
|
|
||||||
- Backend logs
|
|
||||||
- Environment variables
|
|
||||||
|
|
||||||
3. **Test Systematically**
|
|
||||||
- Test backend with curl
|
|
||||||
- Test with Postman
|
|
||||||
- Check database directly
|
|
||||||
- Verify each step
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Development Setup
|
|
||||||
|
|
||||||
### Typical Setup
|
|
||||||
```
|
|
||||||
Frontend: http://localhost:5173 (Vite)
|
|
||||||
Backend: http://localhost:3000 (Node.js)
|
|
||||||
Database: localhost:5432 (PostgreSQL)
|
|
||||||
```
|
|
||||||
|
|
||||||
### .env Configuration
|
|
||||||
```env
|
|
||||||
VITE_API_URL=http://localhost:3000/api/v1
|
|
||||||
VITE_ENV=development
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backend CORS
|
|
||||||
```javascript
|
|
||||||
app.use(cors({
|
|
||||||
origin: 'http://localhost:5173',
|
|
||||||
credentials: true
|
|
||||||
}))
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Login
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3000/api/v1/auth/login \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"email":"admin@example.com","password":"admin123"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated:** 2024
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { authService } from "@/services"
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
email: string
|
email: string
|
||||||
|
|
@ -70,8 +70,8 @@ export function AppShell() {
|
||||||
return item?.label || "Admin Panel"
|
return item?.label || "Admin Panel"
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = async () => {
|
||||||
adminApiHelpers.logout()
|
await authService.logout()
|
||||||
navigate('/login', { replace: true })
|
navigate('/login', { replace: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,335 +0,0 @@
|
||||||
import axios, { type AxiosInstance, type AxiosError } from 'axios';
|
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_API_URL || 'http://localhost:3000/api/v1';
|
|
||||||
|
|
||||||
// Create separate axios instance for public endpoints (no auth required)
|
|
||||||
const publicApi: AxiosInstance = axios.create({
|
|
||||||
baseURL: API_BASE_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
withCredentials: true, // Important: Send cookies with requests
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create axios instance for authenticated endpoints
|
|
||||||
const adminApi: AxiosInstance = axios.create({
|
|
||||||
baseURL: API_BASE_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
withCredentials: true, // Important: Send cookies with requests
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add token interceptor for localStorage fallback (if not using cookies)
|
|
||||||
adminApi.interceptors.request.use(
|
|
||||||
(config) => {
|
|
||||||
// Only add Authorization header if token exists in localStorage
|
|
||||||
// (This is fallback - cookies are preferred)
|
|
||||||
const token = localStorage.getItem('access_token');
|
|
||||||
if (token) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add response interceptor for error handling
|
|
||||||
adminApi.interceptors.response.use(
|
|
||||||
(response) => response,
|
|
||||||
async (error: AxiosError<{ message?: string }>) => {
|
|
||||||
const originalRequest = error.config as any;
|
|
||||||
|
|
||||||
// Handle 401 Unauthorized
|
|
||||||
if (error.response?.status === 401) {
|
|
||||||
// Don't redirect if already on login page
|
|
||||||
if (window.location.pathname.includes('/login')) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to refresh token if not already retrying
|
|
||||||
if (!originalRequest._retry) {
|
|
||||||
originalRequest._retry = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Attempt token refresh
|
|
||||||
await adminApiHelpers.refreshToken();
|
|
||||||
// Retry original request
|
|
||||||
return adminApi(originalRequest);
|
|
||||||
} catch (refreshError) {
|
|
||||||
// Refresh failed, logout user
|
|
||||||
adminApiHelpers.logout();
|
|
||||||
window.location.href = '/login';
|
|
||||||
return Promise.reject(refreshError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If retry failed, logout
|
|
||||||
adminApiHelpers.logout();
|
|
||||||
window.location.href = '/login';
|
|
||||||
} else if (error.response?.status === 403) {
|
|
||||||
// Show access denied
|
|
||||||
const message = error.response?.data?.message || 'You do not have permission to access this resource';
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
import('sonner').then(({ toast }) => {
|
|
||||||
toast.error(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (error.response?.status === 404) {
|
|
||||||
const message = error.response?.data?.message || 'Resource not found';
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
import('sonner').then(({ toast }) => {
|
|
||||||
toast.error(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (error.response?.status === 500) {
|
|
||||||
const message = error.response?.data?.message || 'Server error occurred. Please try again later.';
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
import('sonner').then(({ toast }) => {
|
|
||||||
toast.error(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (!error.response) {
|
|
||||||
// Network error
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
import('sonner').then(({ toast }) => {
|
|
||||||
toast.error('Network error. Please check your connection.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// API helper functions
|
|
||||||
export const adminApiHelpers = {
|
|
||||||
// Auth - uses publicApi (no token required)
|
|
||||||
login: (data: { email: string; password: string }) =>
|
|
||||||
publicApi.post('/auth/login', data),
|
|
||||||
|
|
||||||
logout: async () => {
|
|
||||||
try {
|
|
||||||
// Call backend logout to clear httpOnly cookies
|
|
||||||
await adminApi.post('/auth/logout');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout error:', error);
|
|
||||||
} finally {
|
|
||||||
// Always clear localStorage
|
|
||||||
localStorage.removeItem('access_token');
|
|
||||||
localStorage.removeItem('user');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
refreshToken: () => adminApi.post('/auth/refresh'),
|
|
||||||
|
|
||||||
getCurrentUser: () => adminApi.get('/auth/me'),
|
|
||||||
|
|
||||||
// Users
|
|
||||||
getUsers: (params?: {
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
role?: string;
|
|
||||||
isActive?: boolean;
|
|
||||||
search?: string;
|
|
||||||
}) => adminApi.get('/admin/users', { params }),
|
|
||||||
|
|
||||||
getUser: (id: string) => adminApi.get(`/admin/users/${id}`),
|
|
||||||
|
|
||||||
getUserActivity: (id: string, days: number = 30) =>
|
|
||||||
adminApi.get(`/admin/users/${id}/activity`, { params: { days } }),
|
|
||||||
|
|
||||||
updateUser: (id: string, data: {
|
|
||||||
role?: string;
|
|
||||||
isActive?: boolean;
|
|
||||||
firstName?: string;
|
|
||||||
lastName?: string;
|
|
||||||
}) => adminApi.put(`/admin/users/${id}`, data),
|
|
||||||
|
|
||||||
deleteUser: (id: string, hard: boolean = false) =>
|
|
||||||
adminApi.delete(`/admin/users/${id}?hard=${hard}`),
|
|
||||||
|
|
||||||
resetPassword: (id: string) =>
|
|
||||||
adminApi.post(`/admin/users/${id}/reset-password`),
|
|
||||||
|
|
||||||
exportUsers: (format: string = 'csv') =>
|
|
||||||
adminApi.post('/admin/users/export', null, { params: { format } }),
|
|
||||||
|
|
||||||
importUsers: (file: File) => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
return adminApi.post('/admin/users/import', formData, {
|
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Logs
|
|
||||||
getLogs: (params?: {
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
level?: string;
|
|
||||||
type?: string;
|
|
||||||
userId?: string;
|
|
||||||
startDate?: string;
|
|
||||||
endDate?: string;
|
|
||||||
search?: string;
|
|
||||||
minDuration?: number;
|
|
||||||
}) => adminApi.get('/admin/logs', { params }),
|
|
||||||
|
|
||||||
getErrorLogs: (params?: {
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
userId?: string;
|
|
||||||
startDate?: string;
|
|
||||||
endDate?: string;
|
|
||||||
}) => adminApi.get('/admin/logs/errors', { params }),
|
|
||||||
|
|
||||||
getAccessLogs: (params?: {
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
userId?: string;
|
|
||||||
startDate?: string;
|
|
||||||
endDate?: string;
|
|
||||||
}) => adminApi.get('/admin/logs/access', { params }),
|
|
||||||
|
|
||||||
getLogById: (id: string) => adminApi.get(`/admin/logs/${id}`),
|
|
||||||
|
|
||||||
getLogStats: (startDate?: string, endDate?: string) =>
|
|
||||||
adminApi.get('/admin/logs/stats/summary', { params: { startDate, endDate } }),
|
|
||||||
|
|
||||||
exportLogs: (params: {
|
|
||||||
format?: string;
|
|
||||||
level?: string;
|
|
||||||
startDate?: string;
|
|
||||||
endDate?: string;
|
|
||||||
}) => adminApi.post('/admin/logs/export', null, { params }),
|
|
||||||
|
|
||||||
cleanupLogs: (days: number = 30) =>
|
|
||||||
adminApi.post('/admin/logs/cleanup', null, { params: { days } }),
|
|
||||||
|
|
||||||
// Analytics
|
|
||||||
getOverview: () => adminApi.get('/admin/analytics/overview'),
|
|
||||||
|
|
||||||
getUserGrowth: (days: number = 30) =>
|
|
||||||
adminApi.get('/admin/analytics/users/growth', { params: { days } }),
|
|
||||||
|
|
||||||
getRevenue: (period: string = '30days') =>
|
|
||||||
adminApi.get('/admin/analytics/revenue', { params: { period } }),
|
|
||||||
|
|
||||||
getStorageAnalytics: () => adminApi.get('/admin/analytics/storage'),
|
|
||||||
|
|
||||||
getApiUsage: (days: number = 7) =>
|
|
||||||
adminApi.get('/admin/analytics/api-usage', { params: { days } }),
|
|
||||||
|
|
||||||
getErrorRate: (days: number = 7) =>
|
|
||||||
adminApi.get('/admin/analytics/error-rate', { params: { days } }),
|
|
||||||
|
|
||||||
// System
|
|
||||||
getHealth: () => adminApi.get('/admin/system/health'),
|
|
||||||
|
|
||||||
getSystemInfo: () => adminApi.get('/admin/system/info'),
|
|
||||||
|
|
||||||
getSettings: (category?: string) =>
|
|
||||||
adminApi.get('/admin/system/settings', { params: { category } }),
|
|
||||||
|
|
||||||
getSetting: (key: string) => adminApi.get(`/admin/system/settings/${key}`),
|
|
||||||
|
|
||||||
createSetting: (data: {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
category: string;
|
|
||||||
description?: string;
|
|
||||||
isPublic?: boolean;
|
|
||||||
}) => adminApi.post('/admin/system/settings', data),
|
|
||||||
|
|
||||||
updateSetting: (key: string, data: {
|
|
||||||
value: string;
|
|
||||||
description?: string;
|
|
||||||
isPublic?: boolean;
|
|
||||||
}) => adminApi.put(`/admin/system/settings/${key}`, data),
|
|
||||||
|
|
||||||
deleteSetting: (key: string) => adminApi.delete(`/admin/system/settings/${key}`),
|
|
||||||
|
|
||||||
// Maintenance
|
|
||||||
getMaintenanceStatus: () => adminApi.get('/admin/maintenance'),
|
|
||||||
|
|
||||||
enableMaintenance: (message?: string) =>
|
|
||||||
adminApi.post('/admin/maintenance/enable', { message }),
|
|
||||||
|
|
||||||
disableMaintenance: () => adminApi.post('/admin/maintenance/disable'),
|
|
||||||
|
|
||||||
// Announcements
|
|
||||||
getAnnouncements: (activeOnly: boolean = false) =>
|
|
||||||
adminApi.get('/admin/announcements', { params: { activeOnly } }),
|
|
||||||
|
|
||||||
createAnnouncement: (data: {
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
type?: string;
|
|
||||||
priority?: number;
|
|
||||||
targetAudience?: string;
|
|
||||||
startsAt?: string;
|
|
||||||
endsAt?: string;
|
|
||||||
}) => adminApi.post('/admin/announcements', data),
|
|
||||||
|
|
||||||
updateAnnouncement: (id: string, data: {
|
|
||||||
title?: string;
|
|
||||||
message?: string;
|
|
||||||
type?: string;
|
|
||||||
priority?: number;
|
|
||||||
targetAudience?: string;
|
|
||||||
startsAt?: string;
|
|
||||||
endsAt?: string;
|
|
||||||
}) => adminApi.put(`/admin/announcements/${id}`, data),
|
|
||||||
|
|
||||||
toggleAnnouncement: (id: string) =>
|
|
||||||
adminApi.patch(`/admin/announcements/${id}/toggle`),
|
|
||||||
|
|
||||||
deleteAnnouncement: (id: string) =>
|
|
||||||
adminApi.delete(`/admin/announcements/${id}`),
|
|
||||||
|
|
||||||
// Audit
|
|
||||||
getAuditLogs: (params?: {
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
userId?: string;
|
|
||||||
action?: string;
|
|
||||||
resourceType?: string;
|
|
||||||
resourceId?: string;
|
|
||||||
startDate?: string;
|
|
||||||
endDate?: string;
|
|
||||||
}) => adminApi.get('/admin/audit/logs', { params }),
|
|
||||||
|
|
||||||
getUserAuditActivity: (userId: string, days: number = 30) =>
|
|
||||||
adminApi.get(`/admin/audit/users/${userId}`, { params: { days } }),
|
|
||||||
|
|
||||||
getResourceHistory: (type: string, id: string) =>
|
|
||||||
adminApi.get(`/admin/audit/resource/${type}/${id}`),
|
|
||||||
|
|
||||||
getAuditStats: (startDate?: string, endDate?: string) =>
|
|
||||||
adminApi.get('/admin/audit/stats', { params: { startDate, endDate } }),
|
|
||||||
|
|
||||||
// Security
|
|
||||||
getFailedLogins: (params?: {
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
email?: string;
|
|
||||||
ipAddress?: string;
|
|
||||||
}) => adminApi.get('/admin/security/failed-logins', { params }),
|
|
||||||
|
|
||||||
getSuspiciousActivity: () => adminApi.get('/admin/security/suspicious-activity'),
|
|
||||||
|
|
||||||
getAllApiKeys: () => adminApi.get('/admin/security/api-keys'),
|
|
||||||
|
|
||||||
revokeApiKey: (id: string) =>
|
|
||||||
adminApi.patch(`/admin/security/api-keys/${id}/revoke`),
|
|
||||||
|
|
||||||
getRateLimitViolations: (days: number = 7) =>
|
|
||||||
adminApi.get('/admin/security/rate-limits', { params: { days } }),
|
|
||||||
|
|
||||||
getActiveSessions: () => adminApi.get('/admin/security/sessions'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default adminApi;
|
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Sentry } from './sentry'
|
import { Sentry } from './sentry'
|
||||||
import adminApi from './api-client'
|
import apiClient from '@/services/api/client'
|
||||||
|
|
||||||
interface ErrorLog {
|
interface ErrorLog {
|
||||||
message: string
|
message: string
|
||||||
|
|
@ -105,7 +105,7 @@ class ErrorTracker {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Send to your backend error logging endpoint
|
// Send to your backend error logging endpoint
|
||||||
await adminApi.post('/errors/log', errorLog)
|
await apiClient.post('/errors/log', errorLog)
|
||||||
this.queue.shift() // Remove from queue on success
|
this.queue.shift() // Remove from queue on success
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to log error to backend:', error)
|
console.error('Failed to log error to backend:', error)
|
||||||
|
|
|
||||||
|
|
@ -8,23 +8,17 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { analyticsService } from "@/services"
|
||||||
|
|
||||||
export default function AnalyticsApiPage() {
|
export default function AnalyticsApiPage() {
|
||||||
const { data: apiUsage, isLoading } = useQuery({
|
const { data: apiUsage, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'analytics', 'api-usage'],
|
queryKey: ['admin', 'analytics', 'api-usage'],
|
||||||
queryFn: async () => {
|
queryFn: () => analyticsService.getApiUsage(7),
|
||||||
const response = await adminApiHelpers.getApiUsage(7)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: errorRate, isLoading: errorRateLoading } = useQuery({
|
const { data: errorRate, isLoading: errorRateLoading } = useQuery({
|
||||||
queryKey: ['admin', 'analytics', 'error-rate'],
|
queryKey: ['admin', 'analytics', 'error-rate'],
|
||||||
queryFn: async () => {
|
queryFn: () => analyticsService.getErrorRate(7),
|
||||||
const response = await adminApiHelpers.getErrorRate(7)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { analyticsService } from "@/services"
|
||||||
|
|
||||||
export default function AnalyticsRevenuePage() {
|
export default function AnalyticsRevenuePage() {
|
||||||
const { data: revenue, isLoading } = useQuery({
|
const { data: revenue, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'analytics', 'revenue'],
|
queryKey: ['admin', 'analytics', 'revenue'],
|
||||||
queryFn: async () => {
|
queryFn: () => analyticsService.getRevenue('90days'),
|
||||||
const response = await adminApiHelpers.getRevenue('90days')
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,14 @@
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from "recharts"
|
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from "recharts"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { analyticsService } from "@/services"
|
||||||
|
|
||||||
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']
|
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']
|
||||||
|
|
||||||
export default function AnalyticsStoragePage() {
|
export default function AnalyticsStoragePage() {
|
||||||
const { data: storage, isLoading } = useQuery({
|
const { data: storage, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'analytics', 'storage'],
|
queryKey: ['admin', 'analytics', 'storage'],
|
||||||
queryFn: async () => {
|
queryFn: () => analyticsService.getStorageAnalytics(),
|
||||||
const response = await adminApiHelpers.getStorageAnalytics()
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const formatBytes = (bytes: number) => {
|
const formatBytes = (bytes: number) => {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { analyticsService } from "@/services"
|
||||||
|
|
||||||
export default function AnalyticsUsersPage() {
|
export default function AnalyticsUsersPage() {
|
||||||
const { data: userGrowth, isLoading } = useQuery({
|
const { data: userGrowth, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'analytics', 'users', 'growth'],
|
queryKey: ['admin', 'analytics', 'users', 'growth'],
|
||||||
queryFn: async () => {
|
queryFn: () => analyticsService.getUserGrowth(90),
|
||||||
const response = await adminApiHelpers.getUserGrowth(90)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Plus, Edit, Trash2 } from "lucide-react"
|
import { Plus, Edit, Trash2 } from "lucide-react"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { announcementService } from "@/services"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
|
||||||
|
|
@ -31,16 +31,11 @@ export default function AnnouncementsPage() {
|
||||||
|
|
||||||
const { data: announcements, isLoading } = useQuery({
|
const { data: announcements, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'announcements'],
|
queryKey: ['admin', 'announcements'],
|
||||||
queryFn: async () => {
|
queryFn: () => announcementService.getAnnouncements(false),
|
||||||
const response = await adminApiHelpers.getAnnouncements(false)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: async (id: string) => {
|
mutationFn: (id: string) => announcementService.deleteAnnouncement(id),
|
||||||
await adminApiHelpers.deleteAnnouncement(id)
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] })
|
||||||
toast.success("Announcement deleted successfully")
|
toast.success("Announcement deleted successfully")
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { Search, Eye } from "lucide-react"
|
import { Search, Eye } from "lucide-react"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { auditService } from "@/services"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
|
||||||
export default function AuditPage() {
|
export default function AuditPage() {
|
||||||
|
|
@ -26,8 +26,7 @@ export default function AuditPage() {
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const params: any = { page, limit }
|
const params: any = { page, limit }
|
||||||
if (search) params.search = search
|
if (search) params.search = search
|
||||||
const response = await adminApiHelpers.getAuditLogs(params)
|
return await auditService.getAuditLogs(params)
|
||||||
return response.data
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,49 +2,34 @@ import { useQuery } from "@tanstack/react-query"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Download, Users, FileText, DollarSign, HardDrive, AlertCircle } from "lucide-react"
|
import { Download, Users, FileText, DollarSign, HardDrive, AlertCircle } from "lucide-react"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { analyticsService, systemService } from "@/services"
|
||||||
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
|
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { data: overview, isLoading: overviewLoading } = useQuery({
|
const { data: overview, isLoading: overviewLoading } = useQuery({
|
||||||
queryKey: ['admin', 'analytics', 'overview'],
|
queryKey: ['admin', 'analytics', 'overview'],
|
||||||
queryFn: async () => {
|
queryFn: () => analyticsService.getOverview(),
|
||||||
const response = await adminApiHelpers.getOverview()
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: userGrowth, isLoading: growthLoading } = useQuery({
|
const { data: userGrowth, isLoading: growthLoading } = useQuery({
|
||||||
queryKey: ['admin', 'analytics', 'users', 'growth'],
|
queryKey: ['admin', 'analytics', 'users', 'growth'],
|
||||||
queryFn: async () => {
|
queryFn: () => analyticsService.getUserGrowth(30),
|
||||||
const response = await adminApiHelpers.getUserGrowth(30)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: revenue, isLoading: revenueLoading } = useQuery({
|
const { data: revenue, isLoading: revenueLoading } = useQuery({
|
||||||
queryKey: ['admin', 'analytics', 'revenue'],
|
queryKey: ['admin', 'analytics', 'revenue'],
|
||||||
queryFn: async () => {
|
queryFn: () => analyticsService.getRevenue('30days'),
|
||||||
const response = await adminApiHelpers.getRevenue('30days')
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: health, isLoading: healthLoading } = useQuery({
|
const { data: health, isLoading: healthLoading } = useQuery({
|
||||||
queryKey: ['admin', 'system', 'health'],
|
queryKey: ['admin', 'system', 'health'],
|
||||||
queryFn: async () => {
|
queryFn: () => systemService.getHealth(),
|
||||||
const response = await adminApiHelpers.getHealth()
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: errorRate, isLoading: errorRateLoading } = useQuery({
|
const { data: errorRate, isLoading: errorRateLoading } = useQuery({
|
||||||
queryKey: ['admin', 'analytics', 'error-rate'],
|
queryKey: ['admin', 'analytics', 'error-rate'],
|
||||||
queryFn: async () => {
|
queryFn: () => analyticsService.getErrorRate(7),
|
||||||
const response = await adminApiHelpers.getErrorRate(7)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,21 @@ import { useQuery } from "@tanstack/react-query"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { AlertCircle, CheckCircle, XCircle, Users } from "lucide-react"
|
import { AlertCircle, CheckCircle, XCircle, Users } from "lucide-react"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { systemService } from "@/services"
|
||||||
|
|
||||||
export default function HealthPage() {
|
export default function HealthPage() {
|
||||||
const { data: health, isLoading: healthLoading } = useQuery({
|
const { data: health, isLoading: healthLoading } = useQuery({
|
||||||
queryKey: ['admin', 'system', 'health'],
|
queryKey: ['admin', 'system', 'health'],
|
||||||
queryFn: async () => {
|
queryFn: () => systemService.getHealth(),
|
||||||
const response = await adminApiHelpers.getHealth()
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
refetchInterval: 30000, // Refetch every 30 seconds
|
refetchInterval: 30000, // Refetch every 30 seconds
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: systemInfo, isLoading: infoLoading } = useQuery({
|
const { data: systemInfo, isLoading: infoLoading } = useQuery({
|
||||||
queryKey: ['admin', 'system', 'info'],
|
queryKey: ['admin', 'system', 'info'],
|
||||||
queryFn: async () => {
|
queryFn: () => systemService.getSystemInfo(),
|
||||||
const response = await adminApiHelpers.getSystemInfo()
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
const getStatusIcon = (status?: string) => {
|
||||||
switch (status?.toLowerCase()) {
|
switch (status?.toLowerCase()) {
|
||||||
case 'healthy':
|
case 'healthy':
|
||||||
case 'connected':
|
case 'connected':
|
||||||
|
|
@ -142,19 +136,19 @@ export default function HealthPage() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Platform</p>
|
<p className="text-sm text-muted-foreground">Platform</p>
|
||||||
<p className="font-medium">{systemInfo.platform}</p>
|
<p className="font-medium">{systemInfo.platform || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Architecture</p>
|
<p className="text-sm text-muted-foreground">Architecture</p>
|
||||||
<p className="font-medium">{systemInfo.architecture}</p>
|
<p className="font-medium">{systemInfo.architecture || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Uptime</p>
|
<p className="text-sm text-muted-foreground">Uptime</p>
|
||||||
<p className="font-medium">{formatUptime(systemInfo.uptime)}</p>
|
<p className="font-medium">{formatUptime(systemInfo.uptime || 0)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Environment</p>
|
<p className="text-sm text-muted-foreground">Environment</p>
|
||||||
<p className="font-medium">{systemInfo.env}</p>
|
<p className="font-medium">{systemInfo.env || systemInfo.environment}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Memory Usage</p>
|
<p className="text-sm text-muted-foreground">Memory Usage</p>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { systemService } from "@/services"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
|
|
@ -14,16 +14,11 @@ export default function MaintenancePage() {
|
||||||
|
|
||||||
const { data: status, isLoading } = useQuery({
|
const { data: status, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'maintenance'],
|
queryKey: ['admin', 'maintenance'],
|
||||||
queryFn: async () => {
|
queryFn: () => systemService.getMaintenanceStatus(),
|
||||||
const response = await adminApiHelpers.getMaintenanceStatus()
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const enableMutation = useMutation({
|
const enableMutation = useMutation({
|
||||||
mutationFn: async (msg?: string) => {
|
mutationFn: (msg?: string) => systemService.enableMaintenance(msg),
|
||||||
await adminApiHelpers.enableMaintenance(msg)
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'maintenance'] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'maintenance'] })
|
||||||
toast.success("Maintenance mode enabled")
|
toast.success("Maintenance mode enabled")
|
||||||
|
|
@ -35,9 +30,7 @@ export default function MaintenancePage() {
|
||||||
})
|
})
|
||||||
|
|
||||||
const disableMutation = useMutation({
|
const disableMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: () => systemService.disableMaintenance(),
|
||||||
await adminApiHelpers.disableMaintenance()
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'maintenance'] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'maintenance'] })
|
||||||
toast.success("Maintenance mode disabled")
|
toast.success("Maintenance mode disabled")
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { Ban } from "lucide-react"
|
import { Ban } from "lucide-react"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { securityService } from "@/services"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
|
||||||
|
|
@ -20,16 +20,11 @@ export default function ApiKeysPage() {
|
||||||
|
|
||||||
const { data: apiKeys, isLoading } = useQuery({
|
const { data: apiKeys, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'security', 'api-keys'],
|
queryKey: ['admin', 'security', 'api-keys'],
|
||||||
queryFn: async () => {
|
queryFn: () => securityService.getAllApiKeys(),
|
||||||
const response = await adminApiHelpers.getAllApiKeys()
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const revokeMutation = useMutation({
|
const revokeMutation = useMutation({
|
||||||
mutationFn: async (id: string) => {
|
mutationFn: (id: string) => securityService.revokeApiKey(id),
|
||||||
await adminApiHelpers.revokeApiKey(id)
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'security', 'api-keys'] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'security', 'api-keys'] })
|
||||||
toast.success("API key revoked successfully")
|
toast.success("API key revoked successfully")
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { Search, Ban } from "lucide-react"
|
import { Search, Ban } from "lucide-react"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { securityService } from "@/services"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
|
||||||
export default function FailedLoginsPage() {
|
export default function FailedLoginsPage() {
|
||||||
|
|
@ -26,8 +26,7 @@ export default function FailedLoginsPage() {
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const params: any = { page, limit }
|
const params: any = { page, limit }
|
||||||
if (search) params.email = search
|
if (search) params.email = search
|
||||||
const response = await adminApiHelpers.getFailedLogins(params)
|
return await securityService.getFailedLogins(params)
|
||||||
return response.data
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,12 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { securityService } from "@/services"
|
||||||
|
|
||||||
export default function RateLimitsPage() {
|
export default function RateLimitsPage() {
|
||||||
const { data: violations, isLoading } = useQuery({
|
const { data: violations, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'security', 'rate-limits'],
|
queryKey: ['admin', 'security', 'rate-limits'],
|
||||||
queryFn: async () => {
|
queryFn: () => securityService.getRateLimitViolations(7),
|
||||||
const response = await adminApiHelpers.getRateLimitViolations(7)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,13 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { LogOut } from "lucide-react"
|
import { LogOut } from "lucide-react"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { securityService } from "@/services"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
|
||||||
export default function SessionsPage() {
|
export default function SessionsPage() {
|
||||||
const { data: sessions, isLoading } = useQuery({
|
const { data: sessions, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'security', 'sessions'],
|
queryKey: ['admin', 'security', 'sessions'],
|
||||||
queryFn: async () => {
|
queryFn: () => securityService.getActiveSessions(),
|
||||||
const response = await adminApiHelpers.getActiveSessions()
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,12 @@ import { useQuery } from "@tanstack/react-query"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Shield, Ban } from "lucide-react"
|
import { Shield, Ban } from "lucide-react"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { securityService } from "@/services"
|
||||||
|
|
||||||
export default function SuspiciousActivityPage() {
|
export default function SuspiciousActivityPage() {
|
||||||
const { data: suspicious, isLoading } = useQuery({
|
const { data: suspicious, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'security', 'suspicious'],
|
queryKey: ['admin', 'security', 'suspicious'],
|
||||||
queryFn: async () => {
|
queryFn: () => securityService.getSuspiciousActivity(),
|
||||||
const response = await adminApiHelpers.getSuspiciousActivity()
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -28,9 +25,9 @@ export default function SuspiciousActivityPage() {
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-center py-8">Loading...</div>
|
<div className="text-center py-8">Loading...</div>
|
||||||
) : suspicious?.suspiciousIPs?.length > 0 ? (
|
) : (suspicious?.suspiciousIPs?.length ?? 0) > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{suspicious.suspiciousIPs.map((ip: any, index: number) => (
|
{suspicious?.suspiciousIPs?.map((ip: any, index: number) => (
|
||||||
<div key={index} className="flex items-center justify-between p-2 border rounded">
|
<div key={index} className="flex items-center justify-between p-2 border rounded">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-mono font-medium">{ip.ipAddress}</p>
|
<p className="font-mono font-medium">{ip.ipAddress}</p>
|
||||||
|
|
@ -61,9 +58,9 @@ export default function SuspiciousActivityPage() {
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-center py-8">Loading...</div>
|
<div className="text-center py-8">Loading...</div>
|
||||||
) : suspicious?.suspiciousEmails?.length > 0 ? (
|
) : (suspicious?.suspiciousEmails?.length ?? 0) > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{suspicious.suspiciousEmails.map((email: any, index: number) => (
|
{suspicious?.suspiciousEmails?.map((email: any, index: number) => (
|
||||||
<div key={index} className="flex items-center justify-between p-2 border rounded">
|
<div key={index} className="flex items-center justify-between p-2 border rounded">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{email.email}</p>
|
<p className="font-medium">{email.email}</p>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Plus } from "lucide-react"
|
import { Plus } from "lucide-react"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { settingsService } from "@/services"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
|
|
@ -31,16 +31,12 @@ export default function SettingsPage() {
|
||||||
|
|
||||||
const { data: settings, isLoading } = useQuery({
|
const { data: settings, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'settings', selectedCategory],
|
queryKey: ['admin', 'settings', selectedCategory],
|
||||||
queryFn: async () => {
|
queryFn: () => settingsService.getSettings(selectedCategory),
|
||||||
const response = await adminApiHelpers.getSettings(selectedCategory)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateSettingMutation = useMutation({
|
const updateSettingMutation = useMutation({
|
||||||
mutationFn: async ({ key, value }: { key: string; value: string }) => {
|
mutationFn: ({ key, value }: { key: string; value: string }) =>
|
||||||
await adminApiHelpers.updateSetting(key, { value })
|
settingsService.updateSetting(key, { value }),
|
||||||
},
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] })
|
||||||
toast.success("Setting updated successfully")
|
toast.success("Setting updated successfully")
|
||||||
|
|
@ -51,15 +47,13 @@ export default function SettingsPage() {
|
||||||
})
|
})
|
||||||
|
|
||||||
const createSettingMutation = useMutation({
|
const createSettingMutation = useMutation({
|
||||||
mutationFn: async (data: {
|
mutationFn: (data: {
|
||||||
key: string
|
key: string
|
||||||
value: string
|
value: string
|
||||||
category: string
|
category: string
|
||||||
description?: string
|
description?: string
|
||||||
isPublic?: boolean
|
isPublic?: boolean
|
||||||
}) => {
|
}) => settingsService.createSetting(data),
|
||||||
await adminApiHelpers.createSetting(data)
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] })
|
||||||
toast.success("Setting created successfully")
|
toast.success("Setting created successfully")
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { ArrowLeft } from "lucide-react"
|
import { ArrowLeft } from "lucide-react"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { userService } from "@/services"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
|
||||||
export default function UserActivityPage() {
|
export default function UserActivityPage() {
|
||||||
|
|
@ -12,10 +12,7 @@ export default function UserActivityPage() {
|
||||||
|
|
||||||
const { data: activity, isLoading } = useQuery({
|
const { data: activity, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'users', id, 'activity'],
|
queryKey: ['admin', 'users', id, 'activity'],
|
||||||
queryFn: async () => {
|
queryFn: () => userService.getUserActivity(id!, 30),
|
||||||
const response = await adminApiHelpers.getUserActivity(id!, 30)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import { ArrowLeft, Edit, Key } from "lucide-react"
|
import { ArrowLeft, Edit, Key } from "lucide-react"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { userService } from "@/services"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
|
||||||
export default function UserDetailsPage() {
|
export default function UserDetailsPage() {
|
||||||
|
|
@ -14,10 +14,7 @@ export default function UserDetailsPage() {
|
||||||
|
|
||||||
const { data: user, isLoading } = useQuery({
|
const { data: user, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'users', id],
|
queryKey: ['admin', 'users', id],
|
||||||
queryFn: async () => {
|
queryFn: () => userService.getUser(id!),
|
||||||
const response = await adminApiHelpers.getUser(id!)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -90,7 +87,9 @@ export default function UserDetailsPage() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Updated At</p>
|
<p className="text-sm text-muted-foreground">Updated At</p>
|
||||||
<p className="font-medium">{format(new Date(user.updatedAt), 'PPpp')}</p>
|
<p className="font-medium">
|
||||||
|
{user.updatedAt ? format(new Date(user.updatedAt), 'PPpp') : 'N/A'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Search, Download, Eye, UserPlus, Trash2, Key, Upload } from "lucide-react"
|
import { Search, Download, Eye, UserPlus, Trash2, Key, Upload } from "lucide-react"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { userService } from "@/services"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
|
||||||
|
|
@ -55,15 +55,13 @@ export default function UsersPage() {
|
||||||
if (search) params.search = search
|
if (search) params.search = search
|
||||||
if (roleFilter !== 'all') params.role = roleFilter
|
if (roleFilter !== 'all') params.role = roleFilter
|
||||||
if (statusFilter !== 'all') params.isActive = statusFilter === 'active'
|
if (statusFilter !== 'all') params.isActive = statusFilter === 'active'
|
||||||
const response = await adminApiHelpers.getUsers(params)
|
return await userService.getUsers(params)
|
||||||
return response.data
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteUserMutation = useMutation({
|
const deleteUserMutation = useMutation({
|
||||||
mutationFn: async ({ id, hard }: { id: string; hard: boolean }) => {
|
mutationFn: ({ id, hard }: { id: string; hard: boolean }) =>
|
||||||
await adminApiHelpers.deleteUser(id, hard)
|
userService.deleteUser(id, hard),
|
||||||
},
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] })
|
||||||
toast.success("User deleted successfully")
|
toast.success("User deleted successfully")
|
||||||
|
|
@ -75,10 +73,7 @@ export default function UsersPage() {
|
||||||
})
|
})
|
||||||
|
|
||||||
const resetPasswordMutation = useMutation({
|
const resetPasswordMutation = useMutation({
|
||||||
mutationFn: async (id: string) => {
|
mutationFn: (id: string) => userService.resetPassword(id),
|
||||||
const response = await adminApiHelpers.resetPassword(id)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
toast.success(`Password reset. Temporary password: ${data.temporaryPassword}`)
|
toast.success(`Password reset. Temporary password: ${data.temporaryPassword}`)
|
||||||
setResetPasswordDialogOpen(false)
|
setResetPasswordDialogOpen(false)
|
||||||
|
|
@ -89,18 +84,12 @@ export default function UsersPage() {
|
||||||
})
|
})
|
||||||
|
|
||||||
const importUsersMutation = useMutation({
|
const importUsersMutation = useMutation({
|
||||||
mutationFn: async (file: File) => {
|
mutationFn: (file: File) => userService.importUsers(file),
|
||||||
const response = await adminApiHelpers.importUsers(file)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] })
|
||||||
toast.success(`Imported ${data.success} users. ${data.failed} failed.`)
|
toast.success(`Imported ${data.imported} users. ${data.failed} failed.`)
|
||||||
setImportDialogOpen(false)
|
setImportDialogOpen(false)
|
||||||
setImportFile(null)
|
setImportFile(null)
|
||||||
if (data.errors && data.errors.length > 0) {
|
|
||||||
console.error('Import errors:', data.errors)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
toast.error(error.response?.data?.message || "Failed to import users")
|
toast.error(error.response?.data?.message || "Failed to import users")
|
||||||
|
|
@ -109,8 +98,7 @@ export default function UsersPage() {
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await adminApiHelpers.exportUsers('csv')
|
const blob = await userService.exportUsers('csv')
|
||||||
const blob = new Blob([response.data], { type: 'text/csv' })
|
|
||||||
const url = window.URL.createObjectURL(blob)
|
const url = window.URL.createObjectURL(blob)
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
a.href = url
|
a.href = url
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,175 @@
|
||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Download } from "lucide-react"
|
import { Download, FileText, DollarSign, CreditCard, TrendingUp } from "lucide-react"
|
||||||
|
import { dashboardService } from "@/services"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
|
const { data: profile } = useQuery({
|
||||||
|
queryKey: ['user', 'profile'],
|
||||||
|
queryFn: () => dashboardService.getUserProfile(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: stats, isLoading } = useQuery({
|
||||||
|
queryKey: ['user', 'stats'],
|
||||||
|
queryFn: () => dashboardService.getUserStats(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
toast.success("Exporting your data...")
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatCurrency = (amount: number) => {
|
||||||
|
return new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD',
|
||||||
|
}).format(amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGreeting = () => {
|
||||||
|
const hour = new Date().getHours()
|
||||||
|
if (hour < 12) return 'Good Morning'
|
||||||
|
if (hour < 18) return 'Good Afternoon'
|
||||||
|
return 'Good Evening'
|
||||||
|
}
|
||||||
|
|
||||||
|
const userName = profile ? `${profile.firstName} ${profile.lastName}` : 'User'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-3xl font-bold">Good Morning, Admin</h2>
|
<h2 className="text-3xl font-bold">{getGreeting()}, {userName}</h2>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
01 Sep - 15 Sep 2024
|
{new Date().toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric'
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline">
|
<Button variant="outline" onClick={handleExport}>
|
||||||
<Download className="w-4 h-4 mr-2" />
|
<Download className="w-4 h-4 mr-2" />
|
||||||
Export Data
|
Export Data
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle>Welcome to Dashboard</CardTitle>
|
<CardTitle className="text-sm font-medium">Total Invoices</CardTitle>
|
||||||
|
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-muted-foreground">
|
{isLoading ? (
|
||||||
This is your main dashboard page.
|
<div className="text-2xl font-bold">...</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-2xl font-bold">{stats?.totalInvoices || 0}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{stats?.pendingInvoices || 0} pending
|
||||||
</p>
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Total Transactions</CardTitle>
|
||||||
|
<CreditCard className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="text-2xl font-bold">...</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-2xl font-bold">{stats?.totalTransactions || 0}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
All time transactions
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
|
||||||
|
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="text-2xl font-bold">...</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{formatCurrency(stats?.totalRevenue || 0)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Total earnings
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Growth</CardTitle>
|
||||||
|
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="text-2xl font-bold">...</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{stats?.growthPercentage !== undefined
|
||||||
|
? `${stats.growthPercentage > 0 ? '+' : ''}${stats.growthPercentage.toFixed(1)}%`
|
||||||
|
: 'N/A'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
vs last month
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{stats?.recentActivity && stats.recentActivity.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Recent Activity</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{stats.recentActivity.map((activity) => (
|
||||||
|
<div key={activity.id} className="flex items-center justify-between border-b pb-3 last:border-0">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{activity.description}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{new Date(activity.date).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{activity.amount && (
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="font-semibold">{formatCurrency(activity.amount)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ import { describe, it, expect, vi } from 'vitest'
|
||||||
import { render, screen, waitFor } from '@/test/test-utils'
|
import { render, screen, waitFor } from '@/test/test-utils'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import LoginPage from '../index'
|
import LoginPage from '../index'
|
||||||
import { adminApiHelpers } from '@/lib/api-client'
|
import { authService } from '@/services'
|
||||||
|
|
||||||
// Mock the API client
|
// Mock the service layer
|
||||||
vi.mock('@/lib/api-client', () => ({
|
vi.mock('@/services', () => ({
|
||||||
adminApiHelpers: {
|
authService: {
|
||||||
login: vi.fn(),
|
login: vi.fn(),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
@ -52,11 +52,11 @@ describe('LoginPage', () => {
|
||||||
|
|
||||||
it('should handle form submission', async () => {
|
it('should handle form submission', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
const mockLogin = vi.mocked(adminApiHelpers.login)
|
const mockLogin = vi.mocked(authService.login)
|
||||||
|
|
||||||
mockLogin.mockResolvedValue({
|
mockLogin.mockResolvedValue({
|
||||||
data: {
|
accessToken: 'fake-token',
|
||||||
access_token: 'fake-token',
|
refreshToken: 'fake-refresh-token',
|
||||||
user: {
|
user: {
|
||||||
id: '1',
|
id: '1',
|
||||||
email: 'admin@example.com',
|
email: 'admin@example.com',
|
||||||
|
|
@ -64,8 +64,7 @@ describe('LoginPage', () => {
|
||||||
firstName: 'Admin',
|
firstName: 'Admin',
|
||||||
lastName: 'User',
|
lastName: 'User',
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
} as any)
|
|
||||||
|
|
||||||
render(<LoginPage />)
|
render(<LoginPage />)
|
||||||
|
|
||||||
|
|
@ -87,18 +86,19 @@ describe('LoginPage', () => {
|
||||||
|
|
||||||
it('should show error for non-admin users', async () => {
|
it('should show error for non-admin users', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
const mockLogin = vi.mocked(adminApiHelpers.login)
|
const mockLogin = vi.mocked(authService.login)
|
||||||
|
|
||||||
mockLogin.mockResolvedValue({
|
mockLogin.mockResolvedValue({
|
||||||
data: {
|
accessToken: 'fake-token',
|
||||||
access_token: 'fake-token',
|
refreshToken: 'fake-refresh-token',
|
||||||
user: {
|
user: {
|
||||||
id: '1',
|
id: '1',
|
||||||
email: 'user@example.com',
|
email: 'user@example.com',
|
||||||
|
firstName: 'User',
|
||||||
|
lastName: 'Test',
|
||||||
role: 'USER', // Not ADMIN
|
role: 'USER', // Not ADMIN
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
} as any)
|
|
||||||
|
|
||||||
render(<LoginPage />)
|
render(<LoginPage />)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Eye, EyeOff } from "lucide-react"
|
import { Eye, EyeOff } from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { adminApiHelpers } from "@/lib/api-client"
|
import { authService } from "@/services"
|
||||||
import { errorTracker } from "@/lib/error-tracker"
|
import { errorTracker } from "@/lib/error-tracker"
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
|
|
@ -24,58 +24,37 @@ export default function LoginPage() {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await adminApiHelpers.login({ email, password })
|
const response = await authService.login({ email, password })
|
||||||
console.log('Login response:', response.data) // Debug log
|
|
||||||
|
|
||||||
// Handle different response formats
|
|
||||||
const responseData = response.data
|
|
||||||
const access_token = responseData.access_token || responseData.token || responseData.accessToken
|
|
||||||
const refresh_token = responseData.refresh_token || responseData.refreshToken
|
|
||||||
const user = responseData.user || responseData.data?.user || responseData
|
|
||||||
|
|
||||||
console.log('Extracted token:', access_token) // Debug log
|
|
||||||
console.log('Extracted user:', user) // Debug log
|
|
||||||
|
|
||||||
// Check if user is admin
|
// Check if user is admin
|
||||||
if (user.role !== 'ADMIN') {
|
if (response.user.role !== 'ADMIN') {
|
||||||
toast.error("Access denied. Admin privileges required.")
|
toast.error("Access denied. Admin privileges required.")
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store tokens and user data
|
// Set user context for error tracking
|
||||||
if (access_token) {
|
errorTracker.setUser({
|
||||||
localStorage.setItem('access_token', access_token)
|
id: response.user.id,
|
||||||
console.log('Access token stored in localStorage') // Debug log
|
email: response.user.email,
|
||||||
} else {
|
name: `${response.user.firstName} ${response.user.lastName}`,
|
||||||
console.warn('No access_token in response - assuming httpOnly cookies') // Debug log
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if (refresh_token) {
|
|
||||||
localStorage.setItem('refresh_token', refresh_token)
|
|
||||||
console.log('Refresh token stored in localStorage') // Debug log
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('user', JSON.stringify(user))
|
|
||||||
console.log('User stored in localStorage') // Debug log
|
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
toast.success("Login successful!")
|
toast.success("Login successful!")
|
||||||
|
|
||||||
// Small delay to ensure localStorage is persisted
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100))
|
|
||||||
|
|
||||||
// Verify token is stored before navigation
|
|
||||||
const storedToken = localStorage.getItem('access_token')
|
|
||||||
console.log('Token verification before navigation:', storedToken) // Debug log
|
|
||||||
|
|
||||||
// Navigate to dashboard
|
// Navigate to dashboard
|
||||||
console.log('Navigating to:', from) // Debug log
|
|
||||||
navigate(from, { replace: true })
|
navigate(from, { replace: true })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Login error:', error) // Debug log
|
console.error('Login error:', error)
|
||||||
const message = error.response?.data?.message || "Invalid email or password"
|
const message = error.response?.data?.message || "Invalid email or password"
|
||||||
toast.error(message)
|
toast.error(message)
|
||||||
|
|
||||||
|
// Track login error
|
||||||
|
errorTracker.trackError(error, {
|
||||||
|
extra: { email, action: 'login' }
|
||||||
|
})
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
107
src/services/analytics.service.ts
Normal file
107
src/services/analytics.service.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import apiClient from './api/client'
|
||||||
|
|
||||||
|
export interface OverviewStats {
|
||||||
|
users?: {
|
||||||
|
total: number
|
||||||
|
active: number
|
||||||
|
inactive: number
|
||||||
|
}
|
||||||
|
invoices?: {
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
revenue?: {
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
storage?: {
|
||||||
|
totalSize: number
|
||||||
|
documents: number
|
||||||
|
}
|
||||||
|
totalUsers: number
|
||||||
|
activeUsers: number
|
||||||
|
totalRevenue: number
|
||||||
|
totalTransactions: number
|
||||||
|
storageUsed: number
|
||||||
|
storageLimit: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserGrowthData {
|
||||||
|
date: string
|
||||||
|
users: number
|
||||||
|
activeUsers: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RevenueData {
|
||||||
|
date: string
|
||||||
|
revenue: number
|
||||||
|
transactions: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnalyticsService {
|
||||||
|
/**
|
||||||
|
* Get overview statistics
|
||||||
|
*/
|
||||||
|
async getOverview(): Promise<OverviewStats> {
|
||||||
|
const response = await apiClient.get<OverviewStats>('/admin/analytics/overview')
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user growth data
|
||||||
|
*/
|
||||||
|
async getUserGrowth(days: number = 30): Promise<UserGrowthData[]> {
|
||||||
|
const response = await apiClient.get<UserGrowthData[]>('/admin/analytics/users/growth', {
|
||||||
|
params: { days },
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get revenue data
|
||||||
|
*/
|
||||||
|
async getRevenue(period: '7days' | '30days' | '90days' = '30days'): Promise<RevenueData[]> {
|
||||||
|
const response = await apiClient.get<RevenueData[]>('/admin/analytics/revenue', {
|
||||||
|
params: { period },
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API usage statistics
|
||||||
|
*/
|
||||||
|
async getApiUsage(days: number = 7): Promise<any> {
|
||||||
|
const response = await apiClient.get('/admin/analytics/api-usage', {
|
||||||
|
params: { days },
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get error rate statistics
|
||||||
|
*/
|
||||||
|
async getErrorRate(days: number = 7): Promise<any> {
|
||||||
|
const response = await apiClient.get('/admin/analytics/error-rate', {
|
||||||
|
params: { days },
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get storage usage by user
|
||||||
|
*/
|
||||||
|
async getStorageByUser(limit: number = 10): Promise<any> {
|
||||||
|
const response = await apiClient.get('/admin/analytics/storage/by-user', {
|
||||||
|
params: { limit },
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get storage analytics
|
||||||
|
*/
|
||||||
|
async getStorageAnalytics(): Promise<any> {
|
||||||
|
const response = await apiClient.get('/admin/analytics/storage')
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const analyticsService = new AnalyticsService()
|
||||||
88
src/services/announcement.service.ts
Normal file
88
src/services/announcement.service.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import apiClient from './api/client'
|
||||||
|
|
||||||
|
export interface Announcement {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
message: string
|
||||||
|
type: 'info' | 'warning' | 'success' | 'error'
|
||||||
|
priority: number
|
||||||
|
targetAudience: string
|
||||||
|
isActive: boolean
|
||||||
|
startsAt?: string
|
||||||
|
endsAt?: string
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateAnnouncementData {
|
||||||
|
title: string
|
||||||
|
message: string
|
||||||
|
type?: 'info' | 'warning' | 'success' | 'error'
|
||||||
|
priority?: number
|
||||||
|
targetAudience?: string
|
||||||
|
startsAt?: string
|
||||||
|
endsAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateAnnouncementData {
|
||||||
|
title?: string
|
||||||
|
message?: string
|
||||||
|
type?: 'info' | 'warning' | 'success' | 'error'
|
||||||
|
priority?: number
|
||||||
|
targetAudience?: string
|
||||||
|
startsAt?: string
|
||||||
|
endsAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnnouncementService {
|
||||||
|
/**
|
||||||
|
* Get all announcements
|
||||||
|
*/
|
||||||
|
async getAnnouncements(activeOnly: boolean = false): Promise<Announcement[]> {
|
||||||
|
const response = await apiClient.get<Announcement[]>('/admin/announcements', {
|
||||||
|
params: { activeOnly },
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single announcement by ID
|
||||||
|
*/
|
||||||
|
async getAnnouncement(id: string): Promise<Announcement> {
|
||||||
|
const response = await apiClient.get<Announcement>(`/admin/announcements/${id}`)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new announcement
|
||||||
|
*/
|
||||||
|
async createAnnouncement(data: CreateAnnouncementData): Promise<Announcement> {
|
||||||
|
const response = await apiClient.post<Announcement>('/admin/announcements', data)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update announcement
|
||||||
|
*/
|
||||||
|
async updateAnnouncement(id: string, data: UpdateAnnouncementData): Promise<Announcement> {
|
||||||
|
const response = await apiClient.put<Announcement>(`/admin/announcements/${id}`, data)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle announcement active status
|
||||||
|
*/
|
||||||
|
async toggleAnnouncement(id: string): Promise<Announcement> {
|
||||||
|
const response = await apiClient.patch<Announcement>(`/admin/announcements/${id}/toggle`)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete announcement
|
||||||
|
*/
|
||||||
|
async deleteAnnouncement(id: string): Promise<void> {
|
||||||
|
await apiClient.delete(`/admin/announcements/${id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const announcementService = new AnnouncementService()
|
||||||
71
src/services/api/client.ts
Normal file
71
src/services/api/client.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import axios, { type AxiosInstance, type AxiosError } from 'axios'
|
||||||
|
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_BACKEND_API_URL || 'http://localhost:3001/api/v1'
|
||||||
|
|
||||||
|
// Create axios instance with default config
|
||||||
|
const apiClient: AxiosInstance = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
withCredentials: true, // Send cookies with requests
|
||||||
|
timeout: 30000, // 30 second timeout
|
||||||
|
})
|
||||||
|
|
||||||
|
// Request interceptor - Add auth token
|
||||||
|
apiClient.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// Add token from localStorage as fallback (cookies are preferred)
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Response interceptor - Handle errors and token refresh
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
async (error: AxiosError) => {
|
||||||
|
const originalRequest = error.config as any
|
||||||
|
|
||||||
|
// Handle 401 Unauthorized - Try to refresh token
|
||||||
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||||
|
originalRequest._retry = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to refresh token
|
||||||
|
const refreshToken = localStorage.getItem('refresh_token')
|
||||||
|
if (refreshToken) {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${API_BASE_URL}/auth/refresh`,
|
||||||
|
{ refreshToken },
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const { accessToken } = response.data
|
||||||
|
localStorage.setItem('access_token', accessToken)
|
||||||
|
|
||||||
|
// Retry original request with new token
|
||||||
|
originalRequest.headers.Authorization = `Bearer ${accessToken}`
|
||||||
|
return apiClient(originalRequest)
|
||||||
|
}
|
||||||
|
} catch (refreshError) {
|
||||||
|
// Refresh failed - logout user
|
||||||
|
localStorage.removeItem('access_token')
|
||||||
|
localStorage.removeItem('refresh_token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
window.location.href = '/login'
|
||||||
|
return Promise.reject(refreshError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default apiClient
|
||||||
101
src/services/audit.service.ts
Normal file
101
src/services/audit.service.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import apiClient from './api/client'
|
||||||
|
|
||||||
|
export interface AuditLog {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
action: string
|
||||||
|
resourceType: string
|
||||||
|
resourceId: string
|
||||||
|
changes?: Record<string, any>
|
||||||
|
ipAddress: string
|
||||||
|
userAgent: string
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetAuditLogsParams {
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
userId?: string
|
||||||
|
action?: string
|
||||||
|
resourceType?: string
|
||||||
|
resourceId?: string
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
search?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditStats {
|
||||||
|
totalActions: number
|
||||||
|
uniqueUsers: number
|
||||||
|
topActions: Array<{ action: string; count: number }>
|
||||||
|
topUsers: Array<{ userId: string; count: number }>
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuditService {
|
||||||
|
/**
|
||||||
|
* Get audit logs with pagination and filters
|
||||||
|
*/
|
||||||
|
async getAuditLogs(params?: GetAuditLogsParams): Promise<{
|
||||||
|
data: AuditLog[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
limit: number
|
||||||
|
totalPages: number
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.get('/admin/audit/logs', { params })
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get audit log by ID
|
||||||
|
*/
|
||||||
|
async getAuditLog(id: string): Promise<AuditLog> {
|
||||||
|
const response = await apiClient.get<AuditLog>(`/admin/audit/logs/${id}`)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user audit activity
|
||||||
|
*/
|
||||||
|
async getUserAuditActivity(userId: string, days: number = 30): Promise<AuditLog[]> {
|
||||||
|
const response = await apiClient.get<AuditLog[]>(`/admin/audit/users/${userId}`, {
|
||||||
|
params: { days },
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get resource history
|
||||||
|
*/
|
||||||
|
async getResourceHistory(type: string, id: string): Promise<AuditLog[]> {
|
||||||
|
const response = await apiClient.get<AuditLog[]>(`/admin/audit/resource/${type}/${id}`)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get audit statistics
|
||||||
|
*/
|
||||||
|
async getAuditStats(startDate?: string, endDate?: string): Promise<AuditStats> {
|
||||||
|
const response = await apiClient.get<AuditStats>('/admin/audit/stats', {
|
||||||
|
params: { startDate, endDate },
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export audit logs
|
||||||
|
*/
|
||||||
|
async exportAuditLogs(params?: {
|
||||||
|
format?: 'csv' | 'json'
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
}): Promise<Blob> {
|
||||||
|
const response = await apiClient.get('/admin/audit/export', {
|
||||||
|
params,
|
||||||
|
responseType: 'blob',
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auditService = new AuditService()
|
||||||
99
src/services/auth.service.ts
Normal file
99
src/services/auth.service.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import apiClient from './api/client'
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
accessToken: string
|
||||||
|
refreshToken: string
|
||||||
|
user: {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
role: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshTokenResponse {
|
||||||
|
accessToken: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthService {
|
||||||
|
/**
|
||||||
|
* Login user with email and password
|
||||||
|
*/
|
||||||
|
async login(credentials: LoginRequest): Promise<LoginResponse> {
|
||||||
|
const response = await apiClient.post<LoginResponse>('/auth/login', credentials)
|
||||||
|
|
||||||
|
// Store tokens
|
||||||
|
if (response.data.accessToken) {
|
||||||
|
localStorage.setItem('access_token', response.data.accessToken)
|
||||||
|
}
|
||||||
|
if (response.data.refreshToken) {
|
||||||
|
localStorage.setItem('refresh_token', response.data.refreshToken)
|
||||||
|
}
|
||||||
|
if (response.data.user) {
|
||||||
|
localStorage.setItem('user', JSON.stringify(response.data.user))
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout user
|
||||||
|
*/
|
||||||
|
async logout(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await apiClient.post('/auth/logout')
|
||||||
|
} finally {
|
||||||
|
// Clear local storage even if API call fails
|
||||||
|
localStorage.removeItem('access_token')
|
||||||
|
localStorage.removeItem('refresh_token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh access token
|
||||||
|
*/
|
||||||
|
async refreshToken(): Promise<RefreshTokenResponse> {
|
||||||
|
const refreshToken = localStorage.getItem('refresh_token')
|
||||||
|
const response = await apiClient.post<RefreshTokenResponse>('/auth/refresh', {
|
||||||
|
refreshToken,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.accessToken) {
|
||||||
|
localStorage.setItem('access_token', response.data.accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user from localStorage
|
||||||
|
*/
|
||||||
|
getCurrentUser() {
|
||||||
|
const userStr = localStorage.getItem('user')
|
||||||
|
return userStr ? JSON.parse(userStr) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is authenticated
|
||||||
|
*/
|
||||||
|
isAuthenticated(): boolean {
|
||||||
|
return !!localStorage.getItem('access_token')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is admin
|
||||||
|
*/
|
||||||
|
isAdmin(): boolean {
|
||||||
|
const user = this.getCurrentUser()
|
||||||
|
return user?.role === 'ADMIN'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authService = new AuthService()
|
||||||
54
src/services/dashboard.service.ts
Normal file
54
src/services/dashboard.service.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import apiClient from './api/client'
|
||||||
|
|
||||||
|
export interface UserDashboardStats {
|
||||||
|
totalInvoices: number
|
||||||
|
totalTransactions: number
|
||||||
|
totalRevenue: number
|
||||||
|
pendingInvoices: number
|
||||||
|
growthPercentage?: number
|
||||||
|
recentActivity?: Array<{
|
||||||
|
id: string
|
||||||
|
type: string
|
||||||
|
description: string
|
||||||
|
date: string
|
||||||
|
amount?: number
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserProfile {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
role: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class DashboardService {
|
||||||
|
/**
|
||||||
|
* Get current user profile
|
||||||
|
*/
|
||||||
|
async getUserProfile(): Promise<UserProfile> {
|
||||||
|
const response = await apiClient.get<UserProfile>('/user/profile')
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user dashboard statistics
|
||||||
|
*/
|
||||||
|
async getUserStats(): Promise<UserDashboardStats> {
|
||||||
|
const response = await apiClient.get<UserDashboardStats>('/user/stats')
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user recent activity
|
||||||
|
*/
|
||||||
|
async getRecentActivity(limit: number = 10): Promise<any[]> {
|
||||||
|
const response = await apiClient.get('/user/activity', {
|
||||||
|
params: { limit },
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dashboardService = new DashboardService()
|
||||||
21
src/services/index.ts
Normal file
21
src/services/index.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
// Export all services from a single entry point
|
||||||
|
export { authService } from './auth.service'
|
||||||
|
export { userService } from './user.service'
|
||||||
|
export { analyticsService } from './analytics.service'
|
||||||
|
export { securityService } from './security.service'
|
||||||
|
export { systemService } from './system.service'
|
||||||
|
export { announcementService } from './announcement.service'
|
||||||
|
export { auditService } from './audit.service'
|
||||||
|
export { settingsService } from './settings.service'
|
||||||
|
export { dashboardService } from './dashboard.service'
|
||||||
|
|
||||||
|
// Export types
|
||||||
|
export type { LoginRequest, LoginResponse } from './auth.service'
|
||||||
|
export type { User, GetUsersParams, PaginatedResponse } from './user.service'
|
||||||
|
export type { OverviewStats, UserGrowthData, RevenueData } from './analytics.service'
|
||||||
|
export type { SuspiciousActivity, ActiveSession, FailedLogin, ApiKey } from './security.service'
|
||||||
|
export type { HealthStatus, SystemInfo, MaintenanceStatus } from './system.service'
|
||||||
|
export type { Announcement, CreateAnnouncementData, UpdateAnnouncementData } from './announcement.service'
|
||||||
|
export type { AuditLog, GetAuditLogsParams, AuditStats } from './audit.service'
|
||||||
|
export type { Setting, CreateSettingData, UpdateSettingData } from './settings.service'
|
||||||
|
export type { UserDashboardStats, UserProfile } from './dashboard.service'
|
||||||
112
src/services/security.service.ts
Normal file
112
src/services/security.service.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import apiClient from './api/client'
|
||||||
|
|
||||||
|
export interface SuspiciousActivity {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
type: string
|
||||||
|
description: string
|
||||||
|
ipAddress: string
|
||||||
|
timestamp: string
|
||||||
|
severity: 'low' | 'medium' | 'high'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActiveSession {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
ipAddress: string
|
||||||
|
userAgent: string
|
||||||
|
lastActivity: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FailedLogin {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
ipAddress: string
|
||||||
|
timestamp: string
|
||||||
|
reason: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiKey {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
key: string
|
||||||
|
userId: string
|
||||||
|
createdAt: string
|
||||||
|
lastUsed?: string
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class SecurityService {
|
||||||
|
/**
|
||||||
|
* Get suspicious activity logs
|
||||||
|
*/
|
||||||
|
async getSuspiciousActivity(): Promise<{
|
||||||
|
suspiciousIPs?: any[]
|
||||||
|
suspiciousEmails?: any[]
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.get('/admin/security/suspicious')
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active user sessions
|
||||||
|
*/
|
||||||
|
async getActiveSessions(): Promise<ActiveSession[]> {
|
||||||
|
const response = await apiClient.get<ActiveSession[]>('/admin/security/sessions')
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Terminate a user session
|
||||||
|
*/
|
||||||
|
async terminateSession(sessionId: string): Promise<void> {
|
||||||
|
await apiClient.delete(`/admin/security/sessions/${sessionId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get failed login attempts
|
||||||
|
*/
|
||||||
|
async getFailedLogins(params?: {
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
email?: string
|
||||||
|
}): Promise<{ data: FailedLogin[]; total: number }> {
|
||||||
|
const response = await apiClient.get('/admin/security/failed-logins', { params })
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rate limit violations
|
||||||
|
*/
|
||||||
|
async getRateLimitViolations(days: number = 7): Promise<any[]> {
|
||||||
|
const response = await apiClient.get('/admin/security/rate-limits', {
|
||||||
|
params: { days },
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all API keys
|
||||||
|
*/
|
||||||
|
async getAllApiKeys(): Promise<ApiKey[]> {
|
||||||
|
const response = await apiClient.get<ApiKey[]>('/admin/security/api-keys')
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke an API key
|
||||||
|
*/
|
||||||
|
async revokeApiKey(id: string): Promise<void> {
|
||||||
|
await apiClient.delete(`/admin/security/api-keys/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ban an IP address
|
||||||
|
*/
|
||||||
|
async banIpAddress(ipAddress: string, reason: string): Promise<void> {
|
||||||
|
await apiClient.post('/admin/security/ban-ip', { ipAddress, reason })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const securityService = new SecurityService()
|
||||||
78
src/services/settings.service.ts
Normal file
78
src/services/settings.service.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import apiClient from './api/client'
|
||||||
|
|
||||||
|
export interface Setting {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
category: string
|
||||||
|
description?: string
|
||||||
|
isPublic: boolean
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSettingData {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
category: string
|
||||||
|
description?: string
|
||||||
|
isPublic?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateSettingData {
|
||||||
|
value: string
|
||||||
|
description?: string
|
||||||
|
isPublic?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsService {
|
||||||
|
/**
|
||||||
|
* Get all settings, optionally filtered by category
|
||||||
|
*/
|
||||||
|
async getSettings(category?: string): Promise<Setting[]> {
|
||||||
|
const response = await apiClient.get<Setting[]>('/admin/system/settings', {
|
||||||
|
params: { category },
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single setting by key
|
||||||
|
*/
|
||||||
|
async getSetting(key: string): Promise<Setting> {
|
||||||
|
const response = await apiClient.get<Setting>(`/admin/system/settings/${key}`)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new setting
|
||||||
|
*/
|
||||||
|
async createSetting(data: CreateSettingData): Promise<Setting> {
|
||||||
|
const response = await apiClient.post<Setting>('/admin/system/settings', data)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update setting
|
||||||
|
*/
|
||||||
|
async updateSetting(key: string, data: UpdateSettingData): Promise<Setting> {
|
||||||
|
const response = await apiClient.put<Setting>(`/admin/system/settings/${key}`, data)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete setting
|
||||||
|
*/
|
||||||
|
async deleteSetting(key: string): Promise<void> {
|
||||||
|
await apiClient.delete(`/admin/system/settings/${key}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get public settings (for frontend use)
|
||||||
|
*/
|
||||||
|
async getPublicSettings(): Promise<Record<string, string>> {
|
||||||
|
const response = await apiClient.get<Record<string, string>>('/settings/public')
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingsService = new SettingsService()
|
||||||
94
src/services/system.service.ts
Normal file
94
src/services/system.service.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import apiClient from './api/client'
|
||||||
|
|
||||||
|
export interface HealthStatus {
|
||||||
|
status: 'healthy' | 'degraded' | 'down'
|
||||||
|
database: 'connected' | 'disconnected'
|
||||||
|
redis: 'connected' | 'disconnected'
|
||||||
|
uptime: number
|
||||||
|
timestamp: string
|
||||||
|
recentErrors?: number
|
||||||
|
activeUsers?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemInfo {
|
||||||
|
version: string
|
||||||
|
environment: string
|
||||||
|
nodeVersion: string
|
||||||
|
memory: {
|
||||||
|
used: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
cpu: {
|
||||||
|
usage: number
|
||||||
|
cores: number
|
||||||
|
loadAverage?: number[]
|
||||||
|
}
|
||||||
|
platform?: string
|
||||||
|
architecture?: string
|
||||||
|
uptime?: number
|
||||||
|
env?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MaintenanceStatus {
|
||||||
|
enabled: boolean
|
||||||
|
message?: string
|
||||||
|
scheduledStart?: string
|
||||||
|
scheduledEnd?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class SystemService {
|
||||||
|
/**
|
||||||
|
* Get system health status
|
||||||
|
*/
|
||||||
|
async getHealth(): Promise<HealthStatus> {
|
||||||
|
const response = await apiClient.get<HealthStatus>('/admin/system/health')
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get system information
|
||||||
|
*/
|
||||||
|
async getSystemInfo(): Promise<SystemInfo> {
|
||||||
|
const response = await apiClient.get<SystemInfo>('/admin/system/info')
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get maintenance mode status
|
||||||
|
*/
|
||||||
|
async getMaintenanceStatus(): Promise<MaintenanceStatus> {
|
||||||
|
const response = await apiClient.get<MaintenanceStatus>('/admin/maintenance/status')
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable maintenance mode
|
||||||
|
*/
|
||||||
|
async enableMaintenance(message?: string): Promise<void> {
|
||||||
|
await apiClient.post('/admin/maintenance/enable', { message })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable maintenance mode
|
||||||
|
*/
|
||||||
|
async disableMaintenance(): Promise<void> {
|
||||||
|
await apiClient.post('/admin/maintenance/disable')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear application cache
|
||||||
|
*/
|
||||||
|
async clearCache(): Promise<void> {
|
||||||
|
await apiClient.post('/admin/system/clear-cache')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run database migrations
|
||||||
|
*/
|
||||||
|
async runMigrations(): Promise<{ success: boolean; message: string }> {
|
||||||
|
const response = await apiClient.post('/admin/system/migrate')
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const systemService = new SystemService()
|
||||||
132
src/services/user.service.ts
Normal file
132
src/services/user.service.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
import apiClient from './api/client'
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
role: string
|
||||||
|
isActive: boolean
|
||||||
|
createdAt: string
|
||||||
|
updatedAt?: string
|
||||||
|
lastLogin?: string
|
||||||
|
_count?: {
|
||||||
|
invoices?: number
|
||||||
|
transactions?: number
|
||||||
|
documents?: number
|
||||||
|
activityLogs?: number
|
||||||
|
reports?: number
|
||||||
|
payments?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetUsersParams {
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
search?: string
|
||||||
|
role?: string
|
||||||
|
isActive?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
data: T[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
limit: number
|
||||||
|
totalPages: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserService {
|
||||||
|
/**
|
||||||
|
* Get paginated list of users
|
||||||
|
*/
|
||||||
|
async getUsers(params?: GetUsersParams): Promise<PaginatedResponse<User>> {
|
||||||
|
const response = await apiClient.get<PaginatedResponse<User>>('/admin/users', { params })
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single user by ID
|
||||||
|
*/
|
||||||
|
async getUser(id: string): Promise<User> {
|
||||||
|
const response = await apiClient.get<User>(`/admin/users/${id}`)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new user
|
||||||
|
*/
|
||||||
|
async createUser(data: Partial<User>): Promise<User> {
|
||||||
|
const response = await apiClient.post<User>('/admin/users', data)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user
|
||||||
|
*/
|
||||||
|
async updateUser(id: string, data: Partial<User>): Promise<User> {
|
||||||
|
const response = await apiClient.patch<User>(`/admin/users/${id}`, data)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete user (soft or hard delete)
|
||||||
|
*/
|
||||||
|
async deleteUser(id: string, hard: boolean = false): Promise<void> {
|
||||||
|
await apiClient.delete(`/admin/users/${id}`, {
|
||||||
|
params: { hard },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset user password
|
||||||
|
*/
|
||||||
|
async resetPassword(id: string): Promise<{ temporaryPassword: string }> {
|
||||||
|
const response = await apiClient.post<{ temporaryPassword: string }>(
|
||||||
|
`/admin/users/${id}/reset-password`
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user activity logs
|
||||||
|
*/
|
||||||
|
async getUserActivity(id: string, days: number = 30): Promise<any[]> {
|
||||||
|
const response = await apiClient.get(`/admin/users/${id}/activity`, {
|
||||||
|
params: { days },
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import users from CSV
|
||||||
|
*/
|
||||||
|
async importUsers(file: File): Promise<{ imported: number; failed: number }> {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
const response = await apiClient.post<{ imported: number; failed: number }>(
|
||||||
|
'/admin/users/import',
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export users to CSV
|
||||||
|
*/
|
||||||
|
async exportUsers(format: 'csv' | 'json' = 'csv'): Promise<Blob> {
|
||||||
|
const response = await apiClient.get('/admin/users/export', {
|
||||||
|
params: { format },
|
||||||
|
responseType: 'blob',
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userService = new UserService()
|
||||||
Loading…
Reference in New Issue
Block a user