forked from val-lop20/Pagina-de-Asistencia-MAC
Agrgar SSL
This commit is contained in:
+16
@@ -76,10 +76,23 @@ Desktop.ini
|
||||
# ==============================================
|
||||
docker-compose.override.yml
|
||||
|
||||
# ==============================================
|
||||
# SSL/TLS Certificados
|
||||
# ==============================================
|
||||
docker/ssl/
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
*.csr
|
||||
*.p12
|
||||
*.pfx
|
||||
|
||||
# ==============================================
|
||||
# Archivos Sensibles y Credenciales
|
||||
# ==============================================
|
||||
.env
|
||||
.env.development
|
||||
.env.production
|
||||
backend/.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
@@ -105,6 +118,9 @@ media/
|
||||
backup/
|
||||
backups/
|
||||
|
||||
# EXCEPCIÓN: Permitir backup compartido para colaboradores
|
||||
!backend/fixtures/db_full_backup.sql
|
||||
|
||||
# ==============================================
|
||||
# Archivos Temporales
|
||||
# ==============================================
|
||||
|
||||
@@ -27,62 +27,95 @@ Sistema de gestión de asistencia para ponencias y eventos académicos de Matem
|
||||
|
||||
## 🛠️ Instalación
|
||||
|
||||
### 🌐 URLs de Acceso
|
||||
|
||||
**Modo Desarrollo (HTTP - Recomendado para local):**
|
||||
- Aplicación: http://localhost
|
||||
- Admin: http://localhost/admin
|
||||
|
||||
**Modo Producción (HTTPS - Con advertencia de certificados):**
|
||||
- Aplicación: https://localhost
|
||||
- Admin: https://localhost/admin
|
||||
|
||||
📖 **Guía completa de acceso:** Ver [ACCESO_AL_SISTEMA.md](ACCESO_AL_SISTEMA.md)
|
||||
|
||||
---
|
||||
|
||||
### Opción 1: Con Docker (Recomendado)
|
||||
|
||||
#### Producción
|
||||
#### Desarrollo Local (HTTP sin SSL - Recomendado)
|
||||
|
||||
```bash
|
||||
# Clonar el repositorio
|
||||
git clone <url-del-repositorio>
|
||||
cd pagina-mac-og
|
||||
cd Pagina-de-Asistencia-MAC
|
||||
|
||||
# Copiar variables de entorno
|
||||
cp .env.example .env
|
||||
# Copiar variables de entorno de desarrollo
|
||||
cp .env.development.example .env.development
|
||||
|
||||
# IMPORTANTE: Editar .env y cambiar las credenciales para producción
|
||||
# Cambiar: SECRET_KEY, DB_PASSWORD, etc.
|
||||
# (Opcional) Personalizar tu .env.development
|
||||
# nano .env.development
|
||||
|
||||
# Usar configuración de desarrollo (HTTP sin SSL)
|
||||
docker-compose -f docker-compose.dev.yml up --build -d
|
||||
|
||||
# Ver logs
|
||||
docker-compose -f docker-compose.dev.yml logs -f
|
||||
|
||||
# Acceder a la aplicación
|
||||
# http://localhost (puerto 80 - sin advertencias de seguridad)
|
||||
```
|
||||
|
||||
El sistema estará disponible en `http://localhost` con:
|
||||
- ✅ **Puerto 80** (HTTP estándar - sin advertencias de seguridad)
|
||||
- ✅ **Debug mode** activado
|
||||
- ✅ **Hot reload** para desarrollo
|
||||
- ✅ **Base de datos PostgreSQL** expuesta en puerto 5432
|
||||
- ✅ **Rate limiting** desactivado
|
||||
|
||||
#### Producción (HTTPS con SSL)
|
||||
|
||||
```bash
|
||||
# Copiar variables de entorno de producción
|
||||
cp .env.production.example .env.production
|
||||
|
||||
# IMPORTANTE: Editar .env.production y cambiar las credenciales
|
||||
nano .env.production
|
||||
# - Generar SECRET_KEY segura: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||
# - Cambiar DB_PASSWORD a una contraseña segura
|
||||
# - Verificar ALLOWED_HOSTS (tu dominio o IP)
|
||||
# - Verificar CORS_ALLOWED_ORIGINS (solo HTTPS)
|
||||
|
||||
# Construir e iniciar contenedores
|
||||
docker-compose up --build -d
|
||||
|
||||
# Ver logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Acceder a la aplicación
|
||||
# http://localhost
|
||||
# https://localhost (puerto 443 - aparecerá advertencia de certificados)
|
||||
```
|
||||
|
||||
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)
|
||||
El sistema estará disponible en `https://localhost` con:
|
||||
- 🔒 **Puerto 443** (HTTPS con SSL/TLS)
|
||||
- 🔒 **Certificados autofirmados** (advertencia "No seguro" en navegador - es normal)
|
||||
- 🔒 **Rate limiting** activado
|
||||
- 🔒 **Configuración de producción**
|
||||
- 🔒 **Base de datos PostgreSQL** NO expuesta (solo red interna)
|
||||
|
||||
#### Desarrollo
|
||||
⚠️ **Advertencia de Certificados:**
|
||||
El navegador mostrará "No es seguro" porque usas certificados autofirmados. Ver [ACCESO_AL_SISTEMA.md](ACCESO_AL_SISTEMA.md) para instrucciones sobre cómo aceptar el certificado en cada navegador
|
||||
|
||||
⚠️ **Nota sobre cambios en .env**:
|
||||
Si modificas el archivo `.env.development` o `.env.production` mientras los contenedores están corriendo, **DEBES reiniciarlos** para que los cambios surtan efecto:
|
||||
|
||||
```bash
|
||||
# Usar configuración de desarrollo
|
||||
docker-compose -f docker-compose.dev.yml up --build -d
|
||||
# Desarrollo
|
||||
docker-compose -f docker-compose.dev.yml restart
|
||||
|
||||
# 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
|
||||
# Producción
|
||||
docker-compose restart
|
||||
```
|
||||
|
||||
**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)
|
||||
@@ -109,7 +142,7 @@ pip install django-import-export openpyxl tablib
|
||||
|
||||
4. Configurar variables de entorno:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
cp .env.development.example .env
|
||||
# Editar .env con tus configuraciones
|
||||
```
|
||||
|
||||
@@ -145,6 +178,44 @@ npm run dev
|
||||
|
||||
El frontend estará disponible en `http://localhost:5173`
|
||||
|
||||
## 🔄 Compartir Base de Datos con Colaboradores
|
||||
|
||||
¿Quieres que tus colaboradores trabajen con los mismos datos que tú? El proyecto incluye scripts para compartir fácilmente la base de datos completa.
|
||||
|
||||
### Para crear un backup y compartirlo:
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
.\create_backup.ps1
|
||||
```
|
||||
|
||||
**Linux/Mac:**
|
||||
```bash
|
||||
chmod +x create_backup.sh
|
||||
./create_backup.sh
|
||||
```
|
||||
|
||||
Esto creará un archivo `backend/fixtures/db_full_backup.sql` que puedes:
|
||||
1. Subir a GitHub (ya está configurado en .gitignore para permitirlo)
|
||||
2. Compartir por Google Drive, Dropbox, etc.
|
||||
|
||||
### Para restaurar el backup compartido:
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
.\restore_database.ps1
|
||||
```
|
||||
|
||||
**Linux/Mac:**
|
||||
```bash
|
||||
chmod +x restore_database.sh
|
||||
./restore_database.sh
|
||||
```
|
||||
|
||||
📖 **Documentación completa:** Ver [COMPARTIR_BASE_DE_DATOS.md](COMPARTIR_BASE_DE_DATOS.md)
|
||||
|
||||
---
|
||||
|
||||
## 📥 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.
|
||||
@@ -282,18 +353,37 @@ Sigue las instrucciones para crear:
|
||||
|
||||
## 🔧 Configuración Adicional
|
||||
|
||||
### Variables de Entorno (.env)
|
||||
### Variables de Entorno
|
||||
|
||||
El proyecto usa archivos `.env` separados para desarrollo y producción:
|
||||
|
||||
**Desarrollo** (`.env.development`):
|
||||
```env
|
||||
SECRET_KEY=tu-clave-secreta
|
||||
DEBUG=True
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:5173
|
||||
SECRET_KEY=django-insecure-dev-key-12345
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1,nginx,backend
|
||||
CORS_ALLOWED_ORIGINS=http://localhost,http://127.0.0.1
|
||||
```
|
||||
|
||||
**Producción** (`.env.production`):
|
||||
```env
|
||||
DEBUG=False
|
||||
SECRET_KEY=<generar-clave-segura>
|
||||
ALLOWED_HOSTS=132.248.80.77,tudominio.com
|
||||
CORS_ALLOWED_ORIGINS=https://132.248.80.77,https://tudominio.com
|
||||
```
|
||||
|
||||
📖 Ver `USO_ENV_FILES.md` para documentación completa sobre variables de entorno.
|
||||
|
||||
⚠️ **IMPORTANTE**:
|
||||
- Los archivos `.env.development` y `.env.production` NO se suben a Git
|
||||
- Usa `.env.development.example` y `.env.production.example` como plantillas
|
||||
- Si modificas un archivo `.env` con contenedores corriendo, reinícialos: `docker-compose restart`
|
||||
|
||||
### CORS
|
||||
El backend está configurado para aceptar peticiones desde:
|
||||
- `http://localhost:5173` (desarrollo)
|
||||
- `http://127.0.0.1:5173` (desarrollo)
|
||||
- **Desarrollo**: `http://localhost`, `http://127.0.0.1` (configurado en `.env.development`)
|
||||
- **Producción**: `https://132.248.80.77` (configurado en `.env.production` - solo HTTPS)
|
||||
|
||||
## 🔒 Seguridad
|
||||
|
||||
@@ -383,12 +473,20 @@ Ver `docs/POSTGRESQL_MIGRATION.md` para más detalles.
|
||||
|
||||
## 📚 Documentación
|
||||
|
||||
### Configuración y Despliegue
|
||||
- `USO_ENV_FILES.md` - **Guía completa de variables de entorno (.env)**
|
||||
- `DEPLOYMENT_PRODUCTION.md` - Despliegue en servidor de producción (132.248.80.77)
|
||||
- `CAMBIOS_SEGURIDAD_PUERTOS.md` - Configuración de puertos y seguridad
|
||||
|
||||
### Desarrollo
|
||||
- `docs/DESARROLLO.md` - Guía completa de desarrollo
|
||||
- `docs/ESTRUCTURA_PROYECTO.md` - Estructura del proyecto
|
||||
- `docs/POSTGRESQL_MIGRATION.md` - Migración a PostgreSQL
|
||||
|
||||
### 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
|
||||
- `docs/POSTGRESQL_MIGRATION.md` - Migración a PostgreSQL
|
||||
- `docs/ESTRUCTURA_PROYECTO.md` - Estructura del proyecto
|
||||
|
||||
## 🚧 Mejoras Futuras
|
||||
|
||||
|
||||
@@ -35,16 +35,15 @@ class AttendanceStatsResource(resources.ModelResource):
|
||||
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']
|
||||
search_fields = ['student__full_name', 'student__account_number', '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')
|
||||
'fields': ('student', 'attendee_name', 'attendee_identifier')
|
||||
}),
|
||||
('Información del Evento', {
|
||||
'fields': ('event',)
|
||||
|
||||
@@ -2,32 +2,22 @@ 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 authentication.models import UserProfile
|
||||
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
|
||||
|
||||
# Estudiante que asiste
|
||||
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,
|
||||
@@ -66,12 +56,9 @@ class Attendance(models.Model):
|
||||
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 haya un estudiante
|
||||
if not self.student:
|
||||
raise ValidationError("Debe especificar un estudiante.")
|
||||
|
||||
# Validar que el registrador sea un asistente
|
||||
if self.registered_by.user_type != 'assistant':
|
||||
@@ -111,29 +98,17 @@ class Attendance(models.Model):
|
||||
)
|
||||
|
||||
# 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)
|
||||
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.")
|
||||
|
||||
# Validar eventos simultáneos
|
||||
if self.student:
|
||||
overlapping_events = Event.objects.filter(
|
||||
date=self.event.date,
|
||||
@@ -160,9 +135,8 @@ class Attendance(models.Model):
|
||||
self.clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Actualizar estadísticas si es estudiante regular
|
||||
if self.student:
|
||||
self.update_student_stats()
|
||||
# Actualizar estadísticas del estudiante
|
||||
self.update_student_stats()
|
||||
|
||||
def update_student_stats(self):
|
||||
"""Actualizar las estadísticas de asistencia del estudiante"""
|
||||
@@ -178,21 +152,13 @@ class Attendance(models.Model):
|
||||
|
||||
@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"
|
||||
|
||||
"""Nombre del asistente"""
|
||||
return self.student.full_name if self.student else "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"
|
||||
return self.student.account_number if self.student else "N/A"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.attendee_name} - {self.event.title}"
|
||||
|
||||
@@ -6,6 +6,4 @@ urlpatterns = [
|
||||
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'),
|
||||
]
|
||||
+13
-178
@@ -4,7 +4,7 @@ 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 authentication.models import UserProfile
|
||||
from events.models import Event
|
||||
from .models import Attendance, AttendanceStats
|
||||
|
||||
@@ -48,62 +48,37 @@ def register_attendance(request):
|
||||
'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
|
||||
# Buscar estudiante
|
||||
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
|
||||
return Response({
|
||||
'error': f'Estudiante con número de cuenta {account_number} no encontrado'
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# 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)
|
||||
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)
|
||||
|
||||
# Crear asistencia
|
||||
try:
|
||||
attendance = Attendance.objects.create(
|
||||
student=student_profile,
|
||||
external_user=external_user,
|
||||
event=event,
|
||||
registered_by=assistant_profile,
|
||||
registered_by=registrar_profile,
|
||||
registration_method='manual'
|
||||
)
|
||||
|
||||
return Response({
|
||||
'message': f'Asistencia registrada para {attendee_name}',
|
||||
'message': f'Asistencia registrada para {student_profile.full_name}',
|
||||
'attendance_id': attendance.id,
|
||||
'event': event.title,
|
||||
'registered_by': assistant_profile.full_name,
|
||||
'attendee_type': 'student' if student_profile else 'external'
|
||||
'registered_by': registrar_profile.full_name
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
|
||||
except Exception as e:
|
||||
@@ -244,144 +219,4 @@ def get_my_attendances(request):
|
||||
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)
|
||||
# Funciones de usuarios externos eliminadas - Todos los usuarios son ahora estudiantes regulares
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Script para cargar datos iniciales desde archivos Excel a la base de datos.
|
||||
Este script importa:
|
||||
- Eventos desde Conferencias MAC Agenda Completa.xlsx
|
||||
- Estudiantes desde Student.xlsx
|
||||
- Perfiles de Asistentes desde AssistantProfile.xlsx
|
||||
- Estadísticas de Asistencia desde AttendanceStats.xlsx
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
from pathlib import Path
|
||||
|
||||
# Configurar Django
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
sys.path.append(str(BASE_DIR))
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mac_attendance.settings')
|
||||
django.setup()
|
||||
|
||||
import openpyxl
|
||||
from datetime import datetime, time
|
||||
from django.contrib.auth.models import User
|
||||
from authentication.models import Student, AssistantProfile, Asistente, SystemConfiguration
|
||||
from events.models import Event
|
||||
from attendance.models import Attendance, AttendanceStats
|
||||
from django.utils import timezone
|
||||
|
||||
def import_events(file_path):
|
||||
"""Importar eventos desde Conferencias MAC Agenda Completa.xlsx"""
|
||||
print(f"\n📅 Importando eventos desde {file_path}...")
|
||||
|
||||
wb = openpyxl.load_workbook(file_path)
|
||||
ws = wb.active
|
||||
|
||||
events_created = 0
|
||||
events_updated = 0
|
||||
|
||||
# Saltar el encabezado (primera fila)
|
||||
for row in ws.iter_rows(min_row=2, values_only=True):
|
||||
if not row[0]: # Si no hay título, saltar
|
||||
continue
|
||||
|
||||
title = row[0]
|
||||
speaker = row[1] if len(row) > 1 and row[1] else ""
|
||||
date_str = row[2] if len(row) > 2 and row[2] else None
|
||||
start_time_str = row[3] if len(row) > 3 and row[3] else None
|
||||
end_time_str = row[4] if len(row) > 4 and row[4] else None
|
||||
event_type = row[5] if len(row) > 5 and row[5] else "Conferencia"
|
||||
modality = row[6] if len(row) > 6 and row[6] else "Presencial"
|
||||
location = row[7] if len(row) > 7 and row[7] else ""
|
||||
description = row[8] if len(row) > 8 and row[8] else ""
|
||||
is_active = bool(row[9]) if len(row) > 9 and row[9] else True
|
||||
|
||||
if not date_str or not start_time_str:
|
||||
print(f" ⚠️ Saltando evento sin fecha u hora: {title}")
|
||||
continue
|
||||
|
||||
# Convertir fecha
|
||||
if isinstance(date_str, datetime):
|
||||
event_date = date_str.date()
|
||||
elif isinstance(date_str, str):
|
||||
try:
|
||||
event_date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
except:
|
||||
try:
|
||||
event_date = datetime.strptime(date_str, '%d/%m/%Y').date()
|
||||
except:
|
||||
print(f" ⚠️ Formato de fecha inválido para {title}: {date_str}")
|
||||
continue
|
||||
else:
|
||||
event_date = date_str
|
||||
|
||||
# Convertir hora de inicio
|
||||
if isinstance(start_time_str, time):
|
||||
start_time = start_time_str
|
||||
elif isinstance(start_time_str, str):
|
||||
try:
|
||||
start_time = datetime.strptime(start_time_str, '%H:%M:%S').time()
|
||||
except:
|
||||
try:
|
||||
start_time = datetime.strptime(start_time_str, '%H:%M').time()
|
||||
except:
|
||||
print(f" ⚠️ Formato de hora inválido para {title}: {start_time_str}")
|
||||
continue
|
||||
else:
|
||||
start_time = start_time_str
|
||||
|
||||
# Convertir hora de fin
|
||||
end_time = None
|
||||
if end_time_str:
|
||||
if isinstance(end_time_str, time):
|
||||
end_time = end_time_str
|
||||
elif isinstance(end_time_str, str):
|
||||
try:
|
||||
end_time = datetime.strptime(end_time_str, '%H:%M:%S').time()
|
||||
except:
|
||||
try:
|
||||
end_time = datetime.strptime(end_time_str, '%H:%M').time()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Crear o actualizar evento (sin validaciones para permitir fechas pasadas)
|
||||
try:
|
||||
event = Event.objects.get(title=title, date=event_date)
|
||||
# Actualizar usando update() para evitar validaciones
|
||||
Event.objects.filter(id=event.id).update(
|
||||
description=description,
|
||||
location=location,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
is_active=is_active
|
||||
)
|
||||
created = False
|
||||
except Event.DoesNotExist:
|
||||
# Crear sin validaciones usando el método directo de la base
|
||||
event = Event(
|
||||
title=title,
|
||||
date=event_date,
|
||||
description=description,
|
||||
location=location,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
is_active=is_active
|
||||
)
|
||||
# Guardar sin llamar a clean() para evitar validación de fechas pasadas
|
||||
super(Event, event).save(force_insert=True)
|
||||
created = True
|
||||
|
||||
if created:
|
||||
events_created += 1
|
||||
print(f" ✅ Evento creado: {title} - {event_date} {start_time}")
|
||||
else:
|
||||
events_updated += 1
|
||||
|
||||
print(f"\n✅ Eventos importados: {events_created} creados, {events_updated} actualizados")
|
||||
return events_created + events_updated
|
||||
|
||||
|
||||
def import_students(file_path):
|
||||
"""Importar estudiantes desde Student.xlsx"""
|
||||
print(f"\n👨🎓 Importando estudiantes desde {file_path}...")
|
||||
|
||||
wb = openpyxl.load_workbook(file_path)
|
||||
ws = wb.active
|
||||
|
||||
students_created = 0
|
||||
students_updated = 0
|
||||
|
||||
for row in ws.iter_rows(min_row=2, values_only=True):
|
||||
if not row[0]: # Si no hay número de cuenta, saltar
|
||||
continue
|
||||
|
||||
account_number = str(row[0]).strip()
|
||||
full_name = row[1] if len(row) > 1 and row[1] else "Sin nombre"
|
||||
|
||||
student, created = Student.objects.update_or_create(
|
||||
account_number=account_number,
|
||||
defaults={
|
||||
'full_name': full_name,
|
||||
'user_type': 'student'
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
students_created += 1
|
||||
print(f" ✅ Estudiante creado: {account_number} - {full_name}")
|
||||
else:
|
||||
students_updated += 1
|
||||
|
||||
print(f"\n✅ Estudiantes importados: {students_created} creados, {students_updated} actualizados")
|
||||
return students_created + students_updated
|
||||
|
||||
|
||||
def import_assistant_profiles(file_path):
|
||||
"""Importar perfiles de asistentes desde AssistantProfile.xlsx"""
|
||||
print(f"\n👔 Importando perfiles de asistentes desde {file_path}...")
|
||||
|
||||
from authentication.models import UserProfile
|
||||
wb = openpyxl.load_workbook(file_path)
|
||||
ws = wb.active
|
||||
|
||||
profiles_created = 0
|
||||
profiles_updated = 0
|
||||
profiles_skipped = 0
|
||||
|
||||
for row in ws.iter_rows(min_row=2, values_only=True):
|
||||
if not row[0]: # Si no hay número de cuenta, saltar
|
||||
continue
|
||||
|
||||
account_number = str(row[0]).strip()
|
||||
full_name = row[1] if len(row) > 1 and row[1] else "Sin nombre"
|
||||
|
||||
# Verificar si ya existe un UserProfile con este account_number
|
||||
existing_profile = UserProfile.objects.filter(account_number=account_number).first()
|
||||
|
||||
if existing_profile:
|
||||
# Si existe y es estudiante, actualizar a asistente
|
||||
if existing_profile.user_type == 'student':
|
||||
print(f" ⚠️ Saltando {account_number} - ya existe como estudiante")
|
||||
profiles_skipped += 1
|
||||
continue
|
||||
else:
|
||||
# Si ya es asistente, actualizar
|
||||
existing_profile.full_name = full_name
|
||||
existing_profile.save()
|
||||
profiles_updated += 1
|
||||
else:
|
||||
# No existe, crear nuevo
|
||||
profile = AssistantProfile.objects.create(
|
||||
account_number=account_number,
|
||||
full_name=full_name,
|
||||
user_type='assistant'
|
||||
)
|
||||
profiles_created += 1
|
||||
print(f" ✅ Perfil de asistente creado: {account_number} - {full_name}")
|
||||
|
||||
print(f"\n✅ Perfiles de asistentes importados: {profiles_created} creados, {profiles_updated} actualizados, {profiles_skipped} saltados")
|
||||
return profiles_created + profiles_updated
|
||||
|
||||
|
||||
def assign_permissions_to_assistant():
|
||||
"""Asignar permisos al asistente para registrar asistencias"""
|
||||
print(f"\n🔑 Asignando permisos a asistentes...")
|
||||
|
||||
from authentication.models import UserProfile
|
||||
# Obtener solo los UserProfile que son asistentes
|
||||
assistants = UserProfile.objects.filter(user_type='assistant')
|
||||
permissions_created = 0
|
||||
|
||||
for assistant_profile in assistants:
|
||||
# Crear permiso de asistente (Asistente model)
|
||||
asistente, created = Asistente.objects.get_or_create(
|
||||
user_profile=assistant_profile,
|
||||
defaults={
|
||||
'can_manage_events': True
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
permissions_created += 1
|
||||
print(f" ✅ Permisos asignados a: {assistant_profile.full_name}")
|
||||
|
||||
print(f"\n✅ Permisos asignados: {permissions_created} asistentes")
|
||||
return permissions_created
|
||||
|
||||
|
||||
def import_attendance_stats(file_path):
|
||||
"""Importar estadísticas de asistencia desde AttendanceStats.xlsx"""
|
||||
print(f"\n📊 Importando estadísticas de asistencia desde {file_path}...")
|
||||
|
||||
from authentication.models import UserProfile
|
||||
wb = openpyxl.load_workbook(file_path)
|
||||
ws = wb.active
|
||||
|
||||
stats_created = 0
|
||||
attendances_created = 0
|
||||
|
||||
# Obtener eventos principales
|
||||
event1 = Event.objects.filter(title__icontains="Transformación Digital").first()
|
||||
event2 = Event.objects.filter(title__icontains="Finanzas").first()
|
||||
|
||||
if not event1:
|
||||
print(" ⚠️ Evento 'Transformación Digital' no encontrado")
|
||||
if not event2:
|
||||
print(" ⚠️ Evento 'Finanzas' no encontrado")
|
||||
|
||||
# Obtener el asistente para registered_by
|
||||
assistant = UserProfile.objects.filter(user_type='assistant').first()
|
||||
if not assistant:
|
||||
print(" ⚠️ No hay asistentes en el sistema. Creando asistente por defecto...")
|
||||
assistant = UserProfile.objects.create(
|
||||
account_number='99999999',
|
||||
full_name='Sistema - Importación Automática',
|
||||
user_type='assistant'
|
||||
)
|
||||
print(f" ✅ Asistente creado: {assistant.full_name}")
|
||||
|
||||
for row in ws.iter_rows(min_row=2, values_only=True):
|
||||
if not row[0]: # Si no hay número de cuenta, saltar
|
||||
continue
|
||||
|
||||
account_number = str(row[0]).strip()
|
||||
full_name = row[1] if len(row) > 1 else ""
|
||||
attended_events = int(row[2]) if len(row) > 2 and row[2] else 0
|
||||
total_events = int(row[3]) if len(row) > 3 and row[3] else 18
|
||||
attendance_percentage = float(row[4]) if len(row) > 4 and row[4] else 0.0
|
||||
|
||||
try:
|
||||
student = Student.objects.get(account_number=account_number)
|
||||
except Student.DoesNotExist:
|
||||
# Si el estudiante no existe, crearlo
|
||||
student = Student.objects.create(
|
||||
account_number=account_number,
|
||||
full_name=full_name,
|
||||
user_type='student'
|
||||
)
|
||||
print(f" ℹ️ Estudiante creado desde stats: {account_number}")
|
||||
|
||||
# Crear estadísticas
|
||||
stats, created = AttendanceStats.objects.update_or_create(
|
||||
student=student,
|
||||
defaults={
|
||||
'total_events': total_events,
|
||||
'attended_events': attended_events,
|
||||
'attendance_percentage': attendance_percentage
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
stats_created += 1
|
||||
|
||||
# Crear asistencias según el número total
|
||||
if attended_events == 2:
|
||||
# Asistió a ambas conferencias
|
||||
if event1:
|
||||
try:
|
||||
attendance = Attendance.objects.get(student=student, event=event1)
|
||||
att_created = False
|
||||
except Attendance.DoesNotExist:
|
||||
attendance = Attendance(
|
||||
student=student,
|
||||
event=event1,
|
||||
timestamp=timezone.now(),
|
||||
is_valid=True,
|
||||
registered_by=assistant
|
||||
)
|
||||
# Guardar sin validaciones
|
||||
super(Attendance, attendance).save(force_insert=True)
|
||||
att_created = True
|
||||
attendances_created += 1
|
||||
|
||||
if event2:
|
||||
try:
|
||||
attendance = Attendance.objects.get(student=student, event=event2)
|
||||
att_created = False
|
||||
except Attendance.DoesNotExist:
|
||||
attendance = Attendance(
|
||||
student=student,
|
||||
event=event2,
|
||||
timestamp=timezone.now(),
|
||||
is_valid=True,
|
||||
registered_by=assistant
|
||||
)
|
||||
# Guardar sin validaciones
|
||||
super(Attendance, attendance).save(force_insert=True)
|
||||
att_created = True
|
||||
attendances_created += 1
|
||||
|
||||
elif attended_events == 1:
|
||||
# Asistió solo a "Transformación Digital"
|
||||
if event1:
|
||||
try:
|
||||
attendance = Attendance.objects.get(student=student, event=event1)
|
||||
att_created = False
|
||||
except Attendance.DoesNotExist:
|
||||
attendance = Attendance(
|
||||
student=student,
|
||||
event=event1,
|
||||
timestamp=timezone.now(),
|
||||
is_valid=True,
|
||||
registered_by=assistant
|
||||
)
|
||||
# Guardar sin validaciones
|
||||
super(Attendance, attendance).save(force_insert=True)
|
||||
att_created = True
|
||||
attendances_created += 1
|
||||
|
||||
print(f"\n✅ Estadísticas importadas: {stats_created} creadas")
|
||||
print(f"✅ Asistencias creadas: {attendances_created}")
|
||||
return stats_created
|
||||
|
||||
|
||||
def configure_system_settings():
|
||||
"""Configurar settings globales del sistema"""
|
||||
print(f"\n⚙️ Configurando sistema global...")
|
||||
|
||||
config, created = SystemConfiguration.objects.get_or_create(
|
||||
id=1,
|
||||
defaults={
|
||||
'minimum_attendance_percentage': 80.0,
|
||||
'minutes_before_event': 10, # 10 minutos antes
|
||||
'minutes_after_start': 25, # 25 minutos después
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
print(f" ✅ Configuración creada: 80% asistencia, 10min antes, 25min después")
|
||||
else:
|
||||
config.minimum_attendance_percentage = 80.0
|
||||
config.minutes_before_event = 10
|
||||
config.minutes_after_start = 25
|
||||
config.save()
|
||||
print(f" 🔄 Configuración actualizada")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def main():
|
||||
"""Función principal"""
|
||||
print("="*60)
|
||||
print("🚀 CARGA DE DATOS INICIALES - SISTEMA DE ASISTENCIAS MAC")
|
||||
print("="*60)
|
||||
|
||||
# Rutas a los archivos Excel
|
||||
base_path = Path(__file__).resolve().parent
|
||||
|
||||
events_file = base_path / "Conferencias MAC Agenda Completa (1) (1).xlsx"
|
||||
students_file = base_path / "Student-2025-10-21.xlsx"
|
||||
assistants_file = base_path / "AssistantProfile-2025-10-21.xlsx"
|
||||
stats_file = base_path / "AttendanceStats-2025-10-21.xlsx"
|
||||
|
||||
# Verificar que existan los archivos
|
||||
files_ok = True
|
||||
for file_path, name in [
|
||||
(events_file, "Eventos"),
|
||||
(students_file, "Estudiantes"),
|
||||
(assistants_file, "Asistentes"),
|
||||
(stats_file, "Estadísticas")
|
||||
]:
|
||||
if not file_path.exists():
|
||||
print(f"❌ Archivo no encontrado: {name} - {file_path}")
|
||||
files_ok = False
|
||||
|
||||
if not files_ok:
|
||||
print("\n⚠️ Algunos archivos no se encontraron. Abortando.")
|
||||
return
|
||||
|
||||
# Importar datos
|
||||
try:
|
||||
# 1. Configurar sistema
|
||||
configure_system_settings()
|
||||
|
||||
# 2. Importar eventos
|
||||
import_events(events_file)
|
||||
|
||||
# 3. Importar estudiantes
|
||||
import_students(students_file)
|
||||
|
||||
# 4. Importar asistentes
|
||||
import_assistant_profiles(assistants_file)
|
||||
|
||||
# 5. Asignar permisos a asistentes
|
||||
assign_permissions_to_assistant()
|
||||
|
||||
# 6. Importar estadísticas y asistencias
|
||||
import_attendance_stats(stats_file)
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("✅ IMPORTACIÓN COMPLETADA EXITOSAMENTE")
|
||||
print("="*60)
|
||||
|
||||
# Mostrar resumen
|
||||
print(f"\n📊 Resumen de datos en la base de datos:")
|
||||
print(f" - Eventos: {Event.objects.count()}")
|
||||
print(f" - Estudiantes: {Student.objects.count()}")
|
||||
print(f" - Asistentes: {AssistantProfile.objects.count()}")
|
||||
print(f" - Permisos de asistentes: {Asistente.objects.count()}")
|
||||
print(f" - Asistencias: {Attendance.objects.count()}")
|
||||
print(f" - Estadísticas: {AttendanceStats.objects.count()}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error durante la importación: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,126 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -19,7 +19,19 @@ SECRET_KEY = config('SECRET_KEY')
|
||||
|
||||
DEBUG = config('DEBUG', default=False, cast=bool)
|
||||
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1').split(',')
|
||||
# ALLOWED_HOSTS - En producción NO debe incluir localhost
|
||||
# Solo se permite localhost si DEBUG=True
|
||||
if DEBUG:
|
||||
# Desarrollo: permitir localhost
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1').split(',')
|
||||
else:
|
||||
# Producción: NO permitir localhost, solo dominios reales
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='').split(',')
|
||||
if not ALLOWED_HOSTS or ALLOWED_HOSTS == ['']:
|
||||
raise ValueError(
|
||||
"⚠️ ERROR: ALLOWED_HOSTS debe configurarse en producción (DEBUG=False). "
|
||||
"Ejemplo: ALLOWED_HOSTS=tudominio.com,www.tudominio.com"
|
||||
)
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
@@ -42,12 +54,12 @@ MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'mac_attendance.middleware.DisableCSRFOnAPIMiddleware', # Exentar API de CSRF (usamos JWT)
|
||||
'authentication.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
|
||||
'authentication.middleware.AuditMiddleware', # Auditoría de seguridad
|
||||
]
|
||||
|
||||
# Rate Limiting Configuration
|
||||
@@ -146,20 +158,39 @@ if not DEBUG:
|
||||
# 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 settings - NO permitir localhost en producción
|
||||
if DEBUG:
|
||||
# Desarrollo: permitir localhost
|
||||
CORS_ALLOWED_ORIGINS = config(
|
||||
'CORS_ALLOWED_ORIGINS',
|
||||
default='http://localhost,http://127.0.0.1,https://localhost,https://127.0.0.1'
|
||||
).split(',')
|
||||
CSRF_TRUSTED_ORIGINS = config(
|
||||
'CSRF_TRUSTED_ORIGINS',
|
||||
default='http://localhost,http://127.0.0.1,https://localhost,https://127.0.0.1'
|
||||
).split(',')
|
||||
else:
|
||||
# Producción: NO permitir localhost, solo HTTPS con dominio real
|
||||
CORS_ALLOWED_ORIGINS = config('CORS_ALLOWED_ORIGINS', default='').split(',')
|
||||
CSRF_TRUSTED_ORIGINS = config('CSRF_TRUSTED_ORIGINS', default='').split(',')
|
||||
|
||||
# Validar que se hayan configurado orígenes en producción
|
||||
if not CORS_ALLOWED_ORIGINS or CORS_ALLOWED_ORIGINS == ['']:
|
||||
raise ValueError(
|
||||
"⚠️ ERROR: CORS_ALLOWED_ORIGINS debe configurarse en producción. "
|
||||
"Ejemplo: CORS_ALLOWED_ORIGINS=https://tudominio.com,https://www.tudominio.com"
|
||||
)
|
||||
|
||||
# Validar que TODOS los orígenes usen HTTPS
|
||||
for origin in CORS_ALLOWED_ORIGINS:
|
||||
if not origin.startswith('https://'):
|
||||
raise ValueError(
|
||||
f"⚠️ ERROR: En producción, todos los orígenes deben usar HTTPS. "
|
||||
f"Origen inválido: {origin}"
|
||||
)
|
||||
|
||||
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/.*$']
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Settings module for mac_attendance project.
|
||||
|
||||
Este módulo carga automáticamente la configuración correcta basándose
|
||||
en la variable de entorno DJANGO_SETTINGS_MODULE.
|
||||
|
||||
Estructura:
|
||||
- base.py: Configuración común para todos los entornos
|
||||
- local.py: Configuración para desarrollo local
|
||||
- production.py: Configuración para producción
|
||||
|
||||
Uso:
|
||||
- Desarrollo: DJANGO_SETTINGS_MODULE=mac_attendance.settings.local
|
||||
- Producción: DJANGO_SETTINGS_MODULE=mac_attendance.settings.production
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Determinar qué configuración cargar basándose en la variable de entorno
|
||||
DJANGO_ENV = os.environ.get('DJANGO_ENV', 'production')
|
||||
|
||||
if DJANGO_ENV == 'local':
|
||||
from .local import * # noqa
|
||||
elif DJANGO_ENV == 'production':
|
||||
from .production import * # noqa
|
||||
else:
|
||||
# Por defecto, usar producción (más seguro)
|
||||
from .production import * # noqa
|
||||
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Django settings for mac_attendance project - BASE configuration.
|
||||
|
||||
Configuración compartida entre todos los entornos (desarrollo y producción).
|
||||
NO incluye configuraciones específicas de entorno como DEBUG, ALLOWED_HOSTS, etc.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from datetime import timedelta
|
||||
from decouple import config
|
||||
import os
|
||||
|
||||
# Build paths inside the project
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = config('SECRET_KEY')
|
||||
|
||||
# Application definition
|
||||
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',
|
||||
'authentication.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',
|
||||
'authentication.middleware.AuditMiddleware', # Auditoría de seguridad
|
||||
]
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
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',
|
||||
},
|
||||
]
|
||||
|
||||
# Internationalization
|
||||
LANGUAGE_CODE = 'es-mx'
|
||||
TIME_ZONE = 'America/Mexico_City'
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
|
||||
# Media files
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
# Default primary key field type
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# CORS settings
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# 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
|
||||
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',
|
||||
}
|
||||
|
||||
# Crear directorio de logs si no existe
|
||||
LOGS_DIR = BASE_DIR / 'logs'
|
||||
if not os.path.exists(LOGS_DIR):
|
||||
os.makedirs(LOGS_DIR)
|
||||
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Django settings for mac_attendance project - LOCAL/DEVELOPMENT configuration.
|
||||
|
||||
Configuración específica para entorno de desarrollo local.
|
||||
Hereda de base.py y sobreescribe/agrega configuraciones de desarrollo.
|
||||
"""
|
||||
|
||||
from .base import * # noqa
|
||||
from decouple import config
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
# Allowed hosts en desarrollo
|
||||
ALLOWED_HOSTS = config(
|
||||
'ALLOWED_HOSTS',
|
||||
default='localhost,127.0.0.1,nginx,backend'
|
||||
).split(',')
|
||||
|
||||
# CORS Settings - Desarrollo (permitir localhost)
|
||||
CORS_ALLOWED_ORIGINS = config(
|
||||
'CORS_ALLOWED_ORIGINS',
|
||||
default='http://localhost,http://127.0.0.1,http://localhost:80,https://localhost,https://127.0.0.1'
|
||||
).split(',')
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = config(
|
||||
'CSRF_TRUSTED_ORIGINS',
|
||||
default='http://localhost,http://127.0.0.1,https://localhost,https://127.0.0.1'
|
||||
).split(',')
|
||||
|
||||
# Security Settings - Relajadas para desarrollo
|
||||
SECURE_SSL_REDIRECT = False
|
||||
SESSION_COOKIE_SECURE = False
|
||||
CSRF_COOKIE_SECURE = False
|
||||
|
||||
# Rate Limiting - Desactivado en desarrollo
|
||||
RATELIMIT_ENABLE = False
|
||||
RATELIMIT_USE_CACHE = 'default'
|
||||
|
||||
# Cache Configuration - LocMem para desarrollo
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'LOCATION': 'ratelimit-cache',
|
||||
}
|
||||
}
|
||||
|
||||
# Logging Configuration - Solo consola en desarrollo
|
||||
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',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['console'],
|
||||
'level': 'DEBUG',
|
||||
},
|
||||
'django.security': {
|
||||
'handlers': ['console'],
|
||||
'level': 'WARNING',
|
||||
'propagate': False,
|
||||
},
|
||||
'authentication.audit': {
|
||||
'handlers': ['console'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'django_ratelimit': {
|
||||
'handlers': ['console'],
|
||||
'level': 'WARNING',
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Email backend - Console para desarrollo (imprime en consola)
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
# Mostrar toolbar de debug si está instalada (opcional)
|
||||
# if 'debug_toolbar' not in INSTALLED_APPS:
|
||||
# INSTALLED_APPS += ['debug_toolbar']
|
||||
# MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE
|
||||
# INTERNAL_IPS = ['127.0.0.1']
|
||||
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
Django settings for mac_attendance project - PRODUCTION configuration.
|
||||
|
||||
Configuración específica para entorno de producción.
|
||||
Hereda de base.py y sobreescribe/agrega configuraciones de producción.
|
||||
"""
|
||||
|
||||
from .base import * # noqa
|
||||
from decouple import config
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
|
||||
# Allowed hosts - PRODUCCIÓN (NO permitir localhost)
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='').split(',')
|
||||
if not ALLOWED_HOSTS or ALLOWED_HOSTS == ['']:
|
||||
raise ValueError(
|
||||
"⚠️ ERROR: ALLOWED_HOSTS debe configurarse en producción (DEBUG=False). "
|
||||
"Ejemplo: ALLOWED_HOSTS=132.248.80.77,tudominio.com,www.tudominio.com"
|
||||
)
|
||||
|
||||
# CORS Settings - PRODUCCIÓN (solo HTTPS, NO localhost)
|
||||
CORS_ALLOWED_ORIGINS = config('CORS_ALLOWED_ORIGINS', default='').split(',')
|
||||
CSRF_TRUSTED_ORIGINS = config('CSRF_TRUSTED_ORIGINS', default='').split(',')
|
||||
|
||||
# Validar que se hayan configurado orígenes en producción
|
||||
if not CORS_ALLOWED_ORIGINS or CORS_ALLOWED_ORIGINS == ['']:
|
||||
raise ValueError(
|
||||
"⚠️ ERROR: CORS_ALLOWED_ORIGINS debe configurarse en producción. "
|
||||
"Ejemplo: CORS_ALLOWED_ORIGINS=https://132.248.80.77,https://tudominio.com"
|
||||
)
|
||||
|
||||
# Validar que TODOS los orígenes usen HTTPS
|
||||
for origin in CORS_ALLOWED_ORIGINS:
|
||||
if not origin.startswith('https://'):
|
||||
raise ValueError(
|
||||
f"⚠️ ERROR: En producción, todos los orígenes deben usar HTTPS. "
|
||||
f"Origen inválido: {origin}"
|
||||
)
|
||||
|
||||
# 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')
|
||||
|
||||
# Rate Limiting - ACTIVADO en producción
|
||||
RATELIMIT_ENABLE = config('RATELIMIT_ENABLE', default=True, cast=bool)
|
||||
RATELIMIT_USE_CACHE = 'default'
|
||||
|
||||
# Cache Configuration - Puede usar Redis/Memcached en producción
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'LOCATION': 'ratelimit-cache',
|
||||
}
|
||||
}
|
||||
# TODO: Para mejor rendimiento en producción, considerar Redis:
|
||||
# CACHES = {
|
||||
# 'default': {
|
||||
# 'BACKEND': 'django_redis.cache.RedisCache',
|
||||
# 'LOCATION': config('REDIS_URL', default='redis://127.0.0.1:6379/1'),
|
||||
# 'OPTIONS': {
|
||||
# 'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
# Logging Configuration - Archivos en producción
|
||||
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', # noqa
|
||||
'formatter': 'verbose',
|
||||
},
|
||||
'security_file': {
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': BASE_DIR / 'logs' / 'security.log', # noqa
|
||||
'formatter': 'audit',
|
||||
},
|
||||
'audit_file': {
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': BASE_DIR / 'logs' / 'audit.log', # noqa
|
||||
'formatter': 'audit',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': 'INFO',
|
||||
},
|
||||
'django.security': {
|
||||
'handlers': ['console', 'security_file'],
|
||||
'level': 'WARNING',
|
||||
'propagate': False,
|
||||
},
|
||||
'authentication.audit': {
|
||||
'handlers': ['console', 'audit_file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'django_ratelimit': {
|
||||
'handlers': ['console', 'security_file'],
|
||||
'level': 'WARNING',
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Email backend - Real en producción (requiere configuración SMTP)
|
||||
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
# EMAIL_HOST = config('EMAIL_HOST', default='smtp.gmail.com')
|
||||
# EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int)
|
||||
# EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=True, cast=bool)
|
||||
# EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
|
||||
# EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
|
||||
# DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default='noreply@mac-fes.unam.mx')
|
||||
|
||||
# Admins - Para recibir emails de errores 500
|
||||
# ADMINS = [
|
||||
# ('Admin MAC', 'admin@mac-fes.unam.mx'),
|
||||
# ]
|
||||
# MANAGERS = ADMINS
|
||||
@@ -16,253 +16,7 @@ Including another URLconf
|
||||
"""
|
||||
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/<user_id>/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>',
|
||||
'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 = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API Documentation - Sistema MAC</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 40px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.endpoint {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
.method {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.GET { background: #28a745; color: white; }
|
||||
.POST { background: #007bff; color: white; }
|
||||
.PUT { background: #ffc107; color: black; }
|
||||
.DELETE { background: #dc3545; color: white; }
|
||||
code {
|
||||
background: #f4f4f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
.auth-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>📚 API Documentation</h1>
|
||||
<p>Sistema de Asistencia MAC - FES Acatlán UNAM</p>
|
||||
<p><strong>Version:</strong> 1.0.0 | <strong>Base URL:</strong> /api/</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h2>🔐 Authentication Endpoints</h2>
|
||||
|
||||
<h3><span class="method POST">POST</span> /api/auth/login/</h3>
|
||||
<p>Iniciar sesión con número de cuenta</p>
|
||||
<pre><code>{
|
||||
"account_number": "1234567"
|
||||
}</code></pre>
|
||||
|
||||
<h3><span class="method GET">GET</span> /api/auth/profile/ <span class="auth-badge">AUTH</span></h3>
|
||||
<p>Obtener perfil del usuario autenticado</p>
|
||||
|
||||
<h3><span class="method POST">POST</span> /api/auth/token/refresh/</h3>
|
||||
<p>Refrescar access token</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h2>📅 Events Endpoints</h2>
|
||||
|
||||
<h3><span class="method GET">GET</span> /api/events/</h3>
|
||||
<p>Listar todos los eventos activos</p>
|
||||
|
||||
<h3><span class="method POST">POST</span> /api/events/external/register/ <span class="auth-badge">AUTH</span></h3>
|
||||
<p>Registrar un usuario externo para eventos</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h2>✅ Attendance Endpoints</h2>
|
||||
|
||||
<h3><span class="method POST">POST</span> /api/attendance/ <span class="auth-badge">AUTH</span></h3>
|
||||
<p>Registrar asistencia a un evento</p>
|
||||
|
||||
<h3><span class="method GET">GET</span> /api/attendance/stats/ <span class="auth-badge">AUTH</span></h3>
|
||||
<p>Obtener estadísticas de asistencia del usuario</p>
|
||||
|
||||
<h3><span class="method GET">GET</span> /api/attendance/my/ <span class="auth-badge">AUTH</span></h3>
|
||||
<p>Obtener mis asistencias registradas</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h2>🔑 Authentication</h2>
|
||||
<p>La API utiliza <strong>JWT (JSON Web Tokens)</strong> para autenticación.</p>
|
||||
<p>Para endpoints que requieren autenticación, incluye el header:</p>
|
||||
<pre><code>Authorization: Bearer <your_access_token></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h2>📖 Quick Links</h2>
|
||||
<ul>
|
||||
<li><a href="/api/">API Root (JSON)</a></li>
|
||||
<li><a href="/admin/">Admin Panel</a></li>
|
||||
<li><a href="/">Frontend Application</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HttpResponse(html_content)
|
||||
from .views import api_root, api_docs
|
||||
|
||||
urlpatterns = [
|
||||
path('', api_root, name='api_root'),
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Vistas del proyecto principal MAC Attendance
|
||||
"""
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import render
|
||||
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/<user_id>/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>',
|
||||
'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)"""
|
||||
return render(request, 'api_docs.html')
|
||||
@@ -30,6 +30,7 @@ django-ratelimit==4.1.0
|
||||
|
||||
# Exportación de datos
|
||||
openpyxl==3.1.5
|
||||
pandas==2.2.0
|
||||
|
||||
# Generación de PDFs
|
||||
# reportlab==4.2.5
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API Documentation - Sistema MAC</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 40px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.endpoint {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
.method {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.GET { background: #28a745; color: white; }
|
||||
.POST { background: #007bff; color: white; }
|
||||
.PUT { background: #ffc107; color: black; }
|
||||
.DELETE { background: #dc3545; color: white; }
|
||||
code {
|
||||
background: #f4f4f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
.auth-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>📚 API Documentation</h1>
|
||||
<p>Sistema de Asistencia MAC - FES Acatlán UNAM</p>
|
||||
<p><strong>Version:</strong> 1.0.0 | <strong>Base URL:</strong> /api/</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h2>🔐 Authentication Endpoints</h2>
|
||||
|
||||
<h3><span class="method POST">POST</span> /api/auth/login/</h3>
|
||||
<p>Iniciar sesión con número de cuenta</p>
|
||||
<pre><code>{
|
||||
"account_number": "12345678"
|
||||
}</code></pre>
|
||||
|
||||
<h3><span class="method GET">GET</span> /api/auth/profile/ <span class="auth-badge">AUTH</span></h3>
|
||||
<p>Obtener perfil del usuario autenticado</p>
|
||||
|
||||
<h3><span class="method POST">POST</span> /api/auth/token/refresh/</h3>
|
||||
<p>Refrescar access token</p>
|
||||
|
||||
<h3><span class="method POST">POST</span> /api/auth/logout/</h3>
|
||||
<p>Cerrar sesión del usuario</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h2>📅 Events Endpoints</h2>
|
||||
|
||||
<h3><span class="method GET">GET</span> /api/events/</h3>
|
||||
<p>Listar todos los eventos activos</p>
|
||||
|
||||
<h3><span class="method GET">GET</span> /api/events/{id}/</h3>
|
||||
<p>Obtener detalles de un evento específico</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h2>✅ Attendance Endpoints</h2>
|
||||
|
||||
<h3><span class="method POST">POST</span> /api/attendance/ <span class="auth-badge">AUTH</span></h3>
|
||||
<p>Registrar asistencia a un evento (solo asistentes)</p>
|
||||
<pre><code>{
|
||||
"event_id": 1,
|
||||
"account_number": "12345678"
|
||||
}</code></pre>
|
||||
|
||||
<h3><span class="method GET">GET</span> /api/attendance/stats/?account_number=12345678 <span class="auth-badge">AUTH</span></h3>
|
||||
<p>Obtener estadísticas de asistencia de un estudiante</p>
|
||||
|
||||
<h3><span class="method GET">GET</span> /api/attendance/my/?account_number=12345678 <span class="auth-badge">AUTH</span></h3>
|
||||
<p>Obtener lista de asistencias del estudiante</p>
|
||||
|
||||
<h3><span class="method GET">GET</span> /api/attendance/recent/ <span class="auth-badge">AUTH</span></h3>
|
||||
<p>Obtener asistencias recientes (solo asistentes)</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h2>🔑 Authentication</h2>
|
||||
<p>La API utiliza <strong>JWT (JSON Web Tokens)</strong> para autenticación.</p>
|
||||
<p>Para endpoints que requieren autenticación, incluye el header:</p>
|
||||
<pre><code>Authorization: Bearer <your_access_token></code></pre>
|
||||
|
||||
<h3>Flujo de autenticación:</h3>
|
||||
<ol>
|
||||
<li>Envía tu número de cuenta a <code>/api/auth/login/</code></li>
|
||||
<li>Recibe <code>access_token</code> y <code>refresh_token</code></li>
|
||||
<li>Usa el <code>access_token</code> en el header de las peticiones</li>
|
||||
<li>Cuando expire (1 hora), usa el <code>refresh_token</code> en <code>/api/auth/token/refresh/</code></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h2>⚙️ System Configuration</h2>
|
||||
|
||||
<h3><span class="method GET">GET</span> /api/auth/system-config/ <span class="auth-badge">AUTH</span></h3>
|
||||
<p>Obtener configuración del sistema (% mínimo, tiempos de registro, etc.)</p>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h2>📖 Quick Links</h2>
|
||||
<ul>
|
||||
<li><a href="/api/">API Root (JSON)</a></li>
|
||||
<li><a href="/admin/">Admin Panel</a></li>
|
||||
<li><a href="/">Frontend Application</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h2>ℹ️ Response Codes</h2>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr style="background: #f8f9fa;">
|
||||
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Code</th>
|
||||
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Descripción</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;"><code>200</code></td>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;">Petición exitosa</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;"><code>201</code></td>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;">Recurso creado exitosamente</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;"><code>400</code></td>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;">Petición inválida (error en datos)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;"><code>401</code></td>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;">No autenticado</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;"><code>403</code></td>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;">Sin permisos</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;"><code>404</code></td>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;">Recurso no encontrado</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;"><code>500</code></td>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;">Error del servidor</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,50 @@
|
||||
# Script para crear un backup de la base de datos en Windows
|
||||
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
Write-Host "Creación de Backup de Base de Datos MAC" -ForegroundColor Cyan
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Verificar que los contenedores estén corriendo
|
||||
$dbStatus = docker-compose ps db 2>$null
|
||||
if ($dbStatus -notmatch "Up") {
|
||||
Write-Host "❌ Error: El contenedor de base de datos no está corriendo" -ForegroundColor Red
|
||||
Write-Host "Ejecuta: docker-compose up -d" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "📦 Creando backup de la base de datos..." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
# Crear el directorio de fixtures si no existe
|
||||
if (-Not (Test-Path "backend\fixtures")) {
|
||||
New-Item -ItemType Directory -Path "backend\fixtures" -Force | Out-Null
|
||||
}
|
||||
|
||||
# Crear el backup
|
||||
docker-compose exec -T db pg_dump -U mac_user mac_attendance > backend\fixtures\db_full_backup.sql
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
# Obtener el tamaño del archivo
|
||||
$size = (Get-Item "backend\fixtures\db_full_backup.sql").Length / 1MB
|
||||
$sizeFormatted = "{0:N2} MB" -f $size
|
||||
|
||||
Write-Host "✅ ¡Backup creado exitosamente!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "📄 Archivo: backend\fixtures\db_full_backup.sql" -ForegroundColor White
|
||||
Write-Host "📊 Tamaño: $sizeFormatted" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "Ahora puedes compartir este backup con tus colaboradores:" -ForegroundColor Cyan
|
||||
Write-Host " 1. Sube el archivo a GitHub:" -ForegroundColor White
|
||||
Write-Host " git add backend/fixtures/db_full_backup.sql" -ForegroundColor Gray
|
||||
Write-Host " git commit -m `"Actualizar backup de base de datos`"" -ForegroundColor Gray
|
||||
Write-Host " git push origin main" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host " 2. O compártelo por otro medio (Google Drive, Dropbox, etc.)" -ForegroundColor White
|
||||
Write-Host ""
|
||||
} else {
|
||||
Write-Host "❌ Error al crear el backup" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
# Script para crear un backup de la base de datos
|
||||
|
||||
echo "=========================================="
|
||||
echo "Creación de Backup de Base de Datos MAC"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Verificar que los contenedores estén corriendo
|
||||
if ! docker-compose ps | grep -q "db.*Up"; then
|
||||
echo "❌ Error: El contenedor de base de datos no está corriendo"
|
||||
echo "Ejecuta: docker-compose up -d"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 Creando backup de la base de datos..."
|
||||
echo ""
|
||||
|
||||
# Crear el directorio de fixtures si no existe
|
||||
mkdir -p backend/fixtures
|
||||
|
||||
# Crear el backup
|
||||
docker-compose exec -T db pg_dump -U mac_user mac_attendance > backend/fixtures/db_full_backup.sql
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
# Obtener el tamaño del archivo
|
||||
SIZE=$(du -h backend/fixtures/db_full_backup.sql | cut -f1)
|
||||
|
||||
echo "✅ ¡Backup creado exitosamente!"
|
||||
echo ""
|
||||
echo "📄 Archivo: backend/fixtures/db_full_backup.sql"
|
||||
echo "📊 Tamaño: $SIZE"
|
||||
echo ""
|
||||
echo "Ahora puedes compartir este backup con tus colaboradores:"
|
||||
echo " 1. Sube el archivo a GitHub:"
|
||||
echo " git add backend/fixtures/db_full_backup.sql"
|
||||
echo " git commit -m \"Actualizar backup de base de datos\""
|
||||
echo " git push origin main"
|
||||
echo ""
|
||||
echo " 2. O compártelo por otro medio (Google Drive, Dropbox, etc.)"
|
||||
echo ""
|
||||
else
|
||||
echo "❌ Error al crear el backup"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
File diff suppressed because it is too large
Load Diff
+7
-22
@@ -5,10 +5,8 @@ services:
|
||||
# PostgreSQL Database (Desarrollo)
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
- POSTGRES_DB=mac_attendance
|
||||
- POSTGRES_USER=mac_user
|
||||
- POSTGRES_PASSWORD=mac_password_2024_secure
|
||||
env_file:
|
||||
- .env.development
|
||||
volumes:
|
||||
- postgres_data_dev:/var/lib/postgresql/data
|
||||
networks:
|
||||
@@ -34,21 +32,8 @@ services:
|
||||
- ./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
|
||||
env_file:
|
||||
- .env.development
|
||||
networks:
|
||||
- app-network
|
||||
depends_on:
|
||||
@@ -59,15 +44,15 @@ services:
|
||||
stdin_open: true # Para ipdb y debugging interactivo
|
||||
tty: true
|
||||
|
||||
# Frontend React + Nginx
|
||||
# Frontend React + Nginx (HTTP sin SSL para desarrollo local)
|
||||
nginx:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.frontend
|
||||
ports:
|
||||
- "80:80"
|
||||
- "80:80" # HTTP para desarrollo (sin SSL)
|
||||
volumes:
|
||||
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
- ./docker/nginx.dev.conf:/etc/nginx/conf.d/default.conf # Configuración sin SSL
|
||||
- static_volume:/app/staticfiles
|
||||
- media_volume:/app/media
|
||||
depends_on:
|
||||
|
||||
+10
-24
@@ -2,16 +2,13 @@ services:
|
||||
# PostgreSQL Database
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
- POSTGRES_DB=mac_attendance
|
||||
- POSTGRES_USER=mac_user
|
||||
- POSTGRES_PASSWORD=mac_password_2024_secure
|
||||
env_file:
|
||||
- .env.production
|
||||
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)
|
||||
# NO exponer puertos al host - solo accesible internamente
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U mac_user -d mac_attendance"]
|
||||
interval: 10s
|
||||
@@ -26,26 +23,13 @@ services:
|
||||
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"
|
||||
gunicorn --bind 0.0.0.0:8000 --workers 3 --timeout 300 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
|
||||
env_file:
|
||||
- .env.production
|
||||
networks:
|
||||
- app-network
|
||||
depends_on:
|
||||
@@ -60,9 +44,10 @@ services:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.frontend
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443" # HTTPS (SSL/TLS) - ÚNICO puerto expuesto al host
|
||||
volumes:
|
||||
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
- ./docker/ssl:/etc/nginx/ssl:ro # Certificados SSL (solo lectura)
|
||||
- static_volume:/app/staticfiles
|
||||
- media_volume:/app/media
|
||||
depends_on:
|
||||
@@ -72,7 +57,8 @@ services:
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
# Red interna para comunicación entre contenedores
|
||||
# Solo nginx expone puerto 443 al host
|
||||
|
||||
volumes:
|
||||
static_volume:
|
||||
|
||||
@@ -28,8 +28,8 @@ 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
|
||||
# Nota: collectstatic se ejecuta en docker-compose command, no aquí
|
||||
# porque en build-time no tenemos acceso al archivo .env
|
||||
|
||||
# Exponer puerto
|
||||
EXPOSE 8000
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# Script para generar certificados SSL autofirmados para desarrollo
|
||||
|
||||
# Crear directorio para certificados
|
||||
mkdir -p ssl
|
||||
|
||||
# Generar certificado autofirmado
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
-keyout ssl/key.pem \
|
||||
-out ssl/cert.pem \
|
||||
-subj "/C=MX/ST=Estado/L=Ciudad/O=MAC/OU=IT/CN=localhost"
|
||||
|
||||
echo "✅ Certificados SSL generados en docker/ssl/"
|
||||
echo " - Certificado: docker/ssl/cert.pem"
|
||||
echo " - Clave privada: docker/ssl/key.pem"
|
||||
echo ""
|
||||
echo "⚠️ IMPORTANTE: Estos son certificados autofirmados para DESARROLLO"
|
||||
echo " Para PRODUCCIÓN, usa certificados de Let's Encrypt o una CA confiable"
|
||||
+43
-3
@@ -2,11 +2,44 @@ upstream backend {
|
||||
server backend:8000;
|
||||
}
|
||||
|
||||
# Servidor HTTPS (SSL/TLS)
|
||||
# NOTA: Solo se expone puerto 443. No hay redirección HTTP->HTTPS
|
||||
# Los usuarios DEBEN acceder directamente vía HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl http2;
|
||||
server_name localhost;
|
||||
client_max_body_size 100M;
|
||||
|
||||
# Certificados SSL
|
||||
# NOTA: Debes generar estos certificados o usar Let's Encrypt
|
||||
# Para desarrollo/pruebas locales, usa certificados autofirmados
|
||||
# Para producción, usa certificados reales de Let's Encrypt o una CA
|
||||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||
|
||||
# Configuración SSL moderna y segura
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# HSTS (HTTP Strict Transport Security)
|
||||
# Obliga al navegador a usar HTTPS por 1 año
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
# Seguridad adicional
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
|
||||
# Sesión SSL
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
# OCSP Stapling
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
# Logs
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
@@ -24,7 +57,8 @@ server {
|
||||
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_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Ssl on;
|
||||
proxy_redirect off;
|
||||
|
||||
# Timeouts
|
||||
@@ -39,8 +73,14 @@ server {
|
||||
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_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Ssl on;
|
||||
proxy_redirect off;
|
||||
|
||||
# Timeouts extendidos para importaciones/exportaciones de archivos grandes
|
||||
proxy_connect_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
# Archivos estáticos de Django
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
upstream backend {
|
||||
server backend:8000;
|
||||
}
|
||||
|
||||
# Servidor HTTP (sin SSL) para desarrollo local
|
||||
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 http;
|
||||
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 http;
|
||||
proxy_redirect off;
|
||||
|
||||
# Timeouts extendidos para importaciones/exportaciones de archivos grandes
|
||||
proxy_connect_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
# 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;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
# Script para restaurar la base de datos compartida en Windows
|
||||
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
Write-Host "Restauración de Base de Datos MAC" -ForegroundColor Cyan
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Verificar que exista el archivo de backup
|
||||
if (-Not (Test-Path "backend\fixtures\db_full_backup.sql")) {
|
||||
Write-Host "❌ Error: No se encontró el archivo de backup en backend\fixtures\db_full_backup.sql" -ForegroundColor Red
|
||||
Write-Host "Por favor, asegúrate de tener el archivo de backup." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "📋 Archivo de backup encontrado" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Preguntar confirmación
|
||||
Write-Host "⚠️ ADVERTENCIA: Este proceso eliminará todos los datos actuales de la base de datos." -ForegroundColor Yellow
|
||||
$confirmacion = Read-Host "¿Estás seguro de que deseas continuar? (si/no)"
|
||||
|
||||
if ($confirmacion -ne "si") {
|
||||
Write-Host "❌ Operación cancelada." -ForegroundColor Red
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "🔄 Deteniendo contenedores..." -ForegroundColor Yellow
|
||||
docker-compose down
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "🗑️ Eliminando volumen de base de datos anterior..." -ForegroundColor Yellow
|
||||
docker volume rm pagina-de-asistencia-mac_postgres_data 2>$null
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "🚀 Iniciando contenedor de base de datos..." -ForegroundColor Yellow
|
||||
docker-compose up -d db
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "⏳ Esperando a que la base de datos esté lista..." -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 10
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "📥 Restaurando backup..." -ForegroundColor Yellow
|
||||
Get-Content backend\fixtures\db_full_backup.sql | docker-compose exec -T db psql -U mac_user -d mac_attendance
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "🚀 Iniciando todos los servicios..." -ForegroundColor Yellow
|
||||
docker-compose up -d
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "✅ ¡Base de datos restaurada exitosamente!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Puedes iniciar sesión con:" -ForegroundColor Cyan
|
||||
Write-Host " - Superusuario: admin / admin123" -ForegroundColor White
|
||||
Write-Host " - Asistente: 11111111" -ForegroundColor White
|
||||
Write-Host " - Estudiante: Cualquier número de cuenta de 8 dígitos" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "==========================================" -ForegroundColor Cyan
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
# Script para restaurar la base de datos compartida
|
||||
|
||||
echo "=========================================="
|
||||
echo "Restauración de Base de Datos MAC"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Verificar que exista el archivo de backup
|
||||
if [ ! -f "backend/fixtures/db_full_backup.sql" ]; then
|
||||
echo "❌ Error: No se encontró el archivo de backup en backend/fixtures/db_full_backup.sql"
|
||||
echo "Por favor, asegúrate de tener el archivo de backup."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📋 Archivo de backup encontrado"
|
||||
echo ""
|
||||
|
||||
# Preguntar confirmación
|
||||
echo "⚠️ ADVERTENCIA: Este proceso eliminará todos los datos actuales de la base de datos."
|
||||
read -p "¿Estás seguro de que deseas continuar? (si/no): " confirmacion
|
||||
|
||||
if [ "$confirmacion" != "si" ]; then
|
||||
echo "❌ Operación cancelada."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔄 Deteniendo contenedores..."
|
||||
docker-compose down
|
||||
|
||||
echo ""
|
||||
echo "🗑️ Eliminando volumen de base de datos anterior..."
|
||||
docker volume rm pagina-de-asistencia-mac_postgres_data 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "🚀 Iniciando contenedor de base de datos..."
|
||||
docker-compose up -d db
|
||||
|
||||
echo ""
|
||||
echo "⏳ Esperando a que la base de datos esté lista..."
|
||||
sleep 10
|
||||
|
||||
echo ""
|
||||
echo "📥 Restaurando backup..."
|
||||
docker-compose exec -T db psql -U mac_user -d mac_attendance < backend/fixtures/db_full_backup.sql
|
||||
|
||||
echo ""
|
||||
echo "🚀 Iniciando todos los servicios..."
|
||||
docker-compose up -d
|
||||
|
||||
echo ""
|
||||
echo "✅ ¡Base de datos restaurada exitosamente!"
|
||||
echo ""
|
||||
echo "Puedes iniciar sesión con:"
|
||||
echo " - Superusuario: admin / admin123"
|
||||
echo " - Asistente: 11111111"
|
||||
echo " - Estudiante: Cualquier número de cuenta de 8 dígitos"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
Reference in New Issue
Block a user