views.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. import base64
  2. import binascii
  3. import django.core.exceptions
  4. from django.conf import settings
  5. from django.contrib.auth import user_logged_in
  6. from django.core.mail import EmailMessage
  7. from django.http import Http404
  8. from django.template.loader import get_template
  9. from rest_framework import generics
  10. from rest_framework import mixins
  11. from rest_framework import status
  12. from rest_framework.authentication import get_authorization_header, BaseAuthentication
  13. from rest_framework.exceptions import (NotFound, PermissionDenied, ValidationError)
  14. from rest_framework.generics import (
  15. GenericAPIView, ListCreateAPIView, RetrieveUpdateDestroyAPIView, UpdateAPIView, get_object_or_404
  16. )
  17. from rest_framework.permissions import IsAuthenticated
  18. from rest_framework.response import Response
  19. from rest_framework.reverse import reverse
  20. from rest_framework.views import APIView
  21. from rest_framework.viewsets import GenericViewSet
  22. import desecapi.authentication as auth
  23. from desecapi import serializers
  24. from desecapi.models import Domain, User, RRset, Token, AuthenticatedActivateUserAction, AuthenticatedChangeEmailUserAction, AuthenticatedDeleteUserAction, \
  25. AuthenticatedResetPasswordUserAction
  26. from desecapi.pdns_change_tracker import PDNSChangeTracker
  27. from desecapi.permissions import IsOwner, IsDomainOwner
  28. from desecapi.renderers import PlainTextRenderer
  29. class IdempotentDestroy:
  30. def destroy(self, request, *args, **kwargs):
  31. try:
  32. # noinspection PyUnresolvedReferences
  33. super().destroy(request, *args, **kwargs)
  34. except Http404:
  35. pass
  36. return Response(status=status.HTTP_204_NO_CONTENT)
  37. class DomainView:
  38. def initial(self, request, *args, **kwargs):
  39. # noinspection PyUnresolvedReferences
  40. super().initial(request, *args, **kwargs)
  41. try:
  42. # noinspection PyAttributeOutsideInit, PyUnresolvedReferences
  43. self.domain = self.request.user.domains.get(name=self.kwargs['name'])
  44. except Domain.DoesNotExist:
  45. raise Http404
  46. class TokenViewSet(IdempotentDestroy,
  47. mixins.CreateModelMixin,
  48. mixins.DestroyModelMixin,
  49. mixins.ListModelMixin,
  50. GenericViewSet):
  51. serializer_class = serializers.TokenSerializer
  52. permission_classes = (IsAuthenticated, )
  53. lookup_field = 'user_specific_id'
  54. def get_queryset(self):
  55. return self.request.user.auth_tokens.all()
  56. def perform_create(self, serializer):
  57. serializer.save(user=self.request.user)
  58. class DomainList(ListCreateAPIView):
  59. serializer_class = serializers.DomainSerializer
  60. permission_classes = (IsAuthenticated, IsOwner,)
  61. def get_queryset(self):
  62. return Domain.objects.filter(owner=self.request.user.pk)
  63. def perform_create(self, serializer):
  64. _, parent_domain_name = Domain.partition_name(serializer.validated_data['name'])
  65. domain_is_local = parent_domain_name in settings.LOCAL_PUBLIC_SUFFIXES
  66. domain_kwargs = {'owner': self.request.user}
  67. if domain_is_local:
  68. domain_kwargs['minimum_ttl'] = 60
  69. with PDNSChangeTracker():
  70. domain = serializer.save(**domain_kwargs)
  71. PDNSChangeTracker.track(lambda: self.auto_delegate(domain))
  72. # Send dyn email
  73. if domain.name.endswith('.dedyn.io'):
  74. content_tmpl = get_template('emails/domain-dyndns/content.txt')
  75. subject_tmpl = get_template('emails/domain-dyndns/subject.txt')
  76. from_tmpl = get_template('emails/from.txt')
  77. context = {
  78. 'domain': domain.name,
  79. 'url': 'https://update.dedyn.io/',
  80. 'username': domain.name,
  81. 'password': self.request.auth.key
  82. }
  83. email = EmailMessage(subject_tmpl.render(context),
  84. content_tmpl.render(context),
  85. from_tmpl.render(context),
  86. [self.request.user.email])
  87. email.send()
  88. @staticmethod
  89. def auto_delegate(domain: Domain):
  90. parent_domain_name = domain.partition_name()[1]
  91. if parent_domain_name in settings.LOCAL_PUBLIC_SUFFIXES:
  92. parent_domain = Domain.objects.get(name=parent_domain_name)
  93. parent_domain.update_delegation(domain)
  94. class DomainDetail(IdempotentDestroy, RetrieveUpdateDestroyAPIView):
  95. serializer_class = serializers.DomainSerializer
  96. permission_classes = (IsAuthenticated, IsOwner,)
  97. lookup_field = 'name'
  98. def perform_destroy(self, instance: Domain):
  99. with PDNSChangeTracker():
  100. instance.delete()
  101. if instance.has_local_public_suffix():
  102. parent_domain = Domain.objects.get(name=instance.parent_domain_name())
  103. with PDNSChangeTracker():
  104. parent_domain.update_delegation(instance)
  105. def get_queryset(self):
  106. return Domain.objects.filter(owner=self.request.user.pk)
  107. def update(self, request, *args, **kwargs):
  108. try:
  109. return super().update(request, *args, **kwargs)
  110. except django.core.exceptions.ValidationError as e:
  111. raise ValidationError(detail={"detail": e.message})
  112. class RRsetDetail(IdempotentDestroy, DomainView, RetrieveUpdateDestroyAPIView):
  113. serializer_class = serializers.RRsetSerializer
  114. permission_classes = (IsAuthenticated, IsDomainOwner,)
  115. def get_queryset(self):
  116. return self.domain.rrset_set
  117. def get_object(self):
  118. queryset = self.filter_queryset(self.get_queryset())
  119. filter_kwargs = {k: self.kwargs[k] for k in ['subname', 'type']}
  120. obj = get_object_or_404(queryset, **filter_kwargs)
  121. # May raise a permission denied
  122. self.check_object_permissions(self.request, obj)
  123. return obj
  124. def get_serializer(self, *args, **kwargs):
  125. kwargs['domain'] = self.domain
  126. return super().get_serializer(*args, **kwargs)
  127. def update(self, request, *args, **kwargs):
  128. response = super().update(request, *args, **kwargs)
  129. if response.data is None:
  130. response.status_code = 204
  131. return response
  132. def perform_update(self, serializer):
  133. with PDNSChangeTracker():
  134. super().perform_update(serializer)
  135. def perform_destroy(self, instance):
  136. with PDNSChangeTracker():
  137. super().perform_destroy(instance)
  138. class RRsetList(DomainView, ListCreateAPIView, UpdateAPIView):
  139. serializer_class = serializers.RRsetSerializer
  140. permission_classes = (IsAuthenticated, IsDomainOwner,)
  141. def get_queryset(self):
  142. rrsets = RRset.objects.filter(domain=self.domain)
  143. for filter_field in ('subname', 'type'):
  144. value = self.request.query_params.get(filter_field)
  145. if value is not None:
  146. # TODO consider moving this
  147. if filter_field == 'type' and value in RRset.RESTRICTED_TYPES:
  148. raise PermissionDenied("You cannot tinker with the %s RRset." % value)
  149. rrsets = rrsets.filter(**{'%s__exact' % filter_field: value})
  150. return rrsets
  151. def get_object(self):
  152. # For this view, the object we're operating on is the queryset that one can also GET. Serializing a queryset
  153. # is fine as per https://www.django-rest-framework.org/api-guide/serializers/#serializing-multiple-objects.
  154. # We skip checking object permissions here to avoid evaluating the queryset. The user can access all his RRsets
  155. # anyways.
  156. return self.filter_queryset(self.get_queryset())
  157. def get_serializer(self, *args, **kwargs):
  158. data = kwargs.get('data')
  159. if data and 'many' not in kwargs:
  160. if self.request.method == 'POST':
  161. kwargs['many'] = isinstance(data, list)
  162. elif self.request.method in ['PATCH', 'PUT']:
  163. kwargs['many'] = True
  164. return super().get_serializer(domain=self.domain, *args, **kwargs)
  165. def perform_create(self, serializer):
  166. with PDNSChangeTracker():
  167. serializer.save(domain=self.domain)
  168. def perform_update(self, serializer):
  169. with PDNSChangeTracker():
  170. serializer.save(domain=self.domain)
  171. class Root(APIView):
  172. def get(self, request, *_):
  173. if self.request.user.is_authenticated:
  174. routes = {
  175. 'account': {
  176. 'show': reverse('account', request=request),
  177. 'delete': reverse('account-delete', request=request),
  178. 'change-email': reverse('account-change-email', request=request),
  179. 'reset-password': reverse('account-reset-password', request=request),
  180. },
  181. 'tokens': reverse('token-list', request=request),
  182. 'domains': reverse('domain-list', request=request),
  183. }
  184. else:
  185. routes = {
  186. 'register': reverse('register', request=request),
  187. 'login': reverse('login', request=request),
  188. 'reset-password': reverse('account-reset-password', request=request),
  189. }
  190. return Response(routes)
  191. class DynDNS12Update(APIView):
  192. authentication_classes = (auth.TokenAuthentication, auth.BasicTokenAuthentication, auth.URLParamAuthentication,)
  193. renderer_classes = [PlainTextRenderer]
  194. def _find_domain(self, request):
  195. def find_domain_name(r):
  196. # 1. hostname parameter
  197. if 'hostname' in r.query_params and r.query_params['hostname'] != 'YES':
  198. return r.query_params['hostname']
  199. # 2. host_id parameter
  200. if 'host_id' in r.query_params:
  201. return r.query_params['host_id']
  202. # 3. http basic auth username
  203. try:
  204. domain_name = base64.b64decode(
  205. get_authorization_header(r).decode().split(' ')[1].encode()).decode().split(':')[0]
  206. if domain_name and '@' not in domain_name:
  207. return domain_name
  208. except IndexError:
  209. pass
  210. except UnicodeDecodeError:
  211. pass
  212. except binascii.Error:
  213. pass
  214. # 4. username parameter
  215. if 'username' in r.query_params:
  216. return r.query_params['username']
  217. # 5. only domain associated with this user account
  218. if len(r.user.domains.all()) == 1:
  219. return r.user.domains.all()[0].name
  220. if len(r.user.domains.all()) > 1:
  221. ex = ValidationError(detail={
  222. "detail": "Request does not specify domain unambiguously.",
  223. "code": "domain-ambiguous"
  224. })
  225. ex.status_code = status.HTTP_409_CONFLICT
  226. raise ex
  227. return None
  228. name = find_domain_name(request).lower()
  229. try:
  230. return self.request.user.domains.get(name=name)
  231. except Domain.DoesNotExist:
  232. return None
  233. @staticmethod
  234. def find_ip(request, params, version=4):
  235. if version == 4:
  236. look_for = '.'
  237. elif version == 6:
  238. look_for = ':'
  239. else:
  240. raise Exception
  241. # Check URL parameters
  242. for p in params:
  243. if p in request.query_params:
  244. if not len(request.query_params[p]):
  245. return None
  246. if look_for in request.query_params[p]:
  247. return request.query_params[p]
  248. # Check remote IP address
  249. client_ip = request.META.get('REMOTE_ADDR')
  250. if look_for in client_ip:
  251. return client_ip
  252. # give up
  253. return None
  254. def _find_ip_v4(self, request):
  255. return self.find_ip(request, ['myip', 'myipv4', 'ip'])
  256. def _find_ip_v6(self, request):
  257. return self.find_ip(request, ['myipv6', 'ipv6', 'myip', 'ip'], version=6)
  258. def get(self, request, *_):
  259. domain = self._find_domain(request)
  260. if domain is None:
  261. raise NotFound('nohost')
  262. ipv4 = self._find_ip_v4(request)
  263. ipv6 = self._find_ip_v6(request)
  264. data = [
  265. {'type': 'A', 'subname': '', 'ttl': 60, 'records': [ipv4] if ipv4 else []},
  266. {'type': 'AAAA', 'subname': '', 'ttl': 60, 'records': [ipv6] if ipv6 else []},
  267. ]
  268. instances = domain.rrset_set.filter(subname='', type__in=['A', 'AAAA']).all()
  269. serializer = serializers.RRsetSerializer(instances, domain=domain, data=data, many=True, partial=True)
  270. try:
  271. serializer.is_valid(raise_exception=True)
  272. except ValidationError as e:
  273. raise e
  274. with PDNSChangeTracker():
  275. serializer.save(domain=domain)
  276. return Response('good', content_type='text/plain')
  277. class DonationList(generics.CreateAPIView):
  278. serializer_class = serializers.DonationSerializer
  279. def perform_create(self, serializer):
  280. iban = serializer.validated_data['iban']
  281. obj = serializer.save()
  282. def send_donation_emails(donation):
  283. context = {
  284. 'donation': donation,
  285. 'creditoridentifier': settings.SEPA['CREDITOR_ID'],
  286. 'creditorname': settings.SEPA['CREDITOR_NAME'],
  287. 'complete_iban': iban
  288. }
  289. # internal desec notification
  290. content_tmpl = get_template('emails/donation/desec-content.txt')
  291. subject_tmpl = get_template('emails/donation/desec-subject.txt')
  292. attachment_tmpl = get_template('emails/donation/desec-attachment-jameica.txt')
  293. from_tmpl = get_template('emails/from.txt')
  294. email = EmailMessage(subject_tmpl.render(context),
  295. content_tmpl.render(context),
  296. from_tmpl.render(context),
  297. ['donation@desec.io'],
  298. attachments=[
  299. ('jameica-directdebit.xml',
  300. attachment_tmpl.render(context),
  301. 'text/xml')
  302. ])
  303. email.send()
  304. # donor notification
  305. if donation.email:
  306. content_tmpl = get_template('emails/donation/donor-content.txt')
  307. subject_tmpl = get_template('emails/donation/donor-subject.txt')
  308. email = EmailMessage(subject_tmpl.render(context),
  309. content_tmpl.render(context),
  310. from_tmpl.render(context),
  311. [donation.email])
  312. email.send()
  313. # send emails
  314. send_donation_emails(obj)
  315. class AccountCreateView(generics.CreateAPIView):
  316. serializer_class = serializers.RegisterAccountSerializer
  317. def create(self, request, *args, **kwargs):
  318. # Create user and send trigger email verification.
  319. # Alternative would be to create user once email is verified, but this could be abused for bulk email.
  320. serializer = self.get_serializer(data=request.data)
  321. activation_required = settings.USER_ACTIVATION_REQUIRED
  322. try:
  323. serializer.is_valid(raise_exception=True)
  324. except ValidationError as e:
  325. # Hide existing users
  326. email_detail = e.detail.pop('email', [])
  327. email_detail = [detail for detail in email_detail if detail.code != 'unique']
  328. if email_detail:
  329. e.detail['email'] = email_detail
  330. if e.detail:
  331. raise e
  332. else:
  333. ip = self.request.META.get('REMOTE_ADDR')
  334. user = serializer.save(is_active=(not activation_required), registration_remote_ip=ip)
  335. domain = serializer.validated_data.get('domain')
  336. if domain or activation_required:
  337. action = AuthenticatedActivateUserAction(user=user, domain=domain)
  338. verification_code = serializers.AuthenticatedActivateUserActionSerializer(action).data['code']
  339. user.send_email('activate-with-domain' if domain else 'activate', context={
  340. 'confirmation_link': reverse('confirm-activate-account', request=request, args=[verification_code])
  341. })
  342. # This request is unauthenticated, so don't expose whether we did anything.
  343. message = 'Welcome! Please check your mailbox.' if activation_required else 'Welcome!'
  344. return Response(data={'detail': message}, status=status.HTTP_202_ACCEPTED)
  345. class AccountView(generics.RetrieveAPIView):
  346. permission_classes = (IsAuthenticated,)
  347. serializer_class = serializers.UserSerializer
  348. def get_object(self):
  349. return self.request.user
  350. class AccountDeleteView(GenericAPIView):
  351. authentication_classes = (auth.EmailPasswordPayloadAuthentication,)
  352. permission_classes = (IsAuthenticated,)
  353. def post(self, request, *args, **kwargs):
  354. action = AuthenticatedDeleteUserAction(user=self.request.user)
  355. verification_code = serializers.AuthenticatedDeleteUserActionSerializer(action).data['code']
  356. request.user.send_email('delete-user', context={
  357. 'confirmation_link': reverse('confirm-delete-account', request=request, args=[verification_code])
  358. })
  359. return Response(data={'detail': 'Please check your mailbox for further account deletion instructions.'},
  360. status=status.HTTP_202_ACCEPTED)
  361. class AccountLoginView(GenericAPIView):
  362. authentication_classes = (auth.EmailPasswordPayloadAuthentication,)
  363. permission_classes = (IsAuthenticated,)
  364. def post(self, request, *args, **kwargs):
  365. user = self.request.user
  366. token = Token.objects.create(user=user, name="login")
  367. user_logged_in.send(sender=user.__class__, request=self.request, user=user)
  368. data = serializers.TokenSerializer(token).data
  369. return Response(data)
  370. class AccountChangeEmailView(GenericAPIView):
  371. authentication_classes = (auth.EmailPasswordPayloadAuthentication,)
  372. permission_classes = (IsAuthenticated,)
  373. serializer_class = serializers.ChangeEmailSerializer
  374. def post(self, request, *args, **kwargs):
  375. # Check password and extract email
  376. serializer = self.get_serializer(data=request.data)
  377. serializer.is_valid(raise_exception=True)
  378. new_email = serializer.validated_data['new_email']
  379. action = AuthenticatedChangeEmailUserAction(user=request.user, new_email=new_email)
  380. verification_code = serializers.AuthenticatedChangeEmailUserActionSerializer(action).data['code']
  381. request.user.send_email('change-email', recipient=new_email, context={
  382. 'confirmation_link': reverse('confirm-change-email', request=request, args=[verification_code]),
  383. 'old_email': request.user.email,
  384. 'new_email': new_email,
  385. })
  386. # At this point, we know that we are talking to the user, so we can tell that we sent an email.
  387. return Response(data={'detail': 'Please check your mailbox to confirm email address change.'},
  388. status=status.HTTP_202_ACCEPTED)
  389. class AccountResetPasswordView(GenericAPIView):
  390. serializer_class = serializers.EmailSerializer
  391. def post(self, request, *args, **kwargs):
  392. serializer = self.get_serializer(data=request.data)
  393. serializer.is_valid(raise_exception=True)
  394. try:
  395. email = serializer.validated_data['email']
  396. user = User.objects.get(email=email, is_active=True)
  397. except User.DoesNotExist:
  398. pass
  399. else:
  400. action = AuthenticatedResetPasswordUserAction(user=user)
  401. verification_code = serializers.AuthenticatedResetPasswordUserActionSerializer(action).data['code']
  402. user.send_email('reset-password', context={
  403. 'confirmation_link': reverse('confirm-reset-password', request=request, args=[verification_code])
  404. })
  405. # This request is unauthenticated, so don't expose whether we did anything.
  406. return Response(data={'detail': 'Please check your mailbox for further password reset instructions. '
  407. 'If you did not receive an email, please contact support.'},
  408. status=status.HTTP_202_ACCEPTED)
  409. class AuthenticatedActionView(GenericAPIView):
  410. """
  411. Abstract class. Deserializes the given payload according the serializers specified by the view extending
  412. this class. If the `serializer.is_valid`, `act` is called on the action object.
  413. """
  414. class AuthenticatedActionAuthenticator(BaseAuthentication):
  415. """
  416. Authenticates a request based on whether the serializer determines the validity of the given verification code
  417. and additional data (using `serializer.is_valid()`). The serializer's input data will be determined by (a) the
  418. view's 'code' kwarg and (b) the request payload for POST requests. Request methods other than GET and POST will
  419. fail authentication regardless of other conditions.
  420. If the request is valid, the AuthenticatedAction instance will be attached to the view as `authenticated_action`
  421. attribute.
  422. Note that this class will raise ValidationError instead of AuthenticationFailed, usually resulting in status
  423. 400 instead of 403.
  424. """
  425. def __init__(self, view):
  426. super().__init__()
  427. self.view = view
  428. def authenticate(self, request):
  429. data = {**request.data, 'code': self.view.kwargs['code']} # order crucial to avoid override from payload!
  430. serializer = self.view.serializer_class(data=data, context=self.view.get_serializer_context())
  431. serializer.is_valid(raise_exception=True)
  432. self.view.authenticated_action = serializer.instance
  433. return self.view.authenticated_action.user, None
  434. def __init__(self, *args, **kwargs):
  435. super().__init__(*args, **kwargs)
  436. self.authenticated_action = None
  437. def get_authenticators(self):
  438. return [self.AuthenticatedActionAuthenticator(self)]
  439. def get(self, request, *args, **kwargs):
  440. return self.take_action()
  441. def post(self, request, *args, **kwargs):
  442. return self.take_action()
  443. def finalize(self):
  444. raise NotImplementedError
  445. def take_action(self):
  446. # execute the action
  447. self.authenticated_action.act()
  448. return self.finalize()
  449. class AuthenticatedActivateUserActionView(AuthenticatedActionView):
  450. http_method_names = ['get']
  451. serializer_class = serializers.AuthenticatedActivateUserActionSerializer
  452. def finalize(self):
  453. action = self.authenticated_action
  454. if not action.domain:
  455. return Response({
  456. 'detail': 'Success! Please log in at {}.'.format(self.request.build_absolute_uri(reverse('v1:login')))
  457. })
  458. serializer = serializers.DomainSerializer(
  459. data={'name': action.domain},
  460. context=self.get_serializer_context()
  461. )
  462. try:
  463. serializer.is_valid(raise_exception=True)
  464. except ValidationError as e: # e.g. domain name unavailable
  465. action.user.delete()
  466. reasons = ', '.join([detail.code for detail in e.detail.get('name', [])])
  467. raise ValidationError(
  468. f'The requested domain {action.domain} could not be registered (reason: {reasons}). '
  469. f'Please start over and sign up again.'
  470. )
  471. domain = PDNSChangeTracker.track(lambda: serializer.save(owner=action.user))
  472. if domain.parent_domain_name() in settings.LOCAL_PUBLIC_SUFFIXES:
  473. PDNSChangeTracker.track(lambda: DomainList.auto_delegate(domain))
  474. token = Token.objects.create(user=action.user, name='dyndns')
  475. return Response({
  476. 'detail': 'Success! Here is the password ("auth_token") to configure your router (or any other dynDNS '
  477. 'client). This password is different from your account password for security reasons.',
  478. **serializers.TokenSerializer(token).data,
  479. })
  480. else:
  481. return Response({
  482. 'detail': 'Success! Please check the docs for the next steps, https://desec.readthedocs.io/.'
  483. })
  484. class AuthenticatedChangeEmailUserActionView(AuthenticatedActionView):
  485. http_method_names = ['get']
  486. serializer_class = serializers.AuthenticatedChangeEmailUserActionSerializer
  487. def finalize(self):
  488. return Response({
  489. 'detail': f'Success! Your email address has been changed to {self.authenticated_action.user.email}.'
  490. })
  491. class AuthenticatedResetPasswordUserActionView(AuthenticatedActionView):
  492. http_method_names = ['post']
  493. serializer_class = serializers.AuthenticatedResetPasswordUserActionSerializer
  494. def finalize(self):
  495. return Response({'detail': 'Success! Your password has been changed.'})
  496. class AuthenticatedDeleteUserActionView(AuthenticatedActionView):
  497. http_method_names = ['get']
  498. serializer_class = serializers.AuthenticatedDeleteUserActionSerializer
  499. def finalize(self):
  500. return Response({'detail': 'All your data has been deleted. Bye bye, see you soon! <3'})