diff --git a/django/thunderstore/api/cyberstorm/views/user.py b/django/thunderstore/api/cyberstorm/views/user.py index 32a122790..5fa6ef90f 100644 --- a/django/thunderstore/api/cyberstorm/views/user.py +++ b/django/thunderstore/api/cyberstorm/views/user.py @@ -1,12 +1,23 @@ from django.http import HttpRequest -from rest_framework import serializers -from rest_framework.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers, status +from rest_framework.exceptions import APIException, ValidationError from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from thunderstore.api.utils import conditional_swagger_auto_schema -from thunderstore.social.views import DeleteAccountForm +from thunderstore.social.views import ( + DeleteAccountForm, + LinkedAccountDisconnectExecption, + LinkedAccountDisconnectForm, +) + + +class CyberstormException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _("Issue occured when trying to process action") + default_code = "error" class CyberstormUserDeleteRequestSerialiazer(serializers.Serializer): @@ -38,3 +49,47 @@ def post(self, request: HttpRequest): return Response() else: raise ValidationError(form.errors) + + +class CyberstormUserDisconnectProviderRequestSerialiazer(serializers.Serializer): + provider = serializers.CharField() + + +class CyberstormUserDisconnectProviderResponseSerialiazer(serializers.Serializer): + username = serializers.CharField() + provider = serializers.CharField() + + +class UserLinkedAccountDisconnectAPIView(APIView): + permission_classes = [IsAuthenticated] + + @conditional_swagger_auto_schema( + request_body=CyberstormUserDisconnectProviderRequestSerialiazer, + responses={200: CyberstormUserDisconnectProviderResponseSerialiazer}, + operation_id="cyberstorm.current-user.linked-account-disconnect", + tags=["cyberstorm"], + ) + def post(self, request: HttpRequest): + serializer = CyberstormUserDisconnectProviderRequestSerialiazer( + data=request.data + ) + serializer.is_valid(raise_exception=True) + form = LinkedAccountDisconnectForm( + user=request.user, + data=serializer.validated_data, + ) + if form.is_valid(): + try: + form.disconnect_account(with_raise=True) + except LinkedAccountDisconnectExecption as e: + raise CyberstormException(detail=e) + return Response( + CyberstormUserDisconnectProviderResponseSerialiazer( + { + "username": request.user.username, + "provider": serializer.validated_data["provider"], + } + ).data + ) + else: + raise ValidationError(form.errors) diff --git a/django/thunderstore/api/urls.py b/django/thunderstore/api/urls.py index 4aa1a8e44..71212e510 100644 --- a/django/thunderstore/api/urls.py +++ b/django/thunderstore/api/urls.py @@ -24,7 +24,10 @@ TeamMemberListAPIView, TeamServiceAccountListAPIView, ) -from thunderstore.api.cyberstorm.views.user import UserDeleteAPIView +from thunderstore.api.cyberstorm.views.user import ( + UserDeleteAPIView, + UserLinkedAccountDisconnectAPIView, +) cyberstorm_urls = [ path( @@ -142,6 +145,11 @@ UserDeleteAPIView.as_view(), name="cyberstorm.current-user.delete", ), + path( + "current-user/linked-account-disconnect/", + UserLinkedAccountDisconnectAPIView.as_view(), + name="cyberstorm.current-user.linked-account-disconnect", + ), path( "team//members/remove/", RemoveTeamMemberAPIView.as_view(), diff --git a/django/thunderstore/social/views.py b/django/thunderstore/social/views.py index d79662d85..697b14625 100644 --- a/django/thunderstore/social/views.py +++ b/django/thunderstore/social/views.py @@ -6,9 +6,43 @@ from thunderstore.frontend.views import SettingsViewMixin +class LinkedAccountDisconnectExecption(Exception): + """Some problem with disconnecting a linked account""" + + pass + + class LinkedAccountDisconnectForm(forms.Form): provider = forms.CharField() + def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user") + super().__init__(*args, **kwargs) + + @property + def can_disconnect(self): + return self.user.social_auth.count() > 1 + + def clean_provider(self): + data = self.cleaned_data["provider"] + if data in ["github", "discord", "overwolf"]: + return data + else: + raise forms.ValidationError("Invalid provider") + + def disconnect_account(self, with_raise=False): + if not self.can_disconnect: + if with_raise: + raise LinkedAccountDisconnectExecption( + "User must have at least one linked account" + ) + else: + return + social_auth = self.user.social_auth.filter( + provider=self.cleaned_data["provider"] + ).first() + social_auth.delete() + class LinkedAccountsView(SettingsViewMixin, RequireAuthenticationMixin, FormView): template_name = "settings/linked_accounts.html" @@ -25,14 +59,8 @@ def get_context_data(self, **kwargs): def can_disconnect(self): return self.request.user.social_auth.count() > 1 - def disconnect_account(self, provider): - if not self.can_disconnect: - return - social_auth = self.request.user.social_auth.filter(provider=provider).first() - social_auth.delete() - def form_valid(self, form): - self.disconnect_account(form.cleaned_data["provider"]) + form.disconnect_account() return super().form_valid(form)