diff --git a/.replit b/.replit index 6f108e19..61cd4dfb 100644 --- a/.replit +++ b/.replit @@ -1,4 +1,4 @@ -modules = ["bash", "web", "nodejs-20", "python-3.13"] +modules = ["bash", "web", "nodejs-20", "python-3.13", "postgresql-16"] [nix] channel = "stable-25_05" diff --git a/backend/apps/api/v1/auth/urls.py b/backend/apps/api/v1/auth/urls.py index 6c8ec49f..7cd94e01 100644 --- a/backend/apps/api/v1/auth/urls.py +++ b/backend/apps/api/v1/auth/urls.py @@ -14,17 +14,10 @@ from .views import ( CurrentUserAPIView, PasswordResetAPIView, PasswordChangeAPIView, - SocialProvidersAPIView, AuthStatusAPIView, # Email verification views EmailVerificationAPIView, ResendVerificationAPIView, - # Social provider management views - AvailableProvidersAPIView, - ConnectedProvidersAPIView, - ConnectProviderAPIView, - DisconnectProviderAPIView, - SocialAuthStatusAPIView, ) from rest_framework_simplejwt.views import TokenRefreshView @@ -52,38 +45,6 @@ urlpatterns = [ PasswordChangeAPIView.as_view(), name="auth-password-change", ), - path( - "social/providers/", - SocialProvidersAPIView.as_view(), - name="auth-social-providers", - ), - - # 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//", - ConnectProviderAPIView.as_view(), - name="auth-social-connect", - ), - path( - "social/disconnect//", - 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"), diff --git a/backend/apps/api/v1/auth/views.py b/backend/apps/api/v1/auth/views.py index bef03aef..d3cf10e6 100644 --- a/backend/apps/api/v1/auth/views.py +++ b/backend/apps/api/v1/auth/views.py @@ -6,16 +6,6 @@ login, signup, logout, password management, social authentication, user profiles, and top lists. """ -from .serializers_package.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 @@ -42,7 +32,6 @@ from .serializers import ( PasswordResetOutputSerializer, PasswordChangeInputSerializer, PasswordChangeOutputSerializer, - SocialProviderOutputSerializer, AuthStatusOutputSerializer, ) @@ -406,69 +395,6 @@ class PasswordChangeAPIView(APIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -@extend_schema_view( - get=extend_schema( - summary="Get social providers", - description="Retrieve available social authentication providers.", - responses={200: "List of social providers"}, - tags=["Authentication"], - ), -) -class SocialProvidersAPIView(APIView): - """API endpoint to get available social authentication providers.""" - - permission_classes = [AllowAny] - serializer_class = SocialProviderOutputSerializer - - def get(self, request: Request) -> Response: - from django.core.cache import cache - - # get_current_site expects a django HttpRequest; _get_underlying_request now returns HttpRequest - site = get_current_site(_get_underlying_request(request)) - - # Cache key based on site and request host - use getattr to avoid attribute errors - site_id = getattr(site, "id", getattr(site, "pk", None)) - cache_key = f"social_providers:{site_id}:{request.get_host()}" - - # Try to get from cache first (cache for 15 minutes) - cached_providers = cache.get(cache_key) - if cached_providers is not None: - return Response(cached_providers) - - providers_list = [] - - # Optimized query: filter by site and order by provider name - from allauth.socialaccount.models import SocialApp - - social_apps = SocialApp.objects.filter(sites=site).order_by("provider") - - for social_app in social_apps: - try: - provider_name = ( - social_app.name or getattr(social_app, "provider", "").title() - ) - - auth_url = request.build_absolute_uri( - f"/accounts/{social_app.provider}/login/" - ) - - providers_list.append( - { - "id": social_app.provider, - "name": provider_name, - "authUrl": auth_url, - } - ) - - except Exception: - continue - - serializer = SocialProviderOutputSerializer(providers_list, many=True) - response_data = serializer.data - - cache.set(cache_key, response_data, 900) - - return Response(response_data) @extend_schema_view( @@ -501,234 +427,14 @@ 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) # === EMAIL VERIFICATION API VIEWS === diff --git a/backend/static/js/alpine-components.js b/backend/static/js/alpine-components.js index 00925fc1..54a4fdbf 100644 --- a/backend/static/js/alpine-components.js +++ b/backend/static/js/alpine-components.js @@ -340,8 +340,11 @@ Alpine.data('authModal', (defaultMode = 'login') => ({ open: false, mode: defaultMode, // 'login' or 'register' showPassword: false, - socialProviders: [], - socialLoading: true, + socialProviders: [ + {id: 'google', name: 'Google', auth_url: '/accounts/google/login/'}, + {id: 'discord', name: 'Discord', auth_url: '/accounts/discord/login/'} + ], + socialLoading: false, // Login form data loginForm: { @@ -364,8 +367,6 @@ Alpine.data('authModal', (defaultMode = 'login') => ({ registerError: '', init() { - this.fetchSocialProviders(); - // Listen for auth modal events this.$watch('open', (value) => { if (value) { @@ -377,18 +378,6 @@ Alpine.data('authModal', (defaultMode = 'login') => ({ }); }, - async fetchSocialProviders() { - try { - const response = await fetch('/api/v1/auth/social/providers/'); - const data = await response.json(); - this.socialProviders = data.available_providers || []; - } catch (error) { - console.error('Failed to fetch social providers:', error); - this.socialProviders = []; - } finally { - this.socialLoading = false; - } - }, show(mode = 'login') { this.mode = mode;