📖 INTRODUCCIÓN A LA UNIDAD 4
🎯 ¿Qué aprenderás en esta unidad final?
Esta es la unidad culminante del curso donde integrarás todo lo aprendido y agregarás tecnologías avanzadas como autenticación JWT, OAuth 2.0, comunicación en tiempo real con WebSockets, GraphQL y las mejores prácticas de seguridad para APIs. Al finalizar, tendrás un sistema completo listo para producción.
💡 ¿Qué harás en esta práctica?
Construirás un Sistema de Biblioteca Digital Profesional que incluye:
- 🔐 Autenticación Moderna: Implementarás 3 tipos de autenticación (Token, JWT y OAuth 2.0)
- 💬 Comunicación en Tiempo Real: Crearás un chat y sistema de notificaciones con WebSockets
- 📊 API Flexible: Desarrollarás una API GraphQL que permite consultas personalizadas
- 🛡️ Seguridad Profesional: Aplicarás medidas de seguridad de nivel empresarial
- 🌐 Integración Externa: Conectarás con Google Books API para importar libros
Resultado final: Un sistema completo que podrías poner en tu portafolio profesional y poder mostrarlo.
🎯 OBJETIVOS DE APRENDIZAJE
Al completar esta práctica, serás capaz de:
📚 Conocimientos Técnicos:
- JWT: Entender qué es un token, cómo se genera, cómo se valida y por qué es más seguro que las cookies tradicionales
- OAuth 2.0: Implementar "Login con Google/Facebook" y entender el flujo de autorización
- WebSockets: Crear conexiones bidireccionales para chat en tiempo real y notificaciones push
- GraphQL: Consultar APIs de forma flexible obteniendo solo los datos que necesitas
- Seguridad: Proteger tu aplicación contra los 10 ataques más comunes (SQL Injection, XSS, CSRF, etc.)
- Rate Limiting: Evitar abuso de tu API limitando peticiones por usuario
- Integraciones: Conectar tu app con servicios externos como Google Books, Stripe, SendGrid
🛠️ Habilidades Prácticas:
- Desarrollar APIs listas para producción con autenticación robusta
- Implementar funcionalidades en tiempo real sin recargar la página
- Crear sistemas seguros que protejan datos de usuarios
- Integrar múltiples tecnologías en un solo proyecto cohesivo
- Documentar y desplegar aplicaciones profesionales
💼 Competencias Profesionales:
- Portfolio: Tendrás un proyecto completo para mostrar
- Experiencia real: Habrás trabajado con tecnologías usadas en empresas como Netflix, Uber, Facebook
- Resolución de problemas: Sabrás debuggear y solucionar errores comunes
- Mejores prácticas: Seguirás estándares de la industria en arquitectura y código limpio
⚠️ REQUISITOS PREVIOS - ¡PASO A PASO!
Empezaremos instalando todo desde cero y construiremos el proyecto paso a paso en el orden correcto.
📋 LO QUE NECESITAS TENER ANTES DE EMPEZAR:
- Windows, Mac o Linux - Cualquier sistema operativo
- Conexión a Internet - Para descargar software y librerías
- Editor de Texto/Código - Recomendado: Visual Studio Code (gratis)
- 4GB RAM mínimo - Para correr MySQL y Django
- 5GB espacio en disco - Para software y proyecto
✅ ESTRUCTURA DE LA GUÍA - ORDEN PERFECTO
Esta guía sigue este orden lógico y pedagógico:
- PASO 0: PREPARACIÓN DEL ENTORNO - Instalar Python, MySQL, crear proyecto Django desde cero
- PASO 1: PROYECTO BASE - Crear modelos, configurar base de datos, crear API REST básica
- PASO 2: JWT AUTHENTICATION - Implementar autenticación moderna con tokens
- PASO 3: OAUTH 2.0 - Agregar "Login con Google/Facebook"
- PASO 4: WEBSOCKETS - Comunicación en tiempo real (chat, notificaciones)
- PASO 5: GRAPHQL - API flexible para consultas personalizadas
- PASO 6: SEGURIDAD - Proteger la aplicación contra ataques
- PASO 7: INTEGRACIÓN EXTERNA - Conectar con Google Books API
- PASO 8: DESPLIEGUE - Publicar tu aplicación en Internet
🚨 IMPORTANTE: SEGUIR EL ORDEN EXACTO
NO te saltes pasos - Cada sección construye sobre la anterior. Si saltas pasos, encontrarás errores.
Verifica cada checkpoint - Al final de cada paso hay una sección "✅ Checkpoint" para verificar que todo funciona antes de continuar.
Guarda tu trabajo frecuentemente - Usa Git después de cada paso que funcione correctamente.
📦 PASO 0: PREPARACIÓN DEL ENTORNO (30-45 minutos)
🎯 Objetivo: Tener Python, MySQL, Django y todas las herramientas necesarias instaladas y funcionando.
0.1 Instalar Python 3.9+ (10 minutos)
🪟 En Windows:
- Ir a:
https://www.python.org/downloads/ - Descargar Python 3.11 (o la versión más reciente)
- Ejecutar el instalador
- ⚠️ MUY IMPORTANTE: Marcar la casilla "Add Python to PATH" antes de instalar
- Click en "Install Now"
- Esperar a que termine la instalación
🍎 En Mac:
🐧 En Linux (Ubuntu/Debian):
✅ VERIFICAR INSTALACIÓN:
Abre una terminal/CMD y ejecuta:
Salida esperada: Python 3.11.x
Salida esperada: pip 23.x.x from ...
❌ Si no funciona el comando "python"
En algunos sistemas, el comando es python3 en lugar de python.
Prueba: python3 --version y pip3 --version
Si funciona con python3, usa ese comando en todos los pasos siguientes.
0.2 Instalar MySQL 8.0+ (15 minutos)
🪟 En Windows:
- Ir a:
https://dev.mysql.com/downloads/installer/ - Descargar "MySQL Installer" (el archivo .msi más grande, ~400MB)
- Ejecutar el instalador
- Seleccionar "Developer Default"
- Click "Next" hasta llegar a la configuración
- Configurar password de root:
- Password: root123 (o el que prefieras, pero RECUÉRDALO)
- Confirmar password
- Terminar instalación
- MySQL se instalará como servicio y arrancará automáticamente
🍎 En Mac:
🐧 En Linux:
✅ VERIFICAR INSTALACIÓN:
Salida esperada: Deberías ver algo como:
Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 8 Server version: 8.0.xx mysql>
Si entraste exitosamente, ejecuta dentro de MySQL:
✅ Checkpoint 0.2
MySQL está funcionando correctamente si viste la lista de bases de datos y pudiste salir sin errores.
0.3 Instalar Visual Studio Code (5 minutos)
- Ir a:
https://code.visualstudio.com/ - Descargar para tu sistema operativo
- Instalar con configuración por defecto
- Abrir VS Code
- Instalar extensiones recomendadas:
- Click en el ícono de extensiones (cuadrados en la barra lateral)
- Buscar e instalar: "Python" (por Microsoft)
- Buscar e instalar: "Django" (por Baptiste Darthenay)
- Buscar e instalar: "MySQL" (por cweijan)
💡 Alternativas a VS Code
Si prefieres otro editor, puedes usar:
- PyCharm Community (más pesado pero muy completo)
- Sublime Text (ligero y rápido)
- Notepad++ (solo Windows, muy básico)
0.4 Instalar Git (5 minutos)
🪟 En Windows:
- Ir a:
https://git-scm.com/download/win - Descargar e instalar con opciones por defecto
🍎 En Mac:
🐧 En Linux:
✅ VERIFICAR:
Salida esperada: git version 2.x.x
✅ CHECKPOINT PASO 0: ENTORNO LISTO
Verifica que TODOS estos comandos funcionen:
| Comando | Resultado Esperado |
|---|---|
python --version |
Python 3.9+ o superior |
pip --version |
pip 20+ o superior |
mysql -u root -p |
Conecta a MySQL (ingresa password) |
git --version |
git 2.x o superior |
| Abrir VS Code | Se abre correctamente |
Si TODO funciona → Continúa al PASO 1
Si algo no funciona → Revisa la instalación de ese componente
🏗️ PASO 1: CREAR PROYECTO DJANGO DESDE CERO (45-60 minutos)
🎯 Objetivo: Crear un proyecto Django completo con MySQL, modelos de biblioteca y API REST funcionando.
1.1 Crear Carpeta del Proyecto (2 minutos)
🪟 En Windows (usando CMD o PowerShell):
🍎🐧 En Mac/Linux:
✅ VERIFICAR: Estás dentro de la carpeta. El comando pwd (Mac/Linux) o cd (Windows) debe mostrar la ruta de tu carpeta.
1.2 Crear Entorno Virtual (5 minutos)
📚 ¿Qué es un entorno virtual?
Es como una "caja" aislada donde instalarás todas las librerías de Python para este proyecto, sin afectar otros proyectos ni tu sistema.
Crear el entorno virtual - PowerShell:
Espera 30-60 segundos mientras se crea...
Activar el entorno virtual:
🪟 Windows (CMD):
🪟 Windows (PowerShell):
🍎🐧 Mac/Linux:
✅ VERIFICAR: Verás (venv) al inicio de tu línea de comando.
(venv) C:\Users\TU_USUARIO\Desktop\biblioteca_unidad4>
⚠️ MUY IMPORTANTE: Mantener el entorno activado
SIEMPRE que trabajes en el proyecto, el entorno virtual debe estar activado (debes ver (venv)).
Para desactivar: Escribe deactivate (pero NO lo hagas ahora, necesitamos instalaciones)
1.3 Instalar Django y Dependencias (10 minutos)
Asegúrate de que el entorno virtual esté activado (debes ver (venv))
Esto tomará 2-3 minutos...
❌ Error común: mysqlclient no se instala en Windows
Si ves un error al instalar mysqlclient, prueba esto:
Opción 1: Instalar el paquete compilado
Opción 2: Usar pymysql (alternativa más fácil)
Y luego agrega esto al archivo __init__.py de tu proyecto (lo haremos más adelante)
✅ VERIFICAR instalación:
Deberías ver:
Django 4.2.7 djangorestframework 3.14.0 mysqlclient 2.2.0 (o pymysql 1.1.0) django-cors-headers 4.3.0 django-filter 23.5 ...
1.4 Crear Proyecto Django (3 minutos)
Crear el proyecto (núcleo de configuración):
⚠️ NOTA: El punto . al final es IMPORTANTE (dice "crear aquí, no en subcarpeta")
Crear la aplicación (donde vivirá tu código):
✅ VERIFICAR estructura: Deberías tener esta estructura de carpetas:
biblioteca_unidad4/ ├── venv/ (entorno virtual - ignorar) ├── biblioteca_project/ (configuración del proyecto) │ ├── __init__.py │ ├── settings.py (⭐ archivo principal de configuración) │ ├── urls.py (rutas principales) │ ├── asgi.py │ └── wsgi.py ├── libros/ (tu aplicación) │ ├── migrations/ │ ├── __init__.py │ ├── admin.py (panel de administración) │ ├── apps.py │ ├── models.py (⭐ modelos de base de datos) │ ├── tests.py │ └── views.py (⭐ vistas/lógica) └── manage.py (⭐ comandos de Django)
Abrir el proyecto en VS Code:
Si el comando code no funciona, abre VS Code manualmente y haz "File → Open Folder" seleccionando biblioteca_unidad4
1.5 Configurar Base de Datos MySQL (10 minutos)
Paso 1: Crear la base de datos
Abre una nueva terminal/CMD y conecta a MySQL:
Dentro de MySQL, ejecuta:
Deberías ver biblioteca_uni4 en la lista de bases de datos.
Paso 2: Configurar Django para usar MySQL
En VS Code, abre el archivo biblioteca_project/settings.py y busca la sección DATABASES (línea ~75).
REEMPLAZA esto:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
POR esto:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'biblioteca_uni4',
'USER': 'root',
'PASSWORD': 'root123', # ← Cambia esto a tu password de MySQL
'HOST': 'localhost',
'PORT': '3306',
'OPTIONS': {
'charset': 'utf8mb4',
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
},
}
}
🔐 Si usaste PyMySQL en lugar de mysqlclient -- No recomendable usarlo, solo si es el caso
Abre biblioteca_project/__init__.py y agrega al inicio:
import pymysql pymysql.install_as_MySQLdb()
✅ VERIFICAR conexión a base de datos:
Salida esperada:
System check identified no issues (0 silenced).
❌ Errores comunes:
- "Access denied for user 'root'": Password incorrecto en settings.py
- "Can't connect to MySQL server": MySQL no está corriendo. Inícialo desde Servicios (Windows) o
brew services start mysql(Mac) - "Unknown database 'biblioteca_db'": No creaste la base de datos. Vuelve al paso 1
1.6 Registrar la App y Configurar Django (5 minutos)
En biblioteca_project/settings.py, busca INSTALLED_APPS (línea ~30).
Modifica para que quede así:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party apps
'rest_framework',
'corsheaders',
'django_filters',
# Tu aplicación
'libros',
]
Busca MIDDLEWARE y agrega CORS:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware', # ← AGREGAR ESTO
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
Al final del archivo settings.py, agrega configuración de CORS y DRF:
# ==============================
# CONFIGURACIÓN DE CORS
# ==============================
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
]
CORS_ALLOW_CREDENTIALS = True
# ==============================
# CONFIGURACIÓN DE REST FRAMEWORK
# ==============================
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}
# ==============================
# CONFIGURACIÓN DE IDIOMA Y ZONA HORARIA
# ==============================
LANGUAGE_CODE = 'es-mx'
TIME_ZONE = 'America/Hermosillo' # Hermosillo
USE_I18N = True
USE_TZ = True
✅ VERIFICAR configuración:
Debe decir: System check identified no issues (0 silenced).
✅ CHECKPOINT PASO 1.1-1.6: Proyecto Django Creado
Verifica que TODO esto funcione:
- ✅ Entorno virtual activado (ves
(venv)) - ✅ Django instalado:
python -m django --version→ 4.2.7 - ✅ Base de datos creada en MySQL
- ✅
python manage.py checksin errores - ✅ Archivo
settings.pyconfigurado correctamente
Si TODO está ✅ → Continúa creando los modelos
🚀 ¿Qué Construiremos?
Construiremos un Sistema de Biblioteca Digital completo con:
- Modelos de Datos: Libros, Autores, Categorías, Préstamos
- API REST: Endpoints para operaciones CRUD
- Autenticación JWT: Tokens seguros sin estado
- OAuth 2.0: Login con servicios externos
- WebSockets: Chat y notificaciones en tiempo real
- GraphQL API: Queries flexibles y eficientes
- Seguridad Robusta: HTTPS, validación, rate limiting
- Integración Externa: Google Books API
🏗️ Arquitectura Final del Sistema
┌──────────────────────────────────────────────┐
│ FRONTEND / CLIENTES │
│ (Web, Móvil, Aplicaciones de Terceros) │
└──────────┬───────────────────────────────────┘
│
┌──────▼──────┐
│ Gateway │ ← Rate Limiting, CORS
└──────┬──────┘
│
┌──────▼──────────────────────────────┐
│ AUTENTICACIÓN │
│ JWT / OAuth 2.0 / Token Auth │
└──────┬──────────────────────────────┘
│
┌──────▼──────────────────────────────┐
│ APIS DISPONIBLES │
│ • REST API (CRUD Biblioteca) │
│ • GraphQL API (Queries Flexibles) │
│ • WebSocket (Tiempo Real) │
└──────┬──────────────────────────────┘
│
┌──────▼──────────────────────────────┐
│ SERVICIOS DE NEGOCIO │
│ • Catálogo • Préstamos │
│ • Usuarios • Notificaciones │
└──────┬──────────────────────────────┘
│
┌──────▼──────────────────────────────┐
│ BASE DE DATOS MySQL │
│ Libros, Autores, Categorías │
└─────────────────────────────────────┘
📊 PASO 1 (Continuación): CREAR MODELOS DE DATOS (15 minutos)
1.7 Crear Modelos en models.py
Abre el archivo libros/models.py y reemplaza TODO el contenido por:
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
from django.contrib.auth.models import User
from decimal import Decimal
class Categoria(models.Model):
"""Categorías de libros (Ficción, No Ficción, Ciencia, etc.)"""
nombre = models.CharField(max_length=100, unique=True)
descripcion = models.TextField(blank=True)
activo = models.BooleanField(default=True)
fecha_creacion = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name_plural = "Categorías"
ordering = ['nombre']
def __str__(self):
return self.nombre
class Autor(models.Model):
"""Autores de libros"""
nombre = models.CharField(max_length=100)
apellido = models.CharField(max_length=100)
fecha_nacimiento = models.DateField(null=True, blank=True)
pais_origen = models.CharField(max_length=100, blank=True)
biografia = models.TextField(blank=True)
foto = models.URLField(blank=True, help_text="URL de la foto del autor")
fecha_creacion = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name_plural = "Autores"
ordering = ['apellido', 'nombre']
unique_together = ['nombre', 'apellido'] # No duplicar autor
def __str__(self):
return f"{self.nombre} {self.apellido}"
@property
def nombre_completo(self):
return f"{self.nombre} {self.apellido}"
class Libro(models.Model):
"""Modelo principal de libros"""
# Estados del libro
DISPONIBLE = 'disponible'
PRESTADO = 'prestado'
MANTENIMIENTO = 'mantenimiento'
PERDIDO = 'perdido'
ESTADOS = [
(DISPONIBLE, 'Disponible'),
(PRESTADO, 'Prestado'),
(MANTENIMIENTO, 'En Mantenimiento'),
(PERDIDO, 'Perdido'),
]
# Información básica
titulo = models.CharField(max_length=150)
subtitulo = models.CharField(max_length=200, blank=True)
isbn = models.CharField(max_length=13, unique=True,
help_text="ISBN de 13 dígitos")
# Relaciones
autor = models.ForeignKey(Autor, on_delete=models.PROTECT,
related_name='libros')
categoria = models.ForeignKey(Categoria, on_delete=models.SET_NULL,
null=True, related_name='libros')
# Detalles de publicación
editorial = models.CharField(max_length=200, blank=True)
fecha_publicacion = models.DateField(null=True, blank=True)
paginas = models.PositiveIntegerField(
null=True, blank=True,
validators=[MinValueValidator(1)]
)
idioma = models.CharField(max_length=50, default='Español')
# Descripción y contenido
descripcion = models.TextField(blank=True)
imagen_portada = models.URLField(blank=True)
# Inventario
stock = models.PositiveIntegerField(
default=1,
validators=[MinValueValidator(0)],
help_text="Cantidad de ejemplares disponibles"
)
estado = models.CharField(max_length=20, choices=ESTADOS,
default=DISPONIBLE)
# Precio y valoración
precio = models.DecimalField(
max_digits=8,
decimal_places=2,
validators=[MinValueValidator(Decimal('0.01'))],
help_text="Precio en Lempiras (L)"
)
valoracion = models.DecimalField(
max_digits=3,
decimal_places=2,
default=Decimal('0.00'),
validators=[
MinValueValidator(Decimal('0.00')),
MaxValueValidator(Decimal('5.00'))
],
help_text="Valoración de 0 a 5 estrellas"
)
# Metadata
activo = models.BooleanField(default=True)
fecha_creacion = models.DateTimeField(auto_now_add=True)
fecha_actualizacion = models.DateTimeField(auto_now=True)
creado_por = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
related_name='libros_creados'
)
class Meta:
verbose_name_plural = "Libros"
ordering = ['-fecha_creacion']
indexes = [
models.Index(fields=['isbn']),
models.Index(fields=['titulo']),
models.Index(fields=['autor']),
]
def __str__(self):
return f"{self.titulo} - {self.autor.nombre_completo}"
@property
def esta_disponible(self):
"""Verifica si el libro está disponible para préstamo"""
return self.estado == self.DISPONIBLE and self.stock > 0
def actualizar_stock(self, cantidad):
"""Actualiza el stock del libro"""
self.stock += cantidad
if self.stock < 0:
self.stock = 0
if self.stock == 0:
self.estado = self.PRESTADO
elif self.stock > 0 and self.estado == self.PRESTADO:
self.estado = self.DISPONIBLE
self.save()
class Prestamo(models.Model):
"""Registro de préstamos de libros"""
# Estados del préstamo
ACTIVO = 'activo'
DEVUELTO = 'devuelto'
ATRASADO = 'atrasado'
PERDIDO = 'perdido'
ESTADOS = [
(ACTIVO, 'Activo'),
(DEVUELTO, 'Devuelto'),
(ATRASADO, 'Atrasado'),
(PERDIDO, 'Perdido'),
]
libro = models.ForeignKey(Libro, on_delete=models.PROTECT,
related_name='prestamos')
usuario = models.ForeignKey(User, on_delete=models.PROTECT,
related_name='prestamos')
fecha_prestamo = models.DateTimeField(auto_now_add=True)
fecha_devolucion_esperada = models.DateField()
fecha_devolucion_real = models.DateTimeField(null=True, blank=True)
estado = models.CharField(max_length=20, choices=ESTADOS,
default=ACTIVO)
notas = models.TextField(blank=True)
class Meta:
verbose_name_plural = "Préstamos"
ordering = ['-fecha_prestamo']
def __str__(self):
return f"{self.libro.titulo} - {self.usuario.username}"
@property
def dias_prestamo(self):
"""Calcula días que lleva el préstamo"""
from django.utils import timezone
if self.fecha_devolucion_real:
return (self.fecha_devolucion_real - self.fecha_prestamo).days
return (timezone.now() - self.fecha_prestamo).days
@property
def esta_atrasado(self):
"""Verifica si el préstamo está atrasado"""
from django.utils import timezone
if self.fecha_devolucion_real:
return False
return timezone.now().date() > self.fecha_devolucion_esperada
✅ VERIFICAR que no haya errores de sintaxis:
Debe decir: System check identified no issues (0 silenced).
1.8 Crear y Aplicar Migraciones (5 minutos)
🤔 ¿Qué son migraciones?
Son instrucciones que Django genera para crear/modificar tablas en la base de datos basándose en tus modelos.
Crear las migraciones:
Salida esperada:
Migrations for 'libros':
libros\migrations\0001_initial.py
- Create model Autor
- Create model Categoria
- Create model Libro
- Create model Prestamo
...
Aplicar las migraciones (crear tablas en MySQL):
Salida esperada:
Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK ... Applying libros.0001_initial... OK
✅ VERIFICAR en MySQL:
Deberías ver estas tablas:
+----------------------------+ | Tables_in_biblioteca_db | +----------------------------+ | auth_user | | libros_autor | | libros_categoria | | libros_libro | | libros_prestamo | | ...
1.9 Crear Superusuario (3 minutos)
Crear usuario administrador para acceder al panel de Django:
Te pedirá:
Username: admin Email: admin@biblioteca.com Password: admin123 (escribe, no se verá mientras escribes) Password (again): admin123
✅ Salida esperada:
Superuser created successfully.
1.10 Registrar Modelos en Admin (3 minutos)
Abre libros/admin.py y reemplaza por:
from django.contrib import admin
from .models import Categoria, Autor, Libro, Prestamo
@admin.register(Categoria)
class CategoriaAdmin(admin.ModelAdmin):
list_display = ['nombre', 'activo', 'fecha_creacion']
list_filter = ['activo']
search_fields = ['nombre']
@admin.register(Autor)
class AutorAdmin(admin.ModelAdmin):
list_display = ['nombre_completo', 'pais_origen', 'fecha_nacimiento']
search_fields = ['nombre', 'apellido']
list_filter = ['pais_origen']
@admin.register(Libro)
class LibroAdmin(admin.ModelAdmin):
list_display = ['titulo', 'autor', 'isbn', 'estado', 'stock', 'precio']
list_filter = ['estado', 'categoria', 'autor']
search_fields = ['titulo', 'isbn', 'descripcion']
list_editable = ['stock', 'estado']
readonly_fields = ['fecha_creacion', 'fecha_actualizacion']
@admin.register(Prestamo)
class PrestamoAdmin(admin.ModelAdmin):
list_display = ['libro', 'usuario', 'fecha_prestamo',
'fecha_devolucion_esperada', 'estado']
list_filter = ['estado', 'fecha_prestamo']
search_fields = ['libro__titulo', 'usuario__username']
readonly_fields = ['fecha_prestamo']
Guardar el archivo (Ctrl+S)
1.11 Probar el Admin de Django (5 minutos)
Arrancar el servidor de desarrollo:
Salida esperada:
Watching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). January 23, 2026 - 14:30:00 Django version 4.2.7, using settings 'biblioteca_project.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CTRL-BREAK.
✅ Abrir en el navegador:
- Admin:
http://127.0.0.1:8000/admin/ - Ingresar con: admin / admin123
Deberías ver el panel de administración con tus modelos: Autores, Categorías, Libros, Préstamos
🎯 CREAR DATOS DE PRUEBA:
- Click en "Categorías" → "Agregar Categoría" → Crear: Ficción, Ciencia, Historia
- Click en "Autores" → "Agregar Autor" → Crear 2-3 autores
- Click en "Libros" → "Agregar Libro" → Crear 3-5 libros
TIP: Para ISBN usa números como: 9780123456789, 9781234567890, etc.
✅ CHECKPOINT PASO 1 COMPLETO: Proyecto Base Funcionando
Verifica que TODO esto funcione:
- ✅ Modelos creados en
models.py - ✅ Migraciones aplicadas:
python manage.py migratesin errores - ✅ Tablas creadas en MySQL:
SHOW TABLES;muestra libros_autor, libros_libro, etc. - ✅ Superusuario creado: puedes hacer login en /admin/
- ✅ Admin funcionando: ves los modelos y puedes crear datos
- ✅ Datos de prueba creados: al menos 2 autores, 3 categorías, 5 libros
🎉 ¡FELICIDADES! Tienes el proyecto base funcionando
Ahora continuaremos con la Parte 1: JWT Authentication
✅ Resultado Final de la Unidad
Tendrás un sistema de biblioteca completamente funcional con autenticación moderna, comunicación en tiempo real, múltiples tipos de APIs y seguridad de nivel empresarial, listo para ser desplegado en producción.
🔐 PARTE 1: JWT AUTHENTICATION (90 minutos)
JWT Duración: 90 minutos Dificultad: Media🎯 OBJETIVO DE ESTA PARTE
¿Qué lograrás?
Al finalizar esta sección, tendrás un sistema de autenticación JWT completo donde:
- ✅ Los usuarios pueden hacer login y recibir un token JWT
- ✅ El token se usa para autenticar todas las peticiones a la API
- ✅ Los tokens expiran automáticamente después de 1 hora
- ✅ Los usuarios pueden renovar su token sin volver a hacer login
- ✅ El sistema funciona sin guardar sesiones en el servidor (stateless)
🌟 ¿Por qué es importante?
JWT es el estándar de autenticación moderno usado por empresas como Google, Facebook, Netflix y Twitter. Es perfecto para aplicaciones móviles, SPAs (Single Page Applications) y microservicios.
📱 Casos de uso reales:
- Apps móviles (iOS/Android) que necesitan mantener sesión sin cookies
- Aplicaciones React/Vue/Angular que consumen APIs
- Microservicios que necesitan compartir autenticación
- APIs públicas con rate limiting por usuario
🤔 ¿Qué es JWT? - Explicación Simple
📚 Conceptos Básicos para Principiantes
Imagina que JWT es como una credencial digital que el servidor te da cuando haces login. Esta credencial:
- Es autosuficiente: Contiene toda tu información (ID, username, permisos)
- Está firmada digitalmente: Nadie puede falsificarla sin la clave secreta del servidor
- Expira automáticamente: Después de cierto tiempo deja de funcionar (por seguridad)
- No requiere base de datos: El servidor puede validarla solo leyéndola
🆚 Comparación con autenticación tradicional:
| Aspecto | Cookies/Sesiones Tradicionales | JWT (Moderno) |
|---|---|---|
| Almacenamiento | Servidor guarda sesión en memoria/DB | Cliente guarda token, servidor NO guarda nada |
| Escalabilidad | Difícil con múltiples servidores | Fácil: cualquier servidor puede validar |
| Móvil/SPA | Complicado con cookies | Perfecto para apps modernas |
| Información | Solo ID de sesión | Datos completos del usuario |
| Expiración | Configurada en servidor | Incluida en el token mismo |
📦 Estructura de un JWT - Desmenuzado
Un JWT parece un texto largo y confuso, pero tiene una estructura muy clara:
Se divide en 3 partes separadas por puntos (.):
🔍 Parte 1: HEADER (Encabezado)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Si lo decodificas (es Base64), obtienes:
{
"alg": "HS256", // Algoritmo de firma: HMAC SHA-256
"typ": "JWT" // Tipo de token: JWT
}
🤓 ¿Qué significa? Le dice al servidor cómo verificar que el token es legítimo.
🔍 Parte 2: PAYLOAD (Carga Útil)
eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjQyMzQ1Njc4fQ
Decodificado:
{
"user_id": 1, // ID del usuario
"username": "admin", // Nombre de usuario
"email": "admin@biblioteca.com", // Email
"exp": 1642345678, // Fecha de expiración (timestamp)
"iat": 1642342078 // Fecha de emisión
}
🤓 ¿Qué significa? Aquí va toda la información del usuario. ¡OJO! Estos datos NO están encriptados, solo codificados en Base64, así que nunca pongas contraseñas aquí.
⚠️ IMPORTANTE: Seguridad del Payload
NO incluyas información sensible en el payload porque cualquiera puede decodificar Base64:
- ❌ NO pongas: contraseñas, números de tarjeta, datos bancarios
- ✅ SÍ puedes poner: user_id, username, rol, permisos
🔍 Parte 3: SIGNATURE (Firma)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Se genera así:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
SECRET_KEY_DEL_SERVIDOR
)
🤓 ¿Qué significa? Es una "huella digital" del token. Si alguien modifica el header o payload, la firma no coincidirá y el servidor rechazará el token.
🔐 Flujo de verificación:
- Servidor recibe el token
- Toma header + payload y calcula su firma usando la clave secreta
- Compara su firma calculada con la firma del token
- Si coinciden ✅ = Token válido | Si no coinciden ❌ = Token falso/modificado
2.1 Instalar SimpleJWT (3 minutos)
📚 ¿Qué es djangorestframework-simplejwt?
Es la librería más popular y confiable para implementar JWT en Django REST Framework. Está mantenida activamente, tiene excelente documentación y es usada por miles de proyectos en producción.
Alternativas: PyJWT, django-rest-framework-jwt (deprecated), pero SimpleJWT es la recomendada.
🖥️ En tu terminal (ASEGÚRATE DE QUE EL ENTORNO VIRTUAL ESTÉ ACTIVADO):
❌ Error común: No module named 'pkg_resources'
Si ves este error al instalar djangorestframework-simplejwt==5.3.0, la solución es instalar sin especificar la versión:
Esto instalará la versión más reciente compatible con tu sistema.
✅ Verificar instalación:
Salida esperada:
Name: djangorestframework-simplejwt
Version: 5.5.1
Summary: A minimal JSON Web Token authentication plugin for Django REST Framework
Home-page: https://github.com/jazzband/djangorestframework-simplejwt
Author: David Sanders
...
✅ Checkpoint 2.1: Verificación
Antes de continuar, asegúrate de:
- ✅ El paquete se instaló correctamente (sin errores)
- ✅ Tu entorno virtual está activado (ves
(venv)) - ✅ Estás en la carpeta raíz de tu proyecto Django
💾 Actualizar requirements.txt:
Esto guarda todas tus dependencias para que otros puedan instalarlas fácilmente con pip install -r requirements.txt
2.2 Configurar JWT en Settings (10 minutos)
📁 ¿Qué vamos a hacer?
Editaremos el archivo settings.py para:
- Agregar JWT como método de autenticación
- Configurar cuánto tiempo duran los tokens
- Definir el algoritmo de encriptación
- Establecer políticas de seguridad
📂 Archivo a editar: biblioteca_project/settings.py
🔍 Busca la sección REST_FRAMEWORK (debe estar cerca del final del archivo, después de STATIC_URL). Si no existe, agrégala completa:
# =======================
# REST FRAMEWORK CONFIG
# =======================
from datetime import timedelta # ← Agregar al inicio del archivo
REST_FRAMEWORK = {
# AUTENTICACIÓN: Qué métodos acepta tu API
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication', # JWT (Token moderno)
'oauth2_provider.contrib.rest_framework.OAuth2Authentication', # ← AGREGAR para OAuth 2.0
'rest_framework.authentication.TokenAuthentication', # Token tradicional
'rest_framework.authentication.SessionAuthentication', # Sesión (para admin)
],
# PERMISOS: Qué pueden hacer los usuarios
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
# PAGINACIÓN: Cuántos resultados por página
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
# FILTROS: Permitir búsquedas y ordenamiento
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}
ℹ️ Nota sobre OAuth2Authentication
¿Por qué agregamos oauth2_provider.contrib.rest_framework.OAuth2Authentication?
Esta línea es necesaria para que Django REST Framework pueda:
- ✅ Validar tokens OAuth 2.0 emitidos por django-oauth-toolkit
- ✅ Autenticar peticiones usando
Bearer tokensde OAuth - ✅ Evitar el error
401 Unauthorizedal probar endpoints con tokens OAuth
🎯 Sin esta línea: Cuando ejecutes test_oauth.py (que obtiene un token OAuth), recibirás un error 401 porque DRF no reconoce el token.
✅ Con esta línea: Los tokens OAuth funcionan perfectamente y puedes acceder a la API.
Nota: Esta configuración se implementa ahora (en la Parte 1 de JWT) pero será útil cuando llegues a la Parte 2 de OAuth 2.0.
➕ Ahora agrega la configuración específica de JWT (después de REST_FRAMEWORK):
# =======================
# SIMPLE JWT CONFIG
# =======================
SIMPLE_JWT = {
# ⏱️ DURACIÓN DE TOKENS
'ACCESS_TOKEN_LIFETIME': timedelta(hours=1), # Token de acceso válido 1 hora
'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # Token de refresco válido 7 días
# 🔄 ROTACIÓN DE TOKENS (Seguridad extra)
'ROTATE_REFRESH_TOKENS': True, # Genera nuevo refresh al refrescar
'BLACKLIST_AFTER_ROTATION': True, # Invalida el refresh anterior
'UPDATE_LAST_LOGIN': True, # Actualiza last_login del usuario
# 🔐 ALGORITMO Y CLAVE DE FIRMA
'ALGORITHM': 'HS256', # HMAC SHA-256 (más común)
'SIGNING_KEY': SECRET_KEY, # Usa la SECRET_KEY de Django
'VERIFYING_KEY': None, # Solo para algoritmos asimétricos (RSA)
# 📋 CONFIGURACIÓN DE HEADERS
'AUTH_HEADER_TYPES': ('Bearer',), # Tipo: "Authorization: Bearer TOKEN"
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', # Nombre del header
# 👤 CLAIMS DEL USUARIO
'USER_ID_FIELD': 'id', # Campo del modelo User para ID
'USER_ID_CLAIM': 'user_id', # Nombre del claim en el payload
# 🎫 CONFIGURACIÓN DEL TOKEN
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type', # Claim que identifica tipo de token
'JTI_CLAIM': 'jti', # JWT ID (identificador único)
}
🔍 Explicación Detallada de Cada Configuración:
| Parámetro | ¿Qué hace? | ¿Por qué es importante? |
|---|---|---|
ACCESS_TOKEN_LIFETIME |
Tiempo que dura el token de acceso | Tokens cortos = más seguro. Si alguien roba el token, solo funciona 1 hora. 1 hora es el estándar de la industria. |
REFRESH_TOKEN_LIFETIME |
Tiempo del token de refresco | Permite renovar el access token sin login. 7 días es común para apps web, 30 días para móviles. |
ROTATE_REFRESH_TOKENS |
Crear nuevo refresh al usarlo | Aumenta seguridad: cada vez que renuevas, obtienes un refresh nuevo y el viejo se invalida. |
BLACKLIST_AFTER_ROTATION |
Invalidar refresh anterior | Previene que alguien reutilice un refresh token viejo robado. |
ALGORITHM |
Algoritmo de firma | HS256 es el estándar. Usa HMAC (simétrico). Para mayor seguridad usa RS256 (asimétrico). |
AUTH_HEADER_TYPES |
Prefijo del header | "Bearer" es el estándar OAuth 2.0. El cliente enviará: Authorization: Bearer eyJ0eX... |
⚠️ IMPORTANTE: Agregar import de timedelta
Ve AL INICIO del archivo settings.py (línea 1-15) y agrega:
from datetime import timedelta # ← AGREGAR ESTA LÍNEA
Debe quedar cerca de los otros imports, por ejemplo después de from pathlib import Path
⚠️ IMPORTANTE (2) POR HACER : Agregar 2 Dependencias antes de revisar todo lo que va
pip install django-cors-headers djangorestframework djangorestframework-simplejwt mysqlclient pip install django-filter
Deben quedar instaladas las 2 Dependencias Ambas dependencias
✅ Checkpoint 2.2: Verificación
Verifica tu configuración:
- Guarda el archivo settings.py (Ctrl+S)
- Revisa que no haya errores de sintaxis (indentación correcta)
- Verifica el import: La línea
from datetime import timedeltadebe estar al inicio del archivo - Prueba que Django arranca sin errores:
Salida esperada:
System check identified no issues (0 silenced).
Si hay errores: Revisa la indentación (debe ser con espacios, no tabs) y que todos los parámetros tengan comas al final.
💡 Personalización Opcional
Puedes ajustar los tiempos según tu caso de uso:
- API Pública muy segura:
ACCESS_TOKEN_LIFETIME: timedelta(minutes=15) - App móvil:
REFRESH_TOKEN_LIFETIME: timedelta(days=30) - Intranet corporativa:
ACCESS_TOKEN_LIFETIME: timedelta(hours=8)
2.3 Crear API Views (Serializers y ViewSets) (20 minutos)
📚 ¿Qué son Serializers y ViewSets?
Serializers: Convierten los modelos de Django a JSON (y viceversa) para que la API pueda enviar/recibir datos.
ViewSets: Manejan las operaciones CRUD (Create, Read, Update, Delete) automáticamente.
Con estos dos componentes, Django REST Framework genera automáticamente toda la API por nosotros.
Paso 1: Crear Serializers
Crea el archivo libros/serializers.py (archivo nuevo):
from rest_framework import serializers
from .models import Categoria, Autor, Libro, Prestamo
from django.contrib.auth.models import User
class CategoriaSerializer(serializers.ModelSerializer):
"""Serializer para Categoría"""
class Meta:
model = Categoria
fields = ['id', 'nombre', 'descripcion', 'activo', 'fecha_creacion']
read_only_fields = ['id', 'fecha_creacion']
class AutorSerializer(serializers.ModelSerializer):
"""Serializer para Autor"""
nombre_completo = serializers.ReadOnlyField()
total_libros = serializers.SerializerMethodField()
class Meta:
model = Autor
fields = ['id', 'nombre', 'apellido', 'nombre_completo',
'fecha_nacimiento', 'pais_origen', 'biografia',
'foto', 'total_libros', 'fecha_creacion']
read_only_fields = ['id', 'fecha_creacion']
def get_total_libros(self, obj):
return obj.libros.filter(activo=True).count()
class LibroSerializer(serializers.ModelSerializer):
"""Serializer para Libro"""
autor_nombre = serializers.CharField(source='autor.nombre_completo', read_only=True)
categoria_nombre = serializers.CharField(source='categoria.nombre', read_only=True)
esta_disponible = serializers.ReadOnlyField()
class Meta:
model = Libro
fields = [
'id', 'titulo', 'subtitulo', 'isbn',
'autor', 'autor_nombre',
'categoria', 'categoria_nombre',
'editorial', 'fecha_publicacion', 'paginas', 'idioma',
'descripcion', 'imagen_portada',
'stock', 'estado', 'esta_disponible',
'precio', 'valoracion',
'activo', 'fecha_creacion', 'fecha_actualizacion'
]
read_only_fields = ['id', 'fecha_creacion', 'fecha_actualizacion']
def validate_isbn(self, value):
"""Validar que ISBN tenga 13 dígitos"""
isbn = value.replace('-', '').replace(' ', '')
if not isbn.isdigit():
raise serializers.ValidationError("ISBN debe contener solo números")
if len(isbn) != 13:
raise serializers.ValidationError("ISBN debe tener 13 dígitos")
return value
def validate_precio(self, value):
"""Validar que precio sea positivo"""
if value <= 0:
raise serializers.ValidationError("El precio debe ser mayor a 0")
return value
class PrestamoSerializer(serializers.ModelSerializer):
"""Serializer para Préstamo"""
libro_titulo = serializers.CharField(source='libro.titulo', read_only=True)
usuario_nombre = serializers.CharField(source='usuario.username', read_only=True)
dias_prestamo = serializers.ReadOnlyField()
esta_atrasado = serializers.ReadOnlyField()
class Meta:
model = Prestamo
fields = [
'id', 'libro', 'libro_titulo',
'usuario', 'usuario_nombre',
'fecha_prestamo', 'fecha_devolucion_esperada', 'fecha_devolucion_real',
'estado', 'dias_prestamo', 'esta_atrasado', 'notas'
]
read_only_fields = ['id', 'fecha_prestamo']
def validate(self, data):
"""Validar que el libro esté disponible antes de prestar"""
if self.instance is None: # Solo en creación
libro = data.get('libro')
if not libro.esta_disponible:
raise serializers.ValidationError({
'libro': 'Este libro no está disponible para préstamo'
})
return data
class UserSerializer(serializers.ModelSerializer):
"""Serializer para Usuario"""
total_prestamos = serializers.SerializerMethodField()
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name',
'is_staff', 'date_joined', 'total_prestamos']
read_only_fields = ['id', 'date_joined']
def get_total_prestamos(self, obj):
return obj.prestamos.count()
Paso 2: Crear ViewSets
Crea el archivo libros/api_views.py (archivo nuevo):
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend
from .models import Categoria, Autor, Libro, Prestamo
from .serializers import (
CategoriaSerializer, AutorSerializer,
LibroSerializer, PrestamoSerializer
)
class CategoriaViewSet(viewsets.ModelViewSet):
"""
ViewSet para Categorías
- GET /api/categorias/ - Listar todas
- POST /api/categorias/ - Crear nueva
- GET /api/categorias/{id}/ - Ver detalle
- PUT /api/categorias/{id}/ - Actualizar
- DELETE /api/categorias/{id}/ - Eliminar
"""
queryset = Categoria.objects.all()
serializer_class = CategoriaSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
search_fields = ['nombre', 'descripcion']
ordering_fields = ['nombre', 'fecha_creacion']
ordering = ['nombre']
class AutorViewSet(viewsets.ModelViewSet):
"""ViewSet para Autores"""
queryset = Autor.objects.all()
serializer_class = AutorSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['pais_origen']
search_fields = ['nombre', 'apellido', 'biografia']
ordering_fields = ['apellido', 'nombre', 'fecha_creacion']
ordering = ['apellido', 'nombre']
@action(detail=True, methods=['get'])
def libros(self, request, pk=None):
"""Endpoint personalizado: /api/autores/{id}/libros/"""
autor = self.get_object()
libros = autor.libros.filter(activo=True)
serializer = LibroSerializer(libros, many=True)
return Response(serializer.data)
class LibroViewSet(viewsets.ModelViewSet):
"""ViewSet para Libros"""
queryset = Libro.objects.filter(activo=True).select_related('autor', 'categoria')
serializer_class = LibroSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['estado', 'categoria', 'autor']
search_fields = ['titulo', 'isbn', 'descripcion']
ordering_fields = ['titulo', 'precio', 'fecha_publicacion', 'valoracion']
ordering = ['-fecha_creacion']
@action(detail=False, methods=['get'])
def disponibles(self, request):
"""Endpoint: /api/libros/disponibles/"""
libros = self.queryset.filter(
estado=Libro.DISPONIBLE,
stock__gt=0
)
serializer = self.get_serializer(libros, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def actualizar_stock(self, request, pk=None):
"""
Endpoint: POST /api/libros/{id}/actualizar_stock/
Body: {"cantidad": 5} (puede ser negativo para restar)
"""
libro = self.get_object()
cantidad = request.data.get('cantidad', 0)
try:
cantidad = int(cantidad)
except (ValueError, TypeError):
return Response(
{'error': 'La cantidad debe ser un número entero'},
status=status.HTTP_400_BAD_REQUEST
)
libro.actualizar_stock(cantidad)
serializer = self.get_serializer(libro)
return Response(serializer.data)
class PrestamoViewSet(viewsets.ModelViewSet):
"""ViewSet para Préstamos"""
queryset = Prestamo.objects.all().select_related('libro', 'usuario')
serializer_class = PrestamoSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ['estado', 'usuario']
ordering_fields = ['fecha_prestamo', 'fecha_devolucion_esperada']
ordering = ['-fecha_prestamo']
def perform_create(self, serializer):
"""Al crear préstamo, asignar usuario actual y actualizar stock"""
prestamo = serializer.save(usuario=self.request.user)
prestamo.libro.actualizar_stock(-1) # Reducir stock en 1
@action(detail=True, methods=['post'])
def devolver(self, request, pk=None):
"""
Endpoint: POST /api/prestamos/{id}/devolver/
Marca el préstamo como devuelto
"""
from django.utils import timezone
prestamo = self.get_object()
if prestamo.estado == Prestamo.DEVUELTO:
return Response(
{'error': 'Este préstamo ya fue devuelto'},
status=status.HTTP_400_BAD_REQUEST
)
prestamo.fecha_devolucion_real = timezone.now()
prestamo.estado = Prestamo.DEVUELTO
prestamo.save()
# Incrementar stock del libro
prestamo.libro.actualizar_stock(1)
serializer = self.get_serializer(prestamo)
return Response(serializer.data)
✅ VERIFICAR archivos creados:
Debe decir: System check identified no issues (0 silenced).
✅ Checkpoint 2.3: Serializers y ViewSets creados
Verifica:
- ✅ Archivo
libros/serializers.pycreado con 5 serializers - ✅ Archivo
libros/api_views.pycreado con 4 viewsets - ✅ Sin errores al ejecutar
python manage.py check
2.4 Configurar URLs de la API (10 minutos)
🎯 ¿Qué vamos a crear?
Vamos a crear las rutas (URLs) de nuestra API. Esto incluye:
- 🔑 /api/auth/jwt/login/ - Para hacer login y obtener tokens JWT
- 🔄 /api/token/refresh/ - Para renovar el access token
- ✅ /api/token/verify/ - Para verificar si un token es válido
- 📚 /api/libros/ - CRUD de libros
- ✍️ /api/autores/ - CRUD de autores
- 📂 /api/categorias/ - CRUD de categorías
- 📖 /api/prestamos/ - CRUD de préstamos
Paso 1: Crear archivo de URLs de la API
Crea el archivo libros/api_urls.py (nuevo):
# ===================================
# URLS DE LA API - libros/api_urls.py
# ===================================
from django.urls import path, include
from rest_framework.routers import DefaultRouter
# Importar vistas JWT de SimpleJWT
from rest_framework_simplejwt.views import (
TokenObtainPairView, # Vista para login (obtener tokens)
TokenRefreshView, # Vista para refrescar access token
TokenVerifyView, # Vista para verificar token
)
# Importar ViewSets
from . import api_views
# ===== ROUTER PARA VIEWSETS =====
# El router genera automáticamente las URLs para CRUD
router = DefaultRouter()
router.register(r'libros', api_views.LibroViewSet, basename='libro')
router.register(r'autores', api_views.AutorViewSet, basename='autor')
router.register(r'categorias', api_views.CategoriaViewSet, basename='categoria')
router.register(r'prestamos', api_views.PrestamoViewSet, basename='prestamo')
# ===== URL PATTERNS =====
urlpatterns = [
# ─────────────────────────────────
# 🔐 AUTENTICACIÓN JWT
# ─────────────────────────────────
# Login con JWT (POST: username + password → access y refresh tokens)
path('auth/jwt/login/', TokenObtainPairView.as_view(), name='jwt_login'),
# Refrescar token (POST: refresh_token → nuevo access_token)
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
# Verificar token (POST: token → válido o inválido)
path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
# ─────────────────────────────────
# 📚 ENDPOINTS DE LA API (CRUD)
# ─────────────────────────────────
# Incluir todas las rutas del router
# Esto genera automáticamente:
# GET /api/libros/ - Listar todos los libros
# POST /api/libros/ - Crear nuevo libro
# GET /api/libros/{id}/ - Ver detalle de libro
# PUT /api/libros/{id}/ - Actualizar libro
# DELETE /api/libros/{id}/ - Eliminar libro
# Y lo mismo para autores, categorias, prestamos
path('', include(router.urls)),
]
Paso 2: Conectar con el proyecto principal
Edita biblioteca_project/urls.py para incluir las URLs de la API:
# biblioteca_project/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
# Admin de Django
path('admin/', admin.site.urls),
# ✨ URLs de la API (AGREGAR ESTA LÍNEA)
path('api/', include('libros.api_urls')),
]
✅ Instalar Dependencia:
Salida esperada: volvera a su entorno virtual
✅ VERIFICAR rutas creadas:
Salida esperada: System check identified no issues (0 silenced).
📋 Resumen de URLs Disponibles
Con esta configuración, tu API tendrá estas rutas:
| URL | Método | Descripción |
|---|---|---|
| 🔐 AUTENTICACIÓN | ||
/api/auth/jwt/login/ |
POST | Login → Obtener tokens (access + refresh) |
/api/token/refresh/ |
POST | Renovar access token usando refresh token |
/api/token/verify/ |
POST | Verificar si un token es válido |
| 📚 LIBROS | ||
/api/libros/ |
GET | Listar todos los libros |
/api/libros/ |
POST | Crear nuevo libro |
/api/libros/{id}/ |
GET | Ver detalle de un libro |
/api/libros/{id}/ |
PUT/PATCH | Actualizar libro |
/api/libros/{id}/ |
DELETE | Eliminar libro |
/api/libros/disponibles/ |
GET | Listar solo libros disponibles |
/api/libros/{id}/actualizar_stock/ |
POST | Actualizar stock de un libro |
| ✍️ AUTORES | ||
/api/autores/ |
GET/POST | Listar/Crear autores |
/api/autores/{id}/ |
GET/PUT/DELETE | Ver/Actualizar/Eliminar autor |
/api/autores/{id}/libros/ |
GET | Ver libros de un autor |
| 📂 CATEGORÍAS | ||
/api/categorias/ |
GET/POST | Listar/Crear categorías |
/api/categorias/{id}/ |
GET/PUT/DELETE | Ver/Actualizar/Eliminar categoría |
| 📖 PRÉSTAMOS | ||
/api/prestamos/ |
GET/POST | Listar/Crear préstamos |
/api/prestamos/{id}/ |
GET/PUT/DELETE | Ver/Actualizar/Eliminar préstamo |
/api/prestamos/{id}/devolver/ |
POST | Marcar préstamo como devuelto |
✅ Checkpoint 2.4: URLs configuradas
Verifica:
- ✅ Archivo
libros/api_urls.pycreado - ✅ Archivo
biblioteca_project/urls.pyactualizado - ✅
python manage.py checksin errores
2.5 Probar JWT con Postman/Thunder Client (20 minutos)
🧪 ¿Cómo probar la API?
Vamos a usar Postman o Thunder Client (extensión de VS Code) para probar que JWT funciona correctamente.
Si no tienes Postman:
- Opción 1: Descargar Postman de
https://www.postman.com/downloads/ - Opción 2: Instalar Thunder Client en VS Code (Extensions → Buscar "Thunder Client")
- Opción 3: Usar comandos curl en la terminal (mostrados abajo)
Paso 1: Arrancar el servidor
Deja el servidor corriendo. Abre otra terminal si necesitas ejecutar más comandos.
🔑 Prueba 1: Login y Obtener Tokens JWT
Con Postman/Thunder Client:
- Crear nueva request
- Método: POST
- URL:
http://127.0.0.1:8000/api/auth/jwt/login/ - Headers:
Content-Type: application/json - Body → raw (JSON):
{
"username": "admin",
"password": "admin123"
}
Con curl (desde terminal):
✅ Respuesta esperada (Status 200 OK):
{
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eX...",
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBl..."
}
💾 GUARDA ESTOS TOKENS
Copia el access token (lo usarás en las siguientes pruebas).
Explicación:
- access token: Usar para acceder a la API (dura 1 hora)
- refresh token: Usar para obtener nuevo access token cuando expire (dura 7 días)
📚 Prueba 2: Acceder a la API con el Token
Con Postman/Thunder Client:
- Crear nueva request
- Método: GET
- URL:
http://127.0.0.1:8000/api/libros/ - Headers: Agregar header de autorización:
- Key:
Authorization - Value:
Bearer TU_ACCESS_TOKEN_AQUI
- Key:
⚠️ IMPORTANTE: El valor debe ser Bearer (con B mayúscula) seguido de un espacio y luego el token.
Con curl:
✅ Respuesta esperada (Status 200 OK):
{
"count": 5,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"titulo": "Cien años de soledad",
"autor_nombre": "Gabriel García Márquez",
"isbn": "9780123456789",
"precio": "450.00",
"stock": 10,
"esta_disponible": true,
...
},
...
]
}
🔄 Prueba 3: Refrescar el Access Token
¿Cuándo usar esto? Cuando el access token expire (después de 1 hora), usa el refresh token para obtener uno nuevo sin hacer login nuevamente.
Con Postman/Thunder Client:
- Método: POST
- URL:
http://127.0.0.1:8000/api/token/refresh/ - Body → raw (JSON):
{
"refresh": "TU_REFRESH_TOKEN_AQUI"
}
Con curl:
✅ Respuesta esperada (Status 200 OK):
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Nota: Recibes tanto un nuevo access token como un nuevo refresh token (por la configuración ROTATE_REFRESH_TOKENS).
✅ Prueba 4: Verificar si un Token es Válido
Con Postman/Thunder Client:
- Método: POST
- URL:
http://127.0.0.1:8000/api/token/verify/ - Body → raw (JSON):
{
"token": "TU_ACCESS_TOKEN_AQUI"
}
Con curl:
✅ Si el token es válido: Respuesta 200 OK con {} (objeto vacío)
❌ Si el token es inválido o expirado: Respuesta 401 Unauthorized
📝 Prueba 5: Crear un Libro (Requiere Autenticación)
Con Postman/Thunder Client:
- Método: POST
- URL:
http://127.0.0.1:8000/api/libros/ - Headers:
Authorization: Bearer TU_ACCESS_TOKENContent-Type: application/json
- Body → raw (JSON):
{
"titulo": "El Principito",
"isbn": "9781234567897",
"autor": 1,
"categoria": 1,
"editorial": "Editorial Planeta",
"fecha_publicacion": "1943-04-06",
"paginas": 96,
"idioma": "Español",
"descripcion": "Una historia filosófica sobre la amistad, el amor y la pérdida.",
"stock": 15,
"precio": "250.00",
"valoracion": "4.80"
}
✅ Respuesta esperada (Status 201 Created):
{
"id": 6,
"titulo": "El Principito",
"autor_nombre": "Gabriel García Márquez",
"isbn": "9781234567897",
"precio": "250.00",
"stock": 15,
"esta_disponible": true,
"fecha_creacion": "2026-01-23T19:45:00.123456Z",
...
}
✅ Checkpoint 2.5: JWT funcionando correctamente
Si todas estas pruebas pasaron, ¡FELICIDADES! Tu autenticación JWT está completamente funcional.
Verifica que:
- ✅ Login devuelve access y refresh tokens
- ✅ Puedes acceder a /api/libros/ con el access token
- ✅ Refresh token genera nuevo access token
- ✅ Verify confirma que token es válido
- ✅ Puedes crear/modificar recursos con autenticación
🚨 Errores Comunes y Soluciones
| Error | Solución |
|---|---|
401 Unauthorized |
|
{"detail":"No active account found..."} |
Username o password incorrectos. Verifica las credenciales del superusuario. |
Token is invalid or expired |
El token expiró. Usa el refresh token para obtener uno nuevo. |
CSRF Failed: CSRF token missing |
Estás usando session auth por error. Usa JWT con header Authorization |
{"detail":"Authentication credentials were not provided"} |
No enviaste el header Authorization o está mal formateado. |
🎉 ¡FELICIDADES! PARTE 1 COMPLETADA: JWT AUTHENTICATION
Has implementado exitosamente:
- ✅ Django REST Framework con JWT
- ✅ Login y obtención de tokens (access + refresh)
- ✅ Renovación automática de tokens
- ✅ Protección de endpoints con autenticación
- ✅ API REST completa para Biblioteca (CRUD)
💾 GUARDAR PROGRESO: Este es un buen momento para hacer commit en Git:
git add .
git commit -m "Implementado JWT Authentication completo"
🚀 Continúa con la PARTE 2: OAuth 2.0
🔑 PARTE 2: OAUTH 2.0 - LOGIN CON GOOGLE/FACEBOOK (120 minutos)
OAuth 2.0 Duración: 120 minutos Dificultad: Alta🎯 OBJETIVO DE ESTA PARTE
¿Qué lograrás?
Al finalizar esta sección, tendrás implementado "Login con Google" en tu aplicación:
- ✅ Los usuarios pueden hacer login usando su cuenta de Google
- ✅ No necesitas guardar contraseñas de usuarios externos
- ✅ Obtienes información básica del usuario (nombre, email, foto)
- ✅ Generas tokens JWT después del login OAuth
- ✅ Sistema compatible con múltiples proveedores (Google, Facebook, GitHub)
🌟 ¿Por qué es importante OAuth 2.0?
OAuth 2.0 es EL estándar de la industria para autorización. Permite que los usuarios accedan a tu aplicación usando sus cuentas de servicios confiables (Google, Facebook, etc.) sin compartir contraseñas contigo.
📱 Casos de uso reales:
- Botón "Continuar con Google" en aplicaciones web
- Login social en apps móviles (iOS/Android)
- Integración con APIs de terceros (Google Drive, Gmail, Calendar)
- Single Sign-On (SSO) empresarial
🤔 ¿Qué es OAuth 2.0? - Explicación Simple
📚 OAuth 2.0 para Principiantes
Analogía del Mundo Real:
Imagina que vas a un hotel y quieres que la persona de limpieza entre a tu habitación:
- Forma Insegura (Sin OAuth): Le das tu llave maestra del hotel → Ella tiene acceso total permanente
- Forma Segura (Con OAuth): Pides al hotel una tarjeta de acceso temporal que solo funciona hoy y solo para tu habitación
En OAuth 2.0:
- Tú (Usuario): Dueño de la cuenta de Google
- Hotel: Google (proveedor OAuth)
- Persona de limpieza: Tu aplicación (cliente OAuth)
- Tarjeta temporal: Access Token
- Permiso específico: Scopes (email, perfil, etc.)
🔄 Flujo de OAuth 2.0 - Paso a Paso
📊 Flujo Authorization Code (el más común)
┌─────────────┐ ┌──────────────┐
│ Usuario │ │ Google │
│ (Browser) │ │ (OAuth) │
└──────┬──────┘ └──────┬───────┘
│ │
│ 1. Click "Login con Google" │
│ ────────────────────────────────────────────► │
│ │
│ 2. Redirige a Google para autorización │
│ ◄──────────────────────────────────────────── │
│ URL: https://accounts.google.com/o/oauth2/ │
│ │
│ 3. Usuario acepta permisos en Google │
│ ────────────────────────────────────────────► │
│ │
│ 4. Google redirige con código temporal │
│ ◄──────────────────────────────────────────── │
│ URL: tu-app.com/callback?code=ABC123 │
│ │
┌──────▼──────┐ │
│ Tu Backend │ │
│ (Django) │ │
└──────┬──────┘ │
│ │
│ 5. Intercambiar código por token │
│ ────────────────────────────────────────────► │
│ POST con code + client_secret │
│ │
│ 6. Google devuelve access_token │
│ ◄──────────────────────────────────────────── │
│ { "access_token": "ya29...", ... } │
│ │
│ 7. Obtener info del usuario │
│ ────────────────────────────────────────────► │
│ GET userinfo con access_token │
│ │
│ 8. Datos del usuario (email, nombre, foto) │
│ ◄──────────────────────────────────────────── │
│ │
│ 9. Crear/Login usuario en Django │
│ Generar JWT propio │
│ │
│ 10. Responder con JWT de tu app │
│ ────────────────────────────────────────────► │
│ ┌──────▼──────┐
│ │ Usuario │
│ │ (Logueado) │
│ └─────────────┘
🔑 Conceptos Clave de OAuth 2.0
| Término | Definición | Ejemplo en nuestro caso |
|---|---|---|
| Resource Owner | Dueño de los datos | El usuario que tiene cuenta de Google |
| Client | Aplicación que solicita acceso | Tu aplicación de Biblioteca |
| Authorization Server | Servidor que autoriza y emite tokens | Google OAuth Server |
| Resource Server | Servidor con los datos protegidos | Google API (userinfo, Gmail, Drive) |
| Authorization Code | Código temporal de un solo uso | El parámetro ?code=ABC123 en la URL |
| Access Token | Token para acceder a recursos | Token que usas para obtener email del usuario |
| Refresh Token | Token para obtener nuevos access tokens | Permite acceso de larga duración |
| Scope | Permisos solicitados | email profile (leer email y perfil) |
| Redirect URI | URL de retorno después de autorización | http://localhost:8000/api/auth/google/callback/ |
3.1 Configurar Proyecto en Google Cloud (20 minutos)
📋 ¿Qué vamos a hacer?
Para usar "Login con Google", necesitas registrar tu aplicación en Google Cloud Console y obtener credenciales (Client ID y Client Secret).
Es GRATIS y no necesitas tarjeta de crédito para desarrollo.
Paso 1: Crear Proyecto en Google Cloud
- Ir a https://console.cloud.google.com/
- Hacer login con tu cuenta de Google (cualquiera)
- Click en el selector de proyectos (arriba a la izquierda)
- Click en "NEW PROJECT"
- Configurar:
- Project name:
Biblioteca UTH - Organization: Dejar en "No organization"
- Project name:
- Click "CREATE"
- Esperar 30 segundos a que se cree el proyecto
- Asegurarte de que el proyecto esté seleccionado (verlo arriba)
Paso 2: Habilitar Google+ API
- En el menú lateral (☰), ir a "APIs & Services" → "Library"
- Buscar:
Google+ API - Click en "Google+ API"
- Click "ENABLE"
- Esperar a que se habilite
Paso 3: Configurar OAuth Consent Screen
- En el menú lateral, ir a "APIs & Services" → "OAuth consent screen"
- Seleccionar "External" (para que cualquiera pueda usarlo)
- Click "CREATE"
- Llenar formulario:
- App name:
Sistema Biblioteca UTH - User support email: Tu email
- App logo: (Opcional, skip por ahora)
- App domain: (Dejar vacío por ahora)
- Authorized domains: (Dejar vacío)
- Developer contact: Tu email
- App name:
- Click "SAVE AND CONTINUE"
- Scopes: Click "ADD OR REMOVE SCOPES"
- Buscar y seleccionar:
userinfo.email - Buscar y seleccionar:
userinfo.profile - Click "UPDATE"
- Buscar y seleccionar:
- Click "SAVE AND CONTINUE"
- Test users: Agregar tu email de prueba
- Click "+ ADD USERS"
- Ingresar tu email
- Click "ADD"
- Click "SAVE AND CONTINUE"
- Revisar y click "BACK TO DASHBOARD"
Paso 4: Crear OAuth 2.0 Credentials
- En el menú lateral, ir a "APIs & Services" → "Credentials"
- Click "+ CREATE CREDENTIALS"
- Seleccionar "OAuth client ID"
- Configurar:
- Application type:
Web application - Name:
Biblioteca Web Client - Authorized JavaScript origins:
- Click "+ ADD URI"
- Agregar:
http://localhost:8000 - Click "+ ADD URI"
- Agregar:
http://127.0.0.1:8000
- Authorized redirect URIs:
- Click "+ ADD URI"
- Agregar:
http://localhost:8000/api/auth/google/callback/ - Click "+ ADD URI"
- Agregar:
http://127.0.0.1:8000/api/auth/google/callback/
- Application type:
- Click "CREATE"
Paso 5: Guardar Credenciales
💾 IMPORTANTE: Guarda estas credenciales de forma segura
Aparecerá un modal con:
- Your Client ID:
123456789-abcdefgh.apps.googleusercontent.com - Your Client Secret:
GOCSPX-aBcDeFgHiJkLmNoPqRsTuVwXyZ
📋 Cópialos y guárdalos en un archivo temporal. Los necesitarás en el siguiente paso.
💡 TIP: Puedes verlos nuevamente en "Credentials" → Click en el nombre de tu OAuth client
✅ Checkpoint 3.1: Google Cloud Configurado
Verifica que tengas:
- ✅ Proyecto creado en Google Cloud
- ✅ Google+ API habilitada
- ✅ OAuth Consent Screen configurado
- ✅ OAuth 2.0 Client ID creado
- ✅ Client ID y Client Secret guardados
- ✅ Redirect URIs configuradas correctamente
4 Personalizar JWT Response
Crea libros/jwt_views.py para agregar datos personalizados al token:
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
"""Serializer personalizado con datos adicionales"""
@classmethod
def get_token(cls, user):
token = super().get_token(user)
# Agregar claims personalizados al token
token['username'] = user.username
token['email'] = user.email
token['is_staff'] = user.is_staff
token['full_name'] = f"{user.first_name} {user.last_name}"
return token
def validate(self, attrs):
data = super().validate(attrs)
# Agregar datos extra al response
data['user'] = {
'id': self.user.id,
'username': self.user.username,
'email': self.user.email,
'first_name': self.user.first_name,
'last_name': self.user.last_name,
'is_staff': self.user.is_staff,
'date_joined': str(self.user.date_joined)
}
return data
class CustomTokenObtainPairView(TokenObtainPairView):
"""Vista personalizada para obtener JWT"""
serializer_class = CustomTokenObtainPairSerializer
Actualiza las URLs:
from .jwt_views import CustomTokenObtainPairView
urlpatterns = [
# JWT personalizado
path('auth/jwt/login/', CustomTokenObtainPairView.as_view(), name='jwt_login'),
# ... resto
]
5 Probar JWT
❌ Error en la sintaxis del curl -X en Windows CMD
La sintaxis estándar de curl con comillas simples NO funciona en Windows CMD. Aquí está la sintaxis correcta:
1. Obtener Token (Login):
Respuesta esperada:
{
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"user": {
"id": 1,
"username": "admin",
"email": "admin@biblioteca.com",
"is_staff": true
}
}
2. Usar Token para Acceder a API:
Respuesta esperada:
{
"count": 5,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"titulo": "Cien años de soledad",
"autor": "Gabriel García Márquez",
...
}
]
}
3. Refrescar Token:
✅ ¿Cómo Funciona?
- Usuario envía credenciales (username + password)
- Servidor valida y genera tokens (access + refresh)
- Cliente guarda tokens (localStorage, sessionStorage, cookies seguras)
- Cliente incluye access token en cada request
- Cuando access token expira, usa refresh token para obtener uno nuevo
- Si refresh token expira, usuario debe hacer login nuevamente
🔑 PARTE 2: OAUTH 2.0
OAuth 2.0🤔 ¿Qué es OAuth 2.0?
OAuth 2.0 es un framework de autorización que permite a aplicaciones de terceros obtener acceso limitado a un servicio HTTP. Es el estándar para "Login con Google", "Login con Facebook", etc.
📌 Conceptos Clave de OAuth 2.0
- Resource Owner: El usuario (dueño de los datos)
- Client: La aplicación que quiere acceso
- Authorization Server: Servidor que emite tokens
- Resource Server: Servidor con los datos protegidos
- Access Token: Token para acceder a recursos
- Refresh Token: Token para obtener nuevos access tokens
3.2 Instalar y Configurar Django Allauth (15 minutos)
📚 ¿Qué es Django Allauth?
django-allauth es la librería más completa y popular para autenticación social en Django. Soporta más de 50 proveedores OAuth (Google, Facebook, GitHub, Twitter, etc.) y es muy fácil de configurar.
Alternativas: python-social-auth, pero Allauth es más completa y mantenida.
Paso 1: Instalar Django Allauth
⚠️ IMPORTANTE: Instalar django-oauth-toolkit
Problema común: Al ejecutar las migraciones aparece el error ModuleNotFoundError: No module named 'oauth2_provider'
Solución: Instalar la librería django-oauth-toolkit que proporciona el módulo oauth2_provider
Esta librería es necesaria para:
- ✅ Proporcionar el proveedor OAuth 2.0 completo
- ✅ Generar access tokens y refresh tokens OAuth estándar
- ✅ Gestionar aplicaciones cliente OAuth
✅ Verificar instalación:
Paso 2: Configurar en settings.py
Edita biblioteca_project/settings.py:
2.1 - Agregar a INSTALLED_APPS:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites', # ← AGREGAR (requerido por allauth)
# Third-party apps
'rest_framework',
'corsheaders',
'django_filters',
'rest_framework_simplejwt',
'oauth2_provider', # ← AGREGAR (Django OAuth Toolkit)
'allauth', # ← AGREGAR
'allauth.account', # ← AGREGAR
'allauth.socialaccount', # ← AGREGAR
'allauth.socialaccount.providers.google', # ← AGREGAR
# Tu aplicación
'libros',
]
📌 ¿Por qué oauth2_provider?
oauth2_provider es el módulo que proporciona django-oauth-toolkit. Es necesario incluirlo en INSTALLED_APPS para:
- ✅ Crear las tablas de OAuth 2.0 en la base de datos
- ✅ Gestionar aplicaciones cliente, tokens y scopes
- ✅ Proporcionar endpoints OAuth estándar (/o/token/, /o/authorize/)
2.1.1 - AGREGAR MIDDLEWARE DE ALLAUTH:
❌ Error común al usar python manage.py check
Si ves el error allauth.account.middleware.AccountMiddleware must be added to settings.MIDDLEWARE, necesitas agregar el middleware de allauth.
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'allauth.account.middleware.AccountMiddleware', # ← AGREGAR
]
2.2 - Agregar SITE_ID:
# =======================
# SITE CONFIGURATION
# =======================
SITE_ID = 1 # ← AGREGAR
2.3 - Configurar Authentication Backends:
# =======================
# AUTHENTICATION BACKENDS
# =======================
AUTHENTICATION_BACKENDS = [
# Backend por defecto de Django (username/password)
'django.contrib.auth.backends.ModelBackend',
# Backend de allauth para OAuth social
'allauth.account.auth_backends.AuthenticationBackend',
]
2.4 - Configurar Django Allauth:
# =======================
# DJANGO ALLAUTH CONFIG
# =======================
# Configuración de cuentas
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False # Solo email para login social
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_VERIFICATION = 'optional' # Para desarrollo: 'mandatory' en producción
# Configuración de login social
SOCIALACCOUNT_AUTO_SIGNUP = True # Crear usuario automáticamente
SOCIALACCOUNT_EMAIL_VERIFICATION = 'none' # No verificar email en OAuth
# Proveedores OAuth configurados
SOCIALACCOUNT_PROVIDERS = {
'google': {
'SCOPE': [
'profile',
'email',
],
'AUTH_PARAMS': {
'access_type': 'online',
},
'APP': {
'client_id': 'TU_GOOGLE_CLIENT_ID_AQUI', # ← REEMPLAZAR
'secret': 'TU_GOOGLE_CLIENT_SECRET_AQUI', # ← REEMPLAZAR
'key': ''
}
}
}
🔐 REEMPLAZAR Credenciales de Google
En la sección SOCIALACCOUNT_PROVIDERS:
- Reemplaza
'TU_GOOGLE_CLIENT_ID_AQUI'con tu Client ID de Google - Reemplaza
'TU_GOOGLE_CLIENT_SECRET_AQUI'con tu Client Secret de Google
Ejemplo:
'client_id': '123456789-abcdefgh.apps.googleusercontent.com', 'secret': 'GOCSPX-aBcDeFgHiJkLmNoPqRsTuVwXyZ',
2.5 - Agregar Configuración de OAuth 2.0 Provider (Django OAuth Toolkit):
# =======================
# OAUTH 2.0 PROVIDER SETTINGS
# =======================
# Configuración para django-oauth-toolkit
OAUTH2_PROVIDER = {
# Tiempo de vida de los tokens
'ACCESS_TOKEN_EXPIRE_SECONDS': 3600, # 1 hora
'REFRESH_TOKEN_EXPIRE_SECONDS': 86400 * 7, # 7 días
# Scopes disponibles
'SCOPES': {
'read': 'Acceso de lectura',
'write': 'Acceso de escritura',
},
# Tipo de token por defecto
'ACCESS_TOKEN_MODEL': 'oauth2_provider.models.AccessToken',
'REFRESH_TOKEN_MODEL': 'oauth2_provider.models.RefreshToken',
}
📌 Diferencia entre django-allauth y django-oauth-toolkit
django-allauth:
- ✅ Permite que TU APLICACIÓN use OAuth de otros servicios (Google, Facebook)
- ✅ Tu app es el "cliente" que consume APIs de terceros
- ✅ Ejemplo: "Login con Google" en tu app
django-oauth-toolkit:
- ✅ Permite que TU APLICACIÓN SEA un proveedor OAuth
- ✅ Tu app es el "servidor" que proporciona tokens a otras aplicaciones
- ✅ Ejemplo: Otras apps pueden usar "Login con Biblioteca UTH"
💡 En resumen: Allauth te permite USAR OAuth de otros, OAuth Toolkit te permite SER un proveedor OAuth.
✅ VERIFICAR configuración:
Debe decir: System check identified no issues (0 silenced).
✅ Checkpoint 3.2: Django Allauth y OAuth Toolkit Configurados
Verifica:
- ✅ django-allauth instalado
- ✅ django-oauth-toolkit instalado
- ✅ INSTALLED_APPS actualizado con allauth, oauth2_provider y providers
- ✅ MIDDLEWARE con AccountMiddleware agregado
- ✅ SITE_ID = 1 agregado
- ✅ AUTHENTICATION_BACKENDS configurado
- ✅ SOCIALACCOUNT_PROVIDERS con tus credenciales de Google
- ✅
python manage.py checksin errores
3.3 Ejecutar Migraciones y Configurar Admin (10 minutos)
Paso 1: Ejecutar migraciones
Salida esperada:
Running migrations: Applying sites.0001_initial... OK Applying sites.0002_alter_domain_unique... OK Applying account.0001_initial... OK Applying account.0002_email_max_length... OK Applying socialaccount.0001_initial... OK Applying socialaccount.0002_token_max_lengths... OK Applying socialaccount.0003_extra_data_default_dict... OK ...
Paso 2: Configurar Site en Admin
- Arrancar servidor:
python manage.py runserver - Ir a:
http://127.0.0.1:8000/admin/ - Login con tu superusuario (admin / admin123)
- Click en "Sites"
- Click en "example.com" (el único que hay)
- Editar:
- Domain name:
localhost:8000 - Display name:
Biblioteca UTH
- Domain name:
- Click "SAVE"
Paso 3: Verificar Social Apps
- En el admin, ir a "SOCIAL ACCOUNTS" → "Social applications"
- Deberías ver que está vacío (lo configuraremos por código)
✅ Checkpoint 3.3: Base de datos lista
Verifica:
- ✅ Migraciones ejecutadas sin errores
- ✅ Tablas de oauth2_provider creadas (oauth2_provider_accesstoken, oauth2_provider_application, etc.)
- ✅ Site configurado en admin
- ✅ Admin de allauth accesible
3.3.1 Configurar URLs de OAuth 2.0 (5 minutos)
🔗 ¿Por qué configurar URLs?
Django OAuth Toolkit proporciona endpoints estándar OAuth 2.0 que necesitamos incluir en nuestro proyecto:
/o/authorize/- Para autorizar aplicaciones/o/token/- Para obtener tokens de acceso/o/revoke_token/- Para revocar tokens
Editar biblioteca_project/urls.py:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
# API REST
path('api/', include('libros.api_urls')),
# OAuth URLs de allauth (para login con Google/Facebook)
path('accounts/', include('allauth.urls')),
# ← AGREGAR: OAuth 2.0 URLs de django-oauth-toolkit
path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
]
⚠️ No confundir los dos tipos de OAuth
/accounts/ (allauth):
- URLs para que usuarios hagan login CON Google/Facebook
- Ejemplo:
/accounts/google/login/
/o/ (oauth2_provider):
- URLs para que TU APP proporcione OAuth a otras aplicaciones
- Ejemplo:
/o/token/para que apps externas obtengan tokens
✅ VERIFICAR URLs:
Deberías ver algo como:
/accounts/google/login/ /accounts/google/callback/ /o/authorize/ /o/token/ /o/revoke_token/ ...
3.3.2 Nota Importante sobre el Error 401 en test_oauth.py
🐛 Error Común: Código 401 al ejecutar test_oauth.py
Síntoma: Al ejecutar el script test_oauth.py para probar OAuth 2.0, obtienes Status Code: 401 y el mensaje de error indica que el token no es válido.
Causa del problema:
La configuración de REST_FRAMEWORK no incluía el backend de autenticación de OAuth 2.0 (OAuth2Authentication), por lo que Django REST Framework no reconocía los tokens OAuth emitidos por django-oauth-toolkit.
✅ Solución:
Ya agregamos la línea necesaria en la configuración de REST_FRAMEWORK en settings.py:
'oauth2_provider.contrib.rest_framework.OAuth2Authentication', # ← Esta línea
📝 ¿Qué hace esta línea?
- ✅ Permite que DRF valide tokens OAuth 2.0 emitidos por django-oauth-toolkit
- ✅ Autentica usuarios usando el
Bearer tokenen el headerAuthorization - ✅ Se suma a los otros métodos de autenticación (JWT, Token, Session)
🎯 Resultado: Ahora cuando ejecutes python test_oauth.py, deberías obtener Status Code: 200 y ver los datos de la API correctamente.
3.4 Crear Vista de OAuth y Generar JWT (25 minutos)
🎯 ¿Qué vamos a hacer?
Crearemos un endpoint personalizado que:
- Recibe el callback de Google con el código de autorización
- Intercambia el código por un access token de Google
- Obtiene la información del usuario de Google
- Crea o actualiza el usuario en Django
- Genera nuestros propios tokens JWT
- Devuelve los tokens al frontend
Crear archivo libros/oauth_views.py (nuevo):
from django.conf import settings
from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from urllib.parse import urlencode # ← IMPORTAR URLENCODE
import requests
import logging
logger = logging.getLogger(__name__)
User = get_user_model()
@api_view(['POST', 'GET']) # ← AGREGAR SOPORTE PARA GET
@permission_classes([AllowAny])
def google_oauth_callback(request):
"""
Endpoint que recibe el código de autorización de Google
y devuelve tokens JWT de nuestra aplicación
GET /api/auth/google/callback/?code=4/0AbUR2VN...
o
POST /api/auth/google/callback/
Body: {
"code": "4/0AbUR2VN..." // Código de autorización de Google
}
"""
# 1. Obtener el código de autorización (de POST o GET)
code = request.data.get('code') or request.query_params.get('code') # ← CAMBIO AQUÍ
if not code:
error_msg = 'El código de autorización es requerido'
logger.error(error_msg)
if request.method == 'GET':
return redirect(f'/oauth/login/?error={urllib.parse.quote(error_msg)}')
return Response({'error': error_msg}, status=status.HTTP_400_BAD_REQUEST)
try:
# 2. Intercambiar código por access token de Google
token_url = 'https://oauth2.googleapis.com/token'
google_config = settings.SOCIALACCOUNT_PROVIDERS['google']['APP']
token_data = {
'code': code,
'client_id': google_config['client_id'],
'client_secret': google_config['secret'],
'redirect_uri': 'http://localhost:8000/api/auth/google/callback/', # Debe coincidir con Google Cloud
'grant_type': 'authorization_code'
}
token_response = requests.post(token_url, data=token_data, timeout=10)
token_response.raise_for_status()
tokens = token_response.json()
google_access_token = tokens.get('access_token')
if not google_access_token:
error_msg = 'No se pudo obtener access token de Google'
logger.error(error_msg)
return redirect(f'/oauth/login/?error={urllib.parse.quote(error_msg)}')
logger.info(f"Access token de Google obtenido: {google_access_token[:20]}...")
# 3. Obtener información del usuario de Google
userinfo_url = 'https://www.googleapis.com/oauth2/v2/userinfo'
headers = {'Authorization': f'Bearer {google_access_token}'}
userinfo_response = requests.get(userinfo_url, headers=headers, timeout=10)
userinfo_response.raise_for_status()
user_data = userinfo_response.json()
logger.info(f"Datos de usuario de Google: {user_data}")
# 4. Crear o actualizar usuario en Django
email = user_data.get('email')
if not email:
error_msg = 'No se pudo obtener el email del usuario'
logger.error(error_msg)
return redirect(f'/oauth/login/?error={urllib.parse.quote(error_msg)}')
# Buscar si el usuario ya existe
user, created = User.objects.get_or_create(
email=email,
defaults={
'username': email.split('@')[0], # Usar parte antes del @ como username
'first_name': user_data.get('given_name', ''),
'last_name': user_data.get('family_name', ''),
}
)
# Si el usuario ya existía, actualizar su información
if not created:
user.first_name = user_data.get('given_name', user.first_name)
user.last_name = user_data.get('family_name', user.last_name)
user.save()
logger.info(f"Usuario existente actualizado: {user.email}")
else:
logger.info(f"Nuevo usuario creado: {user.email}")
# 5. Generar tokens JWT de nuestra aplicación
refresh = RefreshToken.for_user(user)
access_token = str(refresh.access_token)
# 6. Preparar datos para enviar al frontend
user_info = {
'id': user.id,
'email': user.email,
'username': user.username,
'first_name': user.first_name,
'last_name': user.last_name,
'is_staff': user.is_staff,
}
google_info = {
'picture': user_data.get('picture'),
'verified_email': user_data.get('verified_email'),
}
# Codificar datos para URL
user_info_json = json.dumps(user_info)
google_info_json = json.dumps(google_info)
# Construir URL de redirección a oauth_login.html con todos los datos
redirect_url = (
f'/oauth/login/?'
f'access_token={access_token}&'
f'refresh_token={str(refresh)}&'
f'user_info={urllib.parse.quote(user_info_json)}&'
f'google_info={urllib.parse.quote(google_info_json)}&'
f'message={urllib.parse.quote("Login exitoso con Google" if not created else "Cuenta creada exitosamente con Google")}'
)
logger.info(f"Redirigiendo a: {redirect_url[:100]}...")
return redirect(redirect_url)
except requests.Timeout:
logger.error("Timeout al comunicarse con Google")
return redirect(f'/oauth/login/?error={urllib.parse.quote("Timeout al comunicarse con Google")}')
except requests.RequestException as e:
logger.error(f"Error al comunicarse con Google: {str(e)}")
return redirect(f'/oauth/login/?error={urllib.parse.quote(f"Error con Google: {str(e)}")}')
except Exception as e:
logger.error(f"Error inesperado en OAuth: {str(e)}")
return redirect(f'/oauth/login/?error={urllib.parse.quote(f"Error inesperado: {str(e)}")}')
@api_view(['GET'])
@permission_classes([AllowAny])
def google_oauth_redirect(request):
"""
Endpoint que redirige al usuario a Google para autorización
GET /api/auth/google/redirect/
Devuelve la URL a la que el frontend debe redirigir al usuario
"""
google_config = settings.SOCIALACCOUNT_PROVIDERS['google']['APP']
scopes = settings.SOCIALACCOUNT_PROVIDERS['google']['SCOPE']
# Construir parámetros como diccionario para usar urlencode
params = {
'client_id': google_config["client_id"],
'redirect_uri': 'http://127.0.0.1:8000/api/auth/google/callback/', # Usar 127.0.0.1
'scope': " ".join(scopes),
'response_type': 'code',
'access_type': 'offline',
'prompt': 'consent',
}
# Construir URL con urlencode para codificar correctamente los parámetros
auth_url = f'https://accounts.google.com/o/oauth2/v2/auth?{urlencode(params)}' # ← USAR URLENCODE
return Response({
'auth_url': auth_url
}, status=status.HTTP_200_OK)
✅ VERIFICAR archivo creado:
Debe decir: System check identified no issues (0 silenced).
🔧 CAMBIOS IMPORTANTES IMPLEMENTADOS EN EL CÓDIGO
1️⃣ Función google_oauth_callback - Agregado soporte para GET:
@api_view(['POST', 'GET']) # ← Ahora acepta GET también
def google_oauth_callback(request):
code = request.data.get('code') or request.query_params.get('code') # ← Obtiene code de POST o GET
📝 Razón: Google redirige con el código en la URL (método GET con parámetros en query string), no en el body (POST). Este cambio permite recibir correctamente el código de autorización.
2️⃣ Función google_oauth_redirect - Uso de urlencode:
from urllib.parse import urlencode # ← Importar al inicio
params = {
'client_id': google_config["client_id"],
'redirect_uri': 'http://127.0.0.1:8000/api/auth/google/callback/',
'scope': " ".join(scopes),
'response_type': 'code',
'access_type': 'offline',
'prompt': 'consent',
}
auth_url = f'https://accounts.google.com/o/oauth2/v2/auth?{urlencode(params)}' # ← urlencode para codificar
📝 Razón: La función urlencode() codifica correctamente los parámetros de URL, evitando errores por espacios o caracteres especiales en los scopes. También hace el código más legible usando un diccionario.
⚠️ IMPORTANTE: Usamos 127.0.0.1 en lugar de localhost porque Google los trata como URIs diferentes. Mantén consistencia en tu código y en Google Cloud Console.
📖 Explicación del Código
google_oauth_redirect:
- Genera la URL de autorización de Google
- El frontend redirige al usuario a esta URL
- Google muestra pantalla de permisos
google_oauth_callback:
- Recibe el código de autorización de Google
- Intercambia código por access token
- Usa access token para obtener datos del usuario
- Crea o actualiza usuario en Django
- Genera JWT propios de la aplicación
- Devuelve JWT al cliente
✅ Checkpoint 3.4: Vistas OAuth creadas
Verifica:
- ✅ Archivo
libros/oauth_views.pycreado - ✅ Función
google_oauth_callbackimplementada - ✅ Función
google_oauth_redirectimplementada - ✅
python manage.py checksin errores
3.5 Agregar URLs para OAuth (5 minutos)
Editar libros/api_urls.py para agregar las rutas OAuth:
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView,
)
from . import api_views
from . import oauth_views # ← AGREGAR
router = DefaultRouter()
router.register(r'libros', api_views.LibroViewSet, basename='libro')
router.register(r'autores', api_views.AutorViewSet, basename='autor')
router.register(r'categorias', api_views.CategoriaViewSet, basename='categoria')
router.register(r'prestamos', api_views.PrestamoViewSet, basename='prestamo')
urlpatterns = [
# ─────────────────────────────────
# 🔐 AUTENTICACIÓN JWT
# ─────────────────────────────────
path('auth/jwt/login/', TokenObtainPairView.as_view(), name='jwt_login'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
# ─────────────────────────────────
# 🔑 AUTENTICACIÓN OAUTH 2.0 (GOOGLE)
# ─────────────────────────────────
path('auth/google/redirect/', oauth_views.google_oauth_redirect, name='google_redirect'),
path('auth/google/callback/', oauth_views.google_oauth_callback, name='google_callback'),
# ─────────────────────────────────
# 📚 ENDPOINTS CRUD
# ─────────────────────────────────
path('', include(router.urls)),
]
✅ Checkpoint 3.5: URLs OAuth agregadas
Verifica:
- ✅ Import de
oauth_viewsagregado - ✅ Rutas
auth/google/redirect/yauth/google/callback/agregadas - ✅
python manage.py checksin errores
3.6 Crear Templates HTML para Pruebas (15 minutos)
🎯 Arquitectura del Proyecto
Este proyecto es una API Backend (sin frontend complejo)
Sin embargo, para probar OAuth necesitamos páginas HTML simples porque:
- OAuth requiere redireccionar al usuario a Google
- Google redirige de vuelta a una URL de callback
- Necesitamos capturar el código de autorización
Usaremos templates HTML SOLO para pruebas, no es un frontend completo.
En producción, tu frontend sería React/Vue/Angular que consume esta API.
Paso 1: Crear carpeta de templates
Paso 2: Configurar templates en settings.py
Edita biblioteca_project/settings.py, busca TEMPLATES y actualiza:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # ← AGREGAR esto
'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',
],
},
},
]
Paso 3: Crear página de inicio (Home)
Crea el archivo templates/home.html:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Biblioteca UTH - Sistema de Autenticación</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
padding: 50px;
border-radius: 15px;
box-shadow: 0 10px 50px rgba(0,0,0,0.2);
max-width: 500px;
width: 90%;
text-align: center;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 2em;
}
.subtitle {
color: #666;
margin-bottom: 40px;
font-size: 1.1em;
}
.btn {
display: inline-block;
padding: 15px 40px;
margin: 10px;
border-radius: 8px;
text-decoration: none;
font-weight: bold;
font-size: 1.1em;
transition: all 0.3s ease;
cursor: pointer;
border: none;
}
.btn-google {
background: #4285f4;
color: white;
}
.btn-google:hover {
background: #357ae8;
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(66, 133, 244, 0.4);
}
.btn-jwt {
background: #11998e;
color: white;
}
.btn-jwt:hover {
background: #0e8074;
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(17, 153, 142, 0.4);
}
.btn-api {
background: #667eea;
color: white;
}
.btn-api:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.divider {
margin: 30px 0;
height: 1px;
background: #ddd;
}
.info {
background: #f0f4ff;
padding: 20px;
border-radius: 8px;
margin-top: 30px;
text-align: left;
}
.info h3 {
color: #667eea;
margin-bottom: 10px;
}
.info ul {
list-style-position: inside;
color: #555;
}
.info li {
margin: 5px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>🏛️ Biblioteca UTH</h1>
<p class="subtitle">Sistema de Gestión de Biblioteca</p>
<!-- Login con Google -->
<a href="{% url 'oauth_login' %}" class="btn btn-google">
🔐 Login con Google
</a>
<div class="divider"></div>
<!-- Login tradicional con JWT -->
<a href="{% url 'jwt_login_page' %}" class="btn btn-jwt">
🔑 Login con JWT
</a>
<div class="divider"></div>
<!-- Ver API -->
<a href="/api/" class="btn btn-api">
📚 Explorar API REST
</a>
<div class="info">
<h3>ℹ️ Información del Sistema</h3>
<ul>
<li>✅ API REST con Django REST Framework</li>
<li>✅ Autenticación JWT</li>
<li>✅ OAuth 2.0 con Google</li>
<li>✅ MySQL como base de datos</li>
<li>✅ Gestión completa de biblioteca</li>
</ul>
</div>
</div>
</body>
</html>
Paso 4: Crear página de login OAuth
Crea el archivo templates/oauth_login.html:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login con Google - Biblioteca UTH</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
padding: 40px;
border-radius: 15px;
box-shadow: 0 10px 50px rgba(0,0,0,0.2);
max-width: 600px;
width: 100%;
}
h1 {
color: #333;
margin-bottom: 30px;
text-align: center;
}
.loading {
text-align: center;
color: #666;
font-size: 1.2em;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.result {
margin-top: 30px;
padding: 20px;
border-radius: 8px;
display: none;
}
.success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.user-info {
background: #f0f4ff;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
}
.token-display {
background: #2d2d2d;
color: #4ec9b0;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
word-wrap: break-word;
font-family: 'Courier New', monospace;
font-size: 0.9em;
}
.btn {
display: inline-block;
padding: 10px 20px;
margin-top: 15px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 5px;
transition: all 0.3s ease;
}
.btn:hover {
background: #5568d3;
}
</style>
</head>
<body>
<div class="container">
<h1>🔐 Login con Google</h1>
<div id="loading" class="loading">
<div class="spinner"></div>
<p>Procesando autenticación...</p>
</div>
<div id="result" class="result"></div>
</div>
<script>
// Obtener código de autorización de la URL
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const error = urlParams.get('error');
if (error) {
showError('Login cancelado o error: ' + error);
} else if (!code) {
// Si no hay código, redirigir a Google
window.location.href = '/api/auth/google/redirect/';
} else {
// Si hay código, intercambiarlo por tokens
exchangeCodeForTokens(code);
}
async function exchangeCodeForTokens(code) {
try {
const response = await fetch('/api/auth/google/callback/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code: code })
});
const data = await response.json();
if (response.ok) {
// Guardar tokens en localStorage
localStorage.setItem('access_token', data.access);
localStorage.setItem('refresh_token', data.refresh);
showSuccess(data);
} else {
showError(data.error || 'Error desconocido');
}
} catch (error) {
showError('Error de red: ' + error.message);
}
}
function showSuccess(data) {
document.getElementById('loading').style.display = 'none';
const resultDiv = document.getElementById('result');
resultDiv.className = 'result success';
resultDiv.style.display = 'block';
resultDiv.innerHTML = `
<h2>✅ ${data.message}</h2>
<div class="user-info">
<h3>👤 Datos del Usuario</h3>
<p><strong>Email:</strong> ${data.user.email}</p>
<p><strong>Nombre:</strong> ${data.user.first_name} ${data.user.last_name}</p>
<p><strong>Username:</strong> ${data.user.username}</p>
${data.google_data.picture ? `<img src="${data.google_data.picture}" alt="Foto de perfil" style="border-radius:50%;margin-top:10px">` : ''}
</div>
<div class="token-display">
<strong>🔑 Access Token (JWT):</strong><br>
${data.access.substring(0, 50)}...
</div>
<a href="/api/" class="btn">📚 Ir a la API</a>
<a href="/" class="btn">🏠 Volver al Inicio</a>
`;
}
function showError(errorMessage) {
document.getElementById('loading').style.display = 'none';
const resultDiv = document.getElementById('result');
resultDiv.className = 'result error';
resultDiv.style.display = 'block';
resultDiv.innerHTML = `
<h2>❌ Error en el Login</h2>
<p>${errorMessage}</p>
<a href="/" class="btn">🏠 Volver al Inicio</a>
`;
}
</script>
</body>
</html>
Paso 5: Crear vistas para servir los templates
Crea el archivo libros/web_views.py (nuevo):
from django.shortcuts import render, redirect
from django.conf import settings
def home(request):
"""Página de inicio"""
return render(request, 'home.html')
def oauth_login(request):
"""Página de login con OAuth que maneja el callback de Google"""
# Si hay un código en la URL, renderizar la página
# que procesará el código
return render(request, 'oauth_login.html')
def jwt_login_page(request):
"""Página de login con JWT (tradicional)"""
return render(request, 'jwt_login.html')
Paso 6: Agregar URLs para las páginas web
Edita biblioteca_project/urls.py:
from django.contrib import admin
from django.urls import path, include
from libros import web_views # ← AGREGAR
urlpatterns = [
path('admin/', admin.site.urls),
# URLs de la API
path('api/', include('libros.api_urls')),
# URLs de páginas web (para pruebas)
path('', web_views.home, name='home'),
path('oauth/login/', web_views.oauth_login, name='oauth_login'),
path('login/jwt/', web_views.jwt_login_page, name='jwt_login_page'),
]
✅ Checkpoint 3.6: Templates creados
Verifica la estructura:
biblioteca_unidad4/ ├── templates/ │ ├── home.html │ └── oauth_login.html ├── libros/ │ ├── web_views.py (nuevo) │ ├── oauth_views.py │ └── ... └── ...
Verifica que Django encuentre los templates:
3.7 Crear Página de Login JWT Completa (10 minutos)
Paso 1: Crear archivo templates/jwt_login.html:
📋 ¿Qué vamos a crear?
Una página completa de login tradicional con username/password que:
- Genera tokens JWT al hacer login
- Guarda los tokens en localStorage del navegador
- Muestra el token generado
- Redirige automáticamente a la API
- Incluye tabs de Login/Registro
- CSS y JavaScript inline (todo en un archivo)
COPIAR TODO ESTE CÓDIGO en templates/jwt_login.html:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login JWT - Biblioteca UTH</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
padding: 40px;
border-radius: 15px;
box-shadow: 0 10px 50px rgba(0,0,0,0.2);
max-width: 500px;
width: 100%;
}
h1 {
color: #333;
margin-bottom: 30px;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: bold;
}
input {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 1em;
transition: all 0.3s ease;
}
input:focus {
outline: none;
border-color: #11998e;
}
.btn {
width: 100%;
padding: 15px;
background: #11998e;
color: white;
border: none;
border-radius: 8px;
font-size: 1.1em;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.btn:hover {
background: #0e8074;
transform: translateY(-2px);
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.result {
margin-top: 20px;
padding: 15px;
border-radius: 8px;
display: none;
}
.success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.token-display {
background: #2d2d2d;
color: #4ec9b0;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
word-wrap: break-word;
font-family: 'Courier New', monospace;
font-size: 0.85em;
max-height: 150px;
overflow-y: auto;
}
.link {
display: inline-block;
margin-top: 15px;
color: #11998e;
text-decoration: none;
font-weight: bold;
}
.link:hover {
text-decoration: underline;
}
.hint {
font-size: 0.85em;
color: #999;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔑 Login con JWT</h1>
<form id="loginForm">
<div class="form-group">
<label for="username">Usuario:</label>
<input type="text" id="username" name="username" required placeholder="admin">
<div class="hint">Usa: admin</div>
</div>
<div class="form-group">
<label for="password">Contraseña:</label>
<input type="password" id="password" name="password" required placeholder="••••••••">
<div class="hint">Usa: admin123</div>
</div>
<button type="submit" class="btn" id="submitBtn">Iniciar Sesión</button>
</form>
<div id="result" class="result"></div>
<a href="/" class="link">← Volver al inicio</a>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', async function(e) {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
const resultDiv = document.getElementById('result');
submitBtn.disabled = true;
submitBtn.textContent = 'Iniciando sesión...';
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
try {
const response = await fetch('/api/auth/jwt/login/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username,
password: password
})
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('access_token', data.access);
localStorage.setItem('refresh_token', data.refresh);
resultDiv.className = 'result success';
resultDiv.style.display = 'block';
resultDiv.innerHTML = `
<h3>✅ Login Exitoso</h3>
<p>Tokens JWT generados correctamente</p>
<div class="token-display">
<strong>Access Token:</strong><br>
${data.access}
</div>
<a href="/api/" class="btn" style="margin-top:15px;text-decoration:none;display:inline-block">
📚 Ir a la API
</a>
`;
document.getElementById('loginForm').reset();
} else {
resultDiv.className = 'result error';
resultDiv.style.display = 'block';
resultDiv.innerHTML = `
<h3>❌ Error en el Login</h3>
<p>${data.detail || 'Credenciales incorrectas'}</p>
`;
}
} catch (error) {
resultDiv.className = 'result error';
resultDiv.style.display = 'block';
resultDiv.innerHTML = `
<h3>❌ Error de Conexión</h3>
<p>${error.message}</p>
`;
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Iniciar Sesión';
}
});
</script>
</body>
</html>
✅ Checkpoint 3.7: Página JWT Login creada
Verifica:
- ✅ Archivo
templates/jwt_login.htmlcreado - ✅ Formulario con campos username y password
- ✅ JavaScript para hacer POST a la API
- ✅ Guardar tokens en localStorage
- ✅ CSS inline (no necesita archivos externos)
Prueba rápida:
- Arrancar servidor:
python manage.py runserver - Ir a:
http://127.0.0.1:8000/login/jwt/ - Login con: admin / admin123
- Deberías ver el token JWT generado
3.8 Probar Todo el Sistema OAuth + JWT (20 minutos)
🧪 Prueba Completa End-to-End
Ahora probaremos TODAS las formas de autenticación implementadas:
- Login tradicional con JWT (username/password)
- Login con Google OAuth 2.0
- Acceso a la API REST con tokens
Paso 1: Verificar que el servidor esté corriendo
Paso 2: Probar página de inicio
- Abrir navegador en:
http://127.0.0.1:8000/ - Deberías ver la página con 3 botones:
- 🔐 Login con Google
- 🔑 Login con JWT
- 📚 Explorar API REST
Paso 3: Probar Login JWT
- Click en "🔑 Login con JWT"
- O ir directamente a:
http://127.0.0.1:8000/login/jwt/ - Ingresar credenciales:
- Usuario: admin
- Contraseña: admin123
- Click "Iniciar Sesión"
- ✅ Salida esperada:
- Mensaje: "Login Exitoso"
- Token JWT visible (eyJhbGc...)
- Botón "Ir a la API"
Paso 4: Probar Login con Google
❌ Error común: redirect_uri_mismatch al hacer Login con Google
Si al intentar hacer login con Google recibes un error "Error 400: redirect_uri_mismatch", esto significa que las URIs de redirección no coinciden entre tu código y Google Cloud Console.
Causas principales:
- Las URIs en Google Cloud Console NO coinciden exactamente con las del código
- Inconsistencias entre
localhosty127.0.0.1 - URIs sin el slash final (
/) - Email de prueba no agregado como Test User en Google Console
Guía paso a paso para resolver el error redirect_uri_mismatch
Solución rápida:
- Verificar que
oauth_views.pyusehttp://127.0.0.1:8000/api/auth/google/callback/en ambas funciones - En Google Cloud Console → Credenciales → Agregar URI:
http://127.0.0.1:8000/api/auth/google/callback/ - Agregar tu email como Test User en Google Console → OAuth consent screen
- Esperar 5 minutos y probar en modo incógnito usando
http://127.0.0.1:8000/
- Volver a la página de inicio:
http://127.0.0.1:8000/ - Click en "🔐 Login con Google"
- O ir a:
http://127.0.0.1:8000/oauth/login/ - Te redirigirá a Google
- Selecciona tu cuenta de Google (debe estar en la lista de test users)
- Acepta los permisos
- ✅ Salida esperada:
- Redirige de vuelta a tu app
- Mensaje: "Login exitoso con Google" o "Cuenta creada exitosamente"
- Tu información de usuario (nombre, email, foto)
- Token JWT generado
Paso 5: Probar API con el token
Después de cualquier login (JWT o Google), el token se guarda en localStorage.
Opción A: Usar el navegador (interfaz navegable de DRF)
- Click en el botón "📚 Ir a la API" o visita:
http://127.0.0.1:8000/api/ - Verás la interfaz navegable de Django REST Framework
- Explora los endpoints disponibles
Opción B: Usar Postman/Thunder Client
📝 Ejemplo: Obtener lista de libros con el token
1. Copiar el token del localStorage:
Abre DevTools (F12) → Console → Ejecuta:
localStorage.getItem('access_token')
Copia el token que aparece
2. Hacer request en Postman:
- Method: GET
- URL:
http://127.0.0.1:8000/api/libros/ - Headers:
- Key: Authorization
- Value: Bearer TU_TOKEN_AQUI
✅ Respuesta esperada:
[
{
"id": 1,
"titulo": "Don Quijote de la Mancha",
"isbn": "978-84-376-0494-7",
"fecha_publicacion": "1605-01-16",
"stock": 5,
"categoria": 1,
"autores": [1]
},
...
]
Paso 6: Probar CRUD completo
a) Crear un nuevo libro (POST):
POST http://127.0.0.1:8000/api/libros/
Headers: Authorization: Bearer TU_TOKEN
Body (JSON):
{
"titulo": "Cien Años de Soledad",
"isbn": "978-03-071-4728-7",
"fecha_publicacion": "1967-05-30",
"stock": 3,
"categoria": 1,
"autores": [1]
}
b) Actualizar un libro (PUT):
PUT http://127.0.0.1:8000/api/libros/1/
Headers: Authorization: Bearer TU_TOKEN
Body (JSON):
{
"titulo": "Don Quijote de la Mancha (Edición Actualizada)",
"stock": 10
}
c) Eliminar un libro (DELETE):
DELETE http://127.0.0.1:8000/api/libros/5/ Headers: Authorization: Bearer TU_TOKEN
🚨 Errores Comunes y Soluciones
| Error | Solución |
|---|---|
401 Unauthorized |
|
403 Forbidden |
No tienes permisos para esa acción. Usa una cuenta de admin. |
redirect_uri_mismatch (Google) |
|
| Página en blanco después de Google login |
|
CSRF verification failed |
|
✅ Checkpoint 3.8: Sistema completo probado
Verifica que hayas probado:
- ✅ Página de inicio funcionando
- ✅ Login JWT con admin/admin123
- ✅ Login con Google OAuth funcionando
- ✅ Token guardado en localStorage
- ✅ API REST accesible con el token
- ✅ CRUD de libros funcionando
- ✅ Endpoints protegidos rechazando requests sin token
Si todo funciona: ¡FELICIDADES! 🎉 Tienes un sistema completo de autenticación con JWT + OAuth 2.0
🎉 ¡FELICIDADES! PARTE 2 COMPLETADA AL 100%: OAuth 2.0 + JWT
Has implementado exitosamente:
- ✅ Proyecto configurado en Google Cloud Console
- ✅ Django Allauth instalado y configurado
- ✅ OAuth 2.0 flow completo implementado
- ✅ Login con Google funcionando
- ✅ Login tradicional con JWT funcionando
- ✅ Creación automática de usuarios desde OAuth
- ✅ Generación de JWT después de OAuth
- ✅ 3 Templates HTML funcionales (home, oauth_login, jwt_login)
- ✅ API REST protegida con autenticación
- ✅ CRUD completo de todas las entidades
- ✅ Sistema de pruebas completo
💾 GUARDAR PROGRESO: Hacer commit en Git:
git add .
git commit -m "✅ JWT + OAuth 2.0 completos al 100% con templates"
📊 PROGRESO DEL PROYECTO:
✅ PASO 0: Preparación del Entorno (COMPLETO)
✅ PASO 1: Proyecto Django + MySQL (COMPLETO)
✅ PARTE 1: JWT Authentication (COMPLETO)
✅ PARTE 2: OAuth 2.0 + Templates (COMPLETO AL 100%)
⏭️ PARTE 3: WebSockets (Opcional)
⏭️ PARTE 4: GraphQL (Opcional)
🚀 ¡YA TIENES UN PROYECTO !
Con JWT + OAuth 2.0 + Templates
Edita biblioteca_project/settings.py:
INSTALLED_APPS = [
# ... apps existentes
'oauth2_provider',
]
# OAuth2 Provider Settings
OAUTH2_PROVIDER = {
'SCOPES': {
'read': 'Read scope - Permite leer datos',
'write': 'Write scope - Permite escribir datos',
'groups': 'Access to groups - Acceso a grupos de usuario'
},
'ACCESS_TOKEN_EXPIRE_SECONDS': 3600, # 1 hora
'REFRESH_TOKEN_EXPIRE_SECONDS': 86400, # 1 día
'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600, # 10 minutos
'ROTATE_REFRESH_TOKEN': True,
}
3 Agregar URLs OAuth
from django.urls import path, include
urlpatterns = [
# ... URLs existentes
path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
]
4 Ejecutar Migraciones
5 Crear Aplicación OAuth
Desde el admin de Django:
- Ir a:
http://127.0.0.1:8000/admin/ - OAuth2 Provider → Applications → Add Application
- Configurar:
- User: admin (tu usuario)
- Client type: Confidential
- Authorization grant type: Resource owner password-based
- Name: Biblioteca Mobile App
- Guardar y copiar Client ID y Client Secret
6 Probar OAuth 2.0
Crea un script de prueba test_oauth.py:
import requests
# Configuración
TOKEN_URL = 'http://127.0.0.1:8000/o/token/'
API_URL = 'http://127.0.0.1:8000/api/libros/'
CLIENT_ID = 'tu_client_id_aqui'
CLIENT_SECRET = 'tu_client_secret_aqui'
USERNAME = 'admin'
PASSWORD = 'tu_password'
print("=== Obteniendo Token OAuth 2.0 ===")
# Obtener token
response = requests.post(TOKEN_URL, data={
'grant_type': 'password',
'username': USERNAME,
'password': PASSWORD,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'scope': 'read write'
})
if response.status_code == 200:
token_data = response.json()
access_token = token_data['access_token']
print(f"✅ Token obtenido: {access_token[:50]}...")
# Usar token para acceder a API
headers = {'Authorization': f'Bearer {access_token}'}
api_response = requests.get(API_URL, headers=headers)
print(f"Status Code: {api_response.status_code}")
print(f"Data: {api_response.json()}")
else:
print(f"❌ Error: {response.status_code}")
print(response.json())
🔄 PARTE 3: WEBSOCKETS EN TIEMPO REAL
WebSockets🤔 ¿Qué son WebSockets?
WebSockets proporcionan comunicación bidireccional en tiempo real entre cliente y servidor. A diferencia de HTTP (request-response), WebSockets mantienen una conexión abierta y persistente.
💡 Casos de Uso de WebSockets
- Chat en tiempo real
- Notificaciones push instantáneas
- Actualizaciones de stock/inventario en vivo
- Tableros colaborativos
- Juegos multiplayer
- Dashboards con datos en tiempo real
1 Instalar Django Channels
Django Channels extiende Django para manejar WebSockets y protocolos asíncronos:
2 Configurar Channels
Edita settings.py:
INSTALLED_APPS = [
'daphne', # ← AGREGAR AL PRINCIPIO
'django.contrib.admin',
# ... resto de apps
'channels',
]
# ASGI Application
ASGI_APPLICATION = 'biblioteca_project.asgi.application'
# Channel Layers - Opción 1: Con Redis (RECOMENDADO para producción)
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
# Opción 2: En memoria (SOLO para desarrollo)
# CHANNEL_LAYERS = {
# 'default': {
# 'BACKEND': 'channels.layers.InMemoryChannelLayer'
# }
# }
⚠️ Nota sobre Redis
Para producción, necesitas Redis instalado. En Windows:
O usa Docker:
3 Configurar ASGI
Edita biblioteca_project/asgi.py:
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'biblioteca_project.settings')
django_asgi_app = get_asgi_application()
from libros.routing import websocket_urlpatterns
application = ProtocolTypeRouter({
"http": django_asgi_app,
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(websocket_urlpatterns)
)
),
})
4 Crear WebSocket Consumer
Crea libros/consumers.py:
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from .models import Libro
class NotificacionesConsumer(AsyncWebsocketConsumer):
"""Consumer para notificaciones en tiempo real"""
async def connect(self):
"""Cuando un cliente se conecta"""
self.room_group_name = 'notificaciones'
# Unirse al grupo
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# Mensaje de bienvenida
await self.send(text_data=json.dumps({
'type': 'connection',
'message': '✅ Conectado a notificaciones en tiempo real'
}))
async def disconnect(self, close_code):
"""Cuando un cliente se desconecta"""
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
"""Recibir mensaje del cliente"""
data = json.loads(text_data)
message_type = data.get('type')
if message_type == 'libro_update':
libro_id = data.get('libro_id')
await self.notificar_cambio_libro(libro_id)
async def notificar_cambio_libro(self, libro_id):
"""Notificar a todos sobre cambio en libro"""
libro_data = await self.get_libro_data(libro_id)
# Broadcast a todo el grupo
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'libro_actualizado',
'libro': libro_data
}
)
async def libro_actualizado(self, event):
"""Enviar notificación al cliente"""
await self.send(text_data=json.dumps({
'type': 'libro_actualizado',
'libro': event['libro']
}))
@database_sync_to_async
def get_libro_data(self, libro_id):
"""Obtener datos del libro (sync to async)"""
try:
libro = Libro.objects.get(pk=libro_id)
return {
'id': libro.id,
'titulo': libro.titulo,
'stock': libro.stock,
'disponible': libro.esta_disponible
}
except Libro.DoesNotExist:
return None
class ChatConsumer(AsyncWebsocketConsumer):
"""Consumer para chat de biblioteca"""
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# Notificar que alguien se conectó
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'user_join',
'message': f'Un usuario se unió al chat'
}
)
async def disconnect(self, close_code):
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
data = json.loads(text_data)
message = data['message']
username = data.get('username', 'Anónimo')
# Enviar mensaje a todos en la sala
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'username': username
}
)
async def chat_message(self, event):
"""Recibir mensaje del grupo y enviarlo al WebSocket"""
await self.send(text_data=json.dumps({
'type': 'message',
'message': event['message'],
'username': event['username']
}))
async def user_join(self, event):
"""Usuario se unió"""
await self.send(text_data=json.dumps({
'type': 'system',
'message': event['message']
}))
5 Crear Routing para WebSockets
Crea libros/routing.py:
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/notificaciones/$', consumers.NotificacionesConsumer.as_asgi()),
re_path(r'ws/chat/(?P\w+)/$', consumers.ChatConsumer.as_asgi()),
]
6 Ejecutar con Daphne
O con el comando tradicional (ahora usa Daphne automáticamente):
7 Cliente JavaScript para WebSocket
En tu HTML/JavaScript:
<script>
// Conectar al WebSocket
const chatSocket = new WebSocket(
'ws://' + window.location.host + '/ws/chat/general/'
);
chatSocket.onopen = function(e) {
console.log('✅ WebSocket conectado');
};
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
console.log('Mensaje recibido:', data);
// Mostrar mensaje en el chat
if (data.type === 'message') {
addMessage(data.username, data.message);
}
};
chatSocket.onclose = function(e) {
console.error('❌ WebSocket cerrado');
};
chatSocket.onerror = function(e) {
console.error('Error en WebSocket:', e);
};
// Enviar mensaje
function sendMessage(message) {
chatSocket.send(JSON.stringify({
'message': message,
'username': 'Tu Nombre'
}));
}
function addMessage(username, message) {
const messagesDiv = document.getElementById('chat-messages');
messagesDiv.innerHTML += `<p><strong>${username}:</strong> ${message}</p>`;
}
</script>
📊 PARTE 4: GRAPHQL
GraphQL🤔 ¿Qué es GraphQL?
GraphQL es un lenguaje de consulta y manipulación de datos para APIs. Desarrollado por Facebook, permite a los clientes solicitar exactamente los datos que necesitan.
💡 Ventajas de GraphQL vs REST
| Aspecto | REST | GraphQL |
|---|---|---|
| Endpoints | Múltiples endpoints | Un solo endpoint |
| Over-fetching | Recibe datos no necesarios | Solo los datos solicitados |
| Under-fetching | Múltiples requests | Una sola request |
| Versionado | Requiere versiones (/v1/, /v2/) | Sin versiones necesarias |
| Documentación | Manual (Swagger) | Auto-documentado (Introspection) |
| Flexibilidad | Estructura fija | Cliente decide estructura |
1 Instalar Graphene
2 Configurar GraphQL
En settings.py:
INSTALLED_APPS = [
# ... apps existentes
'graphene_django',
]
# GraphQL Settings
GRAPHENE = {
'SCHEMA': 'libros.schema.schema',
'MIDDLEWARE': [
'graphene_django.debug.DjangoDebugMiddleware',
],
}
3 Crear Schema GraphQL
Crea libros/schema.py:
import graphene
from graphene_django import DjangoObjectType
from .models import Libro, Autor, Categoria
# ===== TYPES (Tipos de Datos) =====
class AutorType(DjangoObjectType):
class Meta:
model = Autor
fields = '__all__'
class CategoriaType(DjangoObjectType):
class Meta:
model = Categoria
fields = '__all__'
class LibroType(DjangoObjectType):
class Meta:
model = Libro
fields = '__all__'
esta_disponible = graphene.Boolean()
def resolve_esta_disponible(self, info):
return self.esta_disponible
# ===== QUERIES (Consultas) =====
class Query(graphene.ObjectType):
# Queries simples
all_libros = graphene.List(LibroType)
all_autores = graphene.List(AutorType)
all_categorias = graphene.List(CategoriaType)
# Queries con argumentos
libro = graphene.Field(
LibroType,
id=graphene.Int(),
isbn=graphene.String()
)
libros_por_autor = graphene.List(
LibroType,
autor_id=graphene.Int(required=True)
)
libros_disponibles = graphene.List(LibroType)
buscar_libros = graphene.List(
LibroType,
titulo=graphene.String(required=True)
)
# Resolvers
def resolve_all_libros(self, info):
return Libro.objects.filter(activo=True)
def resolve_all_autores(self, info):
return Autor.objects.all()
def resolve_all_categorias(self, info):
return Categoria.objects.all()
def resolve_libro(self, info, id=None, isbn=None):
if id:
return Libro.objects.get(pk=id)
if isbn:
return Libro.objects.get(isbn=isbn)
return None
def resolve_libros_por_autor(self, info, autor_id):
return Libro.objects.filter(
autor_id=autor_id,
activo=True
)
def resolve_libros_disponibles(self, info):
return Libro.objects.filter(
estado=Libro.DISPONIBLE,
stock__gt=0,
activo=True
)
def resolve_buscar_libros(self, info, titulo):
return Libro.objects.filter(
titulo__icontains=titulo,
activo=True
)
# ===== MUTATIONS (Modificaciones) =====
class ActualizarStockLibro(graphene.Mutation):
class Arguments:
libro_id = graphene.Int(required=True)
cantidad = graphene.Int(required=True)
libro = graphene.Field(LibroType)
mensaje = graphene.String()
def mutate(self, info, libro_id, cantidad):
libro = Libro.objects.get(pk=libro_id)
libro.actualizar_stock(cantidad)
return ActualizarStockLibro(
libro=libro,
mensaje=f"Stock actualizado a {libro.stock}"
)
class CrearAutor(graphene.Mutation):
class Arguments:
nombre = graphene.String(required=True)
fecha_nacimiento = graphene.Date(required=True)
pais_origen = graphene.String(required=True)
biografia = graphene.String()
autor = graphene.Field(AutorType)
def mutate(self, info, nombre, fecha_nacimiento, pais_origen, biografia=None):
autor = Autor.objects.create(
nombre=nombre,
fecha_nacimiento=fecha_nacimiento,
pais_origen=pais_origen,
biografia=biografia
)
return CrearAutor(autor=autor)
class Mutation(graphene.ObjectType):
actualizar_stock_libro = ActualizarStockLibro.Field()
crear_autor = CrearAutor.Field()
# ===== SCHEMA =====
schema = graphene.Schema(query=Query, mutation=Mutation)
4 Configurar URL GraphQL
En urls.py:
from graphene_django.views import GraphQLView
from django.views.decorators.csrf import csrf_exempt
urlpatterns = [
# ... URLs existentes
path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
]
5 Probar GraphQL
Abre http://127.0.0.1:8000/graphql/ en tu navegador.
Ejemplo Query 1: Obtener todos los libros con autor
query {
allLibros {
id
titulo
isbn
precio
stock
estaDisponible
autor {
nombre
paisOrigen
}
}
}
Ejemplo Query 2: Buscar libros por título
query {
buscarLibros(titulo: "Cien") {
id
titulo
autor {
nombre
}
precio
}
}
Ejemplo Mutation: Actualizar stock
mutation {
actualizarStockLibro(libroId: 1, cantidad: 5) {
libro {
id
titulo
stock
}
mensaje
}
}
✅ Ventajas de GraphQL en Acción
Con GraphQL, el cliente decide exactamente qué datos necesita. ¡Una sola query puede reemplazar múltiples endpoints REST!
🛡️ PARTE 5: SEGURIDAD EN APIS
Security🔒 Seguridad es Fundamental
La seguridad no es opcional en aplicaciones web modernas. Debes proteger tu API contra ataques comunes y asegurar los datos de tus usuarios.
⚠️ Amenazas Comunes
- SQL Injection: Inyección de código SQL malicioso
- XSS (Cross-Site Scripting): Inyección de scripts maliciosos
- CSRF (Cross-Site Request Forgery): Peticiones no autorizadas
- Brute Force: Intentos masivos de login
- DDoS: Ataques de denegación de servicio
- Man-in-the-Middle: Interceptación de comunicaciones
1 HTTPS en Producción
Configuración segura para producción en settings.py:
# Solo para PRODUCCIÓN (no desarrollo)
if not DEBUG:
# Forzar HTTPS
SECURE_SSL_REDIRECT = True
# Cookies seguras
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Headers de seguridad
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
# HSTS (HTTP Strict Transport Security)
SECURE_HSTS_SECONDS = 31536000 # 1 año
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Proxy SSL headers
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
2 Validación de Entrada
Crea libros/validators.py:
from django.core.exceptions import ValidationError
import re
def validar_isbn(value):
"""Validar formato ISBN-10 o ISBN-13"""
isbn = re.sub(r'[\s-]', '', value)
if len(isbn) not in [10, 13]:
raise ValidationError(
'ISBN debe tener 10 o 13 dígitos'
)
if not isbn.isdigit():
raise ValidationError(
'ISBN debe contener solo dígitos'
)
return isbn
def prevenir_sql_injection(value):
"""Prevenir SQL Injection básico"""
patrones_peligrosos = [
r'\bDROP\b', r'\bDELETE\b', r'\bUPDATE\b',
r'\bINSERT\b', r'\bSELECT\b', r';--',
r"'OR'1'='1", r'\bUNION\b'
]
for patron in patrones_peligrosos:
if re.search(patron, value, re.IGNORECASE):
raise ValidationError(
'Entrada contiene caracteres no permitidos'
)
return value
def sanitizar_html(value):
"""Eliminar tags HTML peligrosos"""
# Remover scripts
value = re.sub(r'', '', value, flags=re.DOTALL | re.IGNORECASE)
# Remover eventos JavaScript
value = re.sub(r'on\w+\s*=\s*["\'].*?["\']', '', value, flags=re.IGNORECASE)
# Remover tags HTML básicos (opcional)
value = re.sub(r'<[^>]+>', '', value)
return value.strip()
def validar_password_fuerte(value):
"""Validar contraseña fuerte"""
if len(value) < 8:
raise ValidationError('La contraseña debe tener al menos 8 caracteres')
if not re.search(r'[A-Z]', value):
raise ValidationError('Debe contener al menos una mayúscula')
if not re.search(r'[a-z]', value):
raise ValidationError('Debe contener al menos una minúscula')
if not re.search(r'\d', value):
raise ValidationError('Debe contener al menos un número')
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', value):
raise ValidationError('Debe contener al menos un carácter especial')
return value
3 Middleware de Seguridad
Crea libros/middleware.py:
from django.core.cache import cache
from django.http import JsonResponse, HttpResponsePermanentRedirect
from django.conf import settings
import logging
logger = logging.getLogger(__name__)
class SecurityMiddleware:
"""Middleware de seguridad personalizado"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Verificar headers de seguridad
if not request.is_secure() and not request.META.get('HTTP_X_FORWARDED_PROTO') == 'https':
if hasattr(settings, 'SECURE_SSL_REDIRECT') and settings.SECURE_SSL_REDIRECT:
return HttpResponsePermanentRedirect(
request.build_absolute_uri().replace('http://', 'https://')
)
response = self.get_response(request)
# Agregar headers de seguridad
response['X-Content-Type-Options'] = 'nosniff'
response['X-Frame-Options'] = 'DENY'
response['X-XSS-Protection'] = '1; mode=block'
return response
class RateLimitMiddleware:
"""Limitar requests por IP"""
def __init__(self, get_response):
self.get_response = get_response
self.limit = 100 # requests
self.period = 3600 # 1 hora en segundos
def __call__(self, request):
ip = self.get_client_ip(request)
# Solo para rutas /api/
if request.path.startswith('/api/'):
cache_key = f'rate_limit_{ip}'
requests_count = cache.get(cache_key, 0)
if requests_count >= self.limit:
logger.warning(f'Rate limit exceeded for IP: {ip}')
return JsonResponse({
'error': 'Rate limit exceeded',
'detail': f'Máximo {self.limit} requests por hora'
}, status=429)
# Incrementar contador
cache.set(cache_key, requests_count + 1, self.period)
response = self.get_response(request)
return response
def get_client_ip(self, request):
"""Obtener IP real del cliente"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
else:
ip = request.META.get('REMOTE_ADDR')
return ip
Agregar a settings.py:
MIDDLEWARE = [
# ... middlewares existentes
'libros.middleware.SecurityMiddleware',
'libros.middleware.RateLimitMiddleware',
]
🔐 Checklist de Seguridad
- ✅ Usar HTTPS en producción
- ✅ Validar todas las entradas de usuario
- ✅ Sanitizar datos antes de guardar
- ✅ Usar prepared statements (ORM hace esto)
- ✅ Implementar rate limiting
- ✅ Configurar CORS correctamente
- ✅ Protección CSRF habilitada
- ✅ Contraseñas hasheadas (Django lo hace)
- ✅ Tokens JWT con expiración
- ✅ Logging de intentos fallidos
- ✅ Mantener dependencias actualizadas
⚡ PARTE 6: RATE LIMITING Y THROTTLING
🚦 Control de Tráfico de API
Rate Limiting y Throttling limitan la cantidad de requests que un cliente puede hacer en un período de tiempo. Protege contra abuso y asegura disponibilidad.
1 Throttling en DRF
Crea libros/throttles.py:
from rest_framework.throttling import UserRateThrottle, AnonRateThrottle
class BurstRateThrottle(UserRateThrottle):
"""Límite para ráfagas cortas"""
scope = 'burst'
class SustainedRateThrottle(UserRateThrottle):
"""Límite sostenido"""
scope = 'sustained'
class AnonBurstRateThrottle(AnonRateThrottle):
"""Límite para usuarios anónimos"""
scope = 'anon_burst'
class PremiumUserThrottle(UserRateThrottle):
"""Límite más alto para usuarios premium"""
scope = 'premium'
def allow_request(self, request, view):
# Usuarios premium tienen límite más alto
if request.user.is_authenticated and hasattr(request.user, 'is_premium'):
if request.user.is_premium:
return True
return super().allow_request(request, view)
2 Configurar en Settings
REST_FRAMEWORK = {
# ... configuración existente
'DEFAULT_THROTTLE_CLASSES': [
'libros.throttles.BurstRateThrottle',
'libros.throttles.SustainedRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'burst': '60/min', # 60 por minuto
'sustained': '1000/day', # 1000 por día
'anon_burst': '20/min', # Anónimos: 20 por minuto
'premium': '10000/day', # Premium: 10000 por día
}
}
3 Aplicar a Vistas Específicas
from rest_framework.decorators import api_view, throttle_classes
from .throttles import BurstRateThrottle
@api_view(['GET'])
@throttle_classes([BurstRateThrottle])
def api_intensiva(request):
# Esta ruta tiene throttling especial
return Response({'data': 'información'})
🌐 PARTE 7: CORS Y CSRF
🔄 CORS (Cross-Origin Resource Sharing)
Configuración ya realizada en Unidad 2. Recordatorio:
# En settings.py
# Orígenes permitidos para CORS
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
"https://tudominio.com",
"https://www.tudominio.com",
]
# Permitir credenciales
CORS_ALLOW_CREDENTIALS = True
# Headers permitidos
CORS_ALLOW_HEADERS = [
'accept',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
]
🛡️ CSRF (Cross-Site Request Forgery)
# Orígenes confiables para CSRF
CSRF_TRUSTED_ORIGINS = [
"https://tudominio.com",
"https://www.tudominio.com",
]
# Cookie CSRF segura en producción
if not DEBUG:
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = 'Strict'
🔗 PARTE 8: INTEGRACIÓN CON SERVICIOS EXTERNOS
1 Integración con Google Books API
Crea libros/external_services.py:
import requests
from decimal import Decimal
import logging
logger = logging.getLogger(__name__)
class GoogleBooksAPI:
"""Cliente para Google Books API"""
BASE_URL = 'https://www.googleapis.com/books/v1/volumes'
TIMEOUT = 10 # segundos
@classmethod
def buscar_libro(cls, isbn):
"""Buscar libro por ISBN"""
url = f"{cls.BASE_URL}?q=isbn:{isbn}"
try:
response = requests.get(url, timeout=cls.TIMEOUT)
response.raise_for_status()
data = response.json()
if data.get('totalItems', 0) > 0:
return cls._parsear_libro(data['items'][0])
logger.info(f'Libro con ISBN {isbn} no encontrado en Google Books')
return None
except requests.Timeout:
logger.error('Timeout al consultar Google Books API')
return None
except requests.RequestException as e:
logger.error(f'Error al consultar Google Books: {e}')
return None
@classmethod
def _parsear_libro(cls, item):
"""Parsear respuesta de Google Books"""
volume_info = item.get('volumeInfo', {})
return {
'titulo': volume_info.get('title'),
'subtitulo': volume_info.get('subtitle', ''),
'autores': volume_info.get('authors', []),
'editorial': volume_info.get('publisher'),
'fecha_publicacion': volume_info.get('publishedDate'),
'descripcion': volume_info.get('description'),
'paginas': volume_info.get('pageCount'),
'categorias': volume_info.get('categories', []),
'imagen_portada': volume_info.get('imageLinks', {}).get('thumbnail'),
'idioma': volume_info.get('language'),
'isbn_10': cls._extraer_isbn(volume_info, 'ISBN_10'),
'isbn_13': cls._extraer_isbn(volume_info, 'ISBN_13'),
}
@classmethod
def _extraer_isbn(cls, volume_info, tipo):
"""Extraer ISBN específico"""
identifiers = volume_info.get('industryIdentifiers', [])
for identifier in identifiers:
if identifier.get('type') == tipo:
return identifier.get('identifier')
return None
2 Endpoint para Importar Libro
Agrega a api_views.py:
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from .external_services import GoogleBooksAPI
@api_view(['POST'])
@permission_classes([IsAdminUser])
def importar_desde_google_books(request):
"""Importar libro desde Google Books por ISBN"""
isbn = request.data.get('isbn')
if not isbn:
return Response({
'error': 'ISBN es requerido'
}, status=400)
# Buscar en Google Books
data = GoogleBooksAPI.buscar_libro(isbn)
if not data:
return Response({
'error': 'Libro no encontrado en Google Books'
}, status=404)
# Aquí puedes crear el libro automáticamente
# o devolver los datos para que el usuario los complete
return Response({
'mensaje': 'Libro encontrado',
'data': data
}, status=200)
📊 RÚBRICA DE EVALUACIÓN UNIDAD 4
| Criterio | Descripción | |
|---|---|---|
| 1. JWT Implementation | ||
| Configuración correcta | Settings y middleware bien configurados | |
| Endpoints funcionando | Login, refresh y verify operativos | |
| Personalización | Claims custom y response personalizado | |
| 2. OAuth 2.0 | ||
| Configuración | OAuth toolkit correctamente configurado | |
| Flujo completo | Obtención y uso de tokens funciona | |
| 3. WebSockets | ||
| Channels configurado | ASGI y routing correctos | |
| Consumers implementados | Chat o notificaciones funcionando | |
| Frontend funcional | Cliente JavaScript conectando | |
| 4. GraphQL | ||
| Schema definido | Types, queries y mutations bien estructurados | |
| Queries funcionando | Al menos 5 queries diferentes | |
| Mutations implementadas | Al menos 2 mutations funcionales | |
| 5. Seguridad | ||
| HTTPS y headers | Configuración segura en producción | |
| Validación de entrada | Validators implementados | |
| Rate limiting | Throttling configurado y funcionando | |
| CORS configurado | CORS y CSRF correctamente configurados | |
| 6. Integraciones | ||
| API externa integrada | Google Books u otra API funcionando | |
| Funcionalidad completa | Import o feature usando la API | |
| 7. Proyecto Final | ||
| Código completo | Todas las unidades integradas | |
| Desplegado | URL funcional en producción | |
RUBRICA TOTAL
🎯 CONCLUSIÓN DEL CURSO
¡Felicidades! Has completado las 4 unidades del curso completo
📚 Unidad 1: Django + MySQL + GitHub + Deploy ✅
🔌 Unidad 2: API REST + DRF + Autenticación ✅
🐳 Unidad 3: SOAP + Microservicios + Docker ✅
🔐 Unidad 4: Seguridad + WebSockets + GraphQL + OAuth ✅
🚀 Tecnologías Dominadas
🎓 ¡Eres ahora un desarrollador de servicios web completo!
Estás listo para construir aplicaciones web robustas, seguras y escalables
🚀 DESPLIEGUE EN PRODUCCIÓN - PYTHONANYWHERE
🌐 ¿Por qué PythonAnywhere?
PythonAnywhere es una plataforma de hosting especializada en Python que te permite desplegar aplicaciones Django de forma gratuita y sencilla. Perfecto para proyectos académicos y portafolio profesional.
Ventajas:
- ✅ Hosting gratuito (cuenta básica incluye 1 aplicación web)
- ✅ MySQL incluido (500 MB gratis)
- ✅ SSL/HTTPS automático
- ✅ Consola Bash integrada en el navegador
- ✅ Fácil configuración sin Docker ni servidores complejos
- ✅ Ideal para mostrar tu proyecto en entrevistas
⚠️ LIMITACIONES DE LA CUENTA GRATUITA
- WebSockets NO soportados en la cuenta gratuita (requiere plan pagado)
- OAuth con Google necesita dominio público (funciona en PythonAnywhere)
- APIs externas: Solo HTTPS permitido (HTTP bloqueado)
- Límite de CPU: 100 segundos/día (suficiente para demostración)
💡 Solución: Desplegaremos las partes core (JWT, OAuth, REST API, GraphQL) y documentaremos que WebSockets funciona en local.
1 Crear Cuenta en PythonAnywhere (5 minutos)
- Ve a: https://www.pythonanywhere.com/
- Click en "Start running Python online in less than a minute!"
- Click en "Create a Beginner account" (gratis)
- Completa el formulario:
- Username: tu nombre o nickname (será parte de la URL:
https://tuusername.pythonanywhere.com) - Email: tu correo (recibirás confirmación)
- Password: contraseña segura
- Username: tu nombre o nickname (será parte de la URL:
- Acepta términos y crea cuenta
- Verifica tu email (revisa spam si no llega)
✅ Cuenta creada exitosamente
Tu URL será: https://TUUSERNAME.pythonanywhere.com
Ejemplo: https://bibliotecauth.pythonanywhere.com
2 Preparar Proyecto para Despliegue (15 minutos)
Paso 2.1: Crear archivo de dependencias
En tu proyecto local, crea requirements.txt con todas las librerías:
O créalo manualmente con estas dependencias mínimas:
Django==5.1.5 djangorestframework==3.15.2 djangorestframework-simplejwt==5.4.0 django-allauth==65.3.0 django-cors-headers==4.6.0 PyJWT==2.10.1 requests==2.32.3 mysqlclient==2.2.6 gunicorn==23.0.0 whitenoise==6.8.2
Paso 2.2: Crear archivo de configuración de producción
Crea biblioteca_project/settings_production.py:
from .settings import *
# Configuración de producción
DEBUG = False
ALLOWED_HOSTS = [
'tuusername.pythonanywhere.com', # Reemplaza con tu username
'localhost',
'127.0.0.1',
]
# Base de datos MySQL de PythonAnywhere
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'tuusername$biblioteca', # Reemplaza con tu username
'USER': 'tuusername', # Tu username de PythonAnywhere
'PASSWORD': 'tu_password_mysql', # La crearás en PythonAnywhere
'HOST': 'tuusername.mysql.pythonanywhere-services.com',
'PORT': '3306',
'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
},
}
}
# Archivos estáticos (CSS, JavaScript, Images)
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# Middleware para servir archivos estáticos
MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware')
# Configuración de seguridad
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
# CORS (ajustar según necesites)
CORS_ALLOWED_ORIGINS = [
'https://tuusername.pythonanywhere.com',
]
# OAuth redirect URIs para producción
# Actualiza oauth_views.py para usar esta variable
OAUTH_REDIRECT_URI = 'https://tuusername.pythonanywhere.com/api/auth/google/callback/'
Paso 2.3: Modificar oauth_views.py para usar configuración dinámica
Edita libros/oauth_views.py para detectar entorno:
from django.conf import settings
# En google_oauth_callback (línea ~45)
# Reemplaza la redirect_uri hardcodeada por:
redirect_uri = getattr(settings, 'OAUTH_REDIRECT_URI', 'http://127.0.0.1:8000/api/auth/google/callback/')
token_data = {
'code': code,
'client_id': google_config['client_id'],
'client_secret': google_config['secret'],
'redirect_uri': redirect_uri, # ← Usar variable dinámica
'grant_type': 'authorization_code'
}
# En google_oauth_redirect (línea ~160)
# Hacer lo mismo:
redirect_uri = getattr(settings, 'OAUTH_REDIRECT_URI', 'http://127.0.0.1:8000/api/auth/google/callback/')
auth_url = (
'https://accounts.google.com/o/oauth2/v2/auth'
f'?client_id={google_config["client_id"]}'
f'&redirect_uri={redirect_uri}' # ← Usar variable dinámica
f'&scope={" ".join(scopes)}'
'&response_type=code'
'&access_type=offline'
'&prompt=consent'
)
3 Subir Código a GitHub (10 minutos)
🔗 ¿Por qué GitHub?
PythonAnywhere permite clonar proyectos directamente desde GitHub, lo que facilita el despliegue y actualizaciones.
Paso 3.1: Crear archivo .gitignore
Antes de subir, crea .gitignore en la raíz del proyecto:
# Python *.pyc __pycache__/ *.py[cod] *$py.class venv/ env/ ENV/ # Django *.log db.sqlite3 db.sqlite3-journal staticfiles/ media/ # Secrets .env secrets.json # IDEs .vscode/ .idea/ *.swp *.swo
Paso 3.2: Inicializar repositorio Git
Paso 3.3: Crear repositorio en GitHub
- Ve a github.com y login
- Click en "New repository" (botón verde)
- Nombre:
biblioteca-django-api - Descripción: "API REST con Django, JWT, OAuth 2.0 y GraphQL"
- Público o Privado (recomendado: Público para portafolio)
- NO marques "Initialize with README" (ya tienes código local)
- Click "Create repository"
Paso 3.4: Subir código a GitHub
✅ Código subido a GitHub
Tu repositorio está en: https://github.com/TU_USUARIO/biblioteca-django-api
4 Configurar Base de Datos MySQL en PythonAnywhere (10 minutos)
- Login en PythonAnywhere
- En el Dashboard, click en "Databases" (menú superior)
- En "MySQL", verás:
- MySQL password: Campo para establecer contraseña
- Establece una contraseña segura y guárdala (la necesitarás en settings)
- Click en "Initialize MySQL"
- Una vez inicializado, verás:
- MySQL hostname:
TUUSERNAME.mysql.pythonanywhere-services.com
- MySQL hostname:
- En "Create a new database", ingresa:
biblioteca - Click "Create"
- Verás la base de datos creada:
TUUSERNAME$biblioteca
📝 Anota estos datos (los usarás después):
- Host:
TUUSERNAME.mysql.pythonanywhere-services.com - Database:
TUUSERNAME$biblioteca - User:
TUUSERNAME - Password: La que acabas de crear
5 Clonar Proyecto en PythonAnywhere (15 minutos)
Paso 5.1: Abrir consola Bash
- En PythonAnywhere Dashboard, click en "Consoles"
- En "Start a new console", click en "Bash"
- Se abrirá una terminal en el navegador
Paso 5.2: Clonar repositorio de GitHub
# Clonar tu repositorio git clone https://github.com/TU_USUARIO/biblioteca-django-api.git # Entrar al directorio cd biblioteca-django-api # Ver archivos ls -la
Paso 5.3: Crear entorno virtual
# Crear virtualenv con Python 3.10 mkvirtualenv --python=/usr/bin/python3.10 biblioteca-env # Activar entorno (se activa automáticamente al crear) workon biblioteca-env # Instalar dependencias pip install -r requirements.txt
⏱️ La instalación puede tardar 5-10 minutos
Especialmente mysqlclient puede tardar. Si hay error, intenta:
pip install --upgrade pip pip install mysqlclient
Paso 5.4: Configurar settings para producción
# Editar settings_production.py nano biblioteca_project/settings_production.py # Actualiza con tus datos reales: # - ALLOWED_HOSTS con tu username.pythonanywhere.com # - DATABASES con tus credenciales MySQL # - OAUTH_REDIRECT_URI con tu dominio # Guardar: Ctrl+O, Enter, Ctrl+X
Paso 5.5: Migrar base de datos
# Configurar variable de entorno para usar settings de producción export DJANGO_SETTINGS_MODULE=biblioteca_project.settings_production # Hacer migraciones python manage.py makemigrations python manage.py migrate # Crear superusuario python manage.py createsuperuser # Recolectar archivos estáticos python manage.py collectstatic --noinput
6 Configurar Aplicación Web en PythonAnywhere (15 minutos)
Paso 6.1: Crear Web App
- En Dashboard, click en "Web" (menú superior)
- Click en "Add a new web app"
- Click "Next" (acepta el dominio gratis)
- Selecciona "Manual configuration"
- Selecciona "Python 3.10"
- Click "Next"
Paso 6.2: Configurar rutas del proyecto
En la página de configuración web, actualiza estas secciones:
📁 Code
- Source code:
/home/TUUSERNAME/biblioteca-django-api - Working directory:
/home/TUUSERNAME/biblioteca-django-api
🐍 Virtualenv
- Click en el link de "Enter path to a virtualenv"
- Ingresa:
/home/TUUSERNAME/.virtualenvs/biblioteca-env - Click ✓ (checkmark)
⚙️ WSGI Configuration File
Click en el link del archivo WSGI (algo como /var/www/TUUSERNAME_pythonanywhere_com_wsgi.py)
Se abrirá un editor. BORRA TODO y pega este código:
import os
import sys
# Agregar proyecto al path
path = '/home/TUUSERNAME/biblioteca-django-api' # Reemplaza TUUSERNAME
if path not in sys.path:
sys.path.insert(0, path)
# Configurar Django settings
os.environ['DJANGO_SETTINGS_MODULE'] = 'biblioteca_project.settings_production'
# Importar aplicación Django
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
Click "Save" (botón verde arriba a la derecha)
📂 Static files
Scroll hasta la sección "Static files" y agrega:
| URL | Directory |
|---|---|
/static/ |
/home/TUUSERNAME/biblioteca-django-api/staticfiles |
Paso 6.3: Recargar aplicación
- Scroll hasta arriba de la página
- Click en el botón verde grande "Reload TUUSERNAME.pythonanywhere.com"
- Espera 10-15 segundos
7 Configurar OAuth con Dominio de Producción (10 minutos)
Paso 7.1: Agregar URI de producción en Google Console
- Ve a Google Cloud Console
- Selecciona tu proyecto OAuth
- Ve a APIs y servicios → Credenciales
- Click en tu "ID de cliente de OAuth 2.0"
- En "URIs de redireccionamiento autorizados", agrega:
https://TUUSERNAME.pythonanywhere.com/api/auth/google/callback/
Click "GUARDAR" y espera 5 minutos para que se propague
Paso 7.2: Actualizar Site en Django Admin
- Ve a:
https://TUUSERNAME.pythonanywhere.com/admin/ - Login con tu superusuario
- Click en "Sites"
- Click en el site existente
- Actualiza:
- Domain name:
TUUSERNAME.pythonanywhere.com - Display name:
Biblioteca UTH
- Domain name:
- Click "Save"
8 Probar Aplicación en Producción (10 minutos) Probar con Postman/Thunder Client
✅ URLs para probar:
| Endpoint | URL | Método |
|---|---|---|
| Página de inicio | https://TUUSERNAME.pythonanywhere.com/ |
GET |
| Admin Django | https://TUUSERNAME.pythonanywhere.com/admin/ |
GET |
| API REST | https://TUUSERNAME.pythonanywhere.com/api/ |
GET |
| Login JWT | https://TUUSERNAME.pythonanywhere.com/api/auth/jwt/login/ |
POST |
| OAuth Google | https://TUUSERNAME.pythonanywhere.com/oauth/login/ |
GET |
| GraphQL | https://TUUSERNAME.pythonanywhere.com/graphql/ |
GET/POST |
| Libros API | https://TUUSERNAME.pythonanywhere.com/api/libros/ |
GET |
🧪 Pruebas recomendadas:
- Admin: Verifica que puedas acceder y gestionar datos
- Login JWT: Obtén un token y úsalo en Postman
- OAuth Google: Haz login con tu cuenta de Google (debe estar en test users)
- API REST: Consulta endpoints con autenticación JWT
- GraphQL: Ejecuta queries en el playground
9 Troubleshooting Común (Referencia)
❌ Error: Internal Server Error (500)
Causa: Error en configuración o código
Solución:
- Ve a Dashboard → Web → Sección "Log files"
- Click en "Error log"
- Lee el error específico (última línea)
- Errores comunes:
- ModuleNotFoundError: Falta instalar una librería →
pip install LIBRERIA - ImproperlyConfigured: Error en settings.py
- OperationalError: Credenciales MySQL incorrectas
- ModuleNotFoundError: Falta instalar una librería →
❌ Error: Disallowed Host
Causa: ALLOWED_HOSTS no incluye tu dominio
Solución:
nano biblioteca_project/settings_production.py # Verifica que ALLOWED_HOSTS tenga: ALLOWED_HOSTS = ['tuusername.pythonanywhere.com'] # Guarda y recarga la web app
❌ Error: Static files no cargan (CSS/JS)
Causa: No ejecutaste collectstatic o ruta incorrecta
Solución:
workon biblioteca-env cd biblioteca-django-api python manage.py collectstatic --noinput # Verifica que se creó la carpeta: ls -la staticfiles/ # En Web tab, verifica la ruta en "Static files": # URL: /static/ # Directory: /home/TUUSERNAME/biblioteca-django-api/staticfiles
❌ OAuth no funciona: redirect_uri_mismatch
Causa: URI no agregada en Google Console
Solución:
- Verifica que en Google Console tengas:
https://tuusername.pythonanywhere.com/api/auth/google/callback/ - Espera 5-10 minutos después de agregar
- Usa modo incógnito para probar
10 Actualizar Código (para futuras modificaciones)
Cuando hagas cambios en tu proyecto local y quieras actualizarlo en producción:
# 1. En tu PC (local): git add . git commit -m "Descripción de cambios" git push origin main # 2. En PythonAnywhere (consola Bash): cd biblioteca-django-api git pull origin main # 3. Si hay cambios en modelos: workon biblioteca-env python manage.py makemigrations python manage.py migrate # 4. Si hay archivos estáticos nuevos: python manage.py collectstatic --noinput # 5. Recargar web app: # Ve a Web tab → Click "Reload TUUSERNAME.pythonanywhere.com"
🎉 ¡APLICACIÓN DESPLEGADA EXITOSAMENTE!
Tu API REST con autenticación JWT y OAuth 2.0 está ahora en producción y accesible públicamente.
✅ Lo que funciona en producción:
- ✅ API REST completa (CRUD de libros, autores, categorías, préstamos)
- ✅ Autenticación JWT (login con usuario/contraseña)
- ✅ OAuth 2.0 con Google (login social)
- ✅ GraphQL API (consultas flexibles)
- ✅ Admin de Django
- ✅ HTTPS/SSL automático
- ✅ Rate limiting y seguridad
📝 Limitaciones de cuenta gratuita:
- ⚠️ WebSockets NO disponibles (requiere plan pagado)
- ⚠️ SOAP puede tener limitaciones (usa REST como alternativa)
🔗 Comparte tu proyecto:
- URL pública:
https://tuusername.pythonanywhere.com - Repositorio GitHub:
https://github.com/TU_USUARIO/biblioteca-django-api - Documentación API:
https://tuusername.pythonanywhere.com/api/
💼 Úsalo en tu CV y portfolio:
Este proyecto demuestra habilidades profesionales en desarrollo backend, APIs REST, autenticación moderna, seguridad y despliegue en producción.
📝 RESUMEN DE CORRECCIONES IMPLEMENTADAS EN ESTA GUÍA
🔧 Cambios Realizados para Corregir Errores
1️⃣ ERROR: ModuleNotFoundError: No module named 'oauth2_provider'
📍 Ubicación del problema: Al ejecutar python manage.py migrate después de configurar OAuth 2.0
❌ Causa: Faltaba instalar la librería django-oauth-toolkit que proporciona el módulo oauth2_provider
✅ Solución implementada:
- Se agregó la instalación de
django-oauth-toolkit==2.3.0en la sección Paso 3.2 - Se incluyó
'oauth2_provider'enINSTALLED_APPSdel settings.py - Se agregó una caja de advertencia destacada en color amarillo explicando la importancia de esta librería
📄 Ubicación en la guía: Sección "PARTE 2: OAUTH 2.0" → Paso 3.2
2️⃣ ERROR: Código 401 Unauthorized al ejecutar test_oauth.py
📍 Ubicación del problema: Después de crear el script test_oauth.py (Paso 6: Probar OAuth 2.0)
❌ Causa: La configuración REST_FRAMEWORK no incluía OAuth2Authentication, por lo que DRF no reconocía los tokens OAuth emitidos por django-oauth-toolkit
✅ Solución implementada:
- Se agregó la línea
'oauth2_provider.contrib.rest_framework.OAuth2Authentication'enDEFAULT_AUTHENTICATION_CLASSESdentro deREST_FRAMEWORK - Se agregó una nota explicativa sobre por qué es necesaria esta línea
- Se creó la sección Paso 3.3.2 específicamente para documentar este error y su solución
📄 Ubicación en la guía:
- Configuración: Sección "PARTE 1: JWT" → Paso 2.3 (REST_FRAMEWORK)
- Explicación del error: Sección "PARTE 2: OAUTH 2.0" → Paso 3.3.2
3️⃣ MEJORAS ADICIONALES IMPLEMENTADAS
✨ Se agregaron las siguientes secciones para mayor claridad:
- Configuración OAuth 2.0 Provider Settings:
- Se agregó la sección
OAUTH2_PROVIDERen settings.py - Configuración de tiempos de expiración de tokens OAuth
- Definición de scopes disponibles
- Se agregó la sección
- Configuración de URLs de OAuth:
- Nueva sección Paso 3.3.1 para configurar las URLs de django-oauth-toolkit
- Inclusión de
path('o/', include('oauth2_provider.urls'))en urls.py - Explicación de la diferencia entre
/accounts/(allauth) y/o/(oauth2_provider)
- Diferenciación entre librerías:
- Caja informativa explicando la diferencia entre django-allauth y django-oauth-toolkit
- Casos de uso de cada una
📚 Resumen de Librerías Necesarias para OAuth 2.0
Para que OAuth 2.0 funcione completamente en tu proyecto, necesitas instalar:
pip install django-allauth==0.57.0 pip install django-oauth-toolkit==2.3.0
¿Qué hace cada una?
- django-allauth: Permite login social (Login con Google/Facebook) - Tu app CONSUME OAuth de otros
- django-oauth-toolkit: Permite que tu app SEA un proveedor OAuth - Otras apps pueden usar tus tokens
✅ Checklist: Verifica que todo esté configurado
Antes de probar OAuth 2.0, asegúrate de tener:
| Configuración | Estado |
|---|---|
| django-allauth instalado | ☐ |
| django-oauth-toolkit instalado | ☐ |
| 'oauth2_provider' en INSTALLED_APPS | ☐ |
| OAuth2Authentication en REST_FRAMEWORK | ☐ |
| OAUTH2_PROVIDER configurado en settings.py | ☐ |
| URLs de oauth2_provider agregadas (path('o/', ...)) | ☐ |
| Migraciones ejecutadas sin errores | ☐ |
| Credenciales de Google configuradas en settings.py | ☐ |
🎯 Con estas correcciones, la guía ahora está completa y funcional
Última actualización: Febrero 2026 | Profesor: Bernardo Prado
🔧 CORRECCIONES ADICIONALES
⚠️ ¿PERSISTEN ERRORES EN TU SISTEMA?
Si después de seguir toda la guía aún experimentas problemas, estas correcciones adicionales pueden resolver situaciones específicas que varían según tu entorno de desarrollo o errores comunes que se presentan durante la implementación.
VERSIÓN 3.0
Correcciones para Desarrollo Local
✅ Configuración de Base de Datos MySQL
✅ Configuración OAuth URLs en Google Console
✅ Corrección Script oauth_login.html
✅ Implementación WebSockets Completa
✅ Configuración API de Google Books
👉 CLICK PARA VER CORRECCIONES →
VERSIÓN 4.0
Correcciones para PythonAnywhere
✅ Solución Incompatibilidad autobahn
✅ Error al instalar txaio
✅ Error en migraciones (importar os)
✅ Página no carga datos (whitenoise)
✅ Configuración settings_production.py
👉 CLICK PARA VER CORRECCIONES →
VERSIÓN 5.0
Errores Comunes Reportados
✅ Error: Invalid model reference OAuth
✅ Error: Unknown command show_urls
✅ Error: redirect_uri mismatch + loop
✅ Error: Token OAuth retorna 401
✅ Experiencia real de estudiantes
👉 CLICK PARA VER SOLUCIONES →
📚 ¿Cuál guía debo usar?
| Situación | Guía Recomendada |
|---|---|
| ⚙️ Estás desarrollando en tu computadora local (localhost:8000) | VERSIÓN 3.0 |
| 🚀 Ya vas a desplegar o desplegaste en PythonAnywhere | VERSIÓN 4.0 |
| 🐛 Te aparecieron errores específicos siguiendo la guía paso a paso | VERSIÓN 5.0 |
| 🔐 Problemas con OAuth o login de Google | VERSIÓN 3.0 o 5.0 |
| 💬 WebSockets no funcionan correctamente | VERSIÓN 3.0 |
| 🐛 Error "Something went wrong" en producción | VERSIÓN 4.0 |
| 📦 Problemas instalando dependencias en PythonAnywhere | VERSIÓN 4.0 |
| ❌ Error: "Invalid model reference" en OAuth | VERSIÓN 5.0 |
| ❌ Error: "Unknown command: show_urls" | VERSIÓN 5.0 |
| ❌ Token OAuth retorna 401 en tests | VERSIÓN 5.0 |
💡 Consejos Importantes
- Revisa primero la guía principal: Estas correcciones son complementarias, no sustitutos
- Aplica solo lo necesario: No todas las correcciones aplican a todos los casos
- Lee con atención: Cada corrección explica cuándo y por qué aplicarla
- Prueba una a la vez: No apliques todas las correcciones de golpe
- Consulta con alguien más: Si persisten problemas después de aplicar las correcciones, no dejes de preguntar y de buscar para encontrar la solución, Entre antes lo hagas más pronto encontraras la respuesta