Closes the four HIGH/MEDIUM items left open after the previous pass.
Login lockout
- The only login limits were keyed on the client IP, so a distributed attempt
could grind on one account indefinitely. Added a per-account lockout with a
budget of 8 failures per 15 minutes.
- The bucket is keyed on the RESOLVED account id, not on the submitted string:
users may sign in with either username or e-mail and neither the lookup nor
the input normaliser folds case, so an input-keyed bucket would hand out a
fresh budget per spelling of the same account.
- precheckLogin and NextAuth's authorize share the bucket, so the pre-check
cannot be used to buy extra attempts and a client that skips it entirely is
still bounded. Both check the lockout BEFORE verifying the password: the
success path clears the counter, which would otherwise walk a locked account
straight back in on the right password.
- A successful login clears the failures, which needs two new primitives in
rate-limit.ts: peekRateLimit (read-only, does not consume a unit) and
clearRateLimit.
- Fixed a latent inconsistency while doing so: the in-process bucket capped its
counter at the limit while Redis' INCR kept climbing, so the two backends
disagreed about how far over the limit a key was. Both now track the true
count.
Mail lookup index
- Added an index on users.mail (0035). Password reset, e-mail verification and
the resend cooldown all resolve a single account from a submitted address and
were full table scans of `users`. Deliberately non-unique: legacy rows can
hold the same address more than once, so a unique index would fail to apply.
Resend captcha
- /verify's resend form triggers real outbound mail and was reachable with only
a cooldown. It now runs the configured captcha before the account lookup and
before any send.
Client message payload
- The root layout serialised the whole catalogue into every page. pages.admin
and admin are ~177 KB of the ~235 KB and are unreachable from the public route
group, so that layout now installs its own provider with the staff namespaces
removed. Nested providers replace rather than merge, which is why this has to
live in the segment layout. /admin, /mod, /client and /admin-next keep the
full set; a guard test fails if a public page ever references a staff
namespace.
Legacy md5/argon2id hashes are now always upgraded to bcrypt on login, so
the CONVERT_PASSWORDS flag is no longer used. Drop it from env schema,
.env.example, the docker installer, and test mocks.
The username normalization, dummy-hash constant, password check and
email-verification gate were duplicated between precheckLogin and the
NextAuth credentials authorize handler. Move them into a single
login-core module so both paths share one source of truth and stay
consistent.
Database:
- Add missing indexes (users.credits, users_currency(type,amount),
users_settings.respects_received, camera_web.timestamp,
messenger_offline.user_id) via migrations 0020/0021
- Use partial .select() everywhere instead of SELECT * (tickets, users,
rooms, audit logs, catalog tree, polls, radio, password reset)
- Add queryPrepared/queryPreparedOne (server-side prepared statements)
and switch the login check to a prepared statement; drop dead
cache options from the pool config
- Raise total_users/total_rooms COUNT(*) cache TTL to 5m
Caching:
- Consolidate the three cache helpers (cached, redisCache, cachedQuery)
into a single memory-first implementation backed by Redis
- invalidateKey now clears the in-process cache as well as Redis
- Cache homepage sections, news list, and leaderboard tabs; share one
news_list cache key between homepage and news archive
- siteSettings: in-process cache with TTL so repeated getters no longer
pay a Redis round-trip per call
- Share a 10s poll cache across all radio SSE connections
- Normalize timestamps after cache reads (Redis JSON round-trip)
Assets:
- Enable AVIF/WebP via images.formats and remove unoptimized from news
covers and the homepage hero (149KB jpg) with proper sizes/priority
- Support ?format=webp|avif|png in the /imaging proxy via sharp
Other:
- Fix pnpm supply-chain minimumReleaseAge failures by excluding the
freshly-published packages (next 16.3.1, hookform resolvers 5.8.0,
resend 6.20.0)
- Remove unused before/after fields from housekeeping AuditEntry