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.
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.
precheckLogin already normalized the username with NFC, but the
NextAuth credentials authorize handler only trimmed it. This caused a
mismatch for accounts with accented/non-ASCII usernames: the precheck
passed while the actual sign-in lookup found no user and returned
'invalid username or password'.
Also normalize the password to NFC in both the precheck and the
authorize handler to match how register.ts hashes it.
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
All user-supplied string values from FormData now go through
String.prototype.normalize('NFC') to prevent Unicode homoglyph
attacks and canonicalization bypasses. NFC is idempotent for
already-normalized strings, so this is a pure security improvement
with zero behavioral change for legitimate users.
- Remove production DB dump (db_backup_*.sql) and update.log from git tracking
- Add DB backups to .gitignore
- Replace all console.log/console.error with structured logger module
- Translate Dutch error messages to English (link-discord.ts)
- Remove dead code blocks (register-form.tsx false && pattern)
- Add ESLint flat config with TypeScript, React, Next.js, jsx-a11y, and security plugins
- Add Prettier config
- Add eslint-plugin-security for security-aware linting
- Fix all 119+ ESLint warnings across the codebase:
- Resolve security/detect-object-injection with safe access patterns
- Resolve security/detect-non-literal-fs-filename with path traversal validation
- Replace <img> with next/image <Image> component
- Remove unused variables and imports
- Replace non-null assertions with proper type guards
- Replace <a> with <Link> for internal navigation
- Use next/script Script component for external scripts
- Fix setState-in-useEffect anti-patterns (navbar-color-picker, logo-generator, theme-switcher)
- Add lint and format scripts to package.json
All checks: typecheck ✓, tests 58/58 ✓, lint 0 errors 0 warnings ✓
- Add DB index on bans.user_id to speed up per-request ban lookups (migration 0008)
- Replace in-process rate limiter with Redis-backed implementation with in-memory fallback
- Add Redis caching layer for site settings with TTL invalidation (migration 0009)
- Add rate limiting to resetPassword to prevent token brute-force attacks
- Update all rateLimit callers to await the now-async function
- Flesh out RadioContests and RadioGiveaways models with title, description, prize, date, and winner columns
- Update radio contest/giveaway pages to display new fields
- Add tests for rate limiter (4 tests) and password-reset actions (3 tests)
- Add REDIS_URL environment variable (optional, falls back to in-memory)