Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 107 additions & 109 deletions hypha/apply/users/templates/users/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,136 +5,134 @@

{% block content %}
<div class="my-5 max-w-2xl">

<section>
<div class="px-4 pt-4">
{% if wizard.steps.current == 'token' %}
{% if device.method == 'call' %}
<p>{% blocktrans trimmed %}We are calling your phone right now, please enter the
digits you hear.{% endblocktrans %}</p>
{% elif device.method == 'sms' %}
<p>{% blocktrans trimmed %}We sent you a text message, please enter the tokens we
sent.{% endblocktrans %}</p>
{% else %}
<h2 class="text-2xl">{% trans "Two factor verification" %}</h2>
<p>{% blocktrans trimmed %}Please enter the 6-digit verification code generated by your Authenticator App.{% endblocktrans %}</p>
{% endif %}
{% elif wizard.steps.current == 'backup' %}
<h2 class="text-2xl">{% trans "Two factor verification" %}</h2>
<p>
{% blocktrans trimmed %}Please enter one of the backup codes to log in to your account.{% endblocktrans %}
</p>
<p class="mb-4 text-sm text-fg-muted">
{% blocktrans trimmed %}Those codes were generated for you during 2FA setup to print or keep safe in a password manager.{% endblocktrans %}
</p>
<div class="px-4 pt-4">
{% if wizard.steps.current == 'token' %}
{% if device.method == 'call' %}
<p class="mb-2">{% blocktrans trimmed %}We are calling your phone right now, please enter the
digits you hear.{% endblocktrans %}</p>
{% elif device.method == 'sms' %}
<p class="mb-2">{% blocktrans trimmed %}We sent you a text message, please enter the tokens we
sent.{% endblocktrans %}</p>
{% else %}
<h1 class="mb-4 text-h1">{% trans "Two factor verification" %}</h1>
<p class="mb-2">{% blocktrans trimmed %}Please enter the 6-digit verification code generated by your Authenticator App.{% endblocktrans %}</p>
{% endif %}
{% elif wizard.steps.current == 'backup' %}
<h1 class="mb-4 text-h1">{% trans "Two factor verification" %}</h1>
<p class="mb-2">
{% blocktrans trimmed %}Please enter one of the backup codes to log in to your account.{% endblocktrans %}
</p>
<p class="mb-4 text-sm text-fg-muted">
{% blocktrans trimmed %}Those codes were generated for you during 2FA setup to print or keep safe in a password manager.{% endblocktrans %}
</p>
{% endif %}

<form class="form form--user-login" method="post">
{% csrf_token %}
{{ wizard.management_form }}

{% if wizard.steps.current == 'auth' %}

<style>
.id_auth-password {
margin-bottom: 0.25rem;
}
</style>

<h1 class="mb-4 text-h1">{% blocktrans %}Log in to {{ ORG_SHORT_NAME }}{% endblocktrans %}</h1>
{% for field in form %}
<div class="relative max-w-sm">
{% include "forms/includes/field.html" %}
{% if field.auto_id == "id_auth-password" %}
<div class="text-end">
<a
class="link link-secondary link-hover"
href="{% url 'users:password_reset' %}{% if redirect_url %}?next={{ redirect_url }}{% endif %}"
hx-boost="true"
>
{% trans "Forgot your password?" %}
</a>
</div>
{% endif %}
</div>
{% endfor %}

<form class="form form--user-login" method="post">
{% csrf_token %}
{{ wizard.management_form }}

{% if wizard.steps.current == 'auth' %}

<style>
.id_auth-password {
margin-bottom: 0.25rem;
}
</style>

<h1 class="mb-4 text-h1">{% blocktrans %}Log in to {{ ORG_SHORT_NAME }}{% endblocktrans %}</h1>
{% for field in form %}
<div class="relative max-w-sm">
{% include "forms/includes/field.html" %}
{% if field.auto_id == "id_auth-password" %}
<div class="text-end">
<a
class="link link-secondary link-hover"
href="{% url 'users:password_reset' %}{% if redirect_url %}?next={{ redirect_url }}{% endif %}"
hx-boost="true"
>
{% trans "Forgot your password?" %}
</a>
</div>
{% endif %}
</div>
{% endfor %}

{% if settings.users.AuthSettings.extra_text %}
<div class="p-4 mb-6 rounded-xs prose prose-sm bg-base-200">
{{ settings.users.AuthSettings.extra_text|richtext }}
</div>
{% endif %}

<div class="mt-4">
<button class="btn btn-primary btn-block sm:btn-wide" type="submit">
{% trans "Log in" %}
</button>
{% if settings.users.AuthSettings.extra_text %}
<div class="p-4 mb-6 rounded-xs prose prose-sm bg-base-200">
{{ settings.users.AuthSettings.extra_text|richtext }}
</div>
{% endif %}

<div class="my-8 max-w-xs divider text-fg-muted">{% translate "OR" %}</div>
<div class="mt-4">
<button class="btn btn-primary btn-block sm:btn-wide" type="submit">
{% trans "Log in" %}
</button>
</div>

<section class="flex-wrap card-actions">
{% if GOOGLE_OAUTH2 %}
{% include "includes/org_login_button.html" %}
{% endif %}
{% include "includes/passwordless_login_button.html" %}
</section>
<div class="my-8 max-w-xs divider text-fg-muted">{% translate "OR" %}</div>

{% else %}
<section class="flex-wrap card-actions">
{% if GOOGLE_OAUTH2 %}
{% include "includes/org_login_button.html" %}
{% endif %}
{% include "includes/passwordless_login_button.html" %}
</section>

<div class="form__group">
{{ wizard.form }}
</div>
{% else %}

{# hidden submit button to enable [enter] key #}
<div class="sr-only"><input type="submit" value=""/></div>

{% if other_devices %}
<p>{% trans "Or, alternatively, use one of your backup phones:" %}</p>
<p>
{% for other in other_devices %}
<button name="challenge_device" value="{{ other.persistent_id }}"
class="btn btn-default btn-block" type="submit">
{{ other.generate_challenge_button_title }}
</button>
{% endfor %}
</p>
{% endif %}
<div class="form__group">
{{ wizard.form }}
</div>

<div class="mt-2">
{% include "two_factor/_wizard_actions.html" %}
</div>
{# hidden submit button to enable [enter] key #}
<div class="sr-only"><input type="submit" value=""/></div>

{% if backup_tokens %}
<p>{% trans "As a last resort, you can use a backup codes: " %}
<button
name="wizard_goto_step"
type="submit"
value="backup"
aria-label="Click here to use a backup code"
class="font-semibold link link-primary"
>
{% trans "Use a Backup Code" %}
{% if other_devices %}
<p>{% trans "Or, alternatively, use one of your backup phones:" %}</p>
<p>
{% for other in other_devices %}
<button name="challenge_device" value="{{ other.persistent_id }}"
class="btn btn-default btn-block" type="submit">
{{ other.generate_challenge_button_title }}
</button>
</p>
{% endif %}
{% endfor %}
</p>
{% endif %}

<div class="mt-2">
{% include "two_factor/_wizard_actions.html" %}
</div>

{% if backup_tokens %}
<p class="my-2">{% trans "As a last resort, you can use a backup codes: " %}
<button
name="wizard_goto_step"
type="submit"
value="backup"
aria-label="Click here to use a backup code"
class="font-semibold link link-primary"
>
{% trans "Use a Backup Code" %}
</button>
</p>
{% endif %}
</form>
</div>
</section>
{% endif %}
</form>
</div>
</div>
{% endblock %}

{% block extra_js %}
{{ block.super }}
{# Fix copy of dynamic fields label #}
<script>
var labelOtpToken = document.querySelector("label[for=id_token-otp_token]");
if(labelOtpToken){
if (labelOtpToken){
labelOtpToken.textContent = "{% trans 'Verification Code' %}: ";
}
var labelBackupOTPToken = document.querySelector("label[for=id_backup-otp_token]");
if(labelBackupOTPToken){
if (labelBackupOTPToken){
labelBackupOTPToken.textContent = "{% trans 'Backup Code' %}: ";
}
</script>


{% endblock %}
14 changes: 10 additions & 4 deletions hypha/apply/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from django_htmx.http import HttpResponseClientRedirect
from django_otp import devices_for_user
from django_ratelimit.decorators import ratelimit
from formtools.wizard.forms import ManagementForm as WizardManagementForm
from hijack.views import AcquireUserView
from social_django.utils import psa
from social_django.views import complete
Expand Down Expand Up @@ -673,10 +674,15 @@ def get(self, request, uidb64, token, *args, **kwargs):
return render(request, "users/activation/invalid.html")

def post(self, request, uidb64, token, *args, **kwargs):
# If storage already has an authenticated user we are in the MFA step
# (user confirmed the link, now submitting their OTP). Delegate to the
# parent WizardView which handles the OTP form.
if self.storage.authenticated_user:
# If the wizard management form is present in POST data, we are in the
# MFA step (user confirmed the link and is now submitting their OTP).
# Delegate to the parent WizardView which handles the OTP form.
# We check for the management form rather than just self.storage.authenticated_user
# because stale session data could have authenticated_user set from a previous
# incomplete MFA flow, causing a SuspiciousOperation crash when the user
# submits a fresh confirmation POST (which has no management form).
management_form = WizardManagementForm(request.POST, prefix=self.prefix)
if management_form.is_valid() and self.storage.authenticated_user:
return super().post(request, *args, **kwargs)

# Initial confirmation POST — validate token and log the user in.
Expand Down
Loading