🏗️ ARQUITECTURA COMPLETA DEL PROYECTO

Sistema de Biblioteca Digital - Django REST + MySQL + JWT + OAuth + WebSockets

✅ TODOS LOS ARCHIVOS CON CÓDIGO COMPLETO INCLUIDO

📚 Proyecto: Biblioteca Digital con Catálogo de Libros y Préstamos

🔧 Tecnologías: Django 4.2, DRF, MySQL, JWT, OAuth 2.0, WebSockets, GraphQL

📖
¿Necesitas la guía paso a paso?

Consulta la guía completa de la Unidad 4 con explicaciones detalladas de cada componente

📚 Ver Guía Completa →
📋 ACERCA DE ESTE DOCUMENTO:
Este documento contiene TODOS los archivos del proyecto de Biblioteca Digital con código fuente completo. Es el mismo proyecto que se desarrolla paso a paso en la guía principal.

🔗 Relación con la guía: Este archivo complementa la Guía Completa Unidad 4 mostrando la arquitectura final completa con todos los archivos del proyecto.

💡 Uso: Puedes copiar cada archivo directamente de aquí para crear tu proyecto Django funcional, o seguir la guía paso a paso para entender cada parte del proceso.
📌 Nota: Los botones amarillos (Templates, CSS/JS, Docker) te llevarán a una página separada con ese contenido funcionando correctamente.

📁 Estructura Completa del Proyecto

proyecto_django/
│
├── 📁 biblioteca_digital/           # Proyecto Django principal
│   ├── 🐍 __init__.py
│   ├── 🐍 settings.py              # Configuración principal ⭐
│   ├── 🐍 urls.py                  # URLs principales
│   ├── 🐍 asgi.py                  # ASGI config para WebSockets
│   └── 🐍 wsgi.py                  # WSGI config
│
├── 📁 libros/                       # Aplicación de Biblioteca Digital 📚
│   ├── 🐍 __init__.py
│   ├── 🐍 models.py                # Modelos: Libro, Autor, Categoría, Préstamo ⭐
│   ├── 🐍 serializers.py           # Serializadores DRF ⭐
│   ├── 🐍 views.py                 # Vistas y endpoints API ⭐
│   ├── 🐍 urls.py                  # URLs de la API ⭐
│   ├── 🐍 authentication.py        # JWT & OAuth ⭐
│   ├── 🐍 permissions.py           # Permisos personalizados
│   ├── 🐍 consumers.py             # WebSocket consumers para chat ⭐
│   ├── 🐍 routing.py               # WebSocket routing ⭐
│   ├── 🐍 admin.py                 # Panel de administración
│   ├── 🐍 apps.py                  # Configuración de la app
│   └── 🐍 tests.py                 # Tests unitarios
│
├── 📁 templates/                    # Templates HTML
│   ├── 🌐 base.html                # Template base ⭐
│   ├── 📁 auth/                     # Templates de autenticación
│   │   ├── 🌐 login.html           # Login con JWT ⭐
│   │   └── 🌐 register.html        # Registro de usuarios ⭐
│   ├── 📁 libros/                   # Templates de libros
│   │   ├── 🌐 catalogo.html        # Catálogo de libros ⭐
│   │   └── 🌐 detalle.html         # Detalle de libro
│   ├── 📁 prestamos/                # Templates de préstamos
│   │   └── 🌐 mis_prestamos.html   # Préstamos del usuario ⭐
│   ├── 🌐 dashboard.html           # Dashboard principal ⭐
│   ├── 🌐 chat.html                # Chat WebSocket ⭐
│   └── 🌐 index.html               # Página principal
│
├── 📁 static/                       # Archivos estáticos
│   ├── 📁 css/
│   │   ├── 🎨 style.css            # Estilos principales ⭐
│   │   ├── 🎨 dashboard.css        # Estilos dashboard
│   │   └── 🎨 libros.css           # Estilos catálogo
│   ├── 📁 js/
│   │   ├── ⚡ auth.js              # Autenticación JWT ⭐
│   │   ├── ⚡ websocket.js         # WebSocket cliente ⭐
│   │   ├── ⚡ oauth.js             # OAuth cliente ⭐
│   │   ├── ⚡ libros.js            # Gestión de libros ⭐
│   │   └── ⚡ prestamos.js         # Gestión de préstamos
│   └── 📁 img/
│       └── logo.png
│
├── 📁 media/                        # Archivos subidos por usuarios
│   ├── avatars/                     # Avatares de usuarios
│   ├── libros/portadas/             # Portadas de libros
│   └── autores/                     # Fotos de autores
│
├── ⚙️ requirements.txt             # Dependencias Python ⭐
├── ⚙️ .env                         # Variables de entorno ⭐
├── ⚙️ .env.example                 # Ejemplo de .env
├── ⚙️ manage.py                    # Script de gestión Django
├── 🐳 Dockerfile                   # Docker para producción ⭐
├── 🐳 docker-compose.yml           # Orquestación Docker ⭐
└── 📄 README.md                    # Documentación
✅ Total de archivos: 40+ archivos con código completo
📦 Tecnologías: Django 4.2, DRF, MySQL, JWT, OAuth2, WebSockets, Docker
📚 Proyecto: Sistema de Biblioteca Digital con catálogo, préstamos y reseñas

⚙️ Archivos de Configuración

requirements.txt DEPENDENCIAS
📍 proyecto_django/requirements.txt
# ========================================
# DEPENDENCIAS DEL PROYECTO DJANGO
# ========================================

# Framework Django
Django==4.2.7
djangorestframework==3.14.0

# Base de datos MySQL
mysqlclient==2.2.0

# Autenticación JWT
djangorestframework-simplejwt==5.3.0
PyJWT==2.8.0

# OAuth 2.0
django-allauth==0.57.0
requests-oauthlib==1.3.1
social-auth-app-django==5.4.0

# WebSockets
channels==4.0.0
channels-redis==4.1.0
daphne==4.0.0

# CORS (para APIs)
django-cors-headers==4.3.0

# Variables de entorno
python-decouple==3.8
python-dotenv==1.0.0

# Validación y seguridad
django-csp==3.7
django-ratelimit==4.1.0

# Producción
gunicorn==21.2.0
whitenoise==6.6.0

# Redis (para WebSockets y cache)
redis==5.0.1

# Utilidades
Pillow==10.1.0
celery==5.3.4
.env.example VARIABLES DE ENTORNO
📍 proyecto_django/.env.example
# ========================================
# VARIABLES DE ENTORNO - COPIAR A .env
# ========================================

# Django
SECRET_KEY=tu-clave-secreta-super-segura-aqui-cambiar
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1

# Base de datos MySQL
DB_ENGINE=django.db.backends.mysql
DB_NAME=mi_base_datos
DB_USER=root
DB_PASSWORD=tu_password_mysql
DB_HOST=localhost
DB_PORT=3306

# JWT
JWT_SECRET_KEY=otra-clave-secreta-para-jwt
JWT_ACCESS_TOKEN_LIFETIME=60
JWT_REFRESH_TOKEN_LIFETIME=1440

# OAuth Google
GOOGLE_OAUTH_CLIENT_ID=tu-client-id-de-google.apps.googleusercontent.com
GOOGLE_OAUTH_CLIENT_SECRET=tu-client-secret-de-google
GOOGLE_OAUTH_REDIRECT_URI=http://localhost:8000/api/auth/google/callback/

# OAuth GitHub
GITHUB_OAUTH_CLIENT_ID=tu-client-id-de-github
GITHUB_OAUTH_CLIENT_SECRET=tu-client-secret-de-github
GITHUB_OAUTH_REDIRECT_URI=http://localhost:8000/api/auth/github/callback/

# WebSockets Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0

# Email (opcional)
EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=tu-email@gmail.com
EMAIL_HOST_PASSWORD=tu-password-de-app

# CORS
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8000

# Producción
SECURE_SSL_REDIRECT=False
SESSION_COOKIE_SECURE=False
CSRF_COOKIE_SECURE=False
settings.py CONFIGURACIÓN PRINCIPAL
📍 biblioteca_digital/settings.py
from pathlib import Path
from datetime import timedelta
from decouple import config

# Build paths
BASE_DIR = Path(__file__).resolve().parent.parent

# SECURITY
SECRET_KEY = config('SECRET_KEY', default='django-insecure-change-this-in-production')
DEBUG = config('DEBUG', default=True, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1').split(',')

# INSTALLED APPS
INSTALLED_APPS = [
    'daphne',  # Para WebSockets (debe ir primero)
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    # Third party apps
    'rest_framework',
    'rest_framework_simplejwt',
    'corsheaders',
    'channels',
    
    # OAuth
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.google',
    'allauth.socialaccount.providers.github',
    
    # Local apps
    'libros',  # App principal del proyecto
]

SITE_ID = 1

# MIDDLEWARE
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # CORS debe ir primero
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'biblioteca_digital.urls'

# TEMPLATES
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# WSGI & ASGI
WSGI_APPLICATION = 'biblioteca_digital.wsgi.application'
ASGI_APPLICATION = 'biblioteca_digital.asgi.application'

# DATABASE - MySQL
DATABASES = {
    'default': {
        'ENGINE': config('DB_ENGINE', default='django.db.backends.mysql'),
        'NAME': config('DB_NAME', default='mi_base_datos'),
        'USER': config('DB_USER', default='root'),
        'PASSWORD': config('DB_PASSWORD', default=''),
        'HOST': config('DB_HOST', default='localhost'),
        'PORT': config('DB_PORT', default='3306'),
        'OPTIONS': {
            'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
            'charset': 'utf8mb4',
        }
    }
}

# CHANNELS (WebSockets)
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [(config('REDIS_HOST', default='localhost'), 
                      config('REDIS_PORT', default=6379, cast=int))],
        },
    },
}

# REST FRAMEWORK
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
    ],
}

# JWT SETTINGS
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=config('JWT_ACCESS_TOKEN_LIFETIME', default=60, cast=int)),
    'REFRESH_TOKEN_LIFETIME': timedelta(minutes=config('JWT_REFRESH_TOKEN_LIFETIME', default=1440, cast=int)),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'ALGORITHM': 'HS256',
    'SIGNING_KEY': config('JWT_SECRET_KEY', default=SECRET_KEY),
    'AUTH_HEADER_TYPES': ('Bearer',),
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
}

# OAUTH SETTINGS
AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'allauth.account.auth_backends.AuthenticationBackend',
]

SOCIALACCOUNT_PROVIDERS = {
    'google': {
        'SCOPE': ['profile', 'email'],
        'AUTH_PARAMS': {'access_type': 'online'},
        'APP': {
            'client_id': config('GOOGLE_OAUTH_CLIENT_ID', default=''),
            'secret': config('GOOGLE_OAUTH_CLIENT_SECRET', default=''),
            'key': ''
        }
    },
    'github': {
        'SCOPE': ['user', 'repo', 'read:org'],
        'APP': {
            'client_id': config('GITHUB_OAUTH_CLIENT_ID', default=''),
            'secret': config('GITHUB_OAUTH_CLIENT_SECRET', default=''),
        }
    }
}

# CORS
CORS_ALLOWED_ORIGINS = config(
    'CORS_ALLOWED_ORIGINS',
    default='http://localhost:3000,http://localhost:8000'
).split(',')
CORS_ALLOW_CREDENTIALS = True

# PASSWORD VALIDATION
AUTH_PASSWORD_VALIDATORS = [
    {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
    {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
    {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
    {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]

# INTERNATIONALIZATION
LANGUAGE_CODE = 'es-es'
TIME_ZONE = 'America/Mexico_City'
USE_I18N = True
USE_TZ = True

# STATIC FILES
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [BASE_DIR / 'static']
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

# MEDIA FILES
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

# DEFAULT PRIMARY KEY
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

# SECURITY (Producción)
if not DEBUG:
    SECURE_SSL_REDIRECT = True
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SECURE = True
    SECURE_BROWSER_XSS_FILTER = True
    SECURE_CONTENT_TYPE_NOSNIFF = True
    X_FRAME_OPTIONS = 'DENY'
urls.py (Principal) RUTAS PRINCIPALES
📍 biblioteca_digital/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('libros.urls')),  # API y templates de la app libros
    path('accounts/', include('allauth.urls')),  # OAuth URLs
    path('', include('libros.urls')),  # Rutas base (templates)
]

# Servir archivos estáticos y media en desarrollo
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
asgi.py ASGI WEBSOCKETS
📍 biblioteca_digital/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_digital.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)
        )
    ),
})

🐍 Backend - Archivos Python de la App LIBROS

models.py MODELOS
📍 libros/models.py
from django.db import models
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator, MaxValueValidator

class UserProfile(models.Model):
    """Perfil extendido del usuario"""
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
    bio = models.TextField(max_length=500, blank=True)
    avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
    phone = models.CharField(max_length=20, blank=True)
    birth_date = models.DateField(null=True, blank=True)
    
    # OAuth
    google_id = models.CharField(max_length=255, blank=True, null=True)
    github_id = models.CharField(max_length=255, blank=True, null=True)
    
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    def __str__(self):
        return f'Profile: {self.user.username}'
    
    class Meta:
        db_table = 'user_profiles'
        verbose_name = 'Perfil de Usuario'
        verbose_name_plural = 'Perfiles de Usuarios'


class Categoria(models.Model):
    """Categorías de libros"""
    nombre = models.CharField(max_length=100, unique=True)
    descripcion = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.nombre
    
    class Meta:
        db_table = 'categorias'
        ordering = ['nombre']
        verbose_name = 'Categoría'
        verbose_name_plural = 'Categorías'


class Autor(models.Model):
    """Autores de libros"""
    nombre = models.CharField(max_length=200)
    apellido = models.CharField(max_length=200)
    biografia = models.TextField(blank=True)
    fecha_nacimiento = models.DateField(null=True, blank=True)
    pais = models.CharField(max_length=100, blank=True)
    foto = models.ImageField(upload_to='autores/', null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    @property
    def nombre_completo(self):
        return f"{self.nombre} {self.apellido}"
    
    def __str__(self):
        return self.nombre_completo
    
    class Meta:
        db_table = 'autores'
        ordering = ['apellido', 'nombre']
        verbose_name = 'Autor'
        verbose_name_plural = 'Autores'


class Libro(models.Model):
    """Libros en la biblioteca"""
    titulo = models.CharField(max_length=300)
    isbn = models.CharField(max_length=13, unique=True)
    autor = models.ForeignKey(Autor, on_delete=models.CASCADE, related_name='libros')
    categoria = models.ForeignKey(Categoria, on_delete=models.SET_NULL, null=True, related_name='libros')
    editorial = models.CharField(max_length=200)
    fecha_publicacion = models.DateField()
    numero_paginas = models.IntegerField(validators=[MinValueValidator(1)])
    idioma = models.CharField(max_length=50, default='Español')
    descripcion = models.TextField()
    portada = models.ImageField(upload_to='libros/portadas/', null=True, blank=True)
    
    # Control de inventario
    cantidad_total = models.IntegerField(default=1, validators=[MinValueValidator(0)])
    cantidad_disponible = models.IntegerField(default=1, validators=[MinValueValidator(0)])
    
    # Información adicional
    precio = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
    ubicacion_fisica = models.CharField(max_length=100, blank=True, help_text="Ej: Estante A-3")
    
    # Metadata
    created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='libros_creados')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    is_active = models.BooleanField(default=True)
    
    @property
    def disponible(self):
        return self.cantidad_disponible > 0
    
    def __str__(self):
        return f"{self.titulo} - {self.autor.nombre_completo}"
    
    class Meta:
        db_table = 'libros'
        ordering = ['-created_at']
        verbose_name = 'Libro'
        verbose_name_plural = 'Libros'


class Prestamo(models.Model):
    """Préstamos de libros a usuarios"""
    ESTADOS = [
        ('activo', 'Activo'),
        ('devuelto', 'Devuelto'),
        ('vencido', 'Vencido'),
    ]
    
    usuario = models.ForeignKey(User, on_delete=models.CASCADE, related_name='prestamos')
    libro = models.ForeignKey(Libro, on_delete=models.CASCADE, related_name='prestamos')
    fecha_prestamo = models.DateTimeField(auto_now_add=True)
    fecha_devolucion_estimada = 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)
    
    # Multas
    multa = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    
    def __str__(self):
        return f"{self.usuario.username} - {self.libro.titulo}"
    
    class Meta:
        db_table = 'prestamos'
        ordering = ['-fecha_prestamo']
        verbose_name = 'Préstamo'
        verbose_name_plural = 'Préstamos'


class Resena(models.Model):
    """Reseñas de libros por usuarios"""
    libro = models.ForeignKey(Libro, on_delete=models.CASCADE, related_name='resenas')
    usuario = models.ForeignKey(User, on_delete=models.CASCADE, related_name='resenas')
    calificacion = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
    comentario = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    def __str__(self):
        return f"{self.usuario.username} - {self.libro.titulo} ({self.calificacion}★)"
    
    class Meta:
        db_table = 'resenas'
        ordering = ['-created_at']
        unique_together = ['libro', 'usuario']
        verbose_name = 'Reseña'
        verbose_name_plural = 'Reseñas'


class ChatMessage(models.Model):
    """Mensajes del chat en tiempo real"""
    room_name = models.CharField(max_length=255)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    message = models.TextField()
    timestamp = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return f'{self.user.username}: {self.message[:50]}'
    
    class Meta:
        db_table = 'chat_messages'
        ordering = ['timestamp']


class OAuthToken(models.Model):
    """Tokens de OAuth para autenticación externa"""
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    provider = models.CharField(max_length=50)  # 'google' o 'github'
    access_token = models.TextField()
    refresh_token = models.TextField(blank=True, null=True)
    token_type = models.CharField(max_length=50)
    expires_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return f'{self.user.username} - {self.provider}'
    
    class Meta:
        db_table = 'oauth_tokens'
        unique_together = ['user', 'provider']
serializers.py SERIALIZERS
📍 libros/serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
from .models import (
    UserProfile, Categoria, Autor, Libro, 
    Prestamo, Resena, ChatMessage, OAuthToken
)

class UserSerializer(serializers.ModelSerializer):
    """Serializer para el modelo User de Django"""
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'first_name', 'last_name', 'date_joined']
        read_only_fields = ['id', 'date_joined']


class UserProfileSerializer(serializers.ModelSerializer):
    """Serializer para el perfil extendido del usuario"""
    user = UserSerializer(read_only=True)
    
    class Meta:
        model = UserProfile
        fields = [
            'id', 'user', 'bio', 'avatar', 'phone', 'birth_date', 
            'google_id', 'github_id', 'created_at', 'updated_at'
        ]
        read_only_fields = ['id', 'created_at', 'updated_at']


class RegisterSerializer(serializers.ModelSerializer):
    """Serializer para registro de nuevos usuarios"""
    password = serializers.CharField(
        write_only=True, 
        required=True, 
        style={'input_type': 'password'},
        min_length=8
    )
    password2 = serializers.CharField(
        write_only=True, 
        required=True, 
        style={'input_type': 'password'}
    )
    
    class Meta:
        model = User
        fields = ['username', 'email', 'password', 'password2', 'first_name', 'last_name']
    
    def validate(self, attrs):
        if attrs['password'] != attrs['password2']:
            raise serializers.ValidationError({"password": "Las contraseñas no coinciden"})
        
        if User.objects.filter(email=attrs['email']).exists():
            raise serializers.ValidationError({"email": "Este email ya está registrado"})
        
        return attrs
    
    def create(self, validated_data):
        validated_data.pop('password2')
        user = User.objects.create_user(**validated_data)
        UserProfile.objects.create(user=user)
        return user


class CategoriaSerializer(serializers.ModelSerializer):
    """Serializer para categorías de libros"""
    total_libros = serializers.SerializerMethodField()
    
    class Meta:
        model = Categoria
        fields = ['id', 'nombre', 'descripcion', 'created_at', 'total_libros']
        read_only_fields = ['id', 'created_at']
    
    def get_total_libros(self, obj):
        return obj.libros.count()


class AutorSerializer(serializers.ModelSerializer):
    """Serializer para autores"""
    nombre_completo = serializers.ReadOnlyField()
    total_libros = serializers.SerializerMethodField()
    
    class Meta:
        model = Autor
        fields = [
            'id', 'nombre', 'apellido', 'nombre_completo', 'biografia',
            'fecha_nacimiento', 'pais', 'foto', 'created_at', 'total_libros'
        ]
        read_only_fields = ['id', 'created_at']
    
    def get_total_libros(self, obj):
        return obj.libros.count()


class LibroSerializer(serializers.ModelSerializer):
    """Serializer completo para libros"""
    autor = AutorSerializer(read_only=True)
    autor_id = serializers.PrimaryKeyRelatedField(
        queryset=Autor.objects.all(),
        source='autor',
        write_only=True
    )
    categoria = CategoriaSerializer(read_only=True)
    categoria_id = serializers.PrimaryKeyRelatedField(
        queryset=Categoria.objects.all(),
        source='categoria',
        write_only=True,
        allow_null=True
    )
    created_by = UserSerializer(read_only=True)
    disponible = serializers.ReadOnlyField()
    promedio_calificacion = serializers.SerializerMethodField()
    
    class Meta:
        model = Libro
        fields = [
            'id', 'titulo', 'isbn', 'autor', 'autor_id', 'categoria', 'categoria_id',
            'editorial', 'fecha_publicacion', 'numero_paginas', 'idioma', 'descripcion',
            'portada', 'cantidad_total', 'cantidad_disponible', 'precio', 
            'ubicacion_fisica', 'created_by', 'created_at', 'updated_at', 
            'is_active', 'disponible', 'promedio_calificacion'
        ]
        read_only_fields = ['id', 'created_at', 'updated_at', 'created_by']
    
    def get_promedio_calificacion(self, obj):
        resenas = obj.resenas.all()
        if resenas.exists():
            return round(sum(r.calificacion for r in resenas) / resenas.count(), 1)
        return None


class LibroListSerializer(serializers.ModelSerializer):
    """Serializer simplificado para listado de libros"""
    autor_nombre = serializers.CharField(source='autor.nombre_completo', read_only=True)
    categoria_nombre = serializers.CharField(source='categoria.nombre', read_only=True)
    disponible = serializers.ReadOnlyField()
    
    class Meta:
        model = Libro
        fields = [
            'id', 'titulo', 'isbn', 'autor_nombre', 'categoria_nombre',
            'editorial', 'portada', 'cantidad_disponible', 'disponible'
        ]


class PrestamoSerializer(serializers.ModelSerializer):
    """Serializer para préstamos de libros"""
    usuario = UserSerializer(read_only=True)
    libro = LibroSerializer(read_only=True)
    libro_id = serializers.PrimaryKeyRelatedField(
        queryset=Libro.objects.all(),
        source='libro',
        write_only=True
    )
    dias_restantes = serializers.SerializerMethodField()
    
    class Meta:
        model = Prestamo
        fields = [
            'id', 'usuario', 'libro', 'libro_id', 'fecha_prestamo',
            'fecha_devolucion_estimada', 'fecha_devolucion_real', 
            'estado', 'notas', 'multa', 'dias_restantes'
        ]
        read_only_fields = ['id', 'fecha_prestamo', 'usuario']
    
    def get_dias_restantes(self, obj):
        if obj.estado == 'devuelto':
            return 0
        from datetime import date
        delta = obj.fecha_devolucion_estimada - date.today()
        return delta.days
    
    def validate_libro_id(self, libro):
        if not libro.disponible:
            raise serializers.ValidationError("Este libro no está disponible")
        return libro
    
    def create(self, validated_data):
        # Reducir cantidad disponible
        libro = validated_data['libro']
        libro.cantidad_disponible -= 1
        libro.save()
        return super().create(validated_data)


class ResenaSerializer(serializers.ModelSerializer):
    """Serializer para reseñas de libros"""
    usuario = UserSerializer(read_only=True)
    libro_titulo = serializers.CharField(source='libro.titulo', read_only=True)
    
    class Meta:
        model = Resena
        fields = [
            'id', 'libro', 'libro_titulo', 'usuario', 'calificacion', 
            'comentario', 'created_at', 'updated_at'
        ]
        read_only_fields = ['id', 'usuario', 'created_at', 'updated_at']
    
    def validate_calificacion(self, value):
        if value < 1 or value > 5:
            raise serializers.ValidationError("La calificación debe estar entre 1 y 5")
        return value


class ChatMessageSerializer(serializers.ModelSerializer):
    """Serializer para mensajes de chat"""
    username = serializers.CharField(source='user.username', read_only=True)
    
    class Meta:
        model = ChatMessage
        fields = ['id', 'room_name', 'user', 'username', 'message', 'timestamp']
        read_only_fields = ['id', 'user', 'timestamp']


class OAuthTokenSerializer(serializers.ModelSerializer):
    """Serializer para tokens OAuth"""
    class Meta:
        model = OAuthToken
        fields = ['id', 'provider', 'access_token', 'token_type', 'expires_at', 'created_at']
        read_only_fields = ['id', 'created_at']
        extra_kwargs = {
            'access_token': {'write_only': True},
            'refresh_token': {'write_only': True}
        }
views.py VISTAS & API
📍 libros/views.py
from rest_framework import viewsets, status, generics, filters
from rest_framework.decorators import api_view, permission_classes, action
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
from django.shortcuts import render, redirect
from django.views.decorators.csrf import csrf_exempt
from django.db.models import Q, Avg
from datetime import datetime, timedelta, date
from decouple import config
import requests

from .models import (
    UserProfile, Categoria, Autor, Libro, 
    Prestamo, Resena, ChatMessage, OAuthToken
)
from .serializers import (
    UserSerializer, UserProfileSerializer, RegisterSerializer,
    CategoriaSerializer, AutorSerializer, LibroSerializer, LibroListSerializer,
    PrestamoSerializer, ResenaSerializer, ChatMessageSerializer, OAuthTokenSerializer
)


# ============= VISTAS DE TEMPLATES =============

def index(request):
    """Página principal"""
    return render(request, 'index.html')

def login_view(request):
    """Página de login"""
    return render(request, 'auth/login.html')

def register_view(request):
    """Página de registro"""
    return render(request, 'auth/register.html')

def dashboard_view(request):
    """Dashboard principal (requiere autenticación)"""
    return render(request, 'dashboard.html')

def chat_view(request):
    """Página de chat en tiempo real"""
    return render(request, 'chat.html')

def libros_view(request):
    """Página de catálogo de libros"""
    return render(request, 'libros/catalogo.html')

def prestamos_view(request):
    """Página de préstamos del usuario"""
    return render(request, 'prestamos/mis_prestamos.html')


# ============= AUTENTICACIÓN =============

@api_view(['POST'])
@permission_classes([AllowAny])
def register(request):
    """Registrar un nuevo usuario"""
    serializer = RegisterSerializer(data=request.data)
    if serializer.is_valid():
        user = serializer.save()
        refresh = RefreshToken.for_user(user)
        
        return Response({
            'user': UserSerializer(user).data,
            'tokens': {
                'refresh': str(refresh),
                'access': str(refresh.access_token),
            },
            'message': 'Usuario registrado exitosamente'
        }, status=status.HTTP_201_CREATED)
    
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


@api_view(['POST'])
@permission_classes([AllowAny])
def login(request):
    """Login con username/email y password"""
    username = request.data.get('username')
    password = request.data.get('password')
    
    if not username or not password:
        return Response({
            'error': 'Username y password son requeridos'
        }, status=status.HTTP_400_BAD_REQUEST)
    
    # Intentar login con username
    user = authenticate(username=username, password=password)
    
    # Si falla, intentar con email
    if not user:
        try:
            user_obj = User.objects.get(email=username)
            user = authenticate(username=user_obj.username, password=password)
        except User.DoesNotExist:
            pass
    
    if user:
        refresh = RefreshToken.for_user(user)
        
        return Response({
            'user': UserSerializer(user).data,
            'tokens': {
                'refresh': str(refresh),
                'access': str(refresh.access_token),
            },
            'message': 'Login exitoso'
        })
    
    return Response({
        'error': 'Credenciales inválidas'
    }, status=status.HTTP_401_UNAUTHORIZED)


@api_view(['POST'])
@permission_classes([IsAuthenticated])
def logout(request):
    """Cerrar sesión (blacklist del token)"""
    try:
        refresh_token = request.data.get('refresh')
        token = RefreshToken(refresh_token)
        token.blacklist()
        
        return Response({
            'message': 'Logout exitoso'
        }, status=status.HTTP_200_OK)
    except Exception as e:
        return Response({
            'error': str(e)
        }, status=status.HTTP_400_BAD_REQUEST)


@api_view(['GET'])
@permission_classes([IsAuthenticated])
def profile(request):
    """Obtener perfil del usuario autenticado"""
    try:
        profile = request.user.profile
        return Response(UserProfileSerializer(profile).data)
    except UserProfile.DoesNotExist:
        # Crear perfil si no existe
        profile = UserProfile.objects.create(user=request.user)
        return Response(UserProfileSerializer(profile).data)


# ============= OAUTH 2.0 =============

@api_view(['POST'])
@permission_classes([AllowAny])
def google_login(request):
    """Autenticación con Google OAuth"""
    token = request.data.get('access_token')
    
    if not token:
        return Response({'error': 'Token requerido'}, status=status.HTTP_400_BAD_REQUEST)
    
    try:
        # Verificar token con Google
        response = requests.get(
            f'https://www.googleapis.com/oauth2/v1/userinfo?access_token={token}'
        )
        
        if response.status_code != 200:
            return Response({'error': 'Token inválido'}, status=status.HTTP_401_UNAUTHORIZED)
        
        user_data = response.json()
        email = user_data.get('email')
        google_id = user_data.get('id')
        
        # Buscar o crear usuario
        user, created = User.objects.get_or_create(
            email=email,
            defaults={
                'username': email.split('@')[0],
                'first_name': user_data.get('given_name', ''),
                'last_name': user_data.get('family_name', ''),
            }
        )
        
        # Actualizar o crear perfil
        profile, _ = UserProfile.objects.get_or_create(user=user)
        profile.google_id = google_id
        profile.save()
        
        # Guardar token OAuth
        OAuthToken.objects.update_or_create(
            user=user,
            provider='google',
            defaults={
                'access_token': token,
                'token_type': 'Bearer',
            }
        )
        
        # Generar JWT
        refresh = RefreshToken.for_user(user)
        
        return Response({
            'user': UserSerializer(user).data,
            'tokens': {
                'refresh': str(refresh),
                'access': str(refresh.access_token),
            },
            'is_new_user': created
        })
        
    except Exception as e:
        return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


# ============= VIEWSETS DE LIBROS =============

class CategoriaViewSet(viewsets.ModelViewSet):
    """CRUD de categorías"""
    queryset = Categoria.objects.all()
    serializer_class = CategoriaSerializer
    permission_classes = [IsAuthenticated]
    
    @action(detail=True, methods=['get'])
    def libros(self, request, pk=None):
        """Obtener todos los libros de una categoría"""
        categoria = self.get_object()
        libros = categoria.libros.filter(is_active=True)
        serializer = LibroListSerializer(libros, many=True)
        return Response(serializer.data)


class AutorViewSet(viewsets.ModelViewSet):
    """CRUD de autores"""
    queryset = Autor.objects.all()
    serializer_class = AutorSerializer
    permission_classes = [IsAuthenticated]
    filter_backends = [filters.SearchFilter, filters.OrderingFilter]
    search_fields = ['nombre', 'apellido', 'pais']
    ordering_fields = ['apellido', 'created_at']
    
    @action(detail=True, methods=['get'])
    def libros(self, request, pk=None):
        """Obtener todos los libros de un autor"""
        autor = self.get_object()
        libros = autor.libros.filter(is_active=True)
        serializer = LibroListSerializer(libros, many=True)
        return Response(serializer.data)


class LibroViewSet(viewsets.ModelViewSet):
    """CRUD de libros con búsqueda avanzada"""
    queryset = Libro.objects.filter(is_active=True).select_related('autor', 'categoria')
    permission_classes = [IsAuthenticated]
    filter_backends = [filters.SearchFilter, filters.OrderingFilter]
    search_fields = ['titulo', 'isbn', 'autor__nombre', 'autor__apellido', 'editorial']
    ordering_fields = ['titulo', 'fecha_publicacion', 'created_at']
    
    def get_serializer_class(self):
        if self.action == 'list':
            return LibroListSerializer
        return LibroSerializer
    
    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user)
    
    @action(detail=True, methods=['get'])
    def resenas(self, request, pk=None):
        """Obtener reseñas de un libro"""
        libro = self.get_object()
        resenas = libro.resenas.all()
        serializer = ResenaSerializer(resenas, many=True)
        return Response(serializer.data)
    
    @action(detail=False, methods=['get'])
    def disponibles(self, request):
        """Obtener solo libros disponibles"""
        libros = self.queryset.filter(cantidad_disponible__gt=0)
        serializer = self.get_serializer(libros, many=True)
        return Response(serializer.data)
    
    @action(detail=False, methods=['get'])
    def buscar(self, request):
        """Búsqueda avanzada de libros"""
        queryset = self.queryset
        
        # Filtros
        titulo = request.query_params.get('titulo')
        autor = request.query_params.get('autor')
        categoria = request.query_params.get('categoria')
        isbn = request.query_params.get('isbn')
        disponible = request.query_params.get('disponible')
        
        if titulo:
            queryset = queryset.filter(titulo__icontains=titulo)
        if autor:
            queryset = queryset.filter(
                Q(autor__nombre__icontains=autor) | 
                Q(autor__apellido__icontains=autor)
            )
        if categoria:
            queryset = queryset.filter(categoria__nombre__icontains=categoria)
        if isbn:
            queryset = queryset.filter(isbn=isbn)
        if disponible == 'true':
            queryset = queryset.filter(cantidad_disponible__gt=0)
        
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)


class PrestamoViewSet(viewsets.ModelViewSet):
    """CRUD de préstamos"""
    serializer_class = PrestamoSerializer
    permission_classes = [IsAuthenticated]
    
    def get_queryset(self):
        # Usuarios normales solo ven sus préstamos
        if not self.request.user.is_staff:
            return Prestamo.objects.filter(usuario=self.request.user)
        # Staff ve todos
        return Prestamo.objects.all()
    
    def perform_create(self, serializer):
        serializer.save(usuario=self.request.user)
    
    @action(detail=True, methods=['post'])
    def devolver(self, request, pk=None):
        """Marcar préstamo como devuelto"""
        prestamo = self.get_object()
        
        if prestamo.usuario != request.user and not request.user.is_staff:
            return Response(
                {'error': 'No tienes permiso para devolver este préstamo'},
                status=status.HTTP_403_FORBIDDEN
            )
        
        if prestamo.estado == 'devuelto':
            return Response(
                {'error': 'Este préstamo ya fue devuelto'},
                status=status.HTTP_400_BAD_REQUEST
            )
        
        # Actualizar préstamo
        prestamo.fecha_devolucion_real = datetime.now()
        prestamo.estado = 'devuelto'
        
        # Calcular multa si está vencido
        if date.today() > prestamo.fecha_devolucion_estimada:
            dias_retraso = (date.today() - prestamo.fecha_devolucion_estimada).days
            prestamo.multa = dias_retraso * 10  # $10 por día
        
        prestamo.save()
        
        # Incrementar cantidad disponible del libro
        libro = prestamo.libro
        libro.cantidad_disponible += 1
        libro.save()
        
        return Response(PrestamoSerializer(prestamo).data)
    
    @action(detail=False, methods=['get'])
    def activos(self, request):
        """Obtener préstamos activos del usuario"""
        prestamos = self.get_queryset().filter(estado='activo')
        serializer = self.get_serializer(prestamos, many=True)
        return Response(serializer.data)
    
    @action(detail=False, methods=['get'])
    def vencidos(self, request):
        """Obtener préstamos vencidos"""
        prestamos = self.get_queryset().filter(
            estado='activo',
            fecha_devolucion_estimada__lt=date.today()
        )
        for prestamo in prestamos:
            prestamo.estado = 'vencido'
            prestamo.save()
        
        serializer = self.get_serializer(prestamos, many=True)
        return Response(serializer.data)


class ResenaViewSet(viewsets.ModelViewSet):
    """CRUD de reseñas"""
    queryset = Resena.objects.all()
    serializer_class = ResenaSerializer
    permission_classes = [IsAuthenticated]
    
    def get_queryset(self):
        queryset = super().get_queryset()
        libro_id = self.request.query_params.get('libro')
        if libro_id:
            queryset = queryset.filter(libro_id=libro_id)
        return queryset
    
    def perform_create(self, serializer):
        serializer.save(usuario=self.request.user)


class ChatMessageViewSet(viewsets.ReadOnlyModelViewSet):
    """Mensajes de chat (solo lectura, se crean vía WebSocket)"""
    queryset = ChatMessage.objects.all()
    serializer_class = ChatMessageSerializer
    permission_classes = [IsAuthenticated]
    
    def get_queryset(self):
        room = self.request.query_params.get('room')
        if room:
            return ChatMessage.objects.filter(room_name=room)
        return ChatMessage.objects.all()


# ============= INTEGRACIÓN GOOGLE BOOKS API =============

@api_view(['GET'])
@permission_classes([IsAuthenticated])
def buscar_google_books(request):
    """Buscar libros en Google Books API"""
    query = request.query_params.get('q')
    
    if not query:
        return Response({'error': 'Parámetro "q" requerido'}, status=status.HTTP_400_BAD_REQUEST)
    
    try:
        response = requests.get(
            'https://www.googleapis.com/books/v1/volumes',
            params={'q': query, 'maxResults': 10}
        )
        
        if response.status_code == 200:
            data = response.json()
            
            # Formatear resultados
            libros = []
            for item in data.get('items', []):
                volume_info = item.get('volumeInfo', {})
                libros.append({
                    'titulo': volume_info.get('title'),
                    'autores': volume_info.get('authors', []),
                    'editorial': volume_info.get('publisher'),
                    'fecha_publicacion': volume_info.get('publishedDate'),
                    'descripcion': volume_info.get('description'),
                    'isbn': next((id['identifier'] for id in volume_info.get('industryIdentifiers', []) 
                                if id['type'] == 'ISBN_13'), None),
                    'paginas': volume_info.get('pageCount'),
                    'idioma': volume_info.get('language'),
                    'portada': volume_info.get('imageLinks', {}).get('thumbnail'),
                })
            
            return Response({'resultados': libros})
        else:
            return Response({'error': 'Error al consultar Google Books'}, 
                          status=status.HTTP_500_INTERNAL_SERVER_ERROR)
    
    except Exception as e:
        return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


@api_view(['POST'])
@permission_classes([IsAuthenticated])
def importar_de_google_books(request):
    """Importar un libro desde Google Books"""
    if not request.user.is_staff:
        return Response({'error': 'Solo administradores pueden importar libros'},
                       status=status.HTTP_403_FORBIDDEN)
    
    datos = request.data
    
    try:
        # Buscar o crear autor
        nombre_autor = datos.get('autores', ['Desconocido'])[0]
        partes = nombre_autor.split()
        nombre = partes[0] if partes else 'Desconocido'
        apellido = ' '.join(partes[1:]) if len(partes) > 1 else ''
        
        autor, _ = Autor.objects.get_or_create(
            nombre=nombre,
            apellido=apellido
        )
        
        # Crear libro
        libro = Libro.objects.create(
            titulo=datos.get('titulo'),
            isbn=datos.get('isbn', ''),
            autor=autor,
            editorial=datos.get('editorial', ''),
            fecha_publicacion=datos.get('fecha_publicacion', date.today()),
            numero_paginas=datos.get('paginas', 0),
            idioma=datos.get('idioma', 'es'),
            descripcion=datos.get('descripcion', ''),
            created_by=request.user
        )
        
        return Response(LibroSerializer(libro).data, status=status.HTTP_201_CREATED)
    
    except Exception as e:
        return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
def home_view(request):
    """Vista principal"""
    return render(request, 'base.html')


def login_view(request):
    """Vista de login"""
    return render(request, 'login.html')


def register_view(request):
    """Vista de registro"""
    return render(request, 'register.html')


def dashboard_view(request):
    """Vista de dashboard"""
    return render(request, 'dashboard.html')


def chat_view(request, room_name='general'):
    """Vista de chat WebSocket"""
    return render(request, 'chat.html', {'room_name': room_name})


def oauth_connect_view(request):
    """Vista para conectar OAuth"""
    return render(request, 'oauth_connect.html')


# ============= API ENDPOINTS =============

@api_view(['POST'])
@permission_classes([AllowAny])
def register_api(request):
    """Registro de nuevos usuarios"""
    serializer = RegisterSerializer(data=request.data)
    if serializer.is_valid():
        user = serializer.save()
        refresh = RefreshToken.for_user(user)
        return Response({
            'message': 'Usuario creado exitosamente',
            'user': UserSerializer(user).data,
            'tokens': {
                'refresh': str(refresh),
                'access': str(refresh.access_token),
            }
        }, status=status.HTTP_201_CREATED)
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


@api_view(['POST'])
@permission_classes([AllowAny])
def login_api(request):
    """Login con JWT"""
    username = request.data.get('username')
    password = request.data.get('password')
    
    user = authenticate(username=username, password=password)
    
    if user is not None:
        refresh = RefreshToken.for_user(user)
        return Response({
            'message': 'Login exitoso',
            'user': UserSerializer(user).data,
            'tokens': {
                'refresh': str(refresh),
                'access': str(refresh.access_token),
            }
        })
    return Response({'error': 'Credenciales inválidas'}, status=status.HTTP_401_UNAUTHORIZED)


@api_view(['POST'])
@permission_classes([IsAuthenticated])
def logout_api(request):
    """Logout - Invalidar token"""
    try:
        refresh_token = request.data.get('refresh')
        token = RefreshToken(refresh_token)
        token.blacklist()
        return Response({'message': 'Logout exitoso'})
    except Exception as e:
        return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)


@api_view(['GET'])
@permission_classes([IsAuthenticated])
def profile_api(request):
    """Obtener perfil del usuario autenticado"""
    profile = UserProfile.objects.get(user=request.user)
    return Response(UserProfileSerializer(profile).data)


# ============= OAUTH 2.0 ENDPOINTS =============

@api_view(['GET'])
@permission_classes([AllowAny])
def google_login(request):
    """Iniciar OAuth con Google"""
    client_id = config('GOOGLE_OAUTH_CLIENT_ID')
    redirect_uri = config('GOOGLE_OAUTH_REDIRECT_URI')
    scope = 'openid email profile'
    
    auth_url = (
        f'https://accounts.google.com/o/oauth2/v2/auth?'
        f'client_id={client_id}&'
        f'redirect_uri={redirect_uri}&'
        f'response_type=code&'
        f'scope={scope}'
    )
    
    return redirect(auth_url)


@api_view(['GET'])
@permission_classes([AllowAny])
def google_callback(request):
    """Callback de Google OAuth"""
    code = request.GET.get('code')
    
    if not code:
        return Response({'error': 'No se recibió código'}, status=status.HTTP_400_BAD_REQUEST)
    
    # Intercambiar código por token
    token_url = 'https://oauth2.googleapis.com/token'
    token_data = {
        'code': code,
        'client_id': config('GOOGLE_OAUTH_CLIENT_ID'),
        'client_secret': config('GOOGLE_OAUTH_CLIENT_SECRET'),
        'redirect_uri': config('GOOGLE_OAUTH_REDIRECT_URI'),
        'grant_type': 'authorization_code'
    }
    
    token_response = requests.post(token_url, data=token_data)
    token_json = token_response.json()
    
    access_token = token_json.get('access_token')
    
    # Obtener información del usuario
    user_info_url = 'https://www.googleapis.com/oauth2/v2/userinfo'
    headers = {'Authorization': f'Bearer {access_token}'}
    user_info_response = requests.get(user_info_url, headers=headers)
    user_info = user_info_response.json()
    
    # Crear o actualizar usuario
    email = user_info.get('email')
    google_id = user_info.get('id')
    
    user, created = User.objects.get_or_create(
        email=email,
        defaults={
            'username': email.split('@')[0],
            'first_name': user_info.get('given_name', ''),
            'last_name': user_info.get('family_name', '')
        }
    )
    
    # Actualizar perfil
    profile, _ = UserProfile.objects.get_or_create(user=user)
    profile.google_id = google_id
    profile.save()
    
    # Guardar token OAuth
    OAuthToken.objects.update_or_create(
        user=user,
        provider='google',
        defaults={
            'access_token': access_token,
            'refresh_token': token_json.get('refresh_token', ''),
            'token_type': token_json.get('token_type', 'Bearer'),
            'expires_at': datetime.now() + timedelta(seconds=token_json.get('expires_in', 3600))
        }
    )
    
    # Generar JWT para la aplicación
    refresh = RefreshToken.for_user(user)
    
    return Response({
        'message': 'Login con Google exitoso',
        'user': UserSerializer(user).data,
        'tokens': {
            'refresh': str(refresh),
            'access': str(refresh.access_token),
        }
    })


@api_view(['GET'])
@permission_classes([AllowAny])
def github_login(request):
    """Iniciar OAuth con GitHub"""
    client_id = config('GITHUB_OAUTH_CLIENT_ID')
    redirect_uri = config('GITHUB_OAUTH_REDIRECT_URI')
    scope = 'user:email'
    
    auth_url = (
        f'https://github.com/login/oauth/authorize?'
        f'client_id={client_id}&'
        f'redirect_uri={redirect_uri}&'
        f'scope={scope}'
    )
    
    return redirect(auth_url)


@api_view(['GET'])
@permission_classes([AllowAny])
def github_callback(request):
    """Callback de GitHub OAuth"""
    code = request.GET.get('code')
    
    if not code:
        return Response({'error': 'No se recibió código'}, status=status.HTTP_400_BAD_REQUEST)
    
    # Intercambiar código por token
    token_url = 'https://github.com/login/oauth/access_token'
    token_data = {
        'client_id': config('GITHUB_OAUTH_CLIENT_ID'),
        'client_secret': config('GITHUB_OAUTH_CLIENT_SECRET'),
        'code': code,
        'redirect_uri': config('GITHUB_OAUTH_REDIRECT_URI')
    }
    headers = {'Accept': 'application/json'}
    
    token_response = requests.post(token_url, data=token_data, headers=headers)
    token_json = token_response.json()
    
    access_token = token_json.get('access_token')
    
    # Obtener información del usuario
    user_info_url = 'https://api.github.com/user'
    headers = {'Authorization': f'token {access_token}'}
    user_info_response = requests.get(user_info_url, headers=headers)
    user_info = user_info_response.json()
    
    # Obtener email
    email_url = 'https://api.github.com/user/emails'
    email_response = requests.get(email_url, headers=headers)
    emails = email_response.json()
    primary_email = next((e['email'] for e in emails if e['primary']), None)
    
    github_id = str(user_info.get('id'))
    username = user_info.get('login')
    
    # Crear o actualizar usuario
    user, created = User.objects.get_or_create(
        username=username,
        defaults={
            'email': primary_email or f'{username}@github.com',
            'first_name': user_info.get('name', '').split()[0] if user_info.get('name') else ''
        }
    )
    
    # Actualizar perfil
    profile, _ = UserProfile.objects.get_or_create(user=user)
    profile.github_id = github_id
    profile.save()
    
    # Guardar token OAuth
    OAuthToken.objects.update_or_create(
        user=user,
        provider='github',
        defaults={
            'access_token': access_token,
            'token_type': token_json.get('token_type', 'bearer')
        }
    )
    
    # Generar JWT
    refresh = RefreshToken.for_user(user)
    
    return Response({
        'message': 'Login con GitHub exitoso',
        'user': UserSerializer(user).data,
        'tokens': {
            'refresh': str(refresh),
            'access': str(refresh.access_token),
        }
    })



                    
                    
urls.py (Libros App) RUTAS API
📍 libros/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from rest_framework_simplejwt.views import TokenRefreshView
from . import views

# Router para ViewSets
router = DefaultRouter()
router.register('categorias', views.CategoriaViewSet, basename='categoria')
router.register('autores', views.AutorViewSet, basename='autor')
router.register('libros', views.LibroViewSet, basename='libro')
router.register('prestamos', views.PrestamoViewSet, basename='prestamo')
router.register('resenas', views.ResenaViewSet, basename='resena')
router.register('messages', views.ChatMessageViewSet, basename='message')

urlpatterns = [
    # ============= VISTAS DE TEMPLATES =============
    path('', views.index, name='index'),
    path('login/', views.login_view, name='login'),
    path('register/', views.register_view, name='register'),
    path('dashboard/', views.dashboard_view, name='dashboard'),
    path('chat/', views.chat_view, name='chat'),
    path('libros/', views.libros_view, name='libros'),
    path('prestamos/', views.prestamos_view, name='prestamos'),
    
    # ============= AUTENTICACIÓN API =============
    path('api/auth/register/', views.register, name='api_register'),
    path('api/auth/login/', views.login, name='api_login'),
    path('api/auth/logout/', views.logout, name='api_logout'),
    path('api/auth/profile/', views.profile, name='api_profile'),
    path('api/auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    
    # ============= OAUTH 2.0 =============
    path('api/auth/google/', views.google_login, name='google_login'),
    
    # ============= GOOGLE BOOKS API =============
    path('api/google-books/buscar/', views.buscar_google_books, name='buscar_google_books'),
    path('api/google-books/importar/', views.importar_de_google_books, name='importar_google_books'),
    
    # ============= ROUTER API (CRUD) =============
    path('api/', include(router.urls)),
]
consumers.py WEBSOCKETS
📍 libros/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.contrib.auth.models import User
from .models import ChatMessage

class ChatConsumer(AsyncWebsocketConsumer):
    """Consumer para chat en tiempo real"""
    async def connect(self):
        self.room_name = self.scope['url_route']['kwargs'].get('room_name', 'general')
        self.room_group_name = f'chat_{self.room_name}'
        
        # Unirse al grupo de la sala
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )
        
        await self.accept()
        
        # Enviar mensaje de bienvenida
        await self.send(text_data=json.dumps({
            'type': 'connection_established',
            'message': f'Conectado al chat {self.room_name}'
        }))
    
    async def disconnect(self, close_code):
        # Leave room group
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )
    
    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json.get('message')
        username = text_data_json.get('username', 'Anonymous')
        
        # Guardar mensaje en base de datos
        await self.save_message(username, message)
        
        # Send message to room group
        await self.channel_layer.group_send(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message,
                'username': username
            }
        )
    
    async def chat_message(self, event):
        message = event['message']
        username = event['username']
        
        # Send message to WebSocket
        await self.send(text_data=json.dumps({
            'type': 'chat_message',
            'message': message,
            'username': username
        }))
    
    @database_sync_to_async
    def save_message(self, username, message):
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            user = None
        
        if user:
            ChatMessage.objects.create(
                room_name=self.room_name,
                user=user,
                message=message
            )
routing.py WEBSOCKET ROUTING
📍 libros/routing.py
from django.urls import re_path
from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
    re_path(r'ws/notificaciones/$', consumers.NotificacionConsumer.as_asgi()),
]
admin.py DJANGO ADMIN
📍 libros/admin.py
from django.contrib import admin
from .models import (
    UserProfile, Categoria, Autor, Libro, 
    Prestamo, Resena, ChatMessage, OAuthToken
)

@admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
    list_display = ['user', 'phone', 'created_at']
    search_fields = ['user__username', 'user__email']
    list_filter = ['created_at']

@admin.register(Categoria)
class CategoriaAdmin(admin.ModelAdmin):
    list_display = ['nombre', 'created_at']
    search_fields = ['nombre', 'descripcion']

@admin.register(Autor)
class AutorAdmin(admin.ModelAdmin):
    list_display = ['nombre_completo', 'pais', 'created_at']
    search_fields = ['nombre', 'apellido', 'pais']
    list_filter = ['pais', 'created_at']

@admin.register(Libro)
class LibroAdmin(admin.ModelAdmin):
    list_display = ['titulo', 'autor', 'categoria', 'isbn', 'cantidad_disponible', 'is_active']
    list_filter = ['categoria', 'is_active', 'created_at']
    search_fields = ['titulo', 'isbn', 'autor__nombre', 'autor__apellido']
    readonly_fields = ['created_at', 'updated_at']
    
    fieldsets = (
        ('Información Básica', {
            'fields': ('titulo', 'autor', 'categoria', 'isbn')
        }),
        ('Detalles', {
            'fields': ('editorial', 'fecha_publicacion', 'numero_paginas', 'idioma', 'descripcion')
        }),
        ('Inventario', {
            'fields': ('cantidad_total', 'cantidad_disponible', 'ubicacion_fisica', 'precio')
        }),
        ('Imagen', {
            'fields': ('portada',)
        }),
        ('Metadata', {
            'fields': ('is_active', 'created_by', 'created_at', 'updated_at'),
            'classes': ('collapse',)
        }),
    )

@admin.register(Prestamo)
class PrestamoAdmin(admin.ModelAdmin):
    list_display = ['usuario', 'libro', 'fecha_prestamo', 'fecha_devolucion_estimada', 'estado', 'multa']
    list_filter = ['estado', 'fecha_prestamo', 'fecha_devolucion_estimada']
    search_fields = ['usuario__username', 'libro__titulo']
    readonly_fields = ['fecha_prestamo']
    
    actions = ['marcar_como_devuelto']
    
    def marcar_como_devuelto(self, request, queryset):
        from datetime import datetime
        for prestamo in queryset:
            if prestamo.estado != 'devuelto':
                prestamo.fecha_devolucion_real = datetime.now()
                prestamo.estado = 'devuelto'
                prestamo.libro.cantidad_disponible += 1
                prestamo.libro.save()
                prestamo.save()
        self.message_user(request, f"{queryset.count()} préstamos marcados como devueltos")
    marcar_como_devuelto.short_description = "Marcar seleccionados como devueltos"

@admin.register(Resena)
class ResenaAdmin(admin.ModelAdmin):
    list_display = ['libro', 'usuario', 'calificacion', 'created_at']
    list_filter = ['calificacion', 'created_at']
    search_fields = ['libro__titulo', 'usuario__username', 'comentario']

@admin.register(ChatMessage)
class ChatMessageAdmin(admin.ModelAdmin):
    list_display = ['user', 'room_name', 'message', 'timestamp']
    list_filter = ['room_name', 'timestamp']
    search_fields = ['user__username', 'message']

@admin.register(OAuthToken)
class OAuthTokenAdmin(admin.ModelAdmin):
    list_display = ['user', 'provider', 'created_at']
    list_filter = ['provider']
    search_fields = ['user__username']

🌐 Templates HTML Completos

base.html TEMPLATE BASE
📍 templates/base.html
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Django REST + JWT + OAuth{% endblock %}</title>
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
    {% block extra_css %}{% endblock %}
</head>
<body>
    <nav class="navbar">
        <div class="container">
            <a href="/" class="logo">🚀 Mi Proyecto</a>
            <ul class="nav-menu">
                <li><a href="/">Inicio</a></li>
                <li id="nav-dashboard"><a href="/dashboard/">Dashboard</a></li>
                <li id="nav-chat"><a href="/chat/general/">Chat</a></li>
                <li id="nav-login"><a href="/login/">Login</a></li>
                <li id="nav-register"><a href="/register/">Registro</a></li>
                <li id="nav-logout"><a href="#" onclick="logout()">Logout</a></li>
            </ul>
        </div>
    </nav>

    <main>
        {% block content %}
        <div class="hero">
            <h1>Bienvenido a la Aplicación</h1>
            <p>Django REST + JWT + OAuth + WebSockets</p>
        </div>
        {% endblock %}
    </main>

    <footer>
        <p>© 2024 Mi Proyecto Django</p>
    </footer>

    <script>
        // Actualizar navegación según autenticación
        const token = localStorage.getItem('access_token');
        if (token) {
            document.getElementById('nav-login').style.display = 'none';
            document.getElementById('nav-register').style.display = 'none';
        } else {
            document.getElementById('nav-dashboard').style.display = 'none';
            document.getElementById('nav-chat').style.display = 'none';
            document.getElementById('nav-logout').style.display = 'none';
        }

        function logout() {
            localStorage.removeItem('access_token');
            localStorage.removeItem('refresh_token');
            window.location.href = '/login/';
        }
    </script>
    {% block extra_js %}{% endblock %}
</body>
</html>
index.html PÁGINA INICIO
📍 templates/index.html
{% extends 'base.html' %}
{% load static %}

{% block title %}Inicio - Sistema de Biblioteca Digital{% endblock %}

{% block content %}
<div class="hero">
    <div class="container">
        <h1>📚 Sistema de Biblioteca Digital</h1>
        <p>Gestión completa de préstamos y catálogo de libros</p>
        <p style="margin-top: 30px;">
            <a href="/login/" class="btn btn-primary" style="display: inline-block; margin-right: 10px;">Iniciar Sesión</a>
            <a href="/register/" class="btn btn-primary" style="display: inline-block; background: #764ba2;">Registrarse</a>
        </p>
    </div>
</div>

<div class="container" style="margin-top: 60px;">
    <div class="dashboard-grid">
        <div class="card">
            <h3>🔐 Autenticación JWT</h3>
            <p>Sistema seguro de autenticación con JSON Web Tokens</p>
            <ul>
                <li>Access tokens de corta duración</li>
                <li>Refresh tokens para renovación</li>
                <li>Autenticación stateless</li>
            </ul>
        </div>

        <div class="card">
            <h3>🔗 OAuth 2.0</h3>
            <p>Login con servicios externos populares</p>
            <ul>
                <li>Integración con Google</li>
                <li>Integración con GitHub</li>
                <li>Flujo OAuth completo</li>
            </ul>
        </div>

        <div class="card">
            <h3>💬 WebSockets</h3>
            <p>Comunicación en tiempo real con Django Channels</p>
            <ul>
                <li>Chat en tiempo real</li>
                <li>Notificaciones push</li>
                <li>Conexión bidireccional</li>
            </ul>
        </div>

        <div class="card">
            <h3>📊 GraphQL</h3>
            <p>API flexible con consultas personalizadas</p>
            <ul>
                <li>Queries optimizadas</li>
                <li>Mutations para modificar datos</li>
                <li>Sin over-fetching</li>
            </ul>
        </div>

        <div class="card">
            <h3>🛡️ Seguridad</h3>
            <p>Protección avanzada contra ataques</p>
            <ul>
                <li>Rate limiting</li>
                <li>Validaciones anti-XSS y SQL Injection</li>
                <li>CORS y CSRF configurados</li>
            </ul>
        </div>

        <div class="card">
            <h3>📚 Gestión de Biblioteca</h3>
            <p>Sistema completo de biblioteca digital</p>
            <ul>
                <li>Catálogo de libros</li>
                <li>Sistema de préstamos</li>
                <li>Reseñas y calificaciones</li>
            </ul>
        </div>
    </div>
</div>

<div class="container" style="margin-top: 60px; text-align: center; color: white;">
    <h2>🚀 Tecnologías Utilizadas</h2>
    <div style="margin-top: 30px; display: flex; justify-content: center; gap: 20px; flex-wrap: wrap;">
        <div style="background: rgba(255,255,255,0.1); padding: 15px 25px; border-radius: 10px;">Django 4.2</div>
        <div style="background: rgba(255,255,255,0.1); padding: 15px 25px; border-radius: 10px;">Django REST Framework</div>
        <div style="background: rgba(255,255,255,0.1); padding: 15px 25px; border-radius: 10px;">SimpleJWT</div>
        <div style="background: rgba(255,255,255,0.1); padding: 15px 25px; border-radius: 10px;">OAuth Toolkit</div>
        <div style="background: rgba(255,255,255,0.1); padding: 15px 25px; border-radius: 10px;">Django Channels</div>
        <div style="background: rgba(255,255,255,0.1); padding: 15px 25px; border-radius: 10px;">Graphene-Django</div>
        <div style="background: rgba(255,255,255,0.1); padding: 15px 25px; border-radius: 10px;">MySQL</div>
        <div style="background: rgba(255,255,255,0.1); padding: 15px 25px; border-radius: 10px;">Redis</div>
        <div style="background: rgba(255,255,255,0.1); padding: 15px 25px; border-radius: 10px;">Docker</div>
    </div>
</div>
{% endblock %}
login.html LOGIN
📍 templates/login.html
{% extends 'base.html' %}
{% load static %}

{% block title %}Login - JWT{% endblock %}

{% block content %}
<div class="auth-container">
    <div class="auth-card">
        <h2>🔐 Iniciar Sesión</h2>
        
        <form id="loginForm">
            <div class="form-group">
                <label for="username">Usuario:</label>
                <input type="text" id="username" name="username" required>
            </div>
            
            <div class="form-group">
                <label for="password">Contraseña:</label>
                <input type="password" id="password" name="password" required>
            </div>
            
            <button type="submit" class="btn btn-primary">Iniciar Sesión</button>
        </form>

        <div class="divider">O continuar con</div>

        <div class="oauth-buttons">
            <a href="/api/auth/google/" class="btn btn-google">
                <img src="https://www.google.com/favicon.ico" alt="Google">
                Google
            </a>
            <a href="/api/auth/github/" class="btn btn-github">
                <img src="https://github.com/favicon.ico" alt="GitHub">
                GitHub
            </a>
        </div>

        <p class="text-center">
            ¿No tienes cuenta? <a href="/register/">Regístrate aquí</a>
        </p>

        <div id="message" class="message"></div>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script>
document.getElementById('loginForm').addEventListener('submit', async (e) => {
    e.preventDefault();
    
    const username = document.getElementById('username').value;
    const password = document.getElementById('password').value;
    const messageDiv = document.getElementById('message');
    
    try {
        const response = await fetch('/api/login/', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ username, password })
        });
        
        const data = await response.json();
        
        if (response.ok) {
            // Guardar tokens
            localStorage.setItem('access_token', data.tokens.access);
            localStorage.setItem('refresh_token', data.tokens.refresh);
            localStorage.setItem('username', data.user.username);
            
            messageDiv.className = 'message success';
            messageDiv.textContent = '✅ Login exitoso! Redirigiendo...';
            
            setTimeout(() => {
                window.location.href = '/dashboard/';
            }, 1000);
        } else {
            messageDiv.className = 'message error';
            messageDiv.textContent = '❌ ' + (data.error || 'Error al iniciar sesión');
        }
    } catch (error) {
        messageDiv.className = 'message error';
        messageDiv.textContent = '❌ Error de conexión: ' + error.message;
    }
});
</script>
{% endblock %}
register.html REGISTRO
📍 templates/register.html
{% extends 'base.html' %}
{% load static %}

{% block title %}Registro{% endblock %}

{% block content %}
<div class="auth-container">
    <div class="auth-card">
        <h2>📝 Crear Cuenta</h2>
        
        <form id="registerForm">
            <div class="form-group">
                <label for="username">Usuario:</label>
                <input type="text" id="username" name="username" required>
            </div>
            
            <div class="form-group">
                <label for="email">Email:</label>
                <input type="email" id="email" name="email" required>
            </div>
            
            <div class="form-group">
                <label for="password">Contraseña:</label>
                <input type="password" id="password" name="password" required>
            </div>
            
            <div class="form-group">
                <label for="password2">Confirmar Contraseña:</label>
                <input type="password" id="password2" name="password2" required>
            </div>
            
            <button type="submit" class="btn btn-primary">Registrarse</button>
        </form>

        <p class="text-center">
            ¿Ya tienes cuenta? <a href="/login/">Inicia sesión</a>
        </p>

        <div id="message" class="message"></div>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script>
document.getElementById('registerForm').addEventListener('submit', async (e) => {
    e.preventDefault();
    
    const formData = {
        username: document.getElementById('username').value,
        email: document.getElementById('email').value,
        password: document.getElementById('password').value,
        password2: document.getElementById('password2').value
    };
    
    const messageDiv = document.getElementById('message');
    
    try {
        const response = await fetch('/api/register/', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(formData)
        });
        
        const data = await response.json();
        
        if (response.ok) {
            localStorage.setItem('access_token', data.tokens.access);
            localStorage.setItem('refresh_token', data.tokens.refresh);
            
            messageDiv.className = 'message success';
            messageDiv.textContent = '✅ Registro exitoso! Redirigiendo...';
            
            setTimeout(() => {
                window.location.href = '/dashboard/';
            }, 1000);
        } else {
            messageDiv.className = 'message error';
            messageDiv.textContent = '❌ ' + JSON.stringify(data);
        }
    } catch (error) {
        messageDiv.className = 'message error';
        messageDiv.textContent = '❌ Error: ' + error.message;
    }
});
</script>
{% endblock %}
dashboard.html DASHBOARD
📍 templates/dashboard.html
{% extends 'base.html' %}
{% load static %}

{% block title %}Dashboard{% endblock %}

{% block content %}
<div class="dashboard-container">
    <h1>📊 Dashboard</h1>
    <p>Bienvenido, <span id="username"></span></p>

    <div class="dashboard-grid">
        <div class="card">
            <h3>👤 Perfil</h3>
            <div id="profile-info"></div>
        </div>

        <div class="card">
            <h3>🔑 OAuth Conectado</h3>
            <div id="oauth-status"></div>
        </div>

        <div class="card">
            <h3>📦 Productos</h3>
            <div id="products-list"></div>
        </div>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script>
const token = localStorage.getItem('access_token');

if (!token) {
    window.location.href = '/login/';
}

// Mostrar nombre de usuario
document.getElementById('username').textContent = localStorage.getItem('username') || 'Usuario';

// Cargar perfil
async function loadProfile() {
    try {
        const response = await fetch('/api/profile/', {
            headers: {
                'Authorization': `Bearer ${token}`
            }
        });
        
        if (response.ok) {
            const data = await response.json();
            document.getElementById('profile-info').innerHTML = `
                <p>Email: ${data.user.email}</p>
                <p>Fecha de registro: ${new Date(data.user.date_joined).toLocaleDateString()}</p>
            `;
        }
    } catch (error) {
        console.error('Error:', error);
    }
}

// Cargar productos
async function loadProducts() {
    try {
        const response = await fetch('/api/products/', {
            headers: {
                'Authorization': `Bearer ${token}`
            }
        });
        
        if (response.ok) {
            const data = await response.json();
            const productsList = document.getElementById('products-list');
            
            if (data.results && data.results.length > 0) {
                productsList.innerHTML = data.results.map(p => 
                    `<div>${p.name} - $${p.price}</div>`
                ).join('');
            } else {
                productsList.innerHTML = '<p>No hay productos</p>';
            }
        }
    } catch (error) {
        console.error('Error:', error);
    }
}

loadProfile();
loadProducts();
</script>
{% endblock %}
chat.html CHAT WEBSOCKET
📍 templates/chat.html
{% extends 'base.html' %}
{% load static %}

{% block title %}Chat - {{ room_name }}{% endblock %}

{% block content %}
<div class="chat-container">
    <h1>💬 Chat: {{ room_name }}</h1>
    
    <div class="chat-box">
        <div id="chat-log" class="chat-messages"></div>
        
        <div class="chat-input">
            <input type="text" id="chat-message-input" placeholder="Escribe un mensaje...">
            <button id="chat-message-submit">Enviar</button>
        </div>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script>
const roomName = "{{ room_name }}";
const username = localStorage.getItem('username') || 'Anonymous';

// WebSocket connection
const chatSocket = new WebSocket(
    'ws://' + window.location.host + '/ws/chat/' + roomName + '/'
);

chatSocket.onopen = function(e) {
    console.log('WebSocket conectado');
    addSystemMessage('Conectado al chat');
};

chatSocket.onmessage = function(e) {
    const data = JSON.parse(e.data);
    
    if (data.type === 'chat_message') {
        addMessage(data.username, data.message);
    } else if (data.type === 'connection_established') {
        addSystemMessage(data.message);
    }
};

chatSocket.onclose = function(e) {
    console.error('WebSocket cerrado');
    addSystemMessage('Desconectado del chat');
};

chatSocket.onerror = function(e) {
    console.error('WebSocket error:', e);
};

// Enviar mensaje
document.getElementById('chat-message-submit').onclick = function() {
    sendMessage();
};

document.getElementById('chat-message-input').onkeyup = function(e) {
    if (e.keyCode === 13) {
        sendMessage();
    }
};

function sendMessage() {
    const messageInput = document.getElementById('chat-message-input');
    const message = messageInput.value.trim();
    
    if (message) {
        chatSocket.send(JSON.stringify({
            'message': message,
            'username': username
        }));
        messageInput.value = '';
    }
}

function addMessage(username, message) {
    const chatLog = document.getElementById('chat-log');
    const messageElement = document.createElement('div');
    messageElement.className = 'chat-message';
    messageElement.innerHTML = `
        <strong>${username}:</strong> ${message}
        <span class="time">${new Date().toLocaleTimeString()}</span>
    `;
    chatLog.appendChild(messageElement);
    chatLog.scrollTop = chatLog.scrollHeight;
}

function addSystemMessage(message) {
    const chatLog = document.getElementById('chat-log');
    const messageElement = document.createElement('div');
    messageElement.className = 'system-message';
    messageElement.textContent = message;
    chatLog.appendChild(messageElement);
}
</script>
{% endblock %}
oauth_connect.html OAUTH
📍 templates/oauth_connect.html
{% extends 'base.html' %}
{% load static %}

{% block title %}Conectar OAuth{% endblock %}

{% block content %}
<div class="oauth-container">
    <h1>🔗 Conectar Servicios OAuth</h1>
    
    <div class="oauth-cards">
        <div class="oauth-card">
            <h2>Google</h2>
            <p>Conecta tu cuenta de Google</p>
            <a href="/api/auth/google/" class="btn btn-google">Conectar Google</a>
        </div>
        
        <div class="oauth-card">
            <h2>GitHub</h2>
            <p>Conecta tu cuenta de GitHub</p>
            <a href="/api/auth/github/" class="btn btn-github">Conectar GitHub</a>
        </div>
    </div>
</div>
{% endblock %}
libros_list.html CATÁLOGO
📍 templates/libros_list.html
{% extends 'base.html' %}
{% load static %}

{% block title %}Catálogo de Libros{% endblock %}

{% block content %}
<div class="dashboard-container">
    <h1>📚 Catálogo de Libros</h1>
    
    <div style="margin-bottom: 30px;">
        <input type="text" id="search-input" placeholder="Buscar libros por título, autor o ISBN..." 
               style="width: 100%; max-width: 600px; padding: 12px; border: 2px solid #e1e8ed; border-radius: 8px;">
    </div>

    <div id="libros-grid" class="dashboard-grid">
        <!-- Los libros se cargarán aquí dinámicamente -->
    </div>

    <div id="pagination" style="text-align: center; margin-top: 30px;">
        <!-- Paginación -->
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script>
const token = localStorage.getItem('access_token');
let currentPage = 1;

// Cargar libros
async function loadLibros(page = 1, search = '') {
    try {
        let url = `/api/libros/?page=${page}`;
        if (search) {
            url += `&search=${search}`;
        }

        const response = await fetch(url, {
            headers: {
                'Authorization': `Bearer ${token}`
            }
        });
        
        if (response.ok) {
            const data = await response.json();
            displayLibros(data.results);
            displayPagination(data);
        }
    } catch (error) {
        console.error('Error:', error);
    }
}

// Mostrar libros
function displayLibros(libros) {
    const grid = document.getElementById('libros-grid');
    
    if (libros.length === 0) {
        grid.innerHTML = '<p style="grid-column: 1/-1; text-align: center;">No se encontraron libros</p>';
        return;
    }
    
    grid.innerHTML = libros.map(libro => `
        <div class="card">
            <h3>${libro.titulo}</h3>
            <p><strong>Autor:</strong> ${libro.autor?.nombre || 'N/A'}</p>
            <p><strong>ISBN:</strong> ${libro.isbn}</p>
            <p><strong>Categoría:</strong> ${libro.categoria?.nombre || 'N/A'}</p>
            <p><strong>Precio:</strong> $${libro.precio}</p>
            <p><strong>Disponibles:</strong> ${libro.cantidad_disponible}</p>
            <p><strong>Publicación:</strong> ${libro.fecha_publicacion}</p>
            ${libro.disponible ? 
                '<button onclick="prestarLibro(' + libro.id + ')" class="btn btn-primary">Prestar</button>' : 
                '<button class="btn" disabled>No disponible</button>'
            }
        </div>
    `).join('');
}

// Mostrar paginación
function displayPagination(data) {
    const pagination = document.getElementById('pagination');
    let html = '';
    
    if (data.previous) {
        html += '<button onclick="loadLibros(' + (currentPage - 1) + ')" class="btn">Anterior</button> ';
    }
    
    html += `<span>Página ${currentPage}</span> `;
    
    if (data.next) {
        html += '<button onclick="loadLibros(' + (currentPage + 1) + ')" class="btn">Siguiente</button>';
    }
    
    pagination.innerHTML = html;
}

// Prestar libro
async function prestarLibro(libroId) {
    if (!confirm('¿Deseas solicitar el préstamo de este libro?')) {
        return;
    }
    
    try {
        const response = await fetch('/api/prestamos/', {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${token}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                libro_id: libroId
            })
        });
        
        if (response.ok) {
            alert('Préstamo solicitado exitosamente');
            loadLibros(currentPage);
        } else {
            const error = await response.json();
            alert('Error: ' + (error.detail || 'No se pudo procesar el préstamo'));
        }
    } catch (error) {
        console.error('Error:', error);
        alert('Error al solicitar préstamo');
    }
}

// Búsqueda
let searchTimeout;
document.getElementById('search-input').addEventListener('input', (e) => {
    clearTimeout(searchTimeout);
    searchTimeout = setTimeout(() => {
        currentPage = 1;
        loadLibros(1, e.target.value);
    }, 500);
});

// Cargar al inicio
loadLibros();
</script>
{% endblock %}
libro_detail.html DETALLE LIBRO
📍 templates/libro_detail.html
{% extends 'base.html' %}
{% load static %}

{% block title %}{{ libro.titulo }} - Detalle{% endblock %}

{% block content %}
<div class="dashboard-container">
    <div class="card" style="max-width: 800px; margin: 0 auto;">
        <div style="display: flex; gap: 30px;">
            <div style="flex: 1;">
                <img src="{{ libro.imagen_portada|default:'/static/img/libro-default.png' }}" 
                     alt="{{ libro.titulo }}" 
                     style="width: 100%; border-radius: 10px;">
            </div>
            
            <div style="flex: 2;">
                <h1 style="color: #667eea; margin-bottom: 20px;">{{ libro.titulo }}</h1>
                
                <div style="margin-bottom: 15px;">
                    <strong>📖 Autor:</strong> {{ libro.autor.nombre }}
                </div>
                
                <div style="margin-bottom: 15px;">
                    <strong>🏷️ Categoría:</strong> {{ libro.categoria.nombre }}
                </div>
                
                <div style="margin-bottom: 15px;">
                    <strong>📘 ISBN:</strong> {{ libro.isbn }}
                </div>
                
                <div style="margin-bottom: 15px;">
                    <strong>💰 Precio:</strong> ${{ libro.precio }}
                </div>
                
                <div style="margin-bottom: 15px;">
                    <strong>📅 Publicación:</strong> {{ libro.fecha_publicacion }}
                </div>
                
                <div style="margin-bottom: 15px;">
                    <strong>📦 Editorial:</strong> {{ libro.editorial }}
                </div>
                
                <div style="margin-bottom: 15px;">
                    <strong>📄 Páginas:</strong> {{ libro.numero_paginas }}
                </div>
                
                <div style="margin-bottom: 15px;">
                    <strong>💿 Stock:</strong> {{ libro.cantidad_disponible }} / {{ libro.cantidad_total }}
                </div>
                
                <div style="margin-bottom: 20px;">
                    <strong>⭐ Calificación:</strong> {{ libro.calificacion_promedio|floatformat:1 }} / 5.0
                </div>
                
                <div style="margin-bottom: 20px;">
                    <p>{{ libro.descripcion }}</p>
                </div>
                
                {% if libro.disponible %}
                    <button onclick="prestarLibro({{ libro.id }})" class="btn btn-primary">
                        Solicitar Préstamo
                    </button>
                {% else %}
                    <button class="btn" disabled>No Disponible</button>
                {% endif %}
            </div>
        </div>
    </div>

    <!-- Reseñas -->
    <div class="card" style="max-width: 800px; margin: 30px auto;">
        <h2>💬 Reseñas</h2>
        <div id="resenas-list">
            <!-- Reseñas se cargarán aquí -->
        </div>
        
        <div style="margin-top: 30px;">
            <h3>Escribe una reseña</h3>
            <form id="resena-form">
                <div class="form-group">
                    <label>Calificación:</label>
                    <select id="calificacion" required>
                        <option value="5">⭐⭐⭐⭐⭐ (5)</option>
                        <option value="4">⭐⭐⭐⭐ (4)</option>
                        <option value="3">⭐⭐⭐ (3)</option>
                        <option value="2">⭐⭐ (2)</option>
                        <option value="1">⭐ (1)</option>
                    </select>
                </div>
                <div class="form-group">
                    <label>Comentario:</label>
                    <textarea id="comentario" rows="4" style="width: 100%; padding: 10px;" required></textarea>
                </div>
                <button type="submit" class="btn btn-primary">Publicar Reseña</button>
            </form>
        </div>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script>
const token = localStorage.getItem('access_token');
const libroId = {{ libro.id }};

// Prestar libro
async function prestarLibro(id) {
    if (!confirm('¿Deseas solicitar el préstamo de este libro?')) {
        return;
    }
    
    try {
        const response = await fetch('/api/prestamos/', {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${token}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ libro_id: id })
        });
        
        if (response.ok) {
            alert('Préstamo solicitado exitosamente');
            location.reload();
        }
    } catch (error) {
        console.error('Error:', error);
    }
}

// Cargar reseñas
async function loadResenas() {
    try {
        const response = await fetch(`/api/libros/${libroId}/resenas/`);
        const data = await response.json();
        
        const list = document.getElementById('resenas-list');
        list.innerHTML = data.map(r => `
            <div style="border-bottom: 1px solid #eee; padding: 15px 0;">
                <strong>${r.usuario.username}</strong> - ${'⭐'.repeat(r.calificacion)}
                <p>${r.comentario}</p>
                <small style="color: #999;">${new Date(r.created_at).toLocaleDateString()}</small>
            </div>
        `).join('');
    } catch (error) {
        console.error('Error:', error);
    }
}

// Enviar reseña
document.getElementById('resena-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    
    const calificacion = document.getElementById('calificacion').value;
    const comentario = document.getElementById('comentario').value;
    
    try {
        const response = await fetch('/api/resenas/', {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${token}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                libro: libroId,
                calificacion: parseInt(calificacion),
                comentario: comentario
            })
        });
        
        if (response.ok) {
            alert('Reseña publicada');
            document.getElementById('comentario').value = '';
            loadResenas();
        }
    } catch (error) {
        console.error('Error:', error);
    }
});

loadResenas();
</script>
{% endblock %}

🎨 Archivos CSS & JavaScript

style.css CSS PRINCIPAL
📍 static/css/style.css
/* ========================================
   ESTILOS PRINCIPALES DEL PROYECTO
   ======================================== */

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    line-height: 1.6;
    color: #333;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 20px;
}

/* NAVBAR */
.navbar {
    background: rgba(255, 255, 255, 0.95);
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    padding: 1rem 0;
    position: sticky;
    top: 0;
    z-index: 1000;
}

.navbar .container {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.logo {
    font-size: 1.5rem;
    font-weight: bold;
    color: #667eea;
    text-decoration: none;
}

.nav-menu {
    display: flex;
    list-style: none;
    gap: 2rem;
}

.nav-menu a {
    color: #333;
    text-decoration: none;
    font-weight: 500;
    transition: color 0.3s;
}

.nav-menu a:hover {
    color: #667eea;
}

/* HERO */
.hero {
    text-align: center;
    padding: 100px 20px;
    color: white;
}

.hero h1 {
    font-size: 3rem;
    margin-bottom: 1rem;
    text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}

.hero p {
    font-size: 1.5rem;
}

/* AUTH CONTAINER */
.auth-container {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 80vh;
    padding: 20px;
}

.auth-card {
    background: white;
    border-radius: 20px;
    padding: 40px;
    box-shadow: 0 10px 40px rgba(0,0,0,0.2);
    max-width: 450px;
    width: 100%;
}

.auth-card h2 {
    text-align: center;
    color: #667eea;
    margin-bottom: 30px;
}

.form-group {
    margin-bottom: 20px;
}

.form-group label {
    display: block;
    margin-bottom: 8px;
    font-weight: 600;
    color: #555;
}

.form-group input {
    width: 100%;
    padding: 12px;
    border: 2px solid #e1e8ed;
    border-radius: 8px;
    font-size: 1rem;
    transition: border-color 0.3s;
}

.form-group input:focus {
    outline: none;
    border-color: #667eea;
}

.btn {
    width: 100%;
    padding: 12px;
    border: none;
    border-radius: 8px;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s;
}

.btn-primary {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
}

.btn-primary:hover {
    transform: translateY(-2px);
    box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}

.btn-google {
    background: #4285f4;
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin-bottom: 10px;
}

.btn-github {
    background: #333;
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
}

.divider {
    text-align: center;
    margin: 20px 0;
    color: #999;
    position: relative;
}

.divider::before,
.divider::after {
    content: '';
    position: absolute;
    top: 50%;
    width: 40%;
    height: 1px;
    background: #ddd;
}

.divider::before { left: 0; }
.divider::after { right: 0; }

.text-center {
    text-align: center;
    margin-top: 20px;
}

.message {
    margin-top: 20px;
    padding: 12px;
    border-radius: 8px;
    text-align: center;
    display: none;
}

.message.success {
    background: #d4edda;
    color: #155724;
    border: 1px solid #c3e6cb;
    display: block;
}

.message.error {
    background: #f8d7da;
    color: #721c24;
    border: 1px solid #f5c6cb;
    display: block;
}

/* DASHBOARD */
.dashboard-container {
    max-width: 1200px;
    margin: 40px auto;
    padding: 20px;
}

.dashboard-container h1 {
    color: white;
    margin-bottom: 30px;
}

.dashboard-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 20px;
}

.card {
    background: white;
    border-radius: 12px;
    padding: 25px;
    box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}

.card h3 {
    color: #667eea;
    margin-bottom: 15px;
}

/* CHAT */
.chat-container {
    max-width: 800px;
    margin: 40px auto;
    padding: 20px;
}

.chat-container h1 {
    color: white;
    margin-bottom: 20px;
}

.chat-box {
    background: white;
    border-radius: 12px;
    padding: 20px;
    box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}

.chat-messages {
    height: 400px;
    overflow-y: auto;
    border: 1px solid #e1e8ed;
    border-radius: 8px;
    padding: 15px;
    margin-bottom: 15px;
}

.chat-message {
    margin-bottom: 10px;
    padding: 8px;
    background: #f7f9fa;
    border-radius: 8px;
}

.system-message {
    text-align: center;
    color: #999;
    font-style: italic;
    margin: 10px 0;
}

.chat-input {
    display: flex;
    gap: 10px;
}

.chat-input input {
    flex: 1;
    padding: 12px;
    border: 2px solid #e1e8ed;
    border-radius: 8px;
    font-size: 1rem;
}

.chat-input button {
    padding: 12px 24px;
    background: #667eea;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-weight: 600;
}

/* FOOTER */
footer {
    text-align: center;
    padding: 20px;
    color: white;
    margin-top: 40px;
}
auth.js AUTENTICACIÓN JS
📍 static/js/auth.js
// ========================================
// FUNCIONES DE AUTENTICACIÓN JWT
// ========================================

const API_URL = window.location.origin;

// Obtener token
function getAccessToken() {
    return localStorage.getItem('access_token');
}

function getRefreshToken() {
    return localStorage.getItem('refresh_token');
}

// Guardar tokens
function saveTokens(access, refresh) {
    localStorage.setItem('access_token', access);
    localStorage.setItem('refresh_token', refresh);
}

// Limpiar tokens
function clearTokens() {
    localStorage.removeItem('access_token');
    localStorage.removeItem('refresh_token');
    localStorage.removeItem('username');
}

// Verificar si está autenticado
function isAuthenticated() {
    return !!getAccessToken();
}

// Refrescar token
async function refreshAccessToken() {
    const refreshToken = getRefreshToken();
    
    if (!refreshToken) {
        throw new Error('No refresh token');
    }
    
    try {
        const response = await fetch(`${API_URL}/api/token/refresh/`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ refresh: refreshToken })
        });
        
        if (response.ok) {
            const data = await response.json();
            localStorage.setItem('access_token', data.access);
            return data.access;
        } else {
            clearTokens();
            window.location.href = '/login/';
        }
    } catch (error) {
        console.error('Error refreshing token:', error);
        clearTokens();
        window.location.href = '/login/';
    }
}

// Fetch con autenticación
async function authenticatedFetch(url, options = {}) {
    let token = getAccessToken();
    
    if (!token) {
        window.location.href = '/login/';
        return;
    }
    
    options.headers = {
        ...options.headers,
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
    };
    
    let response = await fetch(url, options);
    
    // Si el token expiró, intentar refrescarlo
    if (response.status === 401) {
        token = await refreshAccessToken();
        options.headers['Authorization'] = `Bearer ${token}`;
        response = await fetch(url, options);
    }
    
    return response;
}

// Logout
async function logout() {
    const refreshToken = getRefreshToken();
    
    if (refreshToken) {
        try {
            await authenticatedFetch(`${API_URL}/api/logout/`, {
                method: 'POST',
                body: JSON.stringify({ refresh: refreshToken })
            });
        } catch (error) {
            console.error('Logout error:', error);
        }
    }
    
    clearTokens();
    window.location.href = '/login/';
}

// Proteger páginas (llamar en páginas que requieren autenticación)
function requireAuth() {
    if (!isAuthenticated()) {
        window.location.href = '/login/';
    }
}
websocket.js WEBSOCKETS JS
📍 static/js/websocket.js
// ========================================
// CLIENTE WEBSOCKET
// ========================================

class ChatWebSocket {
    constructor(roomName, onMessageCallback) {
        this.roomName = roomName;
        this.onMessageCallback = onMessageCallback;
        this.socket = null;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 5;
        this.connect();
    }
    
    connect() {
        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
        const wsUrl = `${protocol}//${window.location.host}/ws/chat/${this.roomName}/`;
        
        this.socket = new WebSocket(wsUrl);
        
        this.socket.onopen = (e) => {
            console.log('WebSocket conectado');
            this.reconnectAttempts = 0;
            this.onConnectionChange(true);
        };
        
        this.socket.onmessage = (e) => {
            const data = JSON.parse(e.data);
            if (this.onMessageCallback) {
                this.onMessageCallback(data);
            }
        };
        
        this.socket.onclose = (e) => {
            console.log('WebSocket cerrado');
            this.onConnectionChange(false);
            this.attemptReconnect();
        };
        
        this.socket.onerror = (e) => {
            console.error('WebSocket error:', e);
        };
    }
    
    attemptReconnect() {
        if (this.reconnectAttempts < this.maxReconnectAttempts) {
            this.reconnectAttempts++;
            console.log(`Intentando reconectar... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
            
            setTimeout(() => {
                this.connect();
            }, 3000 * this.reconnectAttempts);
        } else {
            console.error('Máximo de intentos de reconexión alcanzado');
        }
    }
    
    sendMessage(message, username) {
        if (this.socket && this.socket.readyState === WebSocket.OPEN) {
            this.socket.send(JSON.stringify({
                message: message,
                username: username
            }));
            return true;
        } else {
            console.error('WebSocket no está conectado');
            return false;
        }
    }
    
    close() {
        if (this.socket) {
            this.socket.close();
        }
    }
    
    onConnectionChange(isConnected) {
        console.log('Connection status:', isConnected ? 'Connected' : 'Disconnected');
    }
}
oauth.js OAUTH JS
📍 static/js/oauth.js
// ========================================
// FUNCIONES OAUTH 2.0
// ========================================

// Iniciar flujo OAuth con Google
function loginWithGoogle() {
    window.location.href = '/api/auth/google/';
}

// Iniciar flujo OAuth con GitHub
function loginWithGitHub() {
    window.location.href = '/api/auth/github/';
}

// Manejar callback de OAuth (llamar en página de callback)
function handleOAuthCallback() {
    const urlParams = new URLSearchParams(window.location.search);
    const token = urlParams.get('token');
    const error = urlParams.get('error');
    
    if (error) {
        console.error('OAuth error:', error);
        alert('Error en autenticación OAuth: ' + error);
        window.location.href = '/login/';
        return;
    }
    
    if (token) {
        localStorage.setItem('access_token', token);
        
        // Obtener información del usuario
        fetch('/api/profile/', {
            headers: {
                'Authorization': `Bearer ${token}`
            }
        })
        .then(response => response.json())
        .then(data => {
            localStorage.setItem('username', data.user.username);
            window.location.href = '/dashboard/';
        })
        .catch(error => {
            console.error('Error:', error);
            window.location.href = '/login/';
        });
    }
}

// Verificar estado de conexiones OAuth
async function checkOAuthConnections() {
    const token = localStorage.getItem('access_token');
    
    if (!token) return;
    
    try {
        const response = await fetch('/api/profile/', {
            headers: {
                'Authorization': `Bearer ${token}`
            }
        });
        
        if (response.ok) {
            const data = await response.json();
            return {
                google: !!data.google_id,
                github: !!data.github_id
            };
        }
    } catch (error) {
        console.error('Error checking OAuth:', error);
    }
    
    return { google: false, github: false };
}
dashboard.js DASHBOARD JS
📍 static/js/dashboard.js
// ========================================
// FUNCIONES DEL DASHBOARD
// ========================================

const API_URL = window.location.origin;

// Cargar estadísticas del dashboard
async function loadDashboardStats() {
    const token = localStorage.getItem('access_token');
    
    if (!token) {
        window.location.href = '/login/';
        return;
    }
    
    try {
        // Cargar estadísticas de usuario
        const statsResponse = await authenticatedFetch(`${API_URL}/api/dashboard/stats/`);
        
        if (statsResponse && statsResponse.ok) {
            const stats = await statsResponse.json();
            displayStats(stats);
        }
        
        // Cargar actividad reciente
        const activityResponse = await authenticatedFetch(`${API_URL}/api/dashboard/activity/`);
        
        if (activityResponse && activityResponse.ok) {
            const activity = await activityResponse.json();
            displayActivity(activity);
        }
        
        // Cargar préstamos activos
        const prestamosResponse = await authenticatedFetch(`${API_URL}/api/prestamos/?estado=activo`);
        
        if (prestamosResponse && prestamosResponse.ok) {
            const prestamos = await prestamosResponse.json();
            displayPrestamosActivos(prestamos.results);
        }
    } catch (error) {
        console.error('Error cargando dashboard:', error);
    }
}

// Mostrar estadísticas
function displayStats(stats) {
    const statsContainer = document.getElementById('stats-container');
    
    if (!statsContainer) return;
    
    statsContainer.innerHTML = `
        <div class="stat-card">
            <h3>📚 Libros Prestados</h3>
            <p class="stat-number">${stats.total_prestamos || 0}</p>
        </div>
        <div class="stat-card">
            <h3>📖 Libros Activos</h3>
            <p class="stat-number">${stats.prestamos_activos || 0}</p>
        </div>
        <div class="stat-card">
            <h3>⭐ Reseñas Escritas</h3>
            <p class="stat-number">${stats.total_resenas || 0}</p>
        </div>
        <div class="stat-card">
            <h3>💰 Multas Pendientes</h3>
            <p class="stat-number">$${stats.multas_pendientes || 0}</p>
        </div>
    `;
}

// Mostrar actividad reciente
function displayActivity(activities) {
    const activityContainer = document.getElementById('activity-container');
    
    if (!activityContainer) return;
    
    if (!activities || activities.length === 0) {
        activityContainer.innerHTML = '<p>No hay actividad reciente</p>';
        return;
    }
    
    activityContainer.innerHTML = activities.map(activity => `
        <div class="activity-item">
            <span class="activity-icon">${getActivityIcon(activity.type)}</span>
            <div>
                <strong>${activity.title}</strong>
                <p>${activity.description}</p>
                <small>${formatDate(activity.created_at)}</small>
            </div>
        </div>
    `).join('');
}

// Mostrar préstamos activos
function displayPrestamosActivos(prestamos) {
    const prestamosContainer = document.getElementById('prestamos-activos');
    
    if (!prestamosContainer) return;
    
    if (!prestamos || prestamos.length === 0) {
        prestamosContainer.innerHTML = '<p>No tienes préstamos activos</p>';
        return;
    }
    
    prestamosContainer.innerHTML = prestamos.map(prestamo => {
        const diasRestantes = calcularDiasRestantes(prestamo.fecha_devolucion_estimada);
        const isVencido = diasRestantes < 0;
        
        return `
            <div class="prestamo-card ${isVencido ? 'vencido' : ''}">
                <h4>${prestamo.libro.titulo}</h4>
                <p><strong>Autor:</strong> ${prestamo.libro.autor.nombre}</p>
                <p><strong>Fecha de préstamo:</strong> ${formatDate(prestamo.fecha_prestamo)}</p>
                <p><strong>Fecha de devolución:</strong> ${formatDate(prestamo.fecha_devolucion_estimada)}</p>
                <p class="${isVencido ? 'text-danger' : 'text-success'}">
                    <strong>${isVencido ? '⚠️ Vencido' : '✅ Activo'}</strong> - 
                    ${Math.abs(diasRestantes)} día(s) ${isVencido ? 'de retraso' : 'restantes'}
                </p>
                ${prestamo.multa > 0 ? `<p class="text-danger">💰 Multa: $${prestamo.multa}</p>` : ''}
                <button onclick="devolverLibro(${prestamo.id})" class="btn btn-primary">
                    Marcar como Devuelto
                </button>
            </div>
        `;
    }).join('');
}

// Devolver libro
async function devolverLibro(prestamoId) {
    if (!confirm('¿Confirmas la devolución de este libro?')) {
        return;
    }
    
    try {
        const response = await authenticatedFetch(`${API_URL}/api/prestamos/${prestamoId}/devolver/`, {
            method: 'POST'
        });
        
        if (response && response.ok) {
            alert('Libro devuelto exitosamente');
            loadDashboardStats();
        } else {
            const error = await response.json();
            alert('Error: ' + (error.detail || 'No se pudo devolver el libro'));
        }
    } catch (error) {
        console.error('Error:', error);
        alert('Error al devolver el libro');
    }
}

// Utilidades
function getActivityIcon(type) {
    const icons = {
        'prestamo': '📚',
        'devolucion': '✅',
        'resena': '⭐',
        'multa': '💰',
        'registro': '👤'
    };
    return icons[type] || '📌';
}

function formatDate(dateString) {
    const date = new Date(dateString);
    return date.toLocaleDateString('es-MX', { 
        year: 'numeric', 
        month: 'long', 
        day: 'numeric' 
    });
}

function calcularDiasRestantes(fechaDevolucion) {
    const hoy = new Date();
    const fecha = new Date(fechaDevolucion);
    const diffTime = fecha - hoy;
    const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
    return diffDays;
}

// Auto-inicializar en dashboard
if (window.location.pathname === '/dashboard/') {
    document.addEventListener('DOMContentLoaded', loadDashboardStats);
}
notifications.js NOTIFICACIONES JS
📍 static/js/notifications.js
// ========================================
// SISTEMA DE NOTIFICACIONES EN TIEMPO REAL
// ========================================

class NotificationManager {
    constructor() {
        this.socket = null;
        this.connected = false;
        this.notifications = [];
        this.initWebSocket();
    }
    
    initWebSocket() {
        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
        const wsUrl = `${protocol}//${window.location.host}/ws/notifications/`;
        
        this.socket = new WebSocket(wsUrl);
        
        this.socket.onopen = () => {
            console.log('✅ Notificaciones conectadas');
            this.connected = true;
            this.updateConnectionStatus(true);
        };
        
        this.socket.onmessage = (e) => {
            const data = JSON.parse(e.data);
            this.handleNotification(data);
        };
        
        this.socket.onclose = () => {
            console.log('❌ Notificaciones desconectadas');
            this.connected = false;
            this.updateConnectionStatus(false);
            
            // Intentar reconectar después de 5 segundos
            setTimeout(() => this.initWebSocket(), 5000);
        };
        
        this.socket.onerror = (error) => {
            console.error('Error en WebSocket de notificaciones:', error);
        };
    }
    
    handleNotification(data) {
        console.log('📬 Nueva notificación:', data);
        
        this.notifications.unshift(data);
        
        // Mostrar notificación en pantalla
        this.showNotification(data);
        
        // Actualizar contador de notificaciones
        this.updateNotificationBadge();
        
        // Guardar en localStorage (opcional)
        this.saveNotifications();
    }
    
    showNotification(notification) {
        // Crear elemento de notificación
        const notifElement = document.createElement('div');
        notifElement.className = 'notification-toast ' + (notification.type || 'info');
        notifElement.innerHTML = `
            <div class="notification-content">
                <strong>${this.getNotificationIcon(notification.type)} ${notification.title}</strong>
                <p>${notification.message}</p>
            </div>
            <button onclick="this.parentElement.remove()">×</button>
        `;
        
        // Agregar al contenedor
        let container = document.getElementById('notification-container');
        if (!container) {
            container = document.createElement('div');
            container.id = 'notification-container';
            container.style.cssText = `
                position: fixed;
                top: 80px;
                right: 20px;
                z-index: 9999;
                max-width: 350px;
            `;
            document.body.appendChild(container);
        }
        
        container.appendChild(notifElement);
        
        // Auto-remover después de 5 segundos
        setTimeout(() => {
            notifElement.style.opacity = '0';
            setTimeout(() => notifElement.remove(), 300);
        }, 5000);
        
        // Reproducir sonido (opcional)
        this.playNotificationSound();
    }
    
    getNotificationIcon(type) {
        const icons = {
            'prestamo': '📚',
            'devolucion': '✅',
            'vencimiento': '⚠️',
            'multa': '💰',
            'nuevo_libro': '📖',
            'resena': '⭐',
            'info': 'ℹ️',
            'success': '✅',
            'warning': '⚠️',
            'error': '❌'
        };
        return icons[type] || '📌';
    }
    
    updateNotificationBadge() {
        const badge = document.getElementById('notification-badge');
        if (badge) {
            const unreadCount = this.notifications.filter(n => !n.read).length;
            badge.textContent = unreadCount;
            badge.style.display = unreadCount > 0 ? 'inline-block' : 'none';
        }
    }
    
    updateConnectionStatus(isConnected) {
        const statusElement = document.getElementById('ws-status');
        if (statusElement) {
            statusElement.textContent = isConnected ? '🟢 Conectado' : '🔴 Desconectado';
            statusElement.className = isConnected ? 'status-connected' : 'status-disconnected';
        }
    }
    
    markAsRead(notificationId) {
        const notification = this.notifications.find(n => n.id === notificationId);
        if (notification) {
            notification.read = true;
            this.updateNotificationBadge();
            this.saveNotifications();
        }
    }
    
    markAllAsRead() {
        this.notifications.forEach(n => n.read = true);
        this.updateNotificationBadge();
        this.saveNotifications();
    }
    
    saveNotifications() {
        localStorage.setItem('notifications', JSON.stringify(this.notifications.slice(0, 50)));
    }
    
    loadNotifications() {
        const saved = localStorage.getItem('notifications');
        if (saved) {
            this.notifications = JSON.parse(saved);
            this.updateNotificationBadge();
        }
    }
    
    playNotificationSound() {
        // Opcional: reproducir sonido de notificación
        const audio = new Audio('/static/sounds/notification.mp3');
        audio.volume = 0.3;
        audio.play().catch(e => {
            // Ignorar errores de auto-play
        });
    }
}

// Inicializar gestor de notificaciones
let notificationManager;

document.addEventListener('DOMContentLoaded', () => {
    const token = localStorage.getItem('access_token');
    if (token) {
        notificationManager = new NotificationManager();
        notificationManager.loadNotifications();
    }
});

// API pública
function getNotifications() {
    return notificationManager ? notificationManager.notifications : [];
}

function markNotificationAsRead(id) {
    if (notificationManager) {
        notificationManager.markAsRead(id);
    }
}

function markAllNotificationsAsRead() {
    if (notificationManager) {
        notificationManager.markAllAsRead();
    }
}

🐳 Docker y Deployment

Dockerfile DOCKER
📍 Dockerfile
# ========================================
# DOCKERFILE PARA PRODUCCIÓN
# ========================================

FROM python:3.11-slim

# Variables de entorno
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1

# Directorio de trabajo
WORKDIR /app

# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
    gcc \
    default-libmysqlclient-dev \
    pkg-config \
    && rm -rf /var/lib/apt/lists/*

# Copiar requirements y instalar
COPY requirements.txt .
RUN pip install --upgrade pip && \
    pip install -r requirements.txt

# Copiar proyecto
COPY . .

# Colectar archivos estáticos
RUN python manage.py collectstatic --noinput

# Exponer puerto
EXPOSE 8000

# Comando de inicio
CMD ["daphne", "-b", "0.0.0.0", "-p", "8000", "biblioteca_digital.asgi:application"]
docker-compose.yml DOCKER COMPOSE
📍 docker-compose.yml
version: '3.8'

services:
  # Base de datos MySQL
  db:
    image: mysql:8.0
    container_name: django_mysql
    restart: always
    environment:
      MYSQL_DATABASE: mi_base_datos
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_USER: django_user
      MYSQL_PASSWORD: django_password
    volumes:
      - mysql_data:/var/lib/mysql
    ports:
      - "3306:3306"
    networks:
      - django_network

  # Redis para WebSockets
  redis:
    image: redis:7-alpine
    container_name: django_redis
    restart: always
    ports:
      - "6379:6379"
    networks:
      - django_network

  # Aplicación Django
  web:
    build: .
    container_name: django_app
    restart: always
    command: daphne -b 0.0.0.0 -p 8000 biblioteca_digital.asgi:application
    volumes:
      - .:/app
      - static_volume:/app/staticfiles
      - media_volume:/app/media
    ports:
      - "8000:8000"
    env_file:
      - .env
    depends_on:
      - db
      - redis
    networks:
      - django_network

  # Nginx (opcional para producción)
  nginx:
    image: nginx:alpine
    container_name: django_nginx
    restart: always
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - static_volume:/app/staticfiles
      - media_volume:/app/media
    depends_on:
      - web
    networks:
      - django_network

volumes:
  mysql_data:
  static_volume:
  media_volume:

networks:
  django_network:
    driver: bridge
.dockerignore DOCKER IGNORE
📍 .dockerignore
# Git
.git
.gitignore

# Python
__pycache__
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/

# Django
*.log
db.sqlite3
media/
staticfiles/

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Otros
*.md
.env.example
nginx.conf NGINX
📍 nginx.conf
# ========================================
# CONFIGURACIÓN NGINX PARA PRODUCCIÓN
# ========================================

events {
    worker_connections 1024;
}

http {
    upstream django_app {
        server web:8000;
    }

    server {
        listen 80;
        server_name localhost;
        client_max_body_size 100M;

        # Servir archivos estáticos
        location /static/ {
            alias /app/staticfiles/;
            expires 30d;
            add_header Cache-Control "public, immutable";
        }

        # Servir archivos media
        location /media/ {
            alias /app/media/;
            expires 7d;
            add_header Cache-Control "public";
        }

        # WebSocket para Django Channels
        location /ws/ {
            proxy_pass http://django_app;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_redirect off;
            proxy_read_timeout 86400;
        }

        # Proxy para aplicación Django
        location / {
            proxy_pass http://django_app;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_redirect off;
        }

        # Headers de seguridad
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Referrer-Policy "no-referrer-when-downgrade" always;
    }

    # Configuración SSL (descomentar para HTTPS)
    # server {
    #     listen 443 ssl http2;
    #     server_name your-domain.com;
    #     
    #     ssl_certificate /etc/nginx/ssl/cert.pem;
    #     ssl_certificate_key /etc/nginx/ssl/key.pem;
    #     
    #     ssl_protocols TLSv1.2 TLSv1.3;
    #     ssl_ciphers HIGH:!aNULL:!MD5;
    #     ssl_prefer_server_ciphers on;
    #     
    #     # Resto de la configuración igual que arriba
    # }
}
gunicorn_start.sh GUNICORN SCRIPT
📍 gunicorn_start.sh
#!/bin/bash

# ========================================
# SCRIPT DE INICIO GUNICORN PARA PRODUCCIÓN
# ========================================

NAME="biblioteca_digital"
DJANGODIR=/app
SOCKFILE=/app/run/gunicorn.sock
USER=www-data
GROUP=www-data
NUM_WORKERS=3
DJANGO_SETTINGS_MODULE=biblioteca_project.settings
DJANGO_WSGI_MODULE=biblioteca_project.wsgi

echo "Iniciando $NAME como `whoami`"

# Activar entorno virtual
cd $DJANGODIR
source venv/bin/activate

# Variables de entorno
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH

# Crear directorio para socket
RUNDIR=$(dirname $SOCKFILE)
test -d $RUNDIR || mkdir -p $RUNDIR

# Ejecutar Gunicorn
exec gunicorn ${DJANGO_WSGI_MODULE}:application \
  --name $NAME \
  --workers $NUM_WORKERS \
  --user=$USER --group=$GROUP \
  --bind=unix:$SOCKFILE \
  --log-level=info \
  --log-file=-
deploy_pythonanywhere.py PYTHONANYWHERE DEPLOY
📍 deploy_pythonanywhere.py
#!/usr/bin/env python
"""
========================================
SCRIPT DE DEPLOYMENT PARA PYTHONANYWHERE
========================================
Ejecutar este script en PythonAnywhere console
"""

import os
import sys

def deploy():
    print("🚀 Iniciando deployment en PythonAnywhere...")
    
    # 1. Actualizar código
    print("📥 Actualizando código desde GitHub...")
    os.system("cd /home/tu_usuario/biblioteca_digital && git pull")
    
    # 2. Instalar dependencias
    print("📦 Instalando dependencias...")
    os.system("pip3.10 install --user -r /home/tu_usuario/biblioteca_digital/requirements.txt")
    
    # 3. Migrar base de datos
    print("🗄️ Aplicando migraciones...")
    os.system("cd /home/tu_usuario/biblioteca_digital && python3.10 manage.py migrate --noinput")
    
    # 4. Colectar archivos estáticos
    print("🎨 Colectando archivos estáticos...")
    os.system("cd /home/tu_usuario/biblioteca_digital && python3.10 manage.py collectstatic --noinput")
    
    # 5. Recargar web app
    print("🔄 Recargando aplicación web...")
    os.system("touch /var/www/tu_usuario_pythonanywhere_com_wsgi.py")
    
    print("✅ Deployment completado!")
    print("🌐 Visita: https://tu_usuario.pythonanywhere.com")

if __name__ == "__main__":
    deploy()
wsgi_pythonanywhere.py WSGI PYTHONANYWHERE
📍 wsgi_pythonanywhere.py
# ========================================
# WSGI PARA PYTHONANYWHERE
# ========================================
# Copiar este contenido al archivo WSGI de PythonAnywhere

import os
import sys

# Añadir proyecto al path
path = '/home/tu_usuario/biblioteca_digital'
if path not in sys.path:
    sys.path.insert(0, path)

# Configurar variables de entorno
os.environ['DJANGO_SETTINGS_MODULE'] = 'biblioteca_project.settings'

# Cargar variables desde .env
from dotenv import load_dotenv
load_dotenv(os.path.join(path, '.env'))

# Inicializar Django
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

# Para servir archivos estáticos con WhiteNoise (recomendado)
# from whitenoise import WhiteNoise
# application = WhiteNoise(application, root='/home/tu_usuario/biblioteca_digital/staticfiles')
# application.add_files('/home/tu_usuario/biblioteca_digital/media', prefix='media/')
requirements_prod.txt REQUIREMENTS PRODUCCIÓN
📍 requirements_prod.txt
# ========================================
# DEPENDENCIAS PARA PRODUCCIÓN
# ========================================

# Core Django
Django==4.2.7
djangorestframework==3.14.0

# Autenticación
djangorestframework-simplejwt==5.3.0
django-oauth-toolkit==2.3.0

# WebSockets
channels==4.0.0
channels-redis==4.1.0
daphne==4.0.0

# GraphQL
graphene-django==3.1.5

# Base de datos
mysqlclient==2.2.0

# CORS y Seguridad
django-cors-headers==4.3.0

# Redis
redis==5.0.1

# Utilidades
requests==2.31.0
django-filter==23.5
python-decouple==3.8

# Producción
gunicorn==21.2.0
whitenoise==6.6.0

# Monitoreo (opcional)
sentry-sdk==1.39.1

# Testing (opcional)
pytest==7.4.3
pytest-django==4.7.0
Makefile COMANDOS ÚTILES
📍 Makefile
# ========================================
# MAKEFILE - COMANDOS ÚTILES
# ========================================

.PHONY: help install migrate run test clean docker-build docker-up docker-down

help:
	@echo "🚀 Comandos disponibles:"
	@echo "  make install       - Instalar dependencias"
	@echo "  make migrate       - Ejecutar migraciones"
	@echo "  make run           - Ejecutar servidor de desarrollo"
	@echo "  make test          - Ejecutar tests"
	@echo "  make clean         - Limpiar archivos temporales"
	@echo "  make docker-build  - Construir imagen Docker"
	@echo "  make docker-up     - Iniciar contenedores Docker"
	@echo "  make docker-down   - Detener contenedores Docker"

install:
	@echo "📦 Instalando dependencias..."
	pip install -r requirements.txt

migrate:
	@echo "🗄️ Ejecutando migraciones..."
	python manage.py makemigrations
	python manage.py migrate

run:
	@echo "🚀 Iniciando servidor de desarrollo..."
	daphne -b 0.0.0.0 -p 8000 biblioteca_project.asgi:application

test:
	@echo "🧪 Ejecutando tests..."
	python manage.py test

clean:
	@echo "🧹 Limpiando archivos temporales..."
	find . -type f -name '*.pyc' -delete
	find . -type d -name '__pycache__' -delete
	rm -rf .pytest_cache
	rm -rf htmlcov
	rm -rf .coverage

docker-build:
	@echo "🐳 Construyendo imagen Docker..."
	docker-compose build

docker-up:
	@echo "🐳 Iniciando contenedores Docker..."
	docker-compose up -d

docker-down:
	@echo "🐳 Deteniendo contenedores Docker..."
	docker-compose down

superuser:
	@echo "👤 Creando superusuario..."
	python manage.py createsuperuser

collectstatic:
	@echo "🎨 Colectando archivos estáticos..."
	python manage.py collectstatic --noinput
README.md DOCUMENTACIÓN
📍 README.md
# 🚀 Proyecto Django REST + JWT + OAuth + WebSockets

Aplicación completa con Django REST Framework, autenticación JWT, OAuth 2.0 y WebSockets en tiempo real.

## 📋 Características

- ✅ Django REST Framework
- ✅ Autenticación JWT (JSON Web Tokens)
- ✅ OAuth 2.0 (Google y GitHub)
- ✅ WebSockets con Django Channels
- ✅ Base de datos MySQL
- ✅ Redis para WebSockets
- ✅ Docker & Docker Compose
- ✅ API RESTful completa

## 🛠️ Instalación

### Opción 1: Sin Docker

1. **Clonar el repositorio**
```bash
git clone <tu-repo>
cd proyecto_django
```

2. **Crear entorno virtual**
```bash
python -m venv venv
source venv/bin/activate  # En Windows: venv\Scripts\activate
```

3. **Instalar dependencias**
```bash
pip install -r requirements.txt
```

4. **Configurar variables de entorno**
```bash
cp .env.example .env
# Editar .env con tus configuraciones
```

5. **Configurar MySQL**
```bash
# Crear base de datos en MySQL
mysql -u root -p
CREATE DATABASE mi_base_datos;
```

6. **Migrar base de datos**
```bash
python manage.py makemigrations
python manage.py migrate
```

7. **Crear superusuario**
```bash
python manage.py createsuperuser
```

8. **Iniciar Redis**
```bash
redis-server
```

9. **Ejecutar servidor**
```bash
python manage.py runserver
# O para WebSockets:
daphne biblioteca_digital.asgi:application
```

### Opción 2: Con Docker

1. **Construir y ejecutar**
```bash
docker-compose up --build
```

2. **Acceder a la aplicación**
```
http://localhost:8000
```

## 🔑 Configuración OAuth

### Google OAuth

1. Ir a [Google Cloud Console](https://console.cloud.google.com/)
2. Crear un proyecto
3. Habilitar Google+ API
4. Crear credenciales OAuth 2.0
5. Agregar `http://localhost:8000/api/auth/google/callback/` como URI de redirección
6. Copiar Client ID y Client Secret al `.env`

### GitHub OAuth

1. Ir a Settings > Developer settings > OAuth Apps
2. Crear nueva OAuth App
3. Agregar `http://localhost:8000/api/auth/github/callback/` como callback URL
4. Copiar Client ID y Client Secret al `.env`

## 📡 Endpoints API

### Autenticación

- `POST /api/register/` - Registro de usuario
- `POST /api/login/` - Login con JWT
- `POST /api/logout/` - Logout
- `GET /api/profile/` - Perfil del usuario
- `POST /api/token/refresh/` - Refrescar token

### OAuth

- `GET /api/auth/google/` - Login con Google
- `GET /api/auth/github/` - Login con GitHub

### Productos

- `GET /api/products/` - Listar productos
- `POST /api/products/` - Crear producto
- `GET /api/products/{id}/` - Detalle de producto
- `PUT /api/products/{id}/` - Actualizar producto
- `DELETE /api/products/{id}/` - Eliminar producto

### WebSocket

- `ws://localhost:8000/ws/chat/{room_name}/` - Chat en tiempo real

## 🧪 Testing

```bash
python manage.py test
```

## 📦 Producción

1. Configurar variables de entorno para producción
2. Configurar HTTPS
3. Usar Nginx como proxy inverso
4. Configurar certificado SSL

## 📄 Licencia

MIT