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.
- password.ts: derive plain and salted digest detection from one DIGEST_SCHEMES
table instead of parallel hardcoded lists, so adding a family is one row.
- auth.ts: move 2FA challenge verification into twofactor-verification.ts and
the website login-log insert into website-login-log.ts, slimming the
NextAuth provider to orchestration only.
- deps: bump @formatjs/icu-messageformat-parser, @tanstack/react-query, jszip,
lucide-react, motion (patch/minor only). @types/react stay pinned per
pnpm-workspace.yaml; next-auth is already at the newest available (v5 beta).
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.
getLoginUser selected users.account_blocked, which does not exist in the
DB (nor the Drizzle schema). Every credentials authorize() call threw a
SQL error -> NextAuth CallbackRouteError -> 'error=Configuration', so no
login could ever succeed. Remove the phantom column from the query and
LoginUser interface.
Also fix all remaining biome noNonNullAssertion / noExplicitAny lint
warnings so CI's check job (biome:lint) passes and the push deploy runs.
- hashPassword now emits argon2id (same params as the legacy AtomCMS
Laravel setup: memory 64MB, iterations 4, parallelism 1)
- legacy md5 and bcrypt hashes are verified and auto-upgraded to
argon2id on successful login (CONVERT_PASSWORDS=true)
- replace BCRYPT_ROUNDS env with ARGON2_MEMORY_KB / ARGON2_ITERATIONS /
ARGON2_PARALLELISM
- update README and add tests for argon2id and bcrypt upgrade paths
- Remove Discord + Google OAuth providers from NextAuth config
- Remove social login buttons (Discord/Google) from login form
- Delete link-discord.ts action and discord-verify-form.tsx component
- Simplify verify page to email-only verification flow
- Remove connections settings page and its link from settings
- Clean env.ts, .env, .env.example of Discord/Google client vars
- Remove discordUrl social icon from register, login, and homepage
- Clean translation files: remove continueWithDiscord, continueWithGoogle, connections keys
Cut Auth.js DB load with cached jwtVersion checks, surface Redis in /api/health and deploy warnings, add admin help-center ticket reply UI, rate-limit API tickets/reactions/referral claims, and revoke PATs on sign-out-everywhere.
Co-authored-by: Cursor <[email protected]>
- 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)
- 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.
- Custom not-found (404) + error / global-error boundaries, styled with
the public design system; raw errors logged, never shown to users.
- In-process rate limiter (src/lib/rate-limit.ts) wired into the abuse-
prone flows: login (10/5min/IP), register (5/10min/IP), password-reset
request (3/15min/IP), keyed by the proxy-forwarded client IP.
- SEO/metadata: root generateMetadata sets a `%s · {hotel}` title
template from the live hotel_name; dynamic generateMetadata on
news/[slug] (article title + excerpt) and u/[username] (name + motto);
static titles on 12 primary public pages.
- env.ts: added the vars introduced since (PASSWORD_HASH, OPENAI_API_KEY,
DISCORD_WEBHOOK_URL, ALERT_EMAIL, PAYPAL_*) so env stays authoritative.
Verified on the prod server: /missing → 404 card, news title renders
"News · Habbo". tsc 0, vitest 49/49, next build 0.
Minimal but real App Router app that builds (next build exit 0):
- src/lib/auth.ts: NextAuth v5 Credentials provider calling checkLogin()
(argon2id/bcrypt + md5->argon2id upgrade gated by CONVERT_PASSWORDS), JWT
session, /api/auth/[...nextauth] route handler.
- src/app: root layout, home (force-dynamic, reads hotel_name via siteSettings),
/login client form (signIn).
- next.config.ts: pinned turbopack.root, serverExternalPackages for the Prisma
MariaDB adapter; tsconfig set up for Next.
Routes: / (dynamic), /login, /api/auth. Verified: next build exit 0, 28 tests.
Still needs DB+APP_KEY to run auth end-to-end. i18n/middleware/pages to follow.