You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2021/04/29 21:29:38 UTC

[airavata-django-portal] branch AIRAVATA-3319-handle-missing-name-and-email-attributes-from-cilo updated (e2d5121 -> a6c9703)

This is an automated email from the ASF dual-hosted git repository.

machristie pushed a change to branch AIRAVATA-3319-handle-missing-name-and-email-attributes-from-cilo
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git.


    from e2d5121  AIRAVATA-3319 Simple user profile editor for editing first name, last name
     new 91cb9c4  AIRAVATA-3455 Verify email change before updating user profile
     new 7af575a  AIRAVATA-3455 Display if there is a pending email change
     new a6c9703  AIRAVATA-3455 Offer to resend verification link

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../django_airavata_api/js/service_config.js       |  8 ++-
 .../auth/migrations/0008_auto_20210422_1838.py     | 59 +++++++++++++++++++
 django_airavata/apps/auth/models.py                | 11 ++++
 django_airavata/apps/auth/serializers.py           | 66 +++++++++++++++++++++-
 .../js/components/UserProfileEditor.vue            | 11 +++-
 .../js/containers/UserProfileContainer.vue         | 21 ++++++-
 django_airavata/apps/auth/urls.py                  |  4 +-
 django_airavata/apps/auth/views.py                 | 48 ++++++++++++++++
 8 files changed, 222 insertions(+), 6 deletions(-)
 create mode 100644 django_airavata/apps/auth/migrations/0008_auto_20210422_1838.py

[airavata-django-portal] 03/03: AIRAVATA-3455 Offer to resend verification link

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch AIRAVATA-3319-handle-missing-name-and-email-attributes-from-cilo
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit a6c97037f338139a99ddb3464b078e9f6b0e293c
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Apr 29 17:29:25 2021 -0400

    AIRAVATA-3455 Offer to resend verification link
---
 .../static/django_airavata_api/js/service_config.js |  8 ++++++--
 django_airavata/apps/auth/serializers.py            |  5 ++---
 .../js/components/UserProfileEditor.vue             |  4 +++-
 .../js/containers/UserProfileContainer.vue          | 21 ++++++++++++++++++++-
 django_airavata/apps/auth/views.py                  |  8 ++++++++
 5 files changed, 39 insertions(+), 7 deletions(-)

diff --git a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
index edbe06c..fb2392b 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
@@ -376,8 +376,12 @@ export default {
     methods: {
       current: {
         url: "/auth/users/current/",
-        requestType: "get"
-      }
+        requestType: "get",
+      },
+      resendEmailVerification: {
+        url: "/auth/users/<lookup>/resend_email_verification/",
+        requestType: "post",
+      },
     },
     modelClass: User,
   },
diff --git a/django_airavata/apps/auth/serializers.py b/django_airavata/apps/auth/serializers.py
index 5b4b929..d5134bb 100644
--- a/django_airavata/apps/auth/serializers.py
+++ b/django_airavata/apps/auth/serializers.py
@@ -47,7 +47,7 @@ class UserSerializer(serializers.ModelSerializer):
             # Email doesn't get updated until it is verified. Create a pending
             # email change record in the meantime
             pending_email_change = models.PendingEmailChange.objects.create(user=request.user, email_address=validated_data['email'])
-            self._send_email_verification_link(pending_email_change)
+            self._send_email_verification_link(request, pending_email_change)
         instance.save()
         # save in the user profile service too
         user_profile_client = request.profile_service['user_profile']
@@ -58,9 +58,8 @@ class UserSerializer(serializers.ModelSerializer):
         user_profile_client.updateUserProfile(request.authz_token, airavata_user_profile)
         return instance
 
-    def _send_email_verification_link(self, pending_email_change):
+    def _send_email_verification_link(self, request, pending_email_change):
 
-        request = self.context['request']
         verification_uri = request.build_absolute_uri(
             reverse(
                 'django_airavata_auth:verify_email_change', kwargs={
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
index ce84e61..c80a6d1 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
@@ -16,7 +16,9 @@
         <strong>{{ user.pending_email_change.email_address }}</strong
         >, your email address will be updated. If you didn't receive the
         verification email,
-        <b-link>click here to resend verification link.</b-link></b-alert
+        <b-link @click="$emit('resend-email-verification')"
+          >click here to resend verification link.</b-link
+        ></b-alert
       >
     </b-form-group>
     <b-button variant="primary" @click="$emit('save', user)">Save</b-button>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue
index fa210bb..e34e269 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue
@@ -1,13 +1,19 @@
 <template>
   <div>
     <h1 class="h4 mb-4">User Profile Editor</h1>
-    <user-profile-editor v-if="user" v-model="user" @save="onSave" />
+    <user-profile-editor
+      v-if="user"
+      v-model="user"
+      @save="onSave"
+      @resend-email-verification="resendEmailVerification"
+    />
   </div>
 </template>
 
 <script>
 import { services } from "django-airavata-api";
 import UserProfileEditor from "../components/UserProfileEditor.vue";
+import { notifications } from "django-airavata-common-ui";
 
 export default {
   components: { UserProfileEditor },
@@ -31,6 +37,19 @@ export default {
         this.user = user;
       });
     },
+    resendEmailVerification() {
+      services.UserService.resendEmailVerification({
+        lookup: this.user.id,
+      }).then(() => {
+        notifications.NotificationList.add(
+          new notifications.Notification({
+            type: "SUCCESS",
+            message: "Verification link sent",
+            duration: 5,
+          })
+        );
+      });
+    },
   },
 };
 </script>
diff --git a/django_airavata/apps/auth/views.py b/django_airavata/apps/auth/views.py
index 3fc0c40..5261b07 100644
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@ -545,6 +545,14 @@ class UserViewSet(viewsets.ModelViewSet):
     def current(self, request):
         return redirect(reverse('django_airavata_auth:user-detail', kwargs={'pk': request.user.id}))
 
+    @action(methods=['post'], detail=True)
+    def resend_email_verification(self, request, pk=None):
+        pending_email_change = models.PendingEmailChange.objects.get(user=request.user, verified=False)
+        if pending_email_change is not None:
+            serializer = serializers.UserSerializer()
+            serializer._send_email_verification_link(request, pending_email_change)
+        return JsonResponse({})
+
 
 @login_required
 @atomic

[airavata-django-portal] 02/03: AIRAVATA-3455 Display if there is a pending email change

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch AIRAVATA-3319-handle-missing-name-and-email-attributes-from-cilo
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 7af575a4de14439335e0c70e1c517a521c92155a
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Apr 29 16:40:32 2021 -0400

    AIRAVATA-3455 Display if there is a pending email change
---
 django_airavata/apps/auth/serializers.py            | 21 +++++++++++++++++++--
 .../js/components/UserProfileEditor.vue             |  7 +++++++
 2 files changed, 26 insertions(+), 2 deletions(-)

diff --git a/django_airavata/apps/auth/serializers.py b/django_airavata/apps/auth/serializers.py
index 310b4a0..5b4b929 100644
--- a/django_airavata/apps/auth/serializers.py
+++ b/django_airavata/apps/auth/serializers.py
@@ -12,12 +12,29 @@ from . import models, utils
 logger = logging.getLogger(__name__)
 
 
+class PendingEmailChangeSerializer(serializers.ModelSerializer):
+
+    class Meta:
+        model = models.PendingEmailChange
+        fields = ['email_address', 'created_date']
+
+
 class UserSerializer(serializers.ModelSerializer):
 
-    # TODO: add a lookup of most recent PendingEmailChange if any
+    pending_email_change = serializers.SerializerMethodField()
+
     class Meta:
         model = get_user_model()
-        fields = ['id', 'username', 'first_name', 'last_name', 'email']
+        fields = ['id', 'username', 'first_name', 'last_name', 'email', 'pending_email_change']
+
+    def get_pending_email_change(self, instance):
+        request = self.context['request']
+        pending_email_change = models.PendingEmailChange.objects.filter(user=request.user, verified=False).first()
+        if pending_email_change is not None:
+            serializer = PendingEmailChangeSerializer(instance=pending_email_change, context=self.context)
+            return serializer.data
+        else:
+            return None
 
     @atomic
     def update(self, instance, validated_data):
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
index 844b3a0..ce84e61 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
@@ -11,6 +11,13 @@
     </b-form-group>
     <b-form-group label="Email">
       <b-form-input v-model="user.email" />
+      <b-alert class="mt-1" show v-if="user.pending_email_change"
+        >Once you verify your email address at
+        <strong>{{ user.pending_email_change.email_address }}</strong
+        >, your email address will be updated. If you didn't receive the
+        verification email,
+        <b-link>click here to resend verification link.</b-link></b-alert
+      >
     </b-form-group>
     <b-button variant="primary" @click="$emit('save', user)">Save</b-button>
     <b-button>Cancel</b-button>

[airavata-django-portal] 01/03: AIRAVATA-3455 Verify email change before updating user profile

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch AIRAVATA-3319-handle-missing-name-and-email-attributes-from-cilo
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 91cb9c49aadf40837569b2db4c07cd097cfc3437
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Apr 29 15:35:23 2021 -0400

    AIRAVATA-3455 Verify email change before updating user profile
---
 .../auth/migrations/0008_auto_20210422_1838.py     | 59 ++++++++++++++++++++++
 django_airavata/apps/auth/models.py                | 11 ++++
 django_airavata/apps/auth/serializers.py           | 48 ++++++++++++++++++
 .../js/components/UserProfileEditor.vue            |  2 +-
 django_airavata/apps/auth/urls.py                  |  4 +-
 django_airavata/apps/auth/views.py                 | 40 +++++++++++++++
 6 files changed, 162 insertions(+), 2 deletions(-)

diff --git a/django_airavata/apps/auth/migrations/0008_auto_20210422_1838.py b/django_airavata/apps/auth/migrations/0008_auto_20210422_1838.py
new file mode 100644
index 0000000..9a327b0
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0008_auto_20210422_1838.py
@@ -0,0 +1,59 @@
+# Generated by Django 2.2.17 on 2021-04-22 18:38
+
+import uuid
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+from django_airavata.apps.auth.models import VERIFY_EMAIL_CHANGE_TEMPLATE
+
+
+def default_templates(apps, schema_editor):
+    EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
+    verify_email_template = EmailTemplate(
+        template_type=VERIFY_EMAIL_CHANGE_TEMPLATE,
+        subject="{{first_name}} {{last_name}} ({{username}}), "
+                "Please Verify Your New Email Address in {{portal_title}}",
+        body="""
+        <p>
+        Dear {{first_name}} {{last_name}},
+        </p>
+
+        <p>
+        Before your email address change can be processed, you need to verify
+        your new email address ({{email}}). Click the link below to verify your email
+        address:
+        </p>
+
+        <p><a href="{{url}}">{{url}}</a></p>
+        """.strip())
+    verify_email_template.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('django_airavata_auth', '0007_auto_20200917_1610'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='emailtemplate',
+            name='template_type',
+            field=models.IntegerField(choices=[(1, 'Verify Email Template'), (2, 'New User Email Template'), (3, 'Password Reset Email Template'), (4, 'User Added to Group Template'), (5, 'Verify Email Change Template')], primary_key=True, serialize=False),
+        ),
+        migrations.CreateModel(
+            name='PendingEmailChange',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('email_address', models.EmailField(max_length=254)),
+                ('verification_code', models.CharField(default=uuid.uuid4, max_length=36, unique=True)),
+                ('created_date', models.DateTimeField(auto_now_add=True)),
+                ('verified', models.BooleanField(default=False)),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+        migrations.RunPython(default_templates, migrations.RunPython.noop),
+    ]
diff --git a/django_airavata/apps/auth/models.py b/django_airavata/apps/auth/models.py
index 082a4a1..c8c0da3 100644
--- a/django_airavata/apps/auth/models.py
+++ b/django_airavata/apps/auth/models.py
@@ -7,6 +7,7 @@ VERIFY_EMAIL_TEMPLATE = 1
 NEW_USER_EMAIL_TEMPLATE = 2
 PASSWORD_RESET_EMAIL_TEMPLATE = 3
 USER_ADDED_TO_GROUP_TEMPLATE = 4
+VERIFY_EMAIL_CHANGE_TEMPLATE = 5
 
 
 class EmailVerification(models.Model):
@@ -24,6 +25,7 @@ class EmailTemplate(models.Model):
         (NEW_USER_EMAIL_TEMPLATE, 'New User Email Template'),
         (PASSWORD_RESET_EMAIL_TEMPLATE, 'Password Reset Email Template'),
         (USER_ADDED_TO_GROUP_TEMPLATE, 'User Added to Group Template'),
+        (VERIFY_EMAIL_CHANGE_TEMPLATE, 'Verify Email Change Template'),
     )
     template_type = models.IntegerField(
         primary_key=True, choices=TEMPLATE_TYPE_CHOICES)
@@ -73,3 +75,12 @@ class UserInfo(models.Model):
 
     def __str__(self):
         return f"{self.claim}={self.value}"
+
+
+class PendingEmailChange(models.Model):
+    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+    email_address = models.EmailField()
+    verification_code = models.CharField(
+        max_length=36, unique=True, default=uuid.uuid4)
+    created_date = models.DateTimeField(auto_now_add=True)
+    verified = models.BooleanField(default=False)
diff --git a/django_airavata/apps/auth/serializers.py b/django_airavata/apps/auth/serializers.py
index 6b51cef..310b4a0 100644
--- a/django_airavata/apps/auth/serializers.py
+++ b/django_airavata/apps/auth/serializers.py
@@ -1,14 +1,62 @@
+import logging
+
+from django.conf import settings
 from django.contrib.auth import get_user_model
+from django.db.transaction import atomic
+from django.template import Context
+from django.urls import reverse
 from rest_framework import serializers
 
+from . import models, utils
+
+logger = logging.getLogger(__name__)
+
 
 class UserSerializer(serializers.ModelSerializer):
+
+    # TODO: add a lookup of most recent PendingEmailChange if any
     class Meta:
         model = get_user_model()
         fields = ['id', 'username', 'first_name', 'last_name', 'email']
 
+    @atomic
     def update(self, instance, validated_data):
+        request = self.context['request']
         instance.first_name = validated_data['first_name']
         instance.last_name = validated_data['last_name']
+        if instance.email != validated_data['email']:
+            # Delete any unverified pending email changes
+            models.PendingEmailChange.objects.filter(user=request.user, verified=False).delete()
+            # Email doesn't get updated until it is verified. Create a pending
+            # email change record in the meantime
+            pending_email_change = models.PendingEmailChange.objects.create(user=request.user, email_address=validated_data['email'])
+            self._send_email_verification_link(pending_email_change)
         instance.save()
+        # save in the user profile service too
+        user_profile_client = request.profile_service['user_profile']
+        airavata_user_profile = user_profile_client.getUserProfileById(
+            request.authz_token, request.user.username, settings.GATEWAY_ID)
+        airavata_user_profile.firstName = instance.first_name
+        airavata_user_profile.lastName = instance.last_name
+        user_profile_client.updateUserProfile(request.authz_token, airavata_user_profile)
         return instance
+
+    def _send_email_verification_link(self, pending_email_change):
+
+        request = self.context['request']
+        verification_uri = request.build_absolute_uri(
+            reverse(
+                'django_airavata_auth:verify_email_change', kwargs={
+                    'code': pending_email_change.verification_code}))
+        logger.debug(
+            "verification_uri={}".format(verification_uri))
+
+        context = Context({
+            "username": pending_email_change.user.username,
+            "email": pending_email_change.email_address,
+            "first_name": pending_email_change.user.first_name,
+            "last_name": pending_email_change.user.last_name,
+            "portal_title": settings.PORTAL_TITLE,
+            "url": verification_uri,
+        })
+        utils.send_email_to_user(models.VERIFY_EMAIL_CHANGE_TEMPLATE, context)
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
index aecfc97..844b3a0 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
@@ -10,7 +10,7 @@
       <b-form-input v-model="user.last_name" />
     </b-form-group>
     <b-form-group label="Email">
-      <b-form-input disabled :value="user.email" />
+      <b-form-input v-model="user.email" />
     </b-form-group>
     <b-button variant="primary" @click="$emit('save', user)">Save</b-button>
     <b-button>Cancel</b-button>
diff --git a/django_airavata/apps/auth/urls.py b/django_airavata/apps/auth/urls.py
index 9f193c3..d8de4a2 100644
--- a/django_airavata/apps/auth/urls.py
+++ b/django_airavata/apps/auth/urls.py
@@ -32,5 +32,7 @@ urlpatterns = [
         views.login_desktop_success, name="login_desktop_success"),
     url(r'^refreshed-token-desktop$', views.refreshed_token_desktop,
         name="refreshed_token_desktop"),
-    url(r'^user-profile/', views.user_profile),
+    url(r'^user-profile/', views.user_profile, name="user_profile"),
+    url(r'^verify-email-change/(?P<code>[\w-]+)/$', views.verify_email_change,
+        name="verify_email_change"),
 ]
diff --git a/django_airavata/apps/auth/views.py b/django_airavata/apps/auth/views.py
index e599abe..3fc0c40 100644
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@ -8,6 +8,7 @@ from django.contrib import messages
 from django.contrib.auth import authenticate, get_user_model, login, logout
 from django.contrib.auth.decorators import login_required
 from django.core.exceptions import ObjectDoesNotExist
+from django.db.transaction import atomic
 from django.forms import ValidationError
 from django.http import HttpResponseBadRequest, JsonResponse
 from django.shortcuts import redirect, render, resolve_url
@@ -543,3 +544,42 @@ class UserViewSet(viewsets.ModelViewSet):
     @action(detail=False)
     def current(self, request):
         return redirect(reverse('django_airavata_auth:user-detail', kwargs={'pk': request.user.id}))
+
+
+@login_required
+@atomic
+def verify_email_change(request, code):
+    try:
+        pending_email_change = models.PendingEmailChange.objects.get(user=request.user, verification_code=code)
+        pending_email_change.verified = True
+        pending_email_change.save()
+        request.user.email = pending_email_change.email_address
+        request.user.save()
+
+        user_profile_client = request.profile_service['user_profile']
+        airavata_user_profile = user_profile_client.getUserProfileById(
+            request.authz_token, request.user.username, settings.GATEWAY_ID)
+        airavata_user_profile.emails = [pending_email_change.email_address]
+        user_profile_client.updateUserProfile(request.authz_token, airavata_user_profile)
+
+        # TODO: add success message
+        return redirect(reverse('django_airavata_auth:user_profile'))
+    except ObjectDoesNotExist:
+        # if doesn't exist, give user a form where they can enter their
+        # username to resend verification code
+        logger.exception("PendingEmailChange object doesn't exist for "
+                         "code {}".format(code))
+        # TODO: add error message
+        # messages.error(
+        #     request,
+        #     "Email verification failed. Please enter your username and we "
+        #     "will send you another email verification link.")
+        return redirect(reverse('django_airavata_auth:user_profile'))
+    except Exception:
+        logger.exception("Email change verification processing failed!")
+        # TODO: add error message
+        # messages.error(
+        #     request,
+        #     "Email verification failed. Please try clicking the email "
+        #     "verification link again later.")
+        return redirect(reverse('django_airavata_auth:user_profile'))