The MFA pages (manage, TOTP activate/deactivate, recovery codes, security keys,
reauthenticate) are built almost entirely from allauth's `element` primitives, so
they are styled by overriding the elements rather than by rewriting eight page
templates. New allauth pages then inherit the look for free.
- field + img elements were missing entirely, so allauth fell back to bare HTML:
the TOTP secret and recovery-code list rendered as unstyled inputs. The QR now
sits on a white plate -- it is dark modules on a transparent ground, so on the
dark theme it was dark-on-dark and phones could not scan it.
- button now honours the tags allauth sets. They were all flattened to
btn-primary, which made "Deactivate" look exactly as safe as "View".
- the `code` field renders as a daisyUI otp wherever it appears, so the
reauthenticate and activate pages get the same input as the login challenge.
The boxes step aside past six characters: allauth accepts a TOTP code (6) or a
recovery code (8) in that one field.
Fixes a crash: the security-key list does {% load humanize %}, which raised
TemplateSyntaxError because django.contrib.humanize was not installed. That page
500'd on every request; it is now installed and covered by a test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 2FA code input was not invisible -- it was absent, along with the fields of
every other allauth form except login.
Cause: the `fields` element passed `attrs.exclude` straight into a filter. On a
page that never sets it, resolving a filter *argument* raises
VariableDoesNotExist; Django rescues that for the main variable of an expression
but not for a filter argument, and {% if %} then swallows it and reads the
condition as false. So every field was skipped. Login was the one page that
passes `exclude`, which is exactly why it kept working and hid the damage.
`exclude` is now pinned to a real variable first, with tests that render the
login, signup and password-reset forms and assert their inputs exist.
Two dangling buttons fixed while in here: `elements/form.html` dropped the `id`
attribute, so the out-of-band forms allauth generates (webauthn_form,
logout-from-stage) had no id for a button's `form` attribute to point at. "Use a
security key" submitted nothing.
Layout: the code is a daisyUI otp field, Cancel sits beside Sign In as a plain
button, both gain icons, and "Use a security key" becomes an accent button.
The otp boxes yield once more than six characters are typed. allauth accepts a
TOTP code (6) *or* a recovery code (8) in this one field, so hard-boxing it to
six would have locked out every recovery code.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
The entrance forms lose their visible field labels and gain an icon inside each
field (mail, lock), and allauth's "Forgot your password?" link -- which is the
password field's help_text -- is spaced away from the input instead of sitting
flush against it.
Three things this depends on:
- allauth already passes `unlabeled=True` on the entrance forms and already sets
a placeholder on every field there, so the visible label was redundant. The
label is still emitted sr-only: a placeholder is not a label, and it vanishes
as soon as you type.
- daisyUI's icon-in-field layout puts the `input` class on the *wrapping label*,
so the input itself must carry only `grow` -- hence the optional css override on
the daisy filter. `input` on both draws a box inside a box, and the error state
belongs on the wrapper for the same reason.
- The help text now carries id="<auto_id>_helptext". Django points the input's
aria-describedby at exactly that id, so without it the reference dangled and a
screen reader never announced the password-reset link.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the `controlpanel` app: a platform-wide (not club-scoped) admin panel for
creating clubs, archiving/restoring them, managing club admins, and per-club
statistics (members, teams & staff, events, shop). Statistics are annotated in
one query so the club list cannot fan out into N+1, and are returned as stat
*groups* so growing the domain means adding one entry.
Two access rules, both enforced by PlatformStaffRequiredMixin:
- staff only (is_staff/is_superuser); anonymous are sent to login, signed-in
non-staff get a 403. Staff already need a second factor, so the panel is
2FA-protected for free.
- base domain only: the panel manages *all* clubs, so it 404s if the tenant
middleware resolved a club from the subdomain.
Granting admin to an unknown email creates the account (unusable password —
they set one via password reset) and the Member behind it, since a ClubRole
hangs off a Member. A member who already holds a role is promoted in place,
because there is only one role per member per club.
UI is Tailwind + daisyUI. allauth ships an element system, so overriding
allauth/layouts/base.html plus ~13 element partials restyles *every* auth and
2FA screen at once — login, signup, password reset, the 2FA challenge, TOTP
enrolment, passkeys and recovery codes — rather than templating 20+ pages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>