You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by sm...@apache.org on 2020/05/01 03:07:20 UTC
[airavata-custos-portal] 09/20: Added email verification
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 <sh...@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', 'admin@example.com')]
+# SERVER_EMAIL = 'portal@example.com'
+
+
+# Portal settings
+PORTAL_TITLE = 'Custos Admin Portal'