Repository navigation
Add a staff user editor and revamp the profile editor - #107
Merged
Merged
Conversation
OPTION_DISABLED was documented as force-expiring active sessions, but only the login controller checked it. A user disabled while logged in stayed logged in until their session expired on its own. Authentication::verify() now discards the session as soon as the user is found to be disabled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The USER_EDITED event and its Discord embed were only emitted when a public profile field changed, so changing only the username, email address, or display name left no audit trail. Log the event whenever any account or profile field was saved, and record which of the two changed. The event is still attributed to the logged-in user making the change. The Discord embed now links to the edited user instead of the editor, since the editor is already shown as the embed author. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/user/edit?id=N lets a user with OPTION_ACL_USER_MODIFY edit another user's username, display name, email address, and options bitmask, which until now could only be changed in the database. Permissions are laid out as a resource by create/view/modify/delete grid, with the disabled and verified status flags shown separately, and presets for none, default, and staff. To keep the page from being a privilege escalation path: - Only permissions the editor holds can be granted or revoked. - Users who hold permissions the editor lacks cannot be edited at all, since changing their email address would allow taking over their account. - Editors cannot disable themselves or remove their own USER_MODIFY. - Bits without a named option are preserved as-is. Saves are validated as a whole and logged as USER_EDITED, attributed to the editor, with the changed fields and the granted and revoked option names. The profile page and user list now link to this page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Models default to a 500 response code and the update controller never set another one for logged-in users, so every profile view and save was served as an internal server error despite rendering normally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/user/update?id=N let staff edit other users through the same form users edit their own profile with. Staff now edit other users at /user/edit, so /user/update always edits the active user. Existing links with another user's id redirect staff to /user/edit, without applying any posted form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The grid showed an em dash where a resource has no option for a column, which read as a locked checkbox: most of the View column looked like permissions the editor was not allowed to grant. Those cells now say Public (anyone can view the resource) or Automatic (event logs are recorded by the site), with an explanation under the heading. The note about greyed-out permissions is now only shown when some permission is actually locked, and says why when editing yourself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since /user/update became self-only, staff had no way to edit another user's biography or social profile fields, for example to remove spam. /user/edit now has a public profile section with every profile field. Profile fields are validated together with the account fields, so nothing is saved unless all of them are valid. Changed fields are recorded in the USER_EDITED event with their old and new values, and listed by name in the Discord embed. A profile row is created for users who do not have one yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/user/verify compared the submitted token to the user's verifier token with !==, so a user whose token was null could be verified by omitting the t parameter entirely. Require a non-empty pending token and compare it in constant time. Non-string query parameters are now treated as missing instead of throwing a TypeError and returning 500. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user editor changed OPTION_VERIFIED as a plain bit, which left the verified date and verifier token stale: unverifying a user left them without any token for a verification link to use. Toggle it through User::setVerified() instead, which issues a new token when unverifying and records the date and clears the token when verifying. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Registration built and sent the account verification email inline, so it could not be sent from anywhere else. VerificationEmail::send() now does this for any user with a pending verifier token, attributing the EMAIL_SENT event to a given actor, and registration uses it unchanged. It also takes a resend flag for emails sent after registration, which replace the welcome line and the note about the link expiring 24 hours after registering, neither of which applies to a resent link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The status section of /user/edit now has a button that sends an unverified user a new verification email at their current address, attributed to the staff member in the EMAIL_SENT event. A user missing a verifier token is issued one first. The button is disabled for verified users, with a hint to untick Verified and save to have them verify again. The button submits its own form, so it never saves the main form, and it asks for confirmation if the main form has unsaved changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A verified user has no pending token, so following their verification link a second time showed the manual user id and token form with an "expired or invalid" error. Respond with 409 Conflict and a short page saying the account is already verified, with a link to log in, or to their profile if they are already logged in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The edit controller narrowed $this->model to EditModel with inline @var tags, which only inform static analysis. Narrow it in one helper with an instanceof check that throws InvalidModelException, as the views already do, so a wrong model fails loudly instead of being trusted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The verification email said its link would only be available for 24 hours after registering, but nothing enforces that. Say instead that the link is only kept for a limited time, and that a password reset still verifies the account if it stops working. Resent emails now include the same note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user list's edit button used 📝, the same as profile and content edit links, while the profile page's Edit user button used 🛡️. Use 🛡️ for both links to /user/edit, which marks it as a staff tool apart from editing a profile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each social link on a profile gets a dot, border, and background tint in its platform's brand color, with the service name in a lightened brand color that keeps at least 4.5:1 contrast on the dark theme. GitHub uses its light dark-mode foreground since its black brand color would not show, Instagram's dot uses its gradient, and websites stay neutral. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Profile::FIELDS lists every editable profile field with its getter and setter, and getField() and setField() access a field by name, so editors no longer keep their own copies of that mapping. The user editor now uses them. A new Profile also now starts with every field set to null. allocate() returned before initializing them when there was no user id to look up, so reading a field of a new profile threw an uninitialized property error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/user/update now matches the staff user editor: account, avatar, and public profile cards, errors shown under each field, and one summary alert instead of a success or error alert per field. The confirm email field only appears once the email address is changed. Saving validates every field first, then saves all of them or none. Before, each field was committed as it was checked, so one bad field could leave a save half-applied. It also fixes: - Taken usernames and email addresses are now reported on the field, instead of failing the database insert with a 500. - The minimum username length was never enforced, because ?? bound to the whole comparison. - USER_EDITED now records only the changed fields with their old and new values, instead of every field and an error for each. The public profile inputs are now a template shared with the staff user editor, with their labels kept in Profile::FIELDS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Changing your email address on /user/update kept the account verified, so it could move to an address nobody had confirmed. It now unverifies the account and emails a verification link to the new address. The current session continues, so a mistyped address can still be fixed, but logging in again requires verifying it. While the account is unverified, the page says so and offers a button to resend the verification email. Verification emails a user sends themselves, by changing their email or resending, are limited to one per VerificationEmail::COOLDOWN seconds, so the page cannot be used to send mail to arbitrary addresses in bulk. VerificationEmail::send() now takes a reason instead of a resend flag, and the email's opening line says why it was sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The None, Default, and Staff preset buttons only work through the page's script, so without JavaScript they were buttons that did nothing. They and their note are now hidden until the script shows them. Every form on the page still works without JavaScript. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When staff change a user's email address on /user/edit, the account is now unverified, whatever the Verified box says, and a verification link is sent to the new address, as on /user/update. A change of letter case alone is still treated as the same address. A failed send is reported on the page and the resend button can be used to try again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The button on your profile page read "Password" while the same link on the profile update page reads "Change password". Both now use the latter, matching the verb-first captions of the buttons beside it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reset password email was built and sent inside the ResetPassword controller. It now lives in ResetPasswordEmail, like VerificationEmail, so staff can send it too. It takes a reason that will change its wording; the controller sends the existing email unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finishing a password reset called setVerified(true) to clear the token, but setVerified() does nothing for an account that is already verified, so the token was kept and the same link could reset the password again. The token is now cleared explicitly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When sent with REASON_STAFF_RESET, the email says a staff member reset the password and logged the user out, and points to Recover Password if the link stops working, instead of telling them to ignore it if they did not ask. The plain text version also gets blank lines around the link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An account with no password hash can never log in, and was told its password was incorrect. It is now told the password must be reset, and how to do it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Staff can reset another user's password from the Status card on /user/edit. It clears the password so it stops working immediately, logs the user out of every session, and emails them a link to choose a new one. A confirmation box must be ticked first, which works without JavaScript. The reset is logged as USER_PASSWORD_RESET by the staff member, with how many sessions were expired and whether the email was sent. Staff cannot reset their own password here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Logging out tried to delete the session record, but compared the binary session id with its hexadecimal form, so nothing was ever removed and the session stayed valid server-side until it expired a month later. Logging out now sets the session's expiry to the current time, and staff password resets do the same for every active session of the user. Session records are no longer deleted by the site; removing expired ones is left to a separate cleanup job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session lookup compared expires_datetime = NULL, which is never true in SQL, so a session with no expiry date was treated as invalid. It now uses IS NULL, matching how expiring a session already treats a missing date as still active. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a staff page for editing users at
/user/edit, revamps the self-service profile editor at/user/update, and fixes several account and session bugs found along the way. Each commit stands alone so the branch can be fast-forwarded ontodevelop.Staff user editor (
/user/edit)/user/update, from a shared template.OPTION_ACL_USER_MODIFY.USER_EDITEDby the logged-in staff member, with the changed fields and the permissions granted or revoked. Resets are logged asUSER_PASSWORD_RESET. The Discord embed says "Email: Changed" without the address.Profile editor (
/user/update)?id=Nfor another user redirects staff to/user/edit.Fixes
expires_datetime = NULL, so sessions without an expiry date were rejected. It now usesIS NULL.new Profile(null)left its fields uninitialized.Also
VerificationEmailandResetPasswordEmaillibraries. Their wording now depends on why they were sent: registration, a resend, an email change, or a staff action.Testing
🤖 Generated with Claude Code