views.py 26 KB

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