ML Engine Updates: - Updated BTCUSD with Polygon API data (2024-2025): 215,699 new records - Re-trained all ML models: Attention (R²: 0.223), Base, Metamodel (87.3% confidence) - Backtest results: +176.71R profit with aggressive_filter strategy Documentation Consolidation: - Created docs/99-analisis/_MAP.md index with 13 new analysis documents - Consolidated inventories: removed duplicates from orchestration/inventarios/ - Updated ML_INVENTORY.yml with BTCUSD metrics and training results - Added execution reports: FASE11-BTCUSD, correction issues, alignment validation Architecture & Integration: - Updated all module documentation with NEXUS v3.4 frontmatter - Fixed _MAP.md indexes across all folders - Updated orchestration plans and traces Files: 229 changed, 5064 insertions(+), 1872 deletions(-) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
40 KiB
| id | title | type | status | rf_parent | epic | version | created_date | updated_date |
|---|---|---|---|---|---|---|---|---|
| ET-EDU-002 | API REST Education Module | Specification | Done | RF-EDU-002 | OQI-002 | 1.0 | 2025-12-05 | 2026-01-04 |
ET-EDU-002: API REST - Endpoints del Módulo Educativo
Versión: 1.0.0 Fecha: 2025-12-05 Épica: OQI-002 - Módulo Educativo Componente: Backend
Descripción
Define todos los endpoints REST del módulo educativo de Trading Platform, implementados en Express.js con TypeScript. Incluye autenticación JWT, validación de datos, paginación, filtros y manejo de errores.
Arquitectura
┌─────────────────────────────────────────────────────────────────┐
│ API Gateway │
│ (Express.js + TypeScript) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Middleware Stack │ │
│ │ - JWT Authentication │ │
│ │ - Rate Limiting │ │
│ │ - Request Validation (Zod) │ │
│ │ - Error Handling │ │
│ │ - CORS │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Courses │ │ Enrollments │ │ Progress │ │
│ │ Controller │ │ Controller │ │ Controller │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ v v v │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Courses │ │ Enrollments │ │ Progress │ │
│ │ Service │ │ Service │ │ Service │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ v v v │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL (education schema) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Quizzes │ │Certificates │ │Achievements │ │
│ │ Controller │ │ Controller │ │ Controller │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Especificación Detallada
Base URL
Production: https://api.trading.ai/v1/education
Development: http://localhost:3000/v1/education
Convenciones
- Formato: JSON
- Encoding: UTF-8
- Versionado: Prefijo
/v1 - Autenticación: Bearer Token (JWT)
- Rate Limit: 100 req/min por IP, 1000 req/hour por usuario autenticado
Endpoints
1. CATEGORIES
GET /categories
Obtener todas las categorías activas.
Auth: No requerido
Query Parameters:
{
include_inactive?: boolean; // Admin only, default: false
parent_id?: string; // Filtrar por categoría padre
}
Response 200:
{
success: true,
data: Array<{
id: string;
name: string;
slug: string;
description: string | null;
parent_id: string | null;
display_order: number;
icon_url: string | null;
color: string | null;
course_count: number; // Número de cursos en la categoría
}>,
metadata: {
total: number;
timestamp: string;
}
}
GET /categories/:slug
Obtener categoría por slug con sus cursos.
Auth: No requerido
Response 200:
{
success: true,
data: {
id: string;
name: string;
slug: string;
description: string | null;
parent_id: string | null;
icon_url: string | null;
color: string | null;
courses: Array<{
id: string;
title: string;
slug: string;
short_description: string;
thumbnail_url: string;
difficulty_level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
duration_minutes: number;
total_lessons: number;
avg_rating: number;
is_free: boolean;
price_usd: number | null;
}>;
}
}
2. COURSES
GET /courses
Listar cursos con filtros y paginación.
Auth: No requerido
Query Parameters:
{
page?: number; // default: 1
limit?: number; // default: 20, max: 100
category?: string; // slug de categoría
difficulty?: 'beginner' | 'intermediate' | 'advanced' | 'expert';
is_free?: boolean;
search?: string; // Búsqueda en título y descripción
sort_by?: 'newest' | 'popular' | 'rating' | 'title';
instructor_id?: string;
}
Response 200:
{
success: true,
data: Array<{
id: string;
title: string;
slug: string;
short_description: string;
thumbnail_url: string;
category: {
id: string;
name: string;
slug: string;
};
difficulty_level: string;
duration_minutes: number;
total_modules: number;
total_lessons: number;
total_enrollments: number;
avg_rating: number;
total_reviews: number;
instructor_name: string;
is_free: boolean;
price_usd: number | null;
xp_reward: number;
published_at: string;
}>,
pagination: {
page: number;
limit: number;
total: number;
total_pages: number;
has_next: boolean;
has_prev: boolean;
}
}
GET /courses/:slug
Obtener detalle completo de un curso.
Auth: No requerido
Response 200:
{
success: true,
data: {
id: string;
title: string;
slug: string;
short_description: string;
full_description: string;
thumbnail_url: string;
trailer_url: string | null;
category: {
id: string;
name: string;
slug: string;
};
difficulty_level: string;
duration_minutes: number;
prerequisites: string[];
learning_objectives: string[];
instructor: {
id: string;
name: string;
avatar_url: string | null;
bio: string | null;
};
is_free: boolean;
price_usd: number | null;
xp_reward: number;
published_at: string;
// Estadísticas
total_modules: number;
total_lessons: number;
total_enrollments: number;
avg_rating: number;
total_reviews: number;
// Módulos con lecciones
modules: Array<{
id: string;
title: string;
description: string;
display_order: number;
duration_minutes: number;
is_locked: boolean;
lessons: Array<{
id: string;
title: string;
description: string;
content_type: 'video' | 'article' | 'interactive' | 'quiz';
video_duration_seconds: number | null;
display_order: number;
is_preview: boolean;
xp_reward: number;
}>;
}>;
// Estado del usuario (si está autenticado)
user_enrollment?: {
is_enrolled: boolean;
progress_percentage: number;
status: 'active' | 'completed' | 'expired' | 'cancelled';
enrolled_at: string;
};
}
}
POST /courses
Crear nuevo curso (solo instructores/admin).
Auth: Required (instructor/admin)
Request Body:
{
title: string; // min: 10, max: 200
short_description: string; // min: 50, max: 500
full_description: string; // min: 100
category_id: string; // UUID
difficulty_level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
thumbnail_url?: string;
trailer_url?: string;
prerequisites?: string[];
learning_objectives: string[]; // min: 3
is_free?: boolean;
price_usd?: number;
xp_reward?: number;
}
Response 201:
{
success: true,
data: {
id: string;
slug: string;
status: 'draft';
// ... resto de campos del curso
}
}
PATCH /courses/:id
Actualizar curso (solo instructor owner/admin).
Auth: Required (owner/admin)
Request Body: Partial de POST /courses
Response 200: Same as GET /courses/:slug
DELETE /courses/:id
Eliminar curso (solo admin).
Auth: Required (admin)
Response 204: No Content
PATCH /courses/:id/publish
Publicar curso (cambiar status a published).
Auth: Required (owner/admin)
Response 200:
{
success: true,
data: {
id: string;
status: 'published';
published_at: string;
}
}
3. MODULES
POST /courses/:courseId/modules
Crear módulo en un curso.
Auth: Required (course owner/admin)
Request Body:
{
title: string;
description: string;
display_order: number;
duration_minutes?: number;
is_locked?: boolean;
unlock_after_module_id?: string;
}
Response 201:
{
success: true,
data: {
id: string;
course_id: string;
title: string;
description: string;
display_order: number;
is_locked: boolean;
created_at: string;
}
}
PATCH /modules/:id
Actualizar módulo.
Auth: Required (course owner/admin)
Request Body: Partial de POST
Response 200: Same as POST response
DELETE /modules/:id
Eliminar módulo (cascade elimina lecciones).
Auth: Required (course owner/admin)
Response 204: No Content
PATCH /modules/:id/reorder
Reordenar módulos de un curso.
Auth: Required (course owner/admin)
Request Body:
{
new_order: number;
}
Response 200:
{
success: true,
data: {
id: string;
display_order: number;
}
}
4. LESSONS
POST /modules/:moduleId/lessons
Crear lección en un módulo.
Auth: Required (course owner/admin)
Request Body:
{
title: string;
description: string;
content_type: 'video' | 'article' | 'interactive' | 'quiz';
// Para video
video_url?: string;
video_duration_seconds?: number;
video_provider?: 'vimeo' | 's3';
video_id?: string;
// Para article
article_content?: string;
// Recursos
attachments?: Array<{
name: string;
url: string;
type: string;
size: number;
}>;
display_order: number;
is_preview?: boolean;
is_mandatory?: boolean;
xp_reward?: number;
}
Response 201:
{
success: true,
data: {
id: string;
module_id: string;
title: string;
content_type: string;
display_order: number;
xp_reward: number;
created_at: string;
}
}
GET /lessons/:id
Obtener detalle de lección.
Auth: Required (enrolled users o lecciones preview)
Response 200:
{
success: true,
data: {
id: string;
module_id: string;
title: string;
description: string;
content_type: string;
// Video
video_url?: string;
video_duration_seconds?: number;
video_provider?: string;
video_id?: string;
// Article
article_content?: string;
// Recursos
attachments?: Array<{
name: string;
url: string;
type: string;
size: number;
}>;
is_preview: boolean;
xp_reward: number;
// Progreso del usuario
user_progress?: {
is_completed: boolean;
last_position_seconds: number;
watch_percentage: number;
last_viewed_at: string;
};
// Navegación
previous_lesson?: {
id: string;
title: string;
};
next_lesson?: {
id: string;
title: string;
};
}
}
PATCH /lessons/:id
Actualizar lección.
Auth: Required (course owner/admin)
Request Body: Partial de POST
Response 200: Same as GET /lessons/:id
DELETE /lessons/:id
Eliminar lección.
Auth: Required (course owner/admin)
Response 204: No Content
5. ENROLLMENTS
POST /enrollments
Enrollarse en un curso.
Auth: Required
Request Body:
{
course_id: string;
}
Response 201:
{
success: true,
data: {
id: string;
user_id: string;
course_id: string;
status: 'active';
progress_percentage: 0;
enrolled_at: string;
total_lessons: number;
}
}
Error 409: Ya está enrollado en el curso
GET /enrollments
Obtener enrollments del usuario autenticado.
Auth: Required
Query Parameters:
{
status?: 'active' | 'completed' | 'expired' | 'cancelled';
page?: number;
limit?: number;
}
Response 200:
{
success: true,
data: Array<{
id: string;
course: {
id: string;
title: string;
slug: string;
thumbnail_url: string;
instructor_name: string;
};
status: string;
progress_percentage: number;
completed_lessons: number;
total_lessons: number;
enrolled_at: string;
started_at: string | null;
completed_at: string | null;
total_xp_earned: number;
}>,
pagination: {
page: number;
limit: number;
total: number;
}
}
GET /enrollments/:id
Obtener detalle de enrollment con progreso completo.
Auth: Required (owner/admin)
Response 200:
{
success: true,
data: {
id: string;
course: {
id: string;
title: string;
slug: string;
thumbnail_url: string;
};
status: string;
progress_percentage: number;
completed_lessons: number;
total_lessons: number;
enrolled_at: string;
started_at: string | null;
completed_at: string | null;
total_xp_earned: number;
// Progreso por módulo
modules_progress: Array<{
module_id: string;
module_title: string;
total_lessons: number;
completed_lessons: number;
progress_percentage: number;
lessons_progress: Array<{
lesson_id: string;
lesson_title: string;
is_completed: boolean;
watch_percentage: number;
last_viewed_at: string | null;
}>;
}>;
}
}
DELETE /enrollments/:id
Cancelar enrollment.
Auth: Required (owner/admin)
Response 200:
{
success: true,
data: {
id: string;
status: 'cancelled';
}
}
6. PROGRESS
POST /progress
Actualizar progreso en una lección.
Auth: Required
Request Body:
{
lesson_id: string;
enrollment_id: string;
last_position_seconds?: number; // Para videos
is_completed?: boolean;
watch_percentage?: number;
}
Response 200:
{
success: true,
data: {
id: string;
lesson_id: string;
is_completed: boolean;
last_position_seconds: number;
watch_percentage: number;
xp_earned: number; // Si se completó la lección
updated_at: string;
}
}
GET /progress/course/:courseId
Obtener progreso del usuario en un curso.
Auth: Required
Response 200:
{
success: true,
data: {
enrollment_id: string;
course_id: string;
progress_percentage: number;
completed_lessons: number;
total_lessons: number;
total_xp_earned: number;
lessons: Array<{
lesson_id: string;
lesson_title: string;
module_title: string;
is_completed: boolean;
watch_percentage: number;
last_viewed_at: string | null;
completed_at: string | null;
}>;
}
}
7. QUIZZES
GET /quizzes/:id
Obtener quiz (sin respuestas correctas).
Auth: Required (enrolled)
Response 200:
{
success: true,
data: {
id: string;
module_id?: string;
lesson_id?: string;
title: string;
description: string;
passing_score_percentage: number;
max_attempts: number | null;
time_limit_minutes: number | null;
xp_reward: number;
xp_perfect_score_bonus: number;
// Estadísticas del usuario
user_attempts: Array<{
id: string;
score_percentage: number;
is_passed: boolean;
completed_at: string;
}>;
remaining_attempts: number | null;
best_score: number | null;
// Preguntas (sin respuestas correctas)
questions: Array<{
id: string;
question_text: string;
question_type: 'multiple_choice' | 'true_false' | 'multiple_select' | 'fill_blank' | 'code_challenge';
options?: Array<{
id: string;
text: string;
}>;
image_url?: string;
code_snippet?: string;
points: number;
display_order: number;
}>;
}
}
POST /quizzes/:id/attempts
Iniciar intento de quiz.
Auth: Required (enrolled)
Response 201:
{
success: true,
data: {
id: string;
quiz_id: string;
started_at: string;
time_limit_expires_at: string | null;
questions: Array<{
id: string;
question_text: string;
question_type: string;
options?: Array<{ id: string; text: string }>;
points: number;
}>;
}
}
Error 403: No quedan intentos disponibles
POST /quizzes/attempts/:attemptId/submit
Enviar respuestas del quiz.
Auth: Required (owner)
Request Body:
{
answers: Array<{
question_id: string;
answer: string | string[]; // string[] para multiple_select
}>;
}
Response 200:
{
success: true,
data: {
id: string;
is_completed: true;
is_passed: boolean;
score_points: number;
max_points: number;
score_percentage: number;
time_taken_seconds: number;
xp_earned: number;
completed_at: string;
// Desglose de respuestas
results: Array<{
question_id: string;
question_text: string;
user_answer: string | string[];
is_correct: boolean;
points_earned: number;
max_points: number;
explanation: string;
}>;
// Nuevo logro si aplica
achievement_earned?: {
id: string;
title: string;
description: string;
badge_icon_url: string;
};
}
}
GET /quizzes/attempts/:attemptId
Obtener resultado de un intento completado.
Auth: Required (owner/admin)
Response 200: Same as POST submit response
8. CERTIFICATES
GET /certificates
Obtener certificados del usuario.
Auth: Required
Response 200:
{
success: true,
data: Array<{
id: string;
certificate_number: string;
course_title: string;
completion_date: string;
final_score: number;
certificate_url: string;
verification_code: string;
issued_at: string;
}>
}
GET /certificates/:id
Obtener detalle de certificado.
Auth: Required (owner/admin) o público con verification_code
Query Parameters:
{
verification_code?: string; // Para acceso público
}
Response 200:
{
success: true,
data: {
id: string;
certificate_number: string;
user_name: string;
course_title: string;
instructor_name: string;
completion_date: string;
final_score: number;
total_xp_earned: number;
certificate_url: string;
verification_code: string;
is_verified: boolean;
issued_at: string;
}
}
GET /certificates/verify/:certificateNumber
Verificar autenticidad de un certificado (público).
Auth: No requerido
Response 200:
{
success: true,
data: {
is_valid: boolean;
certificate_number: string;
user_name: string;
course_title: string;
completion_date: string;
issued_at: string;
}
}
9. ACHIEVEMENTS
GET /achievements
Obtener logros del usuario autenticado.
Auth: Required
Query Parameters:
{
achievement_type?: 'course_completion' | 'quiz_perfect_score' | 'streak_milestone' | 'level_up' | 'special_event';
}
Response 200:
{
success: true,
data: Array<{
id: string;
achievement_type: string;
title: string;
description: string;
badge_icon_url: string;
xp_bonus: number;
earned_at: string;
metadata: {
course_title?: string;
quiz_score?: number;
streak_days?: number;
new_level?: number;
};
}>,
summary: {
total_achievements: number;
total_xp_from_achievements: number;
by_type: {
course_completion: number;
quiz_perfect_score: number;
streak_milestone: number;
level_up: number;
special_event: number;
};
}
}
GET /achievements/available
Obtener logros disponibles para desbloquear.
Auth: Required
Response 200:
{
success: true,
data: Array<{
title: string;
description: string;
badge_icon_url: string;
xp_bonus: number;
requirements: string;
progress_percentage: number;
}>
}
10. GAMIFICATION
GET /gamification/profile
Obtener perfil de gamificación del usuario.
Auth: Required
Response 200:
{
success: true,
data: {
user_id: string;
total_xp: number;
current_level: number;
xp_for_next_level: number;
xp_progress_percentage: number;
// Estadísticas
courses_completed: number;
courses_in_progress: number;
total_learning_time_minutes: number;
// Racha
current_streak_days: number;
longest_streak_days: number;
last_activity_date: string;
// Logros
total_achievements: number;
recent_achievements: Array<{
id: string;
title: string;
badge_icon_url: string;
earned_at: string;
}>;
// Ranking
global_rank: number | null;
percentile: number;
}
}
GET /gamification/leaderboard
Obtener tabla de clasificación.
Auth: Required
Query Parameters:
{
period?: 'all_time' | 'month' | 'week';
limit?: number; // default: 100
}
Response 200:
{
success: true,
data: Array<{
rank: number;
user_id: string;
user_name: string;
avatar_url: string | null;
total_xp: number;
current_level: number;
courses_completed: number;
achievements_count: number;
}>,
user_position: {
rank: number;
total_xp: number;
}
}
Interfaces/Tipos
Request/Response Types
// ==================== COMMON ====================
export interface ApiResponse<T> {
success: boolean;
data: T;
error?: {
code: string;
message: string;
details?: any;
};
}
export interface PaginatedResponse<T> {
success: boolean;
data: T[];
pagination: {
page: number;
limit: number;
total: number;
total_pages: number;
has_next: boolean;
has_prev: boolean;
};
}
// ==================== COURSES ====================
export interface CourseListItem {
id: string;
title: string;
slug: string;
short_description: string;
thumbnail_url: string;
category: {
id: string;
name: string;
slug: string;
};
difficulty_level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
duration_minutes: number;
total_modules: number;
total_lessons: number;
total_enrollments: number;
avg_rating: number;
total_reviews: number;
instructor_name: string;
is_free: boolean;
price_usd: number | null;
xp_reward: number;
published_at: string;
}
export interface CourseDetail extends CourseListItem {
full_description: string;
trailer_url: string | null;
prerequisites: string[];
learning_objectives: string[];
instructor: {
id: string;
name: string;
avatar_url: string | null;
bio: string | null;
};
modules: ModuleWithLessons[];
user_enrollment?: {
is_enrolled: boolean;
progress_percentage: number;
status: EnrollmentStatus;
enrolled_at: string;
};
}
export interface CreateCourseDto {
title: string;
short_description: string;
full_description: string;
category_id: string;
difficulty_level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
thumbnail_url?: string;
trailer_url?: string;
prerequisites?: string[];
learning_objectives: string[];
is_free?: boolean;
price_usd?: number;
xp_reward?: number;
}
// ==================== MODULES ====================
export interface Module {
id: string;
course_id: string;
title: string;
description: string;
display_order: number;
duration_minutes: number;
is_locked: boolean;
unlock_after_module_id: string | null;
}
export interface ModuleWithLessons extends Module {
lessons: Lesson[];
}
export interface CreateModuleDto {
title: string;
description: string;
display_order: number;
duration_minutes?: number;
is_locked?: boolean;
unlock_after_module_id?: string;
}
// ==================== LESSONS ====================
export type LessonContentType = 'video' | 'article' | 'interactive' | 'quiz';
export interface Lesson {
id: string;
module_id: string;
title: string;
description: string;
content_type: LessonContentType;
video_url?: string;
video_duration_seconds?: number;
video_provider?: string;
video_id?: string;
article_content?: string;
attachments?: Attachment[];
display_order: number;
is_preview: boolean;
is_mandatory: boolean;
xp_reward: number;
}
export interface LessonDetail extends Lesson {
user_progress?: {
is_completed: boolean;
last_position_seconds: number;
watch_percentage: number;
last_viewed_at: string;
};
previous_lesson?: {
id: string;
title: string;
};
next_lesson?: {
id: string;
title: string;
};
}
export interface Attachment {
name: string;
url: string;
type: string;
size: number;
}
export interface CreateLessonDto {
title: string;
description: string;
content_type: LessonContentType;
video_url?: string;
video_duration_seconds?: number;
video_provider?: 'vimeo' | 's3';
video_id?: string;
article_content?: string;
attachments?: Attachment[];
display_order: number;
is_preview?: boolean;
is_mandatory?: boolean;
xp_reward?: number;
}
// ==================== ENROLLMENTS ====================
export type EnrollmentStatus = 'active' | 'completed' | 'expired' | 'cancelled';
export interface Enrollment {
id: string;
user_id: string;
course_id: string;
status: EnrollmentStatus;
progress_percentage: number;
completed_lessons: number;
total_lessons: number;
enrolled_at: string;
started_at: string | null;
completed_at: string | null;
expires_at: string | null;
total_xp_earned: number;
}
export interface EnrollmentWithCourse extends Enrollment {
course: {
id: string;
title: string;
slug: string;
thumbnail_url: string;
instructor_name: string;
};
}
// ==================== PROGRESS ====================
export interface Progress {
id: string;
user_id: string;
lesson_id: string;
enrollment_id: string;
is_completed: boolean;
last_position_seconds: number;
total_watch_time_seconds: number;
watch_percentage: number;
first_viewed_at: string | null;
last_viewed_at: string | null;
completed_at: string | null;
}
export interface UpdateProgressDto {
lesson_id: string;
enrollment_id: string;
last_position_seconds?: number;
is_completed?: boolean;
watch_percentage?: number;
}
// ==================== QUIZZES ====================
export type QuestionType = 'multiple_choice' | 'true_false' | 'multiple_select' | 'fill_blank' | 'code_challenge';
export interface Quiz {
id: string;
module_id?: string;
lesson_id?: string;
title: string;
description: string;
passing_score_percentage: number;
max_attempts: number | null;
time_limit_minutes: number | null;
shuffle_questions: boolean;
shuffle_answers: boolean;
show_correct_answers: boolean;
xp_reward: number;
xp_perfect_score_bonus: number;
}
export interface QuizQuestion {
id: string;
quiz_id: string;
question_text: string;
question_type: QuestionType;
options?: Array<{
id: string;
text: string;
isCorrect?: boolean; // Solo para admin
}>;
correct_answer?: string;
explanation: string;
image_url?: string;
code_snippet?: string;
points: number;
display_order: number;
}
export interface QuizAttempt {
id: string;
user_id: string;
quiz_id: string;
enrollment_id: string | null;
is_completed: boolean;
is_passed: boolean;
user_answers: any;
score_points: number;
max_points: number;
score_percentage: number;
started_at: string;
completed_at: string | null;
time_taken_seconds: number | null;
xp_earned: number;
}
export interface SubmitQuizDto {
answers: Array<{
question_id: string;
answer: string | string[];
}>;
}
// ==================== CERTIFICATES ====================
export interface Certificate {
id: string;
certificate_number: string;
user_name: string;
course_title: string;
instructor_name: string;
completion_date: string;
final_score: number;
total_xp_earned: number;
certificate_url: string;
verification_code: string;
is_verified: boolean;
issued_at: string;
}
// ==================== ACHIEVEMENTS ====================
export type AchievementType = 'course_completion' | 'quiz_perfect_score' | 'streak_milestone' | 'level_up' | 'special_event';
export interface Achievement {
id: string;
user_id: string;
achievement_type: AchievementType;
title: string;
description: string;
badge_icon_url: string;
metadata: any;
course_id?: string;
quiz_id?: string;
xp_bonus: number;
earned_at: string;
}
// ==================== GAMIFICATION ====================
export interface GamificationProfile {
user_id: string;
total_xp: number;
current_level: number;
xp_for_next_level: number;
xp_progress_percentage: number;
courses_completed: number;
courses_in_progress: number;
total_learning_time_minutes: number;
current_streak_days: number;
longest_streak_days: number;
last_activity_date: string;
total_achievements: number;
recent_achievements: Achievement[];
global_rank: number | null;
percentile: number;
}
export interface LeaderboardEntry {
rank: number;
user_id: string;
user_name: string;
avatar_url: string | null;
total_xp: number;
current_level: number;
courses_completed: number;
achievements_count: number;
}
Configuración
Variables de Entorno
# Server
PORT=3000
NODE_ENV=production
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/trading
DATABASE_SCHEMA=education
# JWT
JWT_SECRET=your-super-secret-key
JWT_EXPIRES_IN=7d
# Rate Limiting
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX_REQUESTS=100
RATE_LIMIT_AUTHENTICATED_MAX=1000
# CORS
CORS_ORIGIN=https://trading.ai,https://app.trading.ai
CORS_CREDENTIALS=true
# External Services
VIMEO_API_KEY=xxx
AWS_S3_BUCKET=trading-videos
AWS_CLOUDFRONT_URL=https://cdn.trading.ai
# Email (para certificados)
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=xxx
# Redis (para caching)
REDIS_URL=redis://localhost:6379
REDIS_TTL=3600
Dependencias
{
"dependencies": {
"express": "^4.18.2",
"typescript": "^5.3.3",
"@types/express": "^4.17.21",
"zod": "^3.22.4",
"pg": "^8.11.3",
"jsonwebtoken": "^9.0.2",
"bcrypt": "^5.1.1",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"winston": "^3.11.0",
"redis": "^4.6.12",
"axios": "^1.6.5"
},
"devDependencies": {
"@types/node": "^20.10.6",
"@types/jsonwebtoken": "^9.0.5",
"@types/bcrypt": "^5.0.2",
"nodemon": "^3.0.2",
"ts-node": "^10.9.2",
"jest": "^29.7.0",
"@types/jest": "^29.5.11",
"supertest": "^6.3.3",
"@types/supertest": "^6.0.2"
}
}
Consideraciones de Seguridad
1. Autenticación
// Middleware de autenticación JWT
export const authenticate = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
success: false,
error: {
code: 'UNAUTHORIZED',
message: 'Token no proporcionado'
}
});
}
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({
success: false,
error: {
code: 'INVALID_TOKEN',
message: 'Token inválido o expirado'
}
});
}
};
2. Autorización
// Middleware de autorización por rol
export const authorize = (...roles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
error: {
code: 'FORBIDDEN',
message: 'No tienes permisos para esta acción'
}
});
}
next();
};
};
// Verificar ownership del recurso
export const verifyOwnership = async (
req: Request,
res: Response,
next: NextFunction
) => {
const resourceId = req.params.id;
const userId = req.user.id;
// Verificar que el recurso pertenece al usuario
const resource = await db.query(
'SELECT user_id FROM education.enrollments WHERE id = $1',
[resourceId]
);
if (resource.rows[0]?.user_id !== userId && req.user.role !== 'admin') {
return res.status(403).json({
success: false,
error: {
code: 'FORBIDDEN',
message: 'No tienes acceso a este recurso'
}
});
}
next();
};
3. Validación de Input
import { z } from 'zod';
// Schema de validación para crear curso
export const CreateCourseSchema = z.object({
title: z.string().min(10).max(200),
short_description: z.string().min(50).max(500),
full_description: z.string().min(100),
category_id: z.string().uuid(),
difficulty_level: z.enum(['beginner', 'intermediate', 'advanced', 'expert']),
thumbnail_url: z.string().url().optional(),
trailer_url: z.string().url().optional(),
prerequisites: z.array(z.string().uuid()).optional(),
learning_objectives: z.array(z.string()).min(3),
is_free: z.boolean().optional(),
price_usd: z.number().min(0).optional(),
xp_reward: z.number().min(0).optional()
});
// Middleware de validación
export const validate = (schema: z.ZodSchema) => {
return (req: Request, res: Response, next: NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
success: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Datos de entrada inválidos',
details: error.errors
}
});
}
next(error);
}
};
};
4. Rate Limiting
import rateLimit from 'express-rate-limit';
// Rate limiter general
export const generalLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minuto
max: 100,
message: {
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Demasiadas solicitudes, intenta más tarde'
}
}
});
// Rate limiter para autenticados
export const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hora
max: 1000,
skip: (req) => !req.user,
message: {
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Límite de solicitudes excedido'
}
}
});
5. SQL Injection Prevention
- Usar prepared statements siempre
- Usar ORM (Prisma, TypeORM) con queries parametrizadas
- Nunca concatenar strings para queries SQL
6. XSS Prevention
- Sanitizar todo input del usuario
- Usar headers de seguridad (Helmet.js)
- Content Security Policy
7. CORS
import cors from 'cors';
app.use(cors({
origin: process.env.CORS_ORIGIN?.split(','),
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
Testing
Estrategia
- Unit Tests: Servicios y controladores
- Integration Tests: Endpoints completos
- E2E Tests: Flujos de usuario completos
Ejemplo de Test
import request from 'supertest';
import { app } from '../app';
import { generateAuthToken } from '../utils/auth';
describe('POST /enrollments', () => {
let authToken: string;
let courseId: string;
beforeAll(async () => {
// Setup test data
authToken = generateAuthToken({ id: 'test-user-id', role: 'student' });
courseId = await createTestCourse();
});
it('should enroll user in course successfully', async () => {
const response = await request(app)
.post('/v1/education/enrollments')
.set('Authorization', `Bearer ${authToken}`)
.send({ course_id: courseId })
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.course_id).toBe(courseId);
expect(response.body.data.status).toBe('active');
});
it('should return 401 without authentication', async () => {
await request(app)
.post('/v1/education/enrollments')
.send({ course_id: courseId })
.expect(401);
});
it('should return 409 if already enrolled', async () => {
// Enroll first time
await request(app)
.post('/v1/education/enrollments')
.set('Authorization', `Bearer ${authToken}`)
.send({ course_id: courseId });
// Try to enroll again
const response = await request(app)
.post('/v1/education/enrollments')
.set('Authorization', `Bearer ${authToken}`)
.send({ course_id: courseId })
.expect(409);
expect(response.body.error.code).toBe('ALREADY_ENROLLED');
});
});
Códigos de Error
| Código | HTTP | Descripción |
|---|---|---|
| UNAUTHORIZED | 401 | Token no proporcionado o inválido |
| FORBIDDEN | 403 | Sin permisos para esta acción |
| NOT_FOUND | 404 | Recurso no encontrado |
| ALREADY_ENROLLED | 409 | Ya enrollado en el curso |
| VALIDATION_ERROR | 400 | Datos de entrada inválidos |
| RATE_LIMIT_EXCEEDED | 429 | Límite de solicitudes excedido |
| NO_ATTEMPTS_REMAINING | 403 | Sin intentos disponibles para quiz |
| QUIZ_TIME_EXPIRED | 400 | Tiempo límite del quiz expirado |
| COURSE_NOT_PUBLISHED | 403 | Curso no está publicado |
| LESSON_LOCKED | 403 | Lección bloqueada (completar anteriores) |
| INTERNAL_SERVER_ERROR | 500 | Error interno del servidor |
Fin de Especificación ET-EDU-002