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