🔧 CORRECCIÓN COMPLETA: OAuth Template y Función Callback

Solución definitiva para redirección correcta y obtención de datos del usuario

Versión 2.0 - Completamente Funcional

✅ PROBLEMA RESUELTO

Problema original:

  • ❌ El sistema redirigía a Django REST en lugar de mostrar el template personalizado
  • ❌ Error 405 Method Not Allowed al intentar login con Google
  • ❌ No se mostraba la información del usuario después de login exitoso
  • ❌ Los tokens JWT no se visualizaban correctamente

Solución implementada:

  • ✅ Template oauth_login.html completamente reescrito con manejo de múltiples casos
  • ✅ Función google_oauth_callback actualizada para soportar GET y redirigir al template
  • ✅ Información completa del usuario y tokens JWT visibles
  • ✅ Validación correcta del token en Postman

📝 PARTE 1: Actualizar oauth_views.py

🔧 Función google_oauth_callback() Corregida

Ubicación: libros/oauth_views.py

Cambios principales:

  1. Soportar método GET (Google redirige con GET)
  2. Usar redirect() en lugar de Response()
  3. Enviar datos del usuario y tokens en la URL al template
  4. Codificar correctamente los datos con urllib.parse.quote()

📄 CÓDIGO COMPLETO:

from django.conf import settings
from django.contrib.auth import get_user_model
from django.shortcuts import redirect
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from urllib.parse import urlencode, quote
import urllib.parse
import requests
import logging
import json

logger = logging.getLogger(__name__)
User = get_user_model()


@api_view(['GET', 'POST'])  # ← AGREGAR GET
@permission_classes([AllowAny])
def google_oauth_callback(request):
    """
    Endpoint que recibe el código de autorización de Google
    y redirige a oauth_login.html con los tokens
    
    GET /api/auth/google/callback/?code=... (desde Google)
    POST /api/auth/google/callback/ (desde frontend)
    """
    
    # 1. Obtener el código de autorización (GET de Google o POST del frontend)
    if request.method == 'GET':
        code = request.GET.get('code')
        error = request.GET.get('error')
        
        if error:
            logger.error(f"Error de Google: {error}")
            return redirect(f'/oauth/login/?error={urllib.parse.quote(error)}')
    else:  # POST
        code = request.data.get('code')
    
    if not code:
        error_msg = 'El código de autorización es requerido'
        logger.error(error_msg)
        if request.method == 'GET':
            return redirect(f'/oauth/login/?error={urllib.parse.quote(error_msg)}')
        return Response({'error': error_msg}, status=status.HTTP_400_BAD_REQUEST)
    
    try:
        # 2. Intercambiar código por access token de Google
        token_url = 'https://oauth2.googleapis.com/token'
        
        google_config = settings.SOCIALACCOUNT_PROVIDERS['google']['APP']
        
        token_data = {
            'code': code,
            'client_id': google_config['client_id'],
            'client_secret': google_config['secret'],
            'redirect_uri': 'http://127.0.0.1:8000/api/auth/google/callback/',
            'grant_type': 'authorization_code'
        }
        
        token_response = requests.post(token_url, data=token_data, timeout=10)
        token_response.raise_for_status()
        
        tokens = token_response.json()
        google_access_token = tokens.get('access_token')
        
        if not google_access_token:
            error_msg = 'No se pudo obtener access token de Google'
            logger.error(error_msg)
            return redirect(f'/oauth/login/?error={urllib.parse.quote(error_msg)}')
        
        logger.info(f"Access token de Google obtenido: {google_access_token[:20]}...")
        
        # 3. Obtener información del usuario de Google
        userinfo_url = 'https://www.googleapis.com/oauth2/v2/userinfo'
        headers = {'Authorization': f'Bearer {google_access_token}'}
        
        userinfo_response = requests.get(userinfo_url, headers=headers, timeout=10)
        userinfo_response.raise_for_status()
        
        user_data = userinfo_response.json()
        logger.info(f"Datos de usuario de Google: {user_data}")
        
        # 4. Crear o actualizar usuario en Django
        email = user_data.get('email')
        
        if not email:
            error_msg = 'No se pudo obtener el email del usuario'
            logger.error(error_msg)
            return redirect(f'/oauth/login/?error={urllib.parse.quote(error_msg)}')
        
        # Buscar si el usuario ya existe
        user, created = User.objects.get_or_create(
            email=email,
            defaults={
                'username': email.split('@')[0],
                'first_name': user_data.get('given_name', ''),
                'last_name': user_data.get('family_name', ''),
            }
        )
        
        # Si el usuario ya existía, actualizar su información
        if not created:
            user.first_name = user_data.get('given_name', user.first_name)
            user.last_name = user_data.get('family_name', user.last_name)
            user.save()
            logger.info(f"Usuario existente actualizado: {user.email}")
        else:
            logger.info(f"Nuevo usuario creado: {user.email}")
        
        # 5. Generar tokens JWT de nuestra aplicación
        refresh = RefreshToken.for_user(user)
        access_token = str(refresh.access_token)
        
        # 6. Preparar datos para enviar al frontend
        user_info = {
            'id': user.id,
            'email': user.email,
            'username': user.username,
            'first_name': user.first_name,
            'last_name': user.last_name,
            'is_staff': user.is_staff,
        }
        
        google_info = {
            'picture': user_data.get('picture'),
            'verified_email': user_data.get('verified_email'),
        }
        
        # Codificar datos para URL
        user_info_json = json.dumps(user_info)
        google_info_json = json.dumps(google_info)
        
        # Construir URL de redirección a oauth_login.html con todos los datos
        redirect_url = (
            f'/oauth/login/?'
            f'access_token={access_token}&'
            f'refresh_token={str(refresh)}&'
            f'user_info={urllib.parse.quote(user_info_json)}&'
            f'google_info={urllib.parse.quote(google_info_json)}&'
            f'message={urllib.parse.quote("Login exitoso con Google" if not created else "Cuenta creada exitosamente con Google")}'
        )
        
        logger.info(f"Redirigiendo a: {redirect_url[:100]}...")
        return redirect(redirect_url)
    
    except requests.Timeout:
        logger.error("Timeout al comunicarse con Google")
        return redirect(f'/oauth/login/?error={urllib.parse.quote("Timeout al comunicarse con Google")}')
    
    except requests.RequestException as e:
        logger.error(f"Error al comunicarse con Google: {str(e)}")
        return redirect(f'/oauth/login/?error={urllib.parse.quote(f"Error con Google: {str(e)}")}')
    
    except Exception as e:
        logger.error(f"Error inesperado en OAuth: {str(e)}")
        return redirect(f'/oauth/login/?error={urllib.parse.quote(f"Error inesperado: {str(e)}")}')

📝 PARTE 2: Template oauth_login.html Completo

🎨 Template Reescrito Completamente

Ubicación: templates/oauth_login.html

Mejoras implementadas:

  • ✅ Manejo de múltiples casos (código, tokens, errores)
  • ✅ No redirige automáticamente a Django REST
  • ✅ Muestra información del usuario con foto de perfil
  • ✅ Muestra tokens JWT completos
  • ✅ Botones funcionales (API, Inicio, Cerrar sesión, Reintentar)
  • ✅ Almacena tokens en localStorage automáticamente
  • ✅ Diseño mejorado con mejor UX

📄 CÓDIGO COMPLETO DEL TEMPLATE:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login con Google - Biblioteca UTH</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 20px;
        }
       
        .container {
            background: white;
            padding: 40px;
            border-radius: 15px;
            box-shadow: 0 10px 50px rgba(0,0,0,0.2);
            max-width: 600px;
            width: 100%;
        }
       
        h1 {
            color: #333;
            margin-bottom: 30px;
            text-align: center;
        }
       
        .loading {
            text-align: center;
            color: #666;
            font-size: 1.2em;
        }
       
        .spinner {
            border: 4px solid #f3f3f3;
            border-top: 4px solid #667eea;
            border-radius: 50%;
            width: 50px;
            height: 50px;
            animation: spin 1s linear infinite;
            margin: 20px auto;
        }
       
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
       
        .result {
            margin-top: 30px;
            padding: 20px;
            border-radius: 8px;
            display: none;
        }
       
        .success {
            background: #d4edda;
            border: 1px solid #c3e6cb;
            color: #155724;
        }
       
        .error {
            background: #f8d7da;
            border: 1px solid #f5c6cb;
            color: #721c24;
        }
       
        .user-info {
            background: #f0f4ff;
            padding: 15px;
            border-radius: 8px;
            margin-top: 15px;
        }
       
        .user-info img {
            border-radius: 50%;
            margin-top: 10px;
            border: 3px solid #667eea;
        }
       
        .token-display {
            background: #2d2d2d;
            color: #4ec9b0;
            padding: 15px;
            border-radius: 8px;
            margin-top: 15px;
            word-wrap: break-word;
            font-family: 'Courier New', monospace;
            font-size: 0.9em;
            max-height: 100px;
            overflow: auto;
        }
       
        .btn {
            display: inline-block;
            padding: 10px 20px;
            margin: 5px;
            background: #667eea;
            color: white;
            text-decoration: none;
            border-radius: 5px;
            transition: all 0.3s ease;
            border: none;
            cursor: pointer;
        }
       
        .btn:hover {
            background: #5568d3;
            transform: translateY(-2px);
        }
       
        .btn-secondary {
            background: #6c757d;
        }
       
        .btn-secondary:hover {
            background: #5a6268;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🔐 Login con Google</h1>
       
        <div id="loading" class="loading">
            <div class="spinner"></div>
            <p id="loading-message">Procesando autenticación...</p>
        </div>
       
        <div id="result" class="result"></div>
    </div>
   
    <script>
        // Obtener parámetros de la URL
        const urlParams = new URLSearchParams(window.location.search);
        const code = urlParams.get('code');
        const accessToken = urlParams.get('access_token');
        const refreshToken = urlParams.get('refresh_token');
        const userInfoParam = urlParams.get('user_info');
        const googleInfoParam = urlParams.get('google_info');
        const message = urlParams.get('message');
        const error = urlParams.get('error');
       
        // Elementos del DOM
        const loadingEl = document.getElementById('loading');
        const loadingMessage = document.getElementById('loading-message');
        const resultEl = document.getElementById('result');
       
        // Función para mostrar error
        function showError(errorMsg) {
            loadingEl.style.display = 'none';
            resultEl.className = 'result error';
            resultEl.style.display = 'block';
            resultEl.innerHTML = `
                <h2>❌ Error en el Login</h2>
                <p>${errorMsg || 'Error desconocido'}</p>
                <a href="/" class="btn">🏠 Volver al Inicio</a>
                <button onclick="window.location.href='/oauth/login/'" class="btn btn-secondary">🔄 Reintentar</button>
            `;
        }
       
        // Función para mostrar éxito
        function showSuccess(data) {
            loadingEl.style.display = 'none';
            resultEl.className = 'result success';
            resultEl.style.display = 'block';
           
            // Guardar tokens
            if (data.access_token || data.access) {
                localStorage.setItem('access_token', data.access_token || data.access);
            }
            if (data.refresh_token || data.refresh) {
                localStorage.setItem('refresh_token', data.refresh_token || data.refresh);
            }
           
            // Obtener datos del usuario
            const user = data.user_info || data.user || {};
            const google = data.google_info || data.google_data || {};
           
            resultEl.innerHTML = `
                <h2>✅ ${data.message || 'Login exitoso'}</h2>
               
                <div class="user-info">
                    <h3>👤 Datos del Usuario</h3>
                    <p><strong>Email:</strong> ${user.email || 'N/A'}</p>
                    <p><strong>Nombre:</strong> ${user.first_name || ''} ${user.last_name || ''}</p>
                    <p><strong>Username:</strong> ${user.username || ''}</p>
                    ${google.picture ? `<img src="${google.picture}" alt="Foto de perfil" width="100">` : ''}
                </div>
               
                <div class="token-display">
                    <strong>🔑 Access Token:</strong><br>
                    ${data.access_token || data.access || 'N/A'}
                </div>
               
                <a href="/api/" class="btn">📚 Ir a la API</a>
                <a href="/" class="btn">🏠 Volver al Inicio</a>
                <button onclick="logout()" class="btn btn-secondary">🚪 Cerrar Sesión</button>
            `;
        }
       
        // Función para cerrar sesión
        function logout() {
            localStorage.removeItem('access_token');
            localStorage.removeItem('refresh_token');
            window.location.href = '/';
        }
       
        // Función para intercambiar código por token (POST al backend)
        async function exchangeCodeForToken(code) {
            try {
                loadingMessage.textContent = 'Intercambiando código por tokens...';
               
                const response = await fetch('/api/auth/google/callback/', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ code: code })
                });
               
                const data = await response.json();
               
                if (response.ok) {
                    showSuccess(data);
                } else {
                    showError(data.error || 'Error al procesar el código');
                }
            } catch (error) {
                showError('Error de red: ' + error.message);
            }
        }
       
        // FLUJO PRINCIPAL
        (function main() {
            // Caso 1: Hay error en la URL
            if (error) {
                showError(decodeURIComponent(error));
            }
            // Caso 2: Tenemos access_token con user_info (flujo completo desde backend)
            else if (accessToken && userInfoParam) {
                loadingMessage.textContent = 'Procesando datos del usuario...';
               
                try {
                    const userInfo = JSON.parse(decodeURIComponent(userInfoParam));
                    const googleInfo = googleInfoParam ? JSON.parse(decodeURIComponent(googleInfoParam)) : {};
                   
                    showSuccess({
                        access_token: accessToken,
                        refresh_token: refreshToken,
                        user_info: userInfo,
                        google_info: googleInfo,
                        message: message ? decodeURIComponent(message) : 'Login exitoso'
                    });
                } catch (e) {
                    showError('Error procesando datos del usuario');
                }
            }
            // Caso 3: Tenemos código de Google (necesita intercambio)
            else if (code) {
                exchangeCodeForToken(code);
            }
            // Caso 4: No hay nada, redirigir a Google
            else {
                loadingMessage.textContent = 'Redirigiendo a Google...';
                window.location.href = '/api/auth/google/redirect/';
            }
        })();
    </script>
</body>
</html>

📊 PARTE 3: Resultados y Verificación

✅ FUNCIONAMIENTO VERIFICADO

1️⃣ Flujo de Login Completo:

  1. Usuario hace clic en "Login con Google" en la página de inicio
  2. Redirige a /oauth/login/ (template oauth_login.html)
  3. El template detecta que no hay código y redirige a Google
  4. Usuario inicia sesión en Google y autoriza la aplicación
  5. Google redirige a /api/auth/google/callback/?code=... (GET)
  6. La función callback procesa el código y redirige a /oauth/login/?access_token=...&user_info=...
  7. El template muestra los datos del usuario y el token JWT

2️⃣ Información Mostrada:

  • ✅ Mensaje de éxito ("Login exitoso con Google")
  • ✅ Email del usuario
  • ✅ Nombre completo
  • ✅ Username
  • ✅ Foto de perfil de Google
  • ✅ Access Token JWT completo
  • ✅ Refresh Token (guardado en localStorage)

3️⃣ Tokens Almacenados:

  • localStorage.access_token - Token de acceso JWT
  • localStorage.refresh_token - Token de renovación JWT

4️⃣ Token Verificado en Postman:

El token JWT generado funciona correctamente para:

  • ✅ GET /api/libros/ - Listar libros
  • ✅ POST /api/libros/ - Crear nuevos libros
  • ✅ PATCH /api/libros/{id}/ - Actualizar libro (método correcto)
  • ✅ DELETE /api/libros/{id}/ - Eliminar libro

⚠️ CORRECCIÓN ADICIONAL: Método PATCH vs PUT

❌ Error identificado en la guía original:

La guía menciona usar PUT para actualizar libros:

PUT /api/libros/1/
Body: {
    "titulo": "Nuevo Título",
    "stock": 10
}

✅ Corrección necesaria:

Usar PATCH en lugar de PUT para actualizaciones parciales:

PATCH /api/libros/1/
Body: {
    "titulo": "Nuevo Título",
    "stock": 10
}

📝 Explicación:

  • PUT: Requiere TODOS los campos del modelo (reemplazo completo)
  • PATCH: Permite actualizar solo los campos específicos (actualización parcial)

Recomendación: Actualizar la guía para usar PATCH en ejemplos de actualización parcial.

🎯 RESUMEN DE CAMBIOS

📋 Checklist de Implementación

Archivo oauth_views.py:

  • ✅ Agregar imports: redirect, urllib.parse, json
  • ✅ Modificar decorador: @api_view(['GET', 'POST'])
  • ✅ Detectar método (GET de Google vs POST de frontend)
  • ✅ Usar redirect() en lugar de Response()
  • ✅ Preparar datos: user_info y google_info
  • ✅ Codificar JSON para URL con urllib.parse.quote()
  • ✅ Construir URL de redirección completa
  • ✅ Manejar errores con redirect a template

Archivo oauth_login.html:

  • ✅ Reemplazar TODO el contenido del template
  • ✅ Agregar estilos para imagen de perfil
  • ✅ Implementar función showError()
  • ✅ Implementar función showSuccess() unificada
  • ✅ Implementar función logout()
  • ✅ Implementar función exchangeCodeForToken()
  • ✅ Implementar flujo principal con casos múltiples
  • ✅ Parsear JSON de parámetros URL
  • ✅ Mostrar token completo (scrolleable)
  • ✅ Agregar botones funcionales

Actualización de documentación:

  • ✅ Corregir método PUT → PATCH en ejemplos
  • ✅ Agregar capturas de pantalla del flujo
  • ✅ Documentar casos de uso del template
  • ✅ Explicar diferencia entre GET y POST

🎉 ¡SISTEMA OAUTH COMPLETAMENTE FUNCIONAL!

Ahora tienes un sistema de autenticación OAuth 2.0 profesional que:

  • ✅ Redirige correctamente a tu template personalizado
  • ✅ Muestra toda la información del usuario
  • ✅ Genera y muestra tokens JWT válidos
  • ✅ Almacena tokens en localStorage automáticamente
  • ✅ Permite operaciones CRUD con el token
  • ✅ Maneja errores elegantemente
  • ✅ Proporciona buena experiencia de usuario