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

smarru pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/airavata-custos-portal.git

commit 12dbbe27c72721c026db88b25750dc1a24e3291e
Author: Shivam Rastogi <shivam_r...@yahoo.com>
AuthorDate: Fri Mar 27 12:38:02 2020 -0400

    Added email verification
---
 custos_portal/custos_portal/apps/admin/models.py   |  3 -
 .../apps/auth/migrations/0001_initial.py           | 45 +++++++++++
 .../auth/migrations/0002_default_email_template.py | 55 +++++++++++++
 custos_portal/custos_portal/apps/auth/models.py    | 41 ++++++++++
 custos_portal/custos_portal/apps/auth/urls.py      |  2 +
 custos_portal/custos_portal/apps/auth/utils.py     | 25 ++++++
 custos_portal/custos_portal/apps/auth/views.py     | 94 ++++++++++++++++++++--
 custos_portal/custos_portal/settings_local.py      | 21 ++++-
 8 files changed, 275 insertions(+), 11 deletions(-)

diff --git a/custos_portal/custos_portal/apps/admin/models.py 
b/custos_portal/custos_portal/apps/admin/models.py
index 71a8362..e69de29 100644
--- a/custos_portal/custos_portal/apps/admin/models.py
+++ b/custos_portal/custos_portal/apps/admin/models.py
@@ -1,3 +0,0 @@
-from django.db import models
-
-# Create your models here.
diff --git a/custos_portal/custos_portal/apps/auth/migrations/0001_initial.py 
b/custos_portal/custos_portal/apps/auth/migrations/0001_initial.py
new file mode 100644
index 0000000..bfe59d5
--- /dev/null
+++ b/custos_portal/custos_portal/apps/auth/migrations/0001_initial.py
@@ -0,0 +1,45 @@
+# Generated by Django 3.0.4 on 2020-03-27 16:05
+
+from django.db import migrations, models
+import uuid
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='EmailTemplate',
+            fields=[
+                ('template_type', models.IntegerField(choices=[(1, 'Verify 
Email Template'), (2, 'New User Email Template'), (3, 'Password Reset Email 
Template'), (4, 'User Added to Group Template')], primary_key=True, 
serialize=False)),
+                ('subject', models.CharField(max_length=255)),
+                ('body', models.TextField()),
+                ('created_date', models.DateTimeField(auto_now_add=True)),
+                ('updated_date', models.DateTimeField(auto_now=True)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='EmailVerification',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, 
serialize=False, verbose_name='ID')),
+                ('username', models.CharField(max_length=64)),
+                ('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)),
+                ('next', models.CharField(blank=True, max_length=255)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='PasswordResetRequest',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, 
serialize=False, verbose_name='ID')),
+                ('username', models.CharField(max_length=64)),
+                ('reset_code', models.CharField(default=uuid.uuid4, 
max_length=36, unique=True)),
+                ('created_date', models.DateTimeField(auto_now_add=True)),
+            ],
+        ),
+    ]
diff --git 
a/custos_portal/custos_portal/apps/auth/migrations/0002_default_email_template.py
 
b/custos_portal/custos_portal/apps/auth/migrations/0002_default_email_template.py
new file mode 100644
index 0000000..d0db295
--- /dev/null
+++ 
b/custos_portal/custos_portal/apps/auth/migrations/0002_default_email_template.py
@@ -0,0 +1,55 @@
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+from custos_portal.apps.auth.models import (
+    NEW_USER_EMAIL_TEMPLATE,
+    VERIFY_EMAIL_TEMPLATE
+)
+
+
+def default_templates(apps, schema_editor):
+
+    EmailTemplate = apps.get_model("custos_portal_auth", "EmailTemplate")
+    verify_email_template = EmailTemplate(
+        template_type=VERIFY_EMAIL_TEMPLATE,
+        subject="{{first_name}} {{last_name}} ({{username}}), "
+                "Please Verify Your Email Account in {{portal_title}}",
+        body="""
+        <p>
+        Dear {{first_name}} {{last_name}},
+        </p>
+
+        <p>
+        Someone has created an account with this email address. If this was
+        you, click the link below to verify your email address:
+        </p>
+
+        <p><a href="{{url}}">{{url}}</a></p>
+
+        <p>If you didn't create this account, just ignore this message.</p>
+        """.strip())
+    verify_email_template.save()
+    new_user_email_template = EmailTemplate(
+        template_type=NEW_USER_EMAIL_TEMPLATE,
+        subject="New User Account Was Created Successfully",
+        body="""
+        <p>Gateway Portal: {{http_host}}</p>
+        <p>Tenant: {{gateway_id}}</p>
+        <p>Username: {{username}}</p>
+        <p>Name: {{first_name}} {{last_name}}</p>
+        <p>Email: {{email}}</p>
+        """.strip()
+    )
+    new_user_email_template.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('custos_portal_auth', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RunPython(default_templates)
+    ]
diff --git a/custos_portal/custos_portal/apps/auth/models.py 
b/custos_portal/custos_portal/apps/auth/models.py
index 71a8362..c50df65 100644
--- a/custos_portal/custos_portal/apps/auth/models.py
+++ b/custos_portal/custos_portal/apps/auth/models.py
@@ -1,3 +1,44 @@
+import uuid
+
 from django.db import models
 
+VERIFY_EMAIL_TEMPLATE = 1
+NEW_USER_EMAIL_TEMPLATE = 2
+PASSWORD_RESET_EMAIL_TEMPLATE = 3
+USER_ADDED_TO_GROUP_TEMPLATE = 4
+
+
 # Create your models here.
+class EmailVerification(models.Model):
+    username = models.CharField(max_length=64)
+    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)
+    next = models.CharField(max_length=255, blank=True)
+
+
+class EmailTemplate(models.Model):
+    TEMPLATE_TYPE_CHOICES = (
+        (VERIFY_EMAIL_TEMPLATE, 'Verify Email Template'),
+        (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'),
+    )
+    template_type = models.IntegerField(primary_key=True, 
choices=TEMPLATE_TYPE_CHOICES)
+    subject = models.CharField(max_length=255)
+    body = models.TextField()
+    created_date = models.DateTimeField(auto_now_add=True)
+    updated_date = models.DateTimeField(auto_now=True)
+
+    def __str__(self):
+        for choice in self.TEMPLATE_TYPE_CHOICES:
+            if self.template_type == choice[0]:
+                return choice[1]
+        return "Unknown"
+
+
+class PasswordResetRequest(models.Model):
+    username = models.CharField(max_length=64)
+    reset_code = models.CharField(
+        max_length=36, unique=True, default=uuid.uuid4)
+    created_date = models.DateTimeField(auto_now_add=True)
\ No newline at end of file
diff --git a/custos_portal/custos_portal/apps/auth/urls.py 
b/custos_portal/custos_portal/apps/auth/urls.py
index ef1de64..623799e 100644
--- a/custos_portal/custos_portal/apps/auth/urls.py
+++ b/custos_portal/custos_portal/apps/auth/urls.py
@@ -14,5 +14,7 @@ urlpatterns = [
         name='callback-error'),
     url(r'handle-login', views.handle_login, name="handle_login"),
     url(r'^logout$', views.start_logout, name='logout'),
+    url(r'^verify-email/(?P<code>[\w-]+)/$', views.verify_email,
+        name="verify_email"),
 
 ]
diff --git a/custos_portal/custos_portal/apps/auth/utils.py 
b/custos_portal/custos_portal/apps/auth/utils.py
new file mode 100644
index 0000000..a1f5598
--- /dev/null
+++ b/custos_portal/custos_portal/apps/auth/utils.py
@@ -0,0 +1,25 @@
+from django.conf import settings
+from django.core.mail import EmailMessage
+
+from . import models
+from django.template import Context, Template
+
+
+def send_email_to_user(template_id, context):
+    email_template = models.EmailTemplate.objects.get(pk=template_id)
+    subject = Template(email_template.subject).render(context)
+    body = Template(email_template.body).render(context)
+    msg = EmailMessage(
+        subject=subject,
+        body=body,
+        from_email="\"{}\" <{}>".format(settings.PORTAL_TITLE,
+                                        settings.SERVER_EMAIL),
+        to=["\"{} {}\" <{}>".format(context['first_name'],
+                                    context['last_name'],
+                                    context['email'])],
+        reply_to=[f"\"{a[0]}\" <{a[1]}>" for a in getattr(settings,
+                                                          'PORTAL_ADMINS',
+                                                          settings.ADMINS)]
+    )
+    msg.content_subtype = 'html'
+    msg.send()
diff --git a/custos_portal/custos_portal/apps/auth/views.py 
b/custos_portal/custos_portal/apps/auth/views.py
index dc2e688..c806245 100644
--- a/custos_portal/custos_portal/apps/auth/views.py
+++ b/custos_portal/custos_portal/apps/auth/views.py
@@ -8,11 +8,15 @@ from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth import authenticate, login, logout
 from django.core.exceptions import ValidationError
-from django.forms import formset_factory
+from django.forms import formset_factory, models
 from django.shortcuts import render, redirect, resolve_url
+from django.template import Context
 from django.urls import reverse
+from django.utils.http import urlencode
 from requests_oauthlib import OAuth2Session
 
+from . import utils
+from . import models
 from . import forms
 from ... import identity_management_client
 from ... import user_management_client
@@ -132,7 +136,6 @@ def start_logout(request):
 
 
 def create_account(request):
-    print("Create account is called")
     if request.method == 'POST':
         form = forms.CreateAccountForm(request.POST)
         if form.is_valid():
@@ -143,10 +146,11 @@ def create_account(request):
                 last_name = form.cleaned_data['last_name']
                 password = form.cleaned_data['password']
                 is_temp_password = True
-                result = 
user_management_client.register_user(settings.CUSTOS_TOKEN,
-                                                              username, 
first_name, last_name, password, email,
-                                                              is_temp_password)
+                result = 
user_management_client.register_user(settings.CUSTOS_TOKEN, username, 
first_name, last_name,
+                                                              password, email, 
is_temp_password)
                 if result.is_registered:
+                    logger.debug("User account successfully created for : 
{}".format(username))
+                    _create_and_send_email_verification_link(request, 
username, email, first_name, last_name, next)
                     messages.success(
                         request,
                         "Account request processed successfully. Before you "
@@ -154,8 +158,7 @@ def create_account(request):
                         "We've sent you an email with a link that you should "
                         "click on to complete the account creation process.")
                 else:
-                    form.add_error(None, ValidationError(
-                        "Failed to register the user with IAM service"))
+                    form.add_error(None, ValidationError("Failed to register 
the user with IAM service"))
             except TypeError as e:
                 logger.exception(
                     "Failed to create account for user", exc_info=e)
@@ -170,3 +173,80 @@ def create_account(request):
         'options': settings.AUTHENTICATION_OPTIONS,
         'form': form
     })
+
+
+def verify_email(request, code):
+
+    try:
+        email_verification = 
models.EmailVerification.objects.get(verification_code=code)
+        email_verification.verified = True
+        email_verification.save()
+        # Check if user is enabled, if so redirect to login page
+        username = email_verification.username
+        logger.debug("Email address verified for {}".format(username))
+        login_url = reverse('custos_portal_auth:login')
+        if email_verification.next:
+            login_url += "?" + urlencode({'next': email_verification.next})
+        if iam_admin_client.is_user_enabled(username):
+            logger.debug("User {} is already enabled".format(username))
+            messages.success(
+                request,
+                "Your account has already been successfully created. "
+                "Please log in now.")
+            return redirect(login_url)
+        else:
+            logger.debug("Enabling user {}".format(username))
+            # enable user and inform admins
+            iam_admin_client.enable_user(username)
+            user_profile = iam_admin_client.get_user(username)
+            email_address = user_profile.emails[0]
+            first_name = user_profile.firstName
+            last_name = user_profile.lastName
+            utils.send_new_user_email(request,
+                                      username,
+                                      email_address,
+                                      first_name,
+                                      last_name)
+            messages.success(
+                request,
+                "Your account has been successfully created. "
+                "Please log in now.")
+            return redirect(login_url)
+    except ObjectDoesNotExist as e:
+        # if doesn't exist, give user a form where they can enter their
+        # username to resend verification code
+        logger.exception("EmailVerification object doesn't exist for "
+                         "code {}".format(code))
+        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:resend_email_link'))
+    except Exception as e:
+        logger.exception("Email verification processing failed!")
+        messages.error(
+            request,
+            "Email verification failed. Please try clicking the email "
+            "verification link again later.")
+        return redirect(reverse('django_airavata_auth:create_account'))
+
+
+def _create_and_send_email_verification_link(request, username, email, 
first_name, last_name, next_url):
+
+    email_verification = models.EmailVerification(username=username, 
next=next_url)
+    email_verification.save()
+
+    verification_uri = request.build_absolute_uri(
+        reverse('custos_portal_auth:verify_email', kwargs={'code': 
email_verification.verification_code}))
+    logger.debug(
+        "verification_uri={}".format(verification_uri))
+
+    context = Context({
+        "username": username,
+        "email": email,
+        "first_name": first_name,
+        "last_name": last_name,
+        "portal_title": settings.PORTAL_TITLE,
+        "url": verification_uri,
+    })
+    utils.send_email_to_user(models.VERIFY_EMAIL_TEMPLATE, context)
diff --git a/custos_portal/custos_portal/settings_local.py 
b/custos_portal/custos_portal/settings_local.py
index b4e3175..383a3d5 100644
--- a/custos_portal/custos_portal/settings_local.py
+++ b/custos_portal/custos_portal/settings_local.py
@@ -22,4 +22,23 @@ KEYCLOAK_LOGOUT_URL = 
'https://keycloak.custos.scigap.org:31000/auth/realms/1000
 KEYCLOAK_VERIFY_SSL = False
 
 
-SESSION_COOKIE_SECURE = False
\ No newline at end of file
+SESSION_COOKIE_SECURE = False
+
+# Default email backend (for local development)
+EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+
+# Django - Email settings
+# Uncomment and specify the following for sending emails (default email backend
+# just prints to the console)
+# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
+# EMAIL_HOST = '...'
+# EMAIL_PORT = '...'
+# EMAIL_HOST_USER = '...'
+# EMAIL_HOST_PASSWORD = '...'
+# EMAIL_USE_TLS = True
+ADMINS = [('Admin Name', 'ad...@example.com')]
+# SERVER_EMAIL = 'por...@example.com'
+
+
+# Portal settings
+PORTAL_TITLE = 'Custos Admin Portal'

Reply via email to