Give every auth button an icon, and lay out the password and MFA screens
The button element now takes an `icon`, so a page gets one by passing icon="name" rather than by hand-rolling its own button markup. Every button on the account and MFA screens carries one; a test walks each page and asserts no button is left bare. Change password: labels dropped (allauth already sets a placeholder on each field), the current password set apart from the new pair, help text kept on the new password, and Forgot Password promoted from a bare link to an accent button. MFA management: recovery-code actions are now ranked -- View is primary, Download and Generate are outline. Generate silently invalidates the codes you already hold, so it must not read as the obvious thing to click. Panel actions get breathing room from the body text (card-actions mt-4). Viewing recovery codes: Download and Generate sit side by side instead of stacking. TOTP activate: the code box loses its heading -- an otp field never takes a visible label, the boxes say what they are -- and the authenticator secret gets margin around it, since it is copied out by hand. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,7 @@
|
||||
{% if user.is_authenticated %}
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost gap-4">{% lucide "circle-user" %}{{ user.get_full_name }}</div>
|
||||
<ul tabindex="0" class="menu dropdown-content z-10 mt-2 w-60 rounded-box bg-base-100 p-2 shadow">
|
||||
<ul tabindex="0" class="menu dropdown-content z-10 mt-2 w-60 rounded-box bg-base-100 p-2 shadow border border-base-content/20">
|
||||
<li>
|
||||
<a href="{% url 'mfa_index' %}">{% lucide "shield-check" size=16 %} Two-factor authentication</a>
|
||||
</li>
|
||||
|
||||
40
templates/account/password_change.html
Normal file
40
templates/account/password_change.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "account/base_manage_password.html" %}
|
||||
{% load allauth i18n lucide %}
|
||||
|
||||
{% comment %}
|
||||
Overridden for layout: the fields lose their visible labels (allauth already gives each
|
||||
a placeholder), the current password is set apart from the new one, and the actions
|
||||
become real buttons.
|
||||
|
||||
The two field groups are rendered by two `fields` calls with complementary excludes —
|
||||
that is what puts air between "what you have now" and "what you want it to be", rather
|
||||
than three identical boxes in a stack.
|
||||
{% endcomment %}
|
||||
|
||||
{% block head_title %}
|
||||
{% trans "Change Password" %}
|
||||
{% endblock head_title %}
|
||||
|
||||
{% block content %}
|
||||
{% element h1 %}
|
||||
{% trans "Change Password" %}
|
||||
{% endelement %}
|
||||
|
||||
<form class="mt-8" method="post" action="{% url 'account_change_password' %}">
|
||||
{% csrf_token %}
|
||||
{{ redirect_field }}
|
||||
|
||||
{% element fields form=form unlabeled=True exclude="password1,password2" %}
|
||||
{% endelement %}
|
||||
|
||||
<div class="mt-10">
|
||||
{% element fields form=form unlabeled=True exclude="oldpassword" %}
|
||||
{% endelement %}
|
||||
</div>
|
||||
|
||||
<div class="mt-10 flex flex-wrap items-center justify-end gap-2">
|
||||
<a class="btn btn-accent gap-2" href="{% url 'account_reset_password' %}">{% lucide "life-buoy" size=16 %} {% trans "Forgot Password?" %}</a>
|
||||
<button class="btn btn-primary gap-2" type="submit">{% lucide "key-round" size=16 %} {% trans "Change Password" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{% load allauth %}
|
||||
{% load allauth lucide %}
|
||||
|
||||
{% comment %}
|
||||
allauth tags every button it renders, and mapping those tags is what styles the MFA
|
||||
@@ -9,13 +9,14 @@
|
||||
{% endcomment %}
|
||||
{% comment %} djlint:off {% endcomment %}
|
||||
<{% if attrs.href %}a href="{{ attrs.href }}"{% else %}button{% endif %}
|
||||
class="btn {% if attrs.tags and 'danger' in attrs.tags %}btn-error{% elif attrs.tags and 'link' in attrs.tags %}btn-link{% elif attrs.tags and 'secondary' in attrs.tags %}btn-outline btn-neutral{% elif attrs.tags and 'outline' in attrs.tags %}btn-outline btn-primary{% else %}btn-primary{% endif %}"
|
||||
class="btn gap-2 {% if attrs.tags and 'danger' in attrs.tags %}btn-error{% elif attrs.tags and 'link' in attrs.tags %}btn-link{% elif attrs.tags and 'secondary' in attrs.tags %}btn-outline btn-neutral{% elif attrs.tags and 'outline' in attrs.tags %}btn-outline btn-primary{% else %}btn-primary{% endif %}"
|
||||
{% if attrs.form %}form="{{ attrs.form }}"{% endif %}
|
||||
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
|
||||
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
|
||||
{% if attrs.value %}value="{{ attrs.value }}"{% endif %}
|
||||
{% if attrs.type %}type="{{ attrs.type }}"{% endif %}
|
||||
>
|
||||
{% if attrs.icon %}{% lucide attrs.icon size=16 %}{% endif %}
|
||||
{% slot %}
|
||||
{% endslot %}
|
||||
</{% if attrs.href %}a{% else %}button{% endif %}>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
character by character and must not misread.
|
||||
{% endcomment %}
|
||||
{{ attrs.errors }}
|
||||
<div class="form-control my-3 w-full">
|
||||
<div class="form-control my-6 w-full">
|
||||
{% if slots.label %}
|
||||
<label class="label" for="{{ attrs.id }}">
|
||||
<span class="label-text">{% slot label %}{% endslot %}</span>
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
<span class="label-text">{{ field.label }}</span>
|
||||
</label>
|
||||
{% else %}
|
||||
{% if not attrs.unlabeled %}
|
||||
{# An otp box never takes a visible label — the boxes say what they are, and a
|
||||
heading above them just crowds the field. It keeps an sr-only one below. #}
|
||||
{% if not attrs.unlabeled and not field|is_otp %}
|
||||
<label class="label" for="{{ field.id_for_label }}">
|
||||
<span class="label-text">{{ field.label }}</span>
|
||||
</label>
|
||||
@@ -53,7 +55,7 @@
|
||||
For the same reason the sr-only label lives outside the container: another
|
||||
child would throw the count off again.
|
||||
{% endcomment %}
|
||||
{% if attrs.unlabeled %}<label class="sr-only" for="{{ field.id_for_label }}">{{ field.label }}</label>{% endif %}
|
||||
<label class="sr-only" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||
<div class="flex justify-center">
|
||||
<div class="otp otp-lg" data-otp>
|
||||
<span></span>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{% load allauth %}
|
||||
<div class="card border border-base-300 bg-base-100 my-4">
|
||||
<div class="card border border-base-content/20 bg-base-100 my-4">
|
||||
<div class="card-body gap-2">
|
||||
{% if slots.title %}<h2 class="card-title text-lg">{% slot title %}{% endslot %}</h2>{% endif %}
|
||||
<div>{% slot body %}{% endslot %}</div>
|
||||
{% if slots.actions %}<div class="card-actions justify-end">{% slot actions %}{% endslot %}</div>{% endif %}
|
||||
{% if slots.actions %}
|
||||
<div class="card-actions mt-4 justify-end">{% slot actions %}{% endslot %}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
128
templates/mfa/index.html
Normal file
128
templates/mfa/index.html
Normal file
@@ -0,0 +1,128 @@
|
||||
{% extends "mfa/base_manage.html" %}
|
||||
{% load allauth i18n %}
|
||||
|
||||
{% comment %}
|
||||
allauth's own index, with an icon on every button and the recovery-code actions given
|
||||
the ranking they deserve: View is the primary action, Download and Generate are outline
|
||||
(Generate silently invalidates the codes you already have, so it must not look like the
|
||||
obvious thing to click).
|
||||
{% endcomment %}
|
||||
|
||||
{% block head_title %}
|
||||
{% trans "Two-Factor Authentication" %}
|
||||
{% endblock head_title %}
|
||||
|
||||
{% block content %}
|
||||
{% element h1 tags="mfa,index" %}
|
||||
{% trans "Two-Factor Authentication" %}
|
||||
{% endelement %}
|
||||
|
||||
{% if "totp" in MFA_SUPPORTED_TYPES %}
|
||||
{% element panel %}
|
||||
{% slot title %}
|
||||
{% translate "Authenticator App" %}
|
||||
{% endslot %}
|
||||
{% slot body %}
|
||||
{% if authenticators.totp %}
|
||||
{% element p %}
|
||||
{% translate "Authentication using an authenticator app is active." %}
|
||||
{% endelement %}
|
||||
{% else %}
|
||||
{% element p %}
|
||||
{% translate "An authenticator app is not active." %}
|
||||
{% endelement %}
|
||||
{% endif %}
|
||||
{% endslot %}
|
||||
{% slot actions %}
|
||||
{% url 'mfa_deactivate_totp' as deactivate_url %}
|
||||
{% url 'mfa_activate_totp' as activate_url %}
|
||||
{% if authenticators.totp %}
|
||||
{% element button href=deactivate_url icon="power" tags="danger,delete,panel" %}
|
||||
{% translate "Deactivate" %}
|
||||
{% endelement %}
|
||||
{% else %}
|
||||
{% element button href=activate_url icon="smartphone" tags="panel" %}
|
||||
{% translate "Activate" %}
|
||||
{% endelement %}
|
||||
{% endif %}
|
||||
{% endslot %}
|
||||
{% endelement %}
|
||||
{% endif %}
|
||||
|
||||
{% if "webauthn" in MFA_SUPPORTED_TYPES %}
|
||||
{% element panel %}
|
||||
{% slot title %}
|
||||
{% translate "Security Keys" %}
|
||||
{% endslot %}
|
||||
{% slot body %}
|
||||
{% if authenticators.webauthn|length %}
|
||||
{% element p %}
|
||||
{% blocktranslate count count=authenticators.webauthn|length %}You have added {{ count }} security key.{% plural %}You have added {{ count }} security keys.{% endblocktranslate %}
|
||||
{% endelement %}
|
||||
{% else %}
|
||||
{% element p %}
|
||||
{% translate "No security keys have been added." %}
|
||||
{% endelement %}
|
||||
{% endif %}
|
||||
{% endslot %}
|
||||
{% slot actions %}
|
||||
{% if authenticators.webauthn|length %}
|
||||
{% url 'mfa_list_webauthn' as webauthn_list_url %}
|
||||
{% element button href=webauthn_list_url icon="settings" %}
|
||||
{% translate "Manage" %}
|
||||
{% endelement %}
|
||||
{% else %}
|
||||
{% url 'mfa_add_webauthn' as webauthn_add_url %}
|
||||
{% element button href=webauthn_add_url icon="usb" %}
|
||||
{% translate "Add" %}
|
||||
{% endelement %}
|
||||
{% endif %}
|
||||
{% endslot %}
|
||||
{% endelement %}
|
||||
{% endif %}
|
||||
|
||||
{% if "recovery_codes" in MFA_SUPPORTED_TYPES %}
|
||||
{% with total_count=authenticators.recovery_codes.generate_codes|length unused_count=authenticators.recovery_codes.get_unused_codes|length %}
|
||||
{% element panel %}
|
||||
{% slot title %}
|
||||
{% translate "Recovery Codes" %}
|
||||
{% endslot %}
|
||||
{% slot body %}
|
||||
{% if authenticators.recovery_codes %}
|
||||
{% element p %}
|
||||
{% blocktranslate count unused_count=unused_count %}There is {{ unused_count }} out of {{ total_count }} recovery codes available.{% plural %}There are {{ unused_count }} out of {{ total_count }} recovery codes available.{% endblocktranslate %}
|
||||
{% endelement %}
|
||||
{% else %}
|
||||
{% element p %}
|
||||
{% translate "No recovery codes set up." %}
|
||||
{% endelement %}
|
||||
{% endif %}
|
||||
{% endslot %}
|
||||
{% if is_mfa_enabled %}
|
||||
{% if authenticators.recovery_codes %}
|
||||
{% if unused_count > 0 and not MFA_RECOVERY_CODES_SHOW_ONCE %}
|
||||
{% slot actions %}
|
||||
{% url 'mfa_view_recovery_codes' as view_url %}
|
||||
{% element button href=view_url icon="eye" tags="panel" %}
|
||||
{% translate "View" %}
|
||||
{% endelement %}
|
||||
{% endslot %}
|
||||
{% slot actions %}
|
||||
{% url 'mfa_download_recovery_codes' as download_url %}
|
||||
{% element button href=download_url icon="download" tags="outline,panel" %}
|
||||
{% translate "Download" %}
|
||||
{% endelement %}
|
||||
{% endslot %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% slot actions %}
|
||||
{% url 'mfa_generate_recovery_codes' as generate_url %}
|
||||
{% element button href=generate_url icon="refresh-cw" tags="outline,panel" %}
|
||||
{% translate "Generate" %}
|
||||
{% endelement %}
|
||||
{% endslot %}
|
||||
{% endif %}
|
||||
{% endelement %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
35
templates/mfa/recovery_codes/generate.html
Normal file
35
templates/mfa/recovery_codes/generate.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "mfa/recovery_codes/base.html" %}
|
||||
{% load i18n allauth %}
|
||||
|
||||
{% block content %}
|
||||
{% element h1 %}
|
||||
{% translate "Recovery Codes" %}
|
||||
{% endelement %}
|
||||
{% element p %}
|
||||
{% blocktranslate %}You are about to generate a new set of recovery codes for your account.{% endblocktranslate %}
|
||||
{% if unused_code_count %}
|
||||
{% blocktranslate %}This action will invalidate your existing codes.{% endblocktranslate %}
|
||||
{% endif %}
|
||||
{% blocktranslate %}Are you sure?{% endblocktranslate %}
|
||||
{% endelement %}
|
||||
|
||||
{% url 'mfa_generate_recovery_codes' as action_url %}
|
||||
{% element form method="post" action=action_url no_visible_fields=True %}
|
||||
{% slot body %}
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
{% endslot %}
|
||||
{% slot actions %}
|
||||
{# Destructive when codes already exist: generating throws the old ones away. #}
|
||||
{% setvar tags %}
|
||||
{% if unused_code_count %}
|
||||
danger
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% endsetvar %}
|
||||
{% element button type="submit" icon="refresh-cw" tags=tags %}
|
||||
{% trans "Generate" %}
|
||||
{% endelement %}
|
||||
{% endslot %}
|
||||
{% endelement %}
|
||||
{% endblock content %}
|
||||
65
templates/mfa/recovery_codes/index.html
Normal file
65
templates/mfa/recovery_codes/index.html
Normal file
@@ -0,0 +1,65 @@
|
||||
{% extends "mfa/recovery_codes/base.html" %}
|
||||
{% load i18n allauth static %}
|
||||
|
||||
{% comment %}
|
||||
Overridden for layout: Download and Generate sit side by side under the codes rather
|
||||
than stacking. Generate is outline-only — it silently invalidates the codes on screen,
|
||||
so it must not be the button your eye lands on.
|
||||
{% endcomment %}
|
||||
|
||||
{% block content %}
|
||||
{% element h1 %}
|
||||
{% translate "Recovery Codes" %}
|
||||
{% endelement %}
|
||||
{% element p %}
|
||||
{% blocktranslate count unused_count=unused_codes|length %}There is {{ unused_count }} out of {{ total_count }} recovery codes available.{% plural %}There are {{ unused_count }} out of {{ total_count }} recovery codes available.{% endblocktranslate %}
|
||||
{% endelement %}
|
||||
|
||||
{% if can_view_codes %}
|
||||
{% element field id="recovery_codes" type="textarea" rows=unused_codes|length readonly=True %}
|
||||
{% slot label %}
|
||||
{% translate "Unused codes" %}
|
||||
{% endslot %}
|
||||
{# djlint:off #}
|
||||
{% slot value %}{% for code in unused_codes %}{% if forloop.counter0 %}
|
||||
{% endif %}{{ code }}{% endfor %}{% endslot %}
|
||||
{# djlint:on #}
|
||||
{% endelement %}
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-6 flex flex-wrap items-center gap-2">
|
||||
{% if can_download_codes %}
|
||||
{% url 'mfa_download_recovery_codes' as download_url %}
|
||||
{% element button href=download_url icon="download" %}
|
||||
{% translate "Download codes" %}
|
||||
{% endelement %}
|
||||
{% endif %}
|
||||
{% if can_generate_codes %}
|
||||
{% url 'mfa_generate_recovery_codes' as generate_url %}
|
||||
{% element button href=generate_url icon="refresh-cw" tags="outline" %}
|
||||
{% translate "Generate new codes" %}
|
||||
{% endelement %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if MFA_RECOVERY_CODES_SHOW_ONCE and can_view_codes %}
|
||||
{% element field type="checkbox" id="codes_saved" %}
|
||||
{% slot label %}
|
||||
{% translate "I have saved my recovery codes" %}
|
||||
{% endslot %}
|
||||
{% endelement %}
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_body %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'mfa/js/recovery_codes.js' %}"></script>
|
||||
<script src="{% static 'account/js/onload.js' %}"></script>
|
||||
<script data-allauth-onload="allauth.recoveryCodes.forms.viewForm" type="application/json">{
|
||||
"ids": {
|
||||
"saveConfirmation": "codes_saved",
|
||||
"recoveryCodes": "recovery_codes"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock extra_body %}
|
||||
45
templates/mfa/totp/activate_form.html
Normal file
45
templates/mfa/totp/activate_form.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends "mfa/totp/base.html" %}
|
||||
{% load allauth i18n %}
|
||||
|
||||
{% comment %}
|
||||
The code field is boxed and label-less (the `fields` element does that for any `code`
|
||||
field), and the secret sits in a mono field with room around it — it is copied by hand,
|
||||
character by character.
|
||||
{% endcomment %}
|
||||
|
||||
{% block head_title %}
|
||||
{% translate "Activate Authenticator App" %}
|
||||
{% endblock head_title %}
|
||||
|
||||
{% block content %}
|
||||
{% element h1 %}
|
||||
{% translate "Activate Authenticator App" %}
|
||||
{% endelement %}
|
||||
{% element p %}
|
||||
{% blocktranslate %}To protect your account with two-factor authentication, scan the QR code below with your authenticator app. Then, input the verification code generated by the app below.{% endblocktranslate %}
|
||||
{% endelement %}
|
||||
|
||||
{% url 'mfa_activate_totp' as action_url %}
|
||||
{% element form form=form method="post" action=action_url %}
|
||||
{% slot body %}
|
||||
{% element img src=totp_svg_data_uri alt=form.secret tags="mfa,totp,qr" %}
|
||||
{% endelement %}
|
||||
{% csrf_token %}
|
||||
{% element field id="authenticator_secret" type="text" value=form.secret disabled=True %}
|
||||
{% slot label %}
|
||||
{% translate "Authenticator secret" %}
|
||||
{% endslot %}
|
||||
{% slot help_text %}
|
||||
{% translate "You can store this secret and use it to reinstall your authenticator app at a later time." %}
|
||||
{% endslot %}
|
||||
{% endelement %}
|
||||
{% element fields form=form %}
|
||||
{% endelement %}
|
||||
{% endslot %}
|
||||
{% slot actions %}
|
||||
{% element button type="submit" icon="check" %}
|
||||
{% trans "Activate" %}
|
||||
{% endelement %}
|
||||
{% endslot %}
|
||||
{% endelement %}
|
||||
{% endblock content %}
|
||||
30
templates/mfa/totp/deactivate_form.html
Normal file
30
templates/mfa/totp/deactivate_form.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends "mfa/totp/base.html" %}
|
||||
{% load allauth i18n %}
|
||||
|
||||
{% block head_title %}
|
||||
{% trans "Deactivate Authenticator App" %}
|
||||
{% endblock head_title %}
|
||||
|
||||
{% block content %}
|
||||
{% element h1 %}
|
||||
{% trans "Deactivate Authenticator App" %}
|
||||
{% endelement %}
|
||||
{% element p %}
|
||||
{% blocktranslate %}You are about to deactivate authenticator app based authentication. Are you sure?{% endblocktranslate %}
|
||||
{% endelement %}
|
||||
|
||||
{% url 'mfa_deactivate_totp' as action_url %}
|
||||
{% element form form=form method="post" action=action_url no_visible_fields=True %}
|
||||
{% slot body %}
|
||||
{% csrf_token %}
|
||||
{% element fields form=form %}
|
||||
{{ form.as_p }}
|
||||
{% endelement %}
|
||||
{% endslot %}
|
||||
{% slot actions %}
|
||||
{% element button type="submit" icon="power" tags="danger,delete" %}
|
||||
{% trans "Deactivate" %}
|
||||
{% endelement %}
|
||||
{% endslot %}
|
||||
{% endelement %}
|
||||
{% endblock content %}
|
||||
39
templates/mfa/webauthn/add_form.html
Normal file
39
templates/mfa/webauthn/add_form.html
Normal file
@@ -0,0 +1,39 @@
|
||||
{% extends "mfa/webauthn/base.html" %}
|
||||
{% load allauth i18n static %}
|
||||
|
||||
{% block head_title %}
|
||||
{% trans "Add Security Key" %}
|
||||
{% endblock head_title %}
|
||||
|
||||
{% block content %}
|
||||
{% element h1 %}
|
||||
{% trans "Add Security Key" %}
|
||||
{% endelement %}
|
||||
|
||||
{% url 'mfa_add_webauthn' as action_url %}
|
||||
{% element form form=form method="post" action=action_url %}
|
||||
{% slot body %}
|
||||
{% csrf_token %}
|
||||
{% element fields form=form %}
|
||||
{% endelement %}
|
||||
{% endslot %}
|
||||
{% slot actions %}
|
||||
{# type="button": the webauthn script takes over the click and posts the form itself. #}
|
||||
{% element button id="mfa_webauthn_add" type="button" icon="usb" %}
|
||||
{% trans "Add" %}
|
||||
{% endelement %}
|
||||
{% endslot %}
|
||||
{% endelement %}
|
||||
|
||||
{% include "mfa/webauthn/snippets/scripts.html" %}
|
||||
{{ js_data|json_script:"js_data" }}
|
||||
<script data-allauth-onload="allauth.webauthn.forms.addForm" type="application/json">{
|
||||
"ids": {
|
||||
"add": "mfa_webauthn_add",
|
||||
"passwordless": "{{ form.passwordless.auto_id }}",
|
||||
"credential": "{{ form.credential.auto_id }}",
|
||||
"data": "js_data"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock content %}
|
||||
25
templates/mfa/webauthn/authenticator_confirm_delete.html
Normal file
25
templates/mfa/webauthn/authenticator_confirm_delete.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "mfa/webauthn/base.html" %}
|
||||
{% load allauth i18n %}
|
||||
|
||||
{% block head_title %}
|
||||
{% trans "Remove Security Key" %}
|
||||
{% endblock head_title %}
|
||||
|
||||
{% block content %}
|
||||
{% element h1 %}
|
||||
{% trans "Remove Security Key" %}
|
||||
{% endelement %}
|
||||
{% element p %}
|
||||
{% blocktranslate %}Are you sure you want to remove this security key?{% endblocktranslate %}
|
||||
{% endelement %}
|
||||
|
||||
{% url 'mfa_remove_webauthn' pk=authenticator.pk as action_url %}
|
||||
{% element form method="post" action=action_url no_visible_fields=True %}
|
||||
{% slot actions %}
|
||||
{% csrf_token %}
|
||||
{% element button type="submit" icon="trash-2" tags="danger" %}
|
||||
{% translate "Remove" %}
|
||||
{% endelement %}
|
||||
{% endslot %}
|
||||
{% endelement %}
|
||||
{% endblock content %}
|
||||
72
templates/mfa/webauthn/authenticator_list.html
Normal file
72
templates/mfa/webauthn/authenticator_list.html
Normal file
@@ -0,0 +1,72 @@
|
||||
{% extends "mfa/webauthn/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load allauth %}
|
||||
{% load humanize %}
|
||||
{% block content %}
|
||||
{% element h1 %}
|
||||
{% trans "Security Keys" %}
|
||||
{% endelement %}
|
||||
{% if authenticators|length == 0 %}
|
||||
{% element p %}
|
||||
{% blocktranslate %}No security keys have been added.{% endblocktranslate %}
|
||||
{% endelement %}
|
||||
{% else %}
|
||||
{% element table %}
|
||||
{% element thead %}
|
||||
{% element th %}
|
||||
{% translate "Key" %}
|
||||
{% endelement %}
|
||||
{% element th %}
|
||||
{% translate "Usage" %}
|
||||
{% endelement %}
|
||||
{% element th %}
|
||||
{% endelement %}
|
||||
{% endelement %}
|
||||
{% element tbody %}
|
||||
{% for authenticator in authenticators %}
|
||||
{% element tr %}
|
||||
{% element td %}
|
||||
{{ authenticator }}
|
||||
{% if authenticator.wrap.is_passwordless is True %}
|
||||
{% element badge tags="mfa,key,primary" %}
|
||||
{% translate "Passkey" %}
|
||||
{% endelement %}
|
||||
{% elif authenticator.wrap.is_passwordless is False %}
|
||||
{% element badge tags="mfa,key,secondary" %}
|
||||
{% translate "Security key" %}
|
||||
{% endelement %}
|
||||
{% else %}
|
||||
{% element badge title=_("This key does not indicate whether it is a passkey.") tags="mfa,key,warning" %}
|
||||
{% translate "Unspecified" %}
|
||||
{% endelement %}
|
||||
{% endif %}
|
||||
{% endelement %}
|
||||
{% element td %}
|
||||
{% blocktranslate with created_at=authenticator.created_at|date:"SHORT_DATE_FORMAT" %}Added on {{ created_at }}{% endblocktranslate %}.
|
||||
{% if authenticator.last_used_at %}
|
||||
{% blocktranslate with last_used=authenticator.last_used_at|naturaltime %}Last used {{ last_used }}{% endblocktranslate %}
|
||||
{% else %}
|
||||
Not used.
|
||||
{% endif %}
|
||||
{% endelement %}
|
||||
{% element td align="right" %}
|
||||
{% url 'mfa_edit_webauthn' pk=authenticator.pk as edit_url %}
|
||||
{% element button href=edit_url icon="pencil" tags="mfa,authenticator,edit,tool" %}
|
||||
{% translate "Edit" %}
|
||||
{% endelement %}
|
||||
{% url 'mfa_remove_webauthn' pk=authenticator.pk as remove_url %}
|
||||
{% element button href=remove_url icon="trash-2" tags="mfa,authenticator,danger,delete,tool" %}
|
||||
{% translate "Remove" %}
|
||||
{% endelement %}
|
||||
{% endelement %}
|
||||
{% endelement %}
|
||||
{% endfor %}
|
||||
{% endelement %}
|
||||
{% endelement %}
|
||||
{% endif %}
|
||||
{% url 'mfa_add_webauthn' as add_url %}
|
||||
{% element button href=add_url icon="usb" %}
|
||||
{% translate "Add" %}
|
||||
{% endelement %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user