🎓 Initial commit: Math2 Platform - Plataforma de Álgebra Lineal PRO
✨ Características: - 45 ejercicios universitarios (Basic → Advanced) - Renderizado LaTeX profesional - IA generativa (Z.ai/DashScope) - Docker 9 servicios - Tests 123/123 pasando - Seguridad enterprise (JWT, XSS, Rate limiting) 🐳 Infraestructura: - Next.js 14 + Node.js 20 - PostgreSQL 15 + Redis 7 - Docker Compose completo - Nginx + SSL ready 📚 Documentación: - 5 informes técnicos completos - README profesional - Scripts de deployment automatizados Estado: Producción lista ✅
This commit is contained in:
777
docs/API.md
Normal file
777
docs/API.md
Normal file
@@ -0,0 +1,777 @@
|
||||
# API Documentation
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
Development: http://localhost:3001/api
|
||||
Production: https://api.mathplatform.com/api
|
||||
```
|
||||
|
||||
## Autenticación
|
||||
|
||||
Todas las rutas (excepto auth) requieren header:
|
||||
```
|
||||
Authorization: Bearer {jwt_token}
|
||||
```
|
||||
|
||||
### Flujo de Autenticación
|
||||
|
||||
1. **Registro/Login**: Obtiene access token (15 min) + refresh token (7 días)
|
||||
2. **API Calls**: Usa access token en header
|
||||
3. **Refresh**: Cuando expira, usa refresh token para obtener nuevo access token
|
||||
4. **Logout**: Invalida refresh token (agregado a blacklist en Redis)
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Auth
|
||||
|
||||
#### POST /auth/register
|
||||
|
||||
Registra nuevo usuario.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "SecurePass123!",
|
||||
"firstName": "John",
|
||||
"lastName": "Doe"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"user": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"email": "user@example.com",
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"role": "USER",
|
||||
"createdAt": "2024-03-30T12:00:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /auth/login
|
||||
|
||||
Autentica usuario existente.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "SecurePass123!"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"user": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"email": "user@example.com",
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"role": "USER"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /auth/refresh
|
||||
|
||||
Renueva access token usando refresh token.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /auth/logout
|
||||
|
||||
Invalida tokens (agrega refresh token a blacklist).
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer {access_token}
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Logout successful"
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /auth/me
|
||||
|
||||
Obtiene perfil del usuario autenticado.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"email": "user@example.com",
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"role": "USER",
|
||||
"createdAt": "2024-03-30T12:00:00.000Z",
|
||||
"lastLoginAt": "2024-03-30T12:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Modules
|
||||
|
||||
#### GET /modules
|
||||
|
||||
Lista todos los módulos pedagógicos.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "mod-1",
|
||||
"name": "Fundamentos",
|
||||
"description": "Conceptos básicos de álgebra lineal",
|
||||
"type": "FUNDAMENTOS",
|
||||
"order": 1,
|
||||
"topicCount": 5,
|
||||
"exerciseCount": 25
|
||||
},
|
||||
{
|
||||
"id": "mod-2",
|
||||
"name": "Sistemas y Espacios",
|
||||
"description": "Sistemas de ecuaciones y espacios vectoriales",
|
||||
"type": "SISTEMAS_ESPACIOS",
|
||||
"order": 2,
|
||||
"topicCount": 4,
|
||||
"exerciseCount": 30
|
||||
},
|
||||
{
|
||||
"id": "mod-3",
|
||||
"name": "Aplicaciones",
|
||||
"description": "Aplicaciones prácticas de álgebra lineal",
|
||||
"type": "APLICACIONES",
|
||||
"order": 3,
|
||||
"topicCount": 3,
|
||||
"exerciseCount": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /modules/:id
|
||||
|
||||
Obtiene detalle de un módulo específico.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "mod-1",
|
||||
"name": "Fundamentos",
|
||||
"description": "Conceptos básicos de álgebra lineal",
|
||||
"type": "FUNDAMENTOS",
|
||||
"order": 1,
|
||||
"topics": [
|
||||
{
|
||||
"id": "topic-1",
|
||||
"name": "Vectores",
|
||||
"description": "Operaciones con vectores",
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"progress": {
|
||||
"completedExercises": 10,
|
||||
"totalExercises": 25,
|
||||
"percentage": 40
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Topics
|
||||
|
||||
#### GET /topics
|
||||
|
||||
Lista todos los temas.
|
||||
|
||||
**Query Parameters:**
|
||||
- `moduleId` (optional): Filtrar por módulo
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "topic-1",
|
||||
"name": "Vectores",
|
||||
"description": "Operaciones con vectores",
|
||||
"moduleId": "mod-1",
|
||||
"type": "VECTORES",
|
||||
"order": 1,
|
||||
"exerciseCount": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /topics/:id/theory
|
||||
|
||||
Obtiene contenido teórico de un tema.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "topic-1",
|
||||
"name": "Vectores",
|
||||
"content": {
|
||||
"introduction": "Los vectores son...",
|
||||
"formulas": [
|
||||
{
|
||||
"name": "Magnitud",
|
||||
"latex": "\\|\\vec{v}\\| = \\sqrt{x^2 + y^2}"
|
||||
}
|
||||
],
|
||||
"examples": [
|
||||
{
|
||||
"problem": "Calcular la magnitud del vector...",
|
||||
"solution": "Usando la fórmula...",
|
||||
"latex": "\\vec{v} = (3, 4)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Exercises
|
||||
|
||||
#### GET /exercises
|
||||
|
||||
Lista ejercicios con filtros.
|
||||
|
||||
**Query Parameters:**
|
||||
- `moduleId` (optional): Filtrar por módulo
|
||||
- `topicId` (optional): Filtrar por tema
|
||||
- `difficulty` (optional): BASIC | INTERMEDIATE | ADVANCED | EXPERT
|
||||
- `type` (optional): MULTIPLE_CHOICE | OPEN_RESPONSE | CALCULATION | PROOF | TRUE_FALSE
|
||||
- `limit` (optional): Número de resultados (default: 20)
|
||||
- `offset` (optional): Paginación (default: 0)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"exercises": [
|
||||
{
|
||||
"id": "ex-1",
|
||||
"title": "Suma de vectores",
|
||||
"description": "Calcular la suma de dos vectores",
|
||||
"type": "CALCULATION",
|
||||
"difficulty": "BASIC",
|
||||
"topicId": "topic-1",
|
||||
"latex": "\\vec{a} = (1, 2), \\vec{b} = (3, 4)",
|
||||
"points": 10,
|
||||
"hints": ["Recuerda sumar componente a componente"],
|
||||
"createdAt": "2024-03-30T12:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 100,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"hasMore": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /exercises/:id
|
||||
|
||||
Obtiene detalle de un ejercicio.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "ex-1",
|
||||
"title": "Suma de vectores",
|
||||
"description": "Calcular la suma de dos vectores",
|
||||
"type": "CALCULATION",
|
||||
"difficulty": "BASIC",
|
||||
"topic": {
|
||||
"id": "topic-1",
|
||||
"name": "Vectores"
|
||||
},
|
||||
"module": {
|
||||
"id": "mod-1",
|
||||
"name": "Fundamentos"
|
||||
},
|
||||
"latex": "\\vec{a} = (1, 2), \\vec{b} = (3, 4)",
|
||||
"questions": [
|
||||
{
|
||||
"id": "q-1",
|
||||
"text": "¿Cuál es la suma de los vectores?",
|
||||
"options": [
|
||||
"(4, 6)",
|
||||
"(3, 4)",
|
||||
"(1, 2)",
|
||||
"(5, 8)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"points": 10,
|
||||
"hints": ["Suma las componentes x", "Suma las componentes y"],
|
||||
"timeEstimate": 300
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /exercises/:id/attempt
|
||||
|
||||
Envía respuesta a ejercicio.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"answer": "(4, 6)",
|
||||
"timeSpent": 120,
|
||||
"showHints": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"attemptId": "att-123",
|
||||
"isCorrect": true,
|
||||
"score": 10,
|
||||
"feedback": "¡Correcto! La suma de vectores se realiza componente a componente.",
|
||||
"solution": {
|
||||
"steps": [
|
||||
"\\vec{a} + \\vec{b} = (1+3, 2+4)",
|
||||
"= (4, 6)"
|
||||
],
|
||||
"latex": "\\vec{a} + \\vec{b} = (4, 6)"
|
||||
},
|
||||
"progress": {
|
||||
"moduleCompleted": false,
|
||||
"newAchievements": ["first-step"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Progress
|
||||
|
||||
#### GET /progress
|
||||
|
||||
Obtiene progreso general del usuario.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"overall": {
|
||||
"totalExercises": 75,
|
||||
"completedExercises": 25,
|
||||
"percentage": 33.3,
|
||||
"totalScore": 250,
|
||||
"averageScore": 10
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"moduleId": "mod-1",
|
||||
"moduleName": "Fundamentos",
|
||||
"completedExercises": 15,
|
||||
"totalExercises": 25,
|
||||
"percentage": 60,
|
||||
"score": 150
|
||||
}
|
||||
],
|
||||
"streak": {
|
||||
"current": 5,
|
||||
"longest": 12,
|
||||
"lastActivity": "2024-03-30T10:00:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /progress/module/:moduleId
|
||||
|
||||
Obtiene progreso de un módulo específico.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"moduleId": "mod-1",
|
||||
"moduleName": "Fundamentos",
|
||||
"completedExercises": 15,
|
||||
"totalExercises": 25,
|
||||
"percentage": 60,
|
||||
"score": 150,
|
||||
"topics": [
|
||||
{
|
||||
"topicId": "topic-1",
|
||||
"topicName": "Vectores",
|
||||
"completedExercises": 5,
|
||||
"totalExercises": 8,
|
||||
"percentage": 62.5
|
||||
}
|
||||
],
|
||||
"recentAttempts": [
|
||||
{
|
||||
"exerciseId": "ex-1",
|
||||
"exerciseTitle": "Suma de vectores",
|
||||
"isCorrect": true,
|
||||
"score": 10,
|
||||
"attemptedAt": "2024-03-30T10:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ranking
|
||||
|
||||
#### GET /ranking/global
|
||||
|
||||
Obtiene ranking global.
|
||||
|
||||
**Query Parameters:**
|
||||
- `limit` (optional): Resultados por página (default: 50)
|
||||
- `offset` (optional): Paginación (default: 0)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"rankings": [
|
||||
{
|
||||
"position": 1,
|
||||
"user": {
|
||||
"id": "user-1",
|
||||
"firstName": "Alice",
|
||||
"lastName": "Smith"
|
||||
},
|
||||
"score": 1250,
|
||||
"completedExercises": 75,
|
||||
"accuracy": 92.5
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 150,
|
||||
"limit": 50,
|
||||
"offset": 0,
|
||||
"hasMore": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /ranking/my-position
|
||||
|
||||
Obtiene posición del usuario actual.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"globalPosition": 25,
|
||||
"score": 450,
|
||||
"completedExercises": 35,
|
||||
"accuracy": 85.2,
|
||||
"modulePositions": [
|
||||
{
|
||||
"moduleId": "mod-1",
|
||||
"moduleName": "Fundamentos",
|
||||
"position": 15,
|
||||
"score": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Achievements
|
||||
|
||||
#### GET /achievements
|
||||
|
||||
Lista todos los logros disponibles.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"achievements": [
|
||||
{
|
||||
"id": "ach-1",
|
||||
"name": "Primer Paso",
|
||||
"description": "Completa tu primer ejercicio",
|
||||
"category": "EXERCISES",
|
||||
"rarity": "COMMON",
|
||||
"icon": "🎯",
|
||||
"requirement": {
|
||||
"type": "EXERCISE_COUNT",
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /achievements/my
|
||||
|
||||
Obtiene logros desbloqueados del usuario.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"unlocked": [
|
||||
{
|
||||
"id": "ach-1",
|
||||
"name": "Primer Paso",
|
||||
"description": "Completa tu primer ejercicio",
|
||||
"category": "EXERCISES",
|
||||
"rarity": "COMMON",
|
||||
"icon": "🎯",
|
||||
"unlockedAt": "2024-03-30T12:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"progress": [
|
||||
{
|
||||
"achievementId": "ach-2",
|
||||
"name": "En Marcha",
|
||||
"progress": 5,
|
||||
"required": 10,
|
||||
"percentage": 50
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AI Generation
|
||||
|
||||
#### POST /ai/generate-exercise
|
||||
|
||||
Genera ejercicio usando AI.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"topicId": "topic-1",
|
||||
"difficulty": "INTERMEDIATE",
|
||||
"type": "CALCULATION",
|
||||
"context": "Vectores en 3D"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"exercise": {
|
||||
"title": "Producto cruz en 3D",
|
||||
"description": "Calcular el producto cruz de dos vectores en 3D",
|
||||
"latex": "\\vec{a} = (1, 2, 3), \\vec{b} = (4, 5, 6)",
|
||||
"solution": {
|
||||
"steps": [
|
||||
"\\vec{a} \\times \\vec{b} = (2\\cdot6 - 3\\cdot5, 3\\cdot4 - 1\\cdot6, 1\\cdot5 - 2\\cdot4)",
|
||||
"= (12-15, 12-6, 5-8)",
|
||||
"= (-3, 6, -3)"
|
||||
],
|
||||
"latex": "\\vec{a} \\times \\vec{b} = (-3, 6, -3)"
|
||||
},
|
||||
"difficulty": "INTERMEDIATE",
|
||||
"points": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /ai/validate-answer
|
||||
|
||||
Valida respuesta usando AI.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"exerciseId": "ex-1",
|
||||
"userAnswer": "(4, 6)",
|
||||
"context": "Suma de vectores 2D"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"isCorrect": true,
|
||||
"confidence": 0.95,
|
||||
"feedback": "Respuesta correcta. La suma de vectores (1,2) + (3,4) = (4,6)",
|
||||
"explanation": "Se sumaron correctamente las componentes x (1+3=4) y y (2+4=6)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Códigos de Error
|
||||
|
||||
| Código | HTTP | Descripción |
|
||||
|--------|------|-------------|
|
||||
| `BAD_REQUEST` | 400 | Datos inválidos o faltantes |
|
||||
| `UNAUTHORIZED` | 401 | Token inválido o expirado |
|
||||
| `FORBIDDEN` | 403 | Sin permisos para el recurso |
|
||||
| `NOT_FOUND` | 404 | Recurso no existe |
|
||||
| `VALIDATION_ERROR` | 422 | Error de validación de datos |
|
||||
| `RATE_LIMIT` | 429 | Rate limit excedido |
|
||||
| `INTERNAL_ERROR` | 500 | Error interno del servidor |
|
||||
| `SERVICE_UNAVAILABLE` | 503 | Servicio temporalmente no disponible |
|
||||
|
||||
### Formato de Error
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Datos de entrada inválidos",
|
||||
"details": {
|
||||
"field": "email",
|
||||
"issue": "Email inválido"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2024-03-30T12:00:00.000Z",
|
||||
"requestId": "req_abc123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- **Auth endpoints**: 5 requests / 15 min / IP
|
||||
- **API general**: 100 requests / 15 min / user
|
||||
- **AI generation**: 10 requests / hour / user
|
||||
- **Exercise attempts**: Sin limitación
|
||||
|
||||
Headers de respuesta:
|
||||
```
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 95
|
||||
X-RateLimit-Reset: 1711802400
|
||||
```
|
||||
|
||||
## Versionado
|
||||
|
||||
La API usa versionado en URL:
|
||||
- `/api/v1/...` - Versión actual
|
||||
- `/api/...` - Alias a v1 (default)
|
||||
|
||||
## Webhooks (Admin)
|
||||
|
||||
### POST /webhooks/telegram
|
||||
|
||||
Endpoint para recibir actualizaciones de Telegram.
|
||||
|
||||
### POST /webhooks/pdf-processed
|
||||
|
||||
Notificación cuando un PDF es procesado.
|
||||
|
||||
## Health Check
|
||||
|
||||
### GET /health
|
||||
|
||||
Verifica estado del servicio.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"timestamp": "2024-03-30T12:00:00.000Z",
|
||||
"version": "1.0.0",
|
||||
"services": {
|
||||
"database": "connected",
|
||||
"redis": "connected",
|
||||
"ai": "available"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Paginación
|
||||
|
||||
Todas las listas soportan paginación con:
|
||||
- `limit`: Número de items (max: 100)
|
||||
- `offset`: Índice inicial (0-based)
|
||||
- `cursor`: Para paginación basada en cursor (alternativa)
|
||||
|
||||
Formato de respuesta paginada:
|
||||
```json
|
||||
{
|
||||
"data": [...],
|
||||
"pagination": {
|
||||
"total": 150,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"hasMore": true,
|
||||
"nextCursor": "eyJpZCI6..."
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user