Explorar o código

feat(api): add User.email_verified, set during auth action authn

Peter Thomassen %!s(int64=3) %!d(string=hai) anos
pai
achega
4d0528df5c

+ 5 - 0
api/desecapi/authentication.py

@@ -1,4 +1,5 @@
 import base64
+from datetime import datetime, timezone
 from ipaddress import ip_address
 
 from django.contrib.auth.hashers import PBKDF2PasswordHasher
@@ -160,6 +161,10 @@ class AuthenticatedBasicUserActionAuthentication(BaseAuthentication):
         serializer.is_valid(raise_exception=True)
         user = serializer.validated_data['user']
 
+        email_verified = datetime.fromtimestamp(serializer.timestamp, timezone.utc)
+        user.email_verified = max(user.email_verified or email_verified, email_verified)
+        user.save()
+
         # When user.is_active is None, activation is pending.  We need to admit them to finish activation, so only
         # reject strictly False.  There are permissions to make sure that such accounts can't do anything else.
         if user.is_active == False:

+ 31 - 0
api/desecapi/migrations/0020_user_email_verified.py

@@ -0,0 +1,31 @@
+# Generated by Django 4.0.1 on 2022-01-14 13:39
+
+import datetime
+
+from django.db import migrations, models
+from django.db.models import F, Q
+
+
+def forwards_func(apps, schema_editor):
+    User = apps.get_model("desecapi", "User")
+    db_alias = schema_editor.connection.alias
+    User.objects.using(db_alias).filter(
+        Q(is_active=True) | Q(last_login__isnull=False),
+        created__date__gte=datetime.date(2019, 11, 1)
+    ).update(email_verified=F('created'))
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('desecapi', '0019_alter_user_is_active'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='user',
+            name='email_verified',
+            field=models.DateTimeField(blank=True, null=True),
+        ),
+        migrations.RunPython(forwards_func, migrations.RunPython.noop),
+    ]

+ 1 - 0
api/desecapi/models.py

@@ -98,6 +98,7 @@ class User(ExportModelOperationsMixin('User'), AbstractBaseUser):
         verbose_name='email address',
         unique=True,
     )
+    email_verified = models.DateTimeField(null=True, blank=True)
     is_active = models.BooleanField(default=True, null=True)
     is_admin = models.BooleanField(default=False)
     created = models.DateTimeField(auto_now_add=True)

+ 8 - 0
api/desecapi/tests/test_user_management.py

@@ -926,6 +926,14 @@ class HasUserAccountTestCase(UserManagementTestCase):
         self.assertVerificationFailureInvalidCodeResponse(self.client.verify(reset_password_link,
                                                                              data={'new_password': 'dummy'}))
 
+    def test_action_code_updates_email_verified(self):
+        email_verified = User.objects.get(email=self.email).email_verified
+        with mock.patch('time.time', return_value=time.time() + 1):
+            self.assertResetPasswordSuccessResponse(self.reset_password(self.email))
+            confirmation_link = self.assertResetPasswordEmail(self.email)
+            self.client.verify(confirmation_link)  # even without payload
+        self.assertGreaterEqual((User.objects.get(email=self.email).email_verified - email_verified).total_seconds(), 1)
+
 
 class RenewTestCase(UserManagementTestCase, DomainOwnerTestCase):
     DYN = False