Keep dashboard layouts browser-local by default and let users opt in to account storage.
Group library selection and shelf sync in one dropdown, preserve legacy client settings,
and block sync changes for demo-restricted accounts.
Fixes#1646
Wire the existing allow_registration setting to an admin toggle in the
Users tab and a public /register page, so the documented self-registration
flow is reachable from the UI instead of the API alone. Expose the flag on
the public setup-status endpoint the router guard already calls, so the
sign-in page can offer sign-up without an extra request.
Harden the newly public path: return a conflict instead of a 500 when
concurrent signups race the unique index, sanitize the logged username,
and reject control, zero-width, and text-direction characters in the
username and display name so a new account cannot impersonate another in
the user list or audit log.
Closes#952
The lockout was checked before the password was verified, so a correct password on a locked
account returned the same generic invalid-credentials error as a wrong one. Users had no way to
tell the two apart, and the natural response was to keep retrying a password that was already
right.
Completing a password reset did not clear the lock either, so the one recovery path the UI
advertises appeared to fail for up to fifteen minutes after a successful reset.
Verify the password first and report the lockout, with the remaining wait, only when the supplied
password was correct. Someone guessing wrong passwords still gets the generic failure, so account
existence is not leaked. Clear the lock whenever a password is reset or changed, and give admins
an explicit unlock action for the case where waiting is not acceptable.
Parallel refresh requests from a single client (multi-tab SPA, mobile tab restore)
tripped reuse-detection and logged users out every few hours. Track rotation
provenance via new rotated_at + replaced_by_token_hash columns and accept revoked-
token reuse only inside a configurable grace window with a chain that reaches a
live, non-logout link. Scope the rotate UPDATE with revoked_at IS NULL so a
concurrent logout or admin revoke fails the racing refresh instead of handing the
user a fresh access token.
Closes#120