fix(api): все роутеры за авторизацией, отказ от слабого JWT_SECRET, лимитер и X-Forwarded-For

get_current_user навешивается на роутер целиком, публичными остаются только health и auth (список закреплён tests/api/test_route_auth.py). API не стартует с плейсхолдером или коротким JWT_SECRET без ALLOW_DEV_SECRET=1. Лимитер логинов ограничен по числу ключей. Клиентский IP берётся из последнего хопа X-Forwarded-For.
This commit is contained in:
Dmitry
2026-09-19 21:55:23 +03:00
parent bc5d5c0811
commit 600496048f
8 changed files with 140 additions and 31 deletions
+41
View File
@@ -1,3 +1,6 @@
import pytest
async def test_login_refresh_logout_cycle(client, user):
r = await client.post("/api/v1/auth/login", json=user)
assert r.status_code == 200, r.text
@@ -41,3 +44,41 @@ async def test_protected_routes_need_token(client):
assert r.status_code == 401
r = await client.get("/api/v1/auth/me", headers={"Authorization": "Bearer garbage"})
assert r.status_code == 401
def test_client_ip_trusts_only_the_last_forwarded_hop():
from starlette.requests import Request
from fintracker.api.deps import client_ip
def req(xff: str) -> Request:
return Request({"type": "http", "headers": [(b"x-forwarded-for", xff.encode())]})
assert client_ip(req("1.2.3.4")) == "1.2.3.4"
assert client_ip(req("6.6.6.6, 1.2.3.4")) == "1.2.3.4"
def test_limiter_evicts_instead_of_growing_without_bound():
from fintracker.api.ratelimit import SlidingWindowLimiter
limiter = SlidingWindowLimiter(limit=1, max_keys=3)
for i in range(10):
assert limiter.hit(f"ip-{i}") is None
assert len(limiter._hits) <= 3
assert limiter.hit("ip-9") is not None # the freshest key keeps its state
async def test_weak_jwt_secret_refuses_to_start(monkeypatch):
from fintracker.api.app import _lifespan
from fintracker.config import get_settings
monkeypatch.setenv("JWT_SECRET", "change-me")
monkeypatch.delenv("ALLOW_DEV_SECRET", raising=False)
get_settings.cache_clear()
try:
with pytest.raises(RuntimeError, match="JWT_SECRET"):
async with _lifespan(None): # type: ignore[arg-type]
pass
finally:
monkeypatch.undo()
get_settings.cache_clear()