8a4c194e39
- Add rate_limit_auth_requests (5/min) and rate_limit_auth_window_seconds (60) settings - Add rate_limit_redis_enabled flag for opt-in Redis usage - Refactor _SlidingWindowCounter into InMemorySlidingWindow class - Add RedisSlidingWindow using sorted sets with fallback to in-memory - Add third _auth_strict_limiter for POST /auth/* paths (5 req/min) - Add protocol-based backend selection at module load time - Update tests for auth strict limiter and Redis fallback behavior Co-Authored-By: Paperclip <noreply@paperclip.ing>
85 lines
3.2 KiB
Python
85 lines
3.2 KiB
Python
import base64
|
|
|
|
from pydantic import AliasChoices, Field, model_validator
|
|
from pydantic_settings import BaseSettings
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = {"env_prefix": "CARTSNITCH_"}
|
|
|
|
database_url: str = Field(
|
|
default="postgresql+asyncpg://cartsnitch:cartsnitch@localhost:5432/cartsnitch",
|
|
validation_alias=AliasChoices("CARTSNITCH_DATABASE_URL", "DATABASE_URL"),
|
|
)
|
|
redis_url: str = "redis://localhost:6379/0"
|
|
|
|
jwt_secret_key: str
|
|
jwt_algorithm: str = "HS256"
|
|
jwt_access_token_expire_minutes: int = 15
|
|
jwt_refresh_token_expire_days: int = 7
|
|
|
|
service_key: str
|
|
fernet_key: str
|
|
|
|
auth_service_url: str = "http://auth:3001"
|
|
|
|
cors_origins: list[str] = ["http://localhost:3000", "https://cartsnitch.com"]
|
|
|
|
receiptwitness_url: str = "http://receiptwitness:8001"
|
|
stickershock_url: str = "http://stickershock:8002"
|
|
clipartist_url: str = "http://clipartist:8003"
|
|
shrinkray_url: str = "http://shrinkray:8004"
|
|
|
|
rate_limit_requests: int = 60
|
|
rate_limit_window_seconds: int = 60
|
|
rate_limit_auth_requests: int = 5
|
|
rate_limit_auth_window_seconds: int = 60
|
|
rate_limit_redis_enabled: bool = True
|
|
rate_limit_enabled: bool = True
|
|
|
|
_PLACEHOLDER_VALUES = {"change-me-in-production"}
|
|
|
|
@model_validator(mode="after")
|
|
def validate_secrets(self):
|
|
if not self.jwt_secret_key or self.jwt_secret_key in self._PLACEHOLDER_VALUES:
|
|
raise ValueError(
|
|
"CARTSNITCH_JWT_SECRET_KEY must be set to a secure value. "
|
|
'Generate one with: python -c "import secrets; print(secrets.token_urlsafe(32))"'
|
|
)
|
|
if not self.service_key or self.service_key in self._PLACEHOLDER_VALUES:
|
|
raise ValueError(
|
|
"CARTSNITCH_SERVICE_KEY must be set to a secure value. "
|
|
'Generate one with: python -c "import secrets; print(secrets.token_urlsafe(32))"'
|
|
)
|
|
if not self.fernet_key or self.fernet_key in self._PLACEHOLDER_VALUES:
|
|
raise ValueError(
|
|
"CARTSNITCH_FERNET_KEY must be set to a valid Fernet key. "
|
|
"Generate one with: python -c "
|
|
"'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())'"
|
|
)
|
|
try:
|
|
decoded = base64.urlsafe_b64decode(self.fernet_key.encode())
|
|
if len(decoded) != 32:
|
|
raise ValueError
|
|
except Exception:
|
|
raise ValueError(
|
|
"CARTSNITCH_FERNET_KEY must be a valid Fernet key "
|
|
"(32 bytes, url-safe base64 encoded). "
|
|
"Generate one with: python -c "
|
|
"'from cryptography.fernet import Fernet; "
|
|
"print(Fernet.generate_key().decode())'"
|
|
) from None
|
|
return self
|
|
|
|
@model_validator(mode="after")
|
|
def normalize_database_url(self):
|
|
"""Normalize postgresql:// → postgresql+asyncpg:// for the asyncpg driver."""
|
|
if self.database_url.startswith("postgresql://"):
|
|
self.database_url = self.database_url.replace(
|
|
"postgresql://", "postgresql+asyncpg://", 1
|
|
)
|
|
return self
|
|
|
|
|
|
settings = Settings()
|