284 lines
10 KiB
Python
284 lines
10 KiB
Python
from django.shortcuts import redirect
|
|
from django.views import View
|
|
from djoser.views import UserViewSet
|
|
from rest_framework import status
|
|
from rest_framework.permissions import AllowAny, IsAdminUser
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
from rest_framework_simplejwt.tokens import RefreshToken
|
|
from social_django.utils import load_strategy, load_backend
|
|
from social_core.backends.oauth import BaseOAuth2
|
|
from social_core.exceptions import AuthException, AuthForbidden
|
|
from .serializers import SocialLoginSerializer, CustomUserSerializer
|
|
import json
|
|
|
|
|
|
class AdminRestrictedUserViewSet(UserViewSet):
|
|
"""
|
|
Restrict registration and activation-related endpoints to admin users.
|
|
"""
|
|
|
|
def get_permissions(self):
|
|
if self.action in {'create', 'activation', 'resend_activation'}:
|
|
return [IsAdminUser()]
|
|
return super().get_permissions()
|
|
|
|
|
|
class SocialLoginView(APIView):
|
|
"""
|
|
Social authentication endpoint.
|
|
Accepts access_token from social provider and returns JWT tokens.
|
|
|
|
POST /api/v1/auth/social/<provider>/
|
|
Body: { "access_token": "..." }
|
|
|
|
Supported providers: google-oauth2, github, facebook
|
|
"""
|
|
permission_classes = [AllowAny]
|
|
serializer_class = SocialLoginSerializer
|
|
|
|
def post(self, request, provider):
|
|
"""
|
|
Authenticate user with social provider token.
|
|
"""
|
|
# Validate provider
|
|
valid_providers = ['google-oauth2', 'github', 'facebook']
|
|
if provider not in valid_providers:
|
|
return Response(
|
|
{'error': f'Invalid provider. Must be one of: {", ".join(valid_providers)}'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Get access_token from request
|
|
access_token = request.data.get('access_token')
|
|
id_token = request.data.get('id_token', None)
|
|
|
|
if not access_token:
|
|
return Response(
|
|
{'error': 'access_token is required'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
try:
|
|
# Load social auth strategy and backend
|
|
strategy = load_strategy(request)
|
|
backend = load_backend(
|
|
strategy=strategy,
|
|
name=provider,
|
|
redirect_uri=None
|
|
)
|
|
|
|
# Verify token and get user
|
|
if isinstance(backend, BaseOAuth2):
|
|
# For OAuth2 providers, use access_token to get user info
|
|
user = backend.do_auth(access_token)
|
|
else:
|
|
return Response(
|
|
{'error': 'Unsupported authentication backend'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
if not user:
|
|
return Response(
|
|
{'error': 'Authentication failed. Invalid token.'},
|
|
status=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
|
|
# Check if user is active
|
|
if not user.is_active:
|
|
# This shouldn't happen for social auth users, but just in case
|
|
user.is_active = True
|
|
user.save(update_fields=['is_active'])
|
|
|
|
# Generate JWT tokens
|
|
refresh = RefreshToken.for_user(user)
|
|
|
|
# Serialize user data
|
|
user_serializer = CustomUserSerializer(user)
|
|
|
|
return Response({
|
|
'access': str(refresh.access_token),
|
|
'refresh': str(refresh),
|
|
'user': user_serializer.data
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
except AuthForbidden:
|
|
return Response(
|
|
{'error': 'Authentication forbidden. Email not provided by provider or permission denied.'},
|
|
status=status.HTTP_403_FORBIDDEN
|
|
)
|
|
except AuthException as e:
|
|
return Response(
|
|
{'error': f'Authentication error: {str(e)}'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
except Exception as e:
|
|
return Response(
|
|
{'error': f'An error occurred during authentication: {str(e)}'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
|
|
class SocialAuthCallbackView(View):
|
|
"""
|
|
Callback view for OAuth flow completion.
|
|
After successful authentication, redirects to frontend with tokens.
|
|
"""
|
|
permission_classes = [AllowAny]
|
|
authentication_classes = [] # No authentication required for callback
|
|
|
|
def get(self, request):
|
|
"""Handle OAuth callback and redirect to frontend with JWT tokens."""
|
|
from django.http import HttpResponseRedirect
|
|
|
|
# Get the authenticated user from the session
|
|
user = request.user
|
|
|
|
if user and user.is_authenticated:
|
|
# Generate JWT tokens
|
|
refresh = RefreshToken.for_user(user)
|
|
|
|
# Redirect to SPA with tokens (for testing)
|
|
redirect_url = f"/api/v1/spa/?access={str(refresh.access_token)}&refresh={str(refresh)}"
|
|
|
|
print(f"[OAuth Callback] Redirecting to: {redirect_url}")
|
|
|
|
return HttpResponseRedirect(redirect_url)
|
|
else:
|
|
# Authentication failed
|
|
return HttpResponseRedirect("/api/v1/auth/social/error/?error=authentication_failed")
|
|
|
|
|
|
class SocialAuthSuccessView(APIView):
|
|
"""
|
|
Success page after social authentication.
|
|
Displays tokens for testing purposes.
|
|
"""
|
|
permission_classes = [AllowAny]
|
|
authentication_classes = [] # No authentication required
|
|
|
|
def get(self, request):
|
|
"""Display success page with tokens."""
|
|
access_token = request.GET.get('access', '')
|
|
refresh_token = request.GET.get('refresh', '')
|
|
|
|
# Also check if user is in session
|
|
if not access_token and request.user.is_authenticated:
|
|
refresh = RefreshToken.for_user(request.user)
|
|
access_token = str(refresh.access_token)
|
|
refresh_token = str(refresh)
|
|
|
|
html_content = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Authentication Successful</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;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 20px;
|
|
}}
|
|
.container {{
|
|
background: white;
|
|
border-radius: 20px;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
padding: 40px;
|
|
max-width: 600px;
|
|
width: 100%;
|
|
}}
|
|
h1 {{
|
|
color: #28a745;
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
}}
|
|
.success-icon {{
|
|
text-align: center;
|
|
font-size: 64px;
|
|
margin-bottom: 20px;
|
|
}}
|
|
.token-box {{
|
|
background: #f8f9fa;
|
|
border: 2px solid #e1e4e8;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin: 15px 0;
|
|
word-break: break-all;
|
|
}}
|
|
.token-label {{
|
|
font-weight: bold;
|
|
color: #333;
|
|
margin-bottom: 5px;
|
|
}}
|
|
.token-value {{
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
color: #666;
|
|
background: white;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
overflow-x: auto;
|
|
}}
|
|
.btn {{
|
|
display: block;
|
|
width: 100%;
|
|
padding: 12px;
|
|
margin-top: 20px;
|
|
background: #667eea;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
transition: background 0.3s;
|
|
}}
|
|
.btn:hover {{
|
|
background: #5568d3;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="success-icon">✅</div>
|
|
<h1>Authentication Successful!</h1>
|
|
<p style="text-align: center; color: #666; margin-bottom: 30px;">
|
|
You have successfully authenticated with your social account.
|
|
</p>
|
|
|
|
<div class="token-box">
|
|
<div class="token-label">Access Token:</div>
|
|
<div class="token-value" id="accessToken">{access_token}</div>
|
|
</div>
|
|
|
|
<div class="token-box">
|
|
<div class="token-label">Refresh Token:</div>
|
|
<div class="token-value" id="refreshToken">{refresh_token}</div>
|
|
</div>
|
|
|
|
<button class="btn" onclick="copyTokens()">Copy Tokens to Clipboard</button>
|
|
<button class="btn" onclick="window.close()" style="background: #6c757d;">Close Window</button>
|
|
</div>
|
|
|
|
<script>
|
|
function copyTokens() {{
|
|
const tokens = {{
|
|
access: "{access_token}",
|
|
refresh: "{refresh_token}"
|
|
}};
|
|
|
|
navigator.clipboard.writeText(JSON.stringify(tokens, null, 2))
|
|
.then(() => alert('Tokens copied to clipboard!'))
|
|
.catch(err => alert('Failed to copy: ' + err));
|
|
}}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
from django.http import HttpResponse
|
|
return HttpResponse(html_content)
|