Nobody has started this yet — be first.
Business impact
POST /auth/login checks the submitted password against the stored bcrypt hash and returns 401 Unauthorized on a mismatch -- correctly, and without leaking whether the email itself exists. But there is no limit anywhere on how many times a client may attempt this for a given email. Nothing in src/middleware/ tracks attempts, so a single account can be probed with an arbitrarily long password list, as fast as the network allows, forever. bcrypt's hashing cost (config.bcryptSaltRounds) is the only thing slowing an attacker down at all, and it's slowing down legitimate logins by the exact same amount -- it was never meant to be the sole defense against brute force.
Problem
There is no rate-limiting building block anywhere in this codebase. requireAuth and requireOwnership are both stateless, answering their question from the request/DB alone every time -- nothing tracks failed login attempts per email across requests, so POST /auth/login will accept unlimited attempts for the same email forever.
Current behavior
After 5 failed login attempts for the same email, the next attempt still returns 401 (a plain password mismatch) instead of 429 -- there is no threshold at all.
Expected behavior
After a configurable number of failed login attempts for the same email within a time window, further attempts for that email are rejected with 429, body { "error": { "code": "RATE_LIMITED", "message": "..." } }, without even checking the password. A successful login for that email (or the passage of the window) clears the count so a legitimate user who mistypes their password a couple of times is never locked out of their own account. The dedicated test in this ticket exercises a threshold of 5 attempts -- treat that as the contract to build against, though the exact number itself doesn't need to live as a magic literal deep in a route handler.
Steps to reproduce
cd nodejs/blog_api
node_modules/.bin/jest --config practice-tickets/jest.config.js
practice-tickets/tests/ticket07_login_rate_limiting_missing.test.js --verbose
Why this matters
This needs a genuinely new building block: nothing under src/middleware/ currently tracks any kind of per-request state across multiple requests (requireAuth and requireOwnership are both stateless, answering their question from the request/DB alone every time). A rate limiter has to keep a counter keyed by something derived from the request (here, the submitted email) and safely reset it after a time window -- and it has to do this correctly when requests for the same key arrive concurrently, or the limiter itself becomes another race to get wrong. src/lib/errors.js's Errors object doesn't have a 429 case yet either -- every existing status code it can produce is 400/401/403/404/409.
Suggested approach
Two pieces, both new. First, a small middleware (e.g. src/middleware/rateLimiter.js) that, given a key-extraction function (here, req.body.email), tracks recent failed attempts per key and can answer "has this key exceeded the limit right now?" -- keep it in-memory, since this app has no other shared state store and a single-process in-memory counter is the right scope for this exercise. Second, wire it into POST /login in src/routes/auth.js: check the limiter before doing the bcrypt.compare work, record a failure when the password check fails, and clear that email's count on a successful login. You'll also want to add a TooManyRequests case to the Errors object in src/lib/errors.js, following the exact pattern the existing entries (Unauthorized, Forbidden, ...) already use.
Acceptance criteria
Verification
node_modules/.bin/jest --config practice-tickets/jest.config.js practice-tickets/tests/ticket07_login_rate_limiting_missing.test.js --verbose
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b feat/login-rate-limiting-missingFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/login-rate-limiting-missingSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign indocker compose down -vTo run locally without Docker, you need a local PostgreSQL instance:
npm install
cp .env.example .env # edit DATABASE_URL, JWT_SECRET, etc.
npm run prisma:migrate:dev # applies migrations to your local Postgres
npm run dev # starts with --watch on http://localhost:3000
Run the project's own test suite (real Postgres, not mocks -- point DATABASE_URL at a disposable database first):
npm test
Work the tickets in practice-tickets/ (ticket01 through ticket07); each has its own dedicated Jest test under practice-tickets/tests/, run via a separate Jest config (practice-tickets/jest.config.js) so plain npm test never picks them up:
docker compose up -d postgres # if it isn't already running
npm run prisma:migrate
./practice-tickets/run_tickets.sh # all 7, clean pass/fail summary
node_modules/.bin/jest --config practice-tickets/jest.config.js \
practice-tickets/tests/ticket01_pagination_skip_off_by_one.test.js --verbose # a single ticket
Level 2
Implement a feature
Extend the system within its own patterns.