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.
|
||||
|
||||
> 📚 **For detailed documentation, see [dev-docs/](./dev-docs/README.md)**
|
||||
|
||||
## Features
|
||||
|
||||
- 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
|
||||
|
||||
This directory contains comprehensive documentation for the Yaltopia Ticket Admin project.
|
||||
Essential documentation for the Yaltopia Ticket Admin project.
|
||||
|
||||
## 📚 Documentation Index
|
||||
## 📚 Documentation
|
||||
|
||||
### Getting Started
|
||||
- **[Quick Reference](./QUICK_REFERENCE.md)** - Quick start guide and common commands
|
||||
- **[Tech Stack](./TECH_STACK.md)** - Technologies and frameworks used
|
||||
### [Development Guide](./DEVELOPMENT.md)
|
||||
Complete development guide including:
|
||||
- Tech stack & project structure
|
||||
- Quick start & setup
|
||||
- Common tasks & best practices
|
||||
- Troubleshooting
|
||||
|
||||
### Development
|
||||
- **[Authentication](./AUTHENTICATION.md)** - Authentication setup and flow
|
||||
- **[API Standards](./API_STANDARDS.md)** - API client implementation and best practices
|
||||
- **[Login API Documentation](./LOGIN_API_DOCUMENTATION.md)** - Login endpoint specifications
|
||||
- **[Troubleshooting](./TROUBLESHOOTING.md)** - Common issues and solutions
|
||||
### [API & Service Layer Guide](./API_GUIDE.md) ⭐
|
||||
**Essential reading for making API calls:**
|
||||
- Service layer architecture
|
||||
- All available services & methods
|
||||
- Common patterns & examples
|
||||
- Error handling
|
||||
- Best practices
|
||||
|
||||
### Testing & Quality
|
||||
- **[Testing Guide](./TESTING_GUIDE.md)** - Testing setup and best practices
|
||||
- **[CI/CD Setup](./CI_CD_SETUP.md)** - Continuous integration and deployment
|
||||
- **[Error Monitoring](./ERROR_MONITORING.md)** - Sentry integration and error tracking
|
||||
### [Testing Guide](./TESTING_GUIDE.md)
|
||||
Testing setup and practices:
|
||||
- Unit testing with Vitest
|
||||
- Component testing
|
||||
- Integration testing
|
||||
- Test utilities & mocks
|
||||
|
||||
### Security
|
||||
- **[Security Checklist](./SECURITY_CHECKLIST.md)** - Comprehensive security requirements
|
||||
- **[Security](./SECURITY.md)** - Security best practices and guidelines
|
||||
### [Deployment Guide](./DEPLOYMENT.md)
|
||||
Production deployment:
|
||||
- Pre-deployment checklist
|
||||
- Deployment options (Vercel, Netlify, Docker)
|
||||
- Environment configuration
|
||||
- CI/CD setup
|
||||
|
||||
### Deployment
|
||||
- **[Deployment Options](./DEPLOYMENT_OPTIONS.md)** - All deployment configurations (Vercel, Netlify, Docker)
|
||||
- **[Deployment Guide](./DEPLOYMENT.md)** - Step-by-step deployment instructions
|
||||
- **[Pre-Deployment Checklist](./PRE_DEPLOYMENT_CHECKLIST.md)** - Checklist before going live
|
||||
- **[Production Ready Summary](./PRODUCTION_READY_SUMMARY.md)** - Production readiness overview
|
||||
### [Security Guide](./SECURITY.md)
|
||||
Security best practices:
|
||||
- Authentication & authorization
|
||||
- Data protection
|
||||
- Security headers
|
||||
- CORS configuration
|
||||
- Input validation
|
||||
|
||||
## 🎯 Quick Links
|
||||
## 🚀 Quick Start
|
||||
|
||||
### For Developers
|
||||
1. Start with [Quick Reference](./QUICK_REFERENCE.md)
|
||||
2. Understand [Tech Stack](./TECH_STACK.md)
|
||||
3. Set up [Authentication](./AUTHENTICATION.md)
|
||||
4. Review [API Standards](./API_STANDARDS.md)
|
||||
```bash
|
||||
# Install
|
||||
npm install
|
||||
|
||||
### For DevOps
|
||||
1. Review [CI/CD Setup](./CI_CD_SETUP.md)
|
||||
2. Choose deployment from [Deployment Options](./DEPLOYMENT_OPTIONS.md)
|
||||
3. Follow [Deployment Guide](./DEPLOYMENT.md)
|
||||
4. Complete [Pre-Deployment Checklist](./PRE_DEPLOYMENT_CHECKLIST.md)
|
||||
# Configure
|
||||
cp .env.example .env
|
||||
# Edit .env with your backend URL
|
||||
|
||||
### For Security Review
|
||||
1. Review [Security Checklist](./SECURITY_CHECKLIST.md)
|
||||
2. Check [Security](./SECURITY.md) guidelines
|
||||
3. Verify [API Standards](./API_STANDARDS.md) compliance
|
||||
# Develop
|
||||
npm run dev
|
||||
|
||||
### For Troubleshooting
|
||||
1. Check [Troubleshooting](./TROUBLESHOOTING.md) guide
|
||||
2. Review [Error Monitoring](./ERROR_MONITORING.md) setup
|
||||
3. Consult [API Standards](./API_STANDARDS.md) for API issues
|
||||
# Test
|
||||
npm run test
|
||||
|
||||
## 📖 Documentation Standards
|
||||
# Build
|
||||
npm run build
|
||||
```
|
||||
|
||||
All documentation follows these principles:
|
||||
- **Clear and Concise** - Easy to understand
|
||||
- **Actionable** - Includes examples and commands
|
||||
- **Up-to-date** - Reflects current implementation
|
||||
- **Professional** - Industry-standard practices
|
||||
## 📖 Key Concepts
|
||||
|
||||
## 🔄 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:
|
||||
1. Update relevant documentation
|
||||
2. Add new sections if needed
|
||||
3. Remove outdated information
|
||||
4. Keep examples current
|
||||
### React Query
|
||||
Data fetching with caching:
|
||||
```typescript
|
||||
const { data } = useQuery({
|
||||
queryKey: ['users'],
|
||||
queryFn: () => userService.getUsers()
|
||||
})
|
||||
```
|
||||
|
||||
## 📝 Contributing to Documentation
|
||||
|
||||
To improve documentation:
|
||||
1. Identify gaps or unclear sections
|
||||
2. Add examples and use cases
|
||||
3. Include troubleshooting tips
|
||||
4. Keep formatting consistent
|
||||
### Protected Routes
|
||||
Authentication required for admin routes:
|
||||
```typescript
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path="/admin/*" element={<AdminLayout />} />
|
||||
</Route>
|
||||
```
|
||||
|
||||
## 🆘 Need Help?
|
||||
|
||||
If documentation is unclear or missing:
|
||||
1. Check [Troubleshooting](./TROUBLESHOOTING.md)
|
||||
2. Review related documentation
|
||||
3. Check code comments
|
||||
4. Consult team members
|
||||
1. **Making API calls?** → [API & Service Layer Guide](./API_GUIDE.md)
|
||||
2. **General development?** → [Development Guide](./DEVELOPMENT.md)
|
||||
3. **Writing tests?** → [Testing Guide](./TESTING_GUIDE.md)
|
||||
4. **Deploying?** → [Deployment Guide](./DEPLOYMENT.md)
|
||||
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 { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { authService } from "@/services"
|
||||
|
||||
interface User {
|
||||
email: string
|
||||
|
|
@ -70,8 +70,8 @@ export function AppShell() {
|
|||
return item?.label || "Admin Panel"
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
adminApiHelpers.logout()
|
||||
const handleLogout = async () => {
|
||||
await authService.logout()
|
||||
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 adminApi from './api-client'
|
||||
import apiClient from '@/services/api/client'
|
||||
|
||||
interface ErrorLog {
|
||||
message: string
|
||||
|
|
@ -105,7 +105,7 @@ class ErrorTracker {
|
|||
|
||||
try {
|
||||
// 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
|
||||
} catch (error) {
|
||||
console.error('Failed to log error to backend:', error)
|
||||
|
|
|
|||
|
|
@ -8,23 +8,17 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { analyticsService } from "@/services"
|
||||
|
||||
export default function AnalyticsApiPage() {
|
||||
const { data: apiUsage, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'analytics', 'api-usage'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getApiUsage(7)
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => analyticsService.getApiUsage(7),
|
||||
})
|
||||
|
||||
const { data: errorRate, isLoading: errorRateLoading } = useQuery({
|
||||
queryKey: ['admin', 'analytics', 'error-rate'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getErrorRate(7)
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => analyticsService.getErrorRate(7),
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
import { useQuery } from "@tanstack/react-query"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
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() {
|
||||
const { data: revenue, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'analytics', 'revenue'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getRevenue('90days')
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => analyticsService.getRevenue('90days'),
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import { useQuery } from "@tanstack/react-query"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
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']
|
||||
|
||||
export default function AnalyticsStoragePage() {
|
||||
const { data: storage, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'analytics', 'storage'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getStorageAnalytics()
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => analyticsService.getStorageAnalytics(),
|
||||
})
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
import { useQuery } from "@tanstack/react-query"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
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() {
|
||||
const { data: userGrowth, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'analytics', 'users', 'growth'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getUserGrowth(90)
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => analyticsService.getUserGrowth(90),
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import {
|
|||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Plus, Edit, Trash2 } from "lucide-react"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { announcementService } from "@/services"
|
||||
import { toast } from "sonner"
|
||||
import { format } from "date-fns"
|
||||
|
||||
|
|
@ -31,16 +31,11 @@ export default function AnnouncementsPage() {
|
|||
|
||||
const { data: announcements, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'announcements'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getAnnouncements(false)
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => announcementService.getAnnouncements(false),
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
await adminApiHelpers.deleteAnnouncement(id)
|
||||
},
|
||||
mutationFn: (id: string) => announcementService.deleteAnnouncement(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] })
|
||||
toast.success("Announcement deleted successfully")
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Search, Eye } from "lucide-react"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { auditService } from "@/services"
|
||||
import { format } from "date-fns"
|
||||
|
||||
export default function AuditPage() {
|
||||
|
|
@ -26,8 +26,7 @@ export default function AuditPage() {
|
|||
queryFn: async () => {
|
||||
const params: any = { page, limit }
|
||||
if (search) params.search = search
|
||||
const response = await adminApiHelpers.getAuditLogs(params)
|
||||
return response.data
|
||||
return await auditService.getAuditLogs(params)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -2,49 +2,34 @@ import { useQuery } from "@tanstack/react-query"
|
|||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
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 { toast } from "sonner"
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { data: overview, isLoading: overviewLoading } = useQuery({
|
||||
queryKey: ['admin', 'analytics', 'overview'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getOverview()
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => analyticsService.getOverview(),
|
||||
})
|
||||
|
||||
const { data: userGrowth, isLoading: growthLoading } = useQuery({
|
||||
queryKey: ['admin', 'analytics', 'users', 'growth'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getUserGrowth(30)
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => analyticsService.getUserGrowth(30),
|
||||
})
|
||||
|
||||
const { data: revenue, isLoading: revenueLoading } = useQuery({
|
||||
queryKey: ['admin', 'analytics', 'revenue'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getRevenue('30days')
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => analyticsService.getRevenue('30days'),
|
||||
})
|
||||
|
||||
const { data: health, isLoading: healthLoading } = useQuery({
|
||||
queryKey: ['admin', 'system', 'health'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getHealth()
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => systemService.getHealth(),
|
||||
})
|
||||
|
||||
const { data: errorRate, isLoading: errorRateLoading } = useQuery({
|
||||
queryKey: ['admin', 'analytics', 'error-rate'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getErrorRate(7)
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => analyticsService.getErrorRate(7),
|
||||
})
|
||||
|
||||
const handleExport = () => {
|
||||
|
|
|
|||
|
|
@ -2,27 +2,21 @@ import { useQuery } from "@tanstack/react-query"
|
|||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { AlertCircle, CheckCircle, XCircle, Users } from "lucide-react"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { systemService } from "@/services"
|
||||
|
||||
export default function HealthPage() {
|
||||
const { data: health, isLoading: healthLoading } = useQuery({
|
||||
queryKey: ['admin', 'system', 'health'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getHealth()
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => systemService.getHealth(),
|
||||
refetchInterval: 30000, // Refetch every 30 seconds
|
||||
})
|
||||
|
||||
const { data: systemInfo, isLoading: infoLoading } = useQuery({
|
||||
queryKey: ['admin', 'system', 'info'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getSystemInfo()
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => systemService.getSystemInfo(),
|
||||
})
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
const getStatusIcon = (status?: string) => {
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'healthy':
|
||||
case 'connected':
|
||||
|
|
@ -142,19 +136,19 @@ export default function HealthPage() {
|
|||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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 { Switch } from "@/components/ui/switch"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { systemService } from "@/services"
|
||||
import { toast } from "sonner"
|
||||
import { useState } from "react"
|
||||
|
||||
|
|
@ -14,16 +14,11 @@ export default function MaintenancePage() {
|
|||
|
||||
const { data: status, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'maintenance'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getMaintenanceStatus()
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => systemService.getMaintenanceStatus(),
|
||||
})
|
||||
|
||||
const enableMutation = useMutation({
|
||||
mutationFn: async (msg?: string) => {
|
||||
await adminApiHelpers.enableMaintenance(msg)
|
||||
},
|
||||
mutationFn: (msg?: string) => systemService.enableMaintenance(msg),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'maintenance'] })
|
||||
toast.success("Maintenance mode enabled")
|
||||
|
|
@ -35,9 +30,7 @@ export default function MaintenancePage() {
|
|||
})
|
||||
|
||||
const disableMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await adminApiHelpers.disableMaintenance()
|
||||
},
|
||||
mutationFn: () => systemService.disableMaintenance(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'maintenance'] })
|
||||
toast.success("Maintenance mode disabled")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Ban } from "lucide-react"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { securityService } from "@/services"
|
||||
import { toast } from "sonner"
|
||||
import { format } from "date-fns"
|
||||
|
||||
|
|
@ -20,16 +20,11 @@ export default function ApiKeysPage() {
|
|||
|
||||
const { data: apiKeys, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'security', 'api-keys'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getAllApiKeys()
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => securityService.getAllApiKeys(),
|
||||
})
|
||||
|
||||
const revokeMutation = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
await adminApiHelpers.revokeApiKey(id)
|
||||
},
|
||||
mutationFn: (id: string) => securityService.revokeApiKey(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'security', 'api-keys'] })
|
||||
toast.success("API key revoked successfully")
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Search, Ban } from "lucide-react"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { securityService } from "@/services"
|
||||
import { format } from "date-fns"
|
||||
|
||||
export default function FailedLoginsPage() {
|
||||
|
|
@ -26,8 +26,7 @@ export default function FailedLoginsPage() {
|
|||
queryFn: async () => {
|
||||
const params: any = { page, limit }
|
||||
if (search) params.email = search
|
||||
const response = await adminApiHelpers.getFailedLogins(params)
|
||||
return response.data
|
||||
return await securityService.getFailedLogins(params)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -8,15 +8,12 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { securityService } from "@/services"
|
||||
|
||||
export default function RateLimitsPage() {
|
||||
const { data: violations, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'security', 'rate-limits'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getRateLimitViolations(7)
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => securityService.getRateLimitViolations(7),
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -10,16 +10,13 @@ import {
|
|||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { LogOut } from "lucide-react"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { securityService } from "@/services"
|
||||
import { format } from "date-fns"
|
||||
|
||||
export default function SessionsPage() {
|
||||
const { data: sessions, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'security', 'sessions'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getActiveSessions()
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => securityService.getActiveSessions(),
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -2,15 +2,12 @@ import { useQuery } from "@tanstack/react-query"
|
|||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Shield, Ban } from "lucide-react"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { securityService } from "@/services"
|
||||
|
||||
export default function SuspiciousActivityPage() {
|
||||
const { data: suspicious, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'security', 'suspicious'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getSuspiciousActivity()
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => securityService.getSuspiciousActivity(),
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
@ -28,9 +25,9 @@ export default function SuspiciousActivityPage() {
|
|||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8">Loading...</div>
|
||||
) : suspicious?.suspiciousIPs?.length > 0 ? (
|
||||
) : (suspicious?.suspiciousIPs?.length ?? 0) > 0 ? (
|
||||
<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>
|
||||
<p className="font-mono font-medium">{ip.ipAddress}</p>
|
||||
|
|
@ -61,9 +58,9 @@ export default function SuspiciousActivityPage() {
|
|||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8">Loading...</div>
|
||||
) : suspicious?.suspiciousEmails?.length > 0 ? (
|
||||
) : (suspicious?.suspiciousEmails?.length ?? 0) > 0 ? (
|
||||
<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>
|
||||
<p className="font-medium">{email.email}</p>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Plus } from "lucide-react"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { settingsService } from "@/services"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export default function SettingsPage() {
|
||||
|
|
@ -31,16 +31,12 @@ export default function SettingsPage() {
|
|||
|
||||
const { data: settings, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'settings', selectedCategory],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getSettings(selectedCategory)
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => settingsService.getSettings(selectedCategory),
|
||||
})
|
||||
|
||||
const updateSettingMutation = useMutation({
|
||||
mutationFn: async ({ key, value }: { key: string; value: string }) => {
|
||||
await adminApiHelpers.updateSetting(key, { value })
|
||||
},
|
||||
mutationFn: ({ key, value }: { key: string; value: string }) =>
|
||||
settingsService.updateSetting(key, { value }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] })
|
||||
toast.success("Setting updated successfully")
|
||||
|
|
@ -51,15 +47,13 @@ export default function SettingsPage() {
|
|||
})
|
||||
|
||||
const createSettingMutation = useMutation({
|
||||
mutationFn: async (data: {
|
||||
mutationFn: (data: {
|
||||
key: string
|
||||
value: string
|
||||
category: string
|
||||
description?: string
|
||||
isPublic?: boolean
|
||||
}) => {
|
||||
await adminApiHelpers.createSetting(data)
|
||||
},
|
||||
}) => settingsService.createSetting(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] })
|
||||
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 { Button } from "@/components/ui/button"
|
||||
import { ArrowLeft } from "lucide-react"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { userService } from "@/services"
|
||||
import { format } from "date-fns"
|
||||
|
||||
export default function UserActivityPage() {
|
||||
|
|
@ -12,10 +12,7 @@ export default function UserActivityPage() {
|
|||
|
||||
const { data: activity, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'users', id, 'activity'],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getUserActivity(id!, 30)
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => userService.getUserActivity(id!, 30),
|
||||
enabled: !!id,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"
|
|||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { ArrowLeft, Edit, Key } from "lucide-react"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { userService } from "@/services"
|
||||
import { format } from "date-fns"
|
||||
|
||||
export default function UserDetailsPage() {
|
||||
|
|
@ -14,10 +14,7 @@ export default function UserDetailsPage() {
|
|||
|
||||
const { data: user, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'users', id],
|
||||
queryFn: async () => {
|
||||
const response = await adminApiHelpers.getUser(id!)
|
||||
return response.data
|
||||
},
|
||||
queryFn: () => userService.getUser(id!),
|
||||
enabled: !!id,
|
||||
})
|
||||
|
||||
|
|
@ -90,7 +87,9 @@ export default function UserDetailsPage() {
|
|||
</div>
|
||||
<div>
|
||||
<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>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import {
|
|||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
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 { format } from "date-fns"
|
||||
|
||||
|
|
@ -55,15 +55,13 @@ export default function UsersPage() {
|
|||
if (search) params.search = search
|
||||
if (roleFilter !== 'all') params.role = roleFilter
|
||||
if (statusFilter !== 'all') params.isActive = statusFilter === 'active'
|
||||
const response = await adminApiHelpers.getUsers(params)
|
||||
return response.data
|
||||
return await userService.getUsers(params)
|
||||
},
|
||||
})
|
||||
|
||||
const deleteUserMutation = useMutation({
|
||||
mutationFn: async ({ id, hard }: { id: string; hard: boolean }) => {
|
||||
await adminApiHelpers.deleteUser(id, hard)
|
||||
},
|
||||
mutationFn: ({ id, hard }: { id: string; hard: boolean }) =>
|
||||
userService.deleteUser(id, hard),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] })
|
||||
toast.success("User deleted successfully")
|
||||
|
|
@ -75,10 +73,7 @@ export default function UsersPage() {
|
|||
})
|
||||
|
||||
const resetPasswordMutation = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const response = await adminApiHelpers.resetPassword(id)
|
||||
return response.data
|
||||
},
|
||||
mutationFn: (id: string) => userService.resetPassword(id),
|
||||
onSuccess: (data) => {
|
||||
toast.success(`Password reset. Temporary password: ${data.temporaryPassword}`)
|
||||
setResetPasswordDialogOpen(false)
|
||||
|
|
@ -89,18 +84,12 @@ export default function UsersPage() {
|
|||
})
|
||||
|
||||
const importUsersMutation = useMutation({
|
||||
mutationFn: async (file: File) => {
|
||||
const response = await adminApiHelpers.importUsers(file)
|
||||
return response.data
|
||||
},
|
||||
mutationFn: (file: File) => userService.importUsers(file),
|
||||
onSuccess: (data) => {
|
||||
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)
|
||||
setImportFile(null)
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
console.error('Import errors:', data.errors)
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.message || "Failed to import users")
|
||||
|
|
@ -109,8 +98,7 @@ export default function UsersPage() {
|
|||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
const response = await adminApiHelpers.exportUsers('csv')
|
||||
const blob = new Blob([response.data], { type: 'text/csv' })
|
||||
const blob = await userService.exportUsers('csv')
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
|
|
|
|||
|
|
@ -1,35 +1,175 @@
|
|||
import { useQuery } from "@tanstack/react-query"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
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() {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<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="text-sm text-muted-foreground">
|
||||
01 Sep - 15 Sep 2024
|
||||
{new Date().toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<Button variant="outline" onClick={handleExport}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Export Data
|
||||
</Button>
|
||||
</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>
|
||||
<CardHeader>
|
||||
<CardTitle>Welcome to Dashboard</CardTitle>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Invoices</CardTitle>
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground">
|
||||
This is your main dashboard page.
|
||||
{isLoading ? (
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
</Card>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import { describe, it, expect, vi } from 'vitest'
|
|||
import { render, screen, waitFor } from '@/test/test-utils'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import LoginPage from '../index'
|
||||
import { adminApiHelpers } from '@/lib/api-client'
|
||||
import { authService } from '@/services'
|
||||
|
||||
// Mock the API client
|
||||
vi.mock('@/lib/api-client', () => ({
|
||||
adminApiHelpers: {
|
||||
// Mock the service layer
|
||||
vi.mock('@/services', () => ({
|
||||
authService: {
|
||||
login: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
|
@ -52,11 +52,11 @@ describe('LoginPage', () => {
|
|||
|
||||
it('should handle form submission', async () => {
|
||||
const user = userEvent.setup()
|
||||
const mockLogin = vi.mocked(adminApiHelpers.login)
|
||||
const mockLogin = vi.mocked(authService.login)
|
||||
|
||||
mockLogin.mockResolvedValue({
|
||||
data: {
|
||||
access_token: 'fake-token',
|
||||
accessToken: 'fake-token',
|
||||
refreshToken: 'fake-refresh-token',
|
||||
user: {
|
||||
id: '1',
|
||||
email: 'admin@example.com',
|
||||
|
|
@ -64,8 +64,7 @@ describe('LoginPage', () => {
|
|||
firstName: 'Admin',
|
||||
lastName: 'User',
|
||||
},
|
||||
},
|
||||
} as any)
|
||||
})
|
||||
|
||||
render(<LoginPage />)
|
||||
|
||||
|
|
@ -87,18 +86,19 @@ describe('LoginPage', () => {
|
|||
|
||||
it('should show error for non-admin users', async () => {
|
||||
const user = userEvent.setup()
|
||||
const mockLogin = vi.mocked(adminApiHelpers.login)
|
||||
const mockLogin = vi.mocked(authService.login)
|
||||
|
||||
mockLogin.mockResolvedValue({
|
||||
data: {
|
||||
access_token: 'fake-token',
|
||||
accessToken: 'fake-token',
|
||||
refreshToken: 'fake-refresh-token',
|
||||
user: {
|
||||
id: '1',
|
||||
email: 'user@example.com',
|
||||
firstName: 'User',
|
||||
lastName: 'Test',
|
||||
role: 'USER', // Not ADMIN
|
||||
},
|
||||
},
|
||||
} as any)
|
||||
})
|
||||
|
||||
render(<LoginPage />)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input"
|
|||
import { Label } from "@/components/ui/label"
|
||||
import { Eye, EyeOff } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import { adminApiHelpers } from "@/lib/api-client"
|
||||
import { authService } from "@/services"
|
||||
import { errorTracker } from "@/lib/error-tracker"
|
||||
|
||||
export default function LoginPage() {
|
||||
|
|
@ -24,58 +24,37 @@ export default function LoginPage() {
|
|||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const response = await adminApiHelpers.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
|
||||
const response = await authService.login({ email, password })
|
||||
|
||||
// Check if user is admin
|
||||
if (user.role !== 'ADMIN') {
|
||||
if (response.user.role !== 'ADMIN') {
|
||||
toast.error("Access denied. Admin privileges required.")
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Store tokens and user data
|
||||
if (access_token) {
|
||||
localStorage.setItem('access_token', access_token)
|
||||
console.log('Access token stored in localStorage') // Debug log
|
||||
} else {
|
||||
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
|
||||
// Set user context for error tracking
|
||||
errorTracker.setUser({
|
||||
id: response.user.id,
|
||||
email: response.user.email,
|
||||
name: `${response.user.firstName} ${response.user.lastName}`,
|
||||
})
|
||||
|
||||
// Show success message
|
||||
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
|
||||
console.log('Navigating to:', from) // Debug log
|
||||
navigate(from, { replace: true })
|
||||
} 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"
|
||||
toast.error(message)
|
||||
|
||||
// Track login error
|
||||
errorTracker.trackError(error, {
|
||||
extra: { email, action: 'login' }
|
||||
})
|
||||
|
||||
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