Yaltopia-Ticket-Admin/dev-docs/API_STANDARDS.md

477 lines
12 KiB
Markdown

# 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