--- id: "ET-EDU-002" title: "API REST Education Module" type: "Specification" status: "Done" rf_parent: "RF-EDU-002" epic: "OQI-002" version: "1.0" created_date: "2025-12-05" updated_date: "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:** ```typescript { include_inactive?: boolean; // Admin only, default: false parent_id?: string; // Filtrar por categoría padre } ``` **Response 200:** ```typescript { 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:** ```typescript { 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:** ```typescript { 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:** ```typescript { 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:** ```typescript { 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:** ```typescript { 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:** ```typescript { 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:** ```typescript { 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:** ```typescript { title: string; description: string; display_order: number; duration_minutes?: number; is_locked?: boolean; unlock_after_module_id?: string; } ``` **Response 201:** ```typescript { 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:** ```typescript { new_order: number; } ``` **Response 200:** ```typescript { 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:** ```typescript { 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:** ```typescript { 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:** ```typescript { 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:** ```typescript { course_id: string; } ``` **Response 201:** ```typescript { 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:** ```typescript { status?: 'active' | 'completed' | 'expired' | 'cancelled'; page?: number; limit?: number; } ``` **Response 200:** ```typescript { 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:** ```typescript { 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:** ```typescript { success: true, data: { id: string; status: 'cancelled'; } } ``` --- ### 6. PROGRESS #### POST /progress Actualizar progreso en una lección. **Auth:** Required **Request Body:** ```typescript { lesson_id: string; enrollment_id: string; last_position_seconds?: number; // Para videos is_completed?: boolean; watch_percentage?: number; } ``` **Response 200:** ```typescript { 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:** ```typescript { 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:** ```typescript { 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:** ```typescript { 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:** ```typescript { answers: Array<{ question_id: string; answer: string | string[]; // string[] para multiple_select }>; } ``` **Response 200:** ```typescript { 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:** ```typescript { 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:** ```typescript { verification_code?: string; // Para acceso público } ``` **Response 200:** ```typescript { 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:** ```typescript { 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:** ```typescript { achievement_type?: 'course_completion' | 'quiz_perfect_score' | 'streak_milestone' | 'level_up' | 'special_event'; } ``` **Response 200:** ```typescript { 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:** ```typescript { 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:** ```typescript { 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:** ```typescript { period?: 'all_time' | 'month' | 'week'; limit?: number; // default: 100 } ``` **Response 200:** ```typescript { 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 ```typescript // ==================== COMMON ==================== export interface ApiResponse { success: boolean; data: T; error?: { code: string; message: string; details?: any; }; } export interface PaginatedResponse { 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 ```bash # 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 ```json { "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 ```typescript // 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 ```typescript // 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 ```typescript 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 ```typescript 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 ```typescript 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 1. **Unit Tests**: Servicios y controladores 2. **Integration Tests**: Endpoints completos 3. **E2E Tests**: Flujos de usuario completos ### Ejemplo de Test ```typescript 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**