Second review pass covering security, performance, admin tooling and the
public/room flows. All HIGH and MEDIUM findings from the audit are resolved;
nothing in this commit changes the visible feature set.
Authentication & session security
- CSP is now set on the request headers in the proxy, which is what Next.js
uses to derive the render nonce, so the nonce is effective.
- 2FA: an already-enabled user cannot re-enroll, the setup endpoint is
rate-limited per account, and confirmed codes are persisted so the second
secret no longer silently never applies.
- Password reset revokes the ticket, authTicket and all personal access
tokens, and bumps the token version so existing sessions die. The same
revocation is now wired into the staff-side password reset.
- /reset and /verify return a stable error code instead of raw text; the
mail lookups are ordered by id so duplicates cannot vary between runs.
- Resending the verification mail gets a per-address cooldown on top of the
per-user limit.
- Issue API tokens with the narrower radio/ticket ability set instead of "*".
Authorization & input handling
- Mid-rank staff can no longer keep dynamically granted non-view admin.*
permissions: existing grants are revoked by migration and the grant lookup
is restricted to "%.view". Rank guards use the dynamic super-admin check.
- Alerting a user is permission-checked and audited like the other tools.
- Material mutations (giveCredits/giveDuckets/giveDiamonds, the admin user
actions route, bulk user actions) are capped and rank-guarded, and bulk
ids are bounded.
- updateRoom / updateRoomItem write through a field allowlist, and items
may only be edited through their own room.
- Classnames reaching the filesystem are validated before use so a crafted
value cannot escape the asset directories.
- The word filter now also covers offline mails, guild forum threads and
replies, and user mottos.
- Media uploads are validated by magic bytes, /api/media requires the page
edit permission, APP_URL must be configured once mail is enabled, and the
diagnostics error route checks the fetch site header.
Admin tooling
- Secret settings render masked and cannot be overwritten with a blank or
an arbitrary raw key; radio credentials are new password inputs.
- Commandocentrum balance changes are audited.
- Admin list pagination reads the caller's per-page instead of the max, and
the log exporter caps offset and search length.
Performance
- Catalog translations are cached per module, with a cheap revision hash;
the public online count uses a stale window instead of hammering the DB.
- The cache warmup now primes the payload the home route actually reads.
- TopHeader batches its queries into one round trip, and LCP avatars load
eagerly.
- motion/react and sonner are no longer part of the root layout; the nav
dropdown and mobile nav panels are lazy client chunks. Anonymous visitors
again get the navigation chrome, and public pages get an edge cacheable
response.
Accessibility
- Nested <main> elements in phase pages became <section>; the page entrance
and route progress animations are pure CSS that respect reduced motion.
- Add persistent disk cache for rendered avatars/badges (storage/imaging)
so repeats never touch the flaky local renderer and cached renders
survive upstream downtime
- Serve cache-first with stale-on-error; cut primary/fallback timeouts
from 10s/6s to 4s/4s so failing images cannot stall pages
- Avatar proxy now returns a graceful 200 silhouette instead of 502 when
no renderer can produce a figure, so no broken-image glyphs appear
- Badge endpoint becomes a caching proxy trying configured CDN, public
Habbo CDN and local /swf copy in order, and drops the fragile IP rate
limit that could blank badge streams
- Route all site badge images (profile, me, badges, apply pages) through
the cached proxy instead of hot-linking images.habbo.com
Remove the per-route 'export const instant = false' opt-outs now that the root layout carries the single Cache Components opt-out. Child pages inherit the opt-out, so admin/mod/radio leaf pages that only access cached or DB data stay instant while runtime-dependent pages remain dynamic. Update the staff-smoke contract test to assert the root-layout contract.
- Clone import: defer FurnitureData.json writes and append all entries in a
single batched write instead of one read-modify-write per item, removing
the main serialization bottleneck for large batches.
- Clone import: raise SSE batch concurrency cap from 5 to 10 and bump the
clone client/route default from 2 to 6.
- Add a flush hook to runSseBatch so callers can batch deferred work before
batch_complete is emitted, and surface flush errors as an error event.
- Enable Next.js Cache Components (instant: false opt-out) and silence the
related build warnings in next.config.ts.
- Switch isomorphic-dompurify to dompurify and refresh dependencies.
Route all user-facing Atom hotel defaults through resolveHotelName (settings then HOTEL_NAME env then brand constant). Exclude Playwright e2e from tsconfig until deps are installed.
Co-authored-by: Cursor <[email protected]>
Public chrome and width live only under (site). /admin and /client inherit the root layout alone, so housekeeping width is no longer tied to the public template.
Co-authored-by: Cursor <[email protected]>