The password was checked through the login lookup by username or email, which could resolve to a different account, and the email of that account was then changed. Load the account by its ID and check its own password, keeping the re-authentication throttle and the lockout counter.
Review finding: CreateTaskRelationsForImport relied on an unenforced
invariant that every related task came from the same import. It now takes
the imported batch, builds the relations itself and rejects related tasks
outside the batch.
Review finding: two depth checks coexisted. filterGroupDepthExceeds runs
on the exact string passed to fexpr.Parse on both parse paths, and
unmatched parentheses are rejected by fexpr.Parse.
A subtask with several parents was rendered once per path, so diamond-shaped
subtask graphs caused exponential component renders and froze the tab. Each
task now expands its subtasks only on its first occurrence in the tree, and
nesting is capped at five levels.
Checking the old password directly skipped the failed-attempt counter that login and the email-change prompt use. Extract CheckUserPasswordCountingFailures from CheckUserCredentials and use it for the password change too.
user reset-password --direct only replaced the password and left existing sessions valid. Use the shared helper the other password flows use, which deletes the sessions in the same transaction, and dispatch the resulting events after commit.
The old password was checked through the login lookup by username or email, which could resolve to a different account than the one being changed. Load the account by its ID and check its own password, keeping the re-authentication throttle.
ShouldNotify dropped every notification for locked accounts, including the reset email needed to get out of the lock. It now receives the notification and lets password resets through for locked accounts; disabled accounts, bots and all other notifications stay suppressed. The admin reset action no longer refuses locked accounts either.
Consume the exact confirmation token and only apply the update when the pending address is still the one that was read, so a concurrent email change cannot be confirmed by an older token.
ILIKE treated _ and \ in the search term as pattern characters, so the exact-username lookup could match other usernames. Compare case-insensitively with LOWER() instead.
Rotated files were named by millisecond timestamp only, so two rotations
within the same millisecond renamed onto the same path and replaced the
earlier file. Append a counter until the name is free.
The request User-Agent was copied unbounded into every event dispatched
during a request, so bulk operations repeated an arbitrarily long header in
each audit entry.
Relation create/delete, attachment delete and comment delete resolved the
doer with a user-only conversion and dropped the error, so actions taken
through a link share were dispatched without a doer.
Two overlapping exports could both read the same previous archive and
overwrite each other's pointer, leaving one archive unreferenced and
outside expiry cleanup. The pointer is now swapped conditionally; on a
mismatch the export fails, its new file is removed and the event retry
re-runs it against the current pointer.
Task create, bulk create and duplicate responses loaded every stored
position for the task, which could include positions in saved filter views
belonging to other users once the async saved filter listener had run.
Restrict the lookup to views of the task's project.
Markdown mention resolution looked up any username, so mentioning another
user's bot rendered its display name into the stored HTML. Resolution now
gets the requesting identity and applies the same bot visibility rule as
user search: only humans and the requester's own bots are resolved, other
bots stay literal text.