first commit
This commit is contained in:
283
accounts/views.py
Normal file
283
accounts/views.py
Normal file
@@ -0,0 +1,283 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user