Browse Source

feat(api): introduce auth action to verify email for legacy accounts

This commit can be reverted once all email addresses have been
verified.

Usage:
```python
from desecapi import models
for user in models.User.objects.filter(email_verified__isnull=True, is_active=True).all():
    user.send_confirmation_email('confirm-account', created=user.created)
```
Peter Thomassen 3 years ago
parent
commit
1879bad0fb

+ 24 - 0
api/desecapi/migrations/0021_authenticatednoopuseraction.py

@@ -0,0 +1,24 @@
+# Generated by Django 4.0.1 on 2022-01-19 14:41
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('desecapi', '0020_user_email_verified'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='AuthenticatedNoopUserAction',
+            fields=[
+                ('authenticateduseraction_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='desecapi.authenticateduseraction')),
+            ],
+            options={
+                'managed': False,
+            },
+            bases=('desecapi.authenticateduseraction',),
+        ),
+    ]

+ 10 - 0
api/desecapi/models.py

@@ -169,6 +169,7 @@ class User(ExportModelOperationsMixin('User'), AbstractBaseUser):
             'activate-account': slow_lane,
             'activate-account': slow_lane,
             'change-email': slow_lane,
             'change-email': slow_lane,
             'change-email-confirmation-old-email': fast_lane,
             'change-email-confirmation-old-email': fast_lane,
+            'confirm-account': slow_lane,
             'password-change-confirmation': fast_lane,
             'password-change-confirmation': fast_lane,
             'reset-password': fast_lane,
             'reset-password': fast_lane,
             'delete-account': fast_lane,
             'delete-account': fast_lane,
@@ -1022,6 +1023,15 @@ class AuthenticatedChangeEmailUserAction(AuthenticatedUserAction):
         self.user.change_email(self.new_email)
         self.user.change_email(self.new_email)
 
 
 
 
+class AuthenticatedNoopUserAction(AuthenticatedUserAction):
+
+    class Meta:
+        managed = False
+
+    def _act(self):
+        pass
+
+
 class AuthenticatedResetPasswordUserAction(AuthenticatedUserAction):
 class AuthenticatedResetPasswordUserAction(AuthenticatedUserAction):
     new_password = models.CharField(max_length=128)
     new_password = models.CharField(max_length=128)
 
 

+ 8 - 0
api/desecapi/serializers.py

@@ -3,6 +3,7 @@ import copy
 import json
 import json
 import re
 import re
 from base64 import b64encode
 from base64 import b64encode
+from datetime import timedelta
 
 
 import django.core.exceptions
 import django.core.exceptions
 from captcha.audio import AudioCaptcha
 from captcha.audio import AudioCaptcha
@@ -839,6 +840,13 @@ class AuthenticatedChangeEmailUserActionSerializer(AuthenticatedBasicUserActionS
         fields = AuthenticatedBasicUserActionSerializer.Meta.fields + ('new_email',)
         fields = AuthenticatedBasicUserActionSerializer.Meta.fields + ('new_email',)
 
 
 
 
+class AuthenticatedConfirmAccountUserActionSerializer(AuthenticatedBasicUserActionSerializer):
+    validity_period = timedelta(days=14)
+
+    class Meta(AuthenticatedBasicUserActionSerializer.Meta):
+        model = models.AuthenticatedNoopUserAction  # confirmation happens during authentication, so nothing left to do
+
+
 class AuthenticatedResetPasswordUserActionSerializer(AuthenticatedBasicUserActionSerializer):
 class AuthenticatedResetPasswordUserActionSerializer(AuthenticatedBasicUserActionSerializer):
     new_password = serializers.CharField(write_only=True)
     new_password = serializers.CharField(write_only=True)
 
 

+ 25 - 0
api/desecapi/templates/emails/confirm-account/content.txt

@@ -0,0 +1,25 @@
+Hi there,
+
+You have registered with deSEC a long time ago (on {{ created|date:"Y-m-d" }}), probably to
+set up a DNS domain (e.g. under dedyn.io).  At the time when you registered
+your account, we did not verify your email address.
+
+Our terms now require that deSEC account holders provide an up-to-date contact
+email address, in case we need to contact you (e.g. when there is a problem
+with your domain).  We are currently cleaning our database and making sure
+that all addresses are verified.
+
+Therefore, if you would like to continue using your deSEC account, you now
+need to verify your email address.  To do so, please use the following link
+(valid for {% widthratio link_expiration_hours 24 1 %} days):
+
+{{ confirmation_link.0 }}
+
+If you do not want to continue using deSEC, we will delete your account once
+the link has expired, without contacting you again.  In case you miss the
+deadline, we invite you to sign up again with the same email address.
+
+We apologize for the slight inconvenience, and hope you enjoy deSEC!
+
+Stay secure,
+Nils

+ 1 - 0
api/desecapi/templates/emails/confirm-account/subject.txt

@@ -0,0 +1 @@
+[deSEC] Important: Please confirm your deSEC DNS account

+ 1 - 0
api/desecapi/urls/version_1.py

@@ -55,6 +55,7 @@ api_urls = [
     # Authenticated Actions
     # Authenticated Actions
     path('v/activate-account/<code>/', views.AuthenticatedActivateUserActionView.as_view(), name='confirm-activate-account'),
     path('v/activate-account/<code>/', views.AuthenticatedActivateUserActionView.as_view(), name='confirm-activate-account'),
     path('v/change-email/<code>/', views.AuthenticatedChangeEmailUserActionView.as_view(), name='confirm-change-email'),
     path('v/change-email/<code>/', views.AuthenticatedChangeEmailUserActionView.as_view(), name='confirm-change-email'),
+    path('v/confirm-account/<code>/', views.AuthenticatedConfirmAccountUserAction.as_view(), name='confirm-confirm-account'),
     path('v/reset-password/<code>/', views.AuthenticatedResetPasswordUserActionView.as_view(), name='confirm-reset-password'),
     path('v/reset-password/<code>/', views.AuthenticatedResetPasswordUserActionView.as_view(), name='confirm-reset-password'),
     path('v/delete-account/<code>/', views.AuthenticatedDeleteUserActionView.as_view(), name='confirm-delete-account'),
     path('v/delete-account/<code>/', views.AuthenticatedDeleteUserActionView.as_view(), name='confirm-delete-account'),
     path('v/renew-domain/<code>/', views.AuthenticatedRenewDomainBasicUserActionView.as_view(), name='confirm-renew-domain'),
     path('v/renew-domain/<code>/', views.AuthenticatedRenewDomainBasicUserActionView.as_view(), name='confirm-renew-domain'),

+ 9 - 0
api/desecapi/views.py

@@ -750,6 +750,15 @@ class AuthenticatedChangeEmailUserActionView(AuthenticatedActionView):
         })
         })
 
 
 
 
+class AuthenticatedConfirmAccountUserAction(AuthenticatedActionView):
+    html_url = '/confirm/confirm-account/{code}'
+    serializer_class = serializers.AuthenticatedConfirmAccountUserActionSerializer
+
+    def post(self, request, *args, **kwargs):
+        super().post(request, *args, **kwargs)
+        return Response({'detail': 'Success! Your account status has been confirmed.'})
+
+
 class AuthenticatedResetPasswordUserActionView(AuthenticatedActionView):
 class AuthenticatedResetPasswordUserActionView(AuthenticatedActionView):
     html_url = '/confirm/reset-password/{code}/'
     html_url = '/confirm/reset-password/{code}/'
     serializer_class = serializers.AuthenticatedResetPasswordUserActionSerializer
     serializer_class = serializers.AuthenticatedResetPasswordUserActionSerializer