Single attemptLogin() orchestrates lockout check, bcrypt verify (against
real or dummy hash), success/failure logging, and a configurable minimum
response-time clamp. Both login routes will share this — no duplication.
Adds three logger events for the operator's escalation pipeline:
AUTH_LOCKOUT_TRIGGERED, AUTH_LOCKED_ATTEMPT, and AUTH_RATE_LIMITED.
fail2ban filters can pick these up to escalate persistent attackers from
in-app lockout to IP ban.
Constant-time defense: unknown users still pay bcrypt cost (via dummy hash)
and the clamp ensures locked-vs-unknown-vs-wrong-password aren't
distinguishable by response time. Username is canonicalized (lowercase + trim)
before lookup so attackers can't bypass lockout via case variation.
Persists per-username failed-attempt counts and computed locked_until
timestamps. Lockout service computes exponential-backoff durations
(min(base * 2^(count-threshold), max)) with auto-unlock once locked_until
passes. Successful login deletes the row, resetting the counter.
Pure DB-keyed lockout — survives server restarts and shares state across
both login routes (HTML and JSON) when wired in a later step.
Lays the foundation for brute-force defense: per-username attempt tracking
table, configurable lockout/rate-limit thresholds, and a memoized dummy
bcrypt hash so unknown-user paths can be timed identically to wrong-password
paths in a later step.
Adds @fastify/rate-limit dependency for upcoming per-IP rate-limit on
login routes.
- Set up package.json (ESM, scripts), tsconfig.json, vitest.config.ts
- Install runtime and dev dependencies
- Add CLAUDE.md with architecture notes and code quality rules
- Config module with env var parsing and JWT_SECRET validation
- DB schema: users + files tables with FK cascade
- DB queries: createUser, getUserBy*, createFile, getFileById, getFilesByUserId, deleteFile
- Tests for config, db/users, db/files (15 tests passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>