first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:26:46 +03:00
commit 2be3a313ad
55 changed files with 3609 additions and 0 deletions

0
accounts/__init__.py Normal file
View File

37
accounts/admin.py Normal file
View 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', 'active_until', 'date_joined')
list_filter = ('is_staff', 'is_superuser', 'is_active', 'active_until', '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', 'active_until')}),
(_('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', 'active_until', 'is_staff', 'is_active'),
}),
)
readonly_fields = ('date_joined', 'last_login')

5
accounts/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'

View File

View File

View File

@@ -0,0 +1,12 @@
from django.core.management.base import BaseCommand
from accounts.tasks import deactivate_expired_users_task
class Command(BaseCommand):
help = 'Deactivate users whose active period has expired.'
def handle(self, *args, **options):
updated_count = deactivate_expired_users_task()
self.stdout.write(self.style.SUCCESS(f'Deactivated {updated_count} expired user(s).'))

54
accounts/middleware.py Normal file
View File

@@ -0,0 +1,54 @@
"""
Custom middleware for social authentication.
"""
from django.contrib.auth import logout
from django.http import HttpResponseForbidden, JsonResponse
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
class AccountExpirationMiddleware:
"""
Deactivate users automatically when their access period has expired.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
user = getattr(request, 'user', None)
if user and user.is_authenticated and hasattr(user, 'deactivate_if_expired'):
if user.deactivate_if_expired():
logout(request)
if request.path.startswith('/api/'):
return JsonResponse(
{'detail': 'Account expired. Please contact an administrator.'},
status=403,
)
return HttpResponseForbidden('Account expired. Please contact an administrator.')
return self.get_response(request)

View File

@@ -0,0 +1,37 @@
# Generated by Django 6.0 on 2025-12-11 21:31
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',
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.3 on 2026-03-27 19:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='active_until',
field=models.DateTimeField(blank=True, help_text='If set, the account is automatically deactivated after this date.', null=True, verbose_name='active until'),
),
]

View File

129
accounts/models.py Normal file
View File

@@ -0,0 +1,129 @@
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.'
),
)
active_until = models.DateTimeField(
_('active until'),
null=True,
blank=True,
help_text=_('If set, the account is automatically deactivated after this date.'),
)
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
def is_expired(self):
"""Return True when account usage period has ended."""
return bool(self.active_until and timezone.now() >= self.active_until)
def deactivate_if_expired(self, save=True):
"""Deactivate account if active_until has passed."""
if self.is_active and self.is_expired():
self.is_active = False
if save:
self.save(update_fields=['is_active'])
return True
return False
def set_active_for_days(self, days, save=True):
"""Enable user for a fixed number of days from now."""
self.active_until = timezone.now() + timezone.timedelta(days=days)
self.is_active = True
if save:
self.save(update_fields=['active_until', 'is_active'])

19
accounts/pipeline.py Normal file
View 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
View 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

16
accounts/tasks.py Normal file
View File

@@ -0,0 +1,16 @@
from celery import shared_task
from django.contrib.auth import get_user_model
from django.utils import timezone
@shared_task(name='accounts.tasks.deactivate_expired_users_task')
def deactivate_expired_users_task():
"""Deactivate active users whose active_until timestamp has passed."""
user_model = get_user_model()
now = timezone.now()
updated = user_model.objects.filter(
is_active=True,
active_until__isnull=False,
active_until__lte=now,
).update(is_active=False)
return updated

112
accounts/tests.py Normal file
View File

@@ -0,0 +1,112 @@
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APITestCase
from django.utils import timezone
from .models import CustomUser
class AdminOnlyRegistrationEndpointsTests(APITestCase):
def setUp(self):
self.admin_user = CustomUser.objects.create_superuser(
email='admin@example.com',
password='adminpass123',
)
self.regular_user = CustomUser.objects.create_user(
email='user@example.com',
password='userpass123',
is_active=True,
)
def test_register_endpoint_rejects_non_admin(self):
self.client.force_authenticate(user=self.regular_user)
response = self.client.post(
'/api/v1/auth/users/',
{
'email': 'new-user@example.com',
'password': 'strong-pass-123',
're_password': 'strong-pass-123',
},
format='json',
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_register_endpoint_allows_admin(self):
self.client.force_authenticate(user=self.admin_user)
response = self.client.post(
'/api/v1/auth/users/',
{
'email': 'created-by-admin@example.com',
'password': 'strong-pass-123',
're_password': 'strong-pass-123',
},
format='json',
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_activation_endpoint_rejects_non_admin(self):
self.client.force_authenticate(user=self.regular_user)
response = self.client.post(
'/api/v1/auth/users/activation/',
{'uid': 'invalid', 'token': 'invalid'},
format='json',
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_resend_activation_endpoint_rejects_non_admin(self):
self.client.force_authenticate(user=self.regular_user)
response = self.client.post(
'/api/v1/auth/users/resend_activation/',
{'email': self.regular_user.email},
format='json',
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_resend_activation_endpoint_allows_admin_access(self):
self.client.force_authenticate(user=self.admin_user)
response = self.client.post(
'/api/v1/auth/users/resend_activation/',
{'email': self.regular_user.email},
format='json',
)
self.assertNotEqual(response.status_code, status.HTTP_403_FORBIDDEN)
class AccountExpiryTests(TestCase):
def test_user_is_deactivated_when_expired(self):
user = CustomUser.objects.create_user(
email='expired@example.com',
password='pass123456',
is_active=True,
active_until=timezone.now() - timezone.timedelta(days=1),
)
changed = user.deactivate_if_expired()
user.refresh_from_db()
self.assertTrue(changed)
self.assertFalse(user.is_active)
def test_user_stays_active_before_expiry(self):
user = CustomUser.objects.create_user(
email='active@example.com',
password='pass123456',
is_active=True,
active_until=timezone.now() + timezone.timedelta(days=3),
)
changed = user.deactivate_if_expired()
user.refresh_from_db()
self.assertFalse(changed)
self.assertTrue(user.is_active)

22
accounts/urls.py Normal file
View File

@@ -0,0 +1,22 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import AdminRestrictedUserViewSet, SocialLoginView, SocialAuthCallbackView, SocialAuthSuccessView
auth_router = DefaultRouter()
auth_router.register('users', AdminRestrictedUserViewSet, basename='user')
urlpatterns = [
# 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(auth_router.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')),
]

51
accounts/urls.py.bak Normal file
View 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'),
]

283
accounts/views.py Normal file
View 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)