1183 lines
27 KiB
Markdown
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)
|