Feature: Login and password-reset requests are rate-limited per client IP As the maintainer of RenewCMS I want POST /v1/auth/login and POST /v1/auth/password-reset to reject a client that sends too many requests within a short window So that these public routes gain brute-force / abuse protection, which today does not exist at all Background: Given POST /v1/auth/login and POST /v1/auth/password-reset are both public, mounted directly in NewAuthRouter outside the jwt-protected group And a new chi middleware, RateLimiter (api/middleware/rateLimit.go), is layered only around these two routes as an additional r.Group inside NewAuthRouter; /register, /logout, /password-reset/confirm, /verify and the delete-register route are all unaffected And the client is identified by request IP: chi's own RealIP middleware (github.com/go-chi/chi/v5/middleware) runs first and rewrites r.RemoteAddr from X-Forwarded-For/X-Real-IP when present (matching the nginx reverse proxy setup in docs/deployment.md that already sets X-Forwarded-For), falling back to the raw RemoteAddr otherwise; RateLimiter keys its counters on r.RemoteAddr And RateLimiter(maxAttempts int, window time.Duration) func(http.Handler) http.Handler keeps an in-memory, mutex-guarded fixed-window counter per (route, client IP) pair: the first request from a client opens a window starting now; each subsequent request from the same client within window of that start increments the count; once the count exceeds maxAttempts within the window, every further request from that client is rejected until the window elapses, at which point the next request starts a fresh window And every request to a rate-limited route counts toward its limit, whether the login credentials or the reset email turn out to be valid or not And a rejected request never reaches the auth handler or any use case: no AuthenticateUserUseCase or RequestPasswordResetUseCase call happens, no jwt cookie is set or cleared, and no email is sent And a rejected request receives 429, body apperr.TooManyRequestsErrorMessage ("Too many requests. Please try again later."), and a Retry-After header set to the number of whole seconds remaining until the client's current window elapses And /login and /password-reset are rate-limited independently: exhausting one client's login limit does not affect that same client's ability to call /password-reset, and vice versa And two different client IPs are tracked independently on the same route And this slice adds two new environment variables, both given defaults in cmd/server/main.go's envDefaults (not requiredEnvVars, so existing deployments keep working unchanged): AUTH_RATE_LIMIT_MAX_ATTEMPTS (default "5") and AUTH_RATE_LIMIT_WINDOW_SECONDS (default "60"), read by config.NewSettingsFromEnv into a new config.Settings.AuthRateLimit field (MaxAttempts int, Window time.Duration), documented in .env.example and docs/configuration.md; the same MaxAttempts/Window pair configures both the /login and the /password-reset limiter (two independent counters, one shared configuration) And this is a transport-level protection with no domain or use-case involvement: RateLimiter never imports internal/domain, internal/infrastructure/useCases, persistence or mailer Scenario: A client under the limit behaves exactly as before Given AUTH_RATE_LIMIT_MAX_ATTEMPTS is 3 for a window covering the whole test And the user repository contains a user "alice" with a correct password When the client at IP "203.0.113.5" sends POST /v1/auth/login with alice's correct credentials 3 times in a row Then all three responses have status 200 and a jwt cookie is set each time Scenario: The request beyond the limit is rejected Given the same setup as above, with 3 requests already sent from "203.0.113.5" When the client at IP "203.0.113.5" sends a 4th POST /v1/auth/login within the window Then the response status is 429 And the response body is exactly apperr.TooManyRequestsErrorMessage And the response has a Retry-After header And AuthenticateUserUseCase was not invoked for this 4th request Scenario: Failed attempts count toward the same limit as successful ones Given AUTH_RATE_LIMIT_MAX_ATTEMPTS is 3 When the client at IP "203.0.113.7" sends 3 POST /v1/auth/login requests with a wrong password, each answered 403 And then sends a 4th POST /v1/auth/login with the correct password Then the 4th response is 429, not 200 — correct credentials do not bypass the limit Scenario: password-reset requests are limited independently from login Given AUTH_RATE_LIMIT_MAX_ATTEMPTS is 2 When the client at IP "203.0.113.9" sends 2 POST /v1/auth/password-reset requests And then sends a 3rd POST /v1/auth/password-reset Then the 3rd response is 429 and no email was sent for it But that same client's POST /v1/auth/login requests are still counted against a separate, unexhausted limit Scenario: Different clients are tracked independently Given AUTH_RATE_LIMIT_MAX_ATTEMPTS is 1 When client IP "203.0.113.10" sends one POST /v1/auth/login, consuming its own limit And client IP "203.0.113.11" then sends one POST /v1/auth/login Then the second client's request is not rejected by rate limiting Scenario: The window resets once it elapses Given AUTH_RATE_LIMIT_MAX_ATTEMPTS is 1 and a short AUTH_RATE_LIMIT_WINDOW_SECONDS the test can wait out When client IP "203.0.113.12" sends one POST /v1/auth/login, consuming its limit And an immediate second request from the same IP is rejected with 429 And the test waits until the window has elapsed And the same client sends another POST /v1/auth/login Then that request is not rejected by rate limiting Scenario: Routes outside this slice's scope are never rate-limited Given AUTH_RATE_LIMIT_MAX_ATTEMPTS is 1 When the client sends 5 POST /v1/auth/register requests in a row Then none of them is rejected with 429 on account of rate limiting