Files
ColaFlow/docs/frontend/api-integration-guide.md
Yaojia Wang fe8ad1c1f9
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
In progress
2025-11-03 11:51:02 +01:00

1183 lines
27 KiB
Markdown

# API Integration Guide - Frontend
## Document Overview
This guide provides complete documentation for integrating ColaFlow's frontend with the .NET 9 backend API, including all endpoints, request/response formats, error handling, and best practices.
**Base URL**: `http://localhost:5000/api` (Development)
**Production URL**: `https://api.colaflow.com/api`
---
## Table of Contents
1. [API Client Configuration](#api-client-configuration)
2. [Authentication APIs](#authentication-apis)
3. [Tenant Management APIs](#tenant-management-apis)
4. [MCP Token APIs](#mcp-token-apis)
5. [Error Handling](#error-handling)
6. [Rate Limiting](#rate-limiting)
7. [Testing with Mock Data](#testing-with-mock-data)
---
## API Client Configuration
### Axios Instance Setup
**File**: `lib/api-client.ts`
```typescript
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
import { useAuthStore } from '@/stores/useAuthStore';
const apiClient: AxiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true, // Important: Send httpOnly cookies
});
// Request Interceptor: Add Authorization header
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const { accessToken, tenant } = useAuthStore.getState();
// Add Bearer token
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
// Add tenant ID header (optional, for non-browser clients)
if (tenant && !config.headers['X-Tenant-Id']) {
config.headers['X-Tenant-Id'] = tenant.id;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response Interceptor: Handle token refresh
let isRefreshing = false;
let failedQueue: Array<{
resolve: (value?: unknown) => void;
reject: (reason?: unknown) => void;
}> = [];
const processQueue = (error: AxiosError | null, token: string | null = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
// If 401 error and not already retrying
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// If refresh is in progress, queue this request
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then(() => {
return apiClient(originalRequest);
})
.catch((err) => {
return Promise.reject(err);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
// Call refresh token endpoint
const response = await axios.post(
`${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`,
{},
{ withCredentials: true }
);
const { accessToken } = response.data;
// Update token in store
useAuthStore.getState().setAccessToken(accessToken);
// Update Authorization header
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
// Process queued requests
processQueue(null, accessToken);
// Retry original request
return apiClient(originalRequest);
} catch (refreshError) {
// Refresh failed, logout user
processQueue(refreshError as AxiosError, null);
useAuthStore.getState().clearAuth();
// Redirect to login
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
export default apiClient;
```
---
## Authentication APIs
### 1. Local Login
**Endpoint**: `POST /api/auth/login`
**Request**:
```typescript
interface LoginRequest {
email: string;
password: string;
rememberMe?: boolean;
}
```
**Example**:
```json
{
"email": "admin@acme.com",
"password": "SecurePassword123!",
"rememberMe": true
}
```
**Response (200 OK)**:
```typescript
interface LoginResponse {
user: {
id: string;
email: string;
fullName: string;
role: 'Admin' | 'Member' | 'Viewer';
authProvider: 'Local' | 'AzureAD' | 'Google' | 'Okta';
avatarUrl?: string;
};
tenant: {
id: string;
slug: string;
name: string;
plan: 'Free' | 'Starter' | 'Professional' | 'Enterprise';
status: 'Active' | 'Trial' | 'Suspended' | 'Cancelled';
ssoEnabled: boolean;
};
accessToken: string;
// Refresh token is set as httpOnly cookie automatically
}
```
**Example**:
```json
{
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "admin@acme.com",
"fullName": "John Doe",
"role": "Admin",
"authProvider": "Local",
"avatarUrl": null
},
"tenant": {
"id": "660e8400-e29b-41d4-a716-446655440001",
"slug": "acme",
"name": "Acme Corporation",
"plan": "Professional",
"status": "Active",
"ssoEnabled": true
},
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
**Error Responses**:
- `400 Bad Request`: Invalid input (email/password missing)
- `401 Unauthorized`: Invalid credentials
- `403 Forbidden`: Account suspended or disabled
- `404 Not Found`: Tenant not found
---
### 2. SSO Login Initiation
**Endpoint**: `GET /api/auth/sso/login?provider={provider}&redirect={redirect}`
**Query Parameters**:
- `provider` (required): `azuread` | `google` | `okta` | `saml`
- `redirect` (optional): URL to redirect after successful login (default: `/`)
**Example**:
```
GET /api/auth/sso/login?provider=azuread&redirect=/dashboard
```
**Response (302 Redirect)**:
Redirects to IdP login page with state parameter.
**Frontend Implementation**:
```typescript
// services/auth.service.ts
export const authService = {
loginWithSso: (provider: 'azuread' | 'google' | 'okta' | 'saml', redirectUrl?: string) => {
const params = new URLSearchParams({ provider });
if (redirectUrl) params.append('redirect', redirectUrl);
// Backend will redirect, so we navigate directly
window.location.href = `/api/auth/sso/login?${params.toString()}`;
},
};
```
---
### 3. SSO Callback
**Endpoint**: `GET /api/auth/sso/callback?code={code}&state={state}`
**Query Parameters**:
- `code` (required): Authorization code from IdP
- `state` (required): CSRF protection state
**Response (302 Redirect)**:
Redirects to frontend callback page with token:
```
https://app.colaflow.com/auth/callback?token={accessToken}&tenant={tenantSlug}
```
**Frontend Callback Handler**:
**File**: `app/(auth)/auth/callback/page.tsx`
```typescript
'use client';
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useAuthStore } from '@/stores/useAuthStore';
export default function SsoCallbackPage() {
const router = useRouter();
const searchParams = useSearchParams();
const [error, setError] = useState<string | null>(null);
const setUser = useAuthStore((state) => state.setUser);
useEffect(() => {
const handleCallback = async () => {
try {
// Get token from URL
const token = searchParams.get('token');
const tenantSlug = searchParams.get('tenant');
if (!token || !tenantSlug) {
throw new Error('Missing token or tenant in callback');
}
// Validate state parameter (if frontend initiated SSO)
const state = searchParams.get('state');
const storedState = sessionStorage.getItem('sso_state');
if (storedState && state !== storedState) {
throw new Error('Invalid state parameter (CSRF protection)');
}
// Clear stored state
sessionStorage.removeItem('sso_state');
// Decode JWT to get user info (or call /api/auth/me)
// For now, we'll call /api/auth/me with the token
const response = await fetch('/api/auth/me', {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error('Failed to fetch user info');
}
const data = await response.json();
// Store in AuthStore
setUser(data.user, data.tenant, token);
// Redirect to original page or dashboard
const redirectUrl = sessionStorage.getItem('redirect_after_login') || '/dashboard';
sessionStorage.removeItem('redirect_after_login');
router.push(redirectUrl);
} catch (err: any) {
console.error('SSO callback error:', err);
setError(err.message || 'SSO authentication failed');
}
};
handleCallback();
}, [searchParams, router, setUser]);
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="bg-red-50 border border-red-200 rounded-lg p-6 max-w-md">
<h2 className="text-lg font-semibold text-red-800 mb-2">
Authentication Failed
</h2>
<p className="text-red-700 mb-4">{error}</p>
<button
onClick={() => router.push('/login')}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Back to Login
</button>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4" />
<p className="text-gray-600">Completing sign-in...</p>
</div>
</div>
);
}
```
---
### 4. Logout
**Endpoint**: `POST /api/auth/logout`
**Request**: Empty body
**Response (200 OK)**:
```json
{
"message": "Logged out successfully"
}
```
**Frontend Implementation**:
```typescript
// hooks/auth/useLogout.ts
import { useMutation } from '@tanstack/react-query';
import { authService } from '@/services/auth.service';
import { useAuthStore } from '@/stores/useAuthStore';
import { useRouter } from 'next/navigation';
export function useLogout() {
const clearAuth = useAuthStore((state) => state.clearAuth);
const router = useRouter();
return useMutation({
mutationFn: authService.logout,
onSuccess: () => {
clearAuth();
router.push('/login');
},
});
}
```
---
### 5. Refresh Token
**Endpoint**: `POST /api/auth/refresh`
**Request**: Empty body (uses httpOnly cookie)
**Response (200 OK)**:
```json
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
**Error Responses**:
- `401 Unauthorized`: Refresh token expired or invalid
**Frontend Implementation**:
> Handled automatically by Axios interceptor (see [API Client Configuration](#api-client-configuration))
---
### 6. Get Current User
**Endpoint**: `GET /api/auth/me`
**Headers**:
```
Authorization: Bearer {accessToken}
```
**Response (200 OK)**:
```json
{
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "admin@acme.com",
"fullName": "John Doe",
"role": "Admin",
"authProvider": "AzureAD",
"avatarUrl": "https://graph.microsoft.com/v1.0/me/photo/$value"
},
"tenant": {
"id": "660e8400-e29b-41d4-a716-446655440001",
"slug": "acme",
"name": "Acme Corporation",
"plan": "Professional",
"status": "Active",
"ssoEnabled": true
}
}
```
---
## Tenant Management APIs
### 1. Register Tenant (Signup)
**Endpoint**: `POST /api/tenants/register`
**Request**:
```typescript
interface RegisterTenantRequest {
// Organization info
organizationName: string;
slug: string;
// Admin user info
adminEmail: string;
adminPassword: string;
adminFullName: string;
// Subscription
plan: 'Free' | 'Starter' | 'Professional' | 'Enterprise';
}
```
**Example**:
```json
{
"organizationName": "Acme Corporation",
"slug": "acme",
"adminEmail": "admin@acme.com",
"adminPassword": "SecurePassword123!",
"adminFullName": "John Doe",
"plan": "Professional"
}
```
**Response (201 Created)**:
```json
{
"tenant": {
"id": "660e8400-e29b-41d4-a716-446655440001",
"slug": "acme",
"name": "Acme Corporation",
"plan": "Professional",
"status": "Active"
},
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "admin@acme.com",
"fullName": "John Doe",
"role": "Admin"
},
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
**Error Responses**:
- `400 Bad Request`: Validation errors (slug format, password strength)
- `409 Conflict`: Slug already taken
---
### 2. Check Slug Availability
**Endpoint**: `GET /api/tenants/check-slug?slug={slug}`
**Query Parameters**:
- `slug` (required): Tenant slug to check
**Example**:
```
GET /api/tenants/check-slug?slug=acme
```
**Response (200 OK)**:
```json
{
"slug": "acme",
"available": false
}
```
**Frontend Implementation** (with debounce):
```typescript
// hooks/tenants/useCheckSlug.ts
import { useQuery } from '@tanstack/react-query';
import { tenantService } from '@/services/tenant.service';
import { useMemo } from 'react';
import debounce from 'lodash-es/debounce';
export function useCheckSlug(slug: string) {
// Debounce slug changes
const debouncedSlug = useMemo(
() => debounce((value: string) => value, 500),
[]
);
const trimmedSlug = slug.trim().toLowerCase();
return useQuery({
queryKey: ['check-slug', trimmedSlug],
queryFn: () => tenantService.checkSlugAvailability(trimmedSlug),
enabled: trimmedSlug.length >= 3, // Only check if at least 3 chars
staleTime: 5000, // Cache for 5 seconds
});
}
```
---
### 3. Get SSO Configuration
**Endpoint**: `GET /api/tenants/{tenantId}/sso`
**Headers**:
```
Authorization: Bearer {accessToken}
```
**Response (200 OK)**:
```json
{
"enabled": true,
"provider": "AzureAD",
"authority": "https://login.microsoftonline.com/tenant-id",
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"metadataUrl": "https://login.microsoftonline.com/tenant-id/v2.0/.well-known/openid-configuration",
"autoProvisionUsers": true,
"allowedDomains": ["acme.com", "acme.org"]
}
```
**Note**: `clientSecret` is never returned in API responses.
---
### 4. Update SSO Configuration
**Endpoint**: `PUT /api/tenants/{tenantId}/sso`
**Headers**:
```
Authorization: Bearer {accessToken}
```
**Request**:
```typescript
interface UpdateSsoConfigRequest {
provider: 'AzureAD' | 'Google' | 'Okta' | 'GenericSaml';
// OIDC fields (for AzureAD, Google, Okta)
authority?: string;
clientId?: string;
clientSecret?: string;
metadataUrl?: string;
// SAML fields (for GenericSaml)
entityId?: string;
signOnUrl?: string;
certificate?: string;
// Common fields
autoProvisionUsers?: boolean;
allowedDomains?: string[];
}
```
**Example (Azure AD)**:
```json
{
"provider": "AzureAD",
"authority": "https://login.microsoftonline.com/tenant-id",
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"clientSecret": "client-secret-here",
"metadataUrl": "https://login.microsoftonline.com/tenant-id/v2.0/.well-known/openid-configuration",
"autoProvisionUsers": true,
"allowedDomains": ["acme.com"]
}
```
**Response (200 OK)**:
```json
{
"message": "SSO configuration updated successfully"
}
```
**Error Responses**:
- `400 Bad Request`: Validation errors (missing fields, invalid URLs)
- `403 Forbidden`: Only Admin users can update SSO config
- `422 Unprocessable Entity`: SSO not available for Free plan
---
### 5. Test SSO Connection
**Endpoint**: `POST /api/tenants/{tenantId}/sso/test`
**Headers**:
```
Authorization: Bearer {accessToken}
```
**Response (200 OK)**:
```json
{
"success": true,
"message": "SSO connection successful"
}
```
**Response (200 OK) - Failed Test**:
```json
{
"success": false,
"message": "Failed to connect to IdP: Connection timeout"
}
```
---
## MCP Token APIs
### 1. List MCP Tokens
**Endpoint**: `GET /api/mcp/tokens`
**Headers**:
```
Authorization: Bearer {accessToken}
```
**Response (200 OK)**:
```json
{
"tokens": [
{
"id": "770e8400-e29b-41d4-a716-446655440002",
"name": "Claude AI Agent",
"permissions": {
"projects": ["read", "search"],
"issues": ["read", "create", "update", "search"],
"documents": ["read", "search"]
},
"status": "Active",
"createdAt": "2025-11-01T10:00:00Z",
"lastUsedAt": "2025-11-03T14:30:00Z",
"expiresAt": "2026-11-01T10:00:00Z",
"usageCount": 1234
}
]
}
```
---
### 2. Create MCP Token
**Endpoint**: `POST /api/mcp/tokens`
**Headers**:
```
Authorization: Bearer {accessToken}
```
**Request**:
```typescript
interface CreateMcpTokenRequest {
name: string;
permissions: {
[resource: string]: string[];
};
expiresAt?: string; // ISO 8601 date
ipWhitelist?: string[];
}
```
**Example**:
```json
{
"name": "Claude AI Agent",
"permissions": {
"projects": ["read", "search"],
"issues": ["read", "create", "update", "search"],
"documents": ["read", "create", "search"],
"reports": ["read"]
},
"expiresAt": "2026-11-01T00:00:00Z",
"ipWhitelist": ["192.168.1.100", "10.0.0.0/24"]
}
```
**Response (201 Created)**:
```json
{
"tokenId": "770e8400-e29b-41d4-a716-446655440002",
"token": "mcp_acme_7f3d8a9c4e1b2f5a6d8c9e0f1a2b3c4d",
"name": "Claude AI Agent",
"createdAt": "2025-11-03T10:00:00Z",
"expiresAt": "2026-11-01T00:00:00Z"
}
```
**IMPORTANT**: The `token` field is shown **only once**. It will never be returned again.
**Error Responses**:
- `400 Bad Request`: Invalid permissions or validation errors
- `403 Forbidden`: Only Admin users can create MCP tokens
- `422 Unprocessable Entity`: Invalid resource or operation names
---
### 3. Get MCP Token Details
**Endpoint**: `GET /api/mcp/tokens/{tokenId}`
**Headers**:
```
Authorization: Bearer {accessToken}
```
**Response (200 OK)**:
```json
{
"id": "770e8400-e29b-41d4-a716-446655440002",
"name": "Claude AI Agent",
"permissions": {
"projects": ["read", "search"],
"issues": ["read", "create", "update", "search"]
},
"status": "Active",
"createdAt": "2025-11-01T10:00:00Z",
"lastUsedAt": "2025-11-03T14:30:00Z",
"expiresAt": "2026-11-01T10:00:00Z",
"usageCount": 1234,
"ipWhitelist": ["192.168.1.100"]
}
```
---
### 4. Revoke MCP Token
**Endpoint**: `DELETE /api/mcp/tokens/{tokenId}`
**Headers**:
```
Authorization: Bearer {accessToken}
```
**Request Body** (Optional):
```json
{
"reason": "No longer needed"
}
```
**Response (200 OK)**:
```json
{
"message": "Token revoked successfully"
}
```
---
### 5. Get MCP Token Audit Logs
**Endpoint**: `GET /api/mcp/tokens/{tokenId}/audit-logs`
**Headers**:
```
Authorization: Bearer {accessToken}
```
**Query Parameters**:
- `page` (optional): Page number (default: 1)
- `limit` (optional): Results per page (default: 50, max: 100)
- `startDate` (optional): Filter by date range (ISO 8601)
- `endDate` (optional): Filter by date range (ISO 8601)
- `statusCode` (optional): Filter by HTTP status code (e.g., 200, 401, 403)
**Example**:
```
GET /api/mcp/tokens/{tokenId}/audit-logs?page=1&limit=50&statusCode=200
```
**Response (200 OK)**:
```json
{
"logs": [
{
"id": "880e8400-e29b-41d4-a716-446655440003",
"timestamp": "2025-11-03T14:30:15Z",
"httpMethod": "GET",
"endpoint": "/api/mcp/issues/search",
"statusCode": 200,
"durationMs": 125,
"ipAddress": "192.168.1.100",
"userAgent": "Claude-MCP-Client/1.0"
},
{
"id": "880e8400-e29b-41d4-a716-446655440004",
"timestamp": "2025-11-03T14:28:42Z",
"httpMethod": "POST",
"endpoint": "/api/mcp/issues",
"statusCode": 201,
"durationMs": 342,
"ipAddress": "192.168.1.100",
"userAgent": "Claude-MCP-Client/1.0"
}
],
"pagination": {
"page": 1,
"limit": 50,
"totalPages": 5,
"totalCount": 234
}
}
```
---
## Error Handling
### Standard Error Response Format
```typescript
interface ApiError {
error: string;
message: string;
statusCode: number;
timestamp: string;
details?: Record<string, string[]>; // Validation errors
}
```
**Example (400 Bad Request)**:
```json
{
"error": "Validation Error",
"message": "Invalid input data",
"statusCode": 400,
"timestamp": "2025-11-03T10:00:00Z",
"details": {
"email": ["Email is required", "Invalid email format"],
"password": ["Password must be at least 8 characters"]
}
}
```
### HTTP Status Codes
| Code | Meaning | Example |
|------|---------|---------|
| `200` | Success | Login successful |
| `201` | Created | Tenant registered |
| `204` | No Content | Token revoked |
| `400` | Bad Request | Invalid input data |
| `401` | Unauthorized | Invalid credentials or expired token |
| `403` | Forbidden | Insufficient permissions |
| `404` | Not Found | Tenant not found |
| `409` | Conflict | Slug already taken |
| `422` | Unprocessable Entity | Business logic error (e.g., SSO not available for Free plan) |
| `429` | Too Many Requests | Rate limit exceeded |
| `500` | Internal Server Error | Server error |
### Frontend Error Handling
**Global Error Handler**:
```typescript
// lib/api-client.ts (add to interceptor)
apiClient.interceptors.response.use(
(response) => response,
(error: AxiosError<ApiError>) => {
// Handle different error types
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 400:
// Show validation errors
if (data.details) {
Object.entries(data.details).forEach(([field, errors]) => {
errors.forEach((err) => {
toast.error(`${field}: ${err}`);
});
});
} else {
toast.error(data.message || 'Invalid input');
}
break;
case 401:
// Handled by token refresh interceptor
break;
case 403:
toast.error('You do not have permission to perform this action');
break;
case 404:
toast.error('Resource not found');
break;
case 409:
toast.error(data.message || 'Conflict: Resource already exists');
break;
case 422:
toast.error(data.message || 'Business logic error');
break;
case 429:
toast.error('Too many requests. Please try again later.');
break;
case 500:
toast.error('Server error. Please try again later.');
console.error('Server error:', data);
break;
default:
toast.error('An unexpected error occurred');
}
} else if (error.request) {
// Network error (no response)
toast.error('Network error. Please check your connection.');
} else {
// Other errors
toast.error('An unexpected error occurred');
}
return Promise.reject(error);
}
);
```
---
## Rate Limiting
### Rate Limit Headers
Backend includes rate limit information in response headers:
```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1699000000
```
### Frontend Rate Limit Handling
```typescript
// lib/api-client.ts
apiClient.interceptors.response.use(
(response) => {
// Check rate limit headers
const limit = response.headers['x-ratelimit-limit'];
const remaining = response.headers['x-ratelimit-remaining'];
const reset = response.headers['x-ratelimit-reset'];
if (remaining && parseInt(remaining) < 10) {
console.warn(`Rate limit warning: ${remaining}/${limit} requests remaining`);
}
return response;
},
(error) => {
if (error.response?.status === 429) {
const reset = error.response.headers['x-ratelimit-reset'];
const resetDate = new Date(parseInt(reset) * 1000);
toast.error(`Rate limit exceeded. Try again at ${resetDate.toLocaleTimeString()}`);
}
return Promise.reject(error);
}
);
```
---
## Testing with Mock Data
### MSW (Mock Service Worker) Setup
**File**: `mocks/handlers.ts`
```typescript
import { http, HttpResponse } from 'msw';
export const handlers = [
// Auth: Login
http.post('/api/auth/login', async ({ request }) => {
const body = await request.json();
if (body.email === 'admin@acme.com' && body.password === 'password') {
return HttpResponse.json({
user: {
id: '550e8400-e29b-41d4-a716-446655440000',
email: 'admin@acme.com',
fullName: 'John Doe',
role: 'Admin',
authProvider: 'Local',
},
tenant: {
id: '660e8400-e29b-41d4-a716-446655440001',
slug: 'acme',
name: 'Acme Corporation',
plan: 'Professional',
status: 'Active',
ssoEnabled: true,
},
accessToken: 'mock-access-token',
});
}
return HttpResponse.json(
{ error: 'Invalid credentials', message: 'Email or password is incorrect' },
{ status: 401 }
);
}),
// Tenants: Check Slug
http.get('/api/tenants/check-slug', ({ request }) => {
const url = new URL(request.url);
const slug = url.searchParams.get('slug');
const takenSlugs = ['acme', 'beta', 'test'];
return HttpResponse.json({
slug,
available: !takenSlugs.includes(slug || ''),
});
}),
// MCP: List Tokens
http.get('/api/mcp/tokens', () => {
return HttpResponse.json({
tokens: [
{
id: '770e8400-e29b-41d4-a716-446655440002',
name: 'Claude AI Agent',
permissions: {
projects: ['read', 'search'],
issues: ['read', 'create', 'update', 'search'],
},
status: 'Active',
createdAt: '2025-11-01T10:00:00Z',
lastUsedAt: '2025-11-03T14:30:00Z',
expiresAt: '2026-11-01T10:00:00Z',
usageCount: 1234,
},
],
});
}),
// MCP: Create Token
http.post('/api/mcp/tokens', async ({ request }) => {
const body = await request.json();
return HttpResponse.json(
{
tokenId: '770e8400-e29b-41d4-a716-446655440002',
token: 'mcp_acme_7f3d8a9c4e1b2f5a6d8c9e0f1a2b3c4d',
name: body.name,
createdAt: new Date().toISOString(),
expiresAt: body.expiresAt,
},
{ status: 201 }
);
}),
];
```
**Setup MSW**:
```typescript
// mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
```
**Enable MSW in Development**:
```typescript
// app/layout.tsx
if (process.env.NODE_ENV === 'development') {
import('../mocks/browser').then(({ worker }) => {
worker.start();
});
}
```
---
## Conclusion
This API integration guide provides all the information needed to integrate ColaFlow's frontend with the .NET 9 backend API. All endpoints are documented with complete request/response examples, error handling, and best practices.
**Key Takeaways**:
- ✅ Use Axios interceptors for automatic token injection and refresh
- ✅ Handle errors globally with consistent UI feedback
- ✅ Use TanStack Query for data fetching and caching
- ✅ Test with MSW before backend is ready
- ✅ Monitor rate limits to avoid 429 errors
**Next Document**: `state-management-guide.md` (Zustand + TanStack Query integration)