Lay out the login card and wire up the passkey button

Sign In and "Sign in with a passkey" (btn-accent) now sit side by side with
"Remember Me" on the same row, and the email/password block is given room above
and below.

The passkey button was dead. It submits a *different* form -- the hidden
`mfa_login` that allauth renders from its `extra_body` block -- and our layout
base never defined that block, so neither the form nor the webauthn script was
ever emitted and clicking the button did nothing. _base.html now has the block,
and there is a test asserting the form and script are on the page.

The `fields` element grows an `exclude`, so a page can lay a field out itself
(here: "remember", moved onto the button row). It splits on commas rather than
testing for a substring -- "password" is a substring of "password2", and a page
excluding one would otherwise silently drop the other.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 22:47:26 +02:00
parent 7b18b39f49
commit b3f153a2dc
6 changed files with 140 additions and 1 deletions

View File

@@ -121,5 +121,12 @@
applyMode(currentMode());
</script>
{% comment %}
allauth puts page-level scripts and out-of-form markup here — notably the
hidden `mfa_login` form the passkey button submits. Without this block that
form is never rendered and "Sign in with a passkey" is a dead button.
{% endcomment %}
{% block extra_body %}{% endblock extra_body %}
</body>
</html>

View File

@@ -0,0 +1,65 @@
{% extends "account/base_entrance.html" %}
{% load allauth i18n lucide ui %}
{% comment %}
Overridden for layout: "Remember Me" sits on the button row rather than floating as
a third field, and the passkey button sits beside Sign In instead of below a rule.
The passkey button submits a *different* form (the hidden `mfa_login` rendered by the
webauthn snippet in extra_body) via the `form` attribute — forms cannot nest, so it
can live inside the login form and still post elsewhere.
{% endcomment %}
{% block head_title %}
{% trans "Sign In" %}
{% endblock head_title %}
{% block content %}
{% element h1 %}
{% trans "Sign In" %}
{% endelement %}
<form class="mt-8" method="post" action="{% url 'account_login' %}">
{% csrf_token %}
<div class="mb-10">
{% element fields form=form unlabeled=True exclude="remember" %}
{% endelement %}
</div>
{{ redirect_field }}
<div class="flex flex-wrap items-center justify-between gap-3">
{% if form.fields.remember %}
<label class="label cursor-pointer justify-start gap-3" for="{{ form.remember.id_for_label }}">
{{ form.remember|daisy }}
<span class="label-text">{{ form.remember.label }}</span>
</label>
{% else %}
<span></span>
{% endif %}
<div class="flex flex-wrap gap-2">
<button class="btn btn-primary gap-2" type="submit">{% lucide "log-in" size=16 %} {% trans "Sign In" %}</button>
{% if PASSKEY_LOGIN_ENABLED %}
<button class="btn btn-accent gap-2" type="submit" form="mfa_login" id="passkey_login">{% lucide "key-round" size=16 %} {% trans "Sign in with a passkey" %}</button>
{% endif %}
</div>
</div>
</form>
{% if LOGIN_BY_CODE_ENABLED %}
{% element hr %}
{% endelement %}
{% element button href=request_login_code_url tags="prominent,login,outline,primary" %}
{% trans "Send me a sign-in code" %}
{% endelement %}
{% endif %}
{% endblock content %}
{% block extra_body %}
{{ block.super }}
{% if PASSKEY_LOGIN_ENABLED %}
{% include "mfa/webauthn/snippets/login_script.html" with button_id="passkey_login" %}
{% endif %}
{% endblock extra_body %}

View File

@@ -18,6 +18,8 @@
</div>
{% endfor %}
{% for field in attrs.form.visible_fields %}
{# `exclude` lets a page lay a field out itself — the login page puts "remember" on the button row. #}
{% if not field|excluded:attrs.exclude %}
<div class="form-control my-3 w-full">
{% if field.field.widget.input_type == "checkbox" %}
<label class="label cursor-pointer justify-start gap-3" for="{{ field.id_for_label }}">
@@ -51,4 +53,5 @@
{% if field.help_text %}<span id="{{ field.auto_id }}_helptext" class="label-text-alt mt-3 block text-base-content/70">{{ field.help_text }}</span>{% endif %}
{% for error in field.errors %}<span class="label-text-alt mt-1 text-error">{{ error }}</span>{% endfor %}
</div>
{% endif %}
{% endfor %}