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.
- Redesign in-game client toolbar with refined glass styling and buttons
- Live online count + emulator status streamed over SSE multicast
- Who's-online tooltip throttled to reduce repeated requests
- Fix show button so it always returns the hidden toolbar
- Use theme CSS variables instead of hardcoded colors
- Add toolbar translations across all 25 locales
- Enhance SurfaceCard with refined borders, layered shadows, and polished
primary-tinted header with an icon container
- Update homepage blocks (hero, features, stats, login, news, photos) for
proper mobile/tablet/desktop responsiveness and tighter mobile spacing
- Improve content-card, stat-block, card-grid and card styling with subtle
depth (layered shadow + inner hairline) and smoother hover states
- Refine site header, footer, top header and site layout spacing for small
screens while keeping a consistent professional look on large screens
- Add min-tap-height targets and responsive typography on mobile
Reuse the outstanding auth_ticket instead of minting a fresh one on every
/client load, so reloading the page or opening a second tab no longer
invalidates a game session that is still connecting. New tickets are minted
with a guard against the previously-read value so concurrent launches
converge on the same ticket.
Revoke the auth_ticket when signing out (toolbar, header and sign-out
everywhere) so a leaked ticket can no longer be replayed against the
emulator, and prevent SSO leakage via referral by setting no-referrer on the
client iframe. Strip all whitespace from the ticket prefix and build the
launch URL through a tested helper that handles query strings, existing sso
params and URL fragments correctly.
Wrap next/link in a shared Link component that ships prefetch=false by
default, so no route is ever prefetched (viewport or hover) anymore, and
drop the DNS prefetch hint. Removes hidden background requests that were
the source of intermittent issues.
- Fix proxy routes to use resolveImagerBase instead of removed resolveUpstreamBase
- Fix avatarImageUrl type signature to use AvatarOptions
- Run biome formatter
- Move inline form with onSubmit handler from top-header.tsx (Server Component) into new RoomQuickEntry Client Component
- Add badges section to /me page showing equipped badges from users_badges
- Add last website login timestamp from website_login_logs to /me page stats
- Add search bar to top-header for user search
- Add achievement score, respects received, friend count stats to /me page
- Add quick links card to /me page with Messages/Profile/Settings/Shop
- Clean up RoomQuickEntry: use form.elements.namedItem, remove unused ref, add aria-label
- mobile-nav: pure block/hidden toggle, no transforms, no transition-all
- nav-dropdown: clean block/hidden toggle, no CSS animation hacks
- navigation: removed shadow-sm, no will-change, no GPU compositing
- top-header: consistent mobile layout, clean details/summary dropdowns
- admin-mobile-wrapper: inline transition instead of CSS class for sidebar
- globals.css: removed transition-all from nav-item/dropdown-item, replaced
with specific color/background-color transitions only, added hover bg
- Remove duplicate home link in mobile nav
- Remove overflow-hidden clipping main content
- Improve mobile menu scrolling and spacing
- Make top-header currencies wrap on small screens
- Reduce site-header height on mobile
- Add global mobile CSS overrides for tables, padding, fonts
- 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 ✓
- Redesign site header with multi-layer gradient overlays, decorative glows, dot pattern texture, responsive clamp font sizing, and animated ping online badge
- Add glass-dropdown CSS class with 60% opacity backdrop blur (20px) and smooth dropdownIn animation (scale + translate)
- Fix dropdown positioning with top-full for proper alignment under menu triggers
- Standardize all dropdowns (nav, top-header, color picker, language switcher) to use glass-dropdown class
- Reduce dropdown background opacity from 97% to 80% so backdrop-filter is visible
- Add glass-dropdown CSS class with blur(16px) and saturate(180%)
- Add backdrop blur to mobile nav overlay for smoother feel
- Increase photo lightbox backdrop blur from 2px to 8px
- Add background-blend-mode: overlay to site-bg for richer texture
- Rich profile (/u/[username]): wallet (credits/duckets/diamonds), friends
grid (messenger_friendships), and owned rooms sections.
- Login history: new website_login_logs table (model + migration 0007),
recorded on every successful sign-in (ip + user-agent), surfaced on a new
/settings/sessions page (with failed-attempt list from failed_logins).
- Photos lightbox + home article slider (client components, no Swiper dep).
- /client/flash launcher (SSO ticket like the Nitro page).
- Admin: private chatlogs section in /admin/logs, /admin/radio/moderation
(shout moderation), a "users by rank" inline bar chart on the dashboard,
and a TinyMCE rich-text editor on the article admin forms.
- Niche API: /api/values/[id], /api/guilds(+/[id]), /api/radio/auto-play.
Verified live (prod, amx_test): login recorded → /settings/sessions shows
it with device; profile renders wallet/friends/rooms; dashboard chart +
private-chat logs + /client/flash + /api/guilds all OK. Reverted test data.
tsc 0, vitest 49/49, next build 0.
Final parity push (web-tier only):
- REST API write + token auth: POST /api/tokens (issue a personal_access_token
for the session user), Bearer auth via src/lib/api-auth.ts, POST
/api/articles/[slug]/comment, GET/DELETE /api/me/tokens, full tickets API
(/api/tickets +[id] +[id]/reply), radio current-dj/points/points-leaderboard/
embed-config + POST shouts, and a real-time /api/radio/stream (SSE). 31 public
API routes total.
- Pages: /draw-badge (buy a custom profile badge → credits + RCON), /me
dashboard (stats + online friends + referral claim). Wired into the nav.
- HTML sanitisation (sanitize-html) — the HTMLPurifier equivalent — applied to
writeable boxes + article bodies before dangerouslySetInnerHTML.
- "Dusk" dark theme preset + a default-dark site option honoured by the
no-flash boot script.
Verified live (prod, amx_test): token issue → Bearer endpoint 200, no-token
401; /api/me/tokens lists it; current-dj/leaderboard JSON; /me + /draw-badge
200; reverted the test user + tokens. tsc 0, vitest 49/49, next build 0.
Make the template match the atom theme, not just approximate it:
- Add Tailwind v4 (+forms/typography plugins, postcss) and port the real atom
CSS (global.css + atom app.css) into globals.css: nav-item underline, currency
+ navigation icon classes, site-bg, card-base/hover-lift, text utils — asset
paths adapted to /assets.
- Copy the real theme assets (backgrounds, icons/currency/navigation, profile,
leaderboards) into public/assets.
- Rebuild the shell 1:1 from the atom Blade: TopHeader (currency pills +
user/admin dropdowns, auth-only), SiteHeader (header image + black/50 overlay,
logo+online+Nitro client / guest Login+Create-account CTA), Navigation (white
bar, nav-item + Community/Assistance dropdowns), Footer. ThemeVars injects the
DB-driven CSS custom properties into :root like app.blade.php. Layout uses the
atom body/site-bg + grid-cols-12 max-w-7xl content wrapper.
Verified: tsc 0, next build exit 0; curl confirms the atom markup, compiled CSS
references the assets, and background-light.jpg + currency/navigation icons all
serve 200.