first commit
This commit is contained in:
0
accounts/__init__.py
Normal file
0
accounts/__init__.py
Normal file
37
accounts/admin.py
Normal file
37
accounts/admin.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .models import CustomUser
|
||||
|
||||
|
||||
@admin.register(CustomUser)
|
||||
class CustomUserAdmin(BaseUserAdmin):
|
||||
"""
|
||||
Custom admin panel configuration for CustomUser model.
|
||||
"""
|
||||
|
||||
# Fields to display in the user list
|
||||
list_display = ('email', 'first_name', 'last_name', 'is_staff', 'is_active', 'date_joined')
|
||||
list_filter = ('is_staff', 'is_superuser', 'is_active', 'date_joined')
|
||||
search_fields = ('email', 'first_name', 'last_name')
|
||||
ordering = ('-date_joined',)
|
||||
|
||||
# Fields to display on the user detail/edit page
|
||||
fieldsets = (
|
||||
(None, {'fields': ('email', 'password')}),
|
||||
(_('Personal info'), {'fields': ('first_name', 'last_name')}),
|
||||
(_('Permissions'), {
|
||||
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
|
||||
}),
|
||||
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
|
||||
)
|
||||
|
||||
# Fields to display when creating a new user
|
||||
add_fieldsets = (
|
||||
(None, {
|
||||
'classes': ('wide',),
|
||||
'fields': ('email', 'password1', 'password2', 'first_name', 'last_name', 'is_staff', 'is_active'),
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ('date_joined', 'last_login')
|
||||
5
accounts/apps.py
Normal file
5
accounts/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
name = 'accounts'
|
||||
27
accounts/middleware.py
Normal file
27
accounts/middleware.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Custom middleware for social authentication.
|
||||
"""
|
||||
|
||||
|
||||
class SocialAuthExceptionMiddleware:
|
||||
"""
|
||||
Middleware to handle social auth exceptions and redirect properly.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
def process_exception(self, request, exception):
|
||||
"""Handle social auth exceptions."""
|
||||
from social_core.exceptions import AuthException
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
if isinstance(exception, AuthException):
|
||||
return HttpResponseRedirect(f'/api/v1/auth/social/error/?error={str(exception)}')
|
||||
|
||||
return None
|
||||
|
||||
37
accounts/migrations/0001_initial.py
Normal file
37
accounts/migrations/0001_initial.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 6.0.2 on 2026-02-13 20:29
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CustomUser',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('email', models.EmailField(error_messages={'unique': 'A user with that email already exists.'}, max_length=254, unique=True, verbose_name='email address')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
'verbose_name_plural': 'users',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
accounts/migrations/__init__.py
Normal file
0
accounts/migrations/__init__.py
Normal file
103
accounts/models.py
Normal file
103
accounts/models.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class CustomUserManager(BaseUserManager):
|
||||
"""
|
||||
Custom user manager where email is the unique identifier
|
||||
for authentication instead of username.
|
||||
"""
|
||||
|
||||
def create_user(self, email, password=None, **extra_fields):
|
||||
"""
|
||||
Create and save a regular user with the given email and password.
|
||||
"""
|
||||
if not email:
|
||||
raise ValueError(_('The Email field must be set'))
|
||||
|
||||
email = self.normalize_email(email)
|
||||
user = self.model(email=email, **extra_fields)
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_superuser(self, email, password=None, **extra_fields):
|
||||
"""
|
||||
Create and save a SuperUser with the given email and password.
|
||||
"""
|
||||
extra_fields.setdefault('is_staff', True)
|
||||
extra_fields.setdefault('is_superuser', True)
|
||||
extra_fields.setdefault('is_active', True)
|
||||
|
||||
if extra_fields.get('is_staff') is not True:
|
||||
raise ValueError(_('Superuser must have is_staff=True.'))
|
||||
if extra_fields.get('is_superuser') is not True:
|
||||
raise ValueError(_('Superuser must have is_superuser=True.'))
|
||||
|
||||
return self.create_user(email, password, **extra_fields)
|
||||
|
||||
|
||||
class CustomUser(AbstractBaseUser, PermissionsMixin):
|
||||
"""
|
||||
Custom user model where email is used instead of username.
|
||||
|
||||
Fields:
|
||||
- email: unique email address (used for login)
|
||||
- first_name: user's first name
|
||||
- last_name: user's last name
|
||||
- is_staff: designates whether user can log into admin site
|
||||
- is_active: designates whether user account is active
|
||||
- date_joined: when the user account was created
|
||||
"""
|
||||
|
||||
email = models.EmailField(
|
||||
_('email address'),
|
||||
unique=True,
|
||||
error_messages={
|
||||
'unique': _("A user with that email already exists."),
|
||||
}
|
||||
)
|
||||
first_name = models.CharField(_('first name'), max_length=150, blank=True)
|
||||
last_name = models.CharField(_('last name'), max_length=150, blank=True)
|
||||
is_staff = models.BooleanField(
|
||||
_('staff status'),
|
||||
default=False,
|
||||
help_text=_('Designates whether the user can log into this admin site.'),
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
_('active'),
|
||||
default=True,
|
||||
help_text=_(
|
||||
'Designates whether this user should be treated as active. '
|
||||
'Unselect this instead of deleting accounts.'
|
||||
),
|
||||
)
|
||||
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
|
||||
|
||||
# Specify that we use email as the username field
|
||||
USERNAME_FIELD = 'email'
|
||||
REQUIRED_FIELDS = [] # Email is already required by USERNAME_FIELD
|
||||
|
||||
objects = CustomUserManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('user')
|
||||
verbose_name_plural = _('users')
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
def get_full_name(self):
|
||||
"""
|
||||
Return the first_name plus the last_name, with a space in between.
|
||||
"""
|
||||
full_name = f'{self.first_name} {self.last_name}'
|
||||
return full_name.strip()
|
||||
|
||||
def get_short_name(self):
|
||||
"""
|
||||
Return the short name for the user.
|
||||
"""
|
||||
return self.first_name
|
||||
19
accounts/pipeline.py
Normal file
19
accounts/pipeline.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Custom pipeline functions for Python Social Auth.
|
||||
These functions are called during the social authentication process.
|
||||
"""
|
||||
|
||||
|
||||
def activate_user(strategy, details, user=None, *args, **kwargs):
|
||||
"""
|
||||
Custom pipeline step to ensure social auth users are active.
|
||||
|
||||
This ensures that users who register via social login don't need
|
||||
email activation - they are automatically activated since the social
|
||||
provider has already verified their email.
|
||||
"""
|
||||
if user and not user.is_active:
|
||||
user.is_active = True
|
||||
user.save(update_fields=['is_active'])
|
||||
return {'user': user}
|
||||
|
||||
74
accounts/serializers.py
Normal file
74
accounts/serializers.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from rest_framework import serializers
|
||||
from djoser.serializers import UserCreateSerializer as BaseUserCreateSerializer
|
||||
from djoser.serializers import UserSerializer as BaseUserSerializer
|
||||
from .models import CustomUser
|
||||
|
||||
|
||||
class CustomUserCreateSerializer(BaseUserCreateSerializer):
|
||||
"""
|
||||
Custom serializer for user registration.
|
||||
Sets is_active=False by default so users must activate via email.
|
||||
"""
|
||||
|
||||
class Meta(BaseUserCreateSerializer.Meta):
|
||||
model = CustomUser
|
||||
fields = ('id', 'email', 'password', 're_password', 'first_name', 'last_name')
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
Override create to ensure is_active=False for email/password registrations.
|
||||
Social auth users will have is_active=True set via pipeline.
|
||||
"""
|
||||
# Remove re_password as it's only for validation
|
||||
validated_data.pop('re_password', None)
|
||||
|
||||
# Create user with is_active=False
|
||||
user = CustomUser.objects.create_user(
|
||||
email=validated_data['email'],
|
||||
password=validated_data['password'],
|
||||
first_name=validated_data.get('first_name', ''),
|
||||
last_name=validated_data.get('last_name', ''),
|
||||
is_active=False # Requires email activation
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
class CustomUserSerializer(BaseUserSerializer):
|
||||
"""
|
||||
Serializer for user details.
|
||||
Used for current user endpoint and user profile.
|
||||
"""
|
||||
|
||||
class Meta(BaseUserSerializer.Meta):
|
||||
model = CustomUser
|
||||
fields = ('id', 'email', 'first_name', 'last_name', 'is_active', 'date_joined')
|
||||
read_only_fields = ('id', 'email', 'is_active', 'date_joined')
|
||||
|
||||
|
||||
class SocialLoginSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for social authentication.
|
||||
Accepts provider name and access_token from frontend.
|
||||
"""
|
||||
provider = serializers.ChoiceField(
|
||||
choices=['google-oauth2', 'github', 'facebook'],
|
||||
help_text="Social auth provider name"
|
||||
)
|
||||
access_token = serializers.CharField(
|
||||
help_text="Access token from the social provider"
|
||||
)
|
||||
id_token = serializers.CharField(
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
help_text="ID token (optional, used by some providers like Google)"
|
||||
)
|
||||
|
||||
def validate_provider(self, value):
|
||||
"""Validate that the provider is supported."""
|
||||
valid_providers = ['google-oauth2', 'github', 'facebook']
|
||||
if value not in valid_providers:
|
||||
raise serializers.ValidationError(
|
||||
f"Invalid provider. Must be one of: {', '.join(valid_providers)}"
|
||||
)
|
||||
return value
|
||||
|
||||
3
accounts/tests.py
Normal file
3
accounts/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
51
accounts/urls.py
Normal file
51
accounts/urls.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from django.urls import path, include
|
||||
from .views import SocialLoginView, SocialAuthCallbackView, SocialAuthSuccessView
|
||||
|
||||
urlpatterns = [
|
||||
# Python Social Auth URLs (MUST BE FIRST for OAuth redirect flow)
|
||||
# /api/v1/social/login/github/ - GET: Start GitHub OAuth
|
||||
# /api/v1/social/login/google-oauth2/ - GET: Start Google OAuth
|
||||
# /api/v1/social/complete/github/ - GET: GitHub callback (handled by social-auth)
|
||||
# /api/v1/social/complete/google-oauth2/ - GET: Google callback (handled by social-auth)
|
||||
path('social/', include('social_django.urls', namespace='social')),
|
||||
|
||||
# SPA Test Page (Main app)
|
||||
path('spa/', lambda request:
|
||||
__import__('django.shortcuts').shortcuts.render(
|
||||
request, 'spa_test/index.html'
|
||||
), name='spa-test'),
|
||||
|
||||
# SPA Activation Page (Frontend route for email links)
|
||||
path('spa/activate/<str:uid>/<str:token>/', lambda request, uid, token:
|
||||
__import__('django.shortcuts').shortcuts.render(
|
||||
request, 'spa_test/activate.html', {'uid': uid, 'token': token}
|
||||
), name='spa-activate'),
|
||||
|
||||
# Django REST Framework browsable API auth
|
||||
path('api-auth/', include('rest_framework.urls')),
|
||||
|
||||
# Djoser endpoints (registration, activation, etc.)
|
||||
# /api/v1/auth/users/ - POST: Register new user
|
||||
# /api/v1/auth/users/activation/ - POST: Activate account with uid/token
|
||||
# /api/v1/auth/users/me/ - GET: Get current user info
|
||||
# /api/v1/auth/users/resend_activation/ - POST: Resend activation email
|
||||
path('auth/', include('djoser.urls')),
|
||||
|
||||
# Djoser JWT endpoints
|
||||
# /api/v1/auth/jwt/create/ - POST: Login (get JWT tokens)
|
||||
# /api/v1/auth/jwt/refresh/ - POST: Refresh access token
|
||||
# /api/v1/auth/jwt/verify/ - POST: Verify token
|
||||
path('auth/', include('djoser.urls.jwt')),
|
||||
|
||||
# Social authentication endpoints (Token-based - for mobile/SPA)
|
||||
# /api/v1/auth/social/google-oauth2/ - POST: Login with Google (requires access_token)
|
||||
# /api/v1/auth/social/github/ - POST: Login with GitHub (requires access_token)
|
||||
# /api/v1/auth/social/facebook/ - POST: Login with Facebook (requires access_token)
|
||||
path('auth/social/<str:provider>/', SocialLoginView.as_view(), name='social-login'),
|
||||
|
||||
# OAuth callback handler (after social-auth completes)
|
||||
path('auth/social/callback/', SocialAuthCallbackView.as_view(), name='social-callback'),
|
||||
|
||||
# Success/Error pages
|
||||
path('auth/social/success/', SocialAuthSuccessView.as_view(), name='social-success'),
|
||||
]
|
||||
271
accounts/views.py
Normal file
271
accounts/views.py
Normal file
@@ -0,0 +1,271 @@
|
||||
from django.shortcuts import redirect
|
||||
from django.views import View
|
||||
from rest_framework import status
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny
|
||||
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 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