T01: core infrastructure + auth + multi-tenant + RLS
- 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)
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user