refactor(services): Reorganize API layer and consolidate documentation

This commit is contained in:
debudebuye 2026-02-24 17:25:14 +03:00
parent 6021d83385
commit a1c9b689d5
52 changed files with 2575 additions and 4838 deletions

View File

@ -2,6 +2,8 @@
Admin dashboard for Yaltopia Ticket management system built with React, TypeScript, and Vite. Admin dashboard for Yaltopia Ticket management system built with React, TypeScript, and Vite.
> 📚 **For detailed documentation, see [dev-docs/](./dev-docs/README.md)**
## Features ## Features
- User Management - User Management

983
dev-docs/API_GUIDE.md Normal file
View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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! 🚀**

View File

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

View File

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

View File

@ -1,89 +1,99 @@
# Developer Documentation # Developer Documentation
This directory contains comprehensive documentation for the Yaltopia Ticket Admin project. Essential documentation for the Yaltopia Ticket Admin project.
## 📚 Documentation Index ## 📚 Documentation
### Getting Started ### [Development Guide](./DEVELOPMENT.md)
- **[Quick Reference](./QUICK_REFERENCE.md)** - Quick start guide and common commands Complete development guide including:
- **[Tech Stack](./TECH_STACK.md)** - Technologies and frameworks used - Tech stack & project structure
- Quick start & setup
- Common tasks & best practices
- Troubleshooting
### Development ### [API & Service Layer Guide](./API_GUIDE.md) ⭐
- **[Authentication](./AUTHENTICATION.md)** - Authentication setup and flow **Essential reading for making API calls:**
- **[API Standards](./API_STANDARDS.md)** - API client implementation and best practices - Service layer architecture
- **[Login API Documentation](./LOGIN_API_DOCUMENTATION.md)** - Login endpoint specifications - All available services & methods
- **[Troubleshooting](./TROUBLESHOOTING.md)** - Common issues and solutions - Common patterns & examples
- Error handling
- Best practices
### Testing & Quality ### [Testing Guide](./TESTING_GUIDE.md)
- **[Testing Guide](./TESTING_GUIDE.md)** - Testing setup and best practices Testing setup and practices:
- **[CI/CD Setup](./CI_CD_SETUP.md)** - Continuous integration and deployment - Unit testing with Vitest
- **[Error Monitoring](./ERROR_MONITORING.md)** - Sentry integration and error tracking - Component testing
- Integration testing
- Test utilities & mocks
### Security ### [Deployment Guide](./DEPLOYMENT.md)
- **[Security Checklist](./SECURITY_CHECKLIST.md)** - Comprehensive security requirements Production deployment:
- **[Security](./SECURITY.md)** - Security best practices and guidelines - Pre-deployment checklist
- Deployment options (Vercel, Netlify, Docker)
- Environment configuration
- CI/CD setup
### Deployment ### [Security Guide](./SECURITY.md)
- **[Deployment Options](./DEPLOYMENT_OPTIONS.md)** - All deployment configurations (Vercel, Netlify, Docker) Security best practices:
- **[Deployment Guide](./DEPLOYMENT.md)** - Step-by-step deployment instructions - Authentication & authorization
- **[Pre-Deployment Checklist](./PRE_DEPLOYMENT_CHECKLIST.md)** - Checklist before going live - Data protection
- **[Production Ready Summary](./PRODUCTION_READY_SUMMARY.md)** - Production readiness overview - Security headers
- CORS configuration
- Input validation
## 🎯 Quick Links ## 🚀 Quick Start
### For Developers ```bash
1. Start with [Quick Reference](./QUICK_REFERENCE.md) # Install
2. Understand [Tech Stack](./TECH_STACK.md) npm install
3. Set up [Authentication](./AUTHENTICATION.md)
4. Review [API Standards](./API_STANDARDS.md)
### For DevOps # Configure
1. Review [CI/CD Setup](./CI_CD_SETUP.md) cp .env.example .env
2. Choose deployment from [Deployment Options](./DEPLOYMENT_OPTIONS.md) # Edit .env with your backend URL
3. Follow [Deployment Guide](./DEPLOYMENT.md)
4. Complete [Pre-Deployment Checklist](./PRE_DEPLOYMENT_CHECKLIST.md)
### For Security Review # Develop
1. Review [Security Checklist](./SECURITY_CHECKLIST.md) npm run dev
2. Check [Security](./SECURITY.md) guidelines
3. Verify [API Standards](./API_STANDARDS.md) compliance
### For Troubleshooting # Test
1. Check [Troubleshooting](./TROUBLESHOOTING.md) guide npm run test
2. Review [Error Monitoring](./ERROR_MONITORING.md) setup
3. Consult [API Standards](./API_STANDARDS.md) for API issues
## 📖 Documentation Standards # Build
npm run build
```
All documentation follows these principles: ## 📖 Key Concepts
- **Clear and Concise** - Easy to understand
- **Actionable** - Includes examples and commands
- **Up-to-date** - Reflects current implementation
- **Professional** - Industry-standard practices
## 🔄 Keeping Documentation Updated ### Service Layer
All API calls go through typed service classes:
```typescript
import { userService } from '@/services'
const users = await userService.getUsers()
```
When making changes to the project: ### React Query
1. Update relevant documentation Data fetching with caching:
2. Add new sections if needed ```typescript
3. Remove outdated information const { data } = useQuery({
4. Keep examples current queryKey: ['users'],
queryFn: () => userService.getUsers()
})
```
## 📝 Contributing to Documentation ### Protected Routes
Authentication required for admin routes:
To improve documentation: ```typescript
1. Identify gaps or unclear sections <Route element={<ProtectedRoute />}>
2. Add examples and use cases <Route path="/admin/*" element={<AdminLayout />} />
3. Include troubleshooting tips </Route>
4. Keep formatting consistent ```
## 🆘 Need Help? ## 🆘 Need Help?
If documentation is unclear or missing: 1. **Making API calls?** → [API & Service Layer Guide](./API_GUIDE.md)
1. Check [Troubleshooting](./TROUBLESHOOTING.md) 2. **General development?** → [Development Guide](./DEVELOPMENT.md)
2. Review related documentation 3. **Writing tests?** → [Testing Guide](./TESTING_GUIDE.md)
3. Check code comments 4. **Deploying?** → [Deployment Guide](./DEPLOYMENT.md)
4. Consult team members 5. **Security questions?** → [Security Guide](./SECURITY.md)
--- ---

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Avatar, AvatarFallback } from "@/components/ui/avatar" import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { adminApiHelpers } from "@/lib/api-client" import { authService } from "@/services"
interface User { interface User {
email: string email: string
@ -70,8 +70,8 @@ export function AppShell() {
return item?.label || "Admin Panel" return item?.label || "Admin Panel"
} }
const handleLogout = () => { const handleLogout = async () => {
adminApiHelpers.logout() await authService.logout()
navigate('/login', { replace: true }) navigate('/login', { replace: true })
} }

View File

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

View File

@ -1,5 +1,5 @@
import { Sentry } from './sentry' import { Sentry } from './sentry'
import adminApi from './api-client' import apiClient from '@/services/api/client'
interface ErrorLog { interface ErrorLog {
message: string message: string
@ -105,7 +105,7 @@ class ErrorTracker {
try { try {
// Send to your backend error logging endpoint // Send to your backend error logging endpoint
await adminApi.post('/errors/log', errorLog) await apiClient.post('/errors/log', errorLog)
this.queue.shift() // Remove from queue on success this.queue.shift() // Remove from queue on success
} catch (error) { } catch (error) {
console.error('Failed to log error to backend:', error) console.error('Failed to log error to backend:', error)

View File

@ -8,23 +8,17 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { adminApiHelpers } from "@/lib/api-client" import { analyticsService } from "@/services"
export default function AnalyticsApiPage() { export default function AnalyticsApiPage() {
const { data: apiUsage, isLoading } = useQuery({ const { data: apiUsage, isLoading } = useQuery({
queryKey: ['admin', 'analytics', 'api-usage'], queryKey: ['admin', 'analytics', 'api-usage'],
queryFn: async () => { queryFn: () => analyticsService.getApiUsage(7),
const response = await adminApiHelpers.getApiUsage(7)
return response.data
},
}) })
const { data: errorRate, isLoading: errorRateLoading } = useQuery({ const { data: errorRate, isLoading: errorRateLoading } = useQuery({
queryKey: ['admin', 'analytics', 'error-rate'], queryKey: ['admin', 'analytics', 'error-rate'],
queryFn: async () => { queryFn: () => analyticsService.getErrorRate(7),
const response = await adminApiHelpers.getErrorRate(7)
return response.data
},
}) })
return ( return (

View File

@ -1,15 +1,12 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts" import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
import { adminApiHelpers } from "@/lib/api-client" import { analyticsService } from "@/services"
export default function AnalyticsRevenuePage() { export default function AnalyticsRevenuePage() {
const { data: revenue, isLoading } = useQuery({ const { data: revenue, isLoading } = useQuery({
queryKey: ['admin', 'analytics', 'revenue'], queryKey: ['admin', 'analytics', 'revenue'],
queryFn: async () => { queryFn: () => analyticsService.getRevenue('90days'),
const response = await adminApiHelpers.getRevenue('90days')
return response.data
},
}) })
return ( return (

View File

@ -1,17 +1,14 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from "recharts" import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from "recharts"
import { adminApiHelpers } from "@/lib/api-client" import { analyticsService } from "@/services"
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8'] const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']
export default function AnalyticsStoragePage() { export default function AnalyticsStoragePage() {
const { data: storage, isLoading } = useQuery({ const { data: storage, isLoading } = useQuery({
queryKey: ['admin', 'analytics', 'storage'], queryKey: ['admin', 'analytics', 'storage'],
queryFn: async () => { queryFn: () => analyticsService.getStorageAnalytics(),
const response = await adminApiHelpers.getStorageAnalytics()
return response.data
},
}) })
const formatBytes = (bytes: number) => { const formatBytes = (bytes: number) => {

View File

@ -1,15 +1,12 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts" import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
import { adminApiHelpers } from "@/lib/api-client" import { analyticsService } from "@/services"
export default function AnalyticsUsersPage() { export default function AnalyticsUsersPage() {
const { data: userGrowth, isLoading } = useQuery({ const { data: userGrowth, isLoading } = useQuery({
queryKey: ['admin', 'analytics', 'users', 'growth'], queryKey: ['admin', 'analytics', 'users', 'growth'],
queryFn: async () => { queryFn: () => analyticsService.getUserGrowth(90),
const response = await adminApiHelpers.getUserGrowth(90)
return response.data
},
}) })
return ( return (

View File

@ -20,7 +20,7 @@ import {
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Plus, Edit, Trash2 } from "lucide-react" import { Plus, Edit, Trash2 } from "lucide-react"
import { adminApiHelpers } from "@/lib/api-client" import { announcementService } from "@/services"
import { toast } from "sonner" import { toast } from "sonner"
import { format } from "date-fns" import { format } from "date-fns"
@ -31,16 +31,11 @@ export default function AnnouncementsPage() {
const { data: announcements, isLoading } = useQuery({ const { data: announcements, isLoading } = useQuery({
queryKey: ['admin', 'announcements'], queryKey: ['admin', 'announcements'],
queryFn: async () => { queryFn: () => announcementService.getAnnouncements(false),
const response = await adminApiHelpers.getAnnouncements(false)
return response.data
},
}) })
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: async (id: string) => { mutationFn: (id: string) => announcementService.deleteAnnouncement(id),
await adminApiHelpers.deleteAnnouncement(id)
},
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] }) queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] })
toast.success("Announcement deleted successfully") toast.success("Announcement deleted successfully")

View File

@ -13,7 +13,7 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { Search, Eye } from "lucide-react" import { Search, Eye } from "lucide-react"
import { adminApiHelpers } from "@/lib/api-client" import { auditService } from "@/services"
import { format } from "date-fns" import { format } from "date-fns"
export default function AuditPage() { export default function AuditPage() {
@ -26,8 +26,7 @@ export default function AuditPage() {
queryFn: async () => { queryFn: async () => {
const params: any = { page, limit } const params: any = { page, limit }
if (search) params.search = search if (search) params.search = search
const response = await adminApiHelpers.getAuditLogs(params) return await auditService.getAuditLogs(params)
return response.data
}, },
}) })

View File

@ -2,49 +2,34 @@ import { useQuery } from "@tanstack/react-query"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Download, Users, FileText, DollarSign, HardDrive, AlertCircle } from "lucide-react" import { Download, Users, FileText, DollarSign, HardDrive, AlertCircle } from "lucide-react"
import { adminApiHelpers } from "@/lib/api-client" import { analyticsService, systemService } from "@/services"
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts" import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
import { toast } from "sonner" import { toast } from "sonner"
export default function DashboardPage() { export default function DashboardPage() {
const { data: overview, isLoading: overviewLoading } = useQuery({ const { data: overview, isLoading: overviewLoading } = useQuery({
queryKey: ['admin', 'analytics', 'overview'], queryKey: ['admin', 'analytics', 'overview'],
queryFn: async () => { queryFn: () => analyticsService.getOverview(),
const response = await adminApiHelpers.getOverview()
return response.data
},
}) })
const { data: userGrowth, isLoading: growthLoading } = useQuery({ const { data: userGrowth, isLoading: growthLoading } = useQuery({
queryKey: ['admin', 'analytics', 'users', 'growth'], queryKey: ['admin', 'analytics', 'users', 'growth'],
queryFn: async () => { queryFn: () => analyticsService.getUserGrowth(30),
const response = await adminApiHelpers.getUserGrowth(30)
return response.data
},
}) })
const { data: revenue, isLoading: revenueLoading } = useQuery({ const { data: revenue, isLoading: revenueLoading } = useQuery({
queryKey: ['admin', 'analytics', 'revenue'], queryKey: ['admin', 'analytics', 'revenue'],
queryFn: async () => { queryFn: () => analyticsService.getRevenue('30days'),
const response = await adminApiHelpers.getRevenue('30days')
return response.data
},
}) })
const { data: health, isLoading: healthLoading } = useQuery({ const { data: health, isLoading: healthLoading } = useQuery({
queryKey: ['admin', 'system', 'health'], queryKey: ['admin', 'system', 'health'],
queryFn: async () => { queryFn: () => systemService.getHealth(),
const response = await adminApiHelpers.getHealth()
return response.data
},
}) })
const { data: errorRate, isLoading: errorRateLoading } = useQuery({ const { data: errorRate, isLoading: errorRateLoading } = useQuery({
queryKey: ['admin', 'analytics', 'error-rate'], queryKey: ['admin', 'analytics', 'error-rate'],
queryFn: async () => { queryFn: () => analyticsService.getErrorRate(7),
const response = await adminApiHelpers.getErrorRate(7)
return response.data
},
}) })
const handleExport = () => { const handleExport = () => {

View File

@ -2,27 +2,21 @@ import { useQuery } from "@tanstack/react-query"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { AlertCircle, CheckCircle, XCircle, Users } from "lucide-react" import { AlertCircle, CheckCircle, XCircle, Users } from "lucide-react"
import { adminApiHelpers } from "@/lib/api-client" import { systemService } from "@/services"
export default function HealthPage() { export default function HealthPage() {
const { data: health, isLoading: healthLoading } = useQuery({ const { data: health, isLoading: healthLoading } = useQuery({
queryKey: ['admin', 'system', 'health'], queryKey: ['admin', 'system', 'health'],
queryFn: async () => { queryFn: () => systemService.getHealth(),
const response = await adminApiHelpers.getHealth()
return response.data
},
refetchInterval: 30000, // Refetch every 30 seconds refetchInterval: 30000, // Refetch every 30 seconds
}) })
const { data: systemInfo, isLoading: infoLoading } = useQuery({ const { data: systemInfo, isLoading: infoLoading } = useQuery({
queryKey: ['admin', 'system', 'info'], queryKey: ['admin', 'system', 'info'],
queryFn: async () => { queryFn: () => systemService.getSystemInfo(),
const response = await adminApiHelpers.getSystemInfo()
return response.data
},
}) })
const getStatusIcon = (status: string) => { const getStatusIcon = (status?: string) => {
switch (status?.toLowerCase()) { switch (status?.toLowerCase()) {
case 'healthy': case 'healthy':
case 'connected': case 'connected':
@ -142,19 +136,19 @@ export default function HealthPage() {
</div> </div>
<div> <div>
<p className="text-sm text-muted-foreground">Platform</p> <p className="text-sm text-muted-foreground">Platform</p>
<p className="font-medium">{systemInfo.platform}</p> <p className="font-medium">{systemInfo.platform || 'N/A'}</p>
</div> </div>
<div> <div>
<p className="text-sm text-muted-foreground">Architecture</p> <p className="text-sm text-muted-foreground">Architecture</p>
<p className="font-medium">{systemInfo.architecture}</p> <p className="font-medium">{systemInfo.architecture || 'N/A'}</p>
</div> </div>
<div> <div>
<p className="text-sm text-muted-foreground">Uptime</p> <p className="text-sm text-muted-foreground">Uptime</p>
<p className="font-medium">{formatUptime(systemInfo.uptime)}</p> <p className="font-medium">{formatUptime(systemInfo.uptime || 0)}</p>
</div> </div>
<div> <div>
<p className="text-sm text-muted-foreground">Environment</p> <p className="text-sm text-muted-foreground">Environment</p>
<p className="font-medium">{systemInfo.env}</p> <p className="font-medium">{systemInfo.env || systemInfo.environment}</p>
</div> </div>
<div> <div>
<p className="text-sm text-muted-foreground">Memory Usage</p> <p className="text-sm text-muted-foreground">Memory Usage</p>

View File

@ -4,7 +4,7 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { adminApiHelpers } from "@/lib/api-client" import { systemService } from "@/services"
import { toast } from "sonner" import { toast } from "sonner"
import { useState } from "react" import { useState } from "react"
@ -14,16 +14,11 @@ export default function MaintenancePage() {
const { data: status, isLoading } = useQuery({ const { data: status, isLoading } = useQuery({
queryKey: ['admin', 'maintenance'], queryKey: ['admin', 'maintenance'],
queryFn: async () => { queryFn: () => systemService.getMaintenanceStatus(),
const response = await adminApiHelpers.getMaintenanceStatus()
return response.data
},
}) })
const enableMutation = useMutation({ const enableMutation = useMutation({
mutationFn: async (msg?: string) => { mutationFn: (msg?: string) => systemService.enableMaintenance(msg),
await adminApiHelpers.enableMaintenance(msg)
},
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'maintenance'] }) queryClient.invalidateQueries({ queryKey: ['admin', 'maintenance'] })
toast.success("Maintenance mode enabled") toast.success("Maintenance mode enabled")
@ -35,9 +30,7 @@ export default function MaintenancePage() {
}) })
const disableMutation = useMutation({ const disableMutation = useMutation({
mutationFn: async () => { mutationFn: () => systemService.disableMaintenance(),
await adminApiHelpers.disableMaintenance()
},
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'maintenance'] }) queryClient.invalidateQueries({ queryKey: ['admin', 'maintenance'] })
toast.success("Maintenance mode disabled") toast.success("Maintenance mode disabled")

View File

@ -11,7 +11,7 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { Ban } from "lucide-react" import { Ban } from "lucide-react"
import { adminApiHelpers } from "@/lib/api-client" import { securityService } from "@/services"
import { toast } from "sonner" import { toast } from "sonner"
import { format } from "date-fns" import { format } from "date-fns"
@ -20,16 +20,11 @@ export default function ApiKeysPage() {
const { data: apiKeys, isLoading } = useQuery({ const { data: apiKeys, isLoading } = useQuery({
queryKey: ['admin', 'security', 'api-keys'], queryKey: ['admin', 'security', 'api-keys'],
queryFn: async () => { queryFn: () => securityService.getAllApiKeys(),
const response = await adminApiHelpers.getAllApiKeys()
return response.data
},
}) })
const revokeMutation = useMutation({ const revokeMutation = useMutation({
mutationFn: async (id: string) => { mutationFn: (id: string) => securityService.revokeApiKey(id),
await adminApiHelpers.revokeApiKey(id)
},
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'security', 'api-keys'] }) queryClient.invalidateQueries({ queryKey: ['admin', 'security', 'api-keys'] })
toast.success("API key revoked successfully") toast.success("API key revoked successfully")

View File

@ -13,7 +13,7 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { Search, Ban } from "lucide-react" import { Search, Ban } from "lucide-react"
import { adminApiHelpers } from "@/lib/api-client" import { securityService } from "@/services"
import { format } from "date-fns" import { format } from "date-fns"
export default function FailedLoginsPage() { export default function FailedLoginsPage() {
@ -26,8 +26,7 @@ export default function FailedLoginsPage() {
queryFn: async () => { queryFn: async () => {
const params: any = { page, limit } const params: any = { page, limit }
if (search) params.email = search if (search) params.email = search
const response = await adminApiHelpers.getFailedLogins(params) return await securityService.getFailedLogins(params)
return response.data
}, },
}) })

View File

@ -8,15 +8,12 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { adminApiHelpers } from "@/lib/api-client" import { securityService } from "@/services"
export default function RateLimitsPage() { export default function RateLimitsPage() {
const { data: violations, isLoading } = useQuery({ const { data: violations, isLoading } = useQuery({
queryKey: ['admin', 'security', 'rate-limits'], queryKey: ['admin', 'security', 'rate-limits'],
queryFn: async () => { queryFn: () => securityService.getRateLimitViolations(7),
const response = await adminApiHelpers.getRateLimitViolations(7)
return response.data
},
}) })
return ( return (

View File

@ -10,16 +10,13 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { LogOut } from "lucide-react" import { LogOut } from "lucide-react"
import { adminApiHelpers } from "@/lib/api-client" import { securityService } from "@/services"
import { format } from "date-fns" import { format } from "date-fns"
export default function SessionsPage() { export default function SessionsPage() {
const { data: sessions, isLoading } = useQuery({ const { data: sessions, isLoading } = useQuery({
queryKey: ['admin', 'security', 'sessions'], queryKey: ['admin', 'security', 'sessions'],
queryFn: async () => { queryFn: () => securityService.getActiveSessions(),
const response = await adminApiHelpers.getActiveSessions()
return response.data
},
}) })
return ( return (

View File

@ -2,15 +2,12 @@ import { useQuery } from "@tanstack/react-query"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Shield, Ban } from "lucide-react" import { Shield, Ban } from "lucide-react"
import { adminApiHelpers } from "@/lib/api-client" import { securityService } from "@/services"
export default function SuspiciousActivityPage() { export default function SuspiciousActivityPage() {
const { data: suspicious, isLoading } = useQuery({ const { data: suspicious, isLoading } = useQuery({
queryKey: ['admin', 'security', 'suspicious'], queryKey: ['admin', 'security', 'suspicious'],
queryFn: async () => { queryFn: () => securityService.getSuspiciousActivity(),
const response = await adminApiHelpers.getSuspiciousActivity()
return response.data
},
}) })
return ( return (
@ -28,9 +25,9 @@ export default function SuspiciousActivityPage() {
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
<div className="text-center py-8">Loading...</div> <div className="text-center py-8">Loading...</div>
) : suspicious?.suspiciousIPs?.length > 0 ? ( ) : (suspicious?.suspiciousIPs?.length ?? 0) > 0 ? (
<div className="space-y-2"> <div className="space-y-2">
{suspicious.suspiciousIPs.map((ip: any, index: number) => ( {suspicious?.suspiciousIPs?.map((ip: any, index: number) => (
<div key={index} className="flex items-center justify-between p-2 border rounded"> <div key={index} className="flex items-center justify-between p-2 border rounded">
<div> <div>
<p className="font-mono font-medium">{ip.ipAddress}</p> <p className="font-mono font-medium">{ip.ipAddress}</p>
@ -61,9 +58,9 @@ export default function SuspiciousActivityPage() {
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
<div className="text-center py-8">Loading...</div> <div className="text-center py-8">Loading...</div>
) : suspicious?.suspiciousEmails?.length > 0 ? ( ) : (suspicious?.suspiciousEmails?.length ?? 0) > 0 ? (
<div className="space-y-2"> <div className="space-y-2">
{suspicious.suspiciousEmails.map((email: any, index: number) => ( {suspicious?.suspiciousEmails?.map((email: any, index: number) => (
<div key={index} className="flex items-center justify-between p-2 border rounded"> <div key={index} className="flex items-center justify-between p-2 border rounded">
<div> <div>
<p className="font-medium">{email.email}</p> <p className="font-medium">{email.email}</p>

View File

@ -15,7 +15,7 @@ import {
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Plus } from "lucide-react" import { Plus } from "lucide-react"
import { adminApiHelpers } from "@/lib/api-client" import { settingsService } from "@/services"
import { toast } from "sonner" import { toast } from "sonner"
export default function SettingsPage() { export default function SettingsPage() {
@ -31,16 +31,12 @@ export default function SettingsPage() {
const { data: settings, isLoading } = useQuery({ const { data: settings, isLoading } = useQuery({
queryKey: ['admin', 'settings', selectedCategory], queryKey: ['admin', 'settings', selectedCategory],
queryFn: async () => { queryFn: () => settingsService.getSettings(selectedCategory),
const response = await adminApiHelpers.getSettings(selectedCategory)
return response.data
},
}) })
const updateSettingMutation = useMutation({ const updateSettingMutation = useMutation({
mutationFn: async ({ key, value }: { key: string; value: string }) => { mutationFn: ({ key, value }: { key: string; value: string }) =>
await adminApiHelpers.updateSetting(key, { value }) settingsService.updateSetting(key, { value }),
},
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] }) queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] })
toast.success("Setting updated successfully") toast.success("Setting updated successfully")
@ -51,15 +47,13 @@ export default function SettingsPage() {
}) })
const createSettingMutation = useMutation({ const createSettingMutation = useMutation({
mutationFn: async (data: { mutationFn: (data: {
key: string key: string
value: string value: string
category: string category: string
description?: string description?: string
isPublic?: boolean isPublic?: boolean
}) => { }) => settingsService.createSetting(data),
await adminApiHelpers.createSetting(data)
},
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] }) queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] })
toast.success("Setting created successfully") toast.success("Setting created successfully")

View File

@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { ArrowLeft } from "lucide-react" import { ArrowLeft } from "lucide-react"
import { adminApiHelpers } from "@/lib/api-client" import { userService } from "@/services"
import { format } from "date-fns" import { format } from "date-fns"
export default function UserActivityPage() { export default function UserActivityPage() {
@ -12,10 +12,7 @@ export default function UserActivityPage() {
const { data: activity, isLoading } = useQuery({ const { data: activity, isLoading } = useQuery({
queryKey: ['admin', 'users', id, 'activity'], queryKey: ['admin', 'users', id, 'activity'],
queryFn: async () => { queryFn: () => userService.getUserActivity(id!, 30),
const response = await adminApiHelpers.getUserActivity(id!, 30)
return response.data
},
enabled: !!id, enabled: !!id,
}) })

View File

@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { ArrowLeft, Edit, Key } from "lucide-react" import { ArrowLeft, Edit, Key } from "lucide-react"
import { adminApiHelpers } from "@/lib/api-client" import { userService } from "@/services"
import { format } from "date-fns" import { format } from "date-fns"
export default function UserDetailsPage() { export default function UserDetailsPage() {
@ -14,10 +14,7 @@ export default function UserDetailsPage() {
const { data: user, isLoading } = useQuery({ const { data: user, isLoading } = useQuery({
queryKey: ['admin', 'users', id], queryKey: ['admin', 'users', id],
queryFn: async () => { queryFn: () => userService.getUser(id!),
const response = await adminApiHelpers.getUser(id!)
return response.data
},
enabled: !!id, enabled: !!id,
}) })
@ -90,7 +87,9 @@ export default function UserDetailsPage() {
</div> </div>
<div> <div>
<p className="text-sm text-muted-foreground">Updated At</p> <p className="text-sm text-muted-foreground">Updated At</p>
<p className="font-medium">{format(new Date(user.updatedAt), 'PPpp')}</p> <p className="font-medium">
{user.updatedAt ? format(new Date(user.updatedAt), 'PPpp') : 'N/A'}
</p>
</div> </div>
</div> </div>
</CardContent> </CardContent>

View File

@ -30,7 +30,7 @@ import {
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Search, Download, Eye, UserPlus, Trash2, Key, Upload } from "lucide-react" import { Search, Download, Eye, UserPlus, Trash2, Key, Upload } from "lucide-react"
import { adminApiHelpers } from "@/lib/api-client" import { userService } from "@/services"
import { toast } from "sonner" import { toast } from "sonner"
import { format } from "date-fns" import { format } from "date-fns"
@ -55,15 +55,13 @@ export default function UsersPage() {
if (search) params.search = search if (search) params.search = search
if (roleFilter !== 'all') params.role = roleFilter if (roleFilter !== 'all') params.role = roleFilter
if (statusFilter !== 'all') params.isActive = statusFilter === 'active' if (statusFilter !== 'all') params.isActive = statusFilter === 'active'
const response = await adminApiHelpers.getUsers(params) return await userService.getUsers(params)
return response.data
}, },
}) })
const deleteUserMutation = useMutation({ const deleteUserMutation = useMutation({
mutationFn: async ({ id, hard }: { id: string; hard: boolean }) => { mutationFn: ({ id, hard }: { id: string; hard: boolean }) =>
await adminApiHelpers.deleteUser(id, hard) userService.deleteUser(id, hard),
},
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }) queryClient.invalidateQueries({ queryKey: ['admin', 'users'] })
toast.success("User deleted successfully") toast.success("User deleted successfully")
@ -75,10 +73,7 @@ export default function UsersPage() {
}) })
const resetPasswordMutation = useMutation({ const resetPasswordMutation = useMutation({
mutationFn: async (id: string) => { mutationFn: (id: string) => userService.resetPassword(id),
const response = await adminApiHelpers.resetPassword(id)
return response.data
},
onSuccess: (data) => { onSuccess: (data) => {
toast.success(`Password reset. Temporary password: ${data.temporaryPassword}`) toast.success(`Password reset. Temporary password: ${data.temporaryPassword}`)
setResetPasswordDialogOpen(false) setResetPasswordDialogOpen(false)
@ -89,18 +84,12 @@ export default function UsersPage() {
}) })
const importUsersMutation = useMutation({ const importUsersMutation = useMutation({
mutationFn: async (file: File) => { mutationFn: (file: File) => userService.importUsers(file),
const response = await adminApiHelpers.importUsers(file)
return response.data
},
onSuccess: (data) => { onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }) queryClient.invalidateQueries({ queryKey: ['admin', 'users'] })
toast.success(`Imported ${data.success} users. ${data.failed} failed.`) toast.success(`Imported ${data.imported} users. ${data.failed} failed.`)
setImportDialogOpen(false) setImportDialogOpen(false)
setImportFile(null) setImportFile(null)
if (data.errors && data.errors.length > 0) {
console.error('Import errors:', data.errors)
}
}, },
onError: (error: any) => { onError: (error: any) => {
toast.error(error.response?.data?.message || "Failed to import users") toast.error(error.response?.data?.message || "Failed to import users")
@ -109,8 +98,7 @@ export default function UsersPage() {
const handleExport = async () => { const handleExport = async () => {
try { try {
const response = await adminApiHelpers.exportUsers('csv') const blob = await userService.exportUsers('csv')
const blob = new Blob([response.data], { type: 'text/csv' })
const url = window.URL.createObjectURL(blob) const url = window.URL.createObjectURL(blob)
const a = document.createElement('a') const a = document.createElement('a')
a.href = url a.href = url

View File

@ -1,35 +1,175 @@
import { useQuery } from "@tanstack/react-query"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Download } from "lucide-react" import { Download, FileText, DollarSign, CreditCard, TrendingUp } from "lucide-react"
import { dashboardService } from "@/services"
import { toast } from "sonner"
export default function DashboardPage() { export default function DashboardPage() {
const { data: profile } = useQuery({
queryKey: ['user', 'profile'],
queryFn: () => dashboardService.getUserProfile(),
})
const { data: stats, isLoading } = useQuery({
queryKey: ['user', 'stats'],
queryFn: () => dashboardService.getUserStats(),
})
const handleExport = () => {
toast.success("Exporting your data...")
}
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount)
}
const getGreeting = () => {
const hour = new Date().getHours()
if (hour < 12) return 'Good Morning'
if (hour < 18) return 'Good Afternoon'
return 'Good Evening'
}
const userName = profile ? `${profile.firstName} ${profile.lastName}` : 'User'
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-3xl font-bold">Good Morning, Admin</h2> <h2 className="text-3xl font-bold">{getGreeting()}, {userName}</h2>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
01 Sep - 15 Sep 2024 {new Date().toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
})}
</div> </div>
<Button variant="outline"> <Button variant="outline" onClick={handleExport}>
<Download className="w-4 h-4 mr-2" /> <Download className="w-4 h-4 mr-2" />
Export Data Export Data
</Button> </Button>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card> <Card>
<CardHeader> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>Welcome to Dashboard</CardTitle> <CardTitle className="text-sm font-medium">Total Invoices</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-muted-foreground"> {isLoading ? (
This is your main dashboard page. <div className="text-2xl font-bold">...</div>
</p> ) : (
<>
<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> </CardContent>
</Card> </Card>
</div> </div>
{stats?.recentActivity && stats.recentActivity.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{stats.recentActivity.map((activity) => (
<div key={activity.id} className="flex items-center justify-between border-b pb-3 last:border-0">
<div>
<p className="font-medium">{activity.description}</p>
<p className="text-sm text-muted-foreground">
{new Date(activity.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</p>
</div>
{activity.amount && (
<div className="text-right">
<p className="font-semibold">{formatCurrency(activity.amount)}</p>
</div>
)}
</div>
))}
</div>
</CardContent>
</Card>
)}
</div> </div>
) )
} }

View File

@ -2,11 +2,11 @@ import { describe, it, expect, vi } from 'vitest'
import { render, screen, waitFor } from '@/test/test-utils' import { render, screen, waitFor } from '@/test/test-utils'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import LoginPage from '../index' import LoginPage from '../index'
import { adminApiHelpers } from '@/lib/api-client' import { authService } from '@/services'
// Mock the API client // Mock the service layer
vi.mock('@/lib/api-client', () => ({ vi.mock('@/services', () => ({
adminApiHelpers: { authService: {
login: vi.fn(), login: vi.fn(),
}, },
})) }))
@ -52,20 +52,19 @@ describe('LoginPage', () => {
it('should handle form submission', async () => { it('should handle form submission', async () => {
const user = userEvent.setup() const user = userEvent.setup()
const mockLogin = vi.mocked(adminApiHelpers.login) const mockLogin = vi.mocked(authService.login)
mockLogin.mockResolvedValue({ mockLogin.mockResolvedValue({
data: { accessToken: 'fake-token',
access_token: 'fake-token', refreshToken: 'fake-refresh-token',
user: { user: {
id: '1', id: '1',
email: 'admin@example.com', email: 'admin@example.com',
role: 'ADMIN', role: 'ADMIN',
firstName: 'Admin', firstName: 'Admin',
lastName: 'User', lastName: 'User',
},
}, },
} as any) })
render(<LoginPage />) render(<LoginPage />)
@ -87,18 +86,19 @@ describe('LoginPage', () => {
it('should show error for non-admin users', async () => { it('should show error for non-admin users', async () => {
const user = userEvent.setup() const user = userEvent.setup()
const mockLogin = vi.mocked(adminApiHelpers.login) const mockLogin = vi.mocked(authService.login)
mockLogin.mockResolvedValue({ mockLogin.mockResolvedValue({
data: { accessToken: 'fake-token',
access_token: 'fake-token', refreshToken: 'fake-refresh-token',
user: { user: {
id: '1', id: '1',
email: 'user@example.com', email: 'user@example.com',
role: 'USER', // Not ADMIN firstName: 'User',
}, lastName: 'Test',
role: 'USER', // Not ADMIN
}, },
} as any) })
render(<LoginPage />) render(<LoginPage />)

View File

@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Eye, EyeOff } from "lucide-react" import { Eye, EyeOff } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
import { adminApiHelpers } from "@/lib/api-client" import { authService } from "@/services"
import { errorTracker } from "@/lib/error-tracker" import { errorTracker } from "@/lib/error-tracker"
export default function LoginPage() { export default function LoginPage() {
@ -24,58 +24,37 @@ export default function LoginPage() {
setIsLoading(true) setIsLoading(true)
try { try {
const response = await adminApiHelpers.login({ email, password }) const response = await authService.login({ email, password })
console.log('Login response:', response.data) // Debug log
// Handle different response formats
const responseData = response.data
const access_token = responseData.access_token || responseData.token || responseData.accessToken
const refresh_token = responseData.refresh_token || responseData.refreshToken
const user = responseData.user || responseData.data?.user || responseData
console.log('Extracted token:', access_token) // Debug log
console.log('Extracted user:', user) // Debug log
// Check if user is admin // Check if user is admin
if (user.role !== 'ADMIN') { if (response.user.role !== 'ADMIN') {
toast.error("Access denied. Admin privileges required.") toast.error("Access denied. Admin privileges required.")
setIsLoading(false) setIsLoading(false)
return return
} }
// Store tokens and user data // Set user context for error tracking
if (access_token) { errorTracker.setUser({
localStorage.setItem('access_token', access_token) id: response.user.id,
console.log('Access token stored in localStorage') // Debug log email: response.user.email,
} else { name: `${response.user.firstName} ${response.user.lastName}`,
console.warn('No access_token in response - assuming httpOnly cookies') // Debug log })
}
if (refresh_token) {
localStorage.setItem('refresh_token', refresh_token)
console.log('Refresh token stored in localStorage') // Debug log
}
localStorage.setItem('user', JSON.stringify(user))
console.log('User stored in localStorage') // Debug log
// Show success message // Show success message
toast.success("Login successful!") toast.success("Login successful!")
// Small delay to ensure localStorage is persisted
await new Promise(resolve => setTimeout(resolve, 100))
// Verify token is stored before navigation
const storedToken = localStorage.getItem('access_token')
console.log('Token verification before navigation:', storedToken) // Debug log
// Navigate to dashboard // Navigate to dashboard
console.log('Navigating to:', from) // Debug log
navigate(from, { replace: true }) navigate(from, { replace: true })
} catch (error: any) { } catch (error: any) {
console.error('Login error:', error) // Debug log console.error('Login error:', error)
const message = error.response?.data?.message || "Invalid email or password" const message = error.response?.data?.message || "Invalid email or password"
toast.error(message) toast.error(message)
// Track login error
errorTracker.trackError(error, {
extra: { email, action: 'login' }
})
setIsLoading(false) setIsLoading(false)
} }
} }

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

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

View 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

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

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

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

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

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

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

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