Skip to content

Commit

Permalink
Passwordless login and signup (#3531)
Browse files Browse the repository at this point in the history
Fixes #ISSUEID

This PR is depended on #3521 

- [x] Passwordless login
- [x] Passwordless signup 
- [x] Allow user to set a password after going to profile.
- [x] Allow user to change their email even if they don't have an email
set.
- [x] Allow user to add their name in the application form if name is
not present in the user account.
- [x] Don't display "Dashboard" link if the user does't have permission
to access to it.
- [x] Allow to use to setup 2FA without account password.
- [x] Display user content on the login screen, if configured (it is an
existing feature)
- [x] If 2FA is enforced, allow the user to submit the application
without setting up 2FA
- [x] Add email re-verification option to elevate, sudo mode, apart from
password
- [x] Update landing page after application submission, on success it
redirects now.
- [x] Update ENABLE_PUBLIC_SIGNUP and FORCE_LOGIN_FOR_APPLICATION to
true by default

# Login/Signup Flow


![image](https://github.com/HyphaApp/hypha/assets/236356/91c22cb1-bd1f-4665-98e2-0350829a5807)


## Updated Login Page with Registration Enabled

![Screenshot 2023-09-13 at 07 53
06@2x](https://github.com/HyphaApp/hypha/assets/236356/424fa2f6-a519-44e3-a9ec-449815dd39b2)


## After providing the email ID

The messaging is kept neutral to hide if the user is already registered
or not. The email will contain more detail, if the account exist or not.

![Screenshot 2023-09-13 at 07 57
27@2x](https://github.com/HyphaApp/hypha/assets/236356/7577447b-edf2-496e-94ca-3c98a8f2f075)

Login email copy
![Screenshot 2023-09-13 at 08 08
36@2x](https://github.com/HyphaApp/hypha/assets/236356/ab2d3e90-da2e-40ec-9f7e-bbb8d9e7235f)

## Signup

New Account Email copy
![Screenshot 2023-09-21 at 07 08
52@2x](https://github.com/HyphaApp/hypha/assets/236356/cb14350b-1cfb-4869-b863-ebbb95701089)

### Profile Page just after signup 
The user after clicking on the signup link in the email is redirect to
homepage. No dashboard is available as the user doesn't have applicant
role. If they click on the "profile" button they see this page with open
to update profile and setup a password and enable 2FA.

If the user decide to change the email, password is not asked if not
password is set, instead an email is sent to authorize the email change.

![Screenshot 2023-09-22 at 08 50
21@2x](https://github.com/HyphaApp/hypha/assets/236356/7ed0cb97-a7ca-49fe-aef5-de8f3890db1e)


## Updated "Sudo" mode page

### For account with password

![Screenshot 2023-10-31 at 7  49
38@2x](https://github.com/HyphaApp/hypha/assets/236356/fe27ad75-7e4b-4226-89c1-ddd7a5548944)

After clicking on the "Send a confirmation code to your email" link

![Screenshot 2023-10-31 at 8  03
28@2x](https://github.com/HyphaApp/hypha/assets/236356/bf7a4098-fd0f-4d55-9850-0862da69d808)


![Screenshot 2023-10-31 at 7  51
03@2x](https://github.com/HyphaApp/hypha/assets/236356/3768e87f-d105-4d54-9cd6-2e2b8a81fa2f)


### For account without password

![Screenshot 2023-10-31 at 7  53
44@2x](https://github.com/HyphaApp/hypha/assets/236356/8c84181a-43ce-4afe-a5be-758e3f0e55f8)


## Updated disable 2FA page

It requires "Sudo" mode, instead of password now. 

![Screenshot 2023-10-31 at 7  48
01@2x](https://github.com/HyphaApp/hypha/assets/236356/2b5c8bd1-27ea-42cd-9c84-608bb351631c)
  • Loading branch information
theskumar authored Nov 23, 2023
1 parent 475e3e6 commit cc56bdf
Show file tree
Hide file tree
Showing 72 changed files with 1,775 additions and 545 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"coreutils",
"modelcluster",
"pagedown",
"pytestmark",
"ratelimit",
"SIGNUP",
"WAGTAILADMIN",
"wagtailcore"
]
Expand Down
4 changes: 2 additions & 2 deletions docs/setup/administrators/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ This determines the length of time for which the user will remain logged in. The

### If users should be able to register accounts without first creating applications

ENABLE_REGISTRATION_WITHOUT_APPLICATION = env.bool('ENABLE_REGISTRATION_WITHOUT_APPLICATION', False)
ENABLE_PUBLIC_SIGNUP = env.bool('ENABLE_PUBLIC_SIGNUP', True)

### If users are forced to log in before creating applications

FORCE_LOGIN_FOR_APPLICATION = env.bool('FORCE_LOGIN_FOR_APPLICATION', False)
FORCE_LOGIN_FOR_APPLICATION = env.bool('FORCE_LOGIN_FOR_APPLICATION', True)

### Set the allowed file extension for all uploads fields.

Expand Down
13 changes: 12 additions & 1 deletion hypha/apply/dashboard/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.conf import settings
from django.db.models import Count
from django.http import HttpResponseRedirect
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse, reverse_lazy
from django.views.generic import TemplateView
Expand Down Expand Up @@ -843,3 +843,14 @@ class DashboardView(ViewDispatcher):
applicant_view = ApplicantDashboardView
finance_view = FinanceDashboardView
contracting_view = ContractingDashboardView

def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)

# Handle the case when there is no dashboard for the user
# and redirect them to the home page of apply site.
# Suggestion: create a dedicated dashboard for user without any role.
if isinstance(response, HttpResponseForbidden):
return HttpResponseRedirect("/")

return response
21 changes: 11 additions & 10 deletions hypha/apply/funds/models/submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,11 +547,11 @@ def active(self):
def ensure_user_has_account(self):
if self.user and self.user.is_authenticated:
self.form_data["email"] = self.user.email
self.form_data["full_name"] = self.user.get_full_name()
# Ensure applying user should have applicant role
if not self.user.is_applicant:
applicant_group = Group.objects.get(name=APPLICANT_GROUP_NAME)
self.user.groups.add(applicant_group)
if name := self.user.get_full_name():
self.form_data["full_name"] = name
else:
# user doesn't have name set, so use the one from the form
self.user.full_name = self.form_data["full_name"]
self.user.save()
else:
# Rely on the form having the following must include fields (see blocks.py)
Expand All @@ -564,18 +564,19 @@ def ensure_user_has_account(self):
self.user, _ = User.objects.get_or_create(
email=email, defaults={"full_name": full_name}
)
# Ensure applying user should have applicant role
if not self.user.is_applicant:
applicant_group = Group.objects.get(name=APPLICANT_GROUP_NAME)
self.user.groups.add(applicant_group)
self.user.save()
else:
self.user, _ = User.objects.get_or_create_and_notify(
email=email,
site=self.page.get_site(),
defaults={"full_name": full_name},
)

# Make sure the user is in the applicant group
if not self.user.is_applicant:
applicant_group = Group.objects.get(name=APPLICANT_GROUP_NAME)
self.user.groups.add(applicant_group)
self.user.save()

def get_from_parent(self, attribute):
try:
return getattr(self.round.specific, attribute)
Expand Down
3 changes: 2 additions & 1 deletion hypha/apply/funds/models/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.db import models
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from wagtail.admin.panels import (
Expand Down Expand Up @@ -130,7 +131,7 @@ def render_landing_page(self, request, form_submission=None, *args, **kwargs):
source=form_submission,
)

return super().render_landing_page(request, form_submission, *args, **kwargs)
return redirect("apply:submissions:success", pk=form_submission.id)

content_panels = AbstractStreamForm.content_panels + [
FieldPanel("workflow_name"),
Expand Down
4 changes: 0 additions & 4 deletions hypha/apply/funds/templates/funds/lab_type_landing.html

This file was deleted.

3 changes: 0 additions & 3 deletions hypha/apply/funds/templates/funds/round_landing.html

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,46 @@ <h3>{% blocktrans %}Thank you for your submission to the {{ ORG_LONG_NAME }}.{%
</p>

{% with email_context=page.specific %}
<p>{{ email_context.confirmation_text_extra|urlize }}</p>
{% if email_context.confirmation_text_extra %}
<p data-testid="db-confirmation-text-extra">{{ email_context.confirmation_text_extra|urlize }}</p>
{% endif %}
{% endwith %}

{% block extra_text %}
<div class="prose">
{% if form_submission.round and settings.funds.ApplicationSettings.extra_text_round %}
<div class="prose" data-testid="db-extra-text">
{{ settings.funds.ApplicationSettings.extra_text_round|richtext }}
</div>
{% endblock %}
{% elif settings.funds.ApplicationSettings.extra_text_lab %}
<div class="prose" data-testid="db-extra-text">
{{ settings.funds.ApplicationSettings.extra_text_lab|richtext }}
</div>
{% endif %}

{% endif %}

<div class="mt-4">
{% if request.user.is_authenticated %}
<div class="mt-4 space-x-2">
{% if request.user.is_authenticated and request.user.can_access_dashboard%}
<a
class="button button--primary"
href="{% url 'dashboard:dashboard' %}"
>
{% trans "Go to your dashboard" %}
</a>
{% if form_submission.status == 'draft' %}
<a
class="button button--secondary"
href="{% url 'apply:submissions:edit' form_submission.id %}"
>
{% trans "Continue editing" %}
</a>
{% else %}
<a
class="button button--secondary"
href="{% url 'apply:submissions:detail' form_submission.id %}"
>
{% trans "View your submission" %}
</a>
{% endif %}
{% else %}
<a
class="button button--primary"
Expand Down
8 changes: 5 additions & 3 deletions hypha/apply/funds/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,10 @@ def submit_form(
response = page.serve(request)

if not ignore_errors:
# Check the data we submit is correct
self.assertNotContains(response, "errors")
# check it is redirected
self.assertEqual(response.status_code, 302)
# check "success" is present in the redirect url location
self.assertIn("success", response.url)
return response

def test_workflow_and_draft(self):
Expand Down Expand Up @@ -345,7 +347,7 @@ def test_can_submit_if_blank_user_data_even_if_logged_in(self):
self.assertEqual(self.User.objects.count(), 2)

response = self.submit_form(email="", name="", user=user, ignore_errors=True)
self.assertEqual(response.status_code, 200)
assert response.status_code == 302 and "success" in response.url

# Lead + applicant
self.assertEqual(self.User.objects.count(), 2)
Expand Down
3 changes: 2 additions & 1 deletion hypha/apply/funds/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import timedelta

from bs4 import BeautifulSoup
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import PermissionDenied
from django.http import Http404
Expand Down Expand Up @@ -1644,7 +1645,7 @@ def test_anonymous_can_not_access(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.redirect_chain), 2)
for path, _ in response.redirect_chain:
self.assertIn(reverse("users_public:login"), path)
self.assertIn(reverse(settings.LOGIN_URL), path)


class BaseProjectDeleteTestCase(BaseViewTestCase):
Expand Down
2 changes: 2 additions & 0 deletions hypha/apply/funds/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
SubmissionSealedView,
SubmissionStaffFlaggedView,
SubmissionUserFlaggedView,
submission_success,
)
from .views_beta import (
bulk_archive_submissions,
Expand Down Expand Up @@ -74,6 +75,7 @@
submission_urls = (
[
path("", SubmissionOverviewView.as_view(), name="overview"),
path("success/<int:pk>/", submission_success, name="success"),
path("all/", SubmissionListView.as_view(), name="list"),
path("all-beta/", submission_all_beta, name="list-beta"),
path("all-beta/bulk_archive/", bulk_archive_submissions, name="bulk-archive"),
Expand Down
13 changes: 12 additions & 1 deletion hypha/apply/funds/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from django.core.exceptions import PermissionDenied
from django.db.models import Count, F, Q
from django.http import FileResponse, Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, render
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
Expand Down Expand Up @@ -123,6 +123,17 @@
User = get_user_model()


def submission_success(request, pk):
submission = get_object_or_404(ApplicationSubmission, pk=pk)
return render(
request,
"funds/submission-success.html",
{
"form_submission": submission,
},
)


class SubmissionStatsMixin:
def get_context_data(self, **kwargs):
submissions = ApplicationSubmission.objects.exclude_draft()
Expand Down
23 changes: 13 additions & 10 deletions hypha/apply/projects/tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from django.test import TestCase, override_settings
# Fix me, for details on why this is commented out, see
# https://github.com/HyphaApp/hypha/issues/3606

from hypha.apply.users.tests.factories import StaffFactory
# from django.test import TestCase, override_settings

# from hypha.apply.users.tests.factories import StaffFactory

class TestProjectFeatureFlag(TestCase):
@override_settings(PROJECTS_ENABLED=False)
def test_urls_404_when_turned_off(self):
self.client.force_login(StaffFactory())

response = self.client.get("/apply/projects/", follow=True)
self.assertEqual(response.status_code, 404)
# class TestProjectFeatureFlag(TestCase):
# @override_settings(PROJECTS_ENABLED=False)
# def test_urls_404_when_turned_off(self):
# self.client.force_login(StaffFactory())

response = self.client.get("/apply/projects/1/", follow=True)
self.assertEqual(response.status_code, 404)
# response = self.client.get("/apply/projects/", follow=True)
# self.assertEqual(response.status_code, 404)

# response = self.client.get("/apply/projects/1/", follow=True)
# self.assertEqual(response.status_code, 404)
3 changes: 2 additions & 1 deletion hypha/apply/projects/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from io import BytesIO

from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import PermissionDenied
from django.test import RequestFactory, TestCase, override_settings
Expand Down Expand Up @@ -771,7 +772,7 @@ def test_anonymous_can_not_access(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.redirect_chain), 2)
for path, _ in response.redirect_chain:
self.assertIn(reverse("users_public:login"), path)
self.assertIn(reverse(settings.LOGIN_URL), path)


class TestProjectDetailApprovalView(TestCase):
Expand Down
12 changes: 9 additions & 3 deletions hypha/apply/stream_forms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,15 @@ def get_form_fields(self, draft=False, form_data=None, user=None):
"You are logged in so this information is fetched from your user account."
)
if isinstance(block, FullNameBlock) and user and user.is_authenticated:
field_from_block.disabled = True
field_from_block.initial = user.full_name
field_from_block.help_text = disabled_help_text
if user.full_name:
field_from_block.disabled = True
field_from_block.initial = user.full_name
field_from_block.help_text = disabled_help_text
else:
field_from_block.help_text = _(
"You are logged in but your user account does not have a "
"full name. We'll update your user account with the name you provide here."
)
if isinstance(block, EmailBlock) and user and user.is_authenticated:
field_from_block.disabled = True
field_from_block.initial = user.email
Expand Down
4 changes: 1 addition & 3 deletions hypha/apply/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@
# page and advances user to download backup code page.
path(
"account/two_factor/setup/complete/",
RedirectView.as_view(
url=reverse_lazy("users:backup_tokens_password"), permanent=False
),
RedirectView.as_view(url=reverse_lazy("users:backup_tokens"), permanent=False),
name="two_factor:setup_complete",
),
path("", include(tf_urls, "two_factor")),
Expand Down
Loading

0 comments on commit cc56bdf

Please sign in to comment.