Files

75 lines
3.9 KiB
Python

from pydantic import Field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
deployment_environment: str = "development"
app_version: str = "0.1.0"
app_revision: str = "dev"
database_url: str = "postgresql+psycopg://rf4:rf4_local@localhost:5432/rf4_spotter"
admin_token: str = "change-me-in-production"
s3_endpoint_url: str = "http://localhost:9000"
s3_public_endpoint_url: str = "http://localhost:9000"
s3_access_key: str = "rf4-local"
s3_secret_key: str = "rf4-local-secret"
s3_bucket: str = "catch-screenshots"
screenshot_max_bytes: int = 8 * 1024 * 1024
official_records_url: str = "https://rf4game.de/records/region/RU/"
official_records_region: str = "RU"
official_records_category: str = "records"
official_import_required: bool = False
seed_demo_data: bool = True
retention_submission_days: int = Field(default=1, ge=1)
retention_unreviewed_days: int = Field(default=30, ge=7)
retention_approved_personal_days: int = Field(default=180, ge=30)
retention_staging_days: int = Field(default=90, ge=30)
retention_audit_days: int = Field(default=365, ge=90)
retention_published_payload_days: int = Field(default=365, ge=90)
import_interval_seconds: int = Field(default=3600, ge=3600)
community_import_interval_seconds: int = Field(default=1800, ge=1800)
public_cache_seconds: int = Field(default=20, ge=1, le=300)
rf4map_point_url: str = "https://rf4map.ru/points/275"
rf4posts_spot_url: str = "https://rf4-posts.com/ru/spots/d0c6d9c6-4ebf-49a7-98a8-9a562553a8ee"
rate_limit_secret: str = "change-rate-limit-secret"
admin_auth_attempt_limit: int = Field(default=10, ge=3, le=100)
admin_auth_window_seconds: int = Field(default=600, ge=60, le=3600)
log_level: str = "INFO"
cors_origins: list[str] = Field(default_factory=lambda: ["http://localhost:4321", "http://127.0.0.1:4321"])
trusted_proxy_cidrs: list[str] = Field(default_factory=lambda: ["127.0.0.1/32", "::1/128"])
@model_validator(mode="before")
@classmethod
def parse_comma_separated_lists(cls, data: dict) -> dict:
"""Parse comma-separated string values into lists."""
if isinstance(data, dict):
for field_name in ["cors_origins", "trusted_proxy_cidrs"]:
value = data.get(field_name)
if isinstance(value, str) and value:
data[field_name] = [item.strip() for item in value.split(",") if item.strip()]
return data
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
@model_validator(mode="after")
def reject_insecure_production_defaults(self) -> "Settings":
if self.deployment_environment != "production":
return self
insecure = {
"ADMIN_TOKEN": self.admin_token == "change-me-in-production" or len(self.admin_token) < 32,
"RATE_LIMIT_SECRET": self.rate_limit_secret == "change-rate-limit-secret" or len(self.rate_limit_secret) < 32,
"S3_ACCESS_KEY": self.s3_access_key == "rf4-local" or len(self.s3_access_key) < 12,
"S3_SECRET_KEY": self.s3_secret_key == "rf4-local-secret" or len(self.s3_secret_key) < 32,
}
invalid = [name for name, failed in insecure.items() if failed]
if invalid:
raise ValueError(f"insecure production settings: {', '.join(invalid)}")
if not self.cors_origins or any(not origin.startswith("https://") for origin in self.cors_origins):
raise ValueError("production CORS_ORIGINS must contain only HTTPS origins")
if not self.s3_public_endpoint_url.startswith("https://"):
raise ValueError("production S3_PUBLIC_ENDPOINT_URL must use HTTPS")
if self.seed_demo_data:
raise ValueError("SEED_DEMO_DATA must be false in production")
return self
settings = Settings()