From a683957af6f6219ca54eec05a3b9337c0b89a2ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valeria=20L=C3=B3pez?= Date: Wed, 15 Oct 2025 23:45:32 -0600 Subject: [PATCH] Primer commit - Sistema de asistencia MAC --- .dockerignore | 74 + .env.example | 58 + .gitignore | 115 + GUIA_DESPLIEGUE.md | 512 +++ LICENSE | 21 + README.md | 429 +++ backend/.editorconfig | 31 + backend/.env.example | 12 + backend/.flake8 | 61 + backend/.pre-commit-config.yaml | 88 + backend/LINTING.md | 482 +++ backend/Makefile | 107 + backend/attendance/__init__.py | 0 backend/attendance/admin.py | 169 + backend/attendance/apps.py | 6 + backend/attendance/management/__init__.py | 1 + .../management/commands/__init__.py | 1 + .../management/commands/recalculate_stats.py | 35 + backend/attendance/migrations/0001_initial.py | 51 + .../0002_alter_attendance_registered_by.py | 20 + .../0003_alter_attendance_external_user.py | 20 + backend/attendance/migrations/__init__.py | 0 backend/attendance/models.py | 291 ++ backend/attendance/urls.py | 11 + backend/attendance/views.py | 387 ++ backend/authentication/__init__.py | 0 backend/authentication/admin.py | 607 ++++ backend/authentication/apps.py | 6 + backend/authentication/audit.py | 222 ++ .../authentication/migrations/0001_initial.py | 39 + .../migrations/0002_auditlog.py | 43 + ...r_assistant_alter_userprofile_user_type.py | 22 + .../0004_rename_assistant_asistente.py | 17 + ...0005_remove_userprofile_career_and_more.py | 21 + .../migrations/0006_externaluser.py | 32 + .../0007_migrate_externaluser_data.py | 54 + ..._minimum_attendance_percentage_and_more.py | 32 + .../0009_assistantprofile_student.py | 39 + ...tudent_alter_asistente_options_and_more.py | 54 + .../migrations/0010_merge_20251003_2150.py | 14 + .../0011_auto_create_assistant_permissions.py | 35 + ...figuration_minutes_after_start_and_more.py | 24 + backend/authentication/migrations/__init__.py | 0 backend/authentication/models.py | 217 ++ backend/authentication/serializers.py | 64 + backend/authentication/urls.py | 12 + backend/authentication/views.py | 136 + backend/events/__init__.py | 0 backend/events/admin.py | 67 + backend/events/apps.py | 6 + backend/events/migrations/0001_initial.py | 66 + .../migrations/0002_alter_event_created_by.py | 20 + ...0003_remove_externaluser_email_and_more.py | 33 + .../migrations/0004_auto_20251001_1400.py | 31 + .../migrations/0005_delete_externaluser.py | 17 + .../migrations/0006_alter_event_created_by.py | 20 + .../0007_remove_event_created_by.py | 17 + backend/events/migrations/__init__.py | 0 backend/events/models.py | 136 + backend/events/serializers.py | 20 + backend/events/urls.py | 9 + backend/events/views.py | 167 + backend/mac_attendance/__init__.py | 0 backend/mac_attendance/asgi.py | 16 + backend/mac_attendance/exceptions.py | 26 + backend/mac_attendance/middleware.py | 126 + backend/mac_attendance/settings.py | 269 ++ backend/mac_attendance/urls.py | 275 ++ backend/mac_attendance/wsgi.py | 16 + backend/manage.py | 22 + backend/pyproject.toml | 145 + backend/requirements-dev.txt | 40 + backend/requirements.txt | 40 + backend/scripts/check_production.py | 191 + backend/scripts/create_test_data.py | 140 + backend/scripts/create_valid_test_data.py | 184 + backend/scripts/format.sh | 38 + backend/scripts/lint.sh | 84 + backend/scripts/test_ratelimit.py | 127 + backend/static/.gitkeep | 1 + .../templates/admin/asistente_reporte.html | 122 + backend/tox.ini | 192 + create_superuser.sh | 20 + docker-compose.dev.yml | 85 + docker-compose.yml | 80 + docker/Dockerfile.backend | 38 + docker/Dockerfile.backend.dev | 41 + docker/Dockerfile.frontend | 27 + docker/nginx.conf | 69 + docs/ACCESO_ADMIN.md | 234 ++ docs/API_DOCUMENTATION.md | 701 ++++ docs/CHANGELOG_FIX.md | 207 ++ docs/COMO_USAR_ESCANER_USB.md | 143 + docs/DESARROLLO.md | 524 +++ docs/DOCKER_SETUP.md | 271 ++ docs/ESTRUCTURA_PROYECTO.md | 156 + docs/POSTGRESQL_MIGRATION.md | 289 ++ docs/postman_collection.json | 347 ++ frontend/.gitignore | 24 + frontend/README.md | 12 + frontend/eslint.config.js | 29 + frontend/index.html | 13 + frontend/package-lock.json | 3103 +++++++++++++++++ frontend/package.json | 28 + frontend/public/images/escudo.png | Bin 0 -> 234005 bytes frontend/public/images/logo.png | Bin 0 -> 129635 bytes frontend/src/App.css | 50 + frontend/src/App.jsx | 45 + frontend/src/components/admin/AdminPanel.jsx | 336 ++ .../components/admin/ExternalUsersPanel.jsx | 167 + .../components/attendance/AttendancePanel.jsx | 521 +++ frontend/src/components/auth/LandingPage.jsx | 103 + frontend/src/components/auth/Login.jsx | 124 + frontend/src/components/common/Dashboard.jsx | 81 + .../external/ExternalRegistration.jsx | 145 + .../src/components/student/StudentPanel.jsx | 215 ++ frontend/src/contexts/AuthContext.jsx | 63 + frontend/src/index.css | 217 ++ frontend/src/main.jsx | 10 + frontend/src/services/api.js | 97 + frontend/src/styles/Dashboard.css | 335 ++ frontend/src/styles/LandingPage.css | 375 ++ frontend/src/styles/Login.css | 459 +++ frontend/vite.config.js | 7 + install.bat | 87 + install.sh | 85 + 126 files changed, 17298 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 GUIA_DESPLIEGUE.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 backend/.editorconfig create mode 100644 backend/.env.example create mode 100644 backend/.flake8 create mode 100644 backend/.pre-commit-config.yaml create mode 100644 backend/LINTING.md create mode 100644 backend/Makefile create mode 100644 backend/attendance/__init__.py create mode 100644 backend/attendance/admin.py create mode 100644 backend/attendance/apps.py create mode 100644 backend/attendance/management/__init__.py create mode 100644 backend/attendance/management/commands/__init__.py create mode 100644 backend/attendance/management/commands/recalculate_stats.py create mode 100644 backend/attendance/migrations/0001_initial.py create mode 100644 backend/attendance/migrations/0002_alter_attendance_registered_by.py create mode 100644 backend/attendance/migrations/0003_alter_attendance_external_user.py create mode 100644 backend/attendance/migrations/__init__.py create mode 100644 backend/attendance/models.py create mode 100644 backend/attendance/urls.py create mode 100644 backend/attendance/views.py create mode 100644 backend/authentication/__init__.py create mode 100644 backend/authentication/admin.py create mode 100644 backend/authentication/apps.py create mode 100644 backend/authentication/audit.py create mode 100644 backend/authentication/migrations/0001_initial.py create mode 100644 backend/authentication/migrations/0002_auditlog.py create mode 100644 backend/authentication/migrations/0003_rename_teacher_assistant_alter_userprofile_user_type.py create mode 100644 backend/authentication/migrations/0004_rename_assistant_asistente.py create mode 100644 backend/authentication/migrations/0005_remove_userprofile_career_and_more.py create mode 100644 backend/authentication/migrations/0006_externaluser.py create mode 100644 backend/authentication/migrations/0007_migrate_externaluser_data.py create mode 100644 backend/authentication/migrations/0008_remove_asistente_minimum_attendance_percentage_and_more.py create mode 100644 backend/authentication/migrations/0009_assistantprofile_student.py create mode 100644 backend/authentication/migrations/0009_assistantprofile_student_alter_asistente_options_and_more.py create mode 100644 backend/authentication/migrations/0010_merge_20251003_2150.py create mode 100644 backend/authentication/migrations/0011_auto_create_assistant_permissions.py create mode 100644 backend/authentication/migrations/0012_systemconfiguration_minutes_after_start_and_more.py create mode 100644 backend/authentication/migrations/__init__.py create mode 100644 backend/authentication/models.py create mode 100644 backend/authentication/serializers.py create mode 100644 backend/authentication/urls.py create mode 100644 backend/authentication/views.py create mode 100644 backend/events/__init__.py create mode 100644 backend/events/admin.py create mode 100644 backend/events/apps.py create mode 100644 backend/events/migrations/0001_initial.py create mode 100644 backend/events/migrations/0002_alter_event_created_by.py create mode 100644 backend/events/migrations/0003_remove_externaluser_email_and_more.py create mode 100644 backend/events/migrations/0004_auto_20251001_1400.py create mode 100644 backend/events/migrations/0005_delete_externaluser.py create mode 100644 backend/events/migrations/0006_alter_event_created_by.py create mode 100644 backend/events/migrations/0007_remove_event_created_by.py create mode 100644 backend/events/migrations/__init__.py create mode 100644 backend/events/models.py create mode 100644 backend/events/serializers.py create mode 100644 backend/events/urls.py create mode 100644 backend/events/views.py create mode 100644 backend/mac_attendance/__init__.py create mode 100644 backend/mac_attendance/asgi.py create mode 100644 backend/mac_attendance/exceptions.py create mode 100644 backend/mac_attendance/middleware.py create mode 100644 backend/mac_attendance/settings.py create mode 100644 backend/mac_attendance/urls.py create mode 100644 backend/mac_attendance/wsgi.py create mode 100644 backend/manage.py create mode 100644 backend/pyproject.toml create mode 100644 backend/requirements-dev.txt create mode 100644 backend/requirements.txt create mode 100644 backend/scripts/check_production.py create mode 100644 backend/scripts/create_test_data.py create mode 100644 backend/scripts/create_valid_test_data.py create mode 100644 backend/scripts/format.sh create mode 100644 backend/scripts/lint.sh create mode 100644 backend/scripts/test_ratelimit.py create mode 100644 backend/static/.gitkeep create mode 100644 backend/templates/admin/asistente_reporte.html create mode 100644 backend/tox.ini create mode 100644 create_superuser.sh create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 docker/Dockerfile.backend create mode 100644 docker/Dockerfile.backend.dev create mode 100644 docker/Dockerfile.frontend create mode 100644 docker/nginx.conf create mode 100644 docs/ACCESO_ADMIN.md create mode 100644 docs/API_DOCUMENTATION.md create mode 100644 docs/CHANGELOG_FIX.md create mode 100644 docs/COMO_USAR_ESCANER_USB.md create mode 100644 docs/DESARROLLO.md create mode 100644 docs/DOCKER_SETUP.md create mode 100644 docs/ESTRUCTURA_PROYECTO.md create mode 100644 docs/POSTGRESQL_MIGRATION.md create mode 100644 docs/postman_collection.json create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/images/escudo.png create mode 100644 frontend/public/images/logo.png create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/components/admin/AdminPanel.jsx create mode 100644 frontend/src/components/admin/ExternalUsersPanel.jsx create mode 100644 frontend/src/components/attendance/AttendancePanel.jsx create mode 100644 frontend/src/components/auth/LandingPage.jsx create mode 100644 frontend/src/components/auth/Login.jsx create mode 100644 frontend/src/components/common/Dashboard.jsx create mode 100644 frontend/src/components/external/ExternalRegistration.jsx create mode 100644 frontend/src/components/student/StudentPanel.jsx create mode 100644 frontend/src/contexts/AuthContext.jsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.jsx create mode 100644 frontend/src/services/api.js create mode 100644 frontend/src/styles/Dashboard.css create mode 100644 frontend/src/styles/LandingPage.css create mode 100644 frontend/src/styles/Login.css create mode 100644 frontend/vite.config.js create mode 100644 install.bat create mode 100644 install.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..524d6ef --- /dev/null +++ b/.dockerignore @@ -0,0 +1,74 @@ +# Git +.git +.gitignore +.gitattributes + +# Documentation +*.md +README.md +LICENSE +COMO_USAR_ESCANER_USB.md + +# Node modules +**/node_modules +frontend/node_modules +frontend/dist +frontend/.vite + +# Python +**/__pycache__ +**/*.pyc +**/*.pyo +**/*.pyd +.Python +*.so +*.egg +*.egg-info +dist +build +.pytest_cache +.coverage +htmlcov + +# Virtual environments +venv/ +env/ +ENV/ +.venv + +# IDEs +.vscode +.idea +*.swp +*.swo +*~ +.DS_Store + +# Environment variables +.env +.env.local +.env.*.local + +# Database +*.sqlite3 +*.db +db.sqlite3 + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Static files (se generan en build) +backend/staticfiles/ +backend/static/ +backend/media/ + +# OS +Thumbs.db +.DS_Store + +# Docker +docker-compose.override.yml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9ae7dfd --- /dev/null +++ b/.env.example @@ -0,0 +1,58 @@ +# ============================================== +# CONFIGURACIÓN DE ENTORNO - Sistema de Asistencia MAC +# ============================================== +# IMPORTANTE: Copie este archivo como .env y configure los valores apropiados +# ⚠️ NUNCA suba el archivo .env a Git (ya está en .gitignore) + +# ============================================== +# Django Settings +# ============================================== +# DEBUG: False en producción, True solo para desarrollo +DEBUG=False + +# SECRET_KEY: Genere una clave secreta única y aleatoria +# Puede generarla con: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" +SECRET_KEY=your-secret-key-here-CHANGE-THIS-IN-PRODUCTION + +# ALLOWED_HOSTS: Dominios permitidos (separados por coma) +# Ejemplo: ALLOWED_HOSTS=midominio.com,www.midominio.com,192.168.1.100 +ALLOWED_HOSTS=localhost,127.0.0.1 + +# ============================================== +# Security Settings (Producción) +# ============================================== +RATELIMIT_ENABLE=True +SECURE_SSL_REDIRECT=True +SESSION_COOKIE_SECURE=True +CSRF_COOKIE_SECURE=True + +# CORS Settings +# Agregue los orígenes permitidos separados por coma +CORS_ALLOWED_ORIGINS=https://midominio.com +CSRF_TRUSTED_ORIGINS=https://midominio.com + +# ============================================== +# Database - PostgreSQL +# ============================================== +# ⚠️ IMPORTANTE: Cambie estas credenciales por valores seguros en producción +DB_NAME=mac_attendance +DB_USER=mac_user +DB_PASSWORD=CHANGE-THIS-SECURE-PASSWORD-IN-PRODUCTION +DB_HOST=db +DB_PORT=5432 + +# Email Settings (Opcional) +# EMAIL_HOST=smtp.gmail.com +# EMAIL_PORT=587 +# EMAIL_USE_TLS=True +# EMAIL_HOST_USER=your-email@gmail.com +# EMAIL_HOST_PASSWORD=your-app-password + +# Production Settings (Cambiar en producción) +# DEBUG=False +# SECRET_KEY=your-secure-random-secret-key-here +# ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com +# RATELIMIT_ENABLE=True +# SECURE_SSL_REDIRECT=True +# SESSION_COOKIE_SECURE=True +# CSRF_COOKIE_SECURE=True diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1db36f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,115 @@ +# ============================================== +# Python +# ============================================== +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.pyc +*.pyo +*.pyd +.Python +pip-log.txt +pip-delete-this-directory.txt + +# Django +backend/db.sqlite3 +backend/db.sqlite3-journal +backend/logs/ +backend/staticfiles/ +backend/media/ +*.log + +# Testing y Coverage +.pytest_cache/ +.tox/ +.coverage +.coverage.* +htmlcov/ +coverage.xml +*.cover +.hypothesis/ +.mypy_cache/ +.dmypy.json +dmypy.json + +# ============================================== +# Node / Frontend +# ============================================== +frontend/node_modules/ +frontend/dist/ +frontend/.vite/ +frontend/build/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# ============================================== +# IDEs y Editores +# ============================================== +.vscode/ +.idea/ +*.swp +*.swo +*~ +*.sublime-project +*.sublime-workspace +.vs/ + +# ============================================== +# Sistema Operativo +# ============================================== +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +Desktop.ini + +# ============================================== +# Docker +# ============================================== +docker-compose.override.yml + +# ============================================== +# Archivos Sensibles y Credenciales +# ============================================== +.env +backend/.env +.env.local +.env.development.local +.env.test.local +.env.production.local +*.secret +*.pem +*.key +*.crt + +# ============================================== +# Uploads y Media +# ============================================== +uploads/ +media/ + +# ============================================== +# Backups +# ============================================== +*.sql +*.sql.gz +*.backup +backup/ +backups/ + +# ============================================== +# Archivos Temporales +# ============================================== +*.tmp +*.temp +.cache/ +tmp/ +temp/ diff --git a/GUIA_DESPLIEGUE.md b/GUIA_DESPLIEGUE.md new file mode 100644 index 0000000..d1f8c2d --- /dev/null +++ b/GUIA_DESPLIEGUE.md @@ -0,0 +1,512 @@ +# Guía de Despliegue a Producción +## Sistema de Asistencia MAC - FES Acatlán + +Esta guía describe los pasos necesarios para desplegar el Sistema de Asistencia MAC en un entorno de producción. + +--- + +## 📋 Requisitos del Servidor + +### Hardware Mínimo Recomendado +- **CPU:** 2 cores +- **RAM:** 4 GB +- **Disco:** 20 GB SSD +- **Red:** Conexión estable a internet + +### Software Requerido +- **Sistema Operativo:** Ubuntu 20.04+ / Debian 11+ / CentOS 8+ +- **Docker:** Versión 20.10+ +- **Docker Compose:** Versión 2.0+ +- **Dominio:** (Opcional) Para acceso público con SSL + +### Puertos Necesarios +- **80** (HTTP) - Nginx +- **443** (HTTPS) - Nginx con SSL (opcional pero recomendado) +- **5432** (PostgreSQL) - Solo si necesita acceso externo a la base de datos + +--- + +## 🚀 Instalación Paso a Paso + +### 1. Preparar el Servidor + +#### Actualizar el sistema +```bash +sudo apt update && sudo apt upgrade -y +``` + +#### Instalar Docker +```bash +# Instalar dependencias +sudo apt install -y apt-transport-https ca-certificates curl software-properties-common + +# Agregar repositorio oficial de Docker +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg + +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +# Instalar Docker +sudo apt update +sudo apt install -y docker-ce docker-ce-cli containerd.io + +# Verificar instalación +docker --version +``` + +#### Instalar Docker Compose +```bash +sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + +sudo chmod +x /usr/local/bin/docker-compose + +# Verificar instalación +docker-compose --version +``` + +#### Agregar usuario al grupo Docker (opcional) +```bash +sudo usermod -aG docker $USER +newgrp docker +``` + +--- + +### 2. Clonar el Proyecto + +```bash +# Crear directorio para el proyecto +sudo mkdir -p /opt/mac-attendance +cd /opt/mac-attendance + +# Clonar el repositorio +git clone . + +# O si recibiste el proyecto comprimido +# Descomprimir el archivo en /opt/mac-attendance +``` + +--- + +### 3. Configurar Variables de Entorno + +```bash +# Copiar archivo de ejemplo +cp .env.example .env + +# Editar con nano o vim +nano .env +``` + +#### Configuración CRÍTICA de Seguridad + +**⚠️ IMPORTANTE:** Cambie TODOS estos valores antes de desplegar: + +```env +# ============================================== +# Django Settings +# ============================================== +DEBUG=False + +# Generar SECRET_KEY único (ejecutar este comando en Python): +# python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" +SECRET_KEY= + +# Dominio o IP del servidor (separados por coma) +ALLOWED_HOSTS=midominio.com,www.midominio.com,IP_DEL_SERVIDOR + +# ============================================== +# Security Settings +# ============================================== +RATELIMIT_ENABLE=True +SECURE_SSL_REDIRECT=True # Solo si tiene SSL configurado +SESSION_COOKIE_SECURE=True # Solo si tiene SSL configurado +CSRF_COOKIE_SECURE=True # Solo si tiene SSL configurado + +# CORS y CSRF +CORS_ALLOWED_ORIGINS=https://midominio.com +CSRF_TRUSTED_ORIGINS=https://midominio.com + +# ============================================== +# Database - PostgreSQL +# ============================================== +DB_NAME=mac_attendance +DB_USER=mac_user +DB_PASSWORD= # Cambiar por contraseña fuerte +DB_HOST=db +DB_PORT=5432 +``` + +#### Generar SECRET_KEY Seguro + +```bash +# Ejecutar dentro del contenedor después de construirlo la primera vez +docker-compose run --rm backend python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" + +# Copiar el resultado y pegarlo en SECRET_KEY del archivo .env +``` + +--- + +### 4. Configurar PostgreSQL + +Edite las credenciales de PostgreSQL en `docker-compose.yml` para que coincidan con su `.env`: + +```bash +nano docker-compose.yml +``` + +Busque la sección de `db` y actualice: +```yaml +environment: + - POSTGRES_DB=mac_attendance + - POSTGRES_USER=mac_user + - POSTGRES_PASSWORD= # Debe coincidir con DB_PASSWORD +``` + +--- + +### 5. Construir e Iniciar los Contenedores + +```bash +# Construir las imágenes +docker-compose build + +# Iniciar los servicios en segundo plano +docker-compose up -d + +# Verificar que todos los contenedores están corriendo +docker-compose ps +``` + +**Salida esperada:** +``` +NAME STATUS +pagina-mac-og-backend-1 Up +pagina-mac-og-db-1 Up (healthy) +pagina-mac-og-nginx-1 Up +``` + +--- + +### 6. Aplicar Migraciones de Base de Datos + +```bash +# Aplicar migraciones +docker-compose exec backend python manage.py migrate + +# Verificar que no hay errores +``` + +--- + +### 7. Crear Superusuario (Administrador) + +```bash +# Crear superusuario para acceder al panel de administración +docker-compose exec backend python manage.py createsuperuser +``` + +Siga las instrucciones e ingrese: +- **Username:** admin (o el que prefiera) +- **Email:** admin@mac.unam.mx (o su email) +- **Password:** (contraseña segura) + +--- + +### 8. Recolectar Archivos Estáticos + +```bash +# Recolectar archivos estáticos de Django +docker-compose exec backend python manage.py collectstatic --noinput +``` + +--- + +### 9. Verificar el Despliegue + +```bash +# Ver logs en tiempo real +docker-compose logs -f + +# Verificar logs del backend +docker-compose logs backend + +# Verificar logs de nginx +docker-compose logs nginx + +# Verificar logs de PostgreSQL +docker-compose logs db +``` + +#### Probar la Aplicación + +1. Abrir navegador en: `http://IP_DEL_SERVIDOR` o `http://midominio.com` +2. Debería ver la página de login +3. Acceder al admin en: `http://IP_DEL_SERVIDOR/admin/` +4. Login con el superusuario creado + +--- + +## 🔒 Configuración SSL (HTTPS) - Recomendado + +### Opción 1: Con Let's Encrypt (Certbot) + +```bash +# Instalar Certbot +sudo apt install -y certbot python3-certbot-nginx + +# Obtener certificado SSL +sudo certbot --nginx -d midominio.com -d www.midominio.com + +# El certificado se renovará automáticamente +``` + +### Opción 2: Con Certificado Propio + +1. Copiar certificados SSL a `/opt/mac-attendance/ssl/` +2. Modificar `docker/nginx.conf` para incluir SSL +3. Reiniciar nginx: `docker-compose restart nginx` + +--- + +## 🔧 Configuración de Firewall + +```bash +# Permitir puertos necesarios +sudo ufw allow 22/tcp # SSH +sudo ufw allow 80/tcp # HTTP +sudo ufw allow 443/tcp # HTTPS +sudo ufw enable + +# Verificar reglas +sudo ufw status +``` + +--- + +## 📊 Backup y Recuperación + +### Backup de Base de Datos + +**Crear backup automático (recomendado):** + +```bash +# Crear script de backup +sudo nano /opt/scripts/backup-mac-attendance.sh +``` + +Contenido del script: +```bash +#!/bin/bash +BACKUP_DIR="/opt/backups/mac-attendance" +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/mac_attendance_$DATE.sql" + +mkdir -p $BACKUP_DIR +docker-compose -f /opt/mac-attendance/docker-compose.yml exec -T db pg_dump -U mac_user mac_attendance > $BACKUP_FILE +gzip $BACKUP_FILE + +# Mantener solo los últimos 30 días +find $BACKUP_DIR -type f -mtime +30 -delete + +echo "Backup completado: $BACKUP_FILE.gz" +``` + +```bash +# Dar permisos de ejecución +sudo chmod +x /opt/scripts/backup-mac-attendance.sh + +# Agregar a cron para ejecutar diariamente a las 2 AM +sudo crontab -e +``` + +Agregar línea: +``` +0 2 * * * /opt/scripts/backup-mac-attendance.sh >> /var/log/mac-backup.log 2>&1 +``` + +### Restaurar Backup + +```bash +# Detener la aplicación +cd /opt/mac-attendance +docker-compose down + +# Restaurar backup +gunzip backup_file.sql.gz +cat backup_file.sql | docker-compose exec -T db psql -U mac_user mac_attendance + +# Reiniciar aplicación +docker-compose up -d +``` + +--- + +## 🔄 Actualización del Sistema + +### Actualizar a Nueva Versión + +```bash +cd /opt/mac-attendance + +# Hacer backup antes de actualizar +docker-compose exec db pg_dump -U mac_user mac_attendance > backup_pre_update.sql + +# Detener contenedores +docker-compose down + +# Obtener nueva versión +git pull origin main +# O descomprimir nueva versión + +# Reconstruir contenedores +docker-compose build + +# Iniciar contenedores +docker-compose up -d + +# Aplicar nuevas migraciones +docker-compose exec backend python manage.py migrate + +# Recolectar archivos estáticos +docker-compose exec backend python manage.py collectstatic --noinput + +# Verificar logs +docker-compose logs -f +``` + +--- + +## 📈 Monitoreo + +### Ver Estado de Contenedores + +```bash +docker-compose ps +``` + +### Ver Uso de Recursos + +```bash +docker stats +``` + +### Ver Logs + +```bash +# Todos los logs +docker-compose logs -f + +# Solo backend +docker-compose logs -f backend + +# Últimas 100 líneas +docker-compose logs --tail=100 backend +``` + +### Reiniciar Servicios + +```bash +# Reiniciar todo +docker-compose restart + +# Reiniciar solo backend +docker-compose restart backend + +# Reiniciar solo nginx +docker-compose restart nginx +``` + +--- + +## ⚠️ Solución de Problemas + +### Error: "502 Bad Gateway" + +**Causa:** Backend no está respondiendo. + +**Solución:** +```bash +# Verificar logs del backend +docker-compose logs backend + +# Reiniciar backend +docker-compose restart backend + +# Si persiste, reconstruir +docker-compose down +docker-compose up -d --build +``` + +### Error: "Connection refused" en PostgreSQL + +**Causa:** Base de datos no está lista. + +**Solución:** +```bash +# Verificar estado de PostgreSQL +docker-compose logs db + +# Reiniciar base de datos +docker-compose restart db + +# Esperar a que esté "healthy" +docker-compose ps +``` + +### Error: "CSRF verification failed" + +**Causa:** Configuración incorrecta de dominios en .env + +**Solución:** +```bash +# Verificar que ALLOWED_HOSTS y CSRF_TRUSTED_ORIGINS incluyen su dominio +nano .env + +# Reiniciar backend +docker-compose restart backend +``` + +--- + +## 📋 Checklist de Producción + +Antes de poner en producción, verificar: + +- [ ] **Seguridad:** + - [ ] DEBUG=False en .env + - [ ] SECRET_KEY único y aleatorio + - [ ] DB_PASSWORD seguro y único + - [ ] ALLOWED_HOSTS configurado correctamente + - [ ] SSL/HTTPS habilitado (recomendado) + - [ ] Firewall configurado + +- [ ] **Base de Datos:** + - [ ] PostgreSQL corriendo y saludable + - [ ] Migraciones aplicadas + - [ ] Backup automático configurado + +- [ ] **Aplicación:** + - [ ] Superusuario creado + - [ ] Archivos estáticos recolectados + - [ ] Aplicación accesible en navegador + - [ ] Login funcionando correctamente + +- [ ] **Monitoreo:** + - [ ] Logs configurados + - [ ] Alertas configuradas (opcional) + - [ ] Backup verificado y probado + +--- + +## 📞 Soporte Técnico + +Para problemas durante el despliegue: +1. Revisar logs: `docker-compose logs -f` +2. Consultar documentación: `docs/` +3. Contactar a soporte técnico MAC + +--- + +**Última actualización:** Octubre 2025 +**Versión del documento:** 1.0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b55677c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Valeria López + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f8b804 --- /dev/null +++ b/README.md @@ -0,0 +1,429 @@ +# Sistema de Asistencia MAC - FES Acatlán + +Sistema de gestión de asistencia para ponencias y eventos académicos de Matemáticas Aplicadas y Computación (MAC). + +## 🚀 Características + +- **Gestión de Eventos**: Crear y administrar ponencias, talleres, seminarios +- **Registro de Asistencia**: Manual, por código de barras o usuarios externos +- **Panel de Estudiantes**: Consulta de estadísticas y eventos disponibles +- **Panel de Asistentes**: Administración completa y registro de asistencias +- **Usuarios Externos**: Sistema de aprobación para asistentes externos +- **Estadísticas**: Seguimiento de porcentaje de asistencia por estudiante +- **Seguridad Avanzada**: JWT, rate limiting, auditoría completa +- **Sistema de Auditoría**: Registro automático de eventos de seguridad + +## 📋 Requisitos Previos + +### Opción 1: Con Docker (Recomendado) +- Docker y Docker Compose instalados +- Puertos 80 y 5432 disponibles + +### Opción 2: Instalación Manual +- Python 3.11+ +- Node.js 18+ +- npm o yarn +- PostgreSQL 15+ + +## 🛠️ Instalación + +### Opción 1: Con Docker (Recomendado) + +#### Producción + +```bash +# Clonar el repositorio +git clone +cd pagina-mac-og + +# Copiar variables de entorno +cp .env.example .env + +# IMPORTANTE: Editar .env y cambiar las credenciales para producción +# Cambiar: SECRET_KEY, DB_PASSWORD, etc. + +# Construir e iniciar contenedores +docker-compose up --build -d + +# Ver logs +docker-compose logs -f + +# Acceder a la aplicación +# http://localhost +``` + +El sistema estará disponible en `http://localhost` con: +- **Frontend**: Servido por Nginx en puerto 80 +- **Backend**: API REST en `/api/` +- **PostgreSQL**: Base de datos (puerto 5432 expuesto) + +#### Desarrollo + +```bash +# Usar configuración de desarrollo +docker-compose -f docker-compose.dev.yml up --build -d + +# Ver logs +docker-compose -f docker-compose.dev.yml logs -f backend + +# Acceder al contenedor backend +docker-compose -f docker-compose.dev.yml exec backend bash + +# Ejecutar tests +docker-compose -f docker-compose.dev.yml exec backend tox -e test + +# Ver documentación completa de desarrollo +# docs/DESARROLLO.md +``` + +**Características del entorno de desarrollo:** +- ✅ Hot reload automático (Django runserver) +- ✅ Todas las herramientas de testing y calidad de código +- ✅ PostgreSQL con puerto expuesto para acceso desde host +- ✅ Debugging con ipdb +- ✅ Ver `docs/DESARROLLO.md` para más detalles + +### Opción 2: Instalación Manual + +### Backend (Django) + +1. Crear entorno virtual: +```bash +cd backend +python -m venv venv +``` + +2. Activar entorno virtual: +- Windows: `venv\Scripts\activate` +- Linux/Mac: `source venv/bin/activate` + +3. Instalar dependencias: +```bash +pip install -r requirements.txt +``` + +**⚠️ IMPORTANTE**: Si acabas de clonar el repositorio o actualizaste con nuevas funcionalidades de importación/exportación, ejecuta: +```bash +pip install django-import-export openpyxl tablib +``` + +4. Configurar variables de entorno: +```bash +cp .env.example .env +# Editar .env con tus configuraciones +``` + +5. Ejecutar migraciones: +```bash +python manage.py migrate +``` + +6. Crear superusuario (opcional): +```bash +python manage.py createsuperuser +``` + +7. Ejecutar servidor: +```bash +python manage.py runserver +``` + +El backend estará disponible en `http://127.0.0.1:8000` + +### Frontend (React + Vite) + +1. Instalar dependencias: +```bash +cd frontend +npm install +``` + +2. Ejecutar servidor de desarrollo: +```bash +npm run dev +``` + +El frontend estará disponible en `http://localhost:5173` + +## 📥 Importación y Exportación de Datos + +El sistema incluye funcionalidades de importación/exportación de estudiantes y asistentes mediante archivos Excel (.xlsx) o CSV. + +### Formato de Archivos para Importación + +Los archivos deben tener **exactamente 2 columnas**: + +| account_number | full_name | +|----------------|-----------| +| 1234567 | Juan Pérez García | +| 7654321 | María López Sánchez | + +### Desde el Panel de Admin de Django + +1. Ve a `http://127.0.0.1:8000/admin/` +2. Selecciona **Estudiantes** o **Asistentes (Perfiles)** +3. Haz clic en **"Importar"** en la esquina superior derecha +4. Selecciona tu archivo Excel (.xlsx) o CSV +5. Revisa los cambios propuestos +6. Confirma la importación + +### Exportación + +1. Selecciona los registros que deseas exportar +2. En el menú de acciones, selecciona **"📊 Exportar estudiantes/asistentes seleccionados"** +3. Haz clic en **"Ir"** +4. Se descargará un archivo Excel con los datos + +### Creación Manual + +También puedes crear estudiantes y asistentes manualmente: +1. Ve al panel de admin de Django +2. Selecciona **Estudiantes** o **Asistentes (Perfiles)** +3. Haz clic en **"Agregar estudiante"** o **"Agregar asistente"** +4. Completa los campos: + - Número de cuenta (7 dígitos) + - Nombre completo +5. Guarda - el sistema creará automáticamente el usuario de Django asociado + +## 📁 Estructura del Proyecto + +``` +mac_attendance/ +├── backend/ +│ ├── attendance/ # App de registro de asistencias +│ ├── authentication/ # App de autenticación y auditoría +│ ├── events/ # App de eventos y usuarios externos +│ ├── mac_attendance/ # Configuración principal y middleware +│ ├── scripts/ # Scripts de utilidad +│ │ ├── check_production.py # Verificar config de producción +│ │ └── test_ratelimit.py # Probar rate limiting +│ ├── static/ # Archivos estáticos +│ ├── media/ # Archivos subidos +│ ├── logs/ # Archivos de log (no trackeados) +│ ├── requirements.txt # Dependencias Python +│ └── .env.example # Ejemplo de variables de entorno +├── frontend/ +│ ├── src/ +│ │ ├── components/ # Componentes React +│ │ ├── contexts/ # Contextos (AuthContext) +│ │ └── services/ # Servicios API +│ └── package.json # Dependencias Node +├── docker/ # Archivos Docker +│ ├── Dockerfile.backend # Dockerfile producción +│ ├── Dockerfile.backend.dev # Dockerfile desarrollo +│ ├── Dockerfile.frontend # Dockerfile frontend +│ └── nginx.conf # Configuración Nginx +├── docs/ # Documentación del proyecto +│ ├── SECURITY.md # Guía de seguridad completa +│ ├── RATE_LIMITING.md # Documentación rate limiting +│ ├── AUDIT.md # Sistema de auditoría +│ ├── POSTGRESQL_MIGRATION.md # Migración a PostgreSQL +│ ├── DESARROLLO.md # Guía de desarrollo completa +│ └── ESTRUCTURA_PROYECTO.md # Estructura del proyecto +├── docker-compose.yml # Docker Compose producción +├── docker-compose.dev.yml # Docker Compose desarrollo +├── .env.example # Ejemplo de variables de entorno +└── README.md +``` + +## 🔐 Configuración Inicial + +### Crear Superusuario (Administrador) + +Después de iniciar los contenedores, crea un superusuario para acceder al panel de administración: + +```bash +# Con Docker +docker-compose exec backend python manage.py createsuperuser + +# O en desarrollo +docker-compose -f docker-compose.dev.yml exec backend python manage.py createsuperuser +``` + +Sigue las instrucciones para crear: +- Username +- Email +- Password + +### Acceso al Sistema + +**Panel de Administración Django:** +- URL: `http://localhost/admin/` +- Usuario: El superusuario que acabas de crear + +**Aplicación Web:** +- URL: `http://localhost/` +- Los usuarios (asistentes y estudiantes) deben ser creados desde el panel de administración +- Usuarios externos pueden registrarse desde el formulario público + +## 📊 Modelos Principales + +### UserProfile +- Tipo de usuario (estudiante/asistente) +- Número de cuenta (7 dígitos) +- Información personal + +### Event +- Título, descripción, ponente +- Fecha, hora de inicio/fin +- Modalidad (presencial/online/híbrido) +- Capacidad máxima + +### Attendance +- Estudiante o usuario externo +- Evento asociado +- Método de registro (manual/barcode/external) +- Registrado por (asistente) + +### ExternalUser +- Usuarios externos pendientes de aprobación +- Información de institución y motivo +- ID temporal único + +## 🔧 Configuración Adicional + +### Variables de Entorno (.env) +```env +SECRET_KEY=tu-clave-secreta +DEBUG=True +ALLOWED_HOSTS=localhost,127.0.0.1 +CORS_ALLOWED_ORIGINS=http://localhost:5173 +``` + +### CORS +El backend está configurado para aceptar peticiones desde: +- `http://localhost:5173` (desarrollo) +- `http://127.0.0.1:5173` (desarrollo) + +## 🔒 Seguridad + +Este proyecto implementa múltiples capas de seguridad: + +### Autenticación y Autorización +- ✅ **JWT (JSON Web Tokens)** para autenticación stateless +- ✅ **Control de acceso basado en roles** (estudiante/asistente) +- ✅ **Tokens de corta duración** (1 hora) con refresh tokens (7 días) + +### Protección contra Ataques +- ✅ **Rate Limiting**: Límites en todos los endpoints críticos + - Login: 5 intentos/minuto por IP + - Registro externo: 3/hora por IP + - Ver `docs/RATE_LIMITING.md` para detalles +- ✅ **Headers de seguridad** HTTP (HSTS, X-Frame-Options, etc.) +- ✅ **Sanitización automática** de datos sensibles en logs + +### Sistema de Auditoría +- ✅ **Registro automático** de eventos de seguridad +- ✅ **Trazabilidad completa**: IP, user agent, timestamp +- ✅ **Logs inmutables** consultables desde Django Admin +- ✅ Ver `docs/AUDIT.md` para documentación completa + +### Documentación de Seguridad +- 📄 `docs/SECURITY.md` - Guía de seguridad y checklist de producción +- 📄 `docs/RATE_LIMITING.md` - Configuración de rate limiting +- 📄 `docs/AUDIT.md` - Sistema de auditoría + +## 🧪 Testing y Calidad de Código + +El proyecto incluye un entorno completo de testing y calidad de código usando **Tox**. + +### Ejecutar Tests + +```bash +# Con Docker (desarrollo) +docker-compose -f docker-compose.dev.yml exec backend tox -e test + +# Tests rápidos +docker-compose -f docker-compose.dev.yml exec backend tox -e test-fast + +# Con cobertura +docker-compose -f docker-compose.dev.yml exec backend tox -e coverage +``` + +### Herramientas Disponibles + +- **Testing**: pytest, pytest-django, pytest-cov, factory-boy +- **Linting**: flake8, pylint, black, isort +- **Type Checking**: mypy con stubs para Django/DRF +- **Seguridad**: bandit, safety +- **Métricas**: radon (complejidad ciclomática) + +Ver `docs/DESARROLLO.md` para documentación completa. + +## 🐘 PostgreSQL + +El sistema usa PostgreSQL como base de datos para producción. + +### Acceso desde Host + +⚠️ **Configurar credenciales en archivo .env antes de usar:** + +```bash +Host: localhost +Port: 5432 +Database: [DB_NAME del .env] +User: [DB_USER del .env] +Password: [DB_PASSWORD del .env] +``` + +### Comandos Útiles + +```bash +# Conectarse con psql +psql -h localhost -p 5432 -U mac_user -d mac_attendance + +# Backup +docker-compose exec db pg_dump -U mac_user mac_attendance > backup.sql + +# Restore +cat backup.sql | docker-compose exec -T db psql -U mac_user mac_attendance +``` + +Ver `docs/POSTGRESQL_MIGRATION.md` para más detalles. + +## 📚 Documentación + +- `docs/DESARROLLO.md` - Guía completa de desarrollo +- `docs/SECURITY.md` - Guía de seguridad y checklist de producción +- `docs/RATE_LIMITING.md` - Configuración de rate limiting +- `docs/AUDIT.md` - Sistema de auditoría +- `docs/POSTGRESQL_MIGRATION.md` - Migración a PostgreSQL +- `docs/ESTRUCTURA_PROYECTO.md` - Estructura del proyecto + +## 🚧 Mejoras Futuras + +- [x] Implementar JWT para autenticación +- [x] Sistema de auditoría y logging +- [x] Rate limiting en endpoints +- [x] Dockerización del proyecto +- [x] Migración a PostgreSQL +- [x] Entorno de desarrollo con Tox +- [ ] Agregar exportación de reportes (CSV/PDF) +- [ ] Implementar lector de códigos de barras +- [ ] Notificaciones por email +- [ ] Panel de estadísticas avanzadas +- [ ] Cobertura de tests > 80% + +## 📝 Licencia + +Este proyecto está bajo la Licencia MIT. + +## 👥 Contribución + +1. Fork el proyecto +2. Crea una rama para tu feature (`git checkout -b feature/nueva-funcionalidad`) +3. Commit tus cambios (`git commit -m 'Agregar nueva funcionalidad'`) +4. Push a la rama (`git push origin feature/nueva-funcionalidad`) +5. Abre un Pull Request + +## 📧 Soporte + +Para preguntas o soporte técnico sobre el sistema, contactar a: +- Matemáticas Aplicadas y Computación (MAC) +- FES Acatlán - UNAM + +--- + +**Desarrollado para:** Matemáticas Aplicadas y Computación (MAC) +**Institución:** FES Acatlán - UNAM +**Año:** 2025 \ No newline at end of file diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 100644 index 0000000..ef8395f --- /dev/null +++ b/backend/.editorconfig @@ -0,0 +1,31 @@ +# EditorConfig ayuda a mantener estilos de código consistentes +# entre diferentes editores e IDEs +# https://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{py,pyi}] +indent_style = space +indent_size = 4 +max_line_length = 120 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.{json,js,jsx,ts,tsx}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false +max_line_length = off + +[Makefile] +indent_style = tab diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..23945e6 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,12 @@ +# Django Settings +DEBUG=True +SECRET_KEY=cambia-esto-por-una-clave-secreta-muy-larga-y-aleatoria +ALLOWED_HOSTS=localhost,127.0.0.1,nginx,backend + +# Security Settings +RATELIMIT_ENABLE=False +SECURE_SSL_REDIRECT=False + +# CORS Settings +CORS_ALLOWED_ORIGINS=http://localhost,http://127.0.0.1,http://localhost:5173 +CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1 diff --git a/backend/.flake8 b/backend/.flake8 new file mode 100644 index 0000000..d3443db --- /dev/null +++ b/backend/.flake8 @@ -0,0 +1,61 @@ +[flake8] +# Configuración de Flake8 para verificación de estilo PEP8 + +# Longitud máxima de línea (PEP8 recomienda 79, pero muchos proyectos usan 88-120) +max-line-length = 120 + +# Complejidad ciclomática máxima (recomendado: 10-15) +max-complexity = 12 + +# Archivos y directorios a excluir +exclude = + .git, + __pycache__, + */migrations/*, + */venv/*, + */env/*, + .venv, + staticfiles, + media, + */node_modules/*, + .pytest_cache, + htmlcov, + dist, + build, + *.egg-info + +# Reglas a ignorar +ignore = + # E203: whitespace before ':' (conflicto con black) + E203, + # E501: line too long (ya lo controlamos con max-line-length) + E501, + # W503: line break before binary operator (conflicto con PEP8 actualizado) + W503, + # E402: module level import not at top of file (necesario en algunos casos) + # E402, + +# Reglas específicas por archivo +per-file-ignores = + # __init__.py puede tener imports sin usar + __init__.py:F401, + # settings.py puede tener líneas largas + settings.py:E501, + # tests pueden usar asserts + test_*.py:S101, + **/tests.py:S101 + +# Mostrar el código fuente de cada error +show-source = True + +# Mostrar el PEP relevante para cada error +show-pep8 = True + +# Contar número de errores +count = True + +# Estadísticas al final +statistics = True + +# Formato de salida +format = %(path)s:%(row)d:%(col)d: %(code)s %(text)s diff --git a/backend/.pre-commit-config.yaml b/backend/.pre-commit-config.yaml new file mode 100644 index 0000000..1ea4d9b --- /dev/null +++ b/backend/.pre-commit-config.yaml @@ -0,0 +1,88 @@ +# Configuración de pre-commit hooks +# Para instalar: pre-commit install +# Para ejecutar manualmente: pre-commit run --all-files + +repos: + # Hooks generales + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + args: ['--maxkb=1000'] + - id: check-json + - id: check-toml + - id: check-merge-conflict + - id: check-case-conflict + - id: detect-private-key + - id: mixed-line-ending + args: ['--fix=lf'] + + # Black - Formateador de código + - repo: https://github.com/psf/black + rev: 24.8.0 + hooks: + - id: black + language_version: python3.11 + args: [--line-length=120] + + # isort - Ordenador de imports + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + args: [--profile=black, --line-length=120] + + # Flake8 - Linter PEP8 + - repo: https://github.com/PyCQA/flake8 + rev: 7.1.1 + hooks: + - id: flake8 + args: [--config=.flake8] + additional_dependencies: + - flake8-bugbear + - flake8-comprehensions + - flake8-simplify + + # Bandit - Verificador de seguridad + - repo: https://github.com/PyCQA/bandit + rev: 1.8.0 + hooks: + - id: bandit + args: [-c, pyproject.toml] + additional_dependencies: ["bandit[toml]"] + + # Django specific checks + - repo: https://github.com/adamchainz/django-upgrade + rev: 1.21.0 + hooks: + - id: django-upgrade + args: [--target-version, "5.2"] + + # Pylint (opcional, puede ser lento) + # - repo: https://github.com/PyCQA/pylint + # rev: v3.3.2 + # hooks: + # - id: pylint + # args: [--rcfile=pyproject.toml] + # additional_dependencies: + # - pylint-django + +# Configuración global +default_language_version: + python: python3.11 + +# Etapas en las que ejecutar los hooks +default_stages: [commit, push] + +# Excluir archivos +exclude: | + (?x)^( + migrations/.*| + staticfiles/.*| + media/.*| + .*\.min\.(js|css) + )$ diff --git a/backend/LINTING.md b/backend/LINTING.md new file mode 100644 index 0000000..2d3c181 --- /dev/null +++ b/backend/LINTING.md @@ -0,0 +1,482 @@ +# Guía de Linting y Calidad de Código + +Esta guía explica cómo usar las herramientas de linting y formateo configuradas en el proyecto para mantener un código Python de alta calidad siguiendo PEP8 y mejores prácticas. + +## Tabla de Contenidos + +- [Herramientas Configuradas](#herramientas-configuradas) +- [Instalación](#instalación) +- [Uso Rápido](#uso-rápido) +- [Herramientas Individuales](#herramientas-individuales) +- [Pre-commit Hooks](#pre-commit-hooks) +- [Integración con Docker](#integración-con-docker) +- [CI/CD](#cicd) +- [Configuración de IDEs](#configuración-de-ides) + +## Herramientas Configuradas + +### Formateo Automático +- **Black**: Formateador de código opinionado (PEP8) +- **isort**: Ordenador de imports +- **autopep8**: Corrector automático de PEP8 + +### Linting (Análisis Estático) +- **Flake8**: Verificador de estilo PEP8 +- **pycodestyle**: Verificador oficial de PEP8 +- **Pylint**: Analizador estático completo +- **Bandit**: Verificador de seguridad + +### Type Checking +- **MyPy**: Verificador de tipos estáticos +- **django-stubs**: Type hints para Django +- **djangorestframework-stubs**: Type hints para DRF + +### Testing +- **pytest**: Framework de testing +- **pytest-django**: Plugin para Django +- **pytest-cov**: Cobertura de tests +- **coverage**: Reporte de cobertura + +## Instalación + +### Opción 1: Usando Make (Recomendado) + +```bash +# Instalar todas las dependencias de desarrollo +make install-dev +``` + +### Opción 2: Usando pip directamente + +```bash +# Instalar dependencias de producción +pip install -r requirements.txt + +# Instalar dependencias de desarrollo +pip install -r requirements-dev.txt + +# Instalar pre-commit hooks +pre-commit install +``` + +### Opción 3: En Docker + +```bash +# Construir imagen con herramientas de desarrollo +docker-compose exec backend pip install -r requirements-dev.txt +``` + +## Uso Rápido + +### Formatear Código Automáticamente + +```bash +# Usando Make +make format + +# O manualmente +bash scripts/format.sh +``` + +Este comando ejecutará automáticamente: +1. **isort** - Ordena los imports +2. **black** - Formatea el código +3. **autopep8** - Aplica correcciones adicionales de PEP8 + +### Verificar Calidad de Código + +```bash +# Usando Make +make lint + +# O manualmente +bash scripts/lint.sh +``` + +Este comando ejecutará: +1. Black (verificación sin modificar) +2. isort (verificación) +3. Flake8 (PEP8) +4. Pylint (análisis estático) +5. Bandit (seguridad) +6. MyPy (type checking) + +### Ver Todos los Comandos Disponibles + +```bash +make help +``` + +## Herramientas Individuales + +### Black - Formateador de Código + +```bash +# Verificar formato sin modificar +black --check --diff . + +# Formatear todo el código +black . + +# Formatear archivo específico +black path/to/file.py +``` + +**Configuración**: `pyproject.toml` - Longitud de línea: 120 + +### isort - Ordenador de Imports + +```bash +# Verificar imports sin modificar +isort --check-only --diff . + +# Ordenar imports +isort . + +# Ordenar archivo específico +isort path/to/file.py +``` + +**Configuración**: `pyproject.toml` - Compatible con Black + +### Flake8 - Verificador PEP8 + +```bash +# Verificar todo el proyecto +flake8 . + +# Verificar directorio específico +flake8 authentication/ + +# Verificar archivo específico +flake8 path/to/file.py + +# Ignorar reglas específicas +flake8 --ignore=E501,W503 . +``` + +**Configuración**: `.flake8` + +### Pylint - Análisis Estático + +```bash +# Analizar todo el código +pylint authentication attendance events mac_attendance + +# Analizar archivo específico +pylint path/to/file.py + +# Generar reporte de calificación +pylint --output-format=text authentication/ | tee pylint-report.txt +``` + +**Configuración**: `pyproject.toml` + +### Bandit - Verificador de Seguridad + +```bash +# Verificar seguridad en todo el proyecto +bandit -r . -c pyproject.toml + +# Verificar directorio específico +bandit -r authentication/ + +# Generar reporte detallado +bandit -r . -f json -o bandit-report.json +``` + +**Configuración**: `pyproject.toml` + +### MyPy - Type Checking + +```bash +# Verificar tipos +mypy --config-file=pyproject.toml . + +# Verificar archivo específico +mypy path/to/file.py + +# Generar reporte HTML +mypy --html-report mypy-report . +``` + +**Configuración**: `pyproject.toml` + +## Pre-commit Hooks + +Los pre-commit hooks ejecutan automáticamente las herramientas de linting antes de cada commit. + +### Instalar Hooks + +```bash +pre-commit install +``` + +### Ejecutar Hooks Manualmente + +```bash +# Ejecutar en archivos staged +pre-commit run + +# Ejecutar en todos los archivos +pre-commit run --all-files + +# Usando Make +make pre-commit-all +``` + +### Saltar Hooks (No Recomendado) + +```bash +git commit --no-verify -m "mensaje" +``` + +### Actualizar Hooks + +```bash +pre-commit autoupdate +``` + +## Integración con Docker + +### Ejecutar Linting en Docker + +```bash +# Formatear código +docker-compose exec backend bash scripts/format.sh + +# Verificar código +docker-compose exec backend bash scripts/lint.sh + +# Usando Make +docker-compose exec backend make format +docker-compose exec backend make lint +``` + +### Agregar al Dockerfile + +```dockerfile +# En Dockerfile.backend, agregar antes del CMD +COPY requirements-dev.txt . +RUN pip install -r requirements-dev.txt + +# Ejecutar linting en el build +RUN flake8 . || true +``` + +## CI/CD + +### GitHub Actions + +Crear `.github/workflows/lint.yml`: + +```yaml +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install dependencies + run: | + pip install -r backend/requirements-dev.txt + - name: Run linters + run: | + cd backend + make lint +``` + +### GitLab CI + +Crear `.gitlab-ci.yml`: + +```yaml +lint: + stage: test + image: python:3.11 + script: + - cd backend + - pip install -r requirements-dev.txt + - make lint +``` + +## Configuración de IDEs + +### Visual Studio Code + +Crear `.vscode/settings.json`: + +```json +{ + "python.linting.enabled": true, + "python.linting.flake8Enabled": true, + "python.linting.pylintEnabled": true, + "python.linting.banditEnabled": true, + "python.linting.mypyEnabled": true, + "python.formatting.provider": "black", + "python.formatting.blackArgs": ["--line-length", "120"], + "python.sortImports.args": ["--profile", "black"], + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } + }, + "python.linting.flake8Args": ["--config=backend/.flake8"], + "python.linting.pylintArgs": ["--rcfile=backend/pyproject.toml"] +} +``` + +### PyCharm + +1. **Configurar Black**: + - Settings → Tools → External Tools → Add + - Name: Black + - Program: `black` + - Arguments: `$FilePath$` + +2. **Configurar Flake8**: + - Settings → Tools → External Tools → Add + - Name: Flake8 + - Program: `flake8` + - Arguments: `$FilePath$` + +3. **Configurar File Watcher**: + - Settings → Tools → File Watchers → Add + - File type: Python + - Program: `black` + +## Métricas de Calidad + +### Cobertura de Tests + +```bash +# Ejecutar tests con cobertura +make coverage + +# Ver reporte HTML +open htmlcov/index.html +``` + +### Complejidad Ciclomática + +```bash +# Instalar radon +pip install radon + +# Analizar complejidad +radon cc . -a -nb + +# Generar reporte JSON +radon cc . -j > complexity-report.json +``` + +### Mantenibilidad + +```bash +# Índice de mantenibilidad +radon mi . -s + +# Mostrar solo archivos con baja mantenibilidad +radon mi . -s -n C +``` + +## Consejos y Mejores Prácticas + +### 1. Formateo Automático + +Ejecuta `make format` antes de cada commit para mantener el código formateado. + +### 2. Pre-commit Hooks + +Deja que los pre-commit hooks trabajen por ti. No los saltes a menos que sea absolutamente necesario. + +### 3. Gradual + +Si el proyecto tiene mucho código legacy, puedes aplicar linting gradualmente: + +```bash +# Solo en archivos modificados +git diff --name-only | xargs flake8 +``` + +### 4. Ignorar Reglas Específicas + +Si necesitas ignorar una regla en una línea específica: + +```python +# noqa: E501 +long_line = "Esta línea es muy larga pero es necesaria" # noqa: E501 + +# Para múltiples reglas +code = "something" # noqa: E501,W503 +``` + +### 5. Documentación + +Mantén docstrings en funciones importantes: + +```python +def calculate_attendance(student_id: int, event_id: int) -> float: + """ + Calcula el porcentaje de asistencia de un estudiante. + + Args: + student_id: ID del estudiante + event_id: ID del evento + + Returns: + Porcentaje de asistencia (0-100) + + Raises: + ValueError: Si el estudiante o evento no existe + """ + pass +``` + +## Solución de Problemas + +### Error: "command not found" + +Asegúrate de haber instalado las dependencias: +```bash +make install-dev +``` + +### Error: "pre-commit: command not found" + +```bash +pip install pre-commit +pre-commit install +``` + +### Conflictos entre Black e isort + +La configuración está ajustada para que sean compatibles. Si hay conflictos: +```bash +# Ejecutar en orden +isort . +black . +``` + +### Demasiados errores de Flake8 + +Puedes ajustar las reglas en `.flake8` o formatear automáticamente: +```bash +make format +``` + +## Referencias + +- [PEP 8 – Style Guide for Python Code](https://peps.python.org/pep-0008/) +- [Black Documentation](https://black.readthedocs.io/) +- [Flake8 Documentation](https://flake8.pycqa.org/) +- [Pylint Documentation](https://pylint.pycqa.org/) +- [isort Documentation](https://pycqa.github.io/isort/) +- [Pre-commit Documentation](https://pre-commit.com/) diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 0000000..6fcf293 --- /dev/null +++ b/backend/Makefile @@ -0,0 +1,107 @@ +# Makefile para comandos comunes del proyecto + +.PHONY: help install install-dev lint format test coverage clean run migrate shell + +# Variables +PYTHON := python +PIP := pip +MANAGE := $(PYTHON) manage.py + +# Colores para output +BLUE := \033[0;34m +GREEN := \033[0;32m +YELLOW := \033[1;33m +NC := \033[0m # No Color + +help: ## Mostrar ayuda + @echo "$(GREEN)Comandos disponibles:$(NC)" + @echo "" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " $(BLUE)%-20s$(NC) %s\n", $$1, $$2}' + @echo "" + +install: ## Instalar dependencias de producción + @echo "$(YELLOW)Instalando dependencias de producción...$(NC)" + $(PIP) install -r requirements.txt + @echo "$(GREEN)✓ Dependencias instaladas$(NC)" + +install-dev: install ## Instalar dependencias de desarrollo + @echo "$(YELLOW)Instalando dependencias de desarrollo...$(NC)" + $(PIP) install -r requirements-dev.txt + @echo "$(GREEN)✓ Dependencias de desarrollo instaladas$(NC)" + @echo "" + @echo "$(YELLOW)Instalando pre-commit hooks...$(NC)" + pre-commit install + @echo "$(GREEN)✓ Pre-commit hooks instalados$(NC)" + +lint: ## Ejecutar linters (flake8, pylint, bandit) + @echo "$(YELLOW)Ejecutando análisis de código...$(NC)" + @bash scripts/lint.sh + +format: ## Formatear código automáticamente (black, isort, autopep8) + @echo "$(YELLOW)Formateando código...$(NC)" + @bash scripts/format.sh + +test: ## Ejecutar tests + @echo "$(YELLOW)Ejecutando tests...$(NC)" + pytest -v + +coverage: ## Ejecutar tests con cobertura + @echo "$(YELLOW)Ejecutando tests con cobertura...$(NC)" + pytest --cov=. --cov-report=html --cov-report=term-missing + @echo "" + @echo "$(GREEN)✓ Reporte de cobertura generado en htmlcov/index.html$(NC)" + +clean: ## Limpiar archivos temporales + @echo "$(YELLOW)Limpiando archivos temporales...$(NC)" + find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + find . -type f -name "*.pyc" -delete + find . -type f -name "*.pyo" -delete + find . -type f -name "*.coverage" -delete + find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true + find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true + rm -rf htmlcov/ .coverage 2>/dev/null || true + @echo "$(GREEN)✓ Archivos temporales eliminados$(NC)" + +run: ## Ejecutar servidor de desarrollo + @echo "$(YELLOW)Iniciando servidor de desarrollo...$(NC)" + $(MANAGE) runserver + +migrate: ## Aplicar migraciones + @echo "$(YELLOW)Aplicando migraciones...$(NC)" + $(MANAGE) migrate + +makemigrations: ## Crear migraciones + @echo "$(YELLOW)Creando migraciones...$(NC)" + $(MANAGE) makemigrations + +shell: ## Abrir shell de Django + $(MANAGE) shell_plus --ipython || $(MANAGE) shell + +superuser: ## Crear superusuario + $(MANAGE) createsuperuser + +check: ## Verificar proyecto Django + @echo "$(YELLOW)Verificando proyecto...$(NC)" + $(MANAGE) check + +collectstatic: ## Recolectar archivos estáticos + @echo "$(YELLOW)Recolectando archivos estáticos...$(NC)" + $(MANAGE) collectstatic --noinput + +security: ## Verificar vulnerabilidades de seguridad + @echo "$(YELLOW)Verificando vulnerabilidades...$(NC)" + safety check + bandit -r . -c pyproject.toml + +pre-commit-all: ## Ejecutar pre-commit en todos los archivos + @echo "$(YELLOW)Ejecutando pre-commit en todos los archivos...$(NC)" + pre-commit run --all-files + +setup: install-dev migrate ## Setup inicial del proyecto + @echo "" + @echo "$(GREEN)✓ Setup completado$(NC)" + @echo "" + @echo "Próximos pasos:" + @echo " 1. Crea un superusuario: make superuser" + @echo " 2. Ejecuta el servidor: make run" + @echo " 3. Revisa la ayuda: make help" diff --git a/backend/attendance/__init__.py b/backend/attendance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/attendance/admin.py b/backend/attendance/admin.py new file mode 100644 index 0000000..52fbfe4 --- /dev/null +++ b/backend/attendance/admin.py @@ -0,0 +1,169 @@ +from django.contrib import admin +from django.utils.html import format_html +from django.urls import path +from django.http import HttpResponse +from import_export import resources, fields +from import_export.admin import ExportMixin +from .models import Attendance, AttendanceStats + + +class AttendanceStatsResource(resources.ModelResource): + """Recurso para exportar estadísticas de asistencia""" + account_number = fields.Field() + full_name = fields.Field() + cumple_requisito = fields.Field() + + class Meta: + model = AttendanceStats + fields = ('account_number', 'full_name', 'attended_events', 'total_events', + 'attendance_percentage', 'cumple_requisito') + export_order = fields + + def dehydrate_account_number(self, stats): + """Obtener número de cuenta del estudiante""" + return stats.student.account_number + + def dehydrate_full_name(self, stats): + """Obtener nombre completo del estudiante""" + return stats.student.full_name + + def dehydrate_cumple_requisito(self, stats): + """Verificar si cumple el requisito mínimo""" + return 'SÍ' if stats.meets_minimum_requirement() else 'NO' + +@admin.register(Attendance) +class AttendanceAdmin(admin.ModelAdmin): + list_display = ['attendee_name', 'attendee_identifier', 'event', 'timestamp', 'registration_method', 'get_registered_by', 'is_valid'] + list_filter = ['registration_method', 'registered_by', 'event__date', 'is_valid', 'event'] + search_fields = ['student__full_name', 'student__account_number', 'external_user__full_name', + 'external_user__temporary_id', 'event__title', 'registered_by__full_name', + 'registered_by__account_number'] + ordering = ['-timestamp'] + readonly_fields = ['timestamp', 'attendee_name', 'attendee_identifier'] + date_hierarchy = 'timestamp' + + fieldsets = ( + ('Información del Asistente', { + 'fields': ('student', 'external_user', 'attendee_name', 'attendee_identifier') + }), + ('Información del Evento', { + 'fields': ('event',) + }), + ('Registro', { + 'fields': ('registered_by', 'registration_method', 'timestamp', 'is_valid', 'notes') + }), + ) + + def get_registered_by(self, obj): + """Muestra el asistente que registró con formato mejorado""" + if obj.registered_by: + return format_html( + '👤 {}
Cuenta: {}', + obj.registered_by.full_name, + obj.registered_by.account_number + ) + return '-' + get_registered_by.short_description = 'Registrado por' + + def has_add_permission(self, request): + # Prevenir creación manual desde admin (debe hacerse desde la API) + return False + + def has_change_permission(self, request, obj=None): + # Solo superusuarios pueden editar asistencias (para correcciones excepcionales) + return request.user.is_superuser + + def has_delete_permission(self, request, obj=None): + # Solo superusuarios pueden eliminar asistencias + return request.user.is_superuser + +@admin.register(AttendanceStats) +class AttendanceStatsAdmin(ExportMixin, admin.ModelAdmin): + resource_class = AttendanceStatsResource + list_display = ['student', 'attended_events', 'total_events', 'attendance_percentage', 'get_cumple_requisito'] + ordering = ['-attendance_percentage'] + list_filter = ['attendance_percentage'] + search_fields = ['student__account_number', 'student__full_name'] + actions = ['export_selected_stats', 'export_students_with_certificate'] + + def get_cumple_requisito(self, obj): + """Mostrar si cumple el requisito mínimo""" + cumple = obj.meets_minimum_requirement() + color = '#28a745' if cumple else '#dc3545' + text = '✅ SÍ' if cumple else '❌ NO' + return format_html( + '{}', + color, text + ) + get_cumple_requisito.short_description = 'Cumple requisito' + + def export_selected_stats(self, request, queryset): + """Acción para exportar estadísticas seleccionadas""" + resource = AttendanceStatsResource() + dataset = resource.export(queryset) + + from import_export.formats.base_formats import XLSX + xlsx_format = XLSX() + export_data = xlsx_format.export_data(dataset) + + response = HttpResponse( + export_data, + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + response['Content-Disposition'] = 'attachment; filename="estadisticas_asistencia.xlsx"' + + self.message_user(request, f'Se exportaron {queryset.count()} estadísticas.') + return response + + export_selected_stats.short_description = "📊 Exportar estadísticas seleccionadas" + + def export_students_with_certificate(self, request, queryset): + """Acción para exportar solo estudiantes que cumplen el requisito mínimo""" + from authentication.models import SystemConfiguration + config = SystemConfiguration.get_config() + + # Filtrar solo los que cumplen el requisito + qualified_students = queryset.filter( + attendance_percentage__gte=config.minimum_attendance_percentage + ) + + # Crear el recurso y exportar + resource = AttendanceStatsResource() + dataset = resource.export(qualified_students) + + # Generar archivo Excel + from import_export.formats.base_formats import XLSX + xlsx_format = XLSX() + export_data = xlsx_format.export_data(dataset) + + response = HttpResponse( + export_data, + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + response['Content-Disposition'] = 'attachment; filename="estudiantes_con_constancia.xlsx"' + + self.message_user( + request, + f'Se exportaron {qualified_students.count()} estudiantes que cumplen con el {config.minimum_attendance_percentage}% de asistencia mínima.' + ) + + return response + + export_students_with_certificate.short_description = "📊 Exportar estudiantes que cumplen requisito para constancia" + + def get_export_formats(self): + """Formatos permitidos para exportar""" + from import_export.formats.base_formats import XLSX, CSV + return [XLSX, CSV] + + def has_add_permission(self, request): + # Las estadísticas se generan automáticamente + return False + + def has_change_permission(self, request, obj=None): + # Las estadísticas son de solo lectura + return False + + def has_delete_permission(self, request, obj=None): + # Solo superusuarios pueden eliminar estadísticas + return request.user.is_superuser \ No newline at end of file diff --git a/backend/attendance/apps.py b/backend/attendance/apps.py new file mode 100644 index 0000000..ba31b45 --- /dev/null +++ b/backend/attendance/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AttendanceConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'attendance' diff --git a/backend/attendance/management/__init__.py b/backend/attendance/management/__init__.py new file mode 100644 index 0000000..a94f2a3 --- /dev/null +++ b/backend/attendance/management/__init__.py @@ -0,0 +1 @@ +# Management package diff --git a/backend/attendance/management/commands/__init__.py b/backend/attendance/management/commands/__init__.py new file mode 100644 index 0000000..b5a3a84 --- /dev/null +++ b/backend/attendance/management/commands/__init__.py @@ -0,0 +1 @@ +# Commands package diff --git a/backend/attendance/management/commands/recalculate_stats.py b/backend/attendance/management/commands/recalculate_stats.py new file mode 100644 index 0000000..c100e67 --- /dev/null +++ b/backend/attendance/management/commands/recalculate_stats.py @@ -0,0 +1,35 @@ +from django.core.management.base import BaseCommand +from attendance.models import AttendanceStats +from authentication.models import UserProfile + + +class Command(BaseCommand): + help = 'Recalcula las estadísticas de asistencia de todos los estudiantes usando bloques de horario' + + def handle(self, *args, **options): + self.stdout.write('Recalculando estadísticas de asistencia...\n') + + # Obtener todos los estudiantes + students = UserProfile.objects.filter(user_type='student') + total_students = students.count() + + self.stdout.write(f'Se encontraron {total_students} estudiantes\n') + + updated = 0 + for student in students: + # Obtener o crear las estadísticas + stats, created = AttendanceStats.objects.get_or_create(student=student) + + # Actualizar con la nueva lógica + stats.update_stats() + + updated += 1 + + if updated % 10 == 0: + self.stdout.write(f'Procesados {updated}/{total_students} estudiantes...') + + self.stdout.write( + self.style.SUCCESS( + f'\nEstadisticas recalculadas exitosamente para {updated} estudiantes' + ) + ) diff --git a/backend/attendance/migrations/0001_initial.py b/backend/attendance/migrations/0001_initial.py new file mode 100644 index 0000000..202e272 --- /dev/null +++ b/backend/attendance/migrations/0001_initial.py @@ -0,0 +1,51 @@ +# Generated by Django 5.2.6 on 2025-09-24 00:40 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('authentication', '0001_initial'), + ('events', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Attendance', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Hora de registro')), + ('registration_method', models.CharField(choices=[('manual', 'Registro Manual'), ('barcode', 'Código de Barras'), ('external', 'Usuario Externo')], default='manual', max_length=10, verbose_name='Método de registro')), + ('notes', models.TextField(blank=True, null=True, verbose_name='Notas adicionales')), + ('is_valid', models.BooleanField(default=True, verbose_name='Asistencia válida')), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.event', verbose_name='Evento')), + ('external_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='events.externaluser', verbose_name='Usuario Externo')), + ('registered_by', models.ForeignKey(limit_choices_to={'user_type': 'teacher'}, on_delete=django.db.models.deletion.CASCADE, related_name='registered_attendances', to='authentication.userprofile', verbose_name='Registrado por')), + ('student', models.ForeignKey(blank=True, limit_choices_to={'user_type': 'student'}, null=True, on_delete=django.db.models.deletion.CASCADE, to='authentication.userprofile', verbose_name='Estudiante')), + ], + options={ + 'verbose_name': 'Asistencia', + 'verbose_name_plural': 'Asistencias', + 'ordering': ['-timestamp'], + }, + ), + migrations.CreateModel( + name='AttendanceStats', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('total_events', models.IntegerField(default=0, verbose_name='Total de eventos')), + ('attended_events', models.IntegerField(default=0, verbose_name='Eventos asistidos')), + ('attendance_percentage', models.FloatField(default=0.0, verbose_name='Porcentaje de asistencia')), + ('last_updated', models.DateTimeField(auto_now=True, verbose_name='Última actualización')), + ('student', models.OneToOneField(limit_choices_to={'user_type': 'student'}, on_delete=django.db.models.deletion.CASCADE, to='authentication.userprofile', verbose_name='Estudiante')), + ], + options={ + 'verbose_name': 'Estadísticas de asistencia', + 'verbose_name_plural': 'Estadísticas de asistencia', + }, + ), + ] diff --git a/backend/attendance/migrations/0002_alter_attendance_registered_by.py b/backend/attendance/migrations/0002_alter_attendance_registered_by.py new file mode 100644 index 0000000..26f11e3 --- /dev/null +++ b/backend/attendance/migrations/0002_alter_attendance_registered_by.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.6 on 2025-10-01 02:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('attendance', '0001_initial'), + ('authentication', '0003_rename_teacher_assistant_alter_userprofile_user_type'), + ] + + operations = [ + migrations.AlterField( + model_name='attendance', + name='registered_by', + field=models.ForeignKey(limit_choices_to={'user_type': 'assistant'}, on_delete=django.db.models.deletion.CASCADE, related_name='registered_attendances', to='authentication.userprofile', verbose_name='Registrado por'), + ), + ] diff --git a/backend/attendance/migrations/0003_alter_attendance_external_user.py b/backend/attendance/migrations/0003_alter_attendance_external_user.py new file mode 100644 index 0000000..21b2863 --- /dev/null +++ b/backend/attendance/migrations/0003_alter_attendance_external_user.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.6 on 2025-10-02 00:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('attendance', '0002_alter_attendance_registered_by'), + ('authentication', '0006_externaluser'), + ] + + operations = [ + migrations.AlterField( + model_name='attendance', + name='external_user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='authentication.externaluser', verbose_name='Usuario Externo'), + ), + ] diff --git a/backend/attendance/migrations/__init__.py b/backend/attendance/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/attendance/models.py b/backend/attendance/models.py new file mode 100644 index 0000000..45902ce --- /dev/null +++ b/backend/attendance/models.py @@ -0,0 +1,291 @@ +from django.db import models +from django.core.exceptions import ValidationError +from django.utils import timezone +from datetime import datetime, time as dt_time, timedelta +from authentication.models import UserProfile, ExternalUser +from events.models import Event + +class Attendance(models.Model): + REGISTRATION_METHODS = ( + ('manual', 'Registro Manual'), + ('barcode', 'Código de Barras'), + ('external', 'Usuario Externo'), + ) + + # Puede ser estudiante regular o externo + student = models.ForeignKey( + UserProfile, + on_delete=models.CASCADE, + blank=True, + null=True, + limit_choices_to={'user_type': 'student'}, + verbose_name="Estudiante" + ) + external_user = models.ForeignKey( + ExternalUser, + on_delete=models.CASCADE, + blank=True, + null=True, + verbose_name="Usuario Externo" + ) + event = models.ForeignKey( + Event, + on_delete=models.CASCADE, + verbose_name="Evento" + ) + timestamp = models.DateTimeField( + auto_now_add=True, + verbose_name="Hora de registro" + ) + registered_by = models.ForeignKey( + UserProfile, + on_delete=models.CASCADE, + related_name='registered_attendances', + limit_choices_to={'user_type': 'assistant'}, + verbose_name="Registrado por" + ) + registration_method = models.CharField( + max_length=10, + choices=REGISTRATION_METHODS, + default='manual', + verbose_name="Método de registro" + ) + notes = models.TextField( + blank=True, + null=True, + verbose_name="Notas adicionales" + ) + is_valid = models.BooleanField( + default=True, + verbose_name="Asistencia válida" + ) + + class Meta: + verbose_name = "Asistencia" + verbose_name_plural = "Asistencias" + ordering = ['-timestamp'] + + def clean(self): + # Debe tener estudiante O usuario externo, pero no ambos + if not self.student and not self.external_user: + raise ValidationError("Debe especificar un estudiante o usuario externo.") + + if self.student and self.external_user: + raise ValidationError("No puede tener ambos: estudiante y usuario externo.") + + # Validar que el registrador sea un asistente + if self.registered_by.user_type != 'assistant': + raise ValidationError("Solo los asistentes pueden registrar asistencias.") + + # Validar que el evento esté en curso (entre start_time y end_time) + from authentication.models import SystemConfiguration + config = SystemConfiguration.get_config() + + now = timezone.now() + event_date = self.event.date + event_start = datetime.combine(event_date, self.event.start_time) + event_end = datetime.combine(event_date, self.event.end_time) + + # Hacer timezone-aware si es necesario + if timezone.is_naive(event_start): + event_start = timezone.make_aware(event_start) + if timezone.is_naive(event_end): + event_end = timezone.make_aware(event_end) + + # Usar configuración global del sistema + registration_start = event_start - timedelta(minutes=config.minutes_before_event) + registration_end = event_start + timedelta(minutes=config.minutes_after_start) + + if now < registration_start: + raise ValidationError( + f"No se puede registrar asistencia antes del evento. " + f"El evento inicia el {event_date.strftime('%d/%m/%Y')} a las {self.event.start_time.strftime('%H:%M')}. " + f"Puedes registrar desde {config.minutes_before_event} minutos antes." + ) + + if now > registration_end: + raise ValidationError( + f"No se puede registrar asistencia después del tiempo límite. " + f"El evento inició el {event_date.strftime('%d/%m/%Y')} a las {self.event.start_time.strftime('%H:%M')}. " + f"El tiempo límite de registro es {config.minutes_after_start} minutos después del inicio." + ) + + # Validar que no haya duplicados + if self.student: + existing = Attendance.objects.filter( + student=self.student, + event=self.event, + is_valid=True + ) + if self.pk: + existing = existing.exclude(pk=self.pk) + if existing.exists(): + raise ValidationError("Este estudiante ya tiene asistencia registrada para este evento.") + + if self.external_user: + existing = Attendance.objects.filter( + external_user=self.external_user, + event=self.event, + is_valid=True + ) + if self.pk: + existing = existing.exclude(pk=self.pk) + if existing.exists(): + raise ValidationError("Este usuario externo ya tiene asistencia registrada para este evento.") + + # Validar eventos simultáneos (solo para estudiantes regulares) + if self.student: + overlapping_events = Event.objects.filter( + date=self.event.date, + start_time__lt=self.event.end_time, + end_time__gt=self.event.start_time, + is_active=True + ).exclude(id=self.event.id) + + existing_attendance = Attendance.objects.filter( + student=self.student, + event__in=overlapping_events, + is_valid=True + ).exists() + + if existing_attendance: + raise ValidationError( + "El estudiante ya tiene asistencia registrada en un evento simultáneo." + ) + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + + # Actualizar estadísticas si es estudiante regular + if self.student: + self.update_student_stats() + + def update_student_stats(self): + """Actualizar las estadísticas de asistencia del estudiante""" + stats, created = AttendanceStats.objects.get_or_create( + student=self.student, + defaults={ + 'total_events': 0, + 'attended_events': 0, + 'attendance_percentage': 0.0 + } + ) + stats.update_stats() + + @property + def attendee_name(self): + """Nombre del asistente (estudiante o externo)""" + if self.student: + return self.student.full_name + elif self.external_user: + return self.external_user.full_name + return "Desconocido" + + @property + def attendee_identifier(self): + """Identificador del asistente""" + if self.student: + return self.student.account_number + elif self.external_user: + return self.external_user.account_number + return "N/A" + + def __str__(self): + return f"{self.attendee_name} - {self.event.title}" + +class AttendanceStats(models.Model): + student = models.OneToOneField( + UserProfile, + on_delete=models.CASCADE, + limit_choices_to={'user_type': 'student'}, + verbose_name="Estudiante" + ) + total_events = models.IntegerField( + default=0, + verbose_name="Total de eventos" + ) + attended_events = models.IntegerField( + default=0, + verbose_name="Eventos asistidos" + ) + attendance_percentage = models.FloatField( + default=0.0, + verbose_name="Porcentaje de asistencia" + ) + last_updated = models.DateTimeField( + auto_now=True, + verbose_name="Última actualización" + ) + + class Meta: + verbose_name = "Estadísticas de asistencia" + verbose_name_plural = "Estadísticas de asistencia" + + def update_stats(self): + """Actualizar las estadísticas de asistencia""" + from events.models import Event # Importar aquí para evitar circular imports + + # Obtener todos los eventos activos + all_events = Event.objects.filter(is_active=True).order_by('date', 'start_time') + + # Agrupar eventos por bloques de horario (misma fecha y horarios que se solapan) + event_slots = {} # {(fecha, hora_inicio, hora_fin): [lista de eventos]} + + for event in all_events: + # Crear una clave única para el bloque horario + slot_key = (event.date, event.start_time, event.end_time) + + # Buscar si hay un slot existente que se solape con este evento + found_slot = False + for existing_slot in list(event_slots.keys()): + existing_date, existing_start, existing_end = existing_slot + + # Verificar si es el mismo día y hay solapamiento de horarios + if event.date == existing_date: + # Hay solapamiento si el inicio de uno es menor al fin del otro + if (event.start_time < existing_end and event.end_time > existing_start): + event_slots[existing_slot].append(event) + found_slot = True + break + + # Si no encontramos un slot existente, crear uno nuevo + if not found_slot: + event_slots[slot_key] = [event] + + # El total de "bloques" es la cantidad de slots únicos + total_slots = len(event_slots) + + # Obtener asistencias del estudiante + student_attendances = Attendance.objects.filter( + student=self.student, + is_valid=True + ).values_list('event_id', flat=True) + + # Contar cuántos bloques tiene asistencia + attended_slots = 0 + for slot_events in event_slots.values(): + # Si asistió a al menos uno de los eventos del bloque, cuenta + event_ids = [e.id for e in slot_events] + if any(event_id in student_attendances for event_id in event_ids): + attended_slots += 1 + + self.total_events = total_slots + self.attended_events = attended_slots + + # Calcular porcentaje + if total_slots > 0: + self.attendance_percentage = round((attended_slots / total_slots) * 100, 2) + else: + self.attendance_percentage = 0.0 + + self.save() + + def meets_minimum_requirement(self): + """Verifica si cumple con el requisito mínimo de asistencia global""" + from authentication.models import SystemConfiguration + config = SystemConfiguration.get_config() + return self.attendance_percentage >= config.minimum_attendance_percentage + + def __str__(self): + return f"Stats: {self.student.full_name} - {self.attendance_percentage}%" \ No newline at end of file diff --git a/backend/attendance/urls.py b/backend/attendance/urls.py new file mode 100644 index 0000000..214a160 --- /dev/null +++ b/backend/attendance/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('', views.register_attendance, name='register_attendance'), + path('stats/', views.get_student_stats, name='student_stats'), + path('recent/', views.get_recent_attendances, name='recent_attendances'), + path('my/', views.get_my_attendances, name='my_attendances'), + path('external/my/', views.get_external_user_attendances, name='external_user_attendances'), + path('external/stats/', views.get_external_user_stats, name='external_user_stats'), +] \ No newline at end of file diff --git a/backend/attendance/views.py b/backend/attendance/views.py new file mode 100644 index 0000000..78f817e --- /dev/null +++ b/backend/attendance/views.py @@ -0,0 +1,387 @@ +from rest_framework import status +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.response import Response +from django.shortcuts import get_object_or_404 +from django_ratelimit.decorators import ratelimit +from authentication.models import UserProfile, ExternalUser +from events.models import Event +from .models import Attendance, AttendanceStats + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +@ratelimit(key='user', rate='60/m', method='POST', block=True) +def register_attendance(request): + """Registrar asistencia - Solo asistentes: 60 registros por minuto""" + if request.method == 'GET': + return Response({ + 'message': 'API de registro de asistencia activa', + 'methods': ['POST'], + 'required_fields': ['event_id', 'account_number'] + }) + + # Verificar que el usuario autenticado sea asistente + try: + registrar_profile = request.user.userprofile + if registrar_profile.user_type != 'assistant': + return Response({ + 'error': 'Solo los asistentes pueden registrar asistencias' + }, status=status.HTTP_403_FORBIDDEN) + except: + return Response({ + 'error': 'Usuario sin perfil válido' + }, status=status.HTTP_403_FORBIDDEN) + + event_id = request.data.get('event_id') + account_number = request.data.get('account_number') + + if not event_id or not account_number: + return Response({ + 'error': 'Se requiere event_id y account_number' + }, status=status.HTTP_400_BAD_REQUEST) + + # Buscar evento + try: + event = Event.objects.get(id=event_id, is_active=True) + except Event.DoesNotExist: + return Response({ + 'error': 'Evento no encontrado' + }, status=status.HTTP_404_NOT_FOUND) + + # Buscar estudiante regular o usuario externo + student_profile = None + external_user = None + attendee_name = None + + # Primero buscar en estudiantes regulares + try: + student_profile = UserProfile.objects.get( + account_number=account_number, + user_type='student' + ) + attendee_name = student_profile.full_name + except UserProfile.DoesNotExist: + # Si no es estudiante, buscar en usuarios externos + try: + external_user = ExternalUser.objects.get( + account_number=account_number, + status='approved' + ) + attendee_name = external_user.full_name + except ExternalUser.DoesNotExist: + return Response({ + 'error': f'Usuario con número de cuenta {account_number} no encontrado o no aprobado' + }, status=status.HTTP_404_NOT_FOUND) + + # Usar el asistente autenticado como registrador + assistant_profile = registrar_profile + + # Verificar si ya tiene asistencia + if student_profile: + if Attendance.objects.filter(student=student_profile, event=event, is_valid=True).exists(): + return Response({ + 'error': 'El estudiante ya tiene asistencia registrada para este evento' + }, status=status.HTTP_400_BAD_REQUEST) + elif external_user: + if Attendance.objects.filter(external_user=external_user, event=event, is_valid=True).exists(): + return Response({ + 'error': 'El usuario externo ya tiene asistencia registrada para este evento' + }, status=status.HTTP_400_BAD_REQUEST) + + # Crear asistencia + try: + attendance = Attendance.objects.create( + student=student_profile, + external_user=external_user, + event=event, + registered_by=assistant_profile, + registration_method='manual' + ) + + return Response({ + 'message': f'Asistencia registrada para {attendee_name}', + 'attendance_id': attendance.id, + 'event': event.title, + 'registered_by': assistant_profile.full_name, + 'attendee_type': 'student' if student_profile else 'external' + }, status=status.HTTP_201_CREATED) + + except Exception as e: + return Response({ + 'error': f'Error al crear asistencia: {str(e)}' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@ratelimit(key='user', rate='30/m', method='GET', block=True) +def get_student_stats(request): + """Obtener estadísticas de estudiante: 30 consultas por minuto""" + account_number = request.GET.get('account_number') + + if not account_number: + return Response({'error': 'Se requiere account_number'}, status=400) + + # Verificar permisos: estudiantes solo pueden ver sus propias estadísticas + try: + requester_profile = request.user.userprofile + + # Si es estudiante, solo puede consultar sus propias estadísticas + if requester_profile.user_type == 'student': + if requester_profile.account_number != account_number: + return Response({ + 'error': 'Solo puedes consultar tus propias estadísticas' + }, status=status.HTTP_403_FORBIDDEN) + # Asistentes pueden ver estadísticas de cualquier estudiante + elif requester_profile.user_type != 'assistant': + return Response({ + 'error': 'No tienes permisos para consultar estadísticas' + }, status=status.HTTP_403_FORBIDDEN) + except: + return Response({ + 'error': 'Usuario sin perfil válido' + }, status=status.HTTP_403_FORBIDDEN) + + try: + student_profile = UserProfile.objects.get( + account_number=account_number, + user_type='student' + ) + stats, created = AttendanceStats.objects.get_or_create( + student=student_profile + ) + stats.update_stats() + + return Response({ + 'total_events': stats.total_events, + 'attended_events': stats.attended_events, + 'attendance_percentage': stats.attendance_percentage + }) + except UserProfile.DoesNotExist: + return Response({'error': 'Estudiante no encontrado'}, status=404) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@ratelimit(key='user', rate='60/m', method='GET', block=True) +def get_recent_attendances(request): + """Obtener asistencias recientes - Solo asistentes: 60 consultas por minuto""" + # Solo asistentes pueden ver asistencias recientes + try: + requester_profile = request.user.userprofile + if requester_profile.user_type != 'assistant': + return Response({ + 'error': 'Solo los asistentes pueden consultar asistencias recientes' + }, status=status.HTTP_403_FORBIDDEN) + except: + return Response({ + 'error': 'Usuario sin perfil válido' + }, status=status.HTTP_403_FORBIDDEN) + + recent = Attendance.objects.select_related('student', 'event').order_by('-timestamp')[:5] + + data = [] + for attendance in recent: + data.append({ + 'attendee_name': attendance.attendee_name, + 'event_title': attendance.event.title, + 'timestamp': attendance.timestamp.strftime('%H:%M') + }) + + return Response(data) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@ratelimit(key='user', rate='60/m', method='GET', block=True) +def get_my_attendances(request): + """Obtener mis asistencias - Estudiantes pueden ver sus propias asistencias: 60 consultas por minuto""" + account_number = request.GET.get('account_number') + + if not account_number: + return Response({'error': 'Se requiere account_number'}, status=400) + + # Verificar permisos: estudiantes solo pueden ver sus propias asistencias + try: + requester_profile = request.user.userprofile + + # Si es estudiante, solo puede consultar sus propias asistencias + if requester_profile.user_type == 'student': + if requester_profile.account_number != account_number: + return Response({ + 'error': 'Solo puedes consultar tus propias asistencias' + }, status=status.HTTP_403_FORBIDDEN) + # Asistentes pueden ver asistencias de cualquier estudiante + elif requester_profile.user_type != 'assistant': + return Response({ + 'error': 'No tienes permisos para consultar asistencias' + }, status=status.HTTP_403_FORBIDDEN) + except: + return Response({ + 'error': 'Usuario sin perfil válido' + }, status=status.HTTP_403_FORBIDDEN) + + try: + student_profile = UserProfile.objects.get( + account_number=account_number, + user_type='student' + ) + + # Obtener todas las asistencias válidas del estudiante + attendances = Attendance.objects.filter( + student=student_profile, + is_valid=True + ).select_related('event').order_by('-timestamp') + + data = [] + for attendance in attendances: + data.append({ + 'id': attendance.id, + 'event': attendance.event.id, + 'event_title': attendance.event.title, + 'event_date': attendance.event.date.strftime('%Y-%m-%d'), + 'timestamp': attendance.timestamp.strftime('%Y-%m-%d %H:%M:%S') + }) + + return Response(data) + except UserProfile.DoesNotExist: + return Response({'error': 'Estudiante no encontrado'}, status=404) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@ratelimit(key='user', rate='60/m', method='GET', block=True) +def get_external_user_attendances(request): + """Obtener asistencias de usuario externo: 60 consultas por minuto""" + account_number = request.GET.get('account_number') + + if not account_number: + return Response({'error': 'Se requiere account_number'}, status=400) + + # Normalizar el username para comparar (eliminar prefijo "ext_") + user_account = request.user.username.replace('ext_', '') + + # Verificar que el usuario externo solo pueda ver sus propias asistencias + if user_account != account_number: + # Verificar si es asistente (pueden ver todas) + try: + requester_profile = request.user.userprofile + if requester_profile.user_type != 'assistant': + return Response({ + 'error': 'Solo puedes consultar tus propias asistencias' + }, status=status.HTTP_403_FORBIDDEN) + except: + return Response({ + 'error': 'Solo puedes consultar tus propias asistencias' + }, status=status.HTTP_403_FORBIDDEN) + + try: + external_user = ExternalUser.objects.get( + account_number=account_number, + status='approved' + ) + + # Obtener todas las asistencias válidas del usuario externo + attendances = Attendance.objects.filter( + external_user=external_user, + is_valid=True + ).select_related('event').order_by('-timestamp') + + data = [] + for attendance in attendances: + data.append({ + 'id': attendance.id, + 'event': attendance.event.id, + 'event_title': attendance.event.title, + 'event_date': attendance.event.date.strftime('%Y-%m-%d'), + 'timestamp': attendance.timestamp.strftime('%Y-%m-%d %H:%M:%S') + }) + + return Response(data) + except ExternalUser.DoesNotExist: + return Response({'error': 'Usuario externo no encontrado o no aprobado'}, status=404) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@ratelimit(key='user', rate='30/m', method='GET', block=True) +def get_external_user_stats(request): + """Obtener estadísticas de usuario externo: 30 consultas por minuto""" + account_number = request.GET.get('account_number') + + if not account_number: + return Response({'error': 'Se requiere account_number'}, status=400) + + # Normalizar el username para comparar (eliminar prefijo "ext_") + user_account = request.user.username.replace('ext_', '') + + # Verificar que el usuario externo solo pueda ver sus propias estadísticas + if user_account != account_number: + # Verificar si es asistente (pueden ver todas) + try: + requester_profile = request.user.userprofile + if requester_profile.user_type != 'assistant': + return Response({ + 'error': 'Solo puedes consultar tus propias estadísticas' + }, status=status.HTTP_403_FORBIDDEN) + except: + return Response({ + 'error': 'Solo puedes consultar tus propias estadísticas' + }, status=status.HTTP_403_FORBIDDEN) + + try: + external_user = ExternalUser.objects.get( + account_number=account_number, + status='approved' + ) + + # Obtener todos los eventos activos + all_events = Event.objects.filter(is_active=True).order_by('date', 'start_time') + + # Agrupar eventos por bloques de horario (misma fecha y horarios que se solapan) + # Usar la misma lógica que AttendanceStats.update_stats() + event_slots = {} # {(fecha, hora_inicio, hora_fin): [lista de eventos]} + + for event in all_events: + # Crear una clave única para el bloque horario + slot_key = (event.date, event.start_time, event.end_time) + + # Buscar si hay un slot existente que se solape con este evento + found_slot = False + for existing_slot in list(event_slots.keys()): + existing_date, existing_start, existing_end = existing_slot + + # Verificar si es el mismo día y hay solapamiento de horarios + if event.date == existing_date: + # Hay solapamiento si el inicio de uno es menor al fin del otro + if (event.start_time < existing_end and event.end_time > existing_start): + event_slots[existing_slot].append(event) + found_slot = True + break + + # Si no encontramos un slot existente, crear uno nuevo + if not found_slot: + event_slots[slot_key] = [event] + + # El total de "bloques" es la cantidad de slots únicos + total_slots = len(event_slots) + + # Obtener asistencias del usuario externo + user_attendances = Attendance.objects.filter( + external_user=external_user, + is_valid=True + ).values_list('event_id', flat=True) + + # Contar cuántos bloques tiene asistencia + attended_slots = 0 + for slot_events in event_slots.values(): + # Si asistió a al menos uno de los eventos del bloque, cuenta + event_ids = [e.id for e in slot_events] + if any(event_id in user_attendances for event_id in event_ids): + attended_slots += 1 + + # Calcular porcentaje + attendance_percentage = round((attended_slots / total_slots) * 100, 2) if total_slots > 0 else 0.0 + + return Response({ + 'total_events': total_slots, + 'attended_events': attended_slots, + 'attendance_percentage': attendance_percentage + }) + except ExternalUser.DoesNotExist: + return Response({'error': 'Usuario externo no encontrado o no aprobado'}, status=404) diff --git a/backend/authentication/__init__.py b/backend/authentication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/authentication/admin.py b/backend/authentication/admin.py new file mode 100644 index 0000000..53e2535 --- /dev/null +++ b/backend/authentication/admin.py @@ -0,0 +1,607 @@ +from django.contrib import admin +from django.contrib.auth.models import User, Group +from django import forms +from django.utils.html import format_html +from django.http import HttpResponse +from import_export import resources, fields +from import_export.admin import ImportExportModelAdmin +from import_export.widgets import ForeignKeyWidget +from .models import UserProfile, Asistente, ExternalUser, SystemConfiguration, Student, AssistantProfile +from .audit import AuditLog + +# Ocultar modelos de Django que no se usan +admin.site.unregister(User) +admin.site.unregister(Group) + + +# ===== RECURSOS PARA IMPORT/EXPORT ===== + +class StudentResource(resources.ModelResource): + """Recurso para importar/exportar SOLO estudiantes""" + + class Meta: + model = UserProfile + fields = ('account_number', 'full_name') + export_order = ('account_number', 'full_name') + import_id_fields = ['account_number'] + skip_unchanged = True + report_skipped = True + + def before_import_row(self, row, **kwargs): + """Validar y limpiar datos antes de importar""" + # Limpiar espacios + row['account_number'] = str(row.get('account_number', '')).strip() + row['full_name'] = str(row.get('full_name', '')).strip() + # Asignar automáticamente user_type como student + row['user_type'] = 'student' + + def after_save_instance(self, instance, using_transactions, dry_run): + """Crear usuario de Django si no existe y asegurar que sea estudiante""" + if not dry_run: + # Asegurar que sea estudiante + if instance.user_type != 'student': + instance.user_type = 'student' + instance.save() + + # Crear usuario de Django si no existe + if not instance.user_id: + user = User.objects.create_user( + username=instance.account_number, + first_name=instance.full_name, + password=None + ) + user.set_unusable_password() + user.save() + instance.user = user + instance.save() + + +class AssistantResource(resources.ModelResource): + """Recurso para importar/exportar SOLO asistentes""" + + class Meta: + model = UserProfile + fields = ('account_number', 'full_name') + export_order = ('account_number', 'full_name') + import_id_fields = ['account_number'] + skip_unchanged = True + report_skipped = True + + def before_import_row(self, row, **kwargs): + """Validar y limpiar datos antes de importar""" + # Limpiar espacios + row['account_number'] = str(row.get('account_number', '')).strip() + row['full_name'] = str(row.get('full_name', '')).strip() + # Asignar automáticamente user_type como assistant + row['user_type'] = 'assistant' + + def after_save_instance(self, instance, using_transactions, dry_run): + """Crear usuario de Django si no existe y asegurar que sea asistente""" + if not dry_run: + # Asegurar que sea asistente + if instance.user_type != 'assistant': + instance.user_type = 'assistant' + instance.save() + + # Crear usuario de Django si no existe + if not instance.user_id: + user = User.objects.create_user( + username=instance.account_number, + first_name=instance.full_name, + password=None + ) + user.set_unusable_password() + user.save() + instance.user = user + instance.save() + + # Crear automáticamente el registro de Asistente si no existe + from .models import Asistente + Asistente.objects.get_or_create( + user_profile=instance, + defaults={'can_manage_events': True} + ) + + +class UserProfileResource(resources.ModelResource): + """Recurso para importar/exportar UserProfile (LEGACY - no usar)""" + + class Meta: + model = UserProfile + fields = ('account_number', 'full_name', 'user_type') + import_id_fields = ['account_number'] + skip_unchanged = True + report_skipped = True + + def before_import_row(self, row, **kwargs): + """Validar y limpiar datos antes de importar""" + # Limpiar espacios + row['account_number'] = str(row.get('account_number', '')).strip() + row['full_name'] = str(row.get('full_name', '')).strip() + row['user_type'] = str(row.get('user_type', '')).strip().lower() + + def after_save_instance(self, instance, using_transactions, dry_run): + """Crear usuario de Django si no existe""" + if not dry_run and not instance.user_id: + user = User.objects.create_user( + username=instance.account_number, + first_name=instance.full_name, + password=None + ) + user.set_unusable_password() + user.save() + instance.user = user + instance.save() + + +class ExternalUserResource(resources.ModelResource): + """Recurso para exportar usuarios externos""" + approved_by_name = fields.Field() + + class Meta: + model = ExternalUser + fields = ('account_number', 'full_name', 'status', 'approved_by_name', 'created_at', 'rejection_reason') + export_order = fields + + def dehydrate_approved_by_name(self, external_user): + """Obtener nombre del aprobador""" + return external_user.approved_by.full_name if external_user.approved_by else '-' + + +class UserProfileForm(forms.ModelForm): + """Formulario personalizado para crear UserProfile sin contraseña""" + + class Meta: + model = UserProfile + fields = ['account_number', 'full_name', 'user_type'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Hacer el campo user_type obligatorio + self.fields['user_type'].required = True + self.fields['user_type'].empty_label = None # Quitar opción vacía del dropdown + + def save(self, commit=True): + profile = super().save(commit=False) + + # Si el perfil no tiene usuario asociado, crear uno automáticamente + if not profile.user_id: + # Crear usuario con username = número de cuenta, sin contraseña + user = User.objects.create_user( + username=profile.account_number, + first_name=profile.full_name, + password=None # Sin contraseña + ) + # Desactivar contraseña usable + user.set_unusable_password() + user.save() + profile.user = user + + if commit: + profile.save() + + return profile + + +class StudentAdmin(ImportExportModelAdmin): + """Admin SOLO para estudiantes""" + resource_class = StudentResource + list_display = ['account_number', 'full_name'] + search_fields = ['account_number', 'full_name'] + actions = ['export_selected_students'] + + fieldsets = ( + ('Información del Estudiante', { + 'fields': ('account_number', 'full_name'), + 'description': '📚 Importa el Excel con solo 2 columnas: account_number y full_name. El sistema automáticamente los creará como estudiantes.' + }), + ) + + def get_queryset(self, request): + """Mostrar SOLO estudiantes""" + qs = super().get_queryset(request) + return qs.filter(user_type='student') + + def get_import_formats(self): + """Formatos permitidos para importar""" + from import_export.formats.base_formats import XLSX, CSV + return [XLSX, CSV] + + def get_export_formats(self): + """Formatos permitidos para exportar""" + from import_export.formats.base_formats import XLSX, CSV + return [XLSX, CSV] + + def export_selected_students(self, request, queryset): + """Acción para exportar estudiantes seleccionados""" + resource = StudentResource() + dataset = resource.export(queryset) + + from import_export.formats.base_formats import XLSX + xlsx_format = XLSX() + export_data = xlsx_format.export_data(dataset) + + response = HttpResponse( + export_data, + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + response['Content-Disposition'] = 'attachment; filename="estudiantes.xlsx"' + + self.message_user(request, f'Se exportaron {queryset.count()} estudiantes.') + return response + + export_selected_students.short_description = "📊 Exportar estudiantes seleccionados" + + def save_model(self, request, obj, form, change): + """Asegurar que siempre sea estudiante y crear usuario si no existe""" + obj.user_type = 'student' + + # Si no tiene usuario, crear uno + if not obj.user_id: + user = User.objects.create_user( + username=obj.account_number, + first_name=obj.full_name, + password=None + ) + user.set_unusable_password() + user.save() + obj.user = user + + super().save_model(request, obj, form, change) + + +class AssistantProfileAdmin(ImportExportModelAdmin): + """Admin SOLO para asistentes""" + resource_class = AssistantResource + list_display = ['account_number', 'full_name'] + search_fields = ['account_number', 'full_name'] + actions = ['export_selected_assistants'] + + fieldsets = ( + ('Información del Asistente', { + 'fields': ('account_number', 'full_name'), + 'description': '👨‍🏫 Importa el Excel con solo 2 columnas: account_number y full_name. El sistema automáticamente los creará como asistentes.' + }), + ) + + def get_queryset(self, request): + """Mostrar SOLO asistentes""" + qs = super().get_queryset(request) + return qs.filter(user_type='assistant') + + def get_import_formats(self): + """Formatos permitidos para importar""" + from import_export.formats.base_formats import XLSX, CSV + return [XLSX, CSV] + + def get_export_formats(self): + """Formatos permitidos para exportar""" + from import_export.formats.base_formats import XLSX, CSV + return [XLSX, CSV] + + def export_selected_assistants(self, request, queryset): + """Acción para exportar asistentes seleccionados""" + resource = AssistantResource() + dataset = resource.export(queryset) + + from import_export.formats.base_formats import XLSX + xlsx_format = XLSX() + export_data = xlsx_format.export_data(dataset) + + response = HttpResponse( + export_data, + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + response['Content-Disposition'] = 'attachment; filename="asistentes.xlsx"' + + self.message_user(request, f'Se exportaron {queryset.count()} asistentes.') + return response + + export_selected_assistants.short_description = "📊 Exportar asistentes seleccionados" + + def save_model(self, request, obj, form, change): + """Asegurar que siempre sea asistente y crear usuario si no existe""" + obj.user_type = 'assistant' + + # Si no tiene usuario, crear uno + if not obj.user_id: + user = User.objects.create_user( + username=obj.account_number, + first_name=obj.full_name, + password=None + ) + user.set_unusable_password() + user.save() + obj.user = user + + super().save_model(request, obj, form, change) + + # Crear automáticamente el registro de Asistente si no existe + Asistente.objects.get_or_create( + user_profile=obj, + defaults={'can_manage_events': True} + ) + + +# Registrar los admins separados con proxy models +admin.site.register(Student, StudentAdmin) +admin.site.register(AssistantProfile, AssistantProfileAdmin) + + +# UserProfile está oculto del admin - se usan Student y AssistantProfile en su lugar + + +@admin.register(Asistente) +class AsistenteAdmin(admin.ModelAdmin): + list_display = ['get_asistente_info', 'get_numero_cuenta', 'get_registros_realizados', 'ver_alumnos_registrados', 'can_manage_events'] + list_filter = ['can_manage_events'] + search_fields = ['user_profile__full_name', 'user_profile__account_number'] + readonly_fields = ['user_profile', 'get_registros_realizados', 'get_ultimos_registros'] + actions = ['ver_reporte_registros'] + + fieldsets = ( + ('Asistente', { + 'fields': ('user_profile',), + 'description': '📋 Los permisos de asistente se crean automáticamente cuando se registra un nuevo asistente.' + }), + ('Permisos y Configuración', { + 'fields': ('can_manage_events',) + }), + ('Estadísticas de Registros', { + 'fields': ('get_registros_realizados', 'get_ultimos_registros'), + 'classes': ('collapse',) + }), + ) + + def has_add_permission(self, request): + """Ocultar botón de agregar - se crean automáticamente""" + return False + + def has_delete_permission(self, request, obj=None): + """No permitir eliminar permisos de asistente""" + return False + + def get_asistente_info(self, obj): + """Mostrar nombre del asistente""" + return obj.user_profile.full_name + get_asistente_info.short_description = 'Asistente' + + def get_numero_cuenta(self, obj): + """Mostrar número de cuenta""" + return obj.user_profile.account_number + get_numero_cuenta.short_description = 'Número de Cuenta' + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + """Filtrar el campo user_profile para mostrar solo asistentes""" + if db_field.name == "user_profile": + kwargs["queryset"] = UserProfile.objects.filter(user_type='assistant') + return super().formfield_for_foreignkey(db_field, request, **kwargs) + + def get_registros_realizados(self, obj): + """Muestra el total de registros realizados por este asistente""" + from attendance.models import Attendance + count = Attendance.objects.filter(registered_by=obj.user_profile).count() + return f"📊 {count} registros" + get_registros_realizados.short_description = 'Total de registros' + + def get_ultimos_registros(self, obj): + """Muestra los últimos 5 registros realizados por este asistente""" + from django.utils.html import format_html + from attendance.models import Attendance + + registros = Attendance.objects.filter( + registered_by=obj.user_profile + ).select_related('student', 'external_user', 'event').order_by('-timestamp')[:5] + + if not registros: + return "Sin registros" + + html = '' + html += '' + + for reg in registros: + attendee = reg.student.full_name if reg.student else reg.external_user.full_name + html += f''' + + + + + + ''' + + html += '
FechaAsistenteEvento
{reg.timestamp.strftime("%d/%m/%Y %H:%M")}{attendee}{reg.event.title[:30]}...
' + return format_html(html) + get_ultimos_registros.short_description = 'Últimos 5 registros' + + def ver_alumnos_registrados(self, obj): + """Botón para ver todos los alumnos registrados por este asistente""" + from django.utils.html import format_html + from django.urls import reverse + + url = reverse('admin:attendance_attendance_changelist') + f'?registered_by__id__exact={obj.user_profile.id}' + return format_html( + '📋 Ver Alumnos', + url + ) + ver_alumnos_registrados.short_description = 'Alumnos Registrados' + + def ver_reporte_registros(self, request, queryset): + """Acción para ver reporte detallado de registros de asistentes seleccionados""" + from django.shortcuts import render + from attendance.models import Attendance + from django.db.models import Count, Q + + asistentes_data = [] + + for asistente in queryset: + registros = Attendance.objects.filter(registered_by=asistente.user_profile) + + estudiantes = registros.filter(student__isnull=False).select_related('student', 'event').order_by('-timestamp') + externos = registros.filter(external_user__isnull=False).select_related('external_user', 'event').order_by('-timestamp') + + asistentes_data.append({ + 'asistente': asistente, + 'total': registros.count(), + 'estudiantes': estudiantes, + 'externos': externos, + 'total_estudiantes': estudiantes.count(), + 'total_externos': externos.count(), + }) + + context = { + 'asistentes_data': asistentes_data, + 'title': 'Reporte de Registros por Asistente', + } + + return render(request, 'admin/asistente_reporte.html', context) + + ver_reporte_registros.short_description = "📊 Ver reporte detallado de registros" + + +@admin.register(ExternalUser) +class ExternalUserAdmin(ImportExportModelAdmin): + resource_class = ExternalUserResource + list_display = ['account_number', 'full_name', 'get_status', 'get_approved_by', 'created_at'] + list_filter = ['status', 'created_at', 'approved_by'] + search_fields = ['full_name', 'account_number', 'approved_by__full_name'] + ordering = ['-created_at'] + readonly_fields = ['created_at', 'processed_at'] + date_hierarchy = 'created_at' + actions = ['export_selected_external_users'] + + fieldsets = ( + ('Información Personal', { + 'fields': ('account_number', 'full_name') + }), + ('Estado de Aprobación', { + 'fields': ('status', 'approved_by', 'processed_at', 'rejection_reason', 'created_at') + }), + ) + + def get_export_formats(self): + """Solo exportar (no importar usuarios externos)""" + from import_export.formats.base_formats import XLSX, CSV + return [XLSX, CSV] + + def has_import_permission(self, request): + """No permitir importar usuarios externos (se crean desde la app)""" + return False + + def export_selected_external_users(self, request, queryset): + """Acción para exportar usuarios externos seleccionados""" + resource = ExternalUserResource() + dataset = resource.export(queryset) + + from import_export.formats.base_formats import XLSX + xlsx_format = XLSX() + export_data = xlsx_format.export_data(dataset) + + response = HttpResponse( + export_data, + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + response['Content-Disposition'] = 'attachment; filename="usuarios_externos.xlsx"' + + self.message_user(request, f'Se exportaron {queryset.count()} usuarios externos.') + return response + + export_selected_external_users.short_description = "📊 Exportar usuarios externos seleccionados" + + def get_status(self, obj): + """Muestra el estado con iconos y colores""" + status_icons = { + 'pending': ('⏳', '#FFA500', 'Pendiente'), + 'approved': ('✅', '#28a745', 'Aprobado'), + 'rejected': ('❌', '#dc3545', 'Rechazado') + } + icon, color, text = status_icons.get(obj.status, ('?', '#666', obj.status)) + return format_html( + '{} {}', + color, icon, text + ) + get_status.short_description = 'Estado' + + def get_approved_by(self, obj): + """Muestra quién aprobó/rechazó al usuario""" + if obj.approved_by: + color = '#28a745' if obj.status == 'approved' else '#dc3545' + action = 'Aprobado por' if obj.status == 'approved' else 'Rechazado por' + return format_html( + '👤 {}
{}', + color, + obj.approved_by.full_name, + action + ) + return '-' + get_approved_by.short_description = 'Procesado por' + + +@admin.register(AuditLog) +class AuditLogAdmin(admin.ModelAdmin): + list_display = ['timestamp', 'category', 'severity', 'action', 'username', 'ip_address', 'success', 'message'] + list_filter = ['category', 'severity', 'action', 'success', 'timestamp'] + search_fields = ['username', 'ip_address', 'message', 'path'] + readonly_fields = ['timestamp', 'category', 'severity', 'action', 'user', 'username', + 'ip_address', 'user_agent', 'path', 'method', 'message', + 'details', 'success', 'status_code'] + date_hierarchy = 'timestamp' + + def has_add_permission(self, request): + # No permitir crear logs manualmente desde el admin + return False + + def has_change_permission(self, request, obj=None): + # No permitir editar logs (son inmutables) + return False + + def has_delete_permission(self, request, obj=None): + # Solo superusuarios pueden eliminar logs + return request.user.is_superuser + + +@admin.register(SystemConfiguration) +class SystemConfigurationAdmin(admin.ModelAdmin): + list_display = ['get_config_name', 'minimum_attendance_percentage', 'minutes_before_event', 'minutes_after_start', 'updated_at', 'updated_by'] + readonly_fields = ['updated_at', 'updated_by'] + + fieldsets = ( + ('Configuración de Asistencia', { + 'fields': ('minimum_attendance_percentage',), + 'description': '⚙️ Este porcentaje se aplica a TODOS los estudiantes del sistema. Define el porcentaje mínimo de asistencia requerido para obtener la constancia.' + }), + ('Configuración de Tiempos de Registro', { + 'fields': ('minutes_before_event', 'minutes_after_start'), + 'description': ''' + ⏱️ Tiempos de registro de asistencia (aplica a TODAS las ponencias):

+ • Minutos antes del evento: Tiempo permitido para comenzar a registrar asistencia antes del inicio programado.
+ Ejemplo: Si el evento inicia a las 10:00 AM y configuras 10 minutos, se podrá registrar desde las 9:50 AM.

+ • Minutos después del inicio: Tiempo límite después del inicio para registrar asistencia.
+ Ejemplo: Si el evento inicia a las 10:00 AM y configuras 25 minutos, el límite será a las 10:25 AM.

+ Estos valores se aplican automáticamente a todos los eventos del sistema. + ''' + }), + ('Información de Auditoría', { + 'fields': ('updated_at', 'updated_by'), + 'classes': ('collapse',) + }), + ) + + def get_config_name(self, obj): + return "Configuración Global del Sistema" + get_config_name.short_description = 'Configuración' + + def has_add_permission(self, request): + # Solo permitir una configuración + return not SystemConfiguration.objects.exists() + + def has_delete_permission(self, request, obj=None): + # No permitir eliminar la configuración + return False + + def save_model(self, request, obj, form, change): + # Registrar quién actualizó la configuración + try: + obj.updated_by = request.user.profile + except: + pass + super().save_model(request, obj, form, change) \ No newline at end of file diff --git a/backend/authentication/apps.py b/backend/authentication/apps.py new file mode 100644 index 0000000..8bab8df --- /dev/null +++ b/backend/authentication/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AuthenticationConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'authentication' diff --git a/backend/authentication/audit.py b/backend/authentication/audit.py new file mode 100644 index 0000000..72dbe16 --- /dev/null +++ b/backend/authentication/audit.py @@ -0,0 +1,222 @@ +""" +Sistema de auditoría para registrar eventos de seguridad +""" +from django.db import models +from django.contrib.auth.models import User +from django.utils import timezone +import json + + +class AuditLog(models.Model): + """ + Modelo para registrar eventos de auditoría del sistema + """ + + # Categorías de eventos + CATEGORY_CHOICES = [ + ('AUTH', 'Autenticación'), + ('ACCESS', 'Control de Acceso'), + ('DATA', 'Modificación de Datos'), + ('SECURITY', 'Evento de Seguridad'), + ('SYSTEM', 'Sistema'), + ] + + # Niveles de severidad + SEVERITY_CHOICES = [ + ('INFO', 'Información'), + ('WARNING', 'Advertencia'), + ('ERROR', 'Error'), + ('CRITICAL', 'Crítico'), + ] + + # Acciones comunes + ACTION_CHOICES = [ + ('LOGIN_SUCCESS', 'Login exitoso'), + ('LOGIN_FAILED', 'Login fallido'), + ('LOGOUT', 'Logout'), + ('TOKEN_REFRESH', 'Token refrescado'), + ('ACCESS_DENIED', 'Acceso denegado'), + ('RATE_LIMITED', 'Rate limit excedido'), + ('ATTENDANCE_CREATE', 'Asistencia registrada'), + ('ATTENDANCE_UPDATE', 'Asistencia actualizada'), + ('ATTENDANCE_DELETE', 'Asistencia eliminada'), + ('EVENT_CREATE', 'Evento creado'), + ('EVENT_UPDATE', 'Evento actualizado'), + ('EVENT_DELETE', 'Evento eliminado'), + ('EXTERNAL_USER_REGISTER', 'Usuario externo registrado'), + ('EXTERNAL_USER_APPROVE', 'Usuario externo aprobado'), + ('EXTERNAL_USER_REJECT', 'Usuario externo rechazado'), + ('PERMISSION_CHANGE', 'Cambio de permisos'), + ('DATA_EXPORT', 'Exportación de datos'), + ('OTHER', 'Otro'), + ] + + # Campos principales + timestamp = models.DateTimeField(default=timezone.now, db_index=True) + category = models.CharField(max_length=20, choices=CATEGORY_CHOICES, db_index=True) + severity = models.CharField(max_length=20, choices=SEVERITY_CHOICES, default='INFO', db_index=True) + action = models.CharField(max_length=50, choices=ACTION_CHOICES) + + # Usuario relacionado (puede ser None para eventos anónimos) + user = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='audit_logs' + ) + username = models.CharField(max_length=150, blank=True) # Backup si el usuario se elimina + + # Información de la solicitud + ip_address = models.GenericIPAddressField(null=True, blank=True) + user_agent = models.TextField(blank=True) + path = models.CharField(max_length=255, blank=True) + method = models.CharField(max_length=10, blank=True) + + # Detalles del evento + message = models.TextField() + details = models.JSONField(default=dict, blank=True) # Datos adicionales en JSON + + # Resultados + success = models.BooleanField(default=True) + status_code = models.IntegerField(null=True, blank=True) + + class Meta: + ordering = ['-timestamp'] + indexes = [ + models.Index(fields=['-timestamp', 'category']), + models.Index(fields=['-timestamp', 'severity']), + models.Index(fields=['-timestamp', 'user']), + models.Index(fields=['ip_address', '-timestamp']), + ] + verbose_name = 'Registro de Auditoría' + verbose_name_plural = 'Registros de Auditoría' + + def __str__(self): + return f"[{self.timestamp}] {self.get_category_display()} - {self.message}" + + @classmethod + def log(cls, category, action, message, request=None, user=None, + severity='INFO', success=True, status_code=None, **details): + """ + Método de conveniencia para crear logs de auditoría + + Args: + category: Categoría del evento + action: Acción realizada + message: Mensaje descriptivo + request: Objeto request de Django (opcional) + user: Usuario (opcional, se extrae del request si no se provee) + severity: Nivel de severidad + success: Si la operación fue exitosa + status_code: Código HTTP de respuesta + **details: Datos adicionales a guardar en JSON + """ + # Extraer información del request + ip_address = None + user_agent = '' + path = '' + method = '' + + if request: + # Obtener IP real (considerando proxies) + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip_address = x_forwarded_for.split(',')[0].strip() + else: + ip_address = request.META.get('REMOTE_ADDR') + + user_agent = request.META.get('HTTP_USER_AGENT', '')[:500] # Limitar tamaño + path = request.path + method = request.method + + # Si no se proveyó usuario, intentar obtenerlo del request + if not user and hasattr(request, 'user') and request.user.is_authenticated: + user = request.user + + # Obtener username + username = user.username if user else 'Anonymous' + + # Sanitizar detalles (no guardar datos sensibles) + sanitized_details = cls._sanitize_details(details) + + # Crear el log + return cls.objects.create( + category=category, + severity=severity, + action=action, + user=user, + username=username, + ip_address=ip_address, + user_agent=user_agent, + path=path, + method=method, + message=message, + details=sanitized_details, + success=success, + status_code=status_code, + ) + + @staticmethod + def _sanitize_details(details): + """ + Eliminar datos sensibles de los detalles antes de guardar + """ + sensitive_keys = ['password', 'token', 'secret', 'key', 'authorization'] + sanitized = {} + + for key, value in details.items(): + # Verificar si la clave contiene información sensible + if any(sensitive in key.lower() for sensitive in sensitive_keys): + sanitized[key] = '***REDACTED***' + elif isinstance(value, dict): + sanitized[key] = AuditLog._sanitize_details(value) + else: + # Limitar tamaño de strings + if isinstance(value, str) and len(value) > 1000: + sanitized[key] = value[:1000] + '...[truncated]' + else: + sanitized[key] = value + + return sanitized + + @classmethod + def get_user_activity(cls, user, days=30): + """ + Obtener actividad de un usuario en los últimos N días + """ + from datetime import timedelta + cutoff = timezone.now() - timedelta(days=days) + return cls.objects.filter(user=user, timestamp__gte=cutoff) + + @classmethod + def get_failed_logins(cls, ip_address=None, hours=24): + """ + Obtener intentos fallidos de login + """ + from datetime import timedelta + cutoff = timezone.now() - timedelta(hours=hours) + + query = cls.objects.filter( + action='LOGIN_FAILED', + timestamp__gte=cutoff + ) + + if ip_address: + query = query.filter(ip_address=ip_address) + + return query + + @classmethod + def get_security_events(cls, severity='WARNING', hours=24): + """ + Obtener eventos de seguridad recientes + """ + from datetime import timedelta + cutoff = timezone.now() - timedelta(hours=hours) + + return cls.objects.filter( + category='SECURITY', + severity__in=[severity, 'ERROR', 'CRITICAL'], + timestamp__gte=cutoff + ) \ No newline at end of file diff --git a/backend/authentication/migrations/0001_initial.py b/backend/authentication/migrations/0001_initial.py new file mode 100644 index 0000000..44df097 --- /dev/null +++ b/backend/authentication/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2.1 on 2025-09-23 04:57 + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('account_number', models.CharField(max_length=7, unique=True, validators=[django.core.validators.RegexValidator(message='El número de cuenta debe tener exactamente 7 dígitos.', regex='^\\d{7}$')], verbose_name='Número de cuenta')), + ('user_type', models.CharField(choices=[('student', 'Estudiante'), ('teacher', 'Maestro')], max_length=10, verbose_name='Tipo de usuario')), + ('full_name', models.CharField(max_length=200, verbose_name='Nombre completo')), + ('career', models.CharField(blank=True, max_length=100, null=True, verbose_name='Carrera')), + ('semester', models.IntegerField(blank=True, null=True, verbose_name='Semestre')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Teacher', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('minimum_attendance_percentage', models.FloatField(default=80.0)), + ('can_manage_events', models.BooleanField(default=True)), + ('user_profile', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='authentication.userprofile')), + ], + ), + ] diff --git a/backend/authentication/migrations/0002_auditlog.py b/backend/authentication/migrations/0002_auditlog.py new file mode 100644 index 0000000..b538c7a --- /dev/null +++ b/backend/authentication/migrations/0002_auditlog.py @@ -0,0 +1,43 @@ +# Generated by Django 5.2.6 on 2025-09-30 07:53 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='AuditLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField(db_index=True, default=django.utils.timezone.now)), + ('category', models.CharField(choices=[('AUTH', 'Autenticación'), ('ACCESS', 'Control de Acceso'), ('DATA', 'Modificación de Datos'), ('SECURITY', 'Evento de Seguridad'), ('SYSTEM', 'Sistema')], db_index=True, max_length=20)), + ('severity', models.CharField(choices=[('INFO', 'Información'), ('WARNING', 'Advertencia'), ('ERROR', 'Error'), ('CRITICAL', 'Crítico')], db_index=True, default='INFO', max_length=20)), + ('action', models.CharField(choices=[('LOGIN_SUCCESS', 'Login exitoso'), ('LOGIN_FAILED', 'Login fallido'), ('LOGOUT', 'Logout'), ('TOKEN_REFRESH', 'Token refrescado'), ('ACCESS_DENIED', 'Acceso denegado'), ('RATE_LIMITED', 'Rate limit excedido'), ('ATTENDANCE_CREATE', 'Asistencia registrada'), ('ATTENDANCE_UPDATE', 'Asistencia actualizada'), ('ATTENDANCE_DELETE', 'Asistencia eliminada'), ('EVENT_CREATE', 'Evento creado'), ('EVENT_UPDATE', 'Evento actualizado'), ('EVENT_DELETE', 'Evento eliminado'), ('EXTERNAL_USER_REGISTER', 'Usuario externo registrado'), ('EXTERNAL_USER_APPROVE', 'Usuario externo aprobado'), ('EXTERNAL_USER_REJECT', 'Usuario externo rechazado'), ('PERMISSION_CHANGE', 'Cambio de permisos'), ('DATA_EXPORT', 'Exportación de datos'), ('OTHER', 'Otro')], max_length=50)), + ('username', models.CharField(blank=True, max_length=150)), + ('ip_address', models.GenericIPAddressField(blank=True, null=True)), + ('user_agent', models.TextField(blank=True)), + ('path', models.CharField(blank=True, max_length=255)), + ('method', models.CharField(blank=True, max_length=10)), + ('message', models.TextField()), + ('details', models.JSONField(blank=True, default=dict)), + ('success', models.BooleanField(default=True)), + ('status_code', models.IntegerField(blank=True, null=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='audit_logs', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Registro de Auditoría', + 'verbose_name_plural': 'Registros de Auditoría', + 'ordering': ['-timestamp'], + 'indexes': [models.Index(fields=['-timestamp', 'category'], name='authenticat_timesta_506bb9_idx'), models.Index(fields=['-timestamp', 'severity'], name='authenticat_timesta_5cf4a0_idx'), models.Index(fields=['-timestamp', 'user'], name='authenticat_timesta_721c38_idx'), models.Index(fields=['ip_address', '-timestamp'], name='authenticat_ip_addr_d5daa5_idx')], + }, + ), + ] diff --git a/backend/authentication/migrations/0003_rename_teacher_assistant_alter_userprofile_user_type.py b/backend/authentication/migrations/0003_rename_teacher_assistant_alter_userprofile_user_type.py new file mode 100644 index 0000000..1f6fc7d --- /dev/null +++ b/backend/authentication/migrations/0003_rename_teacher_assistant_alter_userprofile_user_type.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.6 on 2025-10-01 02:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0002_auditlog'), + ] + + operations = [ + migrations.RenameModel( + old_name='Teacher', + new_name='Assistant', + ), + migrations.AlterField( + model_name='userprofile', + name='user_type', + field=models.CharField(choices=[('student', 'Estudiante'), ('assistant', 'Asistente')], max_length=10, verbose_name='Tipo de usuario'), + ), + ] diff --git a/backend/authentication/migrations/0004_rename_assistant_asistente.py b/backend/authentication/migrations/0004_rename_assistant_asistente.py new file mode 100644 index 0000000..ffce762 --- /dev/null +++ b/backend/authentication/migrations/0004_rename_assistant_asistente.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.6 on 2025-10-01 03:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0003_rename_teacher_assistant_alter_userprofile_user_type'), + ] + + operations = [ + migrations.RenameModel( + old_name='Assistant', + new_name='Asistente', + ), + ] diff --git a/backend/authentication/migrations/0005_remove_userprofile_career_and_more.py b/backend/authentication/migrations/0005_remove_userprofile_career_and_more.py new file mode 100644 index 0000000..0d658c5 --- /dev/null +++ b/backend/authentication/migrations/0005_remove_userprofile_career_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.6 on 2025-10-01 19:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0004_rename_assistant_asistente'), + ] + + operations = [ + migrations.RemoveField( + model_name='userprofile', + name='career', + ), + migrations.RemoveField( + model_name='userprofile', + name='semester', + ), + ] diff --git a/backend/authentication/migrations/0006_externaluser.py b/backend/authentication/migrations/0006_externaluser.py new file mode 100644 index 0000000..2814bc2 --- /dev/null +++ b/backend/authentication/migrations/0006_externaluser.py @@ -0,0 +1,32 @@ +# Generated by Django 5.2.6 on 2025-10-02 00:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0005_remove_userprofile_career_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ExternalUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('full_name', models.CharField(max_length=200, verbose_name='Nombre completo')), + ('account_number', models.CharField(max_length=7, unique=True, verbose_name='Número de cuenta')), + ('status', models.CharField(choices=[('pending', 'Pendiente'), ('approved', 'Aprobado'), ('rejected', 'Rechazado')], default='approved', max_length=10, verbose_name='Estado de aprobación')), + ('rejection_reason', models.TextField(blank=True, null=True, verbose_name='Motivo de rechazo')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('processed_at', models.DateTimeField(blank=True, null=True)), + ('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='authentication.userprofile', verbose_name='Aprobado por')), + ], + options={ + 'verbose_name': 'Usuario Externo', + 'verbose_name_plural': 'Usuarios Externos', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/backend/authentication/migrations/0007_migrate_externaluser_data.py b/backend/authentication/migrations/0007_migrate_externaluser_data.py new file mode 100644 index 0000000..0ad0bb7 --- /dev/null +++ b/backend/authentication/migrations/0007_migrate_externaluser_data.py @@ -0,0 +1,54 @@ +# Generated by Django 5.2.6 on 2025-10-02 00:21 + +from django.db import migrations + + +def migrate_external_users(apps, schema_editor): + """Migrar datos de events.ExternalUser a authentication.ExternalUser""" + # Obtener los modelos de las versiones históricas + EventsExternalUser = apps.get_model('events', 'ExternalUser') + AuthExternalUser = apps.get_model('authentication', 'ExternalUser') + + # Copiar todos los usuarios externos + for old_user in EventsExternalUser.objects.all(): + AuthExternalUser.objects.create( + id=old_user.id, + full_name=old_user.full_name, + account_number=old_user.account_number, + status=old_user.status, + approved_by=old_user.approved_by, + rejection_reason=old_user.rejection_reason, + created_at=old_user.created_at, + processed_at=old_user.processed_at, + ) + + +def reverse_migrate_external_users(apps, schema_editor): + """Revertir migración de datos""" + AuthExternalUser = apps.get_model('authentication', 'ExternalUser') + EventsExternalUser = apps.get_model('events', 'ExternalUser') + + # Copiar de vuelta + for new_user in AuthExternalUser.objects.all(): + EventsExternalUser.objects.create( + id=new_user.id, + full_name=new_user.full_name, + account_number=new_user.account_number, + status=new_user.status, + approved_by=new_user.approved_by, + rejection_reason=new_user.rejection_reason, + created_at=new_user.created_at, + processed_at=new_user.processed_at, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0006_externaluser'), + ('events', '0004_auto_20251001_1400'), + ] + + operations = [ + migrations.RunPython(migrate_external_users, reverse_migrate_external_users), + ] diff --git a/backend/authentication/migrations/0008_remove_asistente_minimum_attendance_percentage_and_more.py b/backend/authentication/migrations/0008_remove_asistente_minimum_attendance_percentage_and_more.py new file mode 100644 index 0000000..e580ad4 --- /dev/null +++ b/backend/authentication/migrations/0008_remove_asistente_minimum_attendance_percentage_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 5.2.6 on 2025-10-02 04:55 + +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0007_migrate_externaluser_data'), + ] + + operations = [ + migrations.RemoveField( + model_name='asistente', + name='minimum_attendance_percentage', + ), + migrations.CreateModel( + name='SystemConfiguration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('minimum_attendance_percentage', models.FloatField(default=80.0, help_text='Porcentaje mínimo de asistencia requerido para obtener constancia (0-100)', validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(100.0)], verbose_name='Porcentaje mínimo de asistencia')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Última actualización')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='authentication.userprofile', verbose_name='Actualizado por')), + ], + options={ + 'verbose_name': 'Configuración del Sistema', + 'verbose_name_plural': 'Configuración del Sistema', + }, + ), + ] diff --git a/backend/authentication/migrations/0009_assistantprofile_student.py b/backend/authentication/migrations/0009_assistantprofile_student.py new file mode 100644 index 0000000..4f540b1 --- /dev/null +++ b/backend/authentication/migrations/0009_assistantprofile_student.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2.6 on 2025-10-03 18:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0008_remove_asistente_minimum_attendance_percentage_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='AssistantProfile', + fields=[ + ], + options={ + 'verbose_name': 'Asistente (Perfil)', + 'verbose_name_plural': 'Asistentes (Perfiles)', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('authentication.userprofile',), + ), + migrations.CreateModel( + name='Student', + fields=[ + ], + options={ + 'verbose_name': 'Estudiante', + 'verbose_name_plural': 'Estudiantes', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('authentication.userprofile',), + ), + ] diff --git a/backend/authentication/migrations/0009_assistantprofile_student_alter_asistente_options_and_more.py b/backend/authentication/migrations/0009_assistantprofile_student_alter_asistente_options_and_more.py new file mode 100644 index 0000000..29dde5f --- /dev/null +++ b/backend/authentication/migrations/0009_assistantprofile_student_alter_asistente_options_and_more.py @@ -0,0 +1,54 @@ +# Generated by Django 5.2.6 on 2025-10-04 03:46 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0008_remove_asistente_minimum_attendance_percentage_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='AssistantProfile', + fields=[ + ], + options={ + 'verbose_name': 'Asistente (Perfil)', + 'verbose_name_plural': 'Asistentes (Perfiles)', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('authentication.userprofile',), + ), + migrations.CreateModel( + name='Student', + fields=[ + ], + options={ + 'verbose_name': 'Estudiante', + 'verbose_name_plural': 'Estudiantes', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('authentication.userprofile',), + ), + migrations.AlterModelOptions( + name='asistente', + options={'verbose_name': 'Permiso de Asistente', 'verbose_name_plural': 'Permisos de Asistentes'}, + ), + migrations.AlterField( + model_name='asistente', + name='can_manage_events', + field=models.BooleanField(default=True, verbose_name='Puede gestionar eventos'), + ), + migrations.AlterField( + model_name='asistente', + name='user_profile', + field=models.OneToOneField(limit_choices_to={'user_type': 'assistant'}, on_delete=django.db.models.deletion.CASCADE, to='authentication.userprofile'), + ), + ] diff --git a/backend/authentication/migrations/0010_merge_20251003_2150.py b/backend/authentication/migrations/0010_merge_20251003_2150.py new file mode 100644 index 0000000..b119320 --- /dev/null +++ b/backend/authentication/migrations/0010_merge_20251003_2150.py @@ -0,0 +1,14 @@ +# Generated by Django 5.2.6 on 2025-10-04 03:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0009_assistantprofile_student'), + ('authentication', '0009_assistantprofile_student_alter_asistente_options_and_more'), + ] + + operations = [ + ] diff --git a/backend/authentication/migrations/0011_auto_create_assistant_permissions.py b/backend/authentication/migrations/0011_auto_create_assistant_permissions.py new file mode 100644 index 0000000..86f5a9d --- /dev/null +++ b/backend/authentication/migrations/0011_auto_create_assistant_permissions.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.6 on 2025-10-08 19:13 + +from django.db import migrations + + +def create_assistant_permissions(apps, schema_editor): + """Crear automáticamente permisos para todos los asistentes existentes""" + UserProfile = apps.get_model('authentication', 'UserProfile') + Asistente = apps.get_model('authentication', 'Asistente') + + # Obtener todos los UserProfile de tipo 'assistant' + assistants = UserProfile.objects.filter(user_type='assistant') + + # Crear registro de Asistente para cada uno si no existe + for assistant in assistants: + Asistente.objects.get_or_create( + user_profile=assistant, + defaults={'can_manage_events': True} + ) + + +def reverse_migration(apps, schema_editor): + """No hacer nada al revertir - mantener los permisos creados""" + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0010_merge_20251003_2150'), + ] + + operations = [ + migrations.RunPython(create_assistant_permissions, reverse_migration), + ] diff --git a/backend/authentication/migrations/0012_systemconfiguration_minutes_after_start_and_more.py b/backend/authentication/migrations/0012_systemconfiguration_minutes_after_start_and_more.py new file mode 100644 index 0000000..81ba08a --- /dev/null +++ b/backend/authentication/migrations/0012_systemconfiguration_minutes_after_start_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.6 on 2025-10-09 02:43 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0011_auto_create_assistant_permissions'), + ] + + operations = [ + migrations.AddField( + model_name='systemconfiguration', + name='minutes_after_start', + field=models.IntegerField(default=25, help_text='Tiempo límite en minutos después del inicio del evento para registrar asistencia (0-120)', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(120)], verbose_name='Minutos después del inicio'), + ), + migrations.AddField( + model_name='systemconfiguration', + name='minutes_before_event', + field=models.IntegerField(default=10, help_text='Tiempo en minutos antes del inicio del evento para permitir el registro de asistencia (0-60)', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(60)], verbose_name='Minutos antes del evento'), + ), + ] diff --git a/backend/authentication/migrations/__init__.py b/backend/authentication/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/authentication/models.py b/backend/authentication/models.py new file mode 100644 index 0000000..060a349 --- /dev/null +++ b/backend/authentication/models.py @@ -0,0 +1,217 @@ +from django.contrib.auth.models import User +from django.db import models +from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator +from django.utils import timezone +from django.core.exceptions import ValidationError + +class UserProfile(models.Model): + USER_TYPES = ( + ('student', 'Estudiante'), + ('assistant', 'Asistente'), + ) + + user = models.OneToOneField(User, on_delete=models.CASCADE) + account_number = models.CharField( + max_length=7, + unique=True, + validators=[RegexValidator( + regex=r'^\d{7}$', + message='El número de cuenta debe tener exactamente 7 dígitos.' + )], + verbose_name="Número de cuenta" + ) + user_type = models.CharField( + max_length=10, + choices=USER_TYPES, + blank=False, + null=False, + verbose_name="Tipo de usuario" + ) + full_name = models.CharField( + max_length=200, + verbose_name="Nombre completo" + ) + + def __str__(self): + return f"{self.account_number} - {self.full_name}" + +class Asistente(models.Model): + user_profile = models.OneToOneField(UserProfile, on_delete=models.CASCADE, limit_choices_to={'user_type': 'assistant'}) + can_manage_events = models.BooleanField(default=True, verbose_name="Puede gestionar eventos") + + class Meta: + verbose_name = "Permiso de Asistente" + verbose_name_plural = "Permisos de Asistentes" + + def clean(self): + """Validar que el user_profile sea de tipo assistant""" + from django.core.exceptions import ValidationError + if self.user_profile and self.user_profile.user_type != 'assistant': + raise ValidationError({ + 'user_profile': 'Solo se pueden asignar permisos a usuarios de tipo Asistente. Este usuario es un Estudiante.' + }) + + def save(self, *args, **kwargs): + self.full_clean() + super().save(*args, **kwargs) + + def __str__(self): + return f"Permisos: {self.user_profile.full_name}" + +class ExternalUser(models.Model): + """Usuarios externos""" + APPROVAL_STATUS = ( + ('pending', 'Pendiente'), + ('approved', 'Aprobado'), + ('rejected', 'Rechazado'), + ) + + full_name = models.CharField(max_length=200, verbose_name="Nombre completo") + account_number = models.CharField( + max_length=7, + unique=True, + verbose_name="Número de cuenta" + ) + status = models.CharField( + max_length=10, + choices=APPROVAL_STATUS, + default='approved', + verbose_name="Estado de aprobación" + ) + approved_by = models.ForeignKey( + UserProfile, + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name="Aprobado por" + ) + rejection_reason = models.TextField( + blank=True, + null=True, + verbose_name="Motivo de rechazo" + ) + created_at = models.DateTimeField(auto_now_add=True) + processed_at = models.DateTimeField(null=True, blank=True) + + class Meta: + verbose_name = "Usuario Externo" + verbose_name_plural = "Usuarios Externos" + ordering = ['-created_at'] + + @property + def is_approved(self): + return self.status == 'approved' + + @property + def is_pending(self): + return self.status == 'pending' + + def approve(self, approved_by_user): + """Aprobar usuario externo""" + self.status = 'approved' + self.approved_by = approved_by_user + self.processed_at = timezone.now() + self.save() + + def reject(self, rejected_by_user, reason=""): + """Rechazar usuario externo""" + self.status = 'rejected' + self.approved_by = rejected_by_user + self.rejection_reason = reason + self.processed_at = timezone.now() + self.save() + + def __str__(self): + status_icons = { + 'pending': '⏳', + 'approved': '✅', + 'rejected': '❌' + } + icon = status_icons.get(self.status, '?') + return f"{icon} {self.full_name} - {self.account_number}" + + +class SystemConfiguration(models.Model): + """Configuración global del sistema""" + minimum_attendance_percentage = models.FloatField( + default=80.0, + validators=[MinValueValidator(0.0), MaxValueValidator(100.0)], + verbose_name="Porcentaje mínimo de asistencia", + help_text="Porcentaje mínimo de asistencia requerido para obtener constancia (0-100)" + ) + + # Configuración de tiempos de registro + minutes_before_event = models.IntegerField( + default=10, + validators=[MinValueValidator(0), MaxValueValidator(60)], + verbose_name="Minutos antes del evento", + help_text="Tiempo en minutos antes del inicio del evento para permitir el registro de asistencia (0-60)" + ) + + minutes_after_start = models.IntegerField( + default=25, + validators=[MinValueValidator(0), MaxValueValidator(120)], + verbose_name="Minutos después del inicio", + help_text="Tiempo límite en minutos después del inicio del evento para registrar asistencia (0-120)" + ) + + updated_at = models.DateTimeField(auto_now=True, verbose_name="Última actualización") + updated_by = models.ForeignKey( + UserProfile, + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name="Actualizado por" + ) + + class Meta: + verbose_name = "Configuración del Sistema" + verbose_name_plural = "Configuración del Sistema" + + def save(self, *args, **kwargs): + # Asegurar que solo exista una instancia de configuración + if not self.pk and SystemConfiguration.objects.exists(): + raise ValidationError("Solo puede existir una configuración del sistema.") + super().save(*args, **kwargs) + + @classmethod + def get_config(cls): + """Obtener o crear la configuración del sistema""" + config, created = cls.objects.get_or_create( + pk=1, + defaults={ + 'minimum_attendance_percentage': 80.0, + 'minutes_before_event': 10, + 'minutes_after_start': 25 + } + ) + return config + + def __str__(self): + return f"Configuración del Sistema - Asistencia mínima: {self.minimum_attendance_percentage}%" + + +# ===== PROXY MODELS PARA SEPARAR ESTUDIANTES Y ASISTENTES EN EL ADMIN ===== + +class Student(UserProfile): + """Proxy model para estudiantes""" + class Meta: + proxy = True + verbose_name = "Estudiante" + verbose_name_plural = "Estudiantes" + + def save(self, *args, **kwargs): + self.user_type = 'student' + super().save(*args, **kwargs) + + +class AssistantProfile(UserProfile): + """Proxy model para asistentes""" + class Meta: + proxy = True + verbose_name = "Asistente (Perfil)" + verbose_name_plural = "Asistentes (Perfiles)" + + def save(self, *args, **kwargs): + self.user_type = 'assistant' + super().save(*args, **kwargs) \ No newline at end of file diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py new file mode 100644 index 0000000..5c3d702 --- /dev/null +++ b/backend/authentication/serializers.py @@ -0,0 +1,64 @@ +from rest_framework import serializers +from django.contrib.auth import authenticate +from django.contrib.auth.models import User +from .models import UserProfile +import re + +class UserProfileSerializer(serializers.ModelSerializer): + class Meta: + model = UserProfile + fields = ['account_number', 'user_type', 'full_name'] + +class UserSerializer(serializers.ModelSerializer): + profile = UserProfileSerializer(source='userprofile', read_only=True) + + class Meta: + model = User + fields = ['id', 'username', 'email', 'first_name', 'last_name', 'profile', 'is_staff', 'is_superuser'] + +class LoginSerializer(serializers.Serializer): + account_number = serializers.CharField(max_length=20) + + def validate_account_number(self, value): + # Validar formato para números de cuenta (7 dígitos) + if not re.match(r'^\d{7}$', value): + raise serializers.ValidationError('El número de cuenta debe tener exactamente 7 dígitos.') + return value + + def validate(self, data): + account_number = data.get('account_number') + + if not account_number: + raise serializers.ValidationError('Debe incluir número de cuenta.') + + # Primero buscar en usuarios regulares (estudiantes/asistentes) + try: + profile = UserProfile.objects.get(account_number=account_number) + user = profile.user + if not user.is_active: + raise serializers.ValidationError('Esta cuenta está desactivada.') + data['user'] = user + return data + except UserProfile.DoesNotExist: + pass + + # Si no es usuario regular, buscar en usuarios externos + from authentication.models import ExternalUser + try: + external_user = ExternalUser.objects.get(account_number=account_number) + if external_user.status != 'approved': + raise serializers.ValidationError('Usuario externo no aprobado.') + + # Crear o buscar usuario de Django asociado + user, created = User.objects.get_or_create( + username=f'ext_{account_number}', + defaults={'first_name': external_user.full_name} + ) + data['user'] = user + data['external_user'] = external_user + return data + + except ExternalUser.DoesNotExist: + raise serializers.ValidationError('Número de cuenta no encontrado.') + + return data \ No newline at end of file diff --git a/backend/authentication/urls.py b/backend/authentication/urls.py new file mode 100644 index 0000000..b812bc9 --- /dev/null +++ b/backend/authentication/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('login/', views.login_view, name='login'), + path('logout/', views.logout_view, name='logout'), + path('profile/', views.user_profile, name='profile'), + path('check-auth/', views.check_auth_status, name='check_auth'), + path('token/refresh/', views.refresh_token, name='token_refresh'), + path('token/verify/', views.verify_token, name='token_verify'), + path('system-config/', views.get_system_config, name='system_config'), +] \ No newline at end of file diff --git a/backend/authentication/views.py b/backend/authentication/views.py new file mode 100644 index 0000000..fe73fb9 --- /dev/null +++ b/backend/authentication/views.py @@ -0,0 +1,136 @@ +from rest_framework import status +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.response import Response +from django.contrib.auth import login, logout +from django.contrib.auth.models import User +from rest_framework_simplejwt.tokens import RefreshToken +from django_ratelimit.decorators import ratelimit +from django_ratelimit.exceptions import Ratelimited +from .models import UserProfile, Asistente +from .serializers import LoginSerializer, UserSerializer +from .audit import AuditLog + +@api_view(['POST']) +@permission_classes([AllowAny]) +@ratelimit(key='ip', rate='5/m', method='POST', block=True) +def login_view(request): + """Login con rate limiting: 5 intentos por minuto por IP""" + serializer = LoginSerializer(data=request.data) + account_number = request.data.get('account_number', 'unknown') + + if serializer.is_valid(): + user = serializer.validated_data['user'] + + # Generar tokens JWT + refresh = RefreshToken.for_user(user) + + # Log exitoso + AuditLog.log( + category='AUTH', + action='LOGIN_SUCCESS', + message=f'Login exitoso para cuenta {account_number}', + request=request, + user=user, + severity='INFO', + success=True, + status_code=200, + account_number=account_number + ) + + return Response({ + 'message': 'Login exitoso', + 'user': UserSerializer(user).data, + 'tokens': { + 'access': str(refresh.access_token), + 'refresh': str(refresh) + } + }) + + # Log fallido + AuditLog.log( + category='AUTH', + action='LOGIN_FAILED', + message=f'Intento de login fallido para cuenta {account_number}', + request=request, + severity='WARNING', + success=False, + status_code=400, + account_number=account_number, + errors=str(serializer.errors) + ) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def logout_view(request): + # Log logout + AuditLog.log( + category='AUTH', + action='LOGOUT', + message=f'Logout de usuario {request.user.username}', + request=request, + user=request.user, + severity='INFO', + success=True, + status_code=200 + ) + + logout(request) + return Response({'message': 'Logout exitoso'}) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def user_profile(request): + return Response(UserSerializer(request.user).data) + +@api_view(['GET']) +@permission_classes([AllowAny]) +@ratelimit(key='ip', rate='30/m', method='GET', block=True) +def check_auth_status(request): + """Verificar estado de autenticación: 30 consultas por minuto por IP""" + if request.user.is_authenticated: + return Response({ + 'is_authenticated': True, + 'user': UserSerializer(request.user).data + }) + return Response({'is_authenticated': False}) + +@api_view(['POST']) +@permission_classes([AllowAny]) +@ratelimit(key='ip', rate='10/m', method='POST', block=True) +def refresh_token(request): + """Refrescar access token: 10 intentos por minuto por IP""" + from rest_framework_simplejwt.exceptions import TokenError, InvalidToken + from rest_framework_simplejwt.serializers import TokenRefreshSerializer + + serializer = TokenRefreshSerializer(data=request.data) + try: + serializer.is_valid(raise_exception=True) + return Response(serializer.validated_data, status=status.HTTP_200_OK) + except TokenError as e: + return Response({'error': 'Token inválido o expirado'}, status=status.HTTP_401_UNAUTHORIZED) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def verify_token(request): + """Verificar si el token actual es válido""" + return Response({ + 'valid': True, + 'user': UserSerializer(request.user).data + }) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_system_config(request): + """Obtener la configuración global del sistema""" + from .models import SystemConfiguration + + config = SystemConfiguration.get_config() + + return Response({ + 'minimum_attendance_percentage': config.minimum_attendance_percentage, + 'minutes_before_event': config.minutes_before_event, + 'minutes_after_start': config.minutes_after_start + }) \ No newline at end of file diff --git a/backend/events/__init__.py b/backend/events/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/events/admin.py b/backend/events/admin.py new file mode 100644 index 0000000..9b1af14 --- /dev/null +++ b/backend/events/admin.py @@ -0,0 +1,67 @@ +from django.contrib import admin +from django.utils.html import format_html +from import_export import resources +from import_export.admin import ImportExportModelAdmin +from .models import Event + + +class EventResource(resources.ModelResource): + """Recurso para importar/exportar eventos""" + + class Meta: + model = Event + fields = ('title', 'speaker', 'date', 'start_time', 'end_time', 'event_type', + 'modality', 'location', 'description', 'is_active') + import_id_fields = ['title', 'date', 'start_time'] + skip_unchanged = True + report_skipped = True + + def before_import_row(self, row, **kwargs): + """Limpiar datos antes de importar""" + row['title'] = str(row.get('title', '')).strip() + row['speaker'] = str(row.get('speaker', '')).strip() + row['location'] = str(row.get('location', '')).strip() + + + +@admin.register(Event) +class EventAdmin(ImportExportModelAdmin): + resource_class = EventResource + list_display = ['title', 'speaker', 'date', 'start_time', 'modality', 'location', 'is_active'] + list_filter = ['event_type', 'modality', 'date', 'is_active'] + search_fields = ['title', 'speaker', 'location'] + date_hierarchy = 'date' + ordering = ['date', 'start_time'] + readonly_fields = ['created_at'] + actions = ['export_selected_events'] + + def get_import_formats(self): + """Formatos permitidos para importar""" + from import_export.formats.base_formats import XLSX, CSV + return [XLSX, CSV] + + def get_export_formats(self): + """Formatos permitidos para exportar""" + from import_export.formats.base_formats import XLSX, CSV + return [XLSX, CSV] + + def export_selected_events(self, request, queryset): + """Acción para exportar eventos seleccionados""" + from django.http import HttpResponse + resource = EventResource() + dataset = resource.export(queryset) + + from import_export.formats.base_formats import XLSX + xlsx_format = XLSX() + export_data = xlsx_format.export_data(dataset) + + response = HttpResponse( + export_data, + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + response['Content-Disposition'] = 'attachment; filename="eventos.xlsx"' + + self.message_user(request, f'Se exportaron {queryset.count()} eventos.') + return response + + export_selected_events.short_description = "📊 Exportar eventos seleccionados" \ No newline at end of file diff --git a/backend/events/apps.py b/backend/events/apps.py new file mode 100644 index 0000000..20f48f2 --- /dev/null +++ b/backend/events/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class EventsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'events' diff --git a/backend/events/migrations/0001_initial.py b/backend/events/migrations/0001_initial.py new file mode 100644 index 0000000..8b71e98 --- /dev/null +++ b/backend/events/migrations/0001_initial.py @@ -0,0 +1,66 @@ +# Generated by Django 5.2.6 on 2025-09-24 00:32 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('authentication', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200, verbose_name='Título de la ponencia')), + ('description', models.TextField(verbose_name='Descripción')), + ('event_type', models.CharField(choices=[('conference', 'Conferencia'), ('workshop', 'Taller'), ('panel', 'Mesa Redonda'), ('seminar', 'Seminario')], default='conference', max_length=20, verbose_name='Tipo de evento')), + ('modality', models.CharField(choices=[('presencial', 'Presencial'), ('online', 'En línea'), ('hybrid', 'Híbrido')], default='presencial', max_length=15, verbose_name='Modalidad')), + ('speaker', models.CharField(max_length=200, verbose_name='Ponente')), + ('date', models.DateField(verbose_name='Fecha')), + ('start_time', models.TimeField(verbose_name='Hora de inicio')), + ('end_time', models.TimeField(verbose_name='Hora de fin')), + ('location', models.CharField(help_text='Para eventos presenciales: aula/salón. Para eventos en línea: enlace o plataforma', max_length=200, verbose_name='Ubicación/Plataforma')), + ('max_capacity', models.IntegerField(default=100, verbose_name='Capacidad máxima')), + ('is_active', models.BooleanField(default=True, verbose_name='Evento activo')), + ('requires_registration', models.BooleanField(default=False, verbose_name='Requiere registro previo')), + ('meeting_link', models.URLField(blank=True, help_text='Para eventos en línea o híbridos', null=True, verbose_name='Enlace de reunión')), + ('meeting_id', models.CharField(blank=True, help_text='Código de sala/reunión', max_length=50, null=True, verbose_name='ID de reunión')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('created_by', models.ForeignKey(limit_choices_to={'user_type': 'teacher'}, on_delete=django.db.models.deletion.CASCADE, to='authentication.userprofile', verbose_name='Creado por')), + ], + options={ + 'verbose_name': 'Evento/Ponencia', + 'verbose_name_plural': 'Eventos/Ponencias', + 'ordering': ['date', 'start_time'], + }, + ), + migrations.CreateModel( + name='ExternalUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('full_name', models.CharField(max_length=200, verbose_name='Nombre completo')), + ('email', models.EmailField(max_length=254, verbose_name='Correo electrónico')), + ('phone', models.CharField(blank=True, max_length=15, null=True, verbose_name='Teléfono')), + ('institution', models.CharField(max_length=200, verbose_name='Institución de procedencia')), + ('position', models.CharField(blank=True, max_length=100, null=True, verbose_name='Cargo/Posición')), + ('reason', models.TextField(verbose_name='Motivo de asistencia')), + ('temporary_id', models.CharField(max_length=20, unique=True, verbose_name='ID temporal')), + ('status', models.CharField(choices=[('pending', 'Pendiente'), ('approved', 'Aprobado'), ('rejected', 'Rechazado')], default='pending', max_length=10, verbose_name='Estado de aprobación')), + ('rejection_reason', models.TextField(blank=True, null=True, verbose_name='Motivo de rechazo')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('processed_at', models.DateTimeField(blank=True, null=True)), + ('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='authentication.userprofile', verbose_name='Aprobado por')), + ], + options={ + 'verbose_name': 'Usuario Externo', + 'verbose_name_plural': 'Usuarios Externos', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/backend/events/migrations/0002_alter_event_created_by.py b/backend/events/migrations/0002_alter_event_created_by.py new file mode 100644 index 0000000..5b7a6d6 --- /dev/null +++ b/backend/events/migrations/0002_alter_event_created_by.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.6 on 2025-10-01 02:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0003_rename_teacher_assistant_alter_userprofile_user_type'), + ('events', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='created_by', + field=models.ForeignKey(limit_choices_to={'user_type': 'assistant'}, on_delete=django.db.models.deletion.CASCADE, to='authentication.userprofile', verbose_name='Creado por'), + ), + ] diff --git a/backend/events/migrations/0003_remove_externaluser_email_and_more.py b/backend/events/migrations/0003_remove_externaluser_email_and_more.py new file mode 100644 index 0000000..91587c0 --- /dev/null +++ b/backend/events/migrations/0003_remove_externaluser_email_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.6 on 2025-10-01 19:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0002_alter_event_created_by'), + ] + + operations = [ + migrations.RemoveField( + model_name='externaluser', + name='email', + ), + migrations.RemoveField( + model_name='externaluser', + name='institution', + ), + migrations.RemoveField( + model_name='externaluser', + name='phone', + ), + migrations.RemoveField( + model_name='externaluser', + name='position', + ), + migrations.RemoveField( + model_name='externaluser', + name='reason', + ), + ] diff --git a/backend/events/migrations/0004_auto_20251001_1400.py b/backend/events/migrations/0004_auto_20251001_1400.py new file mode 100644 index 0000000..be832c6 --- /dev/null +++ b/backend/events/migrations/0004_auto_20251001_1400.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.6 on 2025-10-01 20:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0003_remove_externaluser_email_and_more'), + ] + + operations = [ + # Renombrar temporary_id a account_number + migrations.RenameField( + model_name='externaluser', + old_name='temporary_id', + new_name='account_number', + ), + # Cambiar max_length de account_number de 20 a 7 + migrations.AlterField( + model_name='externaluser', + name='account_number', + field=models.CharField(max_length=7, unique=True, verbose_name='Número de cuenta'), + ), + # Cambiar el default de status a 'approved' + migrations.AlterField( + model_name='externaluser', + name='status', + field=models.CharField(choices=[('pending', 'Pendiente'), ('approved', 'Aprobado'), ('rejected', 'Rechazado')], default='approved', max_length=10, verbose_name='Estado de aprobación'), + ), + ] diff --git a/backend/events/migrations/0005_delete_externaluser.py b/backend/events/migrations/0005_delete_externaluser.py new file mode 100644 index 0000000..d906efa --- /dev/null +++ b/backend/events/migrations/0005_delete_externaluser.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.6 on 2025-10-02 00:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('attendance', '0003_alter_attendance_external_user'), + ('events', '0004_auto_20251001_1400'), + ] + + operations = [ + migrations.DeleteModel( + name='ExternalUser', + ), + ] diff --git a/backend/events/migrations/0006_alter_event_created_by.py b/backend/events/migrations/0006_alter_event_created_by.py new file mode 100644 index 0000000..0d326d2 --- /dev/null +++ b/backend/events/migrations/0006_alter_event_created_by.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.6 on 2025-10-08 19:06 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0010_merge_20251003_2150'), + ('events', '0005_delete_externaluser'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='authentication.userprofile', verbose_name='Creado por'), + ), + ] diff --git a/backend/events/migrations/0007_remove_event_created_by.py b/backend/events/migrations/0007_remove_event_created_by.py new file mode 100644 index 0000000..ff02d9e --- /dev/null +++ b/backend/events/migrations/0007_remove_event_created_by.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.6 on 2025-10-08 19:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0006_alter_event_created_by'), + ] + + operations = [ + migrations.RemoveField( + model_name='event', + name='created_by', + ), + ] diff --git a/backend/events/migrations/__init__.py b/backend/events/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/events/models.py b/backend/events/models.py new file mode 100644 index 0000000..4469361 --- /dev/null +++ b/backend/events/models.py @@ -0,0 +1,136 @@ +from django.db import models +from django.core.exceptions import ValidationError +from django.utils import timezone +from authentication.models import UserProfile + +class Event(models.Model): + EVENT_TYPES = ( + ('conference', 'Conferencia'), + ('workshop', 'Taller'), + ('panel', 'Mesa Redonda'), + ('seminar', 'Seminario'), + ) + + MODALITY_CHOICES = ( + ('presencial', 'Presencial'), + ('online', 'En línea'), + ('hybrid', 'Híbrido'), + ) + + title = models.CharField( + max_length=200, + verbose_name="Título de la ponencia" + ) + description = models.TextField( + verbose_name="Descripción" + ) + event_type = models.CharField( + max_length=20, + choices=EVENT_TYPES, + default='conference', + verbose_name="Tipo de evento" + ) + modality = models.CharField( + max_length=15, + choices=MODALITY_CHOICES, + default='presencial', + verbose_name="Modalidad" + ) + speaker = models.CharField( + max_length=200, + verbose_name="Ponente" + ) + date = models.DateField(verbose_name="Fecha") + start_time = models.TimeField(verbose_name="Hora de inicio") + end_time = models.TimeField(verbose_name="Hora de fin") + location = models.CharField( + max_length=200, + verbose_name="Ubicación/Plataforma", + help_text="Para eventos presenciales: aula/salón. Para eventos en línea: enlace o plataforma" + ) + max_capacity = models.IntegerField( + default=100, + verbose_name="Capacidad máxima" + ) + is_active = models.BooleanField( + default=True, + verbose_name="Evento activo" + ) + requires_registration = models.BooleanField( + default=False, + verbose_name="Requiere registro previo" + ) + meeting_link = models.URLField( + blank=True, + null=True, + verbose_name="Enlace de reunión", + help_text="Para eventos en línea o híbridos" + ) + meeting_id = models.CharField( + max_length=50, + blank=True, + null=True, + verbose_name="ID de reunión", + help_text="Código de sala/reunión" + ) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['date', 'start_time'] + verbose_name = "Evento/Ponencia" + verbose_name_plural = "Eventos/Ponencias" + + def clean(self): + # Validar que la hora de fin sea después de la hora de inicio + if self.start_time and self.end_time: + if self.start_time >= self.end_time: + raise ValidationError("La hora de fin debe ser posterior a la hora de inicio.") + + # Validar que la fecha/hora del evento no haya terminado + if self.date and self.end_time: + from datetime import datetime + event_end = datetime.combine(self.date, self.end_time) + + # Hacer timezone-aware si es necesario + if timezone.is_naive(event_end): + event_end = timezone.make_aware(event_end) + + # Solo validar si el evento ya terminó completamente + if event_end < timezone.now(): + raise ValidationError("No se puede crear un evento que ya finalizó.") + + # Validar que eventos en línea tengan enlace + if self.modality in ['online', 'hybrid'] and not self.meeting_link: + raise ValidationError("Los eventos en línea o híbridos requieren un enlace de reunión.") + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + + @property + def duration_minutes(self): + """Duración del evento en minutos""" + if self.start_time and self.end_time: + start_datetime = timezone.datetime.combine(self.date, self.start_time) + end_datetime = timezone.datetime.combine(self.date, self.end_time) + duration = end_datetime - start_datetime + return duration.total_seconds() / 60 + return 0 + + @property + def is_happening_now(self): + """Verifica si el evento está ocurriendo ahora""" + now = timezone.now() + if self.date == now.date(): + current_time = now.time() + return self.start_time <= current_time <= self.end_time + return False + + @property + def is_online(self): + """Verifica si el evento es en línea""" + return self.modality in ['online', 'hybrid'] + + def __str__(self): + modality_icon = "🖥️" if self.is_online else "🏢" + return f"{modality_icon} {self.title} - {self.date} {self.start_time}" \ No newline at end of file diff --git a/backend/events/serializers.py b/backend/events/serializers.py new file mode 100644 index 0000000..5c80a74 --- /dev/null +++ b/backend/events/serializers.py @@ -0,0 +1,20 @@ +from rest_framework import serializers +from .models import Event +from authentication.models import ExternalUser + +class EventSerializer(serializers.ModelSerializer): + class Meta: + model = Event + fields = [ + 'id', 'title', 'description', 'event_type', 'modality', + 'speaker', 'date', 'start_time', 'end_time', 'location', + 'max_capacity', 'is_active', 'meeting_link' + ] + +class ExternalUserSerializer(serializers.ModelSerializer): + class Meta: + model = ExternalUser + fields = [ + 'id', 'full_name', 'account_number', 'status', + 'created_at', 'processed_at' + ] \ No newline at end of file diff --git a/backend/events/urls.py b/backend/events/urls.py new file mode 100644 index 0000000..a950d5c --- /dev/null +++ b/backend/events/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('', views.EventListView.as_view(), name='event_list'), + path('external/register/', views.register_external_user, name='register_external'), + path('external/search/', views.search_external_users, name='search_external'), + path('external//approve/', views.approve_external_user, name='approve_external'), +] \ No newline at end of file diff --git a/backend/events/views.py b/backend/events/views.py new file mode 100644 index 0000000..f7d1fc7 --- /dev/null +++ b/backend/events/views.py @@ -0,0 +1,167 @@ +from rest_framework import generics, status +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.response import Response +from django_ratelimit.decorators import ratelimit +from django.utils import timezone +from django.db import models +from .models import Event +from authentication.models import ExternalUser +from .serializers import EventSerializer, ExternalUserSerializer +import re + +class EventListView(generics.ListCreateAPIView): + queryset = Event.objects.filter(is_active=True) + serializer_class = EventSerializer + + def get_permissions(self): + """Permitir lectura pública, pero creación solo para autenticados""" + if self.request.method == 'POST': + return [IsAuthenticated()] + return [AllowAny()] + + def get_queryset(self): + return Event.objects.filter(is_active=True).order_by('date', 'start_time') + + def perform_create(self, serializer): + """Verificar que solo asistentes puedan crear eventos""" + try: + user_profile = self.request.user.userprofile + if user_profile.user_type != 'assistant': + from rest_framework.exceptions import PermissionDenied + raise PermissionDenied('Solo los asistentes pueden crear eventos') + except: + from rest_framework.exceptions import PermissionDenied + raise PermissionDenied('Usuario sin perfil válido') + + serializer.save() + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@ratelimit(key='user', rate='30/m', method='POST', block=True) +def register_external_user(request): + """Crear un usuario externo - Solo asistentes: 30 creaciones por minuto""" + # Verificar que el usuario sea asistente + try: + user_profile = request.user.userprofile + if user_profile.user_type != 'assistant': + return Response({'error': 'Solo los asistentes pueden crear usuarios externos'}, status=403) + except: + return Response({'error': 'Usuario sin perfil válido'}, status=403) + + data = request.data + account_number = data.get('account_number') + full_name = data.get('full_name') + + if not account_number or not full_name: + return Response({'error': 'Número de cuenta y nombre completo son requeridos'}, status=400) + + # Validar formato de número de cuenta (7 dígitos) + if not re.match(r'^\d{7}$', account_number): + return Response({'error': 'El número de cuenta debe tener exactamente 7 dígitos'}, status=400) + + # Verificar que no exista en usuarios regulares + from authentication.models import UserProfile + if UserProfile.objects.filter(account_number=account_number).exists(): + return Response({'error': 'Este número de cuenta ya está registrado como usuario regular'}, status=400) + + # Verificar que no exista en usuarios externos + if ExternalUser.objects.filter(account_number=account_number).exists(): + return Response({'error': 'Este número de cuenta ya está registrado como usuario externo'}, status=400) + + try: + external_user = ExternalUser.objects.create( + full_name=full_name, + account_number=account_number, + status='approved', + approved_by=user_profile + ) + + return Response({ + 'message': 'Usuario externo creado exitosamente', + 'account_number': account_number, + 'full_name': full_name, + 'status': 'approved' + }, status=status.HTTP_201_CREATED) + + except Exception as e: + return Response({ + 'error': f'Error al crear usuario externo: {str(e)}' + }, status=status.HTTP_400_BAD_REQUEST) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@ratelimit(key='user', rate='60/m', method='GET', block=True) +def search_external_users(request): + """Buscar usuarios externos por nombre o número de cuenta - Solo asistentes: 60 búsquedas por minuto""" + # Verificar que el usuario sea asistente + try: + user_profile = request.user.userprofile + if user_profile.user_type != 'assistant': + return Response({'error': 'Solo los asistentes pueden buscar usuarios externos'}, status=403) + except: + return Response({'error': 'Usuario sin perfil válido'}, status=403) + + search_query = request.GET.get('q', '').strip() + + if not search_query: + return Response({'error': 'Parámetro de búsqueda "q" requerido'}, status=400) + + # Buscar por número de cuenta o nombre (contiene) + external_users = ExternalUser.objects.filter( + status='approved' + ).filter( + models.Q(account_number__icontains=search_query) | + models.Q(full_name__icontains=search_query) + )[:10] # Limitar a 10 resultados + + results = [] + for user in external_users: + results.append({ + 'id': user.id, + 'account_number': user.account_number, + 'full_name': user.full_name, + 'created_at': user.created_at.strftime('%Y-%m-%d') + }) + + return Response({ + 'count': len(results), + 'results': results + }) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +@ratelimit(key='user', rate='30/m', method='POST', block=True) +def approve_external_user(request, user_id): + """Aprobar/rechazar usuario externo - Solo asistentes: 30 acciones por minuto""" + # Verificar que el usuario sea asistente + try: + user_profile = request.user.userprofile + if user_profile.user_type != 'assistant': + return Response({'error': 'Solo los asistentes pueden aprobar usuarios externos'}, status=403) + except: + return Response({'error': 'Usuario sin perfil válido'}, status=403) + + try: + external_user = ExternalUser.objects.get(id=user_id) + action = request.data.get('action') # 'approve' o 'reject' + + if action == 'approve': + external_user.status = 'approved' + external_user.approved_by = user_profile + external_user.processed_at = timezone.now() + external_user.save() + return Response({'message': 'Usuario aprobado'}) + elif action == 'reject': + reason = request.data.get('reason', '') + external_user.status = 'rejected' + external_user.approved_by = user_profile + external_user.rejection_reason = reason + external_user.processed_at = timezone.now() + external_user.save() + return Response({'message': 'Usuario rechazado'}) + else: + return Response({'error': 'Acción inválida'}, status=400) + + except ExternalUser.DoesNotExist: + return Response({'error': 'Usuario no encontrado'}, status=404) \ No newline at end of file diff --git a/backend/mac_attendance/__init__.py b/backend/mac_attendance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/mac_attendance/asgi.py b/backend/mac_attendance/asgi.py new file mode 100644 index 0000000..53c276f --- /dev/null +++ b/backend/mac_attendance/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for mac_attendance project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mac_attendance.settings') + +application = get_asgi_application() diff --git a/backend/mac_attendance/exceptions.py b/backend/mac_attendance/exceptions.py new file mode 100644 index 0000000..9a06056 --- /dev/null +++ b/backend/mac_attendance/exceptions.py @@ -0,0 +1,26 @@ +""" +Manejadores de excepciones personalizados para el proyecto +""" +from rest_framework.views import exception_handler +from rest_framework.response import Response +from rest_framework import status +from django_ratelimit.exceptions import Ratelimited + + +def custom_exception_handler(exc, context): + """ + Manejador de excepciones personalizado que incluye soporte para + excepciones de rate limiting + """ + # Manejar excepciones de rate limiting + if isinstance(exc, Ratelimited): + data = { + 'error': 'Demasiadas solicitudes. Por favor, intenta más tarde.', + 'detail': 'Has excedido el límite de solicitudes permitidas. Espera unos momentos antes de intentar nuevamente.' + } + return Response(data, status=status.HTTP_429_TOO_MANY_REQUESTS) + + # Llamar al manejador de excepciones por defecto de DRF para otras excepciones + response = exception_handler(exc, context) + + return response \ No newline at end of file diff --git a/backend/mac_attendance/middleware.py b/backend/mac_attendance/middleware.py new file mode 100644 index 0000000..d01c274 --- /dev/null +++ b/backend/mac_attendance/middleware.py @@ -0,0 +1,126 @@ +""" +Middleware de auditoría para capturar eventos de seguridad +""" +import re +from django.utils.deprecation import MiddlewareMixin +from authentication.audit import AuditLog +from django_ratelimit.exceptions import Ratelimited + + +class AuditMiddleware: + """ + Middleware para registrar eventos de seguridad automáticamente + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Procesar la solicitud + response = self.get_response(request) + + # Registrar eventos de seguridad basados en código de respuesta + self._log_security_events(request, response) + + return response + + def _log_security_events(self, request, response): + """ + Registrar eventos de seguridad basados en códigos de respuesta HTTP + """ + status_code = response.status_code + user = request.user if hasattr(request, 'user') and request.user.is_authenticated else None + + # 401 Unauthorized - Acceso no autorizado + if status_code == 401: + AuditLog.log( + category='SECURITY', + action='ACCESS_DENIED', + message=f'Intento de acceso no autorizado a {request.path}', + request=request, + user=user, + severity='WARNING', + success=False, + status_code=401, + path=request.path, + method=request.method + ) + + # 403 Forbidden - Permisos insuficientes + elif status_code == 403: + AuditLog.log( + category='SECURITY', + action='ACCESS_DENIED', + message=f'Acceso denegado (permisos insuficientes) a {request.path}', + request=request, + user=user, + severity='WARNING', + success=False, + status_code=403, + path=request.path, + method=request.method + ) + + # 429 Too Many Requests - Rate limit exceeded + elif status_code == 429: + AuditLog.log( + category='SECURITY', + action='RATE_LIMITED', + message=f'Rate limit excedido para {request.path}', + request=request, + user=user, + severity='WARNING', + success=False, + status_code=429, + path=request.path, + method=request.method + ) + + def process_exception(self, request, exception): + """ + Capturar excepciones de rate limiting + """ + if isinstance(exception, Ratelimited): + user = request.user if hasattr(request, 'user') and request.user.is_authenticated else None + + AuditLog.log( + category='SECURITY', + action='RATE_LIMITED', + message=f'Rate limit exception en {request.path}', + request=request, + user=user, + severity='WARNING', + success=False, + status_code=429, + path=request.path, + method=request.method, + exception=str(exception) + ) + + return None # Permitir que Django maneje la excepción normalmente + + +class DisableCSRFOnAPIMiddleware(MiddlewareMixin): + """ + Middleware para deshabilitar CSRF en rutas de API + Ya que usamos JWT para autenticación en la API REST + """ + + def process_request(self, request): + """ + Marcar requests de API como exentos de CSRF + """ + from django.conf import settings + + # Obtener patrones de exención de CSRF + csrf_exempt_urls = getattr(settings, 'CSRF_EXEMPT_URLS', []) + + # Verificar si la ruta actual debe estar exenta + path = request.path_info.lstrip('/') + + for pattern in csrf_exempt_urls: + if re.match(pattern, path): + setattr(request, '_dont_enforce_csrf_checks', True) + break + + return None \ No newline at end of file diff --git a/backend/mac_attendance/settings.py b/backend/mac_attendance/settings.py new file mode 100644 index 0000000..91a0569 --- /dev/null +++ b/backend/mac_attendance/settings.py @@ -0,0 +1,269 @@ +""" +Django settings for mac_attendance project. + +Generated by 'django-admin startproject' using Django 5.2.6. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +from pathlib import Path +from decouple import config + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = config('SECRET_KEY') + +DEBUG = config('DEBUG', default=False, cast=bool) + +ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1').split(',') + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'rest_framework_simplejwt', + 'corsheaders', + 'import_export', + 'authentication', + 'events', + 'attendance', +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'mac_attendance.middleware.DisableCSRFOnAPIMiddleware', # Exentar API de CSRF (usamos JWT) + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'mac_attendance.middleware.AuditMiddleware', # Auditoría de seguridad +] + +# Rate Limiting Configuration +RATELIMIT_ENABLE = config('RATELIMIT_ENABLE', default=True, cast=bool) +RATELIMIT_USE_CACHE = 'default' # Usar cache por defecto (memory en dev) + +ROOT_URLCONF = 'mac_attendance.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'mac_attendance.wsgi.application' + +# Database Configuration - PostgreSQL +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': config('DB_NAME', default='mac_attendance'), + 'USER': config('DB_USER', default='mac_user'), + 'PASSWORD': config('DB_PASSWORD', default='mac_password_2024_secure'), + 'HOST': config('DB_HOST', default='db'), + 'PORT': config('DB_PORT', default='5432'), + 'CONN_MAX_AGE': 600, # Conexiones persistentes para mejor rendimiento + 'OPTIONS': { + 'connect_timeout': 10, + } + } +} + +# Cache Configuration (para rate limiting) +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'ratelimit-cache', + } +} + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +LANGUAGE_CODE = 'es-mx' +TIME_ZONE = 'America/Mexico_City' +USE_I18N = True +USE_TZ = True + +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' + +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Security Settings for Production +if not DEBUG: + # HTTPS/SSL Settings + SECURE_SSL_REDIRECT = config('SECURE_SSL_REDIRECT', default=True, cast=bool) + SESSION_COOKIE_SECURE = config('SESSION_COOKIE_SECURE', default=True, cast=bool) + CSRF_COOKIE_SECURE = config('CSRF_COOKIE_SECURE', default=True, cast=bool) + + # HSTS (HTTP Strict Transport Security) + SECURE_HSTS_SECONDS = config('SECURE_HSTS_SECONDS', default=31536000, cast=int) # 1 año + SECURE_HSTS_INCLUDE_SUBDOMAINS = config('SECURE_HSTS_INCLUDE_SUBDOMAINS', default=True, cast=bool) + SECURE_HSTS_PRELOAD = config('SECURE_HSTS_PRELOAD', default=True, cast=bool) + + # Security Headers + SECURE_CONTENT_TYPE_NOSNIFF = True + SECURE_BROWSER_XSS_FILTER = True + X_FRAME_OPTIONS = 'DENY' + + # Proxy Settings (para uso detrás de nginx/apache) + SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + +# CORS settings +CORS_ALLOWED_ORIGINS = config( + 'CORS_ALLOWED_ORIGINS', + default='http://localhost,http://127.0.0.1,http://localhost:5173,http://127.0.0.1:5173' +).split(',') + +CORS_ALLOW_CREDENTIALS = True + +# CSRF Settings - Exentar rutas de API ya que usamos JWT +CSRF_TRUSTED_ORIGINS = config( + 'CSRF_TRUSTED_ORIGINS', + default='http://localhost,http://127.0.0.1' +).split(',') + +# Exentar todas las rutas de API del CSRF check (usamos JWT para seguridad) +CSRF_EXEMPT_URLS = [r'^api/.*$'] + +# Django REST Framework +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework_simplejwt.authentication.JWTAuthentication', + 'rest_framework.authentication.SessionAuthentication', # Mantener para Django admin + ], + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + ], + 'EXCEPTION_HANDLER': 'mac_attendance.exceptions.custom_exception_handler', +} + +# JWT Settings +from datetime import timedelta + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(hours=1), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), + 'ROTATE_REFRESH_TOKENS': True, + 'BLACKLIST_AFTER_ROTATION': False, + 'UPDATE_LAST_LOGIN': True, + + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + + 'AUTH_HEADER_TYPES': ('Bearer',), + 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', + + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', +} + +# Logging Configuration +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', + 'style': '{', + }, + 'simple': { + 'format': '{levelname} {asctime} {message}', + 'style': '{', + }, + 'audit': { + 'format': '[AUDIT] {asctime} {levelname} {message}', + 'style': '{', + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'simple', + }, + 'file': { + 'class': 'logging.FileHandler', + 'filename': BASE_DIR / 'logs' / 'django.log', + 'formatter': 'verbose', + }, + 'security_file': { + 'class': 'logging.FileHandler', + 'filename': BASE_DIR / 'logs' / 'security.log', + 'formatter': 'audit', + }, + 'audit_file': { + 'class': 'logging.FileHandler', + 'filename': BASE_DIR / 'logs' / 'audit.log', + 'formatter': 'audit', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console', 'file'] if not DEBUG else ['console'], + 'level': 'INFO' if not DEBUG else 'DEBUG', + }, + 'django.security': { + 'handlers': ['console', 'security_file'] if not DEBUG else ['console'], + 'level': 'WARNING', + 'propagate': False, + }, + 'authentication.audit': { + 'handlers': ['console', 'audit_file'] if not DEBUG else ['console'], + 'level': 'INFO', + 'propagate': False, + }, + # Logger para debugging de rate limiting + 'django_ratelimit': { + 'handlers': ['console', 'security_file'] if not DEBUG else ['console'], + 'level': 'WARNING', + 'propagate': False, + }, + }, +} + +# Crear directorio de logs si no existe +import os +LOGS_DIR = BASE_DIR / 'logs' +if not os.path.exists(LOGS_DIR): + os.makedirs(LOGS_DIR) + diff --git a/backend/mac_attendance/urls.py b/backend/mac_attendance/urls.py new file mode 100644 index 0000000..9aedc15 --- /dev/null +++ b/backend/mac_attendance/urls.py @@ -0,0 +1,275 @@ +""" +URL configuration for mac_attendance project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from django.http import JsonResponse +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def api_root(request): + """API root endpoint - Returns comprehensive API information (Protected)""" + return JsonResponse({ + 'name': 'Sistema de Asistencia MAC - API', + 'version': '1.0.0', + 'description': 'API REST para el Sistema de Asistencia MAC - FES Acatlán UNAM', + 'documentation': { + 'main': '/api/docs/', + 'swagger': '/api/swagger/', + 'redoc': '/api/redoc/', + }, + 'endpoints': { + 'authentication': { + 'login': { + 'url': '/api/auth/login/', + 'method': 'POST', + 'description': 'Iniciar sesión con número de cuenta', + 'auth_required': False, + }, + 'logout': { + 'url': '/api/auth/logout/', + 'method': 'POST', + 'description': 'Cerrar sesión', + 'auth_required': True, + }, + 'profile': { + 'url': '/api/auth/profile/', + 'method': 'GET', + 'description': 'Obtener perfil de usuario', + 'auth_required': True, + }, + 'check_auth': { + 'url': '/api/auth/check-auth/', + 'method': 'GET', + 'description': 'Verificar estado de autenticación', + 'auth_required': False, + }, + 'token_refresh': { + 'url': '/api/auth/token/refresh/', + 'method': 'POST', + 'description': 'Refrescar access token', + 'auth_required': False, + }, + 'system_config': { + 'url': '/api/auth/system-config/', + 'method': 'GET', + 'description': 'Obtener configuración del sistema', + 'auth_required': True, + }, + }, + 'events': { + 'list': { + 'url': '/api/events/', + 'method': 'GET', + 'description': 'Listar todos los eventos', + 'auth_required': False, + }, + 'register_external': { + 'url': '/api/events/external/register/', + 'method': 'POST', + 'description': 'Registrar usuario externo', + 'auth_required': True, + }, + 'search_external': { + 'url': '/api/events/external/search/', + 'method': 'GET', + 'description': 'Buscar usuarios externos', + 'auth_required': True, + }, + 'approve_external': { + 'url': '/api/events/external//approve/', + 'method': 'POST', + 'description': 'Aprobar usuario externo', + 'auth_required': True, + }, + }, + 'attendance': { + 'register': { + 'url': '/api/attendance/', + 'method': 'POST', + 'description': 'Registrar asistencia a evento', + 'auth_required': True, + }, + 'stats': { + 'url': '/api/attendance/stats/', + 'method': 'GET', + 'description': 'Obtener estadísticas de asistencia', + 'auth_required': True, + }, + 'recent': { + 'url': '/api/attendance/recent/', + 'method': 'GET', + 'description': 'Obtener asistencias recientes', + 'auth_required': True, + }, + 'my_attendances': { + 'url': '/api/attendance/my/', + 'method': 'GET', + 'description': 'Obtener mis asistencias', + 'auth_required': True, + }, + }, + }, + 'authentication': { + 'type': 'JWT Bearer Token', + 'header': 'Authorization: Bearer ', + 'token_lifetime': '1 hour', + 'refresh_token_lifetime': '7 days', + }, + 'status': 'operational', + 'server_time': request.build_absolute_uri(), + }) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def api_docs(request): + """API documentation endpoint (Protected)""" + from django.http import HttpResponse + html_content = """ + + + + + + API Documentation - Sistema MAC + + + +
+

📚 API Documentation

+

Sistema de Asistencia MAC - FES Acatlán UNAM

+

Version: 1.0.0 | Base URL: /api/

+
+ +
+

🔐 Authentication Endpoints

+ +

POST /api/auth/login/

+

Iniciar sesión con número de cuenta

+
{
+  "account_number": "1234567"
+}
+ +

GET /api/auth/profile/ AUTH

+

Obtener perfil del usuario autenticado

+ +

POST /api/auth/token/refresh/

+

Refrescar access token

+
+ +
+

📅 Events Endpoints

+ +

GET /api/events/

+

Listar todos los eventos activos

+ +

POST /api/events/external/register/ AUTH

+

Registrar un usuario externo para eventos

+
+ +
+

✅ Attendance Endpoints

+ +

POST /api/attendance/ AUTH

+

Registrar asistencia a un evento

+ +

GET /api/attendance/stats/ AUTH

+

Obtener estadísticas de asistencia del usuario

+ +

GET /api/attendance/my/ AUTH

+

Obtener mis asistencias registradas

+
+ +
+

🔑 Authentication

+

La API utiliza JWT (JSON Web Tokens) para autenticación.

+

Para endpoints que requieren autenticación, incluye el header:

+
Authorization: Bearer <your_access_token>
+
+ +
+

📖 Quick Links

+ +
+ + + """ + return HttpResponse(html_content) + +urlpatterns = [ + path('', api_root, name='api_root'), + path('api/', api_root, name='api_root_alt'), + path('api/docs/', api_docs, name='api_docs'), + path('admin/', admin.site.urls), + path('api/auth/', include('authentication.urls')), + path('api/events/', include('events.urls')), + path('api/attendance/', include('attendance.urls')), +] \ No newline at end of file diff --git a/backend/mac_attendance/wsgi.py b/backend/mac_attendance/wsgi.py new file mode 100644 index 0000000..108a41d --- /dev/null +++ b/backend/mac_attendance/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for mac_attendance project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mac_attendance.settings') + +application = get_wsgi_application() diff --git a/backend/manage.py b/backend/manage.py new file mode 100644 index 0000000..e67f6b1 --- /dev/null +++ b/backend/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mac_attendance.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..7fc32cf --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,145 @@ +# Configuración centralizada para herramientas de desarrollo Python + +[tool.black] +# Black - Formateador de código opinionado +line-length = 120 +target-version = ['py311'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # Directorios a excluir + \.git + | \.venv + | venv + | env + | __pycache__ + | migrations + | staticfiles + | media + | node_modules + | dist + | build +)/ +''' + +[tool.isort] +# isort - Ordenador de imports +profile = "black" +line_length = 120 +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +skip_glob = [ + "*/migrations/*", + "*/venv/*", + "*/env/*", + ".venv/*", + "staticfiles/*", + "media/*" +] +known_django = "django" +known_first_party = ["authentication", "attendance", "events", "mac_attendance"] +sections = ["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] + +[tool.pytest.ini_options] +# pytest - Framework de testing +DJANGO_SETTINGS_MODULE = "mac_attendance.settings" +python_files = ["tests.py", "test_*.py", "*_tests.py"] +addopts = [ + "--verbose", + "--strict-markers", + "--tb=short", + "--cov=.", + "--cov-report=html", + "--cov-report=term-missing", +] +testpaths = ["tests"] +markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "integration: marks tests as integration tests", + "unit: marks tests as unit tests", +] + +[tool.coverage.run] +# Coverage - Cobertura de código +source = ["."] +omit = [ + "*/migrations/*", + "*/tests/*", + "*/test_*.py", + "*/__pycache__/*", + "*/venv/*", + "*/env/*", + ".venv/*", + "manage.py", + "*/wsgi.py", + "*/asgi.py", +] + +[tool.coverage.report] +precision = 2 +show_missing = true +skip_covered = false +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", +] + +[tool.mypy] +# mypy - Type checker +python_version = "3.11" +check_untyped_defs = true +ignore_missing_imports = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_unused_configs = true +plugins = ["mypy_django_plugin.main", "mypy_drf_plugin.main"] + +[[tool.mypy.overrides]] +module = "*.migrations.*" +ignore_errors = true + +[tool.django-stubs] +django_settings_module = "mac_attendance.settings" + +[tool.pylint.main] +# pylint - Analizador estático +load-plugins = ["pylint_django"] +django-settings-module = "mac_attendance.settings" + +[tool.pylint.format] +max-line-length = 120 + +[tool.pylint.messages_control] +disable = [ + "C0111", # missing-docstring + "C0103", # invalid-name + "R0903", # too-few-public-methods + "R0913", # too-many-arguments +] + +[tool.pylint.design] +max-attributes = 10 +max-args = 7 + +[tool.bandit] +# bandit - Verificador de seguridad +exclude_dirs = [ + "/tests/", + "/migrations/", + "/venv/", + "/.venv/", +] +skips = ["B101", "B601"] + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt new file mode 100644 index 0000000..3e2567e --- /dev/null +++ b/backend/requirements-dev.txt @@ -0,0 +1,40 @@ +# Herramientas de desarrollo y calidad de código + +# Linting y formateo de código +flake8==7.1.1 # Verificador de estilo PEP8 +pycodestyle==2.12.1 # Verificador de estilo PEP8 (antes pep8) +autopep8==2.3.1 # Corrector automático de PEP8 +black==24.8.0 # Formateador de código opinionado +isort==5.13.2 # Ordenador de imports + +# Análisis estático y complejidad +pylint==3.3.2 # Analizador de código estático +mccabe==0.7.0 # Complejidad ciclomática +radon==6.0.1 # Métricas de código + +# Type checking +mypy==1.13.0 # Verificador de tipos estáticos +django-stubs==5.1.1 # Type stubs para Django +djangorestframework-stubs==3.15.1 # Type stubs para DRF + +# Seguridad +bandit==1.8.0 # Verificador de seguridad +safety==3.6.2 # Verificador de vulnerabilidades + +# Testing +pytest==8.3.4 # Framework de testing +pytest-django==4.9.0 # Plugin pytest para Django +pytest-cov==6.0.0 # Cobertura de tests +coverage==7.6.9 # Herramienta de cobertura +factory-boy==3.3.1 # Factories para testing + +# Testing automation +tox==4.23.2 # Automatización de testing multi-entorno + +# Pre-commit hooks +pre-commit==4.0.1 # Framework de pre-commit hooks + +# Utilidades de desarrollo +django-extensions==3.2.3 # Extensiones útiles para Django +ipython==8.29.0 # Shell interactivo mejorado +ipdb==0.13.13 # Debugger interactivo diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..9e9b978 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,40 @@ +# Django Core +Django==5.2.6 +djangorestframework==3.15.2 + +# CORS Headers +django-cors-headers==4.6.0 + +# Database - PostgreSQL +psycopg2-binary==2.9.9 + +# Environment Variables +python-decouple==3.8 + +# Autenticación JWT (recomendado para API REST) +djangorestframework-simplejwt==5.3.1 + +# Rate Limiting (protección contra ataques de fuerza bruta) +django-ratelimit==4.1.0 + +# Exportación de datos + django-import-export==4.3.10 + +# Utilidades de desarrollo +# django-extensions==3.2.3 +# ipython==8.29.0 + +# Testing +# pytest==8.3.4 +# pytest-django==4.9.0 + +# Exportación de datos +# django-import-export==4.2.0 +# openpyxl==3.1.5 + +# Generación de PDFs +# reportlab==4.2.5 + +# Servidor de producción +# gunicorn==23.0.0 +# whitenoise==6.8.2 \ No newline at end of file diff --git a/backend/scripts/check_production.py b/backend/scripts/check_production.py new file mode 100644 index 0000000..4a08208 --- /dev/null +++ b/backend/scripts/check_production.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python +""" +Script de validación para verificar configuraciones de producción +Ejecutar: python check_production.py +""" + +import os +import sys +from pathlib import Path + +# Desactivar colores en Windows para evitar problemas de encoding +if sys.platform == 'win32': + GREEN = '' + YELLOW = '' + RED = '' + RESET = '' + BOLD = '' +else: + # Colores para terminal Unix + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + RESET = '\033[0m' + BOLD = '\033[1m' + +def check_env_file(): + """Verificar que existe archivo .env""" + print(f"\n{BOLD}1. Verificando archivo .env...{RESET}") + + if not os.path.exists('.env'): + print(f"{RED}[X] Archivo .env no encontrado{RESET}") + print(f"{YELLOW} Copia .env.example o .env.production.example como .env{RESET}") + return False + + print(f"{GREEN}[OK] Archivo .env encontrado{RESET}") + return True + +def check_env_vars(): + """Verificar variables de entorno críticas""" + print(f"\n{BOLD}2. Verificando variables de entorno...{RESET}") + + from decouple import config + + issues = [] + + # SECRET_KEY + secret_key = config('SECRET_KEY', default='') + if not secret_key or 'django-insecure' in secret_key or 'CHANGE' in secret_key: + print(f"{RED}[X] SECRET_KEY no configurada o insegura{RESET}") + issues.append("Genera una nueva SECRET_KEY segura") + else: + print(f"{GREEN}[OK] SECRET_KEY configurada{RESET}") + + # DEBUG + debug = config('DEBUG', default=True, cast=bool) + if debug: + print(f"{YELLOW}[!] DEBUG=True (OK para desarrollo, cambiar a False en produccion){RESET}") + else: + print(f"{GREEN}[OK] DEBUG=False (configuracion de produccion){RESET}") + + # ALLOWED_HOSTS + allowed_hosts = config('ALLOWED_HOSTS', default='') + if not debug and not allowed_hosts: + print(f"{RED}[X] ALLOWED_HOSTS no configurado{RESET}") + issues.append("Configura ALLOWED_HOSTS con tus dominios de produccion") + else: + print(f"{GREEN}[OK] ALLOWED_HOSTS: {allowed_hosts}{RESET}") + + return len(issues) == 0, issues + +def check_security_settings(): + """Verificar configuraciones de seguridad para producción""" + print(f"\n{BOLD}3. Verificando configuraciones de seguridad...{RESET}") + + from decouple import config + debug = config('DEBUG', default=True, cast=bool) + + if debug: + print(f"{YELLOW}[!] Modo desarrollo - Configuraciones de seguridad no aplicadas{RESET}") + print(f"{YELLOW} Esto es normal en desarrollo local{RESET}") + return True + + # Verificar configuraciones de produccion + ssl_redirect = config('SECURE_SSL_REDIRECT', default=False, cast=bool) + session_secure = config('SESSION_COOKIE_SECURE', default=False, cast=bool) + csrf_secure = config('CSRF_COOKIE_SECURE', default=False, cast=bool) + + all_secure = True + + if not ssl_redirect: + print(f"{YELLOW}[!] SECURE_SSL_REDIRECT no habilitado{RESET}") + all_secure = False + else: + print(f"{GREEN}[OK] SECURE_SSL_REDIRECT habilitado{RESET}") + + if not session_secure: + print(f"{YELLOW}[!] SESSION_COOKIE_SECURE no habilitado{RESET}") + all_secure = False + else: + print(f"{GREEN}[OK] SESSION_COOKIE_SECURE habilitado{RESET}") + + if not csrf_secure: + print(f"{YELLOW}[!] CSRF_COOKIE_SECURE no habilitado{RESET}") + all_secure = False + else: + print(f"{GREEN}[OK] CSRF_COOKIE_SECURE habilitado{RESET}") + + return all_secure + +def check_dependencies(): + """Verificar que las dependencias estén instaladas""" + print(f"\n{BOLD}4. Verificando dependencias...{RESET}") + + try: + import django + print(f"{GREEN}[OK] Django {django.get_version()}{RESET}") + except ImportError: + print(f"{RED}[X] Django no instalado{RESET}") + return False + + try: + import rest_framework + print(f"{GREEN}[OK] Django REST Framework{RESET}") + except ImportError: + print(f"{RED}[X] Django REST Framework no instalado{RESET}") + return False + + try: + import rest_framework_simplejwt + print(f"{GREEN}[OK] Django REST Framework SimpleJWT{RESET}") + except ImportError: + print(f"{RED}[X] Django REST Framework SimpleJWT no instalado{RESET}") + return False + + try: + import corsheaders + print(f"{GREEN}[OK] Django CORS Headers{RESET}") + except ImportError: + print(f"{RED}[X] Django CORS Headers no instalado{RESET}") + return False + + return True + +def main(): + print(f"{BOLD}{'='*60}{RESET}") + print(f"{BOLD} Verificación de Configuración - MAC Attendance{RESET}") + print(f"{BOLD}{'='*60}{RESET}") + + # Cambiar al directorio del script + os.chdir(Path(__file__).parent) + + all_checks_passed = True + + # Ejecutar verificaciones + if not check_env_file(): + all_checks_passed = False + + env_ok, issues = check_env_vars() + if not env_ok: + all_checks_passed = False + + if not check_security_settings(): + all_checks_passed = False + + if not check_dependencies(): + all_checks_passed = False + + # Resumen + print(f"\n{BOLD}{'='*60}{RESET}") + print(f"{BOLD} RESUMEN{RESET}") + print(f"{BOLD}{'='*60}{RESET}") + + if all_checks_passed: + print(f"{GREEN}[OK] Todas las verificaciones pasaron{RESET}") + else: + print(f"{YELLOW}[!] Algunas verificaciones fallaron o necesitan atencion{RESET}") + if issues: + print(f"\n{BOLD}Acciones requeridas:{RESET}") + for issue in issues: + print(f" - {issue}") + + print(f"\n{BOLD}Siguiente paso:{RESET}") + print(f" Ejecuta: python manage.py check --deploy") + print() + +if __name__ == '__main__': + try: + main() + except Exception as e: + print(f"{RED}Error: {e}{RESET}") + sys.exit(1) \ No newline at end of file diff --git a/backend/scripts/create_test_data.py b/backend/scripts/create_test_data.py new file mode 100644 index 0000000..77c4e5a --- /dev/null +++ b/backend/scripts/create_test_data.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +"""Script para crear datos de prueba para el sistema de asistencia""" +import os +import django + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mac_attendance.settings') +django.setup() + +from django.contrib.auth.models import User +from authentication.models import UserProfile, Asistente +from events.models import Event +from attendance.models import Attendance, AttendanceStats +from datetime import date, time + +def create_test_data(): + print("[+] Creando datos de prueba...") + + # Obtener o crear un asistente para registrar asistencias + try: + asistente_profile = UserProfile.objects.get(user_type='assistant') + print(f"[OK] Asistente encontrado: {asistente_profile.full_name}") + except UserProfile.DoesNotExist: + print("[ERROR] No hay asistentes en el sistema. Por favor crea uno primero.") + return + + # Crear 5 estudiantes de prueba + estudiantes_data = [ + ('3001001', 'María González López', 90.0), # Cumple + ('3001002', 'Juan Pérez Martínez', 85.0), # Cumple + ('3001003', 'Ana Rodríguez Sánchez', 75.0), # NO cumple + ('3001004', 'Carlos López García', 95.0), # Cumple + ('3001005', 'Laura Martínez Ruiz', 60.0), # NO cumple + ] + + estudiantes = [] + for account, name, percentage in estudiantes_data: + # Verificar si ya existe + if UserProfile.objects.filter(account_number=account).exists(): + print(f"[SKIP] Estudiante {account} ya existe, omitiendo...") + estudiantes.append(UserProfile.objects.get(account_number=account)) + continue + + # Crear usuario Django + user = User.objects.create_user( + username=account, + first_name=name, + password=None + ) + user.set_unusable_password() + user.save() + + # Crear perfil + profile = UserProfile.objects.create( + user=user, + account_number=account, + full_name=name, + user_type='student' + ) + estudiantes.append(profile) + print(f"[OK] Estudiante creado: {name} ({account})") + + # Crear 3 eventos de prueba (en diferentes horarios para evitar conflictos) + eventos_data = [ + ('Conferencia de Inteligencia Artificial', 'Dr. Roberto Silva', time(9, 0), time(11, 0)), + ('Taller de Python Avanzado', 'Ing. Patricia Gómez', time(12, 0), time(14, 0)), + ('Seminario de Ciberseguridad', 'M.C. Fernando Ruiz', time(15, 0), time(17, 0)), + ] + + eventos = [] + for title, speaker, start, end in eventos_data: + # Verificar si ya existe + if Event.objects.filter(title=title).exists(): + print(f"[SKIP] Evento '{title}' ya existe, omitiendo...") + eventos.append(Event.objects.get(title=title)) + continue + + evento = Event.objects.create( + title=title, + speaker=speaker, + date=date(2025, 10, 15), + start_time=start, + end_time=end, + event_type='lecture', + modality='presential', + location='Auditorio Principal', + description=f'Descripción de {title}', + created_by=asistente_profile, + is_active=True + ) + eventos.append(evento) + print(f"[OK] Evento creado: {title}") + + # Crear asistencias simuladas para generar los porcentajes + print("\n[+] Creando estadisticas de asistencia simuladas...") + + for i, (profile, (_, _, target_percentage)) in enumerate(zip(estudiantes, estudiantes_data)): + # Crear o actualizar estadísticas + stats, created = AttendanceStats.objects.get_or_create( + student=profile, + defaults={ + 'total_events': len(eventos), + 'attended_events': 0, + 'attendance_percentage': 0.0 + } + ) + + # Calcular eventos a asistir para lograr el porcentaje deseado + total_eventos = len(eventos) + eventos_a_asistir = int((target_percentage / 100) * total_eventos) + + # Registrar asistencias + for j in range(eventos_a_asistir): + if j < len(eventos): + # Verificar si ya existe la asistencia + if not Attendance.objects.filter(student=profile, event=eventos[j]).exists(): + Attendance.objects.create( + student=profile, + event=eventos[j], + registered_by=asistente_profile, + registration_method='manual', + is_valid=True + ) + + # Actualizar estadísticas + stats.update_stats() + print(f" {profile.full_name}: {stats.attendance_percentage}% de asistencia") + + print("\n[OK] Datos de prueba creados exitosamente!") + print(f"\n[RESUMEN]") + print(f" - Estudiantes: {len(estudiantes)}") + print(f" - Eventos: {len(eventos)}") + print(f" - Porcentaje minimo: 80%") + print(f"\n[INFO] Ahora puedes probar:") + print(f" 1. Ir a http://127.0.0.1:8000/admin/") + print(f" 2. Navegar a 'Estadisticas de asistencia'") + print(f" 3. Seleccionar todos los registros") + print(f" 4. Usar la accion: 'Exportar estudiantes que cumplen requisito para constancia'") + +if __name__ == '__main__': + create_test_data() diff --git a/backend/scripts/create_valid_test_data.py b/backend/scripts/create_valid_test_data.py new file mode 100644 index 0000000..929415e --- /dev/null +++ b/backend/scripts/create_valid_test_data.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python +"""Script para crear datos de prueba VÁLIDOS respetando todas las validaciones""" +import os +import django +from datetime import datetime, time, timedelta + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mac_attendance.settings') +django.setup() + +from django.contrib.auth.models import User +from django.utils import timezone +from authentication.models import UserProfile, Asistente +from events.models import Event +from attendance.models import Attendance, AttendanceStats + +def create_valid_test_data(): + print("[+] Creando datos de prueba VALIDOS...") + + # Obtener o crear un asistente para registrar asistencias + try: + asistente_profile = UserProfile.objects.get(user_type='assistant') + print(f"[OK] Asistente encontrado: {asistente_profile.full_name}") + except UserProfile.DoesNotExist: + print("[ERROR] No hay asistentes en el sistema. Por favor crea uno primero.") + return + + # Verificar estudiantes existentes + estudiantes = UserProfile.objects.filter(user_type='student') + if estudiantes.count() < 3: + print("[+] Creando estudiantes de prueba...") + estudiantes_data = [ + ('3001001', 'María González López'), + ('3001002', 'Juan Pérez Martínez'), + ('3001003', 'Ana Rodríguez Sánchez'), + ('3001004', 'Carlos López García'), + ('3001005', 'Laura Martínez Ruiz'), + ] + + for account, name in estudiantes_data: + if UserProfile.objects.filter(account_number=account).exists(): + print(f"[SKIP] Estudiante {account} ya existe") + continue + + user = User.objects.create_user( + username=account, + first_name=name, + password=None + ) + user.set_unusable_password() + user.save() + + UserProfile.objects.create( + user=user, + account_number=account, + full_name=name, + user_type='student' + ) + print(f"[OK] Estudiante creado: {name} ({account})") + + estudiantes = list(UserProfile.objects.filter(user_type='student')[:5]) + print(f"[OK] {len(estudiantes)} estudiantes disponibles") + + # Obtener hora actual en timezone local + now = timezone.now() + now_local = timezone.localtime(now) + today = now_local.date() + + print(f"\n[INFO] Fecha/Hora actual (local): {now_local.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"[INFO] Timezone: {timezone.get_current_timezone()}") + + # Crear eventos para HOY con horarios fijos que funcionen + # Como estamos en la noche, usamos eventos con margen suficiente + + from datetime import date + tomorrow = (now_local + timedelta(days=1)).date() + + eventos_data = [] + + # Evento 1: Mañana 9:00-11:00 (aún no permite asistencias) + eventos_data.append(( + 'Introducción a la Inteligencia Artificial', + 'Dr. Luis Martínez', + time(9, 0), + time(11, 0), + tomorrow + )) + + # Evento 2: Mañana 13:00-15:00 (aún no permite asistencias) + eventos_data.append(( + 'Análisis de Datos con Python', + 'Ing. Patricia Gómez', + time(13, 15), + time(15, 0), + tomorrow + )) + + # Evento 3: Mañana 16:00-18:00 (aún no permite asistencias) + eventos_data.append(( + 'Taller de Machine Learning', + 'Dra. Ana López', + time(16, 0), + time(18, 0), + tomorrow + )) + + eventos = [] + for title, speaker, start, end, event_date in eventos_data: + # Verificar si ya existe + if Event.objects.filter(title=title, date=event_date).exists(): + print(f"[SKIP] Evento '{title}' ya existe") + eventos.append(Event.objects.get(title=title, date=event_date)) + continue + + evento = Event.objects.create( + title=title, + speaker=speaker, + date=event_date, + start_time=start, + end_time=end, + event_type='lecture', + modality='presential', + location='Auditorio Principal', + description=f'Evento de prueba: {title}', + created_by=asistente_profile, + is_active=True + ) + eventos.append(evento) + print(f"[OK] Evento creado: {title} ({event_date} {start.strftime('%H:%M')}-{end.strftime('%H:%M')})") + + # Intentar crear asistencias (TODAS deberían fallar porque los eventos son mañana) + print("\n[+] Intentando registrar asistencias para eventos de mañana...") + print("[NOTA] TODAS las asistencias deberían fallar porque los eventos son mañana") + + asistencias_creadas = 0 + for i, evento in enumerate(eventos): + estudiante = estudiantes[i] if i < len(estudiantes) else estudiantes[0] + print(f"\n[TEST] Intentando registrar en: {evento.title} ({evento.date} {evento.start_time})") + try: + # Verificar si ya existe + if Attendance.objects.filter(student=estudiante, event=evento).exists(): + print(f" [SKIP] {estudiante.full_name} ya tiene asistencia") + continue + + att = Attendance.objects.create( + student=estudiante, + event=evento, + registered_by=asistente_profile, + registration_method='manual', + is_valid=True + ) + print(f" [ERROR] Se permitió registro (NO DEBERÍA) - Asistencia: {estudiante.full_name}") + asistencias_creadas += 1 + except Exception as e: + error_msg = str(e) + if "No se puede registrar asistencia antes del evento" in error_msg: + print(f" [OK] Validación funcionó correctamente - Evento es mañana") + else: + print(f" [OK] Validación funcionó: {error_msg[:80]}...") + + # Actualizar estadísticas + print("\n[+] Actualizando estadísticas...") + for estudiante in estudiantes: + stats, created = AttendanceStats.objects.get_or_create(student=estudiante) + stats.update_stats() + print(f" {estudiante.full_name}: {stats.attendance_percentage}% asistencia") + + print("\n" + "="*70) + print("[OK] Datos de prueba creados exitosamente!") + print("="*70) + print(f"\n[RESUMEN]") + print(f" - Estudiantes: {len(estudiantes)}") + print(f" - Eventos creados: {len(eventos)}") + print(f" - Asistencias válidas: {asistencias_creadas}") + print(f" - Fecha/Hora: {now_local.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"\n[INFO] Ahora puedes:") + print(f" 1. Ir a http://127.0.0.1:8000/admin/") + print(f" 2. Ver eventos en 'Events' - eventos para mañana {tomorrow}") + print(f" 3. Ver asistencias en 'Attendances' - debería estar vacío") + print(f" 4. Ver estadísticas en 'Attendance stats'") + print(f"\n[NOTA] Las asistencias solo se pueden registrar durante los eventos") + print(f"[NOTA] Mañana {tomorrow} a partir de las 8:50 AM se podrán registrar asistencias") + +if __name__ == '__main__': + create_valid_test_data() diff --git a/backend/scripts/format.sh b/backend/scripts/format.sh new file mode 100644 index 0000000..7988025 --- /dev/null +++ b/backend/scripts/format.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Script para formatear automáticamente el código + +set -e + +echo "==================================" +echo " Formateo Automático de Código" +echo "==================================" +echo "" + +# Colores +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# 1. isort - Ordenar imports +echo -e "${YELLOW}▶ Ordenando imports con isort...${NC}" +isort . +echo -e "${GREEN}✓ Imports ordenados${NC}" +echo "" + +# 2. Black - Formatear código +echo -e "${YELLOW}▶ Formateando código con black...${NC}" +black . +echo -e "${GREEN}✓ Código formateado${NC}" +echo "" + +# 3. autopep8 - Correcciones adicionales de PEP8 (opcional) +echo -e "${YELLOW}▶ Aplicando correcciones adicionales de PEP8...${NC}" +autopep8 --in-place --aggressive --aggressive --recursive . +echo -e "${GREEN}✓ Correcciones PEP8 aplicadas${NC}" +echo "" + +echo "==================================" +echo -e "${GREEN}✓ Formateo completado${NC}" +echo "==================================" +echo "" +echo "Tip: Ejecuta './scripts/lint.sh' para verificar la calidad del código" diff --git a/backend/scripts/lint.sh b/backend/scripts/lint.sh new file mode 100644 index 0000000..7af19b4 --- /dev/null +++ b/backend/scripts/lint.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# Script para ejecutar todas las herramientas de linting y formateo + +set -e # Salir si algún comando falla + +echo "==================================" +echo " Análisis de Calidad de Código" +echo "==================================" +echo "" + +# Colores para output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Función para ejecutar comando y capturar resultado +run_check() { + local name=$1 + local cmd=$2 + + echo -e "${YELLOW}▶ Ejecutando $name...${NC}" + if eval "$cmd"; then + echo -e "${GREEN}✓ $name: OK${NC}" + echo "" + return 0 + else + echo -e "${RED}✗ $name: FALLÓ${NC}" + echo "" + return 1 + fi +} + +# Contador de errores +ERRORS=0 + +# 1. Black - Verificar formato +if ! run_check "Black (formato)" "black --check --diff ."; then + echo -e "${YELLOW} Tip: Ejecuta 'black .' para formatear automáticamente${NC}" + ERRORS=$((ERRORS + 1)) +fi + +# 2. isort - Verificar imports +if ! run_check "isort (imports)" "isort --check-only --diff ."; then + echo -e "${YELLOW} Tip: Ejecuta 'isort .' para ordenar imports automáticamente${NC}" + ERRORS=$((ERRORS + 1)) +fi + +# 3. Flake8 - Verificar PEP8 +if ! run_check "Flake8 (PEP8)" "flake8 ."; then + ERRORS=$((ERRORS + 1)) +fi + +# 4. Pylint - Análisis estático +if ! run_check "Pylint (análisis estático)" "pylint --rcfile=pyproject.toml --exit-zero authentication attendance events mac_attendance"; then + echo -e "${YELLOW} Nota: Pylint puede mostrar warnings que no son críticos${NC}" + # No incrementar errores, pylint es solo informativo +fi + +# 5. Bandit - Seguridad +if ! run_check "Bandit (seguridad)" "bandit -r . -c pyproject.toml"; then + ERRORS=$((ERRORS + 1)) +fi + +# 6. MyPy - Type checking (opcional) +echo -e "${YELLOW}▶ Ejecutando MyPy (type checking)...${NC}" +if mypy --config-file=pyproject.toml . 2>/dev/null; then + echo -e "${GREEN}✓ MyPy: OK${NC}" +else + echo -e "${YELLOW}⚠ MyPy: Algunos errores de tipado (no críticos)${NC}" +fi +echo "" + +# Resumen +echo "==================================" +if [ $ERRORS -eq 0 ]; then + echo -e "${GREEN}✓ Todos los checks pasaron correctamente${NC}" + echo "==================================" + exit 0 +else + echo -e "${RED}✗ $ERRORS check(s) fallaron${NC}" + echo "==================================" + exit 1 +fi diff --git a/backend/scripts/test_ratelimit.py b/backend/scripts/test_ratelimit.py new file mode 100644 index 0000000..ecde0f0 --- /dev/null +++ b/backend/scripts/test_ratelimit.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +""" +Script de prueba para verificar rate limiting +Ejecutar con el servidor de desarrollo corriendo: + python test_ratelimit.py +""" + +import requests +import time +import json + +API_BASE_URL = 'http://127.0.0.1:8000/api' + +def test_login_rate_limit(): + """ + Probar rate limiting en login (límite: 5/min) + """ + print("\n" + "="*60) + print("TEST: Rate Limiting en Login (5 intentos por minuto)") + print("="*60) + + endpoint = f"{API_BASE_URL}/auth/login/" + credentials = { + "account_number": "9999999", # Cuenta que no existe + "password": "wrongpassword" + } + + print(f"\nEnviando 7 solicitudes a {endpoint}") + print("Esperado: Primeras 5 pasan, las siguientes 2 reciben 429\n") + + for i in range(1, 8): + try: + response = requests.post(endpoint, json=credentials, timeout=5) + + if response.status_code == 429: + print(f"[{i}] ❌ 429 TOO MANY REQUESTS - Rate limit alcanzado!") + print(f" Respuesta: {response.json()}") + elif response.status_code == 400: + print(f"[{i}] ✓ 400 BAD REQUEST - Solicitud procesada (credenciales inválidas)") + else: + print(f"[{i}] ✓ {response.status_code} - Solicitud procesada") + + except requests.exceptions.ConnectionError: + print(f"[{i}] ❌ Error: No se pudo conectar al servidor") + print(" Asegúrate de que el servidor esté corriendo:") + print(" cd backend && python manage.py runserver") + break + except Exception as e: + print(f"[{i}] ❌ Error: {e}") + + time.sleep(0.5) # Pequeña pausa entre solicitudes + +def test_check_auth_rate_limit(): + """ + Probar rate limiting en check-auth (límite: 30/min) + """ + print("\n" + "="*60) + print("TEST: Rate Limiting en Check Auth (30 por minuto)") + print("="*60) + + endpoint = f"{API_BASE_URL}/auth/check-auth/" + + print(f"\nEnviando 32 solicitudes a {endpoint}") + print("Esperado: Primeras 30 pasan, las siguientes 2 reciben 429\n") + + success_count = 0 + rate_limited_count = 0 + + for i in range(1, 33): + try: + response = requests.get(endpoint, timeout=5) + + if response.status_code == 429: + rate_limited_count += 1 + if rate_limited_count == 1: # Solo mostrar el primero + print(f"[{i}] ❌ 429 - Rate limit alcanzado!") + elif rate_limited_count <= 3: + print(f"[{i}] ❌ 429") + elif response.status_code == 200: + success_count += 1 + if success_count <= 3 or success_count == 30: # Mostrar algunos + print(f"[{i}] ✓ 200 - OK") + + except requests.exceptions.ConnectionError: + print(f"[{i}] ❌ Error: Servidor no disponible") + break + except Exception as e: + print(f"[{i}] ❌ Error: {e}") + break + + if i % 10 == 0: + print(f" ... {i} solicitudes enviadas ...") + + print(f"\nResultado:") + print(f" - Exitosas: {success_count}") + print(f" - Rate limited: {rate_limited_count}") + +def main(): + print("\n" + "#"*60) + print("# PRUEBAS DE RATE LIMITING - MAC Attendance") + print("#"*60) + print("\nAsegúrate de que el servidor esté corriendo:") + print(" cd backend && python manage.py runserver\n") + + input("Presiona ENTER para comenzar las pruebas...") + + # Probar login rate limit + test_login_rate_limit() + + print("\n\nEsperando 5 segundos antes del siguiente test...") + time.sleep(5) + + # Probar check-auth rate limit + test_check_auth_rate_limit() + + print("\n" + "="*60) + print("PRUEBAS COMPLETADAS") + print("="*60) + print("\nNOTA: Si necesitas desactivar rate limiting para testing:") + print(" 1. Agregar RATELIMIT_ENABLE=False en .env") + print(" 2. O cambiar block=False en los decoradores\n") + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + print("\n\nPrueba interrumpida por el usuario") \ No newline at end of file diff --git a/backend/static/.gitkeep b/backend/static/.gitkeep new file mode 100644 index 0000000..cccd46c --- /dev/null +++ b/backend/static/.gitkeep @@ -0,0 +1 @@ +# Este archivo mantiene la carpeta static en git \ No newline at end of file diff --git a/backend/templates/admin/asistente_reporte.html b/backend/templates/admin/asistente_reporte.html new file mode 100644 index 0000000..169297e --- /dev/null +++ b/backend/templates/admin/asistente_reporte.html @@ -0,0 +1,122 @@ +{% extends "admin/base_site.html" %} +{% load static %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+

{{ title }}

+ + {% for data in asistentes_data %} +
+

+ 👤 {{ data.asistente.user_profile.full_name }} + (Cuenta: {{ data.asistente.user_profile.account_number }}) +

+ +
+
+
{{ data.total }}
+
Registros Totales
+
+
+
{{ data.total_estudiantes }}
+
Estudiantes Regulares
+
+
+
{{ data.total_externos }}
+
Usuarios Externos
+
+
+ + {% if data.estudiantes %} +

📚 Estudiantes Regulares ({{ data.total_estudiantes }})

+
+ + + + + + + + + + + + {% for reg in data.estudiantes %} + + + + + + + + {% endfor %} + +
Fecha y HoraEstudianteNúmero de CuentaEventoMétodo
{{ reg.timestamp|date:"d/m/Y H:i" }}{{ reg.student.full_name }}{{ reg.student.account_number }}{{ reg.event.title }} + {% if reg.registration_method == 'manual' %} + ✍️ Manual + {% elif reg.registration_method == 'barcode' %} + 📱 Código + {% else %} + {{ reg.registration_method }} + {% endif %} +
+
+ {% endif %} + + {% if data.externos %} +

🌍 Usuarios Externos ({{ data.total_externos }})

+
+ + + + + + + + + + + + {% for reg in data.externos %} + + + + + + + + {% endfor %} + +
Fecha y HoraUsuario ExternoID TemporalInstituciónEvento
{{ reg.timestamp|date:"d/m/Y H:i" }}{{ reg.external_user.full_name }}{{ reg.external_user.temporary_id }}{{ reg.external_user.institution }}{{ reg.event.title }}
+
+ {% endif %} + + {% if not data.estudiantes and not data.externos %} +

+ Este asistente aún no ha registrado ninguna asistencia. +

+ {% endif %} +
+ {% endfor %} + +
+ + + + +
+
+ + +{% endblock %} diff --git a/backend/tox.ini b/backend/tox.ini new file mode 100644 index 0000000..5e0b748 --- /dev/null +++ b/backend/tox.ini @@ -0,0 +1,192 @@ +# Configuración de Tox para testing multi-entorno +# Sistema de Asistencia MAC + +[tox] +# Entornos a ejecutar por defecto +envlist = + py311-django52 + lint + format-check + type-check + security + coverage + +# Requiere Python 3.11+ +minversion = 4.0 +skipsdist = True + +[testenv] +# Configuración base para todos los entornos +setenv = + PYTHONPATH = {toxinidir} + DJANGO_SETTINGS_MODULE = mac_attendance.settings + DEBUG = False + SECRET_KEY = test-secret-key-for-tox + DB_ENGINE = postgresql + DB_NAME = test_mac_attendance + DB_USER = mac_user + DB_PASSWORD = mac_password_2024_secure + DB_HOST = localhost + DB_PORT = 5432 + +passenv = + CI + POSTGRES_* + DB_* + +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/requirements-dev.txt + +# Entorno principal: Tests con pytest +[testenv:py311-django52] +description = Run tests with Python 3.11 and Django 5.2 +commands = + pytest {posargs:--cov=. --cov-report=html --cov-report=term-missing} + +[testenv:test] +description = Run tests quickly (alias) +commands = + pytest {posargs} + +[testenv:test-fast] +description = Run tests without coverage (faster) +commands = + pytest {posargs:--no-cov} + +# Linting y formateo +[testenv:lint] +description = Run all linters (flake8, pylint) +commands = + flake8 {posargs:.} + pylint {posargs:authentication attendance events mac_attendance} + +[testenv:format] +description = Format code with black and isort +commands = + black {posargs:.} + isort {posargs:.} + +[testenv:format-check] +description = Check code formatting without modifying +commands = + black --check --diff {posargs:.} + isort --check-only --diff {posargs:.} + +# Type checking +[testenv:type-check] +description = Run mypy type checker +commands = + mypy {posargs:.} + +# Seguridad +[testenv:security] +description = Run security checks +commands = + bandit -r {posargs:authentication attendance events mac_attendance} -ll + safety check --json + +[testenv:security-full] +description = Run comprehensive security audit +commands = + bandit -r . -ll -f json -o bandit-report.json + safety check --full-report + +# Cobertura +[testenv:coverage] +description = Generate coverage reports +commands = + coverage run -m pytest + coverage report -m + coverage html + coverage xml + +[testenv:coverage-report] +description = Show coverage report +commands = + coverage report -m + +# Métricas de código +[testenv:metrics] +description = Generate code metrics +commands = + radon cc {posargs:. --min B} + radon mi {posargs:. --min B} + +[testenv:complexity] +description = Check code complexity +commands = + radon cc . --total-average --show-complexity + +# Documentación +[testenv:docs] +description = Generate documentation (placeholder) +commands = + python -c "print('Documentation generation not configured yet')" + +# Entorno limpio +[testenv:clean] +description = Clean up generated files +allowlist_externals = + rm + find +commands = + find . -type d -name __pycache__ -exec rm -rf {{}} + + find . -type f -name '*.pyc' -delete + find . -type f -name '*.pyo' -delete + rm -rf .pytest_cache .mypy_cache .tox htmlcov .coverage coverage.xml + +# Configuración de herramientas +[flake8] +max-line-length = 120 +exclude = + .git, + __pycache__, + */migrations/*, + */venv/*, + */env/*, + .venv, + .tox, + dist, + build, + staticfiles, + media +ignore = + E203, # whitespace before ':' + E266, # too many leading '#' for block comment + E501, # line too long (handled by black) + W503, # line break before binary operator + W504, # line break after binary operator +max-complexity = 10 + +[pytest] +DJANGO_SETTINGS_MODULE = mac_attendance.settings +python_files = tests.py test_*.py *_tests.py +addopts = + --verbose + --strict-markers + --tb=short +testpaths = tests +markers = + slow: marks tests as slow + integration: marks tests as integration tests + unit: marks tests as unit tests + +[coverage:run] +source = . +omit = + */migrations/* + */tests/* + */test_*.py + */__pycache__/* + */venv/* + */env/* + .venv/* + manage.py + */wsgi.py + */asgi.py + +[coverage:report] +precision = 2 +show_missing = True +skip_covered = False diff --git a/create_superuser.sh b/create_superuser.sh new file mode 100644 index 0000000..27aec50 --- /dev/null +++ b/create_superuser.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Script para crear un superusuario de Django en el contenedor + +echo "===================================" +echo "Creando superusuario de Django" +echo "===================================" +echo "" + +# Ejecutar el comando createsuperuser en el contenedor backend +docker-compose exec backend python manage.py createsuperuser + +echo "" +echo "===================================" +echo "Superusuario creado exitosamente!" +echo "===================================" +echo "" +echo "Puedes acceder al panel de administración en:" +echo " http://localhost/admin/" +echo "" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..00723af --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,85 @@ +# Docker Compose para Entorno de Desarrollo +# Incluye todas las herramientas de desarrollo y testing + +services: + # PostgreSQL Database (Desarrollo) + db: + image: postgres:15-alpine + environment: + - POSTGRES_DB=mac_attendance + - POSTGRES_USER=mac_user + - POSTGRES_PASSWORD=mac_password_2024_secure + volumes: + - postgres_data_dev:/var/lib/postgresql/data + networks: + - app-network + ports: + - "5432:5432" # Expuesto para acceso desde host + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mac_user -d mac_attendance"] + interval: 10s + timeout: 5s + retries: 5 + + # Backend Django (Desarrollo) + backend: + build: + context: . + dockerfile: docker/Dockerfile.backend.dev + command: > + sh -c "python manage.py migrate && + python manage.py collectstatic --noinput && + python manage.py runserver 0.0.0.0:8000" + volumes: + - ./backend:/app + - static_volume:/app/staticfiles + - media_volume:/app/media + environment: + - DEBUG=True + - SECRET_KEY=django-insecure-dev-key-change-in-production-12345 + - ALLOWED_HOSTS=localhost,127.0.0.1,nginx,backend + - RATELIMIT_ENABLE=False + - SECURE_SSL_REDIRECT=False + - SESSION_COOKIE_SECURE=False + - CSRF_COOKIE_SECURE=False + - CORS_ALLOWED_ORIGINS=http://localhost,http://127.0.0.1,http://localhost:80 + - DB_ENGINE=postgresql + - DB_NAME=mac_attendance + - DB_USER=mac_user + - DB_PASSWORD=mac_password_2024_secure + - DB_HOST=db + - DB_PORT=5432 + networks: + - app-network + depends_on: + db: + condition: service_healthy + expose: + - "8000" + stdin_open: true # Para ipdb y debugging interactivo + tty: true + + # Frontend React + Nginx + nginx: + build: + context: . + dockerfile: docker/Dockerfile.frontend + ports: + - "80:80" + volumes: + - ./docker/nginx.conf:/etc/nginx/conf.d/default.conf + - static_volume:/app/staticfiles + - media_volume:/app/media + depends_on: + - backend + networks: + - app-network + +networks: + app-network: + driver: bridge + +volumes: + static_volume: + media_volume: + postgres_data_dev: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5cc5020 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,80 @@ +services: + # PostgreSQL Database + db: + image: postgres:15-alpine + environment: + - POSTGRES_DB=mac_attendance + - POSTGRES_USER=mac_user + - POSTGRES_PASSWORD=mac_password_2024_secure + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - app-network + ports: + - "5432:5432" # Expuesto para acceso desde host (comentar en producción si no es necesario) + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mac_user -d mac_attendance"] + interval: 10s + timeout: 5s + retries: 5 + + # Backend Django + backend: + build: + context: . + dockerfile: docker/Dockerfile.backend + command: > + sh -c "python manage.py migrate && + python manage.py collectstatic --noinput && + gunicorn --bind 0.0.0.0:8000 --workers 3 --timeout 120 mac_attendance.wsgi:application" + volumes: + - ./backend:/app + - static_volume:/app/staticfiles + - media_volume:/app/media + environment: + - DEBUG=True + - SECRET_KEY=django-insecure-dev-key-change-in-production-12345 + - ALLOWED_HOSTS=localhost,127.0.0.1,nginx,backend + - RATELIMIT_ENABLE=False + - SECURE_SSL_REDIRECT=False + - SESSION_COOKIE_SECURE=False + - CSRF_COOKIE_SECURE=False + - CORS_ALLOWED_ORIGINS=http://localhost,http://127.0.0.1,http://localhost:80 + - DB_ENGINE=postgresql + - DB_NAME=mac_attendance + - DB_USER=mac_user + - DB_PASSWORD=mac_password_2024_secure + - DB_HOST=db + - DB_PORT=5432 + networks: + - app-network + depends_on: + db: + condition: service_healthy + expose: + - "8000" + + # Frontend React + Nginx + nginx: + build: + context: . + dockerfile: docker/Dockerfile.frontend + ports: + - "80:80" + volumes: + - ./docker/nginx.conf:/etc/nginx/conf.d/default.conf + - static_volume:/app/staticfiles + - media_volume:/app/media + depends_on: + - backend + networks: + - app-network + +networks: + app-network: + driver: bridge + +volumes: + static_volume: + media_volume: + postgres_data: diff --git a/docker/Dockerfile.backend b/docker/Dockerfile.backend new file mode 100644 index 0000000..ac569aa --- /dev/null +++ b/docker/Dockerfile.backend @@ -0,0 +1,38 @@ +# Dockerfile para Backend Django +FROM python:3.11-slim + +# Variables de entorno para Python +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Instalar dependencias del sistema +RUN apt-get update && apt-get install -y \ + gcc \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# Crear directorio de trabajo +WORKDIR /app + +# Copiar requirements y instalar dependencias Python +COPY backend/requirements.txt . +RUN pip install --upgrade pip && \ + pip install -r requirements.txt && \ + pip install gunicorn whitenoise + +# Copiar código del backend +COPY backend/ . + +# Crear directorio para archivos estáticos y media +RUN mkdir -p /app/staticfiles /app/media + +# Colectar archivos estáticos +RUN python manage.py collectstatic --noinput || true + +# Exponer puerto +EXPOSE 8000 + +# Comando para ejecutar con gunicorn +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "mac_attendance.wsgi:application"] diff --git a/docker/Dockerfile.backend.dev b/docker/Dockerfile.backend.dev new file mode 100644 index 0000000..e6bba27 --- /dev/null +++ b/docker/Dockerfile.backend.dev @@ -0,0 +1,41 @@ +# Dockerfile para Backend Django - Entorno de Desarrollo +FROM python:3.11-slim + +# Variables de entorno para Python +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Instalar dependencias del sistema +RUN apt-get update && apt-get install -y \ + gcc \ + postgresql-client \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Crear directorio de trabajo +WORKDIR /app + +# Copiar requirements y instalar dependencias Python +COPY backend/requirements.txt . +COPY backend/requirements-dev.txt . +RUN pip install --upgrade pip && \ + pip install -r requirements.txt && \ + pip install -r requirements-dev.txt && \ + pip install gunicorn whitenoise + +# Copiar código del backend +COPY backend/ . + +# Crear directorio para archivos estáticos y media +RUN mkdir -p /app/staticfiles /app/media /app/logs + +# Crear directorio para reportes de cobertura +RUN mkdir -p /app/htmlcov + +# Exponer puerto +EXPOSE 8000 + +# Comando para ejecutar con Django development server +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/docker/Dockerfile.frontend b/docker/Dockerfile.frontend new file mode 100644 index 0000000..5ba5d6e --- /dev/null +++ b/docker/Dockerfile.frontend @@ -0,0 +1,27 @@ +# Etapa 1: Build del Frontend +FROM node:20-alpine AS frontend-build + +WORKDIR /app/frontend + +# Copiar archivos de dependencias +COPY frontend/package*.json ./ + +# Instalar dependencias (incluyendo devDependencies para el build) +RUN npm ci + +# Copiar código fuente +COPY frontend/ ./ + +# Construir aplicación para producción +RUN npm run build + +# Etapa 2: Nginx para servir el frontend +FROM nginx:alpine + +# Copiar archivos construidos del frontend +COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html + +# Exponer puerto +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..0a8e8ba --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,69 @@ +upstream backend { + server backend:8000; +} + +server { + listen 80; + server_name localhost; + client_max_body_size 100M; + + # Logs + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Frontend - Servir archivos estáticos de React + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + # API Backend - Proxy al backend Django + location /api/ { + proxy_pass http://backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Admin de Django + location /admin/ { + proxy_pass http://backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + } + + # Archivos estáticos de Django + location /static/ { + alias /app/staticfiles/; + expires 30d; + add_header Cache-Control "public, immutable"; + } + + # Archivos media de Django + location /media/ { + alias /app/media/; + expires 30d; + add_header Cache-Control "public"; + } + + # Configuración de gzip + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript + application/json application/javascript application/xml+rss + application/rss+xml font/truetype font/opentype + application/vnd.ms-fontobject image/svg+xml; +} diff --git a/docs/ACCESO_ADMIN.md b/docs/ACCESO_ADMIN.md new file mode 100644 index 0000000..50218a9 --- /dev/null +++ b/docs/ACCESO_ADMIN.md @@ -0,0 +1,234 @@ +# Acceso al Panel de Administración de Django + +## Estado Actual + +Los contenedores Docker están corriendo correctamente: +- ✅ **Frontend (Nginx)**: http://localhost +- ✅ **Backend (Django)**: Disponible en http://localhost/api/ +- ✅ **Panel Admin**: http://localhost/admin/ + +## Problema Resuelto + +Se han corregido los siguientes problemas: + +1. **STATIC_ROOT no configurado**: Se agregó `STATIC_ROOT = BASE_DIR / 'staticfiles'` en `settings.py` +2. **MEDIA_ROOT no configurado**: Se agregó `MEDIA_ROOT = BASE_DIR / 'media'` en `settings.py` +3. **Variables de entorno faltantes**: Se agregaron todas las variables necesarias en `docker-compose.yml` +4. **PostgreSQL removido**: Se simplificó usando SQLite para facilitar el desarrollo + +## Crear Superusuario + +Para acceder al panel de administración, primero necesitas crear un superusuario. + +### En Windows (PowerShell): + +```powershell +docker-compose exec backend python manage.py createsuperuser +``` + +### En Linux/Mac: + +```bash +bash create_superuser.sh +``` + +O manualmente: + +```bash +docker-compose exec backend python manage.py createsuperuser +``` + +### Proceso Interactivo: + +El comando te pedirá: +1. **Username**: Ingresa un nombre de usuario (ejemplo: admin) +2. **Email**: Ingresa un email (ejemplo: admin@example.com) +3. **Password**: Ingresa una contraseña segura +4. **Password (again)**: Confirma la contraseña + +Ejemplo: +``` +Username: admin +Email address: admin@example.com +Password: ********** +Password (again): ********** +Superuser created successfully. +``` + +## Acceder al Panel de Administración + +1. Abre tu navegador web +2. Ve a: **http://localhost/admin/** +3. Ingresa tus credenciales (username y password que creaste) +4. ¡Listo! Ya puedes administrar tu aplicación + +## URLs Disponibles + +| Servicio | URL | Descripción | +|----------|-----|-------------| +| Frontend | http://localhost | Aplicación React | +| API Backend | http://localhost/api/ | API REST de Django | +| Admin Django | http://localhost/admin/ | Panel de administración | +| Archivos Estáticos | http://localhost/static/ | CSS, JS, imágenes de Django | +| Archivos Media | http://localhost/media/ | Archivos subidos por usuarios | + +## Verificar Estado de los Contenedores + +```bash +# Ver estado de los contenedores +docker-compose ps + +# Ver logs del backend +docker-compose logs backend + +# Ver logs del nginx +docker-compose logs nginx + +# Seguir logs en tiempo real +docker-compose logs -f +``` + +## Comandos Útiles de Django + +```bash +# Crear migraciones +docker-compose exec backend python manage.py makemigrations + +# Aplicar migraciones +docker-compose exec backend python manage.py migrate + +# Crear superusuario adicional +docker-compose exec backend python manage.py createsuperuser + +# Acceder al shell de Django +docker-compose exec backend python manage.py shell + +# Colectar archivos estáticos +docker-compose exec backend python manage.py collectstatic --noinput + +# Ver rutas disponibles +docker-compose exec backend python manage.py show_urls +``` + +## Solución de Problemas + +### Error 502 Bad Gateway + +Si ves este error, verifica que el backend esté corriendo: + +```bash +docker-compose logs backend +docker-compose restart backend +``` + +### No puedo acceder a /admin/ + +1. Verifica que el backend esté corriendo: + ```bash + docker-compose ps + ``` + +2. Verifica los logs de nginx: + ```bash + docker-compose logs nginx + ``` + +3. Verifica que hayas creado un superusuario + +### Error de permisos + +Si hay errores de permisos en archivos: + +```bash +# En el backend +docker-compose exec backend chmod -R 755 /app/staticfiles +docker-compose exec backend chmod -R 755 /app/media +``` + +### Reiniciar todo desde cero + +Si algo sale mal, puedes reiniciar todo: + +```bash +# Detener y eliminar todo (¡cuidado! elimina la base de datos) +docker-compose down -v + +# Reconstruir +docker-compose build --no-cache + +# Levantar +docker-compose up -d + +# Aplicar migraciones +docker-compose exec backend python manage.py migrate + +# Crear superusuario +docker-compose exec backend python manage.py createsuperuser +``` + +## Configuración de Producción + +Para producción, debes cambiar estas variables en `docker-compose.yml`: + +```yaml +environment: + - DEBUG=False # ⚠️ IMPORTANTE: Cambiar a False en producción + - SECRET_KEY=cambiar-por-una-clave-super-segura-y-aleatoria + - ALLOWED_HOSTS=tu-dominio.com,www.tu-dominio.com + - SECURE_SSL_REDIRECT=True + - SESSION_COOKIE_SECURE=True + - CSRF_COOKIE_SECURE=True +``` + +También deberías considerar usar PostgreSQL en lugar de SQLite para producción. + +## Arquitectura + +``` +┌─────────────────────────────────────────┐ +│ Cliente (Navegador) │ +└────────────────┬────────────────────────┘ + │ HTTP + ▼ +┌─────────────────────────────────────────┐ +│ Nginx (Puerto 80) │ +│ ├─ / → Frontend React (build estático) │ +│ ├─ /api/* → Backend Django (proxy) │ +│ ├─ /admin/* → Django Admin (proxy) │ +│ ├─ /static/* → Archivos estáticos │ +│ └─ /media/* → Archivos media │ +└────────────────┬────────────────────────┘ + │ + ┌─────────┴─────────┐ + ▼ ▼ +┌─────────────┐ ┌─────────────┐ +│ Frontend │ │ Backend │ +│ (React) │ │ (Django) │ +│ /usr/share/│ │ Puerto: │ +│ nginx/html │ │ 8000 │ +└─────────────┘ └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ SQLite │ + │ (db.sqlite3)│ + └─────────────┘ +``` + +## Seguridad + +- ✅ CORS configurado correctamente +- ✅ CSRF protection habilitado +- ✅ JWT Authentication para API +- ✅ Rate limiting disponible (deshabilitado en desarrollo) +- ⚠️ DEBUG=True (solo para desarrollo) +- ⚠️ Cambiar SECRET_KEY en producción +- ⚠️ Habilitar HTTPS en producción + +## Próximos Pasos + +1. ✅ Contenedores corriendo +2. ⚠️ Crear superusuario (pendiente - ejecuta el comando arriba) +3. ⚠️ Acceder a http://localhost/admin/ +4. ⚠️ Configurar usuarios y permisos +5. ⚠️ Probar las funcionalidades de la aplicación diff --git a/docs/API_DOCUMENTATION.md b/docs/API_DOCUMENTATION.md new file mode 100644 index 0000000..6546f8f --- /dev/null +++ b/docs/API_DOCUMENTATION.md @@ -0,0 +1,701 @@ +# Sistema de Asistencia MAC - Documentación de API + +## Información General + +- **Version**: 1.0.0 +- **Base URL**: `http://localhost/api/` +- **Autenticación**: JWT Bearer Token +- **Formato**: JSON + +## Índice + +- [Inicio Rápido](#inicio-rápido) +- [Autenticación](#autenticación) +- [Endpoints](#endpoints) + - [Authentication](#authentication-endpoints) + - [Events](#events-endpoints) + - [Attendance](#attendance-endpoints) +- [Códigos de Estado](#códigos-de-estado) +- [Ejemplos con cURL](#ejemplos-con-curl) +- [Testing con Postman](#testing-con-postman) + +## Inicio Rápido + +### Acceder a la Documentación + +- **API Root (JSON)**: http://localhost/api/ 🔒 **Requiere Autenticación** +- **Documentación HTML**: http://localhost/api/docs/ 🔒 **Requiere Autenticación** +- **Panel Admin**: http://localhost/admin/ + +> **Nota**: Los endpoints `/api/` y `/api/docs/` ahora requieren autenticación JWT. +> Primero debes hacer login en `/api/auth/login/` para obtener un token de acceso. + +### Flujo Básico + +1. **Login**: Obtener tokens de acceso +2. **Usar Token**: Incluir en header `Authorization` +3. **Refrescar Token**: Cuando expire el access token + +## Autenticación + +La API utiliza **JWT (JSON Web Tokens)** para autenticación. + +### Obtener Token + +```bash +POST /api/auth/login/ +Content-Type: application/json + +{ + "account_number": "3123123" +} +``` + +**Respuesta:** +```json +{ + "message": "Login exitoso", + "user": { + "id": 3, + "username": "3123123", + "profile": { + "account_number": "3123123", + "user_type": "assistant", + "full_name": "pancho" + } + }, + "tokens": { + "access": "eyJhbGci....", + "refresh": "eyJhbGci...." + } +} +``` + +### Usar Token + +Para endpoints que requieren autenticación, incluye el header: + +``` +Authorization: Bearer +``` + +### Refrescar Token + +```bash +POST /api/auth/token/refresh/ +Content-Type: application/json + +{ + "refresh": "" +} +``` + +## Endpoints + +### Authentication Endpoints + +#### 1. Login +``` +POST /api/auth/login/ +``` + +**Descripción**: Iniciar sesión con número de cuenta + +**Auth Required**: ❌ No + +**Body**: +```json +{ + "account_number": "1234567" +} +``` + +**Response 200**: +```json +{ + "message": "Login exitoso", + "user": { ... }, + "tokens": { + "access": "...", + "refresh": "..." + } +} +``` + +**Response 400**: +```json +{ + "account_number": ["El número de cuenta debe tener exactamente 7 dígitos."] +} +``` + +--- + +#### 2. Logout +``` +POST /api/auth/logout/ +``` + +**Descripción**: Cerrar sesión + +**Auth Required**: ✅ Sí + +**Response 200**: +```json +{ + "message": "Logout exitoso" +} +``` + +--- + +#### 3. Get Profile +``` +GET /api/auth/profile/ +``` + +**Descripción**: Obtener perfil del usuario autenticado + +**Auth Required**: ✅ Sí + +**Response 200**: +```json +{ + "id": 3, + "username": "3123123", + "email": "", + "profile": { + "account_number": "3123123", + "user_type": "assistant", + "full_name": "pancho" + }, + "is_staff": false +} +``` + +--- + +#### 4. Check Auth Status +``` +GET /api/auth/check-auth/ +``` + +**Descripción**: Verificar si el usuario está autenticado + +**Auth Required**: ❌ No + +**Response 200**: +```json +{ + "is_authenticated": true, + "user": { ... } +} +``` + +--- + +#### 5. Refresh Token +``` +POST /api/auth/token/refresh/ +``` + +**Descripción**: Refrescar access token usando refresh token + +**Auth Required**: ❌ No + +**Body**: +```json +{ + "refresh": "" +} +``` + +**Response 200**: +```json +{ + "access": "" +} +``` + +--- + +#### 6. System Configuration +``` +GET /api/auth/system-config/ +``` + +**Descripción**: Obtener configuración del sistema + +**Auth Required**: ✅ Sí + +**Response 200**: +```json +{ + "minimum_attendance_percentage": 80.0, + "minutes_before_event": 10, + "minutes_after_start": 25 +} +``` + +--- + +### Events Endpoints + +#### 1. List Events +``` +GET /api/events/ +``` + +**Descripción**: Listar todos los eventos activos + +**Auth Required**: ❌ No + +**Response 200**: +```json +[ + { + "id": 1, + "title": "Inteligencia Artificial", + "description": "Conferencia sobre IA", + "event_type": "conference", + "modality": "presencial", + "speaker": "Dr. García", + "date": "2025-10-12", + "start_time": "10:00:00", + "end_time": "12:00:00", + "location": "Auditorio A", + "max_capacity": 100, + "is_active": true + } +] +``` + +--- + +#### 2. Register External User +``` +POST /api/events/external/register/ +``` + +**Descripción**: Registrar un usuario externo para eventos + +**Auth Required**: ✅ Sí (Solo asistentes) + +**Body**: +```json +{ + "full_name": "Juan Pérez", + "account_number": "9999999" +} +``` + +**Response 201**: +```json +{ + "id": 10, + "full_name": "Juan Pérez", + "account_number": "9999999", + "status": "approved" +} +``` + +--- + +#### 3. Search External Users +``` +GET /api/events/external/search/?account_number=9999999 +``` + +**Descripción**: Buscar usuarios externos + +**Auth Required**: ✅ Sí + +**Query Parameters**: +- `account_number` (string): Número de cuenta a buscar + +**Response 200**: +```json +[ + { + "id": 10, + "full_name": "Juan Pérez", + "account_number": "9999999", + "status": "approved" + } +] +``` + +--- + +#### 4. Approve External User +``` +POST /api/events/external//approve/ +``` + +**Descripción**: Aprobar un usuario externo + +**Auth Required**: ✅ Sí (Solo asistentes) + +**Body**: +```json +{ + "approved": true +} +``` + +**Response 200**: +```json +{ + "message": "Usuario aprobado exitosamente" +} +``` + +--- + +### Attendance Endpoints + +#### 1. Register Attendance +``` +POST /api/attendance/ +``` + +**Descripción**: Registrar asistencia a un evento + +**Auth Required**: ✅ Sí (Solo asistentes) + +**Body**: +```json +{ + "event_id": 1, + "account_number": "3123123", + "registration_method": "manual" +} +``` + +**Response 201**: +```json +{ + "id": 42, + "event": 1, + "timestamp": "2025-10-12T10:30:00Z", + "is_valid": true +} +``` + +--- + +#### 2. Get Student Stats +``` +GET /api/attendance/stats/?account_number=0000111 +``` + +**Descripción**: Obtener estadísticas de asistencia de un estudiante + +**Auth Required**: ✅ Sí + +**Query Parameters**: +- `account_number` (string): Número de cuenta del estudiante + +**Response 200**: +```json +{ + "total_events": 10, + "attended_events": 8, + "attendance_percentage": 80.0, + "meets_minimum": true +} +``` + +--- + +#### 3. Get Recent Attendances +``` +GET /api/attendance/recent/ +``` + +**Descripción**: Obtener asistencias recientes (últimas 50) + +**Auth Required**: ✅ Sí + +**Response 200**: +```json +[ + { + "id": 42, + "student": { + "account_number": "0000111", + "full_name": "María García" + }, + "event": { + "id": 1, + "title": "Inteligencia Artificial" + }, + "timestamp": "2025-10-12T10:30:00Z", + "is_valid": true + } +] +``` + +--- + +#### 4. Get My Attendances +``` +GET /api/attendance/my/ +``` + +**Descripción**: Obtener mis asistencias registradas + +**Auth Required**: ✅ Sí + +**Response 200**: +```json +[ + { + "id": 42, + "event": { + "id": 1, + "title": "Inteligencia Artificial", + "date": "2025-10-12", + "start_time": "10:00:00" + }, + "timestamp": "2025-10-12T10:30:00Z", + "is_valid": true + } +] +``` + +--- + +## Códigos de Estado + +| Código | Descripción | +|--------|-------------| +| 200 | OK - Solicitud exitosa | +| 201 | Created - Recurso creado exitosamente | +| 400 | Bad Request - Datos inválidos | +| 401 | Unauthorized - No autenticado o token inválido | +| 403 | Forbidden - Sin permisos | +| 404 | Not Found - Recurso no encontrado | +| 429 | Too Many Requests - Rate limit excedido | +| 500 | Internal Server Error - Error del servidor | + +## Ejemplos con cURL + +### 1. Login y obtener token + +```bash +curl -X POST http://localhost/api/auth/login/ \ + -H "Content-Type: application/json" \ + -d '{"account_number":"3123123"}' +``` + +### 2. Obtener perfil (con token) + +```bash +TOKEN="your_access_token_here" + +curl -X GET http://localhost/api/auth/profile/ \ + -H "Authorization: Bearer $TOKEN" +``` + +### 3. Listar eventos + +```bash +curl -X GET http://localhost/api/events/ +``` + +### 4. Registrar asistencia + +```bash +TOKEN="your_access_token_here" + +curl -X POST http://localhost/api/attendance/ \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "event_id": 1, + "account_number": "0000111", + "registration_method": "manual" + }' +``` + +### 5. Obtener estadísticas + +```bash +TOKEN="your_access_token_here" + +curl -X GET "http://localhost/api/attendance/stats/?account_number=0000111" \ + -H "Authorization: Bearer $TOKEN" +``` + +### 6. Refrescar token + +```bash +REFRESH_TOKEN="your_refresh_token_here" + +curl -X POST http://localhost/api/auth/token/refresh/ \ + -H "Content-Type: application/json" \ + -d "{\"refresh\":\"$REFRESH_TOKEN\"}" +``` + +## Testing con Postman + +### Importar Collection + +1. Descarga la colección: `postman_collection.json` +2. En Postman: File → Import +3. Selecciona el archivo JSON + +### Variables de Entorno + +Crea un entorno con estas variables: + +```json +{ + "base_url": "http://localhost", + "access_token": "", + "refresh_token": "", + "account_number": "3123123" +} +``` + +### Workflow de Testing + +1. **Login**: Ejecuta el request de login +2. **Set Token**: Copia el access_token a la variable de entorno +3. **Test Endpoints**: Ejecuta otros requests usando `{{access_token}}` + +## Rate Limiting + +La API implementa rate limiting para prevenir abuso: + +- **Login**: 5 intentos por minuto por IP +- **Check Auth**: 30 consultas por minuto por IP +- **Token Refresh**: 10 intentos por minuto por IP + +## Errores Comunes + +### 401 Unauthorized + +**Causa**: Token inválido o expirado + +**Solución**: Refrescar el token o hacer login nuevamente + +```bash +# Refrescar token +curl -X POST http://localhost/api/auth/token/refresh/ \ + -H "Content-Type: application/json" \ + -d '{"refresh":""}' +``` + +### 400 Bad Request + +**Causa**: Datos inválidos en el request + +**Solución**: Verificar el formato y campos requeridos + +```json +{ + "account_number": ["El número de cuenta debe tener exactamente 7 dígitos."] +} +``` + +### 429 Too Many Requests + +**Causa**: Rate limit excedido + +**Solución**: Esperar un minuto antes de reintentar + +## Best Practices + +### 1. Manejo de Tokens + +```javascript +// Guardar tokens +localStorage.setItem('access_token', response.tokens.access); +localStorage.setItem('refresh_token', response.tokens.refresh); + +// Incluir en requests +headers: { + 'Authorization': `Bearer ${localStorage.getItem('access_token')}` +} + +// Refrescar cuando expire +if (error.status === 401) { + // Intentar refrescar + const newToken = await refreshToken(); + // Reintentar request original +} +``` + +### 2. Manejo de Errores + +```javascript +try { + const response = await fetch('/api/endpoint/', options); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Request failed'); + } + return await response.json(); +} catch (error) { + console.error('API Error:', error); + // Mostrar mensaje al usuario +} +``` + +### 3. Rate Limiting + +```javascript +// Implementar backoff exponencial +const retryWithBackoff = async (fn, retries = 3) => { + for (let i = 0; i < retries; i++) { + try { + return await fn(); + } catch (error) { + if (error.status === 429 && i < retries - 1) { + await new Promise(r => setTimeout(r, 2 ** i * 1000)); + continue; + } + throw error; + } + } +}; +``` + +## Seguridad + +### Headers de Seguridad + +La API implementa los siguientes headers de seguridad: + +- `X-Content-Type-Options: nosniff` +- `X-Frame-Options: DENY` +- `X-XSS-Protection: 1; mode=block` + +### CORS + +CORS está configurado para permitir requests desde: +- `http://localhost` +- `http://127.0.0.1` + +### HTTPS + +En producción, **siempre** usar HTTPS para proteger los tokens. + +## Soporte + +- **Documentación**: http://localhost/api/docs/ +- **Admin Panel**: http://localhost/admin/ +- **Issues**: https://github.com/yourusername/mac_attendance/issues + +## Changelog + +### v1.0.0 (2025-10-12) +- ✅ Autenticación JWT +- ✅ Gestión de eventos +- ✅ Registro de asistencias +- ✅ Estadísticas de asistencia +- ✅ Usuarios externos +- ✅ Rate limiting +- ✅ Documentación completa diff --git a/docs/CHANGELOG_FIX.md b/docs/CHANGELOG_FIX.md new file mode 100644 index 0000000..94512c6 --- /dev/null +++ b/docs/CHANGELOG_FIX.md @@ -0,0 +1,207 @@ +# Changelog - Corrección de Errores de Autenticación + +**Fecha**: 2025-10-12 +**Versión**: 1.0.1 + +## Problemas Resueltos + +### 1. Error 403 CSRF en Login desde Navegador + +**Síntoma**: Al intentar hacer login desde el navegador web, el backend retornaba error 403 (Forbidden) con mensaje "Acceso denegado (permisos insuficientes)". + +**Causa Raíz**: Django estaba aplicando validación CSRF a todas las rutas, incluyendo las rutas de API REST que usan JWT para autenticación. El middleware `CsrfViewMiddleware` rechazaba requests sin token CSRF. + +**Solución Implementada**: + +1. **Creado nuevo middleware** `DisableCSRFOnAPIMiddleware` en `backend/mac_attendance/middleware.py` + - Desactiva validación CSRF para todas las rutas que coincidan con `^api/.*$` + - Colocado antes de `CsrfViewMiddleware` en la cadena de middleware + +2. **Actualizado `settings.py`**: + - Agregado `CSRF_EXEMPT_URLS = [r'^api/.*$']` + - Actualizado `CORS_ALLOWED_ORIGINS` para incluir `http://localhost` + - Agregado `CSRF_TRUSTED_ORIGINS` para desarrollo local + - Agregado middleware en posición correcta (línea 45) + +3. **Configuración final de middleware**: +```python +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'mac_attendance.middleware.DisableCSRFOnAPIMiddleware', # ← NUEVO + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'mac_attendance.middleware.AuditMiddleware', +] +``` + +**Resultado**: ✅ Login desde navegador funciona correctamente sin error 403 + +--- + +### 2. Endpoint `/api/` Expuesto Públicamente + +**Síntoma**: Cualquier usuario no autenticado podía acceder a `/api/` y `/api/docs/` para ver información sobre todos los endpoints del sistema. + +**Riesgo de Seguridad**: Exposición de información sensible sobre la estructura de la API. + +**Solución Implementada**: + +1. **Actualizado `backend/mac_attendance/urls.py`**: + - Agregado decorador `@permission_classes([IsAuthenticated])` a función `api_root()` + - Agregado decorador `@permission_classes([IsAuthenticated])` a función `api_docs()` + - Ambos endpoints ahora requieren token JWT válido + +2. **Código modificado**: +```python +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def api_root(request): + """API root endpoint - Returns comprehensive API information (Protected)""" + # ... + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def api_docs(request): + """API documentation endpoint (Protected)""" + # ... +``` + +**Resultado**: +- ✅ `/api/` sin autenticación → Error 401 "Las credenciales de autenticación no se proveyeron" +- ✅ `/api/` con token válido → Muestra información de la API +- ✅ `/api/docs/` ahora también protegido + +--- + +## Archivos Modificados + +### 1. `backend/mac_attendance/middleware.py` +- **Agregado**: Clase `DisableCSRFOnAPIMiddleware` +- **Líneas**: 103-126 + +### 2. `backend/mac_attendance/settings.py` +- **Modificado**: Lista `MIDDLEWARE` (línea 40-51) +- **Agregado**: `CSRF_EXEMPT_URLS` (línea 154) +- **Modificado**: `CORS_ALLOWED_ORIGINS` (línea 140-143) +- **Agregado**: `CSRF_TRUSTED_ORIGINS` (línea 148-151) + +### 3. `backend/mac_attendance/urls.py` +- **Modificado**: Función `api_root()` - agregado autenticación (línea 23-26) +- **Modificado**: Función `api_docs()` - agregado autenticación (línea 138-140) + +### 4. `API_DOCUMENTATION.md` +- **Actualizado**: Sección "Acceder a la Documentación" con advertencia de autenticación + +--- + +## Testing Realizado + +### Test 1: Login desde cURL (simulando navegador) +```bash +curl -X POST http://localhost/api/auth/login/ \ + -H "Content-Type: application/json" \ + -H "Origin: http://localhost" \ + -d '{"account_number":"3123123"}' +``` +**Resultado**: ✅ Status 200, retorna tokens JWT + +### Test 2: API Root sin autenticación +```bash +curl -X GET http://localhost/api/ +``` +**Resultado**: ✅ Status 401, mensaje de error apropiado + +### Test 3: API Root con autenticación +```bash +curl -X GET http://localhost/api/ \ + -H "Authorization: Bearer " +``` +**Resultado**: ✅ Status 200, retorna información de API + +--- + +## Seguridad Mejorada + +### Antes de la Corrección: +- ❌ Login fallaba con 403 desde navegador +- ❌ `/api/` accesible sin autenticación +- ❌ `/api/docs/` accesible sin autenticación +- ⚠️ Información de endpoints expuesta públicamente + +### Después de la Corrección: +- ✅ Login funciona correctamente desde navegador +- ✅ `/api/` requiere autenticación JWT +- ✅ `/api/docs/` requiere autenticación JWT +- ✅ Información de API protegida +- ✅ CSRF deshabilitado solo en rutas API (usamos JWT) +- ✅ CSRF sigue activo en panel admin (usa sesiones) + +--- + +## Notas Técnicas + +### ¿Por qué deshabilitar CSRF en API? + +Django REST Framework con JWT no necesita protección CSRF porque: +1. **No usa cookies de sesión**: JWT se envía en header `Authorization` +2. **Tokens stateless**: No hay estado de sesión en servidor +3. **CORS protege**: CORS ya previene requests desde orígenes no autorizados +4. **JWT expira**: Tokens tienen tiempo de vida limitado (1 hora) + +### Alternativa No Recomendada +Usar `@csrf_exempt` en cada vista → ❌ Requiere modificar múltiples archivos +**Nuestra solución**: Middleware centralizado → ✅ Un solo punto de configuración + +--- + +## Cómo Probar + +### 1. Desde el navegador (http://localhost) +1. Abrir la aplicación +2. Ingresar número de cuenta: `3123123` +3. Click en "Iniciar Sesión" +4. ✅ Debe iniciar sesión exitosamente + +### 2. Verificar protección de `/api/` +1. Abrir navegador e ir a http://localhost/api/ +2. ✅ Debe mostrar: `{"detail": "Las credenciales de autenticación no se proveyeron."}` + +### 3. Acceder a `/api/` autenticado +1. Hacer login primero +2. Copiar el access_token +3. Usar extensión de navegador o Postman: + - URL: http://localhost/api/ + - Header: `Authorization: Bearer ` +4. ✅ Debe mostrar información completa de la API + +--- + +## Próximos Pasos (Opcional) + +- [ ] Implementar rate limiting específico para login (ya existe global) +- [ ] Agregar logs de accesos exitosos/fallidos a `/api/` +- [ ] Considerar implementar API key para documentación pública +- [ ] Agregar tests automatizados para validación CSRF + +--- + +## Contacto + +Si encuentras algún problema con estas correcciones, revisa: +1. Logs del backend: `docker-compose logs backend` +2. Logs de nginx: `docker-compose logs nginx` +3. Consola del navegador (F12) para ver errores de JavaScript + +--- + +**Revisado y probado**: ✅ +**Contenedores actualizados**: ✅ +**Documentación actualizada**: ✅ diff --git a/docs/COMO_USAR_ESCANER_USB.md b/docs/COMO_USAR_ESCANER_USB.md new file mode 100644 index 0000000..287b7fa --- /dev/null +++ b/docs/COMO_USAR_ESCANER_USB.md @@ -0,0 +1,143 @@ +# 📱 Cómo Usar un Escáner de Código de Barras USB + +## ✅ Qué es un Escáner USB + +Un **escáner de código de barras USB** (también llamado pistola o lector) es un dispositivo físico que se conecta a la computadora por USB y **funciona automáticamente como un teclado**. + +### Tipos de Escáneres USB: +- 🔫 **Pistola/Lector de mano** - Se sostiene y se apunta al código +- 📟 **Lector fijo de mesa** - Se coloca en el escritorio y se pasa el código +- 🖊️ **Lápiz lector** - Tipo pluma, se desliza sobre el código + +## 🔌 Cómo Funciona + +1. **Conectas el escáner USB a la computadora** +2. Windows/Mac/Linux lo reconoce automáticamente como teclado +3. **No necesita drivers ni software especial** +4. Cuando escaneas un código de barras: + - El escáner lee el código + - **Escribe automáticamente los números** como si los escribieras en el teclado + - Presiona ENTER automáticamente (configurable) + +## 📋 Pasos para Usar con el Sistema + +### 1. Conectar el Escáner +``` +1. Conecta el escáner USB a la computadora +2. Espera el sonido de "dispositivo conectado" +3. ¡Listo! Ya funciona +``` + +### 2. Registrar Asistencia +``` +1. Abre el sistema en el navegador +2. Inicia sesión como ASISTENTE +3. Selecciona el EVENTO activo +4. Haz clic en el campo "Número de Cuenta" +5. Escanea el código de barras de la credencial +6. El número se escribe automáticamente +7. Presiona ENTER o clic en "Registrar Asistencia" +``` + +### 3. Flujo Rápido (Recomendado) +``` +1. Selecciona el evento +2. Mantén el cursor en el campo "Número de Cuenta" +3. Escanea código → automáticamente aparece el número +4. ENTER → se registra la asistencia +5. El cursor vuelve al campo +6. Repite: Escanea → ENTER → Escanea → ENTER +``` + +## ⚙️ Configuración del Escáner + +### Activar ENTER Automático +La mayoría de escáneres USB tienen configuración para presionar ENTER automáticamente después de escanear. + +**Para activarlo:** +1. Busca el manual de tu escáner +2. Escanea el código de barras "Add Suffix CR" o "Enable Enter Key" +3. Esto hará que después de escanear presione ENTER automáticamente + +### Configuraciones Comunes: +- ✅ **Suffix CR** - Presiona ENTER después de escanear +- ✅ **Beep on Read** - Sonido al leer correctamente +- ✅ **LED Indicator** - Luz al leer correctamente + +## 🛒 Dónde Comprar + +### Tiendas en Línea: +- **Amazon México** - Desde $200 MXN +- **Mercado Libre** - Desde $150 MXN +- **AliExpress** - Desde $100 MXN (tarda más) + +### Modelos Recomendados: +1. **Honeywell Voyager 1200g** - $800-1200 MXN (Profesional) +2. **Zebra DS2208** - $1500-2000 MXN (Alta calidad) +3. **Genéricos USB** - $200-400 MXN (Económicos, funcionan bien) + +**Búsqueda sugerida:** +- "Lector código barras USB" +- "Pistola escáner USB" +- "Barcode scanner USB" + +## 💡 Ventajas vs Cámara + +| Característica | Escáner USB | Cámara Web | +|---------------|-------------|------------| +| Velocidad | ⚡ Instantáneo | 🐌 Lento (2-3 seg) | +| Precisión | ✅ 99.9% | ❌ 60-70% | +| Distancia | 📏 10-30cm | 📏 5-10cm | +| Iluminación | ☀️ Funciona con poca luz | 💡 Requiere buena luz | +| Costo | 💰 $200-1500 MXN | 💰 $0 (ya tienes cámara) | +| Instalación | 🔌 Plug & Play | 🔧 Requiere código | +| Confiabilidad | ✅ Muy alta | ❌ Media-baja | + +## 🎯 Recomendación Final + +**Para un evento real de asistencia masiva:** +- ✅ **USA ESCÁNER USB** - Rápido, preciso, confiable +- ❌ Evita la cámara - Lento, impreciso, frustrante + +**Inversión mínima:** +- Un escáner genérico de $200-400 MXN funciona perfectamente +- Se paga solo con el tiempo ahorrado en 1 evento + +## 📝 Ejemplo de Uso + +``` +EVENTO: Conferencia MAC - 9 de Octubre 2025 +ASISTENTES: 150 personas + +Con Escáner USB: +- 2 segundos por persona +- Total: 5 minutos para todos +- 0 errores + +Con Cámara: +- 10-15 segundos por persona (intentos fallidos) +- Total: 25-40 minutos +- 10-20% de errores (reintento manual) +``` + +## 🔧 Solución de Problemas + +### El escáner no escribe nada: +1. Verifica que esté conectado (luz LED encendida) +2. Abre un Bloc de Notas y escanea - debe escribir +3. Si no escribe, intenta otro puerto USB +4. Reinicia la computadora + +### Escribe caracteres raros: +1. El escáner está en modo "teclado internacional" +2. Busca código de barras "Set Keyboard Layout to English" +3. Escanéalo para cambiar a modo correcto + +### No presiona ENTER: +1. Busca en el manual el código "Add Suffix CR" +2. Escanea ese código de configuración +3. Ahora presionará ENTER automáticamente + +--- + +**Sistema desarrollado para FES Acatlán - UNAM** diff --git a/docs/DESARROLLO.md b/docs/DESARROLLO.md new file mode 100644 index 0000000..840b400 --- /dev/null +++ b/docs/DESARROLLO.md @@ -0,0 +1,524 @@ +# Guía de Desarrollo - Sistema de Asistencia MAC + +Esta guía describe cómo configurar y usar el entorno de desarrollo con todas las herramientas de calidad de código, testing y análisis. + +## Índice + +1. [Configuración del Entorno](#configuración-del-entorno) +2. [Herramientas de Desarrollo](#herramientas-de-desarrollo) +3. [Testing con Tox](#testing-con-tox) +4. [Acceso a PostgreSQL](#acceso-a-postgresql) +5. [Comandos Útiles](#comandos-útiles) +6. [Flujo de Trabajo](#flujo-de-trabajo) + +--- + +## Configuración del Entorno + +### Requisitos Previos + +- Docker y Docker Compose instalados +- Git +- Puerto 80 (nginx), 5432 (PostgreSQL) disponibles + +### Iniciar Entorno de Desarrollo + +**Opción 1: Usando docker-compose.dev.yml (Recomendado para desarrollo)** + +```bash +# Construir e iniciar todos los servicios +docker-compose -f docker-compose.dev.yml up --build + +# En modo detached (segundo plano) +docker-compose -f docker-compose.dev.yml up -d --build +``` + +**Opción 2: Usando docker-compose.yml (Producción)** + +```bash +docker-compose up --build +``` + +### Diferencias entre Entornos + +| Característica | docker-compose.yml (Producción) | docker-compose.dev.yml (Desarrollo) | +|----------------|--------------------------------|-------------------------------------| +| Servidor | Gunicorn (3 workers) | Django runserver | +| Herramientas dev | ❌ No incluidas | ✅ Tox, pytest, linters, etc. | +| Puerto PostgreSQL | ✅ Expuesto (5432) | ✅ Expuesto (5432) | +| Debugging | ❌ Limitado | ✅ ipdb, logging detallado | +| Volúmenes | Solo static/media | Código completo montado | +| Hot reload | ❌ No | ✅ Sí (Django runserver) | + +--- + +## Herramientas de Desarrollo + +### Instaladas en el Contenedor + +El archivo `backend/requirements-dev.txt` incluye: + +#### **Linting y Formateo** +- **flake8** - Verificador de estilo PEP8 +- **pycodestyle** - Verificador de estilo PEP8 +- **autopep8** - Corrector automático de PEP8 +- **black** - Formateador de código opinionado +- **isort** - Ordenador de imports + +#### **Análisis Estático** +- **pylint** - Analizador de código estático +- **mccabe** - Complejidad ciclomática +- **radon** - Métricas de código + +#### **Type Checking** +- **mypy** - Verificador de tipos estáticos +- **django-stubs** - Type stubs para Django +- **djangorestframework-stubs** - Type stubs para DRF + +#### **Seguridad** +- **bandit** - Verificador de seguridad +- **safety** - Verificador de vulnerabilidades + +#### **Testing** +- **pytest** - Framework de testing +- **pytest-django** - Plugin pytest para Django +- **pytest-cov** - Cobertura de tests +- **coverage** - Herramienta de cobertura +- **factory-boy** - Factories para testing + +#### **Testing Automation** +- **tox** - Automatización de testing multi-entorno + +#### **Utilidades** +- **django-extensions** - Extensiones útiles para Django +- **ipython** - Shell interactivo mejorado +- **ipdb** - Debugger interactivo +- **pre-commit** - Framework de pre-commit hooks + +--- + +## Testing con Tox + +Tox automatiza testing en múltiples entornos. Configurado en `backend/tox.ini`. + +### Entornos Disponibles + +```bash +# Entrar al contenedor backend +docker-compose -f docker-compose.dev.yml exec backend bash + +# Ver todos los entornos disponibles +tox -l +``` + +**Salida esperada:** +``` +py311-django52 +test +test-fast +lint +format +format-check +type-check +security +security-full +coverage +coverage-report +metrics +complexity +docs +clean +``` + +### Ejecutar Tests + +**Tests completos con cobertura:** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e py311-django52 +``` + +**Tests rápidos (sin cobertura):** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e test-fast +``` + +**Solo tests unitarios:** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e test -- -m unit +``` + +**Solo tests de integración:** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e test -- -m integration +``` + +### Linting y Formateo + +**Ejecutar linters (flake8 + pylint):** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e lint +``` + +**Verificar formato (sin modificar):** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e format-check +``` + +**Formatear código automáticamente:** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e format +``` + +### Type Checking + +**Verificar tipos con mypy:** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e type-check +``` + +### Análisis de Seguridad + +**Escaneo de seguridad básico:** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e security +``` + +**Escaneo completo (genera reportes JSON):** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e security-full +``` + +### Cobertura de Código + +**Generar reporte de cobertura:** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e coverage +``` + +**Ver reporte en consola:** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e coverage-report +``` + +**Reporte HTML** se genera en `backend/htmlcov/index.html` + +### Métricas de Código + +**Complejidad ciclomática y mantenibilidad:** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e metrics +``` + +**Solo complejidad (con promedio):** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e complexity +``` + +### Limpieza + +**Eliminar archivos generados:** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox -e clean +``` + +### Ejecutar Todos los Entornos + +**Ejecutar todos los chequeos:** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox +``` + +⚠️ **Nota:** Esto puede tardar varios minutos. + +--- + +## Acceso a PostgreSQL + +### Desde tu Laptop (Host) + +El puerto 5432 está expuesto en ambos entornos (producción y desarrollo). + +**Credenciales por defecto:** +- **Host:** localhost +- **Puerto:** 5432 +- **Database:** mac_attendance +- **Usuario:** mac_user +- **Password:** mac_password_2024_secure + +#### **Con psql (línea de comandos)** + +```bash +psql -h localhost -p 5432 -U mac_user -d mac_attendance +``` + +#### **Con pgAdmin** + +1. Abrir pgAdmin +2. Crear nueva conexión: + - Name: MAC Attendance + - Host: localhost + - Port: 5432 + - Database: mac_attendance + - Username: mac_user + - Password: mac_password_2024_secure + +#### **Con DBeaver / DataGrip** + +Similar a pgAdmin, usar las credenciales arriba. + +### Desde el Contenedor Backend + +```bash +# Entrar al contenedor backend +docker-compose -f docker-compose.dev.yml exec backend bash + +# Conectarse a PostgreSQL +psql -h db -U mac_user -d mac_attendance +``` + +### Comandos Útiles de PostgreSQL + +**Listar tablas:** +```sql +\dt +``` + +**Describir tabla:** +```sql +\d authentication_student +``` + +**Ver tamaño de base de datos:** +```sql +SELECT pg_size_pretty(pg_database_size('mac_attendance')); +``` + +**Ver conexiones activas:** +```sql +SELECT * FROM pg_stat_activity; +``` + +**Contar registros:** +```sql +SELECT COUNT(*) FROM authentication_student; +SELECT COUNT(*) FROM events_event; +SELECT COUNT(*) FROM attendance_attendance; +``` + +--- + +## Comandos Útiles + +### Gestión de Contenedores + +**Ver logs en tiempo real:** +```bash +docker-compose -f docker-compose.dev.yml logs -f backend +docker-compose -f docker-compose.dev.yml logs -f db +``` + +**Entrar al contenedor backend:** +```bash +docker-compose -f docker-compose.dev.yml exec backend bash +``` + +**Ejecutar comando Django:** +```bash +docker-compose -f docker-compose.dev.yml exec backend python manage.py +``` + +**Crear superusuario:** +```bash +docker-compose -f docker-compose.dev.yml exec backend python manage.py createsuperuser +``` + +**Hacer migraciones:** +```bash +docker-compose -f docker-compose.dev.yml exec backend python manage.py makemigrations +docker-compose -f docker-compose.dev.yml exec backend python manage.py migrate +``` + +**Django shell:** +```bash +docker-compose -f docker-compose.dev.yml exec backend python manage.py shell +``` + +**Django shell_plus (con django-extensions):** +```bash +docker-compose -f docker-compose.dev.yml exec backend python manage.py shell_plus +``` + +### Backup y Restore + +**Backup de base de datos:** +```bash +docker-compose -f docker-compose.dev.yml exec db pg_dump -U mac_user mac_attendance > backup_$(date +%Y%m%d).sql +``` + +**Restaurar backup:** +```bash +cat backup.sql | docker-compose -f docker-compose.dev.yml exec -T db psql -U mac_user mac_attendance +``` + +--- + +## Flujo de Trabajo + +### 1. Desarrollo de Nueva Funcionalidad + +```bash +# 1. Iniciar entorno de desarrollo +docker-compose -f docker-compose.dev.yml up -d + +# 2. Crear rama de feature +git checkout -b feature/nueva-funcionalidad + +# 3. Desarrollar (los cambios se reflejan automáticamente) +# Editar archivos en backend/ o frontend/ + +# 4. Ejecutar tests mientras desarrollas +docker-compose -f docker-compose.dev.yml exec backend tox -e test-fast + +# 5. Verificar formato antes de commit +docker-compose -f docker-compose.dev.yml exec backend tox -e format-check + +# 6. Formatear código si es necesario +docker-compose -f docker-compose.dev.yml exec backend tox -e format + +# 7. Ejecutar linters +docker-compose -f docker-compose.dev.yml exec backend tox -e lint + +# 8. Verificar tipos +docker-compose -f docker-compose.dev.yml exec backend tox -e type-check + +# 9. Verificar seguridad +docker-compose -f docker-compose.dev.yml exec backend tox -e security + +# 10. Ejecutar suite completa antes de commit +docker-compose -f docker-compose.dev.yml exec backend tox +``` + +### 2. Debugging + +**Con ipdb:** + +```python +# En tu código Python +import ipdb; ipdb.set_trace() +``` + +Luego: +```bash +docker-compose -f docker-compose.dev.yml up +# El contenedor se pausará en el breakpoint +``` + +**Ver logs detallados:** +```bash +docker-compose -f docker-compose.dev.yml logs -f backend +``` + +### 3. Testing + +```bash +# Tests rápidos durante desarrollo +docker-compose -f docker-compose.dev.yml exec backend pytest + +# Con cobertura +docker-compose -f docker-compose.dev.yml exec backend pytest --cov=. + +# Solo una app +docker-compose -f docker-compose.dev.yml exec backend pytest authentication/ + +# Solo un archivo +docker-compose -f docker-compose.dev.yml exec backend pytest authentication/tests/test_models.py + +# Solo una función de test +docker-compose -f docker-compose.dev.yml exec backend pytest authentication/tests/test_models.py::test_student_creation +``` + +### 4. Antes de Hacer Commit + +**Checklist:** + +- [ ] Tests pasan: `tox -e test` +- [ ] Código formateado: `tox -e format` +- [ ] Linters limpios: `tox -e lint` +- [ ] Type checking OK: `tox -e type-check` +- [ ] Sin vulnerabilidades: `tox -e security` +- [ ] Cobertura > 80%: `tox -e coverage` + +**Ejecutar todo de una vez:** +```bash +docker-compose -f docker-compose.dev.yml exec backend tox +``` + +### 5. Pre-commit Hooks (Opcional) + +Para automatizar chequeos antes de cada commit: + +```bash +# Dentro del contenedor backend +docker-compose -f docker-compose.dev.yml exec backend bash + +# Instalar hooks +pre-commit install + +# Ejecutar manualmente +pre-commit run --all-files +``` + +--- + +## Solución de Problemas + +### "Database does not exist" + +```bash +docker-compose -f docker-compose.dev.yml down -v +docker-compose -f docker-compose.dev.yml up -d +``` + +### "Port 5432 already in use" + +Cambiar el puerto en `docker-compose.dev.yml`: +```yaml +ports: + - "5433:5432" # Usar puerto 5433 en host +``` + +### "Permission denied" en archivos + +```bash +# Dentro del contenedor +chown -R $(whoami) /app +``` + +### Limpiar todo y empezar de nuevo + +```bash +docker-compose -f docker-compose.dev.yml down -v +docker system prune -a +docker volume prune +docker-compose -f docker-compose.dev.yml up --build +``` + +--- + +## Recursos Adicionales + +- [Documentación de Django](https://docs.djangoproject.com/) +- [Documentación de Django REST Framework](https://www.django-rest-framework.org/) +- [Documentación de Tox](https://tox.wiki/) +- [Documentación de pytest](https://docs.pytest.org/) +- [PostgreSQL Docs](https://www.postgresql.org/docs/) + +--- + +## Fecha de Creación + +**Octubre 15, 2025** + +--- + +**Nota:** Este documento describe el entorno de desarrollo completo con todas las herramientas de calidad de código, testing y análisis integradas. diff --git a/docs/DOCKER_SETUP.md b/docs/DOCKER_SETUP.md new file mode 100644 index 0000000..9b448a3 --- /dev/null +++ b/docs/DOCKER_SETUP.md @@ -0,0 +1,271 @@ +# Configuración Docker con Nginx + +Esta configuración permite ejecutar la aplicación completa (Frontend React + Backend Django) usando Docker con Nginx como servidor web. + +## Arquitectura + +``` +┌─────────────────────────────────────────┐ +│ Nginx (Puerto 80) │ +│ - Sirve Frontend (React build) │ +│ - Proxy reverso para Backend Django │ +└─────────────────────────────────────────┘ + │ │ + │ │ + ▼ ▼ + ┌──────────┐ ┌──────────┐ + │ Frontend │ │ Backend │ + │ React │ │ Django │ + │ (Vite) │ │ (Puerto │ + └──────────┘ │ 8000) │ + └──────────┘ + │ + ▼ + ┌──────────┐ + │PostgreSQL│ + │ DB │ + └──────────┘ +``` + +## Archivos Creados + +- `Dockerfile.frontend` - Build del frontend React +- `Dockerfile.backend` - Configuración del backend Django +- `docker-compose.yml` - Orquestación de servicios +- `nginx/nginx.conf` - Configuración de Nginx +- `.dockerignore` - Archivos excluidos del build + +## Requisitos Previos + +- Docker Desktop instalado +- Docker Compose instalado + +## Instrucciones de Uso + +### 1. Configurar Variables de Entorno + +Edita el archivo `docker-compose.yml` y ajusta las variables de entorno del servicio `backend`: + +```yaml +environment: + - SECRET_KEY=tu-secret-key-super-segura-cambiar-en-produccion + - ALLOWED_HOSTS=localhost,127.0.0.1,nginx,tu-dominio.com +``` + +### 2. Construir y Levantar los Contenedores + +```bash +# Construir las imágenes +docker-compose build + +# Levantar todos los servicios +docker-compose up -d + +# Ver logs +docker-compose logs -f +``` + +### 3. Acceder a la Aplicación + +- **Frontend**: http://localhost +- **API Backend**: http://localhost/api/ +- **Admin Django**: http://localhost/admin/ + +### 4. Comandos Útiles + +```bash +# Detener los contenedores +docker-compose down + +# Detener y eliminar volúmenes (¡cuidado, elimina la base de datos!) +docker-compose down -v + +# Reconstruir un servicio específico +docker-compose build backend +docker-compose up -d backend + +# Ver logs de un servicio específico +docker-compose logs -f nginx +docker-compose logs -f backend + +# Ejecutar comandos en el backend +docker-compose exec backend python manage.py createsuperuser +docker-compose exec backend python manage.py migrate + +# Reiniciar un servicio +docker-compose restart backend +``` + +### 5. Gestión de la Base de Datos + +```bash +# Crear migraciones +docker-compose exec backend python manage.py makemigrations + +# Aplicar migraciones +docker-compose exec backend python manage.py migrate + +# Crear superusuario +docker-compose exec backend python manage.py createsuperuser + +# Backup de la base de datos (PostgreSQL) +docker-compose exec db pg_dump -U postgres mac_attendance > backup.sql + +# Restaurar backup +docker-compose exec -T db psql -U postgres mac_attendance < backup.sql +``` + +## Configuración para Producción + +### 1. Usar SQLite en lugar de PostgreSQL + +Si prefieres usar SQLite (más simple pero menos escalable): + +En `docker-compose.yml`: +- Comenta o elimina el servicio `db` +- Comenta la línea `DATABASE_URL` en el servicio `backend` +- Asegúrate de que Django esté configurado para usar SQLite en `settings.py` + +### 2. Variables de Entorno Seguras + +Crea un archivo `.env` en la raíz del proyecto: + +```env +SECRET_KEY=tu-secret-key-muy-segura-y-larga +DEBUG=False +ALLOWED_HOSTS=localhost,tu-dominio.com +DB_NAME=mac_attendance +DB_USER=postgres +DB_PASSWORD=password-super-seguro +DB_HOST=db +DB_PORT=5432 +``` + +Modifica `docker-compose.yml` para usar el archivo `.env`: + +```yaml +services: + backend: + env_file: + - .env +``` + +### 3. HTTPS con Certificados SSL + +Para habilitar HTTPS, necesitas: + +1. Obtener certificados SSL (Let's Encrypt, Certbot, etc.) +2. Modificar `nginx/nginx.conf` para escuchar en el puerto 443 +3. Agregar la configuración SSL + +Ejemplo: + +```nginx +server { + listen 443 ssl http2; + server_name tu-dominio.com; + + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + + # ... resto de la configuración +} + +server { + listen 80; + server_name tu-dominio.com; + return 301 https://$server_name$request_uri; +} +``` + +### 4. Optimizaciones de Producción + +En `Dockerfile.backend`, ajusta workers de Gunicorn según tu servidor: + +```dockerfile +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--threads", "2", "mac_attendance.wsgi:application"] +``` + +Regla general: `workers = (2 x CPU cores) + 1` + +## Troubleshooting + +### El frontend no carga + +```bash +# Verificar logs del nginx +docker-compose logs nginx + +# Reconstruir el frontend +docker-compose build nginx --no-cache +docker-compose up -d nginx +``` + +### Error de conexión al backend + +```bash +# Verificar que el backend esté corriendo +docker-compose ps + +# Ver logs del backend +docker-compose logs backend + +# Verificar conectividad +docker-compose exec nginx ping backend +``` + +### Problemas con migraciones + +```bash +# Entrar al contenedor del backend +docker-compose exec backend sh + +# Ejecutar migraciones manualmente +python manage.py migrate --run-syncdb +``` + +### Permisos en archivos media/static + +```bash +# Dar permisos al directorio +docker-compose exec backend chmod -R 755 /app/media +docker-compose exec backend chmod -R 755 /app/staticfiles +``` + +## Monitoreo y Logs + +```bash +# Ver uso de recursos +docker stats + +# Ver todos los logs +docker-compose logs --tail=100 + +# Seguir logs en tiempo real +docker-compose logs -f --tail=100 +``` + +## Limpieza + +```bash +# Limpiar contenedores detenidos +docker container prune + +# Limpiar imágenes sin usar +docker image prune -a + +# Limpiar volúmenes sin usar +docker volume prune + +# Limpieza completa (¡cuidado!) +docker system prune -a --volumes +``` + +## Mejoras Futuras + +- [ ] Configurar Redis para caché +- [ ] Agregar Celery para tareas asíncronas +- [ ] Implementar health checks más robustos +- [ ] Configurar logging centralizado +- [ ] Agregar monitoreo con Prometheus/Grafana +- [ ] Implementar CI/CD con GitHub Actions diff --git a/docs/ESTRUCTURA_PROYECTO.md b/docs/ESTRUCTURA_PROYECTO.md new file mode 100644 index 0000000..21ff34c --- /dev/null +++ b/docs/ESTRUCTURA_PROYECTO.md @@ -0,0 +1,156 @@ +# Estructura del Proyecto + +Este documento describe la organización de carpetas y archivos del Sistema de Asistencia MAC. + +## Estructura de Directorios + +``` +pagina-mac-og/ +├── backend/ # Aplicación Django (Backend) +│ ├── attendance/ # App de gestión de asistencias +│ ├── authentication/ # App de autenticación y usuarios +│ ├── events/ # App de gestión de eventos +│ ├── mac_attendance/ # Configuración principal de Django +│ ├── scripts/ # Scripts de utilidad y pruebas +│ ├── static/ # Archivos estáticos (CSS, JS) +│ ├── templates/ # Plantillas HTML +│ ├── db/ # Base de datos SQLite +│ ├── logs/ # Logs de la aplicación +│ ├── media/ # Archivos multimedia subidos +│ └── manage.py # CLI de Django +│ +├── frontend/ # Aplicación React (Frontend) +│ ├── src/ # Código fuente +│ │ ├── components/ # Componentes React +│ │ ├── contexts/ # Contextos (Auth, etc.) +│ │ ├── services/ # Servicios (API calls) +│ │ └── styles/ # Estilos CSS +│ ├── public/ # Archivos públicos +│ └── package.json # Dependencias de Node.js +│ +├── docker/ # Configuración Docker +│ ├── Dockerfile.backend # Dockerfile para Django +│ ├── Dockerfile.frontend # Dockerfile para React + Nginx +│ └── nginx.conf # Configuración de Nginx +│ +├── docs/ # Documentación +│ ├── ACCESO_ADMIN.md # Guía de acceso al admin +│ ├── API_DOCUMENTATION.md # Documentación de API +│ ├── DOCKER_SETUP.md # Configuración de Docker +│ ├── GUIA_COMPARTIR_DOCKER.md # Guía para compartir +│ ├── INSTRUCCIONES_GITHUB.md # Instrucciones de Git +│ ├── ESTRUCTURA_PROYECTO.md # Este archivo +│ └── postman_collection.json # Colección de Postman +│ +├── docker-compose.yml # Orquestación de contenedores +├── .dockerignore # Archivos ignorados por Docker +├── .env.example # Ejemplo de variables de entorno +├── .gitignore # Archivos ignorados por Git +├── install.sh # Script de instalación (Linux/Mac) +├── install.bat # Script de instalación (Windows) +├── create_superuser.sh # Script para crear superusuario +├── README.md # Documentación principal +└── LICENSE # Licencia del proyecto +``` + +## Descripción de Componentes + +### Backend (Django) + +**Aplicaciones Django:** +- **attendance**: Gestión de asistencias y estadísticas +- **authentication**: Sistema de autenticación, usuarios, perfiles +- **events**: Gestión de eventos del MAC +- **mac_attendance**: Configuración principal del proyecto Django + +**Archivos importantes:** +- `manage.py`: CLI de Django para ejecutar comandos +- `requirements.txt`: Dependencias de Python +- `db.sqlite3`: Base de datos SQLite (desarrollo) + +### Frontend (React) + +**Estructura:** +- `src/components/`: Componentes React organizados por rol (admin, student, attendance) +- `src/contexts/`: Contextos de React (AuthContext para autenticación) +- `src/services/`: Servicios para llamadas a la API +- `src/styles/`: Archivos CSS + +### Docker + +**Archivos:** +- `Dockerfile.backend`: Imagen de Docker para el backend Django +- `Dockerfile.frontend`: Imagen de Docker para el frontend React + Nginx +- `nginx.conf`: Configuración del servidor Nginx como proxy reverso + +### Documentación + +Toda la documentación técnica, guías de uso y configuración se encuentra en la carpeta `docs/`. + +## Flujo de Trabajo + +### Desarrollo + +1. **Backend**: Modificar código en `backend/` +2. **Frontend**: Modificar código en `frontend/src/` +3. **Docker**: Los cambios se reflejan automáticamente con volúmenes + +### Despliegue + +1. Construir imágenes: `docker-compose build` +2. Iniciar servicios: `docker-compose up -d` +3. Verificar logs: `docker-compose logs -f` + +## Scripts Útiles + +### Backend (desde `backend/`) +```bash +python manage.py migrate # Aplicar migraciones +python manage.py createsuperuser # Crear superusuario +python manage.py collectstatic # Recopilar archivos estáticos +python scripts/create_test_data.py # Crear datos de prueba +``` + +### Docker +```bash +docker-compose up -d # Iniciar servicios +docker-compose down # Detener servicios +docker-compose logs backend # Ver logs del backend +docker-compose restart backend # Reiniciar backend +``` + +## Variables de Entorno + +Copia `.env.example` a `.env` y configura las variables según tu entorno: + +```bash +cp .env.example .env +# Editar .env con tus valores +``` + +## Puertos + +- **Frontend (Nginx)**: http://localhost (puerto 80) +- **Backend (Django)**: http://localhost:8000 (interno, proxy via Nginx) +- **Admin Django**: http://localhost/admin + +## Notas Importantes + +1. **Base de datos**: SQLite por defecto en desarrollo. Para producción, usar PostgreSQL. +2. **Archivos estáticos**: Servidos por Nginx en producción +3. **CORS**: Configurado para permitir localhost en desarrollo +4. **Logs**: Disponibles en `backend/logs/` y via `docker-compose logs` + +## Convenciones + +- **Backend**: Seguir guía de estilo PEP 8 para Python +- **Frontend**: Seguir guía de estilo de Airbnb para JavaScript/React +- **Commits**: Mensajes descriptivos en español +- **Branches**: `main` para producción, crear feature branches para desarrollo + +## Soporte + +Para más información, consulta: +- [README.md](../README.md) - Guía general del proyecto +- [API_DOCUMENTATION.md](API_DOCUMENTATION.md) - Documentación de la API +- [DOCKER_SETUP.md](DOCKER_SETUP.md) - Configuración detallada de Docker diff --git a/docs/POSTGRESQL_MIGRATION.md b/docs/POSTGRESQL_MIGRATION.md new file mode 100644 index 0000000..93d6c48 --- /dev/null +++ b/docs/POSTGRESQL_MIGRATION.md @@ -0,0 +1,289 @@ +# Migración a PostgreSQL + +## Resumen + +El Sistema de Asistencia MAC ha sido migrado de SQLite a PostgreSQL para mejorar el rendimiento, escalabilidad y prepararlo para producción. + +## ¿Por qué PostgreSQL? + +### Ventajas sobre SQLite + +✅ **Escalabilidad**: Maneja millones de registros sin degradación de rendimiento +✅ **Concurrencia**: Múltiples usuarios simultáneos sin bloqueos +✅ **Rendimiento**: Mejor optimización de queries complejos +✅ **Integridad**: Constraints y validaciones más robustas +✅ **Backups**: Herramientas profesionales de respaldo (pg_dump, pg_basebackup) +✅ **Replicación**: Soporte nativo para alta disponibilidad +✅ **Producción**: Estándar en la industria para aplicaciones web + +## Cambios Realizados + +### 1. Backend - Requirements + +**Archivo:** `backend/requirements.txt` + +```python +# Database - PostgreSQL +psycopg2-binary==2.9.9 +``` + +### 2. Backend - Settings + +**Archivo:** `backend/mac_attendance/settings.py` + +```python +# Database Configuration - PostgreSQL +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': config('DB_NAME', default='mac_attendance'), + 'USER': config('DB_USER', default='mac_user'), + 'PASSWORD': config('DB_PASSWORD', default='mac_password_2024_secure'), + 'HOST': config('DB_HOST', default='db'), + 'PORT': config('DB_PORT', default='5432'), + 'CONN_MAX_AGE': 600, # Conexiones persistentes + 'OPTIONS': { + 'connect_timeout': 10, + } + } +} +``` + +### 3. Docker Compose + +**Archivo:** `docker-compose.yml` + +Se agregó el servicio de PostgreSQL: + +```yaml +services: + # PostgreSQL Database + db: + image: postgres:15-alpine + environment: + - POSTGRES_DB=mac_attendance + - POSTGRES_USER=mac_user + - POSTGRES_PASSWORD=mac_password_2024_secure + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - app-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mac_user -d mac_attendance"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + depends_on: + db: + condition: service_healthy + environment: + - DB_ENGINE=postgresql + - DB_NAME=mac_attendance + - DB_USER=mac_user + - DB_PASSWORD=mac_password_2024_secure + - DB_HOST=db + - DB_PORT=5432 + +volumes: + postgres_data: +``` + +### 4. Variables de Entorno + +**Archivo:** `.env.example` + +```env +# Database - PostgreSQL (Producción) +DB_NAME=mac_attendance +DB_USER=mac_user +DB_PASSWORD=mac_password_2024_secure +DB_HOST=db +DB_PORT=5432 +``` + +## Estado del Sistema + +### Contenedores Activos + +```bash +$ docker-compose ps +NAME STATUS +pagina-mac-og-backend-1 Up (running) +pagina-mac-og-db-1 Up (healthy) +pagina-mac-og-nginx-1 Up (running) +``` + +### Migraciones Aplicadas + +✅ Todas las migraciones de Django aplicadas correctamente +✅ Base de datos creada: `mac_attendance` +✅ Usuario de base de datos: `mac_user` +✅ Tablas creadas: 16 tablas (auth, authentication, events, attendance, admin, sessions) + +## Gestión de PostgreSQL + +### Comandos Útiles + +**Conectarse a PostgreSQL:** +```bash +docker-compose exec db psql -U mac_user -d mac_attendance +``` + +**Listar tablas:** +```sql +\dt +``` + +**Ver tamaño de base de datos:** +```sql +SELECT pg_size_pretty(pg_database_size('mac_attendance')); +``` + +**Backup de base de datos:** +```bash +docker-compose exec db pg_dump -U mac_user mac_attendance > backup.sql +``` + +**Restaurar backup:** +```bash +cat backup.sql | docker-compose exec -T db psql -U mac_user mac_attendance +``` + +**Ver conexiones activas:** +```sql +SELECT * FROM pg_stat_activity; +``` + +## Configuración de Producción + +### 1. Cambiar Credenciales + +**IMPORTANTE:** Antes de desplegar en producción, cambiar: + +```env +DB_USER=tu_usuario_seguro +DB_PASSWORD=contraseña_muy_segura_y_compleja +SECRET_KEY=tu_secret_key_super_segura +``` + +### 2. Backups Automáticos + +Configurar cron job para backups diarios: + +```bash +#!/bin/bash +# /etc/cron.daily/postgres-backup + +BACKUP_DIR="/backups/postgres" +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/mac_attendance_$DATE.sql" + +mkdir -p $BACKUP_DIR +docker-compose exec -T db pg_dump -U mac_user mac_attendance > $BACKUP_FILE +gzip $BACKUP_FILE + +# Mantener solo los últimos 30 días +find $BACKUP_DIR -type f -mtime +30 -delete +``` + +### 3. Optimización + +**Configurar índices adicionales (si es necesario):** + +```python +# En los modelos, agregar: +class Meta: + indexes = [ + models.Index(fields=['account_number']), + models.Index(fields=['event', 'student']), + ] +``` + +### 4. Monitoring + +**Ver queries lentas:** +```sql +SELECT query, mean_exec_time +FROM pg_stat_statements +ORDER BY mean_exec_time DESC +LIMIT 10; +``` + +## Capacidad y Límites + +### Capacidad PostgreSQL + +- **Tamaño máximo de base de datos**: Ilimitado +- **Tamaño máximo de tabla**: 32 TB +- **Filas por tabla**: Ilimitadas (prácticamente) +- **Usuarios concurrentes**: Miles + +### Estimaciones para el Sistema MAC + +**Escenario actual:** +- 1,000 estudiantes +- 100 eventos/año +- ~5,000 asistencias/año +- **Tamaño estimado**: < 100 MB/año + +**Escenario a 10 años:** +- **Tamaño estimado**: < 1 GB +- **Performance**: Excelente + +## Rollback (Volver a SQLite) + +Si necesitas volver a SQLite por alguna razón: + +### 1. Cambiar settings.py + +```python +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} +``` + +### 2. Comentar servicio db en docker-compose.yml + +```yaml +# db: +# image: postgres:15-alpine +# ... +``` + +### 3. Reconstruir + +```bash +docker-compose down -v +docker-compose up -d --build +``` + +## Verificación + +### ✅ Checklist Post-Migración + +- [✅] PostgreSQL corriendo y saludable +- [✅] Backend conectado a PostgreSQL +- [✅] Todas las migraciones aplicadas +- [✅] Aplicación accesible en http://localhost +- [✅] Variables de entorno configuradas +- [✅] Documentación actualizada + +## Soporte + +Para más información: +- [Documentación de PostgreSQL](https://www.postgresql.org/docs/) +- [Django con PostgreSQL](https://docs.djangoproject.com/en/stable/ref/databases/#postgresql-notes) +- [psycopg2](https://www.psycopg.org/docs/) + +## Fecha de Migración + +**Octubre 14, 2025** + +--- + +**Nota:** Este documento describe la migración a PostgreSQL para preparar el sistema para producción. Todas las funcionalidades permanecen idénticas, solo cambió el motor de base de datos. diff --git a/docs/postman_collection.json b/docs/postman_collection.json new file mode 100644 index 0000000..b038f90 --- /dev/null +++ b/docs/postman_collection.json @@ -0,0 +1,347 @@ +{ + "info": { + "name": "Sistema MAC - API Collection", + "_postman_id": "mac-attendance-api", + "description": "Colección completa de endpoints para el Sistema de Asistencia MAC", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "base_url", + "value": "http://localhost", + "type": "string" + }, + { + "key": "access_token", + "value": "", + "type": "string" + }, + { + "key": "refresh_token", + "value": "", + "type": "string" + } + ], + "item": [ + { + "name": "Authentication", + "item": [ + { + "name": "Login", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.response.code === 200) {", + " const jsonData = pm.response.json();", + " pm.environment.set('access_token', jsonData.tokens.access);", + " pm.environment.set('refresh_token', jsonData.tokens.refresh);", + " pm.test('Login successful', () => {", + " pm.expect(jsonData.message).to.eql('Login exitoso');", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"account_number\": \"3123123\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/auth/login/", + "host": ["{{base_url}}"], + "path": ["api", "auth", "login", ""] + }, + "description": "Iniciar sesión con número de cuenta" + }, + "response": [] + }, + { + "name": "Get Profile", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{access_token}}" + } + ], + "url": { + "raw": "{{base_url}}/api/auth/profile/", + "host": ["{{base_url}}"], + "path": ["api", "auth", "profile", ""] + }, + "description": "Obtener perfil del usuario autenticado" + }, + "response": [] + }, + { + "name": "Check Auth Status", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/auth/check-auth/", + "host": ["{{base_url}}"], + "path": ["api", "auth", "check-auth", ""] + }, + "description": "Verificar estado de autenticación" + }, + "response": [] + }, + { + "name": "Refresh Token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.response.code === 200) {", + " const jsonData = pm.response.json();", + " pm.environment.set('access_token', jsonData.access);", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"refresh\": \"{{refresh_token}}\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/auth/token/refresh/", + "host": ["{{base_url}}"], + "path": ["api", "auth", "token", "refresh", ""] + }, + "description": "Refrescar access token" + }, + "response": [] + }, + { + "name": "Logout", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{access_token}}" + } + ], + "url": { + "raw": "{{base_url}}/api/auth/logout/", + "host": ["{{base_url}}"], + "path": ["api", "auth", "logout", ""] + }, + "description": "Cerrar sesión" + }, + "response": [] + }, + { + "name": "System Config", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{access_token}}" + } + ], + "url": { + "raw": "{{base_url}}/api/auth/system-config/", + "host": ["{{base_url}}"], + "path": ["api", "auth", "system-config", ""] + }, + "description": "Obtener configuración del sistema" + }, + "response": [] + } + ] + }, + { + "name": "Events", + "item": [ + { + "name": "List Events", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/events/", + "host": ["{{base_url}}"], + "path": ["api", "events", ""] + }, + "description": "Listar todos los eventos activos" + }, + "response": [] + }, + { + "name": "Register External User", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{access_token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"full_name\": \"Juan Pérez\",\n \"account_number\": \"9999999\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/events/external/register/", + "host": ["{{base_url}}"], + "path": ["api", "events", "external", "register", ""] + }, + "description": "Registrar usuario externo" + }, + "response": [] + }, + { + "name": "Search External Users", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{access_token}}" + } + ], + "url": { + "raw": "{{base_url}}/api/events/external/search/?account_number=9999999", + "host": ["{{base_url}}"], + "path": ["api", "events", "external", "search", ""], + "query": [ + { + "key": "account_number", + "value": "9999999" + } + ] + }, + "description": "Buscar usuarios externos" + }, + "response": [] + } + ] + }, + { + "name": "Attendance", + "item": [ + { + "name": "Register Attendance", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{access_token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"event_id\": 1,\n \"account_number\": \"0000111\",\n \"registration_method\": \"manual\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/attendance/", + "host": ["{{base_url}}"], + "path": ["api", "attendance", ""] + }, + "description": "Registrar asistencia a evento" + }, + "response": [] + }, + { + "name": "Get Student Stats", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{access_token}}" + } + ], + "url": { + "raw": "{{base_url}}/api/attendance/stats/?account_number=0000111", + "host": ["{{base_url}}"], + "path": ["api", "attendance", "stats", ""], + "query": [ + { + "key": "account_number", + "value": "0000111" + } + ] + }, + "description": "Obtener estadísticas de asistencia" + }, + "response": [] + }, + { + "name": "Get Recent Attendances", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{access_token}}" + } + ], + "url": { + "raw": "{{base_url}}/api/attendance/recent/", + "host": ["{{base_url}}"], + "path": ["api", "attendance", "recent", ""] + }, + "description": "Obtener asistencias recientes" + }, + "response": [] + }, + { + "name": "Get My Attendances", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{access_token}}" + } + ], + "url": { + "raw": "{{base_url}}/api/attendance/my/", + "host": ["{{base_url}}"], + "path": ["api", "attendance", "my", ""] + }, + "description": "Obtener mis asistencias" + }, + "response": [] + } + ] + } + ] +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..7059a96 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,12 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..cee1e2c --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..0c589ec --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..aa16545 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3103 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "axios": "^1.12.2", + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.3", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.4.0", + "vite": "^7.1.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", + "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.35", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz", + "integrity": "sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.0.tgz", + "integrity": "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.0.tgz", + "integrity": "sha512-pqDirm8koABIKvzL59YI9W9DWbRlTX7RWhN+auR8HXJxo89m4mjqbah7nJZjeKNTNYopqL+yGg+0mhCpf3xZtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.0.tgz", + "integrity": "sha512-YCdWlY/8ltN6H78HnMsRHYlPiKvqKagBP1r+D7SSylxX+HnsgXGCmLiV3Y4nSyY9hW8qr8U9LDUx/Lo7M6MfmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.0.tgz", + "integrity": "sha512-z4nw6y1j+OOSGzuVbSWdIp1IUks9qNw4dc7z7lWuWDKojY38VMWBlEN7F9jk5UXOkUcp97vA1N213DF+Lz8BRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.0.tgz", + "integrity": "sha512-Q/dv9Yvyr5rKlK8WQJZVrp5g2SOYeZUs9u/t2f9cQ2E0gJjYB/BWoedXfUT0EcDJefi2zzVfhcOj8drWCzTviw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.0.tgz", + "integrity": "sha512-kdBsLs4Uile/fbjZVvCRcKB4q64R+1mUq0Yd7oU1CMm1Av336ajIFqNFovByipciuUQjBCPMxwJhCgfG2re3rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.0.tgz", + "integrity": "sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.0.tgz", + "integrity": "sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.0.tgz", + "integrity": "sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.0.tgz", + "integrity": "sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.0.tgz", + "integrity": "sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.0.tgz", + "integrity": "sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.0.tgz", + "integrity": "sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.0.tgz", + "integrity": "sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.0.tgz", + "integrity": "sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.0.tgz", + "integrity": "sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.0.tgz", + "integrity": "sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.0.tgz", + "integrity": "sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.0.tgz", + "integrity": "sha512-YQugafP/rH0eOOHGjmNgDURrpYHrIX0yuojOI8bwCyXwxC9ZdTd3vYkmddPX0oHONLXu9Rb1dDmT0VNpjkzGGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.0.tgz", + "integrity": "sha512-zYdUYhi3Qe2fndujBqL5FjAFzvNeLxtIqfzNEVKD1I7C37/chv1VxhscWSQHTNfjPCrBFQMnynwA3kpZpZ8w4A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.0.tgz", + "integrity": "sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.0.tgz", + "integrity": "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", + "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.3.tgz", + "integrity": "sha512-PFVHhosKkofGH0Yzrw1BipSedTH68BFF8ZWy1kfUpCtJcouXXY0+racG8sExw7hw0HoX36813ga5o3LTWZ4FUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.35", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", + "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001743", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", + "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.222", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", + "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", + "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.36.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.21.tgz", + "integrity": "sha512-MWDWTtNC4voTcWDxXbdmBNe8b/TxfxRFUL6hXgKWJjN9c1AagYEmpiFWBWzDw+5H3SulWUe1pJKTnoSdmk88UA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.0.tgz", + "integrity": "sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.0", + "@rollup/rollup-android-arm64": "4.52.0", + "@rollup/rollup-darwin-arm64": "4.52.0", + "@rollup/rollup-darwin-x64": "4.52.0", + "@rollup/rollup-freebsd-arm64": "4.52.0", + "@rollup/rollup-freebsd-x64": "4.52.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.0", + "@rollup/rollup-linux-arm-musleabihf": "4.52.0", + "@rollup/rollup-linux-arm64-gnu": "4.52.0", + "@rollup/rollup-linux-arm64-musl": "4.52.0", + "@rollup/rollup-linux-loong64-gnu": "4.52.0", + "@rollup/rollup-linux-ppc64-gnu": "4.52.0", + "@rollup/rollup-linux-riscv64-gnu": "4.52.0", + "@rollup/rollup-linux-riscv64-musl": "4.52.0", + "@rollup/rollup-linux-s390x-gnu": "4.52.0", + "@rollup/rollup-linux-x64-gnu": "4.52.0", + "@rollup/rollup-linux-x64-musl": "4.52.0", + "@rollup/rollup-openharmony-arm64": "4.52.0", + "@rollup/rollup-win32-arm64-msvc": "4.52.0", + "@rollup/rollup-win32-ia32-msvc": "4.52.0", + "@rollup/rollup-win32-x64-gnu": "4.52.0", + "@rollup/rollup-win32-x64-msvc": "4.52.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", + "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..e82bc18 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.12.2", + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.3", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.4.0", + "vite": "^7.1.7" + } +} diff --git a/frontend/public/images/escudo.png b/frontend/public/images/escudo.png new file mode 100644 index 0000000000000000000000000000000000000000..7934d6a43c0fc25a4ca88403d616ef416ed80109 GIT binary patch literal 234005 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 literal 0 HcmV?d00001 diff --git a/frontend/public/images/logo.png b/frontend/public/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..674c359f60db1fd05b2d1a2b6ac38406e3432e85 GIT binary patch literal 129635 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%0DCW#N^u0%?lKX^4t56;< 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�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 z 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 JE03XW{vUlbj*b8T literal 0 HcmV?d00001 diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..59e3e8c --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,50 @@ +/* App Component Styles */ + +#root { + width: 100%; + min-height: 100vh; +} + +/* Loading State */ +.app-loading { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background-color: #1e3a8a; +} + +.app-loading-content { + color: white; + text-align: center; +} + +.app-loading-spinner { + width: 3rem; + height: 3rem; + border: 2px solid white; + border-top: 2px solid transparent; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 1rem; +} + +.app-loading-text { + font-size: clamp(0.875rem, 2vw, 1rem); + margin: 0; +} + +/* Responsive Design */ +@media (max-width: 767px) { + .app-loading-spinner { + width: 2.5rem; + height: 2.5rem; + } +} + +@media (min-width: 768px) { + .app-loading-spinner { + width: 3.5rem; + height: 3.5rem; + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..5f2ebcf --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,45 @@ +import { useState, useEffect } from 'react' +import './App.css' +import { AuthProvider, useAuth } from './contexts/AuthContext' +import LandingPage from './components/auth/LandingPage' +import Login from './components/auth/Login' +import Dashboard from './components/common/Dashboard' + +const MacAttendanceApp = () => { + const { user, loading } = useAuth() + const [showLogin, setShowLogin] = useState(false) + + // Resetear showLogin cuando el usuario cierra sesión + useEffect(() => { + if (!user) { + setShowLogin(false) + } + }, [user]) + + if (loading) { + return ( +
+
+
+

Cargando sistema...

+
+
+ ) + } + + if (!user) { + return showLogin ? : setShowLogin(true)} /> + } + + return +} + +function App() { + return ( + + + + ) +} + +export default App \ No newline at end of file diff --git a/frontend/src/components/admin/AdminPanel.jsx b/frontend/src/components/admin/AdminPanel.jsx new file mode 100644 index 0000000..6b87790 --- /dev/null +++ b/frontend/src/components/admin/AdminPanel.jsx @@ -0,0 +1,336 @@ +import { useState, useEffect } from 'react' +import { apiRequest } from '../../services/api' +import ExternalUsersPanel from './ExternalUsersPanel' + +const AdminPanel = () => { + const [activeTab, setActiveTab] = useState('events') + const [events, setEvents] = useState([]) + const [loading, setLoading] = useState(true) + const [showCreateForm, setShowCreateForm] = useState(false) + const [newEvent, setNewEvent] = useState({ + title: '', + description: '', + event_type: 'conference', + modality: 'presencial', + speaker: '', + date: '', + start_time: '', + end_time: '', + location: '', + max_capacity: 100, + meeting_link: '' + }) + + useEffect(() => { + fetchEvents() + }, []) + + const fetchEvents = async () => { + try { + const response = await apiRequest('/events/') + setEvents(response.results || response) + } catch (error) { + console.error('Error fetching events:', error) + } finally { + setLoading(false) + } + } + + const handleCreateEvent = async (e) => { + e.preventDefault() + try { + await apiRequest('/events/', { + method: 'POST', + body: newEvent + }) + alert('Evento creado exitosamente') + setShowCreateForm(false) + setNewEvent({ + title: '', + description: '', + event_type: 'conference', + modality: 'presencial', + speaker: '', + date: '', + start_time: '', + end_time: '', + location: '', + max_capacity: 100, + meeting_link: '' + }) + fetchEvents() + } catch (error) { + alert('Error al crear evento: ' + (error.response?.data?.error || 'Error desconocido')) + } + } + + if (loading) return
Cargando...
+ + return ( +
+

+ Panel de Administración +

+ + {/* Tabs */} +
+ + +
+ + {/* Panel de Eventos */} + {activeTab === 'events' && ( +
+
+ +
+ + {showCreateForm && ( +
+

Crear Nuevo Evento

+
+
+ + setNewEvent({ ...newEvent, title: e.target.value })} + required + style={{ width: '100%', padding: '0.5rem', borderRadius: '0.375rem', border: '1px solid #d1d5db' }} + /> +
+
+ +