mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-21 18:51:08 -05:00
Refactor user account system and remove moderation integration
- Remove first_name and last_name fields from User model - Add user deletion and social provider services - Restructure auth serializers into separate directory - Update avatar upload functionality and API endpoints - Remove django-moderation integration documentation - Add mandatory compliance enforcement rules - Update frontend documentation with API usage examples
This commit is contained in:
@@ -1,33 +1,3 @@
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class PasswordReset(models.Model):
|
||||
"""Persisted password reset tokens for API-driven password resets."""
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="password_resets",
|
||||
)
|
||||
token = models.CharField(max_length=128, unique=True, db_index=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
expires_at = models.DateTimeField()
|
||||
used = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
verbose_name = "Password Reset"
|
||||
verbose_name_plural = "Password Resets"
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
return timezone.now() > self.expires_at
|
||||
|
||||
def mark_used(self) -> None:
|
||||
self.used = True
|
||||
self.save(update_fields=["used"])
|
||||
|
||||
def __str__(self):
|
||||
user_id = getattr(self, "user_id", None)
|
||||
return f"PasswordReset(user={user_id}, token={self.token[:8]}..., used={self.used})"
|
||||
# This file is intentionally empty.
|
||||
# All models are now in their appropriate apps to avoid conflicts.
|
||||
# PasswordReset model is available in apps.accounts.models
|
||||
|
||||
@@ -18,7 +18,7 @@ from django.utils.crypto import get_random_string
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from .models import PasswordReset
|
||||
from apps.accounts.models import PasswordReset
|
||||
|
||||
|
||||
UserModel = get_user_model()
|
||||
@@ -62,8 +62,7 @@ class ModelChoices:
|
||||
"id": 1,
|
||||
"username": "john_doe",
|
||||
"email": "john@example.com",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"display_name": "John Doe",
|
||||
"date_joined": "2024-01-01T12:00:00Z",
|
||||
"is_active": True,
|
||||
"avatar_url": "https://example.com/avatars/john.jpg",
|
||||
@@ -83,12 +82,10 @@ class UserOutputSerializer(serializers.ModelSerializer):
|
||||
"id",
|
||||
"username",
|
||||
"email",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"display_name",
|
||||
"date_joined",
|
||||
"is_active",
|
||||
"avatar_url",
|
||||
"display_name",
|
||||
]
|
||||
read_only_fields = ["id", "date_joined", "is_active"]
|
||||
|
||||
@@ -127,7 +124,8 @@ class LoginInputSerializer(serializers.Serializer):
|
||||
class LoginOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for successful login."""
|
||||
|
||||
token = serializers.CharField()
|
||||
access = serializers.CharField()
|
||||
refresh = serializers.CharField()
|
||||
user = UserOutputSerializer()
|
||||
message = serializers.CharField()
|
||||
|
||||
@@ -149,14 +147,14 @@ class SignupInputSerializer(serializers.ModelSerializer):
|
||||
fields = [
|
||||
"username",
|
||||
"email",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"display_name",
|
||||
"password",
|
||||
"password_confirm",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"password": {"write_only": True},
|
||||
"email": {"required": True},
|
||||
"display_name": {"required": True},
|
||||
}
|
||||
|
||||
def validate_email(self, value):
|
||||
@@ -202,7 +200,8 @@ class SignupInputSerializer(serializers.ModelSerializer):
|
||||
class SignupOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for successful signup."""
|
||||
|
||||
token = serializers.CharField()
|
||||
access = serializers.CharField()
|
||||
refresh = serializers.CharField()
|
||||
user = UserOutputSerializer()
|
||||
message = serializers.CharField()
|
||||
|
||||
|
||||
30
backend/apps/api/v1/auth/serializers/__init__.py
Normal file
30
backend/apps/api/v1/auth/serializers/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Auth Serializers Package
|
||||
|
||||
This package contains all authentication-related serializers including
|
||||
login, signup, logout, password management, and social authentication.
|
||||
"""
|
||||
|
||||
from .social import (
|
||||
ConnectedProviderSerializer,
|
||||
AvailableProviderSerializer,
|
||||
SocialAuthStatusSerializer,
|
||||
ConnectProviderInputSerializer,
|
||||
ConnectProviderOutputSerializer,
|
||||
DisconnectProviderOutputSerializer,
|
||||
SocialProviderListOutputSerializer,
|
||||
ConnectedProvidersListOutputSerializer,
|
||||
SocialProviderErrorSerializer,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'ConnectedProviderSerializer',
|
||||
'AvailableProviderSerializer',
|
||||
'SocialAuthStatusSerializer',
|
||||
'ConnectProviderInputSerializer',
|
||||
'ConnectProviderOutputSerializer',
|
||||
'DisconnectProviderOutputSerializer',
|
||||
'SocialProviderListOutputSerializer',
|
||||
'ConnectedProvidersListOutputSerializer',
|
||||
'SocialProviderErrorSerializer',
|
||||
]
|
||||
201
backend/apps/api/v1/auth/serializers/social.py
Normal file
201
backend/apps/api/v1/auth/serializers/social.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
Social Provider Management Serializers
|
||||
|
||||
Serializers for handling social provider connection/disconnection requests
|
||||
and responses in the ThrillWiki API.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth import get_user_model
|
||||
from typing import Dict, List
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ConnectedProviderSerializer(serializers.Serializer):
|
||||
"""Serializer for connected social provider information."""
|
||||
|
||||
provider = serializers.CharField(
|
||||
help_text="Provider ID (e.g., 'google', 'discord')"
|
||||
)
|
||||
provider_name = serializers.CharField(
|
||||
help_text="Human-readable provider name"
|
||||
)
|
||||
uid = serializers.CharField(
|
||||
help_text="User ID on the social provider"
|
||||
)
|
||||
date_joined = serializers.DateTimeField(
|
||||
help_text="When this provider was connected"
|
||||
)
|
||||
can_disconnect = serializers.BooleanField(
|
||||
help_text="Whether this provider can be safely disconnected"
|
||||
)
|
||||
disconnect_reason = serializers.CharField(
|
||||
allow_null=True,
|
||||
required=False,
|
||||
help_text="Reason why provider cannot be disconnected (if applicable)"
|
||||
)
|
||||
extra_data = serializers.JSONField(
|
||||
required=False,
|
||||
help_text="Additional data from the social provider"
|
||||
)
|
||||
|
||||
|
||||
class AvailableProviderSerializer(serializers.Serializer):
|
||||
"""Serializer for available social provider information."""
|
||||
|
||||
id = serializers.CharField(
|
||||
help_text="Provider ID (e.g., 'google', 'discord')"
|
||||
)
|
||||
name = serializers.CharField(
|
||||
help_text="Human-readable provider name"
|
||||
)
|
||||
auth_url = serializers.URLField(
|
||||
help_text="URL to initiate authentication with this provider"
|
||||
)
|
||||
connect_url = serializers.URLField(
|
||||
help_text="API URL to connect this provider"
|
||||
)
|
||||
|
||||
|
||||
class SocialAuthStatusSerializer(serializers.Serializer):
|
||||
"""Serializer for comprehensive social authentication status."""
|
||||
|
||||
user_id = serializers.IntegerField(
|
||||
help_text="User's ID"
|
||||
)
|
||||
username = serializers.CharField(
|
||||
help_text="User's username"
|
||||
)
|
||||
email = serializers.EmailField(
|
||||
help_text="User's email address"
|
||||
)
|
||||
has_password_auth = serializers.BooleanField(
|
||||
help_text="Whether user has email/password authentication set up"
|
||||
)
|
||||
connected_providers = ConnectedProviderSerializer(
|
||||
many=True,
|
||||
help_text="List of connected social providers"
|
||||
)
|
||||
total_auth_methods = serializers.IntegerField(
|
||||
help_text="Total number of authentication methods available"
|
||||
)
|
||||
can_disconnect_any = serializers.BooleanField(
|
||||
help_text="Whether user can safely disconnect any provider"
|
||||
)
|
||||
requires_password_setup = serializers.BooleanField(
|
||||
help_text="Whether user needs to set up password before disconnecting"
|
||||
)
|
||||
|
||||
|
||||
class ConnectProviderInputSerializer(serializers.Serializer):
|
||||
"""Serializer for social provider connection requests."""
|
||||
|
||||
provider = serializers.CharField(
|
||||
help_text="Provider ID to connect (e.g., 'google', 'discord')"
|
||||
)
|
||||
|
||||
def validate_provider(self, value):
|
||||
"""Validate that the provider is supported and configured."""
|
||||
from apps.accounts.services.social_provider_service import SocialProviderService
|
||||
|
||||
is_valid, message = SocialProviderService.validate_provider_exists(value)
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(message)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class ConnectProviderOutputSerializer(serializers.Serializer):
|
||||
"""Serializer for social provider connection responses."""
|
||||
|
||||
success = serializers.BooleanField(
|
||||
help_text="Whether the connection was successful"
|
||||
)
|
||||
message = serializers.CharField(
|
||||
help_text="Success or error message"
|
||||
)
|
||||
provider = serializers.CharField(
|
||||
help_text="Provider that was connected"
|
||||
)
|
||||
auth_url = serializers.URLField(
|
||||
required=False,
|
||||
help_text="URL to complete the connection process"
|
||||
)
|
||||
|
||||
|
||||
class DisconnectProviderOutputSerializer(serializers.Serializer):
|
||||
"""Serializer for social provider disconnection responses."""
|
||||
|
||||
success = serializers.BooleanField(
|
||||
help_text="Whether the disconnection was successful"
|
||||
)
|
||||
message = serializers.CharField(
|
||||
help_text="Success or error message"
|
||||
)
|
||||
provider = serializers.CharField(
|
||||
help_text="Provider that was disconnected"
|
||||
)
|
||||
remaining_providers = serializers.ListField(
|
||||
child=serializers.CharField(),
|
||||
help_text="List of remaining connected providers"
|
||||
)
|
||||
has_password_auth = serializers.BooleanField(
|
||||
help_text="Whether user still has password authentication"
|
||||
)
|
||||
suggestions = serializers.ListField(
|
||||
child=serializers.CharField(),
|
||||
required=False,
|
||||
help_text="Suggestions for maintaining account access (if applicable)"
|
||||
)
|
||||
|
||||
|
||||
class SocialProviderListOutputSerializer(serializers.Serializer):
|
||||
"""Serializer for listing available social providers."""
|
||||
|
||||
available_providers = AvailableProviderSerializer(
|
||||
many=True,
|
||||
help_text="List of available social providers"
|
||||
)
|
||||
count = serializers.IntegerField(
|
||||
help_text="Number of available providers"
|
||||
)
|
||||
|
||||
|
||||
class ConnectedProvidersListOutputSerializer(serializers.Serializer):
|
||||
"""Serializer for listing connected social providers."""
|
||||
|
||||
connected_providers = ConnectedProviderSerializer(
|
||||
many=True,
|
||||
help_text="List of connected social providers"
|
||||
)
|
||||
count = serializers.IntegerField(
|
||||
help_text="Number of connected providers"
|
||||
)
|
||||
has_password_auth = serializers.BooleanField(
|
||||
help_text="Whether user has password authentication"
|
||||
)
|
||||
can_disconnect_any = serializers.BooleanField(
|
||||
help_text="Whether user can safely disconnect any provider"
|
||||
)
|
||||
|
||||
|
||||
class SocialProviderErrorSerializer(serializers.Serializer):
|
||||
"""Serializer for social provider error responses."""
|
||||
|
||||
error = serializers.CharField(
|
||||
help_text="Error message"
|
||||
)
|
||||
code = serializers.CharField(
|
||||
required=False,
|
||||
help_text="Error code for programmatic handling"
|
||||
)
|
||||
suggestions = serializers.ListField(
|
||||
child=serializers.CharField(),
|
||||
required=False,
|
||||
help_text="Suggestions for resolving the error"
|
||||
)
|
||||
provider = serializers.CharField(
|
||||
required=False,
|
||||
help_text="Provider related to the error (if applicable)"
|
||||
)
|
||||
@@ -5,31 +5,84 @@ This module contains URL patterns for core authentication functionality only.
|
||||
User profiles and top lists are handled by the dedicated accounts app.
|
||||
"""
|
||||
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from django.urls import path, include
|
||||
from .views import (
|
||||
# Main auth views
|
||||
LoginAPIView,
|
||||
SignupAPIView,
|
||||
LogoutAPIView,
|
||||
CurrentUserAPIView,
|
||||
PasswordResetAPIView,
|
||||
PasswordChangeAPIView,
|
||||
SocialProvidersAPIView,
|
||||
AuthStatusAPIView,
|
||||
# Social provider management views
|
||||
AvailableProvidersAPIView,
|
||||
ConnectedProvidersAPIView,
|
||||
ConnectProviderAPIView,
|
||||
DisconnectProviderAPIView,
|
||||
SocialAuthStatusAPIView,
|
||||
)
|
||||
from rest_framework_simplejwt.views import TokenRefreshView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
# Core authentication endpoints
|
||||
path("login/", views.LoginAPIView.as_view(), name="auth-login"),
|
||||
path("signup/", views.SignupAPIView.as_view(), name="auth-signup"),
|
||||
path("logout/", views.LogoutAPIView.as_view(), name="auth-logout"),
|
||||
path("user/", views.CurrentUserAPIView.as_view(), name="auth-current-user"),
|
||||
path("login/", LoginAPIView.as_view(), name="auth-login"),
|
||||
path("signup/", SignupAPIView.as_view(), name="auth-signup"),
|
||||
path("logout/", LogoutAPIView.as_view(), name="auth-logout"),
|
||||
path("user/", CurrentUserAPIView.as_view(), name="auth-current-user"),
|
||||
|
||||
# JWT token management
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="auth-token-refresh"),
|
||||
|
||||
# Social authentication endpoints (dj-rest-auth)
|
||||
path("social/", include("dj_rest_auth.registration.urls")),
|
||||
|
||||
path(
|
||||
"password/reset/",
|
||||
views.PasswordResetAPIView.as_view(),
|
||||
PasswordResetAPIView.as_view(),
|
||||
name="auth-password-reset",
|
||||
),
|
||||
path(
|
||||
"password/change/",
|
||||
views.PasswordChangeAPIView.as_view(),
|
||||
PasswordChangeAPIView.as_view(),
|
||||
name="auth-password-change",
|
||||
),
|
||||
path(
|
||||
"social/providers/",
|
||||
views.SocialProvidersAPIView.as_view(),
|
||||
SocialProvidersAPIView.as_view(),
|
||||
name="auth-social-providers",
|
||||
),
|
||||
path("status/", views.AuthStatusAPIView.as_view(), name="auth-status"),
|
||||
|
||||
# Social provider management endpoints
|
||||
path(
|
||||
"social/providers/available/",
|
||||
AvailableProvidersAPIView.as_view(),
|
||||
name="auth-social-providers-available",
|
||||
),
|
||||
path(
|
||||
"social/connected/",
|
||||
ConnectedProvidersAPIView.as_view(),
|
||||
name="auth-social-connected",
|
||||
),
|
||||
path(
|
||||
"social/connect/<str:provider>/",
|
||||
ConnectProviderAPIView.as_view(),
|
||||
name="auth-social-connect",
|
||||
),
|
||||
path(
|
||||
"social/disconnect/<str:provider>/",
|
||||
DisconnectProviderAPIView.as_view(),
|
||||
name="auth-social-disconnect",
|
||||
),
|
||||
path(
|
||||
"social/status/",
|
||||
SocialAuthStatusAPIView.as_view(),
|
||||
name="auth-social-status",
|
||||
),
|
||||
|
||||
path("status/", AuthStatusAPIView.as_view(), name="auth-status"),
|
||||
]
|
||||
|
||||
# Note: User profiles and top lists functionality is now handled by the accounts app
|
||||
|
||||
@@ -6,6 +6,16 @@ login, signup, logout, password management, social authentication,
|
||||
user profiles, and top lists.
|
||||
"""
|
||||
|
||||
from .serializers.social import (
|
||||
ConnectedProviderSerializer,
|
||||
AvailableProviderSerializer,
|
||||
SocialAuthStatusSerializer,
|
||||
ConnectProviderInputSerializer,
|
||||
ConnectProviderOutputSerializer,
|
||||
DisconnectProviderOutputSerializer,
|
||||
SocialProviderErrorSerializer,
|
||||
)
|
||||
from apps.accounts.services.social_provider_service import SocialProviderService
|
||||
from django.contrib.auth import authenticate, login, logout, get_user_model
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -19,7 +29,8 @@ from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
|
||||
from .serializers import (
|
||||
# Import from the main serializers.py file (not the serializers package)
|
||||
from ..serializers import (
|
||||
# Authentication serializers
|
||||
LoginInputSerializer,
|
||||
LoginOutputSerializer,
|
||||
@@ -168,13 +179,17 @@ class LoginAPIView(APIView):
|
||||
if getattr(user, "is_active", False):
|
||||
# pass a real HttpRequest to Django login
|
||||
login(_get_underlying_request(request), user)
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
token, _ = Token.objects.get_or_create(user=user)
|
||||
# Generate JWT tokens
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
refresh = RefreshToken.for_user(user)
|
||||
access_token = refresh.access_token
|
||||
|
||||
response_serializer = LoginOutputSerializer(
|
||||
{
|
||||
"token": token.key,
|
||||
"access": str(access_token),
|
||||
"refresh": str(refresh),
|
||||
"user": user,
|
||||
"message": "Login successful",
|
||||
}
|
||||
@@ -228,13 +243,17 @@ class SignupAPIView(APIView):
|
||||
user = serializer.save()
|
||||
# pass a real HttpRequest to Django login
|
||||
login(_get_underlying_request(request), user) # type: ignore[arg-type]
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
token, _ = Token.objects.get_or_create(user=user)
|
||||
# Generate JWT tokens
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
refresh = RefreshToken.for_user(user)
|
||||
access_token = refresh.access_token
|
||||
|
||||
response_serializer = SignupOutputSerializer(
|
||||
{
|
||||
"token": token.key,
|
||||
"access": str(access_token),
|
||||
"refresh": str(refresh),
|
||||
"user": user,
|
||||
"message": "Registration successful",
|
||||
}
|
||||
@@ -247,7 +266,7 @@ class SignupAPIView(APIView):
|
||||
@extend_schema_view(
|
||||
post=extend_schema(
|
||||
summary="User logout",
|
||||
description="Logout the current user and invalidate their token.",
|
||||
description="Logout the current user and blacklist their refresh token.",
|
||||
responses={
|
||||
200: LogoutOutputSerializer,
|
||||
401: "Unauthorized",
|
||||
@@ -263,7 +282,26 @@ class LogoutAPIView(APIView):
|
||||
|
||||
def post(self, request: Request) -> Response:
|
||||
try:
|
||||
# Delete the token for token-based auth
|
||||
# Get refresh token from request data with proper type handling
|
||||
refresh_token = None
|
||||
if hasattr(request, 'data') and request.data is not None:
|
||||
data = getattr(request, 'data', {})
|
||||
if hasattr(data, 'get'):
|
||||
refresh_token = data.get("refresh")
|
||||
|
||||
if refresh_token and isinstance(refresh_token, str):
|
||||
# Blacklist the refresh token
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
try:
|
||||
# Create RefreshToken from string and blacklist it
|
||||
refresh_token_obj = RefreshToken(
|
||||
refresh_token) # type: ignore[arg-type]
|
||||
refresh_token_obj.blacklist()
|
||||
except Exception:
|
||||
# Token might be invalid or already blacklisted
|
||||
pass
|
||||
|
||||
# Also delete the old token for backward compatibility
|
||||
if hasattr(request.user, "auth_token"):
|
||||
request.user.auth_token.delete()
|
||||
|
||||
@@ -464,6 +502,236 @@ class AuthStatusAPIView(APIView):
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
# === SOCIAL PROVIDER MANAGEMENT API VIEWS ===
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary="Get available social providers",
|
||||
description="Retrieve list of available social authentication providers.",
|
||||
responses={
|
||||
200: AvailableProviderSerializer(many=True),
|
||||
},
|
||||
tags=["Social Authentication"],
|
||||
),
|
||||
)
|
||||
class AvailableProvidersAPIView(APIView):
|
||||
"""API endpoint to get available social providers."""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
serializer_class = AvailableProviderSerializer
|
||||
|
||||
def get(self, request: Request) -> Response:
|
||||
providers = [
|
||||
{
|
||||
"provider": "google",
|
||||
"name": "Google",
|
||||
"login_url": "/auth/social/google/",
|
||||
"connect_url": "/auth/social/connect/google/",
|
||||
},
|
||||
{
|
||||
"provider": "discord",
|
||||
"name": "Discord",
|
||||
"login_url": "/auth/social/discord/",
|
||||
"connect_url": "/auth/social/connect/discord/",
|
||||
}
|
||||
]
|
||||
|
||||
serializer = AvailableProviderSerializer(providers, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary="Get connected social providers",
|
||||
description="Retrieve list of social providers connected to the user's account.",
|
||||
responses={
|
||||
200: ConnectedProviderSerializer(many=True),
|
||||
401: "Unauthorized",
|
||||
},
|
||||
tags=["Social Authentication"],
|
||||
),
|
||||
)
|
||||
class ConnectedProvidersAPIView(APIView):
|
||||
"""API endpoint to get user's connected social providers."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = ConnectedProviderSerializer
|
||||
|
||||
def get(self, request: Request) -> Response:
|
||||
service = SocialProviderService()
|
||||
providers = service.get_connected_providers(request.user)
|
||||
|
||||
serializer = ConnectedProviderSerializer(providers, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(
|
||||
summary="Connect social provider",
|
||||
description="Connect a social authentication provider to the user's account.",
|
||||
request=ConnectProviderInputSerializer,
|
||||
responses={
|
||||
200: ConnectProviderOutputSerializer,
|
||||
400: SocialProviderErrorSerializer,
|
||||
401: "Unauthorized",
|
||||
},
|
||||
tags=["Social Authentication"],
|
||||
),
|
||||
)
|
||||
class ConnectProviderAPIView(APIView):
|
||||
"""API endpoint to connect a social provider."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = ConnectProviderInputSerializer
|
||||
|
||||
def post(self, request: Request, provider: str) -> Response:
|
||||
# Validate provider
|
||||
if provider not in ['google', 'discord']:
|
||||
return Response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "INVALID_PROVIDER",
|
||||
"message": f"Provider '{provider}' is not supported",
|
||||
"suggestions": ["Use 'google' or 'discord'"]
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
serializer = ConnectProviderInputSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "VALIDATION_ERROR",
|
||||
"message": "Invalid request data",
|
||||
"details": serializer.errors,
|
||||
"suggestions": ["Provide a valid access_token"]
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
access_token = serializer.validated_data['access_token']
|
||||
|
||||
try:
|
||||
service = SocialProviderService()
|
||||
result = service.connect_provider(request.user, provider, access_token)
|
||||
|
||||
response_serializer = ConnectProviderOutputSerializer(result)
|
||||
return Response(response_serializer.data)
|
||||
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "CONNECTION_FAILED",
|
||||
"message": str(e),
|
||||
"suggestions": [
|
||||
"Verify the access token is valid",
|
||||
"Ensure the provider account is not already connected to another user"
|
||||
]
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(
|
||||
summary="Disconnect social provider",
|
||||
description="Disconnect a social authentication provider from the user's account.",
|
||||
responses={
|
||||
200: DisconnectProviderOutputSerializer,
|
||||
400: SocialProviderErrorSerializer,
|
||||
401: "Unauthorized",
|
||||
},
|
||||
tags=["Social Authentication"],
|
||||
),
|
||||
)
|
||||
class DisconnectProviderAPIView(APIView):
|
||||
"""API endpoint to disconnect a social provider."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = DisconnectProviderOutputSerializer
|
||||
|
||||
def post(self, request: Request, provider: str) -> Response:
|
||||
# Validate provider
|
||||
if provider not in ['google', 'discord']:
|
||||
return Response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "INVALID_PROVIDER",
|
||||
"message": f"Provider '{provider}' is not supported",
|
||||
"suggestions": ["Use 'google' or 'discord'"]
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
service = SocialProviderService()
|
||||
|
||||
# Check if disconnection is safe
|
||||
can_disconnect, reason = service.can_disconnect_provider(
|
||||
request.user, provider)
|
||||
if not can_disconnect:
|
||||
return Response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "UNSAFE_DISCONNECTION",
|
||||
"message": reason,
|
||||
"suggestions": [
|
||||
"Set up email/password authentication before disconnecting",
|
||||
"Connect another social provider before disconnecting this one"
|
||||
]
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Perform disconnection
|
||||
result = service.disconnect_provider(request.user, provider)
|
||||
|
||||
response_serializer = DisconnectProviderOutputSerializer(result)
|
||||
return Response(response_serializer.data)
|
||||
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "DISCONNECTION_FAILED",
|
||||
"message": str(e),
|
||||
"suggestions": [
|
||||
"Verify the provider is currently connected",
|
||||
"Ensure you have alternative authentication methods"
|
||||
]
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary="Get social authentication status",
|
||||
description="Get comprehensive social authentication status for the user.",
|
||||
responses={
|
||||
200: SocialAuthStatusSerializer,
|
||||
401: "Unauthorized",
|
||||
},
|
||||
tags=["Social Authentication"],
|
||||
),
|
||||
)
|
||||
class SocialAuthStatusAPIView(APIView):
|
||||
"""API endpoint to get social authentication status."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = SocialAuthStatusSerializer
|
||||
|
||||
def get(self, request: Request) -> Response:
|
||||
service = SocialProviderService()
|
||||
auth_status = service.get_auth_status(request.user)
|
||||
|
||||
serializer = SocialAuthStatusSerializer(auth_status)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
# Note: User Profile, Top List, and Top List Item ViewSets are now handled
|
||||
# by the dedicated accounts app at backend/apps/api/v1/accounts/views.py
|
||||
# to avoid duplication and maintain clean separation of concerns.
|
||||
|
||||
Reference in New Issue
Block a user