7a7daf8100
- 10 models: tenants, users, user_tenants, roles, sessions, audit_log, deletion_log, notifications, password_reset_tokens, api_tokens - Session-based auth (Redis + PostgreSQL audit trail) - Multi-tenant with ORM-level filtering + PostgreSQL RLS (set_config) - RBAC with roles/permissions + field-level permissions - CSRF protection via Origin header validation - Auth rate limiting (Redis counters with TTL) - CORS with explicit origins (no wildcard) - Health endpoint (no auth required) - Notification service + audit log middleware - 29 tests, 26 ACs, all passing - Coverage: 62% (infrastructure modules pending coverage in later tasks)
48 lines
1.4 KiB
Python
48 lines
1.4 KiB
Python
"""Redis-based rate limiting for auth endpoints."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import HTTPException, Request, status
|
|
|
|
from app.core.auth import get_redis
|
|
from app.config import get_settings
|
|
|
|
|
|
async def check_rate_limit(
|
|
redis_key: str,
|
|
max_attempts: int,
|
|
window_seconds: int,
|
|
) -> None:
|
|
"""Check rate limit using Redis INCR + EXPIRE.
|
|
Raises 429 if limit exceeded.
|
|
"""
|
|
redis = get_redis()
|
|
current = await redis.incr(redis_key)
|
|
if current == 1:
|
|
await redis.expire(redis_key, window_seconds)
|
|
if current > max_attempts:
|
|
ttl = await redis.ttl(redis_key)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
detail={
|
|
"detail": "Rate limit exceeded",
|
|
"code": "rate_limited",
|
|
"retry_after": ttl,
|
|
},
|
|
headers={"Retry-After": str(ttl)} if ttl > 0 else {},
|
|
)
|
|
|
|
|
|
async def reset_rate_limit(redis_key: str) -> None:
|
|
"""Reset a rate limit counter (e.g. on successful login)."""
|
|
redis = get_redis()
|
|
await redis.delete(redis_key)
|
|
|
|
|
|
def get_client_ip(request: Request) -> str:
|
|
"""Extract client IP from request."""
|
|
forwarded = request.headers.get("x-forwarded-for")
|
|
if forwarded:
|
|
return forwarded.split(",")[0].strip()
|
|
return request.client.host if request.client else "unknown"
|