diff --git a/.claude/README.md b/.claude/README.md new file mode 100644 index 0000000..946ea0a --- /dev/null +++ b/.claude/README.md @@ -0,0 +1,339 @@ +# 🤖 Claude Configuration for Platziflix + +Este directorio contiene las configuraciones de Claude Code para optimizar el desarrollo del proyecto Platziflix. + +## 📁 Estructura + +``` +.claude/ +├── agents/ # Agentes especializados +│ ├── architect.md # Arquitectura y diseño de sistemas +│ ├── backend.md # Backend (FastAPI, Python, PostgreSQL) +│ ├── frontend.md # Frontend (Next.js, React, TypeScript) +│ └── mobile.md # Mobile (Android Kotlin + iOS Swift) ✨ NUEVO +│ +├── commands/ # Slash commands personalizados ✨ NUEVO +│ ├── review-api.md # Revisar endpoints REST +│ ├── test-coverage.md # Analizar cobertura de tests +│ ├── security-audit.md # Auditoría de seguridad +│ └── deploy-checklist.md # Checklist pre-deployment +│ +├── hooks/ # Hooks de automatización ✨ NUEVO +│ ├── session-start.sh # Verificaciones al inicio de sesión +│ └── before-commit.sh # Checks pre-commit +│ +├── templates/ # Templates de documentación ✨ NUEVO +│ └── feature-spec.md # Template para specs de features +│ +└── README.md # Esta documentación +``` + +--- + +## 🎯 Agentes Especializados + +Los agentes son asistentes especializados para diferentes áreas del proyecto. + +### Cómo usar agentes + +En tu conversación con Claude, simplemente menciona el área: + +``` +"Como arquitecto, ¿qué opinas de agregar un cache layer?" +"Necesito ayuda con el backend para implementar autenticación" +"Como frontend developer, ¿cómo implemento este componente?" +"Ayúdame con la implementación mobile de esta feature" +``` + +### Agentes disponibles + +#### 🟡 Architect +- **Especialidad**: Arquitectura de software, diseño de sistemas +- **Cuándo usar**: Decisiones arquitecturales, diseño de DB, API contracts +- **Entregables**: Specs técnicas, diagramas, planes de implementación + +#### 🔵 Backend +- **Especialidad**: FastAPI, Python, SQLAlchemy, PostgreSQL +- **Cuándo usar**: API endpoints, modelos de datos, migraciones, tests backend +- **Stack**: FastAPI + SQLAlchemy + Alembic + Pytest + +#### 🔴 Frontend +- **Especialidad**: Next.js, React, TypeScript, UI/UX +- **Cuándo usar**: Componentes React, integración API, testing frontend +- **Stack**: Next.js 15 + React 19 + TypeScript + Vitest + +#### 🟢 Mobile +- **Especialidad**: Android (Kotlin) + iOS (Swift) +- **Cuándo usar**: Apps nativas, ViewModels, Repository pattern +- **Stack**: Jetpack Compose + SwiftUI + Clean Architecture + +--- + +## ⚡ Slash Commands + +Comandos personalizados para tareas frecuentes. + +### Cómo usar slash commands + +En Claude Code CLI o en conversaciones, usa el comando con `/`: + +```bash +/review-api # Revisar endpoints de API +/test-coverage # Analizar cobertura de tests +/security-audit # Auditoría de seguridad +/deploy-checklist # Checklist pre-deployment +``` + +### Comandos disponibles + +#### `/review-api` +**Revisa endpoints REST siguiendo best practices** + +Verifica: +- REST compliance (verbos HTTP, status codes) +- Validaciones y error handling +- Documentación (Swagger) +- Performance (N+1 queries) +- Consistencia en respuestas + +**Output**: Reporte markdown con findings y recomendaciones + +--- + +#### `/test-coverage` +**Analiza cobertura de tests e identifica gaps** + +Revisa: +- Backend: pytest coverage, tests unitarios e integración +- Frontend: componentes sin tests, lógica crítica +- Mobile: ViewModels, Repository, Mappers + +**Output**: Lista priorizada de tests a crear + +--- + +#### `/security-audit` +**Auditoría de seguridad completa del proyecto** + +Analiza: +- Backend: Auth, input validation, secrets, CORS, rate limiting +- Frontend: XSS, sensitive data, API keys, dependencies +- Database: Access control, backups +- Mobile: Permisos, network security, ofuscación + +**Output**: Reporte con vulnerabilidades y remediations + +--- + +#### `/deploy-checklist` +**Checklist completo pre-deployment a producción** + +Verifica: +- Backend: Migrations, tests, environment, security +- Frontend: Build, SEO, performance, error handling +- Mobile: Builds firmados, API endpoints +- Infrastructure: Docker, CI/CD, monitoring, backups + +**Output**: Checklist interactivo con status de cada item + +--- + +## 🪝 Hooks + +Scripts de automatización que se ejecutan en eventos específicos. + +### `session-start.sh` +**Se ejecuta al inicio de cada sesión de Claude** + +Verifica: +- ✅ Docker está corriendo +- ✅ Containers del Backend activos +- ✅ Node.js instalado +- ✅ Dependencias del Frontend instaladas +- ✅ Archivos `.env` existen +- ✅ Estado de Git + +**Uso manual**: +```bash +./.claude/hooks/session-start.sh +``` + +--- + +### `before-commit.sh` +**Pre-commit checks antes de commitear código** + +Ejecuta: +- 🔐 Verifica archivos sensibles (.env, secrets) +- 🐍 Flake8 en Backend (linting) +- ⚛️ ESLint + TypeScript check en Frontend +- 📦 Verifica tamaño de archivos + +**Uso manual**: +```bash +./.claude/hooks/before-commit.sh +``` + +Para ejecutarlo automáticamente antes de cada commit, configura un Git hook: +```bash +ln -s ../../.claude/hooks/before-commit.sh .git/hooks/pre-commit +``` + +--- + +## 📝 Templates + +Templates para documentación consistente. + +### `feature-spec.md` +**Template completo para especificaciones de features** + +Incluye: +- Resumen ejecutivo y objetivos +- Diseño técnico (Backend, Frontend, Mobile) +- Cambios en base de datos +- Plan de testing +- Plan de implementación paso a paso +- Checklist de calidad +- Deployment checklist + +**Cómo usar**: +```bash +cp .claude/templates/feature-spec.md spec/nueva-feature.md +``` + +--- + +## 🚀 Guía de Uso Rápida + +### Scenario 1: Implementar nueva feature + +1. **Arquitecto**: Diseñar la solución + ``` + "Como arquitecto, diseña un sistema de comentarios en cursos" + ``` + +2. **Crear spec**: Usar template + ```bash + cp .claude/templates/feature-spec.md spec/comentarios-cursos.md + ``` + +3. **Backend**: Implementar API + ``` + "Como backend developer, implementa los endpoints de comentarios" + ``` + +4. **Frontend**: Implementar UI + ``` + "Como frontend developer, crea el componente de comentarios" + ``` + +5. **Mobile**: Implementar en apps + ``` + "Como mobile developer, implementa comentarios en Android e iOS" + ``` + +--- + +### Scenario 2: Code Review + +```bash +# Revisar calidad de API +/review-api + +# Verificar seguridad +/security-audit + +# Analizar tests +/test-coverage +``` + +--- + +### Scenario 3: Pre-Deployment + +```bash +# Ejecutar checklist completo +/deploy-checklist + +# Verificar seguridad +/security-audit + +# Ejecutar hooks manualmente +./.claude/hooks/before-commit.sh +``` + +--- + +## 🎓 Best Practices + +### Trabajando con Agentes + +1. **Sé específico**: "Como backend developer, implementa autenticación JWT" +2. **Contexto**: Menciona archivos relevantes +3. **Iterativo**: Valida cada paso antes de continuar + +### Usando Slash Commands + +1. **Frecuencia**: Ejecuta `/review-api` después de cambios en endpoints +2. **Pre-commit**: Ejecuta `/test-coverage` antes de PRs +3. **Pre-deploy**: SIEMPRE ejecuta `/deploy-checklist` + +### Hooks + +1. **Session Start**: Deja que se ejecute automáticamente +2. **Before Commit**: Configura como Git hook para automation + +--- + +## 🔧 Configuración Adicional + +### Git Hooks (Recomendado) + +Para ejecutar hooks automáticamente: + +```bash +# Pre-commit hook +ln -s ../../.claude/hooks/before-commit.sh .git/hooks/pre-commit +chmod +x .git/hooks/pre-commit +``` + +### Editor Integration + +Si usas VS Code, agrega estos snippets para llamar comandos: + +```json +{ + "Claude Review API": { + "prefix": "claude-review", + "body": ["/review-api"] + }, + "Claude Test Coverage": { + "prefix": "claude-tests", + "body": ["/test-coverage"] + } +} +``` + +--- + +## 📚 Referencias + +- [Claude Code Documentation](https://docs.claude.com/en/docs/claude-code) +- [CLAUDE.md](../CLAUDE.md) - Instrucciones del proyecto +- [Makefile](../Makefile) - Comandos globales del proyecto + +--- + +## 🤝 Contribuir + +Para agregar nuevos agentes, comandos o hooks: + +1. Crea el archivo en el directorio apropiado +2. Sigue el formato de los existentes +3. Documenta en este README +4. Testa el comando/hook antes de commitear + +--- + +**¡Configuración completa! Ahora Claude está optimizado para Platziflix 🚀** diff --git a/.claude/agents/mobile.md b/.claude/agents/mobile.md new file mode 100644 index 0000000..8824178 --- /dev/null +++ b/.claude/agents/mobile.md @@ -0,0 +1,188 @@ +--- +name: mobile +description: Especialista en desarrollo mobile Android (Kotlin) y iOS (Swift) +color: green +model: inherit +--- + +# Agent Mobile - Especialista en Desarrollo Mobile + +Eres un especialista en desarrollo mobile con expertise en: + +## Stack Técnico Principal +- **Android**: Kotlin, Jetpack Compose, Material 3, Retrofit +- **iOS**: Swift, SwiftUI, Combine, URLSession +- **Architecture**: Clean Architecture, MVVM, Repository Pattern +- **Networking**: Retrofit (Android), URLSession/Alamofire (iOS) +- **Image Loading**: Coil (Android), Kingfisher/SDWebImage (iOS) +- **Testing**: JUnit + Coroutines Test (Android), XCTest (iOS) + +## Responsabilidades Específicas +1. **UI Components**: Crear componentes nativos reutilizables y responsive +2. **Network Layer**: Implementar llamadas API con manejo de errores robusto +3. **State Management**: ViewModels y flujos de datos reactivos +4. **Platform-specific**: Aprovechar features nativas de cada plataforma +5. **Testing**: Unit tests para ViewModels, Repository y Mappers +6. **Performance**: Optimizar listas, imágenes y memoria + +## Contexto del Proyecto: Platziflix +- **Android**: Kotlin + Jetpack Compose + Retrofit + Coil +- **iOS**: Swift + SwiftUI + Repository Pattern +- **Shared API**: Backend FastAPI en http://localhost:8000 (emulador: 10.0.2.2:8000) +- **Architecture**: Clean Architecture con capas Data/Domain/Presentation + +### Estructura Android +``` +app/ +├── data/ # DTOs, API clients, Repository implementations +├── domain/ # Models, Repository interfaces +├── presentation/ # ViewModels, Screens, Components +└── di/ # Dependency Injection (Hilt/Koin) +``` + +### Estructura iOS +``` +App/ +├── Data/ # DTOs, Entities, Mappers, Repository implementations +├── Domain/ # Models, Repository protocols +├── Presentation/ # ViewModels, Views, Components +└── Services/ # Network layer, API endpoints +``` + +## Patrones y Convenciones + +### Android (Kotlin) +- **Naming**: PascalCase para clases, camelCase para funciones/variables +- **Composables**: Funciones que devuelven UI, nomenclatura descriptiva +- **ViewModels**: StateFlow para estado reactivo +- **Repository**: Inyección de dependencias con constructor +- **Coroutines**: async/await para operaciones asíncronas + +### iOS (Swift) +- **Naming**: PascalCase para types, camelCase para properties/functions +- **Views**: SwiftUI views con @StateObject/@ObservedObject +- **ViewModels**: @Published properties para binding +- **Repository**: Protocol-based con dependency injection +- **Async/Await**: Swift concurrency para operaciones asíncronas + +## Instrucciones de Trabajo +- **Consistencia cross-platform**: Features similares en ambos OS con UX nativa +- **Native patterns**: Usar idioms nativos de cada plataforma (no código genérico) +- **Offline support**: Implementar caching local y manejo elegante de errores de red +- **Responsive**: Soporte para tablets, foldables y diferentes tamaños +- **Accessibility**: VoiceOver (iOS), TalkBack (Android), contrast ratios +- **Testing**: Tests unitarios para lógica de negocio (ViewModels, Mappers) +- **Error handling**: User-friendly messages, retry mechanisms + +## Comandos Frecuentes que Ejecutarás + +### Android +```bash +# Build y testing +./gradlew build +./gradlew test +./gradlew assembleDebug +./gradlew installDebug + +# Linting y análisis +./gradlew lint +./gradlew detekt + +# Ejecutar en emulador +adb devices +adb install -r app/build/outputs/apk/debug/app-debug.apk +``` + +### iOS +```bash +# Build +xcodebuild build -scheme PlatziFlixiOS -configuration Debug + +# Testing +xcodebuild test -scheme PlatziFlixiOS -destination 'platform=iOS Simulator,name=iPhone 15' + +# SwiftLint +swiftlint lint + +# Ejecutar en simulador +xcrun simctl list devices +open -a Simulator +``` + +## Checklist de Calidad Mobile +- [ ] **Networking**: Timeout configurado, retry logic, error handling +- [ ] **UI**: Responsive en diferentes tamaños, dark/light mode +- [ ] **Performance**: Lazy loading de listas, image caching +- [ ] **Testing**: Unit tests para ViewModels (>80% coverage) +- [ ] **Accessibility**: Labels, hints, semantic content +- [ ] **Security**: No API keys hardcodeadas, HTTPS only +- [ ] **Offline**: Manejo de estados sin conexión +- [ ] **Memory**: No memory leaks (profiling con Instruments/Android Profiler) + +## Ejemplos de Código + +### Android - ViewModel con StateFlow +```kotlin +class CourseListViewModel( + private val repository: CourseRepository +) : ViewModel() { + + private val _state = MutableStateFlow(CourseListState.Loading) + val state: StateFlow = _state.asStateFlow() + + init { + loadCourses() + } + + fun loadCourses() { + viewModelScope.launch { + _state.value = CourseListState.Loading + repository.getCourses() + .onSuccess { courses -> + _state.value = CourseListState.Success(courses) + } + .onFailure { error -> + _state.value = CourseListState.Error(error.message) + } + } + } +} +``` + +### iOS - ViewModel con Combine +```swift +class CourseListViewModel: ObservableObject { + @Published var courses: [Course] = [] + @Published var isLoading = false + @Published var errorMessage: String? + + private let repository: CourseRepositoryProtocol + private var cancellables = Set() + + init(repository: CourseRepositoryProtocol) { + self.repository = repository + loadCourses() + } + + func loadCourses() { + isLoading = true + + Task { + do { + let courses = try await repository.getCourses() + await MainActor.run { + self.courses = courses + self.isLoading = false + } + } catch { + await MainActor.run { + self.errorMessage = error.localizedDescription + self.isLoading = false + } + } + } + } +} +``` + +Responde siempre con código nativo idiomático, siguiendo las mejores prácticas de cada plataforma. diff --git a/.claude/commands/deploy-checklist.md b/.claude/commands/deploy-checklist.md new file mode 100644 index 0000000..a71ba69 --- /dev/null +++ b/.claude/commands/deploy-checklist.md @@ -0,0 +1,483 @@ +--- +name: deploy-checklist +description: Checklist completo pre-deployment a producción +--- + +Verifica que el proyecto está listo para deployment a producción. + +# 🚀 PRE-DEPLOYMENT CHECKLIST - PLATZIFLIX + +## 1. BACKEND READINESS + +### 1.1 Database + +- [ ] **Migraciones ejecutadas** + ```bash + cd Backend + docker-compose exec api alembic current + docker-compose exec api alembic upgrade head + ``` + +- [ ] **Backup de DB creado** + ```bash + docker-compose exec db pg_dump -U platziflix_user platziflix_db > backup_$(date +%Y%m%d).sql + ``` + +- [ ] **Seeds actualizados** (si aplica) + ```bash + cd Backend && make seed + ``` + +- [ ] **Índices optimizados** + - Revisar queries lentas + - Agregar índices donde sea necesario + +- [ ] **Connection pooling configurado** + - SQLAlchemy pool_size apropiado + - max_overflow configurado + +### 1.2 Environment Variables + +- [ ] **`.env` de producción creado** + ```bash + # Verificar que existe Backend/.env.production + ``` + +- [ ] **Secrets seguros** + - [ ] `SECRET_KEY` generado con: `openssl rand -hex 32` + - [ ] `DATABASE_URL` con password fuerte + - [ ] No hay valores de desarrollo + +- [ ] **Environment configurado** + - [ ] `DEBUG=False` + - [ ] `ENVIRONMENT=production` + - [ ] `CORS_ORIGINS` con dominios reales + +### 1.3 Tests + +- [ ] **Todos los tests pasando** + ```bash + cd Backend + pytest app/tests/ -v + ``` + +- [ ] **Coverage aceptable** (>80%) + ```bash + pytest app/tests/ --cov=app --cov-report=term + ``` + +- [ ] **Tests de integración funcionando** + +### 1.4 Code Quality + +- [ ] **Linter sin errores** + ```bash + flake8 app/ --max-line-length=120 + ``` + +- [ ] **Type checking** (si aplica) + ```bash + mypy app/ + ``` + +- [ ] **No hay TODOs críticos** + ```bash + grep -r "TODO" Backend/app/ | grep -i "critical\|urgent\|important" + ``` + +### 1.5 Security + +- [ ] **Authentication habilitado** +- [ ] **Rate limiting configurado** +- [ ] **CORS settings para producción** +- [ ] **HTTPS only** +- [ ] **Secrets no hardcodeados** +- [ ] **SQL injection prevención** (ORM) +- [ ] **Input validation completa** + +### 1.6 Logging & Monitoring + +- [ ] **Logging configurado** + - [ ] Log level: INFO o WARNING + - [ ] Logs estructurados (JSON) + - [ ] Rotación de logs + +- [ ] **Error tracking** (Sentry, etc.) + ```python + # SENTRY_DSN configurado + ``` + +- [ ] **Health check funcionando** + ```bash + curl http://localhost:8000/health + ``` + +### 1.7 Performance + +- [ ] **No N+1 queries** +- [ ] **Eager loading implementado** +- [ ] **Caching configurado** (Redis, si aplica) +- [ ] **Query optimization** + +--- + +## 2. FRONTEND READINESS + +### 2.1 Build + +- [ ] **Build exitoso** + ```bash + cd Frontend + yarn build + ``` + +- [ ] **No errores de build** + +- [ ] **Build size aceptable** + ```bash + # Revisar output de next build + # First Load JS < 200 KB es ideal + ``` + +### 2.2 Tests + +- [ ] **Tests pasando** + ```bash + yarn test --passWithNoTests + ``` + +- [ ] **No console.log en producción** + ```bash + grep -r "console.log" src/ --include="*.ts" --include="*.tsx" + ``` + +### 2.3 Environment + +- [ ] **`.env.production` creado** +- [ ] **API_URL apunta a producción** + ```bash + NEXT_PUBLIC_API_URL=https://api.platziflix.com + ``` + +- [ ] **Feature flags configurados** + +### 2.4 SEO & Meta + +- [ ] **Meta tags completos** + - [ ] Title + - [ ] Description + - [ ] Open Graph tags + - [ ] Twitter Cards + +- [ ] **Sitemap generado** +- [ ] **robots.txt configurado** +- [ ] **Favicon y app icons** + +### 2.5 Performance + +- [ ] **Images optimizadas** + - [ ] Usando next/image + - [ ] Formatos modernos (WebP) + - [ ] Lazy loading + +- [ ] **Code splitting** + - [ ] Dynamic imports donde corresponde + - [ ] Route-based code splitting + +- [ ] **Bundle analysis** + ```bash + yarn analyze + ``` + +- [ ] **Lighthouse score > 90** + +### 2.6 Security + +- [ ] **No API keys expuestas** +- [ ] **CSP headers configurados** +- [ ] **HTTPS only** +- [ ] **Secure cookies** (httpOnly, secure, sameSite) + +### 2.7 Error Handling + +- [ ] **Error boundaries implementados** +- [ ] **404 page customizada** +- [ ] **500 page customizada** +- [ ] **Loading states apropiados** + +--- + +## 3. MOBILE READINESS + +### 3.1 Android + +- [ ] **Build release exitoso** + ```bash + cd Mobile/PlatziFlixAndroid + ./gradlew assembleRelease + ``` + +- [ ] **APK firmado** + - [ ] Keystore generado + - [ ] signing config en gradle + +- [ ] **ProGuard/R8 habilitado** + +- [ ] **Version code incrementado** + ```kotlin + // app/build.gradle.kts + versionCode = 2 + versionName = "1.1.0" + ``` + +- [ ] **API endpoints apuntan a producción** + +- [ ] **Tests pasando** + ```bash + ./gradlew test + ``` + +### 3.2 iOS + +- [ ] **Build release exitoso** + ```bash + cd Mobile/PlatziFlixiOS + xcodebuild archive -scheme PlatziFlixiOS -configuration Release + ``` + +- [ ] **App firmada con certificado de distribución** + +- [ ] **Version y build number incrementados** + +- [ ] **API endpoints apuntan a producción** + +- [ ] **Tests pasando** + ```bash + xcodebuild test -scheme PlatziFlixiOS + ``` + +--- + +## 4. INFRASTRUCTURE & DEVOPS + +### 4.1 Docker + +- [ ] **Dockerfile optimizado** + - [ ] Multi-stage build + - [ ] Imagen mínima + - [ ] USER no-root + +- [ ] **Docker Compose para producción** + - [ ] restart: always + - [ ] resource limits + - [ ] health checks + +- [ ] **Images pushed a registry** + ```bash + docker push platziflix-backend:v1.0.0 + ``` + +### 4.2 CI/CD + +- [ ] **Pipeline ejecutando correctamente** + - [ ] Tests automáticos + - [ ] Build verification + - [ ] Security scanning + +- [ ] **Deployment automatizado** (opcional) + +- [ ] **Rollback strategy documentada** + +### 4.3 Monitoring + +- [ ] **Application monitoring configurado** + - [ ] Sentry / DataDog / New Relic + +- [ ] **Infrastructure monitoring** + - [ ] CPU, Memory, Disk usage + - [ ] Request rates + - [ ] Error rates + +- [ ] **Alertas configuradas** + - [ ] High error rate + - [ ] Service down + - [ ] High latency + +### 4.4 Backup & Recovery + +- [ ] **Backup automático de DB** + - [ ] Daily backups + - [ ] Retention policy + +- [ ] **Backup testing** + - [ ] Restore procedure documentado + - [ ] Backup tested recientemente + +- [ ] **Disaster recovery plan** + +--- + +## 5. DOCUMENTATION + +- [ ] **README.md actualizado** + - [ ] Instrucciones de instalación + - [ ] Comandos comunes + - [ ] Troubleshooting + +- [ ] **API documentation actualizada** + - [ ] Swagger/OpenAPI up to date + - [ ] Ejemplos actualizados + +- [ ] **CHANGELOG.md actualizado** + +- [ ] **Deployment documentation** + - [ ] Deployment steps + - [ ] Rollback procedure + - [ ] Environment variables + +- [ ] **Architecture diagram** (si es major release) + +--- + +## 6. LEGAL & COMPLIANCE + +- [ ] **Privacy policy actualizada** +- [ ] **Terms of service actualizados** +- [ ] **GDPR compliance** (si aplica) +- [ ] **License files presentes** + +--- + +## 7. COMMUNICATION + +- [ ] **Stakeholders notificados** +- [ ] **Maintenance window comunicado** (si aplica) +- [ ] **Release notes preparados** +- [ ] **Support team briefed** + +--- + +## 8. POST-DEPLOYMENT + +### Inmediatamente después del deploy + +- [ ] **Health checks pasando** + ```bash + curl https://api.platziflix.com/health + ``` + +- [ ] **Smoke tests ejecutados** + - [ ] Login funciona + - [ ] Features principales funcionan + - [ ] No errores en consola + +- [ ] **Monitoring activo** + - [ ] Error rates normales + - [ ] Response times aceptables + - [ ] No memory leaks + +### Primera hora + +- [ ] **Logs monitoreados** + - [ ] No errores críticos + - [ ] Tráfico normal + +- [ ] **User feedback monitoreado** + - [ ] Support tickets + - [ ] Social media + +### Primer día + +- [ ] **Performance metrics revisados** +- [ ] **Error rates comparados con baseline** +- [ ] **User adoption monitoreado** + +--- + +## 9. ROLLBACK PLAN + +**Si algo sale mal**: + +1. **Identificar el problema** + - Revisar logs + - Revisar monitoring + - Reproducir issue + +2. **Decidir: Fix forward o Rollback** + - Hot fix si es rápido (<15 min) + - Rollback si es complejo + +3. **Ejecutar rollback** + ```bash + # Docker + docker-compose up -d --scale api=3 platziflix-backend:v0.9.0 + + # Database rollback + alembic downgrade -1 + ``` + +4. **Comunicar** + - Notificar a stakeholders + - Post-mortem después + +--- + +## OUTPUT FORMAT + +Genera un checklist interactivo: + +```markdown +# 🚀 DEPLOYMENT READINESS REPORT +**Date**: [Fecha] +**Version**: [Version a deployar] +**Target**: Production + +--- + +## Summary +- ✅ Passed: X/Y checks +- ⚠️ Warnings: Z checks +- ❌ Failed: W checks + +**Status**: 🟢 READY | 🟡 READY WITH WARNINGS | 🔴 NOT READY + +--- + +## Detailed Results + +### Backend +- ✅ Tests passing (100%) +- ✅ Migrations executed +- ⚠️ Coverage at 75% (target: 80%) +- ❌ Rate limiting not configured + +### Frontend +- ✅ Build successful +- ✅ Tests passing +- ✅ Performance score: 94 + +### Mobile +- ✅ Android build signed +- ⚠️ iOS tests skipped + +--- + +## Blockers +1. ❌ Rate limiting must be configured before deploy +2. ❌ Production secrets not configured + +## Recommendations +1. ⚠️ Increase test coverage to 80%+ +2. ⚠️ Setup error monitoring (Sentry) + +--- + +## Next Steps +1. [ ] Fix blockers +2. [ ] Address high-priority warnings +3. [ ] Schedule deployment window +4. [ ] Execute deployment +5. [ ] Monitor for 24 hours +``` + +**Sé honesto sobre el estado del proyecto. Es mejor retrasar un deploy que tener problemas en producción.** diff --git a/.claude/commands/review-api.md b/.claude/commands/review-api.md new file mode 100644 index 0000000..d9095e2 --- /dev/null +++ b/.claude/commands/review-api.md @@ -0,0 +1,97 @@ +--- +name: review-api +description: Revisa endpoints de API siguiendo REST best practices +--- + +Realiza una revisión exhaustiva de los endpoints de la API en `Backend/app/main.py`: + +## 1. REST Compliance + +Verifica que cada endpoint cumple con: +- **Verbos HTTP correctos**: + - GET para lectura + - POST para creación + - PUT/PATCH para actualización + - DELETE para eliminación +- **Status codes apropiados**: + - 200 OK, 201 Created, 204 No Content + - 400 Bad Request, 404 Not Found + - 500 Internal Server Error +- **Naming conventions**: + - Recursos en plural: `/courses`, `/ratings` + - Kebab-case para paths + - Parámetros descriptivos + +## 2. Validaciones y Seguridad + +Revisa: +- **Pydantic schemas**: Todas las request/response tienen schemas definidos +- **Error handling**: Try/except con mensajes user-friendly +- **Input validation**: Validación de tipos y constraints +- **SQL Injection**: Uso de ORM en lugar de queries raw +- **Authentication**: Endpoints protegidos (si aplica) +- **Rate limiting**: Considerar implementación +- **CORS**: Configuración apropiada + +## 3. Documentación + +Evalúa: +- **Docstrings**: Cada endpoint tiene docstring descriptivo +- **OpenAPI tags**: Endpoints agrupados lógicamente +- **Response models**: response_model definido +- **Ejemplos**: Ejemplos en docstrings para Swagger +- **Error responses**: Responses documentados en decorador + +## 4. Performance + +Analiza: +- **N+1 queries**: Uso de `joinedload()` para relaciones +- **Eager loading**: Cargar relaciones necesarias +- **Índices**: Campos de búsqueda indexados en modelos +- **Paginación**: Endpoints que retornan listas implementan paginación +- **Caching**: Considerar cache para datos estáticos + +## 5. Consistency + +Verifica consistencia en: +- Formato de respuestas (siempre JSON) +- Naming de campos (snake_case) +- Estructura de errores +- Headers HTTP +- Versionado de API (si aplica) + +## Output + +Genera un reporte en formato markdown con: + +```markdown +# API Review Report - [Fecha] + +## Summary +- Total endpoints: X +- Issues found: Y +- Critical: Z + +## Findings + +### 🔴 Critical Issues +1. [Issue] - [Endpoint] - [Descripción] + +### 🟡 Warnings +1. [Issue] - [Endpoint] - [Descripción] + +### 🟢 Good Practices Found +1. [Practice] - [Endpoint] + +## Recommendations +1. Prioridad Alta: [...] +2. Prioridad Media: [...] +3. Prioridad Baja: [...] + +## Next Steps +- [ ] Fix critical issues +- [ ] Address warnings +- [ ] Implement recommended improvements +``` + +Sé detallado y constructivo en tus observaciones. diff --git a/.claude/commands/security-audit.md b/.claude/commands/security-audit.md new file mode 100644 index 0000000..2a197c1 --- /dev/null +++ b/.claude/commands/security-audit.md @@ -0,0 +1,385 @@ +--- +name: security-audit +description: Auditoría de seguridad completa del proyecto +--- + +Realiza una auditoría de seguridad exhaustiva del proyecto Platziflix. + +# 🔐 SECURITY AUDIT - PLATZIFLIX + +## 1. BACKEND SECURITY (FastAPI) + +### 1.1 Authentication & Authorization + +**Verificar**: +- [ ] ¿Está implementado sistema de autenticación? +- [ ] ¿Se valida el `user_id` en requests? +- [ ] ¿Hay protección contra CSRF? +- [ ] ¿Se usan tokens JWT correctamente? +- [ ] ¿Los passwords están hasheados? (bcrypt, argon2) +- [ ] ¿Hay rate limiting por usuario? + +**Archivos a revisar**: +- `Backend/app/main.py` - Endpoints sin protección +- `Backend/app/core/security.py` - (si existe) +- `Backend/app/middleware/` - Middleware de auth + +**Issues conocidos**: +```python +# Backend/app/main.py línea 157-186 +# CRÍTICO: user_id no validado, cualquiera puede crear ratings +@app.post("/courses/{course_id}/ratings") +def add_course_rating(rating_data: RatingRequest, ...): + # No verifica que el user_id sea del usuario autenticado +``` + +### 1.2 Input Validation + +**Verificar**: +- [ ] Pydantic schemas en todos los endpoints +- [ ] Validación de tipos (int, str, email) +- [ ] Constraints de rangos (min/max) +- [ ] Sanitización de strings +- [ ] Validación de IDs (positivos, existentes) +- [ ] Path traversal prevention + +**SQL Injection**: +- [ ] Uso de ORM (SQLAlchemy) ✅ +- [ ] No queries raw SQL con f-strings +- [ ] Parámetros bindeados en queries + +**Comando**: +```bash +cd Backend +grep -r "f\"SELECT" app/ +grep -r "execute(text(" app/ +``` + +### 1.3 Secrets & Configuration + +**Verificar**: +- [ ] No hay API keys hardcodeadas +- [ ] Passwords no en código fuente +- [ ] `.env` en `.gitignore` +- [ ] Secrets en environment variables +- [ ] `SECRET_KEY` fuerte en producción + +**Buscar secrets hardcodeados**: +```bash +grep -r "password.*=" Backend/app/ --include="*.py" +grep -r "api_key" Backend/app/ --include="*.py" +grep -r "secret" Backend/app/ --include="*.py" +``` + +### 1.4 CORS Configuration + +**Verificar**: +- [ ] CORS configurado en FastAPI +- [ ] Origins específicos (no `*` en producción) +- [ ] Credentials handling apropiado + +**Buscar**: +```python +# Backend/app/main.py +# ¿Existe CORSMiddleware? +from fastapi.middleware.cors import CORSMiddleware +``` + +### 1.5 Rate Limiting + +**Verificar**: +- [ ] Rate limiting implementado +- [ ] Límites por endpoint +- [ ] Límites por IP +- [ ] Protección contra brute force + +**Librerías recomendadas**: +- slowapi +- fastapi-limiter + +### 1.6 Dependencies Vulnerabilities + +**Ejecutar**: +```bash +cd Backend +pip install safety +safety check +``` + +O con Docker: +```bash +cd Backend +docker-compose exec api pip install safety +docker-compose exec api safety check +``` + +### 1.7 Error Handling + +**Verificar**: +- [ ] No se exponen stack traces en producción +- [ ] Mensajes de error genéricos al usuario +- [ ] Logging apropiado de errores +- [ ] No se revelan detalles de implementación + +--- + +## 2. FRONTEND SECURITY (Next.js) + +### 2.1 XSS (Cross-Site Scripting) + +**Verificar**: +- [ ] No uso de `dangerouslySetInnerHTML` +- [ ] Inputs sanitizados antes de render +- [ ] Content Security Policy configurado +- [ ] Escape de datos de usuario + +**Buscar**: +```bash +cd Frontend +grep -r "dangerouslySetInnerHTML" src/ +grep -r "innerHTML" src/ +``` + +### 2.2 Sensitive Data Exposure + +**Verificar**: +- [ ] API keys no en código frontend +- [ ] Uso de `NEXT_PUBLIC_` solo para datos públicos +- [ ] No tokens en localStorage sin encriptar +- [ ] HTTPS only en producción +- [ ] Cookies con flags `secure`, `httpOnly`, `sameSite` + +**Buscar**: +```bash +cd Frontend +grep -r "localStorage.setItem" src/ +grep -r "sessionStorage" src/ +grep -r "API_KEY" src/ +``` + +### 2.3 API Keys & Environment Variables + +**Verificar**: +- [ ] `.env.local` en `.gitignore` +- [ ] No API keys en código +- [ ] Variables sensibles sin `NEXT_PUBLIC_` + +**Buscar en Git history**: +```bash +git log --all --full-history -- "*.env" +``` + +### 2.4 Dependency Vulnerabilities + +**Ejecutar**: +```bash +cd Frontend +yarn audit +# o +npm audit +``` + +**Arreglar vulnerabilities**: +```bash +yarn audit fix +``` + +### 2.5 HTTPS & Security Headers + +**Verificar en producción**: +- [ ] HTTPS enabled +- [ ] HSTS header +- [ ] X-Frame-Options +- [ ] X-Content-Type-Options +- [ ] Referrer-Policy +- [ ] Content-Security-Policy + +--- + +## 3. DATABASE SECURITY (PostgreSQL) + +### 3.1 Access Control + +**Verificar**: +- [ ] User de DB con mínimos privilegios +- [ ] Password fuerte +- [ ] Conexión solo desde containers autorizados +- [ ] No puerto 5432 expuesto públicamente en prod + +**Revisar**: +```yaml +# Backend/docker-compose.yml +# ¿Está expuesto el puerto 5432? +ports: + - "5432:5432" # ⚠️ Solo para desarrollo +``` + +### 3.2 Data Integrity + +**Verificar**: +- [ ] Foreign keys con CASCADE apropiado +- [ ] CHECK constraints +- [ ] NOT NULL donde corresponde +- [ ] UNIQUE constraints +- [ ] Soft deletes implementados ✅ + +### 3.3 Backup & Recovery + +**Verificar**: +- [ ] Estrategia de backup definida +- [ ] Backups automáticos +- [ ] Plan de disaster recovery +- [ ] Backups encriptados + +--- + +## 4. MOBILE SECURITY + +### 4.1 Android + +**Verificar**: +- [ ] Manifest sin permisos innecesarios +- [ ] Network security config (HTTPS only) +- [ ] API keys ofuscadas (BuildConfig) +- [ ] ProGuard/R8 habilitado en release +- [ ] Certificate pinning para API + +**Revisar**: +```xml + + +``` + +### 4.2 iOS + +**Verificar**: +- [ ] App Transport Security configurado +- [ ] Keychain para datos sensibles +- [ ] API keys en configuración, no hardcodeadas +- [ ] Certificate pinning + +--- + +## 5. CI/CD & INFRASTRUCTURE + +### 5.1 GitHub Actions + +**Verificar**: +- [ ] Secrets en GitHub Secrets (no en workflows) +- [ ] Permisos mínimos en workflows +- [ ] No exposición de tokens en logs + +### 5.2 Docker + +**Verificar**: +- [ ] Base images oficiales y actualizadas +- [ ] Multi-stage builds para reducir superficie +- [ ] No copia de archivos sensibles en imagen +- [ ] USER no-root en Dockerfile + +--- + +## OUTPUT REPORT + +Genera el siguiente reporte: + +```markdown +# 🔐 SECURITY AUDIT REPORT - PLATZIFLIX +**Date**: [Fecha] +**Auditor**: Claude Agent + +--- + +## EXECUTIVE SUMMARY + +- **Critical Issues**: X +- **High Priority**: Y +- **Medium Priority**: Z +- **Low Priority**: W + +**Overall Security Score**: X/100 + +--- + +## 🔴 CRITICAL VULNERABILITIES (Fix Immediately) + +### 1. [Vulnerability Name] +- **Severity**: Critical +- **Component**: Backend/Frontend/Mobile +- **Location**: [Archivo:línea] +- **Description**: [Descripción del problema] +- **Impact**: [Qué puede pasar] +- **Remediation**: + ```python + # Código para arreglar + ``` + +--- + +## 🟡 HIGH PRIORITY ISSUES + +### 1. [Issue] +- **Severity**: High +- **Component**: [Backend/Frontend] +- **Description**: [...] +- **Remediation**: [...] + +--- + +## 🟢 MEDIUM & LOW PRIORITY + +[Lista de issues no críticos] + +--- + +## ✅ SECURITY BEST PRACTICES FOUND + +1. SQLAlchemy ORM previene SQL injection +2. Soft deletes implementados +3. Pydantic validation en API +4. [Otros puntos positivos] + +--- + +## 📋 ACTION ITEMS (Prioritized) + +### Immediate (Week 1) +- [ ] [Critical fix 1] +- [ ] [Critical fix 2] + +### Short-term (Month 1) +- [ ] [High priority fix 1] +- [ ] [High priority fix 2] + +### Long-term (Backlog) +- [ ] [Medium/Low priority items] + +--- + +## 🛡️ RECOMMENDATIONS + +### Backend +1. Implementar autenticación con JWT +2. Agregar rate limiting +3. Configurar CORS apropiadamente + +### Frontend +1. Implementar CSP headers +2. Audit y fix de dependencias +3. Revisar manejo de tokens + +### Infrastructure +1. Setup automated security scanning +2. Implement backup strategy +3. Add monitoring and alerting + +--- + +## 📚 REFERENCES + +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) +- [FastAPI Security](https://fastapi.tiangolo.com/tutorial/security/) +- [Next.js Security](https://nextjs.org/docs/authentication) +``` + +**Enfócate en vulnerabilidades reales y provee soluciones accionables.** diff --git a/.claude/commands/test-coverage.md b/.claude/commands/test-coverage.md new file mode 100644 index 0000000..85fb3a0 --- /dev/null +++ b/.claude/commands/test-coverage.md @@ -0,0 +1,149 @@ +--- +name: test-coverage +description: Analiza cobertura de tests y sugiere tests faltantes +--- + +Analiza la cobertura de tests del proyecto Platziflix e identifica gaps de testing. + +## BACKEND (Backend/app/tests/) + +### 1. Ejecutar Coverage Report + +Si Docker está corriendo: +```bash +cd Backend +docker-compose exec api pytest app/tests/ --cov=app --cov-report=term-missing --cov-report=html +``` + +### 2. Analizar Coverage + +Identifica: +- **Archivos sin tests**: Modelos, servicios, endpoints sin coverage +- **Líneas no cubiertas**: Funciones críticas sin testear +- **Edge cases**: Casos límite no considerados + +### 3. Revisar Tests Existentes + +Para cada test file: +- ¿Sigue AAA pattern? (Arrange, Act, Assert) +- ¿Tests unitarios vs integración? +- ¿Mock de dependencias externas? +- ¿Assertions completas? + +### 4. Gaps Identificados + +Lista casos de prueba faltantes: +- Validaciones de modelos +- Lógica de servicios +- Error handling en endpoints +- Soft deletes +- Constraints de DB + +## FRONTEND (Frontend/src/) + +### 1. Listar Componentes + +Revisa estructura: +``` +Frontend/src/components/ +Frontend/src/app/ +``` + +### 2. Identificar Componentes Sin Tests + +Para cada componente: +- ¿Tiene archivo `*.test.tsx`? +- ¿Qué funcionalidad crítica no está testeada? + +### 3. Lógica No Testeada + +Identifica: +- Custom hooks sin tests +- API calls sin tests +- Form validations +- Error boundaries +- Loading states + +### 4. Gaps Identificados + +Lista componentes y funcionalidad a testear: +- Renders condicionales +- User interactions (clicks, inputs) +- API integration +- Error states + +## MOBILE (Opcional) + +### Android (Mobile/PlatziFlixAndroid/) +- ViewModels: ¿Tests de estados? +- Repository: ¿Tests de transformaciones? +- Mappers: ¿Tests de conversión DTO → Domain? + +### iOS (Mobile/PlatziFlixiOS/) +- ViewModels: ¿Tests con XCTest? +- Repository: ¿Tests de network calls? +- Mappers: ¿Tests de transformaciones? + +## Output Report + +Genera reporte con priorización: + +```markdown +# Test Coverage Report - [Fecha] + +## Executive Summary +- Backend Coverage: X% +- Frontend Coverage: Y% +- Critical gaps: Z + +## Backend Tests + +### ✅ Well Tested +- [File] - Coverage: XX% + +### ❌ Missing Tests +1. **[File/Module]** - Priority: HIGH/MEDIUM/LOW + - Missing: [Descripción de qué falta testear] + - Suggested tests: + - `test_[scenario]`: [Descripción] + - `test_[edge_case]`: [Descripción] + +## Frontend Tests + +### ✅ Well Tested Components +- [Component] - Tests: X + +### ❌ Untested Components +1. **[Component]** - Priority: HIGH/MEDIUM/LOW + - Missing tests: + - Should render correctly + - Should handle user interaction + - Should display error state + +## Recommended Test Suite + +### Backend (crear estos archivos): +1. `Backend/app/tests/test_[feature].py` + ```python + # Template de test sugerido + ``` + +### Frontend (crear estos archivos): +1. `Frontend/src/components/[Component]/__tests__/[Component].test.tsx` + ```typescript + // Template de test sugerido + ``` + +## Action Items +- [ ] Alcanzar 80% coverage en Backend +- [ ] Alcanzar 70% coverage en Frontend +- [ ] Testear todos los componentes críticos +- [ ] Agregar integration tests + +## Priorización +1. **Inmediato**: [Tests críticos faltantes] +2. **Esta semana**: [Tests importantes] +3. **Backlog**: [Tests nice-to-have] +``` + +Enfócate en identificar gaps críticos que afecten funcionalidad core. diff --git a/.claude/hooks/before-commit.sh b/.claude/hooks/before-commit.sh new file mode 100755 index 0000000..b28b2fa --- /dev/null +++ b/.claude/hooks/before-commit.sh @@ -0,0 +1,198 @@ +#!/bin/bash + +# ============================================ +# PLATZIFLIX - Before Commit Hook +# ============================================ +# Este hook se ejecuta antes de hacer commits +# Verifica calidad de código y tests + +set -e + +echo "╔═══════════════════════════════════════════════════╗" +echo "║ 🔍 PRE-COMMIT CHECKS - PLATZIFLIX ║" +echo "╚═══════════════════════════════════════════════════╝" +echo "" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +CYAN='\033[0;36m' +NC='\033[0m' + +ERRORS=0 + +# ============================================ +# 1. Verificar que hay cambios para commitear +# ============================================ +if git diff --cached --quiet; then + echo -e "${YELLOW}⚠️ No hay cambios staged para commit${NC}" + exit 1 +fi + +echo -e "${CYAN}📝 Archivos a commitear:${NC}" +git diff --cached --name-only +echo "" + +# ============================================ +# 2. Verificar archivos sensibles +# ============================================ +echo -e "${CYAN}🔐 Verificando archivos sensibles...${NC}" + +SENSITIVE_FILES=( + ".env" + ".env.local" + ".env.production" + "secrets.yaml" + "credentials.json" + "*.pem" + "*.key" + "id_rsa" +) + +for pattern in "${SENSITIVE_FILES[@]}"; do + if git diff --cached --name-only | grep -q "$pattern"; then + echo -e "${RED}❌ PELIGRO: Intentando commitear archivo sensible: $pattern${NC}" + echo -e "${RED} NO commitees archivos con secrets o credenciales${NC}" + ERRORS=$((ERRORS + 1)) + fi +done + +if [ $ERRORS -eq 0 ]; then + echo -e "${GREEN}✓ No se detectaron archivos sensibles${NC}" +fi +echo "" + +# ============================================ +# 3. Backend Checks +# ============================================ +if git diff --cached --name-only | grep -q "^Backend/"; then + echo -e "${CYAN}🐍 Verificando Backend (Python)...${NC}" + + # Check si Docker está corriendo + if docker info > /dev/null 2>&1; then + cd Backend + + # Linting con flake8 (si está instalado) + echo -e "${YELLOW} Running flake8...${NC}" + if docker-compose exec -T api flake8 app/ --max-line-length=120 --exclude=__pycache__,migrations 2>/dev/null; then + echo -e "${GREEN} ✓ Flake8 passed${NC}" + else + echo -e "${YELLOW} ⚠️ Flake8 warnings (no bloqueante)${NC}" + fi + + # Type checking con mypy (opcional) + # echo -e "${YELLOW} Running mypy...${NC}" + # docker-compose exec -T api mypy app/ 2>/dev/null || true + + cd .. + else + echo -e "${YELLOW} ⚠️ Docker no está corriendo, saltando checks de Backend${NC}" + fi + echo "" +fi + +# ============================================ +# 4. Frontend Checks +# ============================================ +if git diff --cached --name-only | grep -q "^Frontend/"; then + echo -e "${CYAN}⚛️ Verificando Frontend (TypeScript)...${NC}" + + if [ -d "Frontend/node_modules" ]; then + cd Frontend + + # ESLint + echo -e "${YELLOW} Running ESLint...${NC}" + if yarn lint --quiet 2>/dev/null; then + echo -e "${GREEN} ✓ ESLint passed${NC}" + else + echo -e "${RED} ❌ ESLint failed${NC}" + ERRORS=$((ERRORS + 1)) + fi + + # TypeScript type check + echo -e "${YELLOW} Running TypeScript check...${NC}" + if yarn tsc --noEmit 2>/dev/null; then + echo -e "${GREEN} ✓ TypeScript check passed${NC}" + else + echo -e "${RED} ❌ TypeScript errors found${NC}" + ERRORS=$((ERRORS + 1)) + fi + + cd .. + else + echo -e "${YELLOW} ⚠️ node_modules no encontrado, saltando checks de Frontend${NC}" + fi + echo "" +fi + +# ============================================ +# 5. Tests (opcional pero recomendado) +# ============================================ +echo -e "${CYAN}🧪 Tests...${NC}" +echo -e "${YELLOW}Ejecutar tests antes de commit?${NC}" +echo -e "${YELLOW}(Recomendado pero puede ser lento)${NC}" +echo -e "${YELLOW}Para ejecutar tests: make test${NC}" +echo -e "${YELLOW}Saltando por ahora...${NC}" +echo "" + +# Descomentar para ejecutar tests automáticamente: +# if git diff --cached --name-only | grep -q "^Backend/"; then +# echo -e "${YELLOW} Running Backend tests...${NC}" +# cd Backend && make test-quick || ERRORS=$((ERRORS + 1)) +# cd .. +# fi +# +# if git diff --cached --name-only | grep -q "^Frontend/"; then +# echo -e "${YELLOW} Running Frontend tests...${NC}" +# cd Frontend && yarn test --run || ERRORS=$((ERRORS + 1)) +# cd .. +# fi + +# ============================================ +# 6. Verificar tamaño de archivos +# ============================================ +echo -e "${CYAN}📦 Verificando tamaño de archivos...${NC}" + +MAX_FILE_SIZE=5242880 # 5MB en bytes + +while IFS= read -r file; do + if [ -f "$file" ]; then + size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null) + if [ "$size" -gt "$MAX_FILE_SIZE" ]; then + size_mb=$(echo "scale=2; $size / 1048576" | bc) + echo -e "${RED}❌ Archivo muy grande: $file (${size_mb}MB)${NC}" + echo -e "${YELLOW} Considera usar Git LFS para archivos grandes${NC}" + ERRORS=$((ERRORS + 1)) + fi + fi +done < <(git diff --cached --name-only) + +if [ $ERRORS -eq 0 ]; then + echo -e "${GREEN}✓ Todos los archivos tienen tamaño apropiado${NC}" +fi +echo "" + +# ============================================ +# 7. Resultado final +# ============================================ +echo "╔═══════════════════════════════════════════════════╗" + +if [ $ERRORS -eq 0 ]; then + echo "║ ✅ PRE-COMMIT CHECKS PASSED ║" + echo "╚═══════════════════════════════════════════════════╝" + echo "" + echo -e "${GREEN}🎉 Todo listo para commit!${NC}" + echo "" + echo -e "${CYAN}Próximos pasos:${NC}" + echo " git commit -m 'tu mensaje descriptivo'" + echo " git push" + exit 0 +else + echo "║ ❌ PRE-COMMIT CHECKS FAILED ║" + echo "╚═══════════════════════════════════════════════════╝" + echo "" + echo -e "${RED}❌ Se encontraron ${ERRORS} error(es)${NC}" + echo -e "${YELLOW}Corrige los errores antes de commitear${NC}" + exit 1 +fi diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100755 index 0000000..6e03a5a --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,153 @@ +#!/bin/bash + +# ============================================ +# PLATZIFLIX - Session Start Hook +# ============================================ +# Este hook se ejecuta al inicio de cada sesión de Claude +# Verifica el estado del ambiente de desarrollo + +set -e + +echo "╔═══════════════════════════════════════════════════╗" +echo "║ 🚀 INICIANDO SESIÓN DE CLAUDE - PLATZIFLIX ║" +echo "╚═══════════════════════════════════════════════════╝" +echo "" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# ============================================ +# 1. Verificar Docker +# ============================================ +echo -e "${CYAN}📦 Verificando Docker...${NC}" + +if ! command -v docker &> /dev/null; then + echo -e "${RED}❌ Docker no está instalado${NC}" + echo -e "${YELLOW} Instala Docker Desktop: https://www.docker.com/products/docker-desktop${NC}" + exit 1 +fi + +if ! docker info > /dev/null 2>&1; then + echo -e "${RED}❌ Docker no está corriendo${NC}" + echo -e "${YELLOW} Inicia Docker Desktop y vuelve a intentar${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ Docker está corriendo${NC}" + +# ============================================ +# 2. Verificar Docker Compose +# ============================================ +if ! command -v docker-compose &> /dev/null; then + echo -e "${YELLOW}⚠️ docker-compose no encontrado (usando 'docker compose')${NC}" +fi + +# ============================================ +# 3. Verificar estado de containers del Backend +# ============================================ +echo "" +echo -e "${CYAN}🐳 Verificando containers del Backend...${NC}" + +cd Backend + +if docker-compose ps | grep -q "Up"; then + echo -e "${GREEN}✓ Containers del Backend están corriendo${NC}" + + # Mostrar estado de containers + echo -e "${CYAN} Estado de servicios:${NC}" + docker-compose ps --format "table {{.Name}}\t{{.Status}}" | tail -n +2 +else + echo -e "${YELLOW}⚠️ Containers del Backend no están corriendo${NC}" + echo -e "${YELLOW} Para iniciarlos: cd Backend && make start${NC}" +fi + +cd .. + +# ============================================ +# 4. Verificar Node.js y dependencias del Frontend +# ============================================ +echo "" +echo -e "${CYAN}📦 Verificando Frontend...${NC}" + +if ! command -v node &> /dev/null; then + echo -e "${RED}❌ Node.js no está instalado${NC}" + echo -e "${YELLOW} Instala Node.js: https://nodejs.org/${NC}" +else + NODE_VERSION=$(node --version) + echo -e "${GREEN}✓ Node.js ${NODE_VERSION} instalado${NC}" +fi + +if [ ! -d "Frontend/node_modules" ]; then + echo -e "${YELLOW}⚠️ node_modules no encontrado${NC}" + echo -e "${YELLOW} Instala dependencias: cd Frontend && yarn install${NC}" +else + echo -e "${GREEN}✓ Dependencias del Frontend instaladas${NC}" +fi + +# ============================================ +# 5. Verificar archivos de entorno +# ============================================ +echo "" +echo -e "${CYAN}🔐 Verificando archivos de entorno...${NC}" + +if [ ! -f "Backend/.env" ]; then + echo -e "${YELLOW}⚠️ Backend/.env no encontrado${NC}" + echo -e "${YELLOW} Copia el template: cp Backend/.env.example Backend/.env${NC}" +else + echo -e "${GREEN}✓ Backend/.env existe${NC}" +fi + +if [ ! -f "Frontend/.env.local" ]; then + echo -e "${YELLOW}⚠️ Frontend/.env.local no encontrado${NC}" + echo -e "${YELLOW} Copia el template: cp Frontend/.env.example Frontend/.env.local${NC}" +else + echo -e "${GREEN}✓ Frontend/.env.local existe${NC}" +fi + +# ============================================ +# 6. Verificar Git status +# ============================================ +echo "" +echo -e "${CYAN}📝 Estado de Git...${NC}" + +if [ -d ".git" ]; then + BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown") + echo -e "${GREEN}✓ Branch actual: ${BRANCH}${NC}" + + # Verificar si hay cambios sin commit + if ! git diff-index --quiet HEAD -- 2>/dev/null; then + MODIFIED_FILES=$(git status --short | wc -l) + echo -e "${YELLOW}⚠️ ${MODIFIED_FILES} archivo(s) con cambios sin commit${NC}" + fi +else + echo -e "${YELLOW}⚠️ No es un repositorio Git${NC}" +fi + +# ============================================ +# 7. Resumen y próximos pasos +# ============================================ +echo "" +echo "╔═══════════════════════════════════════════════════╗" +echo "║ 📋 COMANDOS ÚTILES ║" +echo "╚═══════════════════════════════════════════════════╝" +echo "" +echo -e "${CYAN}Global:${NC}" +echo " make help - Ver todos los comandos disponibles" +echo " make start - Iniciar Backend + Frontend" +echo " make test - Ejecutar todos los tests" +echo "" +echo -e "${CYAN}Backend:${NC}" +echo " cd Backend && make start - Iniciar API + DB" +echo " cd Backend && make migrate - Ejecutar migraciones" +echo " cd Backend && make logs - Ver logs" +echo "" +echo -e "${CYAN}Frontend:${NC}" +echo " cd Frontend && yarn dev - Iniciar servidor dev" +echo " cd Frontend && yarn test - Ejecutar tests" +echo "" +echo -e "${GREEN}✅ Ambiente verificado - ¡Listo para desarrollar!${NC}" +echo "" diff --git a/.claude/templates/feature-spec.md b/.claude/templates/feature-spec.md new file mode 100644 index 0000000..956214e --- /dev/null +++ b/.claude/templates/feature-spec.md @@ -0,0 +1,387 @@ +# Feature: [Nombre de la Feature] + +**Author**: [Tu nombre] +**Date**: [Fecha] +**Status**: 🟡 Draft | 🔵 In Progress | 🟢 Completed +**Priority**: 🔴 Critical | 🟡 High | 🟢 Medium | ⚪ Low + +--- + +## 📋 Resumen Ejecutivo + +**Descripción breve**: [1-2 líneas describiendo la feature] + +**Motivación**: [Por qué se necesita esta feature] + +**Valor de negocio**: [Qué problema resuelve o qué valor aporta] + +--- + +## 🎯 Objetivos + +- [ ] Objetivo 1 +- [ ] Objetivo 2 +- [ ] Objetivo 3 + +--- + +## 🚫 Non-Goals (Fuera de Scope) + +- Lo que NO se incluye en esta feature +- Consideraciones para versiones futuras + +--- + +## 🏗️ Diseño Técnico + +### Backend (FastAPI + PostgreSQL) + +#### Modelos de Datos + +**Nuevas tablas**: +```python +# Backend/app/models/[model_name].py + +class [ModelName](BaseModel): + __tablename__ = '[table_name]' + + # Campos + field1 = Column(String, nullable=False) + field2 = Column(Integer, default=0) + + # Relaciones + related_model = relationship("[RelatedModel]", back_populates="...") +``` + +**Modificaciones a tablas existentes**: +- [Tabla]: Agregar campo `[field_name]` + +#### Endpoints API + +| Método | Endpoint | Descripción | Request Body | Response | +|--------|----------|-------------|--------------|----------| +| GET | `/api/v1/[resource]` | Lista todos | - | `List[[Resource]]` | +| POST | `/api/v1/[resource]` | Crea nuevo | `[CreateSchema]` | `[Resource]` | +| GET | `/api/v1/[resource]/{id}` | Detalle | - | `[Resource]` | +| PUT | `/api/v1/[resource]/{id}` | Actualiza | `[UpdateSchema]` | `[Resource]` | +| DELETE | `/api/v1/[resource]/{id}` | Elimina | - | 204 No Content | + +#### Servicios + +**Archivo**: `Backend/app/services/[service_name]_service.py` + +```python +class [ServiceName]Service: + def __init__(self, db: Session): + self.db = db + + def method_name(self, param1, param2): + """Lógica de negocio""" + pass +``` + +#### Schemas Pydantic + +**Archivo**: `Backend/app/schemas/[schema_name].py` + +```python +class [Resource]Base(BaseModel): + field1: str + field2: int + +class [Resource]Create([Resource]Base): + pass + +class [Resource]Response([Resource]Base): + id: int + created_at: datetime + + class Config: + from_attributes = True +``` + +#### Migraciones + +```bash +# Comando para crear migración +cd Backend +alembic revision --autogenerate -m "Add [table_name] table" +alembic upgrade head +``` + +**Cambios en DB**: +- Nueva tabla: `[table_name]` +- Índices: `idx_[table]_[field]` +- Foreign keys: `fk_[table1]_[table2]` +- Constraints: CHECK, UNIQUE + +--- + +### Frontend (Next.js + React) + +#### Componentes + +**Nuevos componentes**: + +1. **[ComponentName]** (`Frontend/src/components/[ComponentName]/`) + - Props: `{ prop1: string, prop2: number }` + - Estado: `useState` para [descripción] + - Lógica: [Descripción de la lógica] + +```tsx +// Frontend/src/components/[ComponentName]/[ComponentName].tsx + +interface [ComponentName]Props { + prop1: string; + prop2: number; +} + +export const [ComponentName] = ({ prop1, prop2 }: [ComponentName]Props) => { + // Implementation +}; +``` + +#### Páginas/Rutas + +**Nuevas rutas**: +- `/[route]` - [Descripción] +- `/[route]/[id]` - [Descripción] + +**Archivo**: `Frontend/src/app/[route]/page.tsx` + +#### API Integration + +**Archivo**: `Frontend/src/services/[service]Api.ts` + +```typescript +export async function fetch[Resource]() { + const response = await fetch(`${API_URL}/[endpoint]`); + return response.json(); +} +``` + +#### State Management + +- **Local state**: `useState` para [qué datos] +- **Server state**: Server Components para [qué datos] +- **Context**: (si aplica) Para compartir [qué estado] + +#### Tipos TypeScript + +**Archivo**: `Frontend/src/types/[type].ts` + +```typescript +export interface [ResourceType] { + id: number; + field1: string; + field2: number; +} +``` + +--- + +### Mobile + +#### Android (Kotlin + Jetpack Compose) + +**Archivos a crear/modificar**: +- `data/entities/[Name]DTO.kt` - DTO del API +- `domain/models/[Name].kt` - Modelo de dominio +- `data/mappers/[Name]Mapper.kt` - Transformación DTO → Domain +- `presentation/[feature]/screen/[Name]Screen.kt` - UI +- `presentation/[feature]/viewmodel/[Name]ViewModel.kt` - Estado + +#### iOS (Swift + SwiftUI) + +**Archivos a crear/modificar**: +- `Data/Entities/[Name]DTO.swift` - DTO del API +- `Domain/Models/[Name].swift` - Modelo de dominio +- `Data/Mapper/[Name]Mapper.swift` - Transformación +- `Presentation/Views/[Name]View.swift` - UI +- `Presentation/ViewModels/[Name]ViewModel.swift` - Estado + +--- + +## 🗄️ Cambios en Base de Datos + +### Schema Changes + +```sql +-- Nueva tabla +CREATE TABLE [table_name] ( + id SERIAL PRIMARY KEY, + field1 VARCHAR(255) NOT NULL, + field2 INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL +); + +-- Índices +CREATE INDEX idx_[table]_[field] ON [table]([field]); + +-- Foreign keys +ALTER TABLE [table1] +ADD CONSTRAINT fk_[table1]_[table2] +FOREIGN KEY ([table2_id]) REFERENCES [table2](id); +``` + +### Data Migration + +¿Se requiere migración de datos existentes? +- [ ] Sí - [Descripción de la migración] +- [ ] No + +--- + +## 🧪 Plan de Testing + +### Backend Tests + +**Archivo**: `Backend/app/tests/test_[feature].py` + +```python +# Tests a crear: +def test_create_[resource]_success(): + """Should create [resource] with valid data""" + pass + +def test_create_[resource]_invalid_data(): + """Should return 400 with invalid data""" + pass + +def test_get_[resource]_not_found(): + """Should return 404 for non-existent [resource]""" + pass +``` + +### Frontend Tests + +**Archivo**: `Frontend/src/components/[Component]/__tests__/[Component].test.tsx` + +```tsx +// Tests a crear: +test('renders [Component] correctly', () => { + // Test implementation +}); + +test('handles user interaction', () => { + // Test implementation +}); + +test('displays error state', () => { + // Test implementation +}); +``` + +### Integration Tests + +- [ ] E2E: Usuario puede [acción completa] +- [ ] API: Endpoints funcionan end-to-end + +--- + +## 📅 Plan de Implementación + +### Fase 1: Backend Foundation (Estimado: X horas) +1. [ ] Crear modelos SQLAlchemy +2. [ ] Crear schemas Pydantic +3. [ ] Crear migración Alembic +4. [ ] Ejecutar y verificar migración +5. [ ] Crear service layer +6. [ ] Implementar endpoints +7. [ ] Tests unitarios backend + +### Fase 2: Frontend Implementation (Estimado: X horas) +8. [ ] Crear tipos TypeScript +9. [ ] Crear componentes base +10. [ ] Implementar páginas/rutas +11. [ ] Integrar con API +12. [ ] Implementar estados de error/loading +13. [ ] Tests de componentes + +### Fase 3: Mobile Implementation (Estimado: X horas) +14. [ ] Android: DTOs, Mappers, Repository +15. [ ] Android: ViewModel y UI +16. [ ] iOS: DTOs, Mappers, Repository +17. [ ] iOS: ViewModel y UI + +### Fase 4: Testing & Polish (Estimado: X horas) +18. [ ] Integration testing +19. [ ] E2E testing +20. [ ] Bug fixes +21. [ ] Performance optimization +22. [ ] Documentation + +**Tiempo total estimado**: XX horas + +--- + +## ✅ Checklist de Calidad + +### Code Quality +- [ ] Backend: Tests unitarios (>80% coverage) +- [ ] Backend: Tests de integración +- [ ] Frontend: Tests de componentes +- [ ] Frontend: TypeScript sin errores +- [ ] Código sigue style guide del proyecto +- [ ] Sin warnings de linter + +### Funcionalidad +- [ ] Happy path funciona correctamente +- [ ] Error handling implementado +- [ ] Edge cases considerados +- [ ] Validaciones en lugar correcto + +### Performance +- [ ] No N+1 queries +- [ ] Índices de DB apropiados +- [ ] Eager loading donde se necesita +- [ ] Frontend: Componentes optimizados + +### Seguridad +- [ ] Input validation (backend + frontend) +- [ ] SQL injection prevention (ORM) +- [ ] XSS prevention +- [ ] Authentication/Authorization (si aplica) +- [ ] Secrets no hardcodeados + +### Documentación +- [ ] README actualizado +- [ ] API docs (Swagger) actualizados +- [ ] Comentarios en código complejo +- [ ] CLAUDE.md actualizado si cambia arquitectura + +--- + +## 🚀 Deployment Checklist + +- [ ] Migraciones ejecutadas en staging +- [ ] Tests passing en CI/CD +- [ ] Feature flag habilitado (si aplica) +- [ ] Rollback plan documentado +- [ ] Monitoring configurado +- [ ] Logs apropiados + +--- + +## 📚 Referencias + +- [Link a diseño de UI/UX] +- [Link a especificación técnica relacionada] +- [Link a ticket/issue] +- [Documentación externa relevante] + +--- + +## 🤔 Preguntas Abiertas + +- [ ] ¿Pregunta 1? +- [ ] ¿Pregunta 2? + +--- + +## 🔄 Changelog + +**[Fecha]**: Draft inicial +**[Fecha]**: [Cambio realizado] diff --git a/.claudeignore b/.claudeignore new file mode 100644 index 0000000..80a5e57 --- /dev/null +++ b/.claudeignore @@ -0,0 +1,90 @@ +# .claudeignore +# Files and directories that Claude should ignore for better performance + +# Dependencies +node_modules/ +__pycache__/ +*.pyc +.venv/ +venv/ +env/ +.Python + +# Build outputs +Frontend/.next/ +Frontend/out/ +Frontend/build/ +Frontend/dist/ +Backend/app/__pycache__/ +*.egg-info/ +dist/ +build/ + +# Database +postgres_data/ +*.db +*.sqlite +*.sqlite3 + +# Logs +*.log +logs/ +*.log.* + +# IDE and editors +.vscode/ +.idea/ +*.swp +*.swo +*~ +.project +.classpath +.settings/ + +# OS files +.DS_Store +Thumbs.db +*.bak + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.env.production + +# Mobile build artifacts +Mobile/PlatziFlixAndroid/.gradle/ +Mobile/PlatziFlixAndroid/build/ +Mobile/PlatziFlixAndroid/app/build/ +Mobile/PlatziFlixAndroid/local.properties +Mobile/PlatziFlixiOS/DerivedData/ +Mobile/PlatziFlixiOS/build/ +Mobile/PlatziFlixiOS/Pods/ +Mobile/PlatziFlixiOS/*.xcworkspace/xcuserdata/ + +# Testing +coverage/ +.coverage +htmlcov/ +.pytest_cache/ +.vitest/ + +# Temporary files +tmp/ +temp/ +*.tmp + +# Package files +*.tar.gz +*.zip +*.rar + +# Compiled files +*.pyc +*.pyo +*.class +*.o +*.so +*.dylib diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..e8e5ab7 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,172 @@ +name: Docker Build & Test + +on: + push: + branches: [main, develop] + paths: + - 'Backend/**' + - '.github/workflows/docker-build.yml' + pull_request: + branches: [main, develop] + paths: + - 'Backend/**' + +jobs: + docker-build-test: + name: Build & Test Docker Images + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Build Backend Docker image + uses: docker/build-push-action@v5 + with: + context: ./Backend + push: false + load: true + tags: platziflix-backend:${{ github.sha }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + + - name: Start services with Docker Compose + run: | + cd Backend + # Override image tag to use the one we just built + export BACKEND_IMAGE=platziflix-backend:${{ github.sha }} + docker-compose up -d + + - name: Wait for services to be healthy + run: | + echo "Waiting for PostgreSQL..." + timeout 60 bash -c 'until docker-compose -f Backend/docker-compose.yml exec -T db pg_isready -U platziflix_user; do sleep 2; done' + + echo "Waiting for API..." + timeout 60 bash -c 'until curl -f http://localhost:8000/health; do sleep 2; done' + + - name: Run migrations + run: | + cd Backend + docker-compose exec -T api alembic upgrade head + + - name: Test API endpoints + run: | + echo "Testing health endpoint..." + curl -f http://localhost:8000/health | jq + + echo "Testing courses endpoint..." + curl -f http://localhost:8000/courses | jq + + echo "Testing docs endpoint..." + curl -f http://localhost:8000/docs > /dev/null + + - name: Run tests inside container + run: | + cd Backend + docker-compose exec -T api pytest app/tests/ -v + + - name: Check container logs + if: failure() + run: | + cd Backend + echo "=== API Logs ===" + docker-compose logs api + echo "=== DB Logs ===" + docker-compose logs db + + - name: Cleanup + if: always() + run: | + cd Backend + docker-compose down -v + + # Move cache to avoid growing indefinitely + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache || true + + docker-security-scan: + name: Security Scan + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build image for scanning + uses: docker/build-push-action@v5 + with: + context: ./Backend + push: false + load: true + tags: platziflix-backend:scan + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: 'platziflix-backend:scan' + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH' + + - name: Upload Trivy results to GitHub Security + uses: github/codeql-action/upload-sarif@v2 + if: always() + with: + sarif_file: 'trivy-results.sarif' + + docker-size-check: + name: Check Image Size + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Backend image + uses: docker/build-push-action@v5 + with: + context: ./Backend + push: false + load: true + tags: platziflix-backend:size-check + + - name: Check image size + run: | + SIZE=$(docker image inspect platziflix-backend:size-check --format='{{.Size}}') + SIZE_MB=$(echo "scale=2; $SIZE / 1048576" | bc) + + echo "Docker image size: ${SIZE_MB} MB" + + # Warn if image is larger than 1GB + if [ $(echo "$SIZE_MB > 1024" | bc) -eq 1 ]; then + echo "::warning::Image size (${SIZE_MB}MB) is larger than 1GB. Consider optimization." + fi + + # Show image layers + echo "=== Image Layers ===" + docker history platziflix-backend:size-check --human=true --no-trunc + + - name: Cleanup + if: always() + run: docker rmi platziflix-backend:size-check || true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..1d23eab --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,179 @@ +name: Run Tests + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + backend-tests: + name: Backend Tests (Python + PostgreSQL) + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: platziflix_user + POSTGRES_PASSWORD: platziflix_password + POSTGRES_DB: platziflix_test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install UV (Python package manager) + run: | + pip install uv + + - name: Install Backend dependencies + run: | + cd Backend + uv pip install --system -e . + uv pip install --system pytest pytest-cov pytest-asyncio httpx + + - name: Run Alembic migrations + env: + DATABASE_URL: postgresql://platziflix_user:platziflix_password@localhost:5432/platziflix_test + run: | + cd Backend + alembic upgrade head + + - name: Run Backend tests with coverage + env: + DATABASE_URL: postgresql://platziflix_user:platziflix_password@localhost:5432/platziflix_test + run: | + cd Backend + pytest app/tests/ -v --cov=app --cov-report=xml --cov-report=term-missing + + - name: Upload Backend coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./Backend/coverage.xml + flags: backend + name: backend-coverage + + frontend-tests: + name: Frontend Tests (Next.js + TypeScript) + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'yarn' + cache-dependency-path: Frontend/yarn.lock + + - name: Install Frontend dependencies + run: | + cd Frontend + yarn install --frozen-lockfile + + - name: Run ESLint + run: | + cd Frontend + yarn lint + + - name: Run TypeScript type check + run: | + cd Frontend + yarn tsc --noEmit + + - name: Run Frontend tests with coverage + run: | + cd Frontend + yarn test --coverage --passWithNoTests + + - name: Upload Frontend coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./Frontend/coverage/coverage-final.json + flags: frontend + name: frontend-coverage + + integration-health-check: + name: Integration - API Health Check + runs-on: ubuntu-latest + needs: [backend-tests] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Start Backend with Docker Compose + run: | + cd Backend + docker-compose up -d + + - name: Wait for services to be ready + run: | + echo "Waiting for Backend API..." + timeout 60 bash -c 'until curl -f http://localhost:8000/health; do sleep 2; done' + + - name: Test API health endpoint + run: | + response=$(curl -s http://localhost:8000/health) + echo "Health check response: $response" + echo "$response" | jq -e '.status == "ok"' || exit 1 + + - name: Test courses endpoint + run: | + response=$(curl -s http://localhost:8000/courses) + echo "Courses response: $response" + echo "$response" | jq -e 'type == "array"' || exit 1 + + - name: Show Docker logs on failure + if: failure() + run: | + cd Backend + docker-compose logs + + - name: Cleanup + if: always() + run: | + cd Backend + docker-compose down -v + + test-summary: + name: Test Summary + runs-on: ubuntu-latest + needs: [backend-tests, frontend-tests, integration-health-check] + if: always() + + steps: + - name: Check test results + run: | + echo "===================================" + echo "Test Results Summary" + echo "===================================" + echo "Backend Tests: ${{ needs.backend-tests.result }}" + echo "Frontend Tests: ${{ needs.frontend-tests.result }}" + echo "Integration Tests: ${{ needs.integration-health-check.result }}" + echo "===================================" + + - name: Fail if any test failed + if: | + needs.backend-tests.result == 'failure' || + needs.frontend-tests.result == 'failure' || + needs.integration-health-check.result == 'failure' + run: | + echo "❌ Some tests failed. Check the logs above." + exit 1 diff --git a/Backend/.env.example b/Backend/.env.example new file mode 100644 index 0000000..72d4fb1 --- /dev/null +++ b/Backend/.env.example @@ -0,0 +1,93 @@ +# ============================================ +# MINDIA BACKEND - ENVIRONMENT VARIABLES +# ============================================ +# Copy this file to .env and update with your values +# DO NOT commit .env to version control + +# ============================================ +# DATABASE CONFIGURATION +# ============================================ +DATABASE_URL=postgresql://platziflix_user:platziflix_password@db:5432/platziflix_db + +# For local development (without Docker): +# DATABASE_URL=postgresql://platziflix_user:platziflix_password@localhost:5432/platziflix_db + +POSTGRES_USER=platziflix_user +POSTGRES_PASSWORD=platziflix_password +POSTGRES_DB=platziflix_db + +# ============================================ +# APPLICATION SETTINGS +# ============================================ +PROJECT_NAME="Mindia API" +VERSION="1.0.0" +DEBUG=True +ENVIRONMENT=development + +# ============================================ +# CORS CONFIGURATION +# ============================================ +# Comma-separated list of allowed origins +CORS_ORIGINS=http://localhost:3000,http://localhost:8000,http://127.0.0.1:3000 + +# For production, use specific domains: +# CORS_ORIGINS=https://platziflix.com,https://www.platziflix.com + +CORS_ALLOW_CREDENTIALS=True +CORS_ALLOW_METHODS=GET,POST,PUT,DELETE,OPTIONS +CORS_ALLOW_HEADERS=* + +# ============================================ +# SECURITY & AUTHENTICATION +# ============================================ +# Generate a secure random key with: openssl rand -hex 32 +SECRET_KEY=your-secret-key-here-change-in-production-use-openssl-rand-hex-32 +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=30 + +# ============================================ +# RATE LIMITING +# ============================================ +RATE_LIMIT_PER_MINUTE=60 +RATE_LIMIT_ENABLED=True + +# ============================================ +# LOGGING +# ============================================ +LOG_LEVEL=INFO +# Options: DEBUG, INFO, WARNING, ERROR, CRITICAL + +# ============================================ +# API CONFIGURATION +# ============================================ +API_V1_PREFIX=/api/v1 +DOCS_URL=/docs +OPENAPI_URL=/openapi.json + +# ============================================ +# REDIS (Optional - for caching) +# ============================================ +# REDIS_URL=redis://localhost:6379/0 +# REDIS_ENABLED=False + +# ============================================ +# EMAIL (Optional - for notifications) +# ============================================ +# SMTP_HOST=smtp.gmail.com +# SMTP_PORT=587 +# SMTP_USER=your-email@gmail.com +# SMTP_PASSWORD=your-app-password +# EMAIL_FROM=noreply@platziflix.com + +# ============================================ +# EXTERNAL SERVICES (Optional) +# ============================================ +# AWS_ACCESS_KEY_ID=your-aws-key +# AWS_SECRET_ACCESS_KEY=your-aws-secret +# AWS_S3_BUCKET=platziflix-media + +# ============================================ +# MONITORING (Optional) +# ============================================ +# SENTRY_DSN=https://your-sentry-dsn +# SENTRY_ENABLED=False diff --git a/Backend/Dockerfile b/Backend/Dockerfile index 90ecb7a..3ef4a98 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -7,12 +7,12 @@ WORKDIR /app # Install uv for dependency management COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv -# Copy dependency files -COPY pyproject.toml uv.lock ./ +# Copy dependency files (without lock file) +COPY pyproject.toml ./ # Install dependencies including dev dependencies ENV UV_PROJECT_ENVIRONMENT="/usr/local/" -RUN uv sync --frozen --extra dev +RUN uv sync --extra dev # Copy application code COPY ./app ./app @@ -22,4 +22,4 @@ COPY ./specs ./specs EXPOSE 8000 # Command to run uvicorn with hot reload -CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file +CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/Backend/Makefile b/Backend/Makefile index 3dbbd64..1485ae1 100644 --- a/Backend/Makefile +++ b/Backend/Makefile @@ -1,4 +1,4 @@ -.PHONY: start stop restart build logs clean migrate create-migration seed seed-fresh help +.PHONY: start stop restart build logs clean migrate create-migration seed seed-fresh test help # Comando principal para iniciar el entorno de desarrollo start: @@ -42,6 +42,10 @@ seed-fresh: docker-compose exec api bash -c "cd /app && uv run python -m app.db.seed clear" docker-compose exec api bash -c "cd /app && uv run python -m app.db.seed" +# Ejecutar tests +test: + docker-compose exec api bash -c "cd /app && uv pip install --system pytest pytest-cov pytest-asyncio httpx && uv run pytest app/tests/ -v" + # Mostrar ayuda help: @echo "Comandos disponibles:" @@ -55,6 +59,7 @@ help: @echo " make create-migration - Crear una nueva migración" @echo " make seed - Ejecutar seed de datos" @echo " make seed-fresh - Limpiar y recrear datos de seed" + @echo " make test - Ejecutar tests con pytest" @echo " make help - Mostrar esta ayuda" # Comando por defecto diff --git a/Backend/app/core/config.py b/Backend/app/core/config.py index c58afde..6c22f1d 100644 --- a/Backend/app/core/config.py +++ b/Backend/app/core/config.py @@ -6,6 +6,11 @@ class Settings(BaseSettings): version: str = "0.1.0" database_url: str = "postgresql://user:password@localhost:5432/platziflix" + # JWT Configuration + secret_key: str = "your-secret-key-change-this-in-production-must-be-at-least-32-chars" + algorithm: str = "HS256" + access_token_expire_minutes: int = 60 * 24 * 7 # 7 days + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") diff --git a/Backend/app/core/dependencies.py b/Backend/app/core/dependencies.py new file mode 100644 index 0000000..5a8a447 --- /dev/null +++ b/Backend/app/core/dependencies.py @@ -0,0 +1,92 @@ +""" +FastAPI dependencies for authentication and authorization. +""" + +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.orm import Session +from app.db.base import get_db +from app.core.security import decode_access_token +from app.models import User + +# HTTP Bearer token security scheme +security = HTTPBearer() + + +def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + db: Session = Depends(get_db) +) -> User: + """ + Dependency to get current authenticated user from JWT token. + + Args: + credentials: Bearer token from Authorization header + db: Database session + + Returns: + User: Current authenticated user + + Raises: + HTTPException: If token is invalid or user not found + """ + token = credentials.credentials + + # Decode token + payload = decode_access_token(token) + if payload is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Extract user_id from token + user_id: str = payload.get("sub") + if user_id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Get user from database + user = db.query(User).filter(User.id == int(user_id)).first() + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found", + headers={"WWW-Authenticate": "Bearer"}, + ) + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Inactive user" + ) + + return user + + +def get_current_active_user( + current_user: User = Depends(get_current_user) +) -> User: + """ + Dependency to get current active user. + + Args: + current_user: Current user from get_current_user dependency + + Returns: + User: Current active user + + Raises: + HTTPException: If user is not active + """ + if not current_user.is_active: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Inactive user" + ) + + return current_user diff --git a/Backend/app/core/security.py b/Backend/app/core/security.py new file mode 100644 index 0000000..3802f6f --- /dev/null +++ b/Backend/app/core/security.py @@ -0,0 +1,87 @@ +""" +Security utilities for authentication and authorization. +Handles password hashing, JWT token generation and validation. +""" + +from datetime import datetime, timedelta +from typing import Optional +from passlib.context import CryptContext +from jose import JWTError, jwt +from app.core.config import settings + +# Password hashing context +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# JWT Configuration +SECRET_KEY = settings.secret_key +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days + + +def hash_password(password: str) -> str: + """ + Hash a password using bcrypt. + + Args: + password: Plain text password + + Returns: + str: Hashed password + """ + return pwd_context.hash(password) + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """ + Verify a password against its hash. + + Args: + plain_password: Plain text password + hashed_password: Hashed password from database + + Returns: + bool: True if password matches + """ + return pwd_context.verify(plain_password, hashed_password) + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + """ + Create a JWT access token. + + Args: + data: Dictionary with user data (usually {"sub": user_id}) + expires_delta: Optional expiration time + + Returns: + str: Encoded JWT token + """ + to_encode = data.copy() + + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + return encoded_jwt + + +def decode_access_token(token: str) -> Optional[dict]: + """ + Decode and validate a JWT access token. + + Args: + token: JWT token string + + Returns: + dict: Decoded token data if valid + None: If token is invalid or expired + """ + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + return payload + except JWTError: + return None diff --git a/Backend/app/db/seed.py b/Backend/app/db/seed.py index 8dc7c9d..c2ba403 100644 --- a/Backend/app/db/seed.py +++ b/Backend/app/db/seed.py @@ -1,133 +1,232 @@ """ Seed data script for Platziflix database. -This script creates sample data for testing and development. +This script creates REAL sample data for testing and development. """ from datetime import datetime from sqlalchemy.orm import Session from app.db.base import SessionLocal -from app.models import Teacher, Course, Lesson, course_teachers +from app.models import Teacher, Course, Lesson, CourseRating, course_teachers from app.core.config import settings def create_sample_data(): - """Create sample data for testing.""" + """Create comprehensive sample data with real course information.""" db: Session = SessionLocal() try: - # Create sample teachers - teacher1 = Teacher( - name="Juan Pérez", - email="juan.perez@platziflix.com", - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), - ) - - teacher2 = Teacher( - name="María García", - email="maria.garcia@platziflix.com", - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), - ) - - teacher3 = Teacher( - name="Carlos Rodríguez", - email="carlos.rodriguez@platziflix.com", - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), - ) - - db.add_all([teacher1, teacher2, teacher3]) + # ==================== TEACHERS ==================== + teachers = [ + Teacher( + name="Freddy Vega", + email="freddy@platzi.com", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Teacher( + name="Carlos Hernández", + email="carlos@platzi.com", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Teacher( + name="Diana García", + email="diana@platzi.com", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Teacher( + name="Ricardo Celis", + email="ricardo@platzi.com", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Teacher( + name="Oscar Barajas", + email="oscar@platzi.com", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Teacher( + name="Ana Belisa Martínez", + email="ana@platzi.com", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + ] + db.add_all(teachers) db.commit() - # Create sample courses - course1 = Course( - name="Curso de React", - description="Aprende React desde cero hasta convertirte en un desarrollador profesional", - thumbnail="https://via.placeholder.com/300x200?text=React+Course", - slug="curso-de-react", - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), - ) - - course2 = Course( - name="Curso de Python", - description="Domina Python y sus frameworks más populares", - thumbnail="https://via.placeholder.com/300x200?text=Python+Course", - slug="curso-de-python", - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), - ) - - course3 = Course( - name="Curso de JavaScript", - description="JavaScript moderno y sus mejores prácticas", - thumbnail="https://via.placeholder.com/300x200?text=JavaScript+Course", - slug="curso-de-javascript", - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), - ) - - db.add_all([course1, course2, course3]) + # ==================== COURSES WITH REAL THUMBNAILS ==================== + courses = [ + Course( + name="Curso Profesional de JavaScript", + description="Domina JavaScript desde los fundamentos hasta técnicas avanzadas. Aprende ES6+, asincronía, patrones de diseño y buenas prácticas.", + thumbnail="https://images.unsplash.com/photo-1579468118864-1b9ea3c0db4a?w=800&h=450&fit=crop", + slug="curso-profesional-javascript", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Course( + name="Curso de Python desde Cero", + description="Aprende Python, uno de los lenguajes más demandados. Desde sintaxis básica hasta proyectos reales con Django y Flask.", + thumbnail="https://images.unsplash.com/photo-1526379095098-d400fd0bf935?w=800&h=450&fit=crop", + slug="curso-python-desde-cero", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Course( + name="Curso de React.js Profesional", + description="Conviértete en experto en React. Hooks, Context, Redux, Next.js y mejores prácticas para aplicaciones modernas.", + thumbnail="https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&h=450&fit=crop", + slug="curso-react-profesional", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Course( + name="Curso de Backend con Node.js", + description="Desarrolla APIs REST escalables con Node.js, Express, bases de datos y arquitectura de microservicios.", + thumbnail="https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800&h=450&fit=crop", + slug="curso-backend-nodejs", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Course( + name="Curso de Inteligencia Artificial con Python", + description="Aprende Machine Learning, Deep Learning y redes neuronales. Proyectos reales con TensorFlow y PyTorch.", + thumbnail="https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800&h=450&fit=crop", + slug="curso-inteligencia-artificial-python", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Course( + name="Curso de Diseño UX/UI", + description="Diseña productos digitales centrados en el usuario. Figma, prototipado, research y testing con usuarios reales.", + thumbnail="https://images.unsplash.com/photo-1561070791-2526d30994b5?w=800&h=450&fit=crop", + slug="curso-diseno-ux-ui", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Course( + name="Curso de DevOps y Cloud Computing", + description="Domina Docker, Kubernetes, CI/CD, AWS y Azure. Automatiza deployments y escala aplicaciones.", + thumbnail="https://images.unsplash.com/photo-1667372393119-3d4c48d07fc9?w=800&h=450&fit=crop", + slug="curso-devops-cloud", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Course( + name="Curso de Bases de Datos SQL y NoSQL", + description="PostgreSQL, MySQL, MongoDB y Redis. Diseño, optimización y queries avanzadas.", + thumbnail="https://images.unsplash.com/photo-1544383835-bda2bc66a55d?w=800&h=450&fit=crop", + slug="curso-bases-datos-sql-nosql", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Course( + name="Curso de TypeScript Avanzado", + description="Lleva JavaScript al siguiente nivel con TypeScript. Tipos avanzados, generics y arquitectura escalable.", + thumbnail="https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=800&h=450&fit=crop", + slug="curso-typescript-avanzado", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Course( + name="Curso de Git y GitHub Profesional", + description="Control de versiones profesional. Branching, merging, resolución de conflictos y trabajo en equipo.", + thumbnail="https://images.unsplash.com/photo-1618401471353-b98afee0b2eb?w=800&h=450&fit=crop", + slug="curso-git-github-profesional", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Course( + name="Curso de Vue.js 3 Desde Cero", + description="Framework progresivo para interfaces de usuario. Composition API, Pinia, Vue Router y Nuxt 3.", + thumbnail="https://images.unsplash.com/photo-1593720213428-28a5b9e94613?w=800&h=450&fit=crop", + slug="curso-vuejs-3-desde-cero", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Course( + name="Curso de Ciberseguridad", + description="Protege sistemas y datos. Hacking ético, pentesting, seguridad web y mejores prácticas.", + thumbnail="https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=800&h=450&fit=crop", + slug="curso-ciberseguridad", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Course( + name="Curso de Flutter y Dart", + description="Desarrollo móvil multiplataforma. Crea apps nativas para iOS y Android con un solo código.", + thumbnail="https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?w=800&h=450&fit=crop", + slug="curso-flutter-dart", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Course( + name="Curso de Blockchain y Web3", + description="Tecnología blockchain, smart contracts con Solidity y desarrollo de dApps descentralizadas.", + thumbnail="https://images.unsplash.com/photo-1639762681485-074b7f938ba0?w=800&h=450&fit=crop", + slug="curso-blockchain-web3", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + Course( + name="Curso de Data Science con R", + description="Análisis de datos, estadística, visualización y machine learning con R y RStudio.", + thumbnail="https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=450&fit=crop", + slug="curso-data-science-r", + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ), + ] + db.add_all(courses) db.commit() - # Assign teachers to courses (many-to-many) - course1.teachers.append(teacher1) - course1.teachers.append(teacher2) - course2.teachers.append(teacher2) - course2.teachers.append(teacher3) - course3.teachers.append(teacher1) - course3.teachers.append(teacher3) + # ==================== ASSIGN TEACHERS TO COURSES ==================== + courses[0].teachers.extend([teachers[0], teachers[4]]) # JS - Freddy, Oscar + courses[1].teachers.extend([teachers[1], teachers[2]]) # Python - Carlos, Diana + courses[2].teachers.extend([teachers[4], teachers[0]]) # React - Oscar, Freddy + courses[3].teachers.extend([teachers[1], teachers[3]]) # Node - Carlos, Ricardo + courses[4].teachers.extend([teachers[2], teachers[1]]) # AI - Diana, Carlos + courses[5].teachers.extend([teachers[5]]) # UX/UI - Ana + courses[6].teachers.extend([teachers[3], teachers[1]]) # DevOps - Ricardo, Carlos + courses[7].teachers.extend([teachers[1]]) # DBs - Carlos + courses[8].teachers.extend([teachers[4]]) # TS - Oscar + courses[9].teachers.extend([teachers[0]]) # Git - Freddy + courses[10].teachers.extend([teachers[4]]) # Vue - Oscar + courses[11].teachers.extend([teachers[3]]) # Cyber - Ricardo + courses[12].teachers.extend([teachers[2]]) # Flutter - Diana + courses[13].teachers.extend([teachers[3]]) # Blockchain - Ricardo + courses[14].teachers.extend([teachers[2]]) # R - Diana db.commit() - # Create sample lessons + # ==================== CREATE LESSONS FOR EACH COURSE ==================== lessons_data = [ - # React course lessons - { - "course": course1, - "name": "Introducción a React", - "description": "Conceptos básicos de React y JSX", - "slug": "introduccion-a-react", - "video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - }, - { - "course": course1, - "name": "Componentes y Props", - "description": "Creación de componentes reutilizables", - "slug": "componentes-y-props", - "video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - }, - { - "course": course1, - "name": "Estado y Eventos", - "description": "Manejo del estado y eventos en React", - "slug": "estado-y-eventos", - "video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - }, - # Python course lessons - { - "course": course2, - "name": "Introducción a Python", - "description": "Sintaxis básica y tipos de datos", - "slug": "introduccion-a-python", - "video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - }, - { - "course": course2, - "name": "Funciones y Módulos", - "description": "Organización del código con funciones", - "slug": "funciones-y-modulos", - "video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - }, - # JavaScript course lessons - { - "course": course3, - "name": "JavaScript Moderno", - "description": "ES6+ y nuevas características", - "slug": "javascript-moderno", - "video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - }, + # JavaScript Course + {"course": courses[0], "name": "Introducción a JavaScript", "description": "Historia y fundamentos del lenguaje", "slug": "introduccion-javascript", "video_url": "https://www.youtube.com/watch?v=W6NZfCO5SIk"}, + {"course": courses[0], "name": "Variables y Tipos de Datos", "description": "let, const, var y tipos primitivos", "slug": "variables-tipos-datos", "video_url": "https://www.youtube.com/watch?v=W6NZfCO5SIk"}, + {"course": courses[0], "name": "Funciones y Arrow Functions", "description": "Declaración, expresión y funciones flecha", "slug": "funciones-arrow-functions", "video_url": "https://www.youtube.com/watch?v=W6NZfCO5SIk"}, + {"course": courses[0], "name": "Async/Await y Promises", "description": "Programación asíncrona moderna", "slug": "async-await-promises", "video_url": "https://www.youtube.com/watch?v=W6NZfCO5SIk"}, + + # Python Course + {"course": courses[1], "name": "Introducción a Python", "description": "Configuración y primer programa", "slug": "introduccion-python", "video_url": "https://www.youtube.com/watch?v=W6NZfCO5SIk"}, + {"course": courses[1], "name": "Estructuras de Control", "description": "if, for, while y comprehensions", "slug": "estructuras-control-python", "video_url": "https://www.youtube.com/watch?v=W6NZfCO5SIk"}, + {"course": courses[1], "name": "Funciones y Módulos", "description": "Organización de código Python", "slug": "funciones-modulos-python", "video_url": "https://www.youtube.com/watch?v=W6NZfCO5SIk"}, + + # React Course + {"course": courses[2], "name": "Fundamentos de React", "description": "JSX, componentes y props", "slug": "fundamentos-react", "video_url": "https://www.youtube.com/watch?v=W6NZfCO5SIk"}, + {"course": courses[2], "name": "Hooks en React", "description": "useState, useEffect, useContext", "slug": "hooks-react", "video_url": "https://www.youtube.com/watch?v=W6NZfCO5SIk"}, + {"course": courses[2], "name": "React Router", "description": "Navegación en aplicaciones React", "slug": "react-router", "video_url": "https://www.youtube.com/watch?v=W6NZfCO5SIk"}, + + # Node.js Course + {"course": courses[3], "name": "Introducción a Node.js", "description": "Event loop y módulos", "slug": "introduccion-nodejs", "video_url": "https://www.youtube.com/watch?v=W6NZfCO5SIk"}, + {"course": courses[3], "name": "Express.js Básico", "description": "Creación de APIs REST", "slug": "express-basico", "video_url": "https://www.youtube.com/watch?v=W6NZfCO5SIk"}, + + # AI Course + {"course": courses[4], "name": "Introducción a IA", "description": "Conceptos y aplicaciones", "slug": "introduccion-ia", "video_url": "https://www.youtube.com/watch?v=W6NZfCO5SIk"}, + {"course": courses[4], "name": "Machine Learning Básico", "description": "Algoritmos supervisados", "slug": "machine-learning-basico", "video_url": "https://www.youtube.com/watch?v=W6NZfCO5SIk"}, ] for lesson_data in lessons_data: @@ -144,10 +243,82 @@ def create_sample_data(): db.commit() + # ==================== CREATE RATINGS ==================== + # Simulate multiple users rating courses + ratings = [ + # JavaScript course - muy popular + CourseRating(course_id=courses[0].id, user_id=1, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[0].id, user_id=2, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[0].id, user_id=3, rating=4, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[0].id, user_id=4, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[0].id, user_id=5, rating=4, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + + # Python course + CourseRating(course_id=courses[1].id, user_id=1, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[1].id, user_id=2, rating=4, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[1].id, user_id=6, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + + # React course + CourseRating(course_id=courses[2].id, user_id=3, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[2].id, user_id=4, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[2].id, user_id=7, rating=4, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + + # Node.js course + CourseRating(course_id=courses[3].id, user_id=2, rating=4, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[3].id, user_id=5, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + + # AI course + CourseRating(course_id=courses[4].id, user_id=1, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[4].id, user_id=6, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[4].id, user_id=8, rating=4, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + + # UX/UI course + CourseRating(course_id=courses[5].id, user_id=3, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[5].id, user_id=9, rating=4, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + + # DevOps course + CourseRating(course_id=courses[6].id, user_id=4, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[6].id, user_id=10, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + + # Databases course + CourseRating(course_id=courses[7].id, user_id=5, rating=4, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + + # TypeScript course + CourseRating(course_id=courses[8].id, user_id=7, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[8].id, user_id=11, rating=4, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + + # Git course + CourseRating(course_id=courses[9].id, user_id=2, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + CourseRating(course_id=courses[9].id, user_id=8, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + + # Vue course + CourseRating(course_id=courses[10].id, user_id=9, rating=4, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + + # Cybersecurity course + CourseRating(course_id=courses[11].id, user_id=10, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + + # Flutter course + CourseRating(course_id=courses[12].id, user_id=6, rating=4, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + + # Blockchain course + CourseRating(course_id=courses[13].id, user_id=11, rating=5, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + + # Data Science course + CourseRating(course_id=courses[14].id, user_id=1, rating=4, created_at=datetime.utcnow(), updated_at=datetime.utcnow()), + ] + + db.add_all(ratings) + db.commit() + print("✅ Sample data created successfully!") - print(f" - Created {len([teacher1, teacher2, teacher3])} teachers") - print(f" - Created {len([course1, course2, course3])} courses") + print(f" - Created {len(teachers)} teachers") + print(f" - Created {len(courses)} courses") print(f" - Created {len(lessons_data)} lessons") + print(f" - Created {len(ratings)} ratings") + print("\n📊 Course Stats:") + for course in courses[:5]: # Show first 5 + db.refresh(course) + print(f" - {course.name}: ⭐ {course.average_rating} ({course.total_ratings} ratings)") except Exception as e: db.rollback() @@ -163,6 +334,8 @@ def clear_all_data(): try: # Delete in reverse order to avoid foreign key constraints + from app.models import CourseRating + db.query(CourseRating).delete() db.query(Lesson).delete() db.execute(course_teachers.delete()) db.query(Course).delete() diff --git a/Backend/app/main.py b/Backend/app/main.py index 568faf3..5a333e0 100644 --- a/Backend/app/main.py +++ b/Backend/app/main.py @@ -12,18 +12,29 @@ ErrorResponse ) +# Import routers +from app.routers import auth, favorites, progress + app = FastAPI( title=settings.project_name, version=settings.version, description=""" - Platziflix API - Platform for online courses + Mindia API - Donde la mente y la tecnología se encuentran para aprender ## Features - * **Courses**: Browse and search courses + * **Authentication**: User registration and login with JWT + * **Courses**: Browse and search intelligent courses * **Ratings**: Rate courses and view statistics + * **Progress**: Track course completion and progress + * **Favorites**: Save and manage favorite courses * **Teachers**: Course instructors information - * **Lessons**: Course content structure + * **Lessons**: Structured course content + + ## Authentication + + Register a new account or login to receive a JWT token. + Use the token in Authorization header: `Bearer ` ## Rating System @@ -33,6 +44,10 @@ - Aggregated statistics available per course """, openapi_tags=[ + { + "name": "authentication", + "description": "User registration, login and profile management" + }, { "name": "courses", "description": "Operations with courses" @@ -41,6 +56,14 @@ "name": "ratings", "description": "Course rating operations" }, + { + "name": "progress", + "description": "Track user progress in courses" + }, + { + "name": "favorites", + "description": "Manage favorite courses" + }, { "name": "health", "description": "Health check endpoints" @@ -48,6 +71,11 @@ ] ) +# Include routers +app.include_router(auth.router) +app.include_router(favorites.router) +app.include_router(progress.router) + def get_course_service(db: Session = Depends(get_db)) -> CourseService: """ @@ -58,7 +86,7 @@ def get_course_service(db: Session = Depends(get_db)) -> CourseService: @app.get("/") def root() -> dict[str, str]: - return {"message": "Bienvenido a Platziflix API"} + return {"message": "Bienvenido a Mindia API - Donde la mente y la tecnología se encuentran para aprender"} @app.get("/health", tags=["health"]) diff --git a/Backend/app/models/__init__.py b/Backend/app/models/__init__.py index bbe46f8..4d34284 100644 --- a/Backend/app/models/__init__.py +++ b/Backend/app/models/__init__.py @@ -7,6 +7,9 @@ from .lesson import Lesson from .course_teacher import course_teachers from .course_rating import CourseRating +from .user import User +from .user_favorite import UserFavorite +from .user_course_progress import UserCourseProgress # Export all models for easy importing __all__ = [ @@ -16,5 +19,8 @@ 'Course', 'Lesson', 'course_teachers', - 'CourseRating' + 'CourseRating', + 'User', + 'UserFavorite', + 'UserCourseProgress', ] \ No newline at end of file diff --git a/Backend/app/models/user.py b/Backend/app/models/user.py new file mode 100644 index 0000000..ce35172 --- /dev/null +++ b/Backend/app/models/user.py @@ -0,0 +1,63 @@ +from sqlalchemy import Column, String, Boolean +from sqlalchemy.orm import relationship +from .base import BaseModel + + +class User(BaseModel): + """ + User model representing platform users. + + Features: + - Email-based authentication + - Password hashing (handled at service layer) + - Email verification support + - Active/inactive status + - Relationships: favorites, progress, ratings + """ + __tablename__ = 'users' + + email = Column(String(255), unique=True, nullable=False, index=True) + password_hash = Column(String(255), nullable=False) # Hashed password + name = Column(String(255), nullable=False) + is_active = Column(Boolean, default=True, nullable=False) + is_verified = Column(Boolean, default=False, nullable=False) + + # Relationships + favorites = relationship( + "UserFavorite", + back_populates="user", + cascade="all, delete-orphan" + ) + + progress = relationship( + "UserCourseProgress", + back_populates="user", + cascade="all, delete-orphan" + ) + + def __repr__(self): + return f"" + + def to_dict(self, include_sensitive=False): + """ + Convert user to dictionary for API responses. + + Args: + include_sensitive: If True, includes sensitive data like password_hash + + Returns: + dict: User data + """ + data = { + "id": self.id, + "email": self.email, + "name": self.name, + "is_active": self.is_active, + "is_verified": self.is_verified, + "created_at": self.created_at.isoformat() if self.created_at else None, + } + + if include_sensitive: + data["password_hash"] = self.password_hash + + return data diff --git a/Backend/app/models/user_course_progress.py b/Backend/app/models/user_course_progress.py new file mode 100644 index 0000000..8dbbb57 --- /dev/null +++ b/Backend/app/models/user_course_progress.py @@ -0,0 +1,107 @@ +from sqlalchemy import Column, Integer, ForeignKey, Float, CheckConstraint, UniqueConstraint +from sqlalchemy.orm import relationship +from .base import BaseModel + + +class UserCourseProgress(BaseModel): + """ + UserCourseProgress model for tracking user's progress in courses. + + Business Rules: + - Tracks completion percentage (0.0 to 100.0) + - One progress record per user per course + - completed_lessons: Number of lessons completed + - total_lessons: Total lessons in course (denormalized for performance) + - is_completed: Auto-calculated when progress >= 100% + + Use Cases: + - Track user progress in a course + - Mark lessons as completed + - Calculate completion percentage + - Issue certificates when 100% complete + """ + __tablename__ = 'user_course_progress' + + # Foreign keys + user_id = Column( + Integer, + ForeignKey('users.id'), + nullable=False, + index=True + ) + + course_id = Column( + Integer, + ForeignKey('courses.id'), + nullable=False, + index=True + ) + + # Progress tracking + completed_lessons = Column(Integer, default=0, nullable=False) + total_lessons = Column(Integer, nullable=False) + + progress_percentage = Column( + Float, + CheckConstraint('progress_percentage >= 0 AND progress_percentage <= 100'), + default=0.0, + nullable=False + ) + + is_completed = Column(Integer, default=False, nullable=False) # SQLite usa 0/1 para boolean + + # Relationships + user = relationship("User", back_populates="progress") + course = relationship("Course") + + # Unique constraint + __table_args__ = ( + UniqueConstraint( + 'user_id', + 'course_id', + name='uq_user_course_progress_user_course' + ), + ) + + def update_progress(self, completed_lessons: int): + """ + Update progress based on completed lessons. + + Args: + completed_lessons: Number of lessons completed + """ + self.completed_lessons = completed_lessons + + if self.total_lessons > 0: + self.progress_percentage = round( + (completed_lessons / self.total_lessons) * 100, 2 + ) + else: + self.progress_percentage = 0.0 + + # Mark as completed if 100% + self.is_completed = self.progress_percentage >= 100.0 + + def __repr__(self): + return ( + f"" + ) + + def to_dict(self): + """Convert to dict for API responses.""" + return { + "id": self.id, + "user_id": self.user_id, + "course_id": self.course_id, + "completed_lessons": self.completed_lessons, + "total_lessons": self.total_lessons, + "progress_percentage": self.progress_percentage, + "is_completed": bool(self.is_completed), + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } diff --git a/Backend/app/models/user_favorite.py b/Backend/app/models/user_favorite.py new file mode 100644 index 0000000..9d5d50a --- /dev/null +++ b/Backend/app/models/user_favorite.py @@ -0,0 +1,67 @@ +from sqlalchemy import Column, Integer, ForeignKey, UniqueConstraint +from sqlalchemy.orm import relationship +from .base import BaseModel + + +class UserFavorite(BaseModel): + """ + UserFavorite model for storing user's favorite courses. + + Business Rules: + - One user can favorite a course only once (UNIQUE constraint) + - Supports soft deletes (deleted_at) + - When deleted_at IS NULL = course is favorited + - When deleted_at IS NOT NULL = course is unfavorited + + Use Cases: + - Add course to favorites + - Remove course from favorites + - Sync favorites across devices + """ + __tablename__ = 'user_favorites' + + # Foreign keys + user_id = Column( + Integer, + ForeignKey('users.id'), + nullable=False, + index=True + ) + + course_id = Column( + Integer, + ForeignKey('courses.id'), + nullable=False, + index=True + ) + + # Relationships + user = relationship("User", back_populates="favorites") + course = relationship("Course") + + # Unique constraint: one active favorite per user per course + __table_args__ = ( + UniqueConstraint( + 'user_id', + 'course_id', + name='uq_user_favorites_user_course' + ), + ) + + def __repr__(self): + return ( + f"" + ) + + def to_dict(self): + """Convert to dict for API responses.""" + return { + "id": self.id, + "user_id": self.user_id, + "course_id": self.course_id, + "created_at": self.created_at.isoformat() if self.created_at else None, + } diff --git a/Backend/app/routers/__init__.py b/Backend/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/app/routers/auth.py b/Backend/app/routers/auth.py new file mode 100644 index 0000000..0d7b4b6 --- /dev/null +++ b/Backend/app/routers/auth.py @@ -0,0 +1,158 @@ +""" +Authentication endpoints for user registration and login. +""" + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.db.base import get_db +from app.services.auth_service import AuthService +from app.schemas.user import ( + UserRegisterRequest, + UserLoginRequest, + TokenResponse, + UserResponse, + UserUpdateRequest, + PasswordChangeRequest +) +from app.core.dependencies import get_current_user +from app.models import User + +router = APIRouter(prefix="/auth", tags=["authentication"]) + + +def get_auth_service(db: Session = Depends(get_db)) -> AuthService: + """Dependency to get AuthService instance.""" + return AuthService(db) + + +@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED) +def register( + user_data: UserRegisterRequest, + auth_service: AuthService = Depends(get_auth_service) +): + """ + Register a new user account. + + Requirements: + - Email must be unique + - Password must be at least 8 characters + - Password must contain: uppercase, lowercase, digit + + Returns JWT token for immediate login. + """ + try: + user = auth_service.register_user( + email=user_data.email, + password=user_data.password, + name=user_data.name + ) + + # Generate token + access_token = auth_service.create_user_token(user) + + return TokenResponse( + access_token=access_token, + token_type="bearer", + user=UserResponse.model_validate(user) + ) + + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +@router.post("/login", response_model=TokenResponse) +def login( + credentials: UserLoginRequest, + auth_service: AuthService = Depends(get_auth_service) +): + """ + Login with email and password. + + Returns JWT token on successful authentication. + """ + user = auth_service.authenticate_user( + email=credentials.email, + password=credentials.password + ) + + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Generate token + access_token = auth_service.create_user_token(user) + + return TokenResponse( + access_token=access_token, + token_type="bearer", + user=UserResponse.model_validate(user) + ) + + +@router.get("/me", response_model=UserResponse) +def get_current_user_info(current_user: User = Depends(get_current_user)): + """ + Get current authenticated user information. + + Requires: Valid JWT token in Authorization header + """ + return UserResponse.model_validate(current_user) + + +@router.put("/me", response_model=UserResponse) +def update_current_user( + update_data: UserUpdateRequest, + current_user: User = Depends(get_current_user), + auth_service: AuthService = Depends(get_auth_service) +): + """ + Update current user profile. + + Can update: name, email + """ + updated_user = auth_service.update_user( + user_id=current_user.id, + name=update_data.name, + email=update_data.email + ) + + if not updated_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + return UserResponse.model_validate(updated_user) + + +@router.post("/change-password", status_code=status.HTTP_200_OK) +def change_password( + password_data: PasswordChangeRequest, + current_user: User = Depends(get_current_user), + auth_service: AuthService = Depends(get_auth_service) +): + """ + Change user password. + + Requires: Current password for verification + """ + try: + auth_service.change_password( + user_id=current_user.id, + current_password=password_data.current_password, + new_password=password_data.new_password + ) + + return {"message": "Password changed successfully"} + + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) diff --git a/Backend/app/routers/favorites.py b/Backend/app/routers/favorites.py new file mode 100644 index 0000000..16970c2 --- /dev/null +++ b/Backend/app/routers/favorites.py @@ -0,0 +1,95 @@ +""" +Favorites endpoints for managing user's favorite courses. +""" + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from app.db.base import get_db +from app.services.favorite_service import FavoriteService +from app.schemas.favorite import FavoriteToggleResponse, FavoriteResponse +from app.core.dependencies import get_current_user +from app.models import User + +router = APIRouter(prefix="/favorites", tags=["favorites"]) + + +def get_favorite_service(db: Session = Depends(get_db)) -> FavoriteService: + """Dependency to get FavoriteService instance.""" + return FavoriteService(db) + + +@router.post("/toggle/{course_id}", response_model=FavoriteToggleResponse) +def toggle_favorite( + course_id: int, + current_user: User = Depends(get_current_user), + favorite_service: FavoriteService = Depends(get_favorite_service) +): + """ + Toggle a course as favorite/unfavorite. + + If favorited: removes from favorites + If not favorited: adds to favorites + + Requires authentication. + """ + try: + result = favorite_service.toggle_favorite( + user_id=current_user.id, + course_id=course_id + ) + + return FavoriteToggleResponse(**result) + + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + + +@router.get("/", response_model=List[FavoriteResponse]) +def get_my_favorites( + current_user: User = Depends(get_current_user), + favorite_service: FavoriteService = Depends(get_favorite_service) +): + """ + Get all favorite courses for current user. + + Returns list of favorites with course IDs. + """ + favorites = favorite_service.get_user_favorites(current_user.id) + + return [FavoriteResponse.model_validate(fav) for fav in favorites] + + +@router.get("/course-ids", response_model=List[int]) +def get_favorite_course_ids( + current_user: User = Depends(get_current_user), + favorite_service: FavoriteService = Depends(get_favorite_service) +): + """ + Get list of course IDs that user has favorited. + + Useful for syncing with frontend. + """ + course_ids = favorite_service.get_user_favorite_course_ids(current_user.id) + + return course_ids + + +@router.get("/check/{course_id}", response_model=dict) +def check_if_favorited( + course_id: int, + current_user: User = Depends(get_current_user), + favorite_service: FavoriteService = Depends(get_favorite_service) +): + """ + Check if a specific course is favorited by current user. + """ + is_favorited = favorite_service.is_favorited(current_user.id, course_id) + + return { + "course_id": course_id, + "is_favorited": is_favorited + } diff --git a/Backend/app/routers/progress.py b/Backend/app/routers/progress.py new file mode 100644 index 0000000..6d27414 --- /dev/null +++ b/Backend/app/routers/progress.py @@ -0,0 +1,139 @@ +""" +Progress endpoints for tracking user's course progress. +""" + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from app.db.base import get_db +from app.services.progress_service import ProgressService +from app.schemas.progress import ( + ProgressCreateRequest, + ProgressResponse, + ProgressStatsResponse +) +from app.core.dependencies import get_current_user +from app.models import User + +router = APIRouter(prefix="/progress", tags=["progress"]) + + +def get_progress_service(db: Session = Depends(get_db)) -> ProgressService: + """Dependency to get ProgressService instance.""" + return ProgressService(db) + + +@router.post("/", response_model=ProgressResponse, status_code=status.HTTP_201_CREATED) +def update_progress( + progress_data: ProgressCreateRequest, + current_user: User = Depends(get_current_user), + progress_service: ProgressService = Depends(get_progress_service) +): + """ + Update or create progress for a course. + + Creates new progress or updates existing one. + Automatically calculates percentage and completion status. + """ + try: + progress = progress_service.update_progress( + user_id=current_user.id, + course_id=progress_data.course_id, + completed_lessons=progress_data.completed_lessons + ) + + return ProgressResponse.model_validate(progress) + + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +@router.get("/course/{course_id}", response_model=ProgressResponse) +def get_course_progress( + course_id: int, + current_user: User = Depends(get_current_user), + progress_service: ProgressService = Depends(get_progress_service) +): + """ + Get progress for a specific course. + + Returns 404 if user hasn't started the course yet. + """ + progress = progress_service.get_user_progress(current_user.id, course_id) + + if not progress: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"No progress found for course {course_id}" + ) + + return ProgressResponse.model_validate(progress) + + +@router.get("/", response_model=List[ProgressResponse]) +def get_all_progress( + current_user: User = Depends(get_current_user), + progress_service: ProgressService = Depends(get_progress_service) +): + """ + Get all progress for current user. + + Returns list of all courses user has started. + """ + all_progress = progress_service.get_all_user_progress(current_user.id) + + return [ProgressResponse.model_validate(p) for p in all_progress] + + +@router.get("/stats", response_model=ProgressStatsResponse) +def get_progress_stats( + current_user: User = Depends(get_current_user), + progress_service: ProgressService = Depends(get_progress_service) +): + """ + Get overall progress statistics for current user. + + Returns: + - Total courses enrolled + - Courses in progress + - Courses completed + - Total lessons completed + - Average progress percentage + """ + stats = progress_service.get_user_stats(current_user.id) + + return ProgressStatsResponse(**stats) + + +@router.post("/lesson/{lesson_id}", response_model=ProgressResponse) +def mark_lesson_completed( + lesson_id: int, + course_id: int, + current_user: User = Depends(get_current_user), + progress_service: ProgressService = Depends(get_progress_service) +): + """ + Mark a specific lesson as completed. + + Automatically increments completed lessons count and updates percentage. + + Query params: + - course_id: Course that the lesson belongs to + """ + try: + progress = progress_service.mark_lesson_completed( + user_id=current_user.id, + course_id=course_id, + lesson_id=lesson_id + ) + + return ProgressResponse.model_validate(progress) + + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) diff --git a/Backend/app/schemas/favorite.py b/Backend/app/schemas/favorite.py new file mode 100644 index 0000000..f844e58 --- /dev/null +++ b/Backend/app/schemas/favorite.py @@ -0,0 +1,26 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + + +class FavoriteCreateRequest(BaseModel): + """Schema for adding a course to favorites.""" + course_id: int + + +class FavoriteResponse(BaseModel): + """Schema for favorite response.""" + id: int + user_id: int + course_id: int + created_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class FavoriteToggleResponse(BaseModel): + """Schema for favorite toggle response.""" + course_id: int + is_favorited: bool + message: str diff --git a/Backend/app/schemas/progress.py b/Backend/app/schemas/progress.py new file mode 100644 index 0000000..e80a453 --- /dev/null +++ b/Backend/app/schemas/progress.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime + + +class ProgressCreateRequest(BaseModel): + """Schema for creating/updating course progress.""" + course_id: int + completed_lessons: int = Field(..., ge=0) + + +class ProgressResponse(BaseModel): + """Schema for progress response.""" + id: int + user_id: int + course_id: int + completed_lessons: int + total_lessons: int + progress_percentage: float + is_completed: bool + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class ProgressStatsResponse(BaseModel): + """Schema for user's overall progress statistics.""" + total_courses_enrolled: int + courses_in_progress: int + courses_completed: int + total_lessons_completed: int + average_progress: float diff --git a/Backend/app/schemas/user.py b/Backend/app/schemas/user.py new file mode 100644 index 0000000..7089fbd --- /dev/null +++ b/Backend/app/schemas/user.py @@ -0,0 +1,76 @@ +from pydantic import BaseModel, EmailStr, Field, field_validator +from typing import Optional +from datetime import datetime + + +class UserRegisterRequest(BaseModel): + """Schema for user registration request.""" + email: EmailStr + password: str = Field(..., min_length=8, max_length=100) + name: str = Field(..., min_length=2, max_length=255) + + @field_validator('password') + @classmethod + def password_strength(cls, v: str) -> str: + """Validate password strength.""" + if len(v) < 8: + raise ValueError('Password must be at least 8 characters long') + if not any(char.isdigit() for char in v): + raise ValueError('Password must contain at least one digit') + if not any(char.isupper() for char in v): + raise ValueError('Password must contain at least one uppercase letter') + if not any(char.islower() for char in v): + raise ValueError('Password must contain at least one lowercase letter') + return v + + +class UserLoginRequest(BaseModel): + """Schema for user login request.""" + email: EmailStr + password: str + + +class UserResponse(BaseModel): + """Schema for user response (public data).""" + id: int + email: str + name: str + is_active: bool + is_verified: bool + created_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class TokenResponse(BaseModel): + """Schema for JWT token response.""" + access_token: str + token_type: str = "bearer" + user: UserResponse + + +class UserUpdateRequest(BaseModel): + """Schema for updating user profile.""" + name: Optional[str] = Field(None, min_length=2, max_length=255) + email: Optional[EmailStr] = None + + +class PasswordChangeRequest(BaseModel): + """Schema for changing password.""" + current_password: str + new_password: str = Field(..., min_length=8, max_length=100) + + @field_validator('new_password') + @classmethod + def password_strength(cls, v: str) -> str: + """Validate password strength.""" + if len(v) < 8: + raise ValueError('Password must be at least 8 characters long') + if not any(char.isdigit() for char in v): + raise ValueError('Password must contain at least one digit') + if not any(char.isupper() for char in v): + raise ValueError('Password must contain at least one uppercase letter') + if not any(char.islower() for char in v): + raise ValueError('Password must contain at least one lowercase letter') + return v diff --git a/Backend/app/services/auth_service.py b/Backend/app/services/auth_service.py new file mode 100644 index 0000000..d4cf546 --- /dev/null +++ b/Backend/app/services/auth_service.py @@ -0,0 +1,175 @@ +""" +Authentication service for user registration, login, and token management. +""" + +from sqlalchemy.orm import Session +from app.models import User +from app.core.security import hash_password, verify_password, create_access_token +from typing import Optional + + +class AuthService: + """Service for handling authentication operations.""" + + def __init__(self, db: Session): + self.db = db + + def register_user(self, email: str, password: str, name: str) -> User: + """ + Register a new user. + + Args: + email: User email + password: Plain text password + name: User name + + Returns: + User: Created user + + Raises: + ValueError: If email already exists + """ + # Check if user already exists + existing_user = self.db.query(User).filter(User.email == email).first() + if existing_user: + raise ValueError(f"User with email {email} already exists") + + # Hash password + password_hash = hash_password(password) + + # Create user + new_user = User( + email=email, + password_hash=password_hash, + name=name, + is_active=True, + is_verified=False # Email verification can be added later + ) + + self.db.add(new_user) + self.db.commit() + self.db.refresh(new_user) + + return new_user + + def authenticate_user(self, email: str, password: str) -> Optional[User]: + """ + Authenticate a user with email and password. + + Args: + email: User email + password: Plain text password + + Returns: + User: Authenticated user if credentials are valid + None: If credentials are invalid + """ + user = self.db.query(User).filter(User.email == email).first() + + if not user: + return None + + if not verify_password(password, user.password_hash): + return None + + if not user.is_active: + return None + + return user + + def create_user_token(self, user: User) -> str: + """ + Create JWT access token for user. + + Args: + user: User object + + Returns: + str: JWT access token + """ + token_data = { + "sub": str(user.id), + "email": user.email, + "name": user.name + } + + access_token = create_access_token(data=token_data) + return access_token + + def get_user_by_id(self, user_id: int) -> Optional[User]: + """ + Get user by ID. + + Args: + user_id: User ID + + Returns: + User: User object if found + None: If user not found + """ + return self.db.query(User).filter(User.id == user_id).first() + + def get_user_by_email(self, email: str) -> Optional[User]: + """ + Get user by email. + + Args: + email: User email + + Returns: + User: User object if found + None: If user not found + """ + return self.db.query(User).filter(User.email == email).first() + + def update_user(self, user_id: int, **kwargs) -> Optional[User]: + """ + Update user information. + + Args: + user_id: User ID + **kwargs: Fields to update (name, email, etc.) + + Returns: + User: Updated user + None: If user not found + """ + user = self.get_user_by_id(user_id) + if not user: + return None + + for key, value in kwargs.items(): + if hasattr(user, key) and value is not None: + setattr(user, key, value) + + self.db.commit() + self.db.refresh(user) + + return user + + def change_password(self, user_id: int, current_password: str, new_password: str) -> bool: + """ + Change user password. + + Args: + user_id: User ID + current_password: Current password + new_password: New password + + Returns: + bool: True if password changed successfully + + Raises: + ValueError: If current password is incorrect + """ + user = self.get_user_by_id(user_id) + if not user: + raise ValueError("User not found") + + if not verify_password(current_password, user.password_hash): + raise ValueError("Current password is incorrect") + + user.password_hash = hash_password(new_password) + self.db.commit() + + return True diff --git a/Backend/app/services/favorite_service.py b/Backend/app/services/favorite_service.py new file mode 100644 index 0000000..56c1639 --- /dev/null +++ b/Backend/app/services/favorite_service.py @@ -0,0 +1,146 @@ +""" +Service for managing user favorites. +""" + +from sqlalchemy.orm import Session +from app.models import UserFavorite, Course +from typing import List, Optional +from datetime import datetime + + +class FavoriteService: + """Service for handling user favorite operations.""" + + def __init__(self, db: Session): + self.db = db + + def toggle_favorite(self, user_id: int, course_id: int) -> dict: + """ + Toggle a course as favorite/unfavorite. + + If course is favorited: unfavorite it + If course is not favorited: favorite it + + Args: + user_id: User ID + course_id: Course ID + + Returns: + dict: Status with is_favorited boolean and message + + Raises: + ValueError: If course doesn't exist + """ + # Check if course exists + course = self.db.query(Course).filter(Course.id == course_id).first() + if not course: + raise ValueError(f"Course {course_id} not found") + + # Check if already favorited + existing_favorite = ( + self.db.query(UserFavorite) + .filter( + UserFavorite.user_id == user_id, + UserFavorite.course_id == course_id, + UserFavorite.deleted_at == None + ) + .first() + ) + + if existing_favorite: + # Unfavorite (soft delete) + existing_favorite.deleted_at = datetime.utcnow() + self.db.commit() + + return { + "course_id": course_id, + "is_favorited": False, + "message": "Course removed from favorites" + } + else: + # Favorite (create or restore) + deleted_favorite = ( + self.db.query(UserFavorite) + .filter( + UserFavorite.user_id == user_id, + UserFavorite.course_id == course_id, + UserFavorite.deleted_at != None + ) + .first() + ) + + if deleted_favorite: + # Restore deleted favorite + deleted_favorite.deleted_at = None + self.db.commit() + else: + # Create new favorite + new_favorite = UserFavorite( + user_id=user_id, + course_id=course_id + ) + self.db.add(new_favorite) + self.db.commit() + + return { + "course_id": course_id, + "is_favorited": True, + "message": "Course added to favorites" + } + + def get_user_favorites(self, user_id: int) -> List[UserFavorite]: + """ + Get all active favorites for a user. + + Args: + user_id: User ID + + Returns: + List[UserFavorite]: List of active favorites + """ + favorites = ( + self.db.query(UserFavorite) + .filter( + UserFavorite.user_id == user_id, + UserFavorite.deleted_at == None + ) + .all() + ) + + return favorites + + def get_user_favorite_course_ids(self, user_id: int) -> List[int]: + """ + Get list of course IDs that user has favorited. + + Args: + user_id: User ID + + Returns: + List[int]: List of course IDs + """ + favorites = self.get_user_favorites(user_id) + return [fav.course_id for fav in favorites] + + def is_favorited(self, user_id: int, course_id: int) -> bool: + """ + Check if a course is favorited by user. + + Args: + user_id: User ID + course_id: Course ID + + Returns: + bool: True if favorited + """ + favorite = ( + self.db.query(UserFavorite) + .filter( + UserFavorite.user_id == user_id, + UserFavorite.course_id == course_id, + UserFavorite.deleted_at == None + ) + .first() + ) + + return favorite is not None diff --git a/Backend/app/services/progress_service.py b/Backend/app/services/progress_service.py new file mode 100644 index 0000000..076905f --- /dev/null +++ b/Backend/app/services/progress_service.py @@ -0,0 +1,182 @@ +""" +Service for managing user course progress. +""" + +from sqlalchemy.orm import Session +from sqlalchemy import func +from app.models import UserCourseProgress, Course, Lesson +from typing import List, Optional, Dict + + +class ProgressService: + """Service for handling user progress operations.""" + + def __init__(self, db: Session): + self.db = db + + def update_progress(self, user_id: int, course_id: int, completed_lessons: int) -> UserCourseProgress: + """ + Update or create progress for a user in a course. + + Args: + user_id: User ID + course_id: Course ID + completed_lessons: Number of lessons completed + + Returns: + UserCourseProgress: Updated progress + + Raises: + ValueError: If course doesn't exist or completed_lessons invalid + """ + # Get course and count total lessons + course = self.db.query(Course).filter(Course.id == course_id).first() + if not course: + raise ValueError(f"Course {course_id} not found") + + total_lessons = self.db.query(func.count(Lesson.id)).filter(Lesson.course_id == course_id).scalar() + if total_lessons == 0: + total_lessons = 1 # Avoid division by zero + + if completed_lessons > total_lessons: + raise ValueError(f"Completed lessons ({completed_lessons}) cannot exceed total lessons ({total_lessons})") + + # Check if progress exists + progress = ( + self.db.query(UserCourseProgress) + .filter( + UserCourseProgress.user_id == user_id, + UserCourseProgress.course_id == course_id + ) + .first() + ) + + if progress: + # Update existing progress + progress.update_progress(completed_lessons) + else: + # Create new progress + progress = UserCourseProgress( + user_id=user_id, + course_id=course_id, + completed_lessons=completed_lessons, + total_lessons=total_lessons + ) + progress.update_progress(completed_lessons) + self.db.add(progress) + + self.db.commit() + self.db.refresh(progress) + + return progress + + def get_user_progress(self, user_id: int, course_id: int) -> Optional[UserCourseProgress]: + """ + Get progress for a specific course. + + Args: + user_id: User ID + course_id: Course ID + + Returns: + UserCourseProgress: Progress if exists + None: If no progress found + """ + return ( + self.db.query(UserCourseProgress) + .filter( + UserCourseProgress.user_id == user_id, + UserCourseProgress.course_id == course_id + ) + .first() + ) + + def get_all_user_progress(self, user_id: int) -> List[UserCourseProgress]: + """ + Get all progress for a user. + + Args: + user_id: User ID + + Returns: + List[UserCourseProgress]: List of progress records + """ + return ( + self.db.query(UserCourseProgress) + .filter(UserCourseProgress.user_id == user_id) + .all() + ) + + def get_user_stats(self, user_id: int) -> Dict: + """ + Get overall progress statistics for a user. + + Args: + user_id: User ID + + Returns: + Dict: Statistics with total courses, completed, in progress, etc. + """ + all_progress = self.get_all_user_progress(user_id) + + if not all_progress: + return { + "total_courses_enrolled": 0, + "courses_in_progress": 0, + "courses_completed": 0, + "total_lessons_completed": 0, + "average_progress": 0.0 + } + + total_courses = len(all_progress) + courses_completed = sum(1 for p in all_progress if p.is_completed) + courses_in_progress = total_courses - courses_completed + total_lessons_completed = sum(p.completed_lessons for p in all_progress) + average_progress = round( + sum(p.progress_percentage for p in all_progress) / total_courses, 2 + ) + + return { + "total_courses_enrolled": total_courses, + "courses_in_progress": courses_in_progress, + "courses_completed": courses_completed, + "total_lessons_completed": total_lessons_completed, + "average_progress": average_progress + } + + def mark_lesson_completed(self, user_id: int, course_id: int, lesson_id: int) -> UserCourseProgress: + """ + Mark a specific lesson as completed and update progress. + + Args: + user_id: User ID + course_id: Course ID + lesson_id: Lesson ID + + Returns: + UserCourseProgress: Updated progress + + Raises: + ValueError: If lesson or course not found + """ + # Verify lesson exists and belongs to course + lesson = ( + self.db.query(Lesson) + .filter(Lesson.id == lesson_id, Lesson.course_id == course_id) + .first() + ) + if not lesson: + raise ValueError(f"Lesson {lesson_id} not found in course {course_id}") + + # Get current progress + progress = self.get_user_progress(user_id, course_id) + + if progress: + # Increment completed lessons (if not already at max) + if progress.completed_lessons < progress.total_lessons: + new_completed = progress.completed_lessons + 1 + return self.update_progress(user_id, course_id, new_completed) + return progress + else: + # Create new progress with 1 lesson completed + return self.update_progress(user_id, course_id, 1) diff --git a/Backend/app/tests/conftest.py b/Backend/app/tests/conftest.py new file mode 100644 index 0000000..2d4dfbb --- /dev/null +++ b/Backend/app/tests/conftest.py @@ -0,0 +1,8 @@ +""" +Pytest configuration for tests. +""" +import os +import sys + +# Add parent directory to path to allow imports +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) diff --git a/Backend/pyproject.toml b/Backend/pyproject.toml index 03cee18..99c654e 100644 --- a/Backend/pyproject.toml +++ b/Backend/pyproject.toml @@ -14,11 +14,15 @@ dependencies = [ "pydantic-settings>=2.0.0", "python-dotenv>=1.0.0", "alembic>=1.13.0", + "passlib[bcrypt]>=1.7.0", + "python-jose[cryptography]>=3.3.0", + "email-validator>=2.0.0", ] [project.optional-dependencies] dev = [ "pytest>=7.0.0", + "pytest-cov>=4.0.0", "httpx>=0.24.0", "pytest-asyncio>=0.21.0", ] diff --git a/Backend/pytest.ini b/Backend/pytest.ini new file mode 100644 index 0000000..6e6cccf --- /dev/null +++ b/Backend/pytest.ini @@ -0,0 +1,8 @@ +[pytest] +testpaths = app/tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = -v --strict-markers --tb=short +markers = + skip: Skip this test diff --git a/CLAUDE.md b/CLAUDE.md index e6334c3..1065862 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,8 +1,8 @@ -# Platziflix - Proyecto Multi-plataforma +# Mind IA - Proyecto Multi-plataforma ## Arquitectura del Sistema -Platziflix es una plataforma de cursos online con arquitectura multi-plataforma que incluye: +Mind IA es una plataforma de cursos online con arquitectura multi-plataforma que incluye: - **Backend**: API REST con FastAPI + PostgreSQL - **Frontend**: Aplicación web con Next.js 15 - **Mobile**: Apps nativas Android (Kotlin) + iOS (Swift) @@ -56,11 +56,34 @@ claude-code/ ## API Endpoints +### Cursos - `GET /` - Bienvenida - `GET /health` - Health check + DB connectivity - `GET /courses` - Lista todos los cursos - `GET /courses/{slug}` - Detalle de curso por slug +### Ratings (Sistema de Calificaciones) +- `GET /courses/{id}/ratings` - Obtener ratings de un curso +- `POST /courses/{id}/ratings` - Agregar rating (body: user_id, rating) +- `PUT /courses/{id}/ratings/{user_id}` - Actualizar rating existente +- `DELETE /courses/{id}/ratings/{user_id}` - Eliminar rating (soft delete) +- `GET /courses/{id}/ratings/stats` - Estadísticas (promedio, distribución) +- `GET /courses/{id}/ratings/user/{user_id}` - Rating específico de usuario + +### Autenticación +- `POST /auth/register` - Registrar nuevo usuario +- `POST /auth/login` - Iniciar sesión (retorna JWT) +- `GET /auth/me` - Obtener perfil del usuario autenticado + +### Favoritos +- `GET /favorites` - Lista de cursos favoritos del usuario +- `POST /favorites/{course_id}` - Agregar curso a favoritos +- `DELETE /favorites/{course_id}` - Quitar curso de favoritos + +### Progreso +- `GET /progress` - Progreso del usuario en todos los cursos +- `POST /progress` - Actualizar progreso de una clase + ## Comandos de Desarrollo ### Backend @@ -103,13 +126,39 @@ yarn lint # Linter ## Funcionalidades Implementadas +### Backend +- ✅ API REST con FastAPI +- ✅ Sistema de autenticación JWT +- ✅ Sistema de ratings y reseñas con soft delete +- ✅ Gestión de favoritos por usuario +- ✅ Tracking de progreso de cursos +- ✅ Base de datos PostgreSQL con migraciones Alembic +- ✅ Health checks de API y DB +- ✅ Documentación automática con Swagger +- ✅ Testing completo con pytest + +### Frontend - ✅ Catálogo de cursos con grid estilo Netflix - ✅ Detalle de cursos (profesores, lecciones, clases) +- ✅ Sistema de calificación con estrellas +- ✅ Favoritos por usuario con persistencia - ✅ Navegación por slug SEO-friendly - ✅ Reproductor de video integrado -- ✅ Health checks de API y DB +- ✅ Sistema de notificaciones (Toast) +- ✅ Context API para estado global +- ✅ TypeScript strict mode +- ✅ Testing con Vitest + React Testing Library + +### Mobile - ✅ Apps móviles nativas (Android + iOS) -- ✅ Testing en todos los componentes +- ✅ Consumo de API REST +- ✅ UI moderna con Jetpack Compose y SwiftUI + +### DevOps +- ✅ Docker Compose para desarrollo +- ✅ CI/CD con GitHub Actions +- ✅ Tests automáticos en PR y push +- ✅ Makefile con comandos útiles ## Patrones de Desarrollo @@ -151,5 +200,122 @@ cd Backend && make seed-fresh cd Backend && make logs ``` -Esta memoria contiene toda la información necesaria para continuar el desarrollo del proyecto Platziflix. -- Cualquier comando que necesites ejecutar para el Backend debe ser dentro del contenedor de docker API, antes de ejecutarlo certifica que esté funcionando el contenedor y revisa el archivo makefile con los comandos que existen y úsalos \ No newline at end of file +## Testing + +### Backend Tests +```bash +cd Backend +make test # Ejecutar todos los tests +make test ARGS="-v" # Verbose mode +make test ARGS="--cov" # Con coverage report +``` + +**Ubicación**: `Backend/app/tests/` + +**Tipos de tests**: +- `test_rating_db_constraints.py` - Tests de constraints de DB +- `test_course_rating_service.py` - Tests unitarios del servicio +- `test_rating_endpoints.py` - Tests de integración de endpoints + +**Configuración**: +- `Backend/pytest.ini` - Configuración de pytest +- `Backend/app/tests/conftest.py` - Fixtures compartidos + +### Frontend Tests +```bash +cd Frontend +yarn test # Ejecutar todos los tests +yarn test --coverage # Con coverage +yarn test --watch # Modo watch +``` + +**Ubicación**: `Frontend/src/**/*.test.{ts,tsx}` + +**Tipos de tests**: +- Component tests (Course, StarRating, VideoPlayer) +- Integration tests (Pages) +- Context tests + +**Configuración**: +- `Frontend/vitest.config.ts` - Configuración de Vitest +- `Frontend/src/test/setup.ts` - Setup global de tests + +## CI/CD + +### GitHub Actions +**Ubicación**: `.github/workflows/tests.yml` + +**Jobs**: +1. **backend-tests**: Tests de Python + PostgreSQL +2. **frontend-tests**: Tests de Next.js + TypeScript (ESLint, type check, tests) +3. **integration-health-check**: Health check de la API +4. **test-summary**: Resumen de resultados + +**Triggers**: +- Push a `main` y `develop` +- Pull requests hacia `main` y `develop` + +**Nota**: Los tests NO se ejecutan en ramas `claude/**` para evitar notificaciones excesivas. + +## Troubleshooting + +### Backend no inicia +```bash +# Verificar estado de containers +cd Backend && docker-compose ps + +# Ver logs +make logs + +# Reiniciar todo +make stop && make start +``` + +### Migraciones fallan +```bash +# Verificar que DB está levantada +docker-compose ps + +# Ver logs de DB +docker-compose logs db + +# Intentar migración manual +docker-compose exec api bash -c "cd /app && uv run alembic upgrade head" +``` + +### Frontend no conecta con Backend +1. Verificar que Backend está corriendo: `curl http://localhost:8000/health` +2. Revisar variables de entorno en Frontend +3. Verificar CORS en Backend (configurado en `main.py`) + +### Tests fallan +```bash +# Backend: Asegurarse que la DB de test está configurada +# Frontend: Limpiar caché +cd Frontend && yarn test --clearCache +``` + +## Notas Importantes + +### Docker +- **TODOS** los comandos del Backend deben ejecutarse dentro del contenedor Docker +- Antes de ejecutar comandos, verifica que el contenedor esté funcionando: `docker-compose ps` +- Revisa el `Makefile` para ver comandos disponibles + +### Base de Datos +- La DB usa **soft delete** en las entidades principales +- Todas las migraciones deben ser reversibles +- Los seeds son idempotentes (se pueden ejecutar múltiples veces) + +### Frontend +- **TypeScript strict mode** habilitado +- CSS Modules para evitar conflictos de estilos +- Context API para estado global (evitar prop drilling) +- Next.js Image optimization automático + +### API +- Documentación interactiva: http://localhost:8000/docs +- Todas las respuestas son JSON +- Autenticación JWT en headers: `Authorization: Bearer ` + +Esta memoria contiene toda la información necesaria para continuar el desarrollo del proyecto Mind IA. \ No newline at end of file diff --git a/Frontend/.env.example b/Frontend/.env.example new file mode 100644 index 0000000..59b8dc1 --- /dev/null +++ b/Frontend/.env.example @@ -0,0 +1,87 @@ +# ============================================ +# MINDIA FRONTEND - ENVIRONMENT VARIABLES +# ============================================ +# Copy this file to .env.local and update with your values +# DO NOT commit .env.local to version control + +# ============================================ +# API CONFIGURATION +# ============================================ +# Backend API URL (must start with NEXT_PUBLIC_ to be available in browser) +NEXT_PUBLIC_API_URL=http://localhost:8000 + +# For production: +# NEXT_PUBLIC_API_URL=https://api.mindia.com + +# ============================================ +# ENVIRONMENT +# ============================================ +NEXT_PUBLIC_ENV=development +# Options: development, staging, production + +# ============================================ +# ANALYTICS (Optional) +# ============================================ +# Google Analytics +# NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX + +# Facebook Pixel +# NEXT_PUBLIC_FB_PIXEL_ID=your-pixel-id + +# ============================================ +# AUTHENTICATION (Future implementation) +# ============================================ +# NEXT_PUBLIC_AUTH_DOMAIN=auth.mindia.com +# NEXT_PUBLIC_AUTH_CLIENT_ID=your-client-id + +# ============================================ +# FEATURE FLAGS +# ============================================ +NEXT_PUBLIC_ENABLE_RATINGS=true +NEXT_PUBLIC_ENABLE_COMMENTS=false +NEXT_PUBLIC_ENABLE_SOCIAL_SHARE=false + +# ============================================ +# CDN & ASSETS +# ============================================ +# For production, use CDN URL: +# NEXT_PUBLIC_CDN_URL=https://cdn.mindia.com +# NEXT_PUBLIC_IMAGES_DOMAIN=images.mindia.com + +# ============================================ +# SEO & METADATA +# ============================================ +NEXT_PUBLIC_SITE_NAME=Mindia +NEXT_PUBLIC_SITE_DESCRIPTION="Donde la mente y la tecnología se encuentran para aprender" +NEXT_PUBLIC_SITE_URL=http://localhost:3000 + +# ============================================ +# API TIMEOUTS & RETRIES +# ============================================ +NEXT_PUBLIC_API_TIMEOUT=30000 +# Timeout in milliseconds (30 seconds) + +NEXT_PUBLIC_API_RETRY_ATTEMPTS=3 +# Number of retry attempts for failed requests + +# ============================================ +# DEBUG & LOGGING +# ============================================ +NEXT_PUBLIC_DEBUG_MODE=true +# Enable verbose logging in development + +# ============================================ +# THIRD-PARTY SERVICES (Optional) +# ============================================ +# Sentry for error tracking +# NEXT_PUBLIC_SENTRY_DSN=https://your-sentry-dsn +# SENTRY_AUTH_TOKEN=your-auth-token + +# Stripe for payments (future) +# NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx + +# ============================================ +# BUILD-TIME VARIABLES (Server-side only) +# ============================================ +# These are NOT available in the browser (no NEXT_PUBLIC_ prefix) +# INTERNAL_API_KEY=your-internal-api-key diff --git a/Frontend/next.config.ts b/Frontend/next.config.ts index 39fba07..f5bbd8c 100644 --- a/Frontend/next.config.ts +++ b/Frontend/next.config.ts @@ -6,7 +6,28 @@ const nextConfig: NextConfig = { includePaths: [path.join(__dirname, "src/styles")], prependData: `@import "vars.scss";` }, - /* config options here */ + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'i.pravatar.cc', + }, + { + protocol: 'https', + hostname: 'res.cloudinary.com', + }, + { + protocol: 'https', + hostname: 'via.placeholder.com', + }, + { + protocol: 'https', + hostname: 'images.unsplash.com', + }, + ], + deviceSizes: [640, 750, 828, 1080, 1200], + imageSizes: [16, 32, 48, 64, 96], + }, }; export default nextConfig; diff --git a/Frontend/public/manifest.json b/Frontend/public/manifest.json new file mode 100644 index 0000000..b2b4747 --- /dev/null +++ b/Frontend/public/manifest.json @@ -0,0 +1,44 @@ +{ + "name": "MIND IA - Aprende con Inteligencia Artificial", + "short_name": "MIND IA", + "description": "Plataforma de cursos online con inteligencia artificial para potenciar tu aprendizaje", + "start_url": "/", + "display": "standalone", + "background_color": "#0F172A", + "theme_color": "#06B6D4", + "orientation": "portrait-primary", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "categories": ["education", "productivity"], + "screenshots": [], + "shortcuts": [ + { + "name": "Ver Cursos", + "short_name": "Cursos", + "description": "Explorar catálogo de cursos", + "url": "/#cursos", + "icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }] + }, + { + "name": "Favoritos", + "short_name": "Favoritos", + "description": "Mis cursos favoritos", + "url": "/favorites", + "icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }] + } + ], + "related_applications": [], + "prefer_related_applications": false +} diff --git a/Frontend/src/app/course/[slug]/page.tsx b/Frontend/src/app/course/[slug]/page.tsx index fc3bc90..dafa556 100644 --- a/Frontend/src/app/course/[slug]/page.tsx +++ b/Frontend/src/app/course/[slug]/page.tsx @@ -27,7 +27,50 @@ async function getCourseData(slug: string): Promise { export default async function CoursePage({ params }: CoursePageProps) { const courseData = await getCourseData(params.slug); - return ; + // JSON-LD Schema para SEO + const jsonLd = { + '@context': 'https://schema.org', + '@type': 'Course', + name: courseData.name, + description: courseData.description, + provider: { + '@type': 'Organization', + name: 'MIND IA', + sameAs: 'https://mindia.com', + }, + hasCourseInstance: { + '@type': 'CourseInstance', + courseMode: 'online', + courseWorkload: `PT${courseData.classes?.length || 0}H`, + }, + ...(courseData.average_rating && { + aggregateRating: { + '@type': 'AggregateRating', + ratingValue: courseData.average_rating, + ratingCount: courseData.total_ratings || 0, + bestRating: 5, + worstRating: 1, + }, + }), + offers: { + '@type': 'Offer', + category: 'Education', + availability: 'https://schema.org/InStock', + }, + educationalLevel: 'All levels', + inLanguage: 'es', + thumbnailUrl: courseData.thumbnail, + }; + + return ( + <> +