230 lines
8 KiB
Python
230 lines
8 KiB
Python
from dataclasses import dataclass
|
|
import os
|
|
|
|
DEFAULT_SECURITY_PERMISSIONS_POLICY = (
|
|
"accelerometer=(), autoplay=(), camera=(), display-capture=(), "
|
|
"geolocation=(), gyroscope=(), microphone=(), payment=(), usb=()"
|
|
)
|
|
DEFAULT_SECURITY_CSP_POLICY = (
|
|
"default-src 'self'; "
|
|
"base-uri 'self'; "
|
|
"form-action 'self'; "
|
|
"frame-ancestors 'none'; "
|
|
"img-src 'self' data: https:; "
|
|
"script-src 'self'; "
|
|
"style-src 'self' 'unsafe-inline'; "
|
|
"font-src 'self' data:; "
|
|
"connect-src 'self'; "
|
|
"object-src 'none'"
|
|
)
|
|
DEFAULT_SECURITY_CSP_DOCS_POLICY = (
|
|
"default-src 'self'; "
|
|
"img-src 'self' data: https:; "
|
|
"script-src 'self' 'unsafe-inline'; "
|
|
"style-src 'self' 'unsafe-inline'; "
|
|
"font-src 'self' data:; "
|
|
"connect-src 'self' https:; "
|
|
"object-src 'none'; "
|
|
"frame-ancestors 'none'"
|
|
)
|
|
|
|
|
|
def _env_bool(name: str, default: bool) -> bool:
|
|
value = os.getenv(name)
|
|
if value is None:
|
|
return default
|
|
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def _env_int(name: str, default: int) -> int:
|
|
value = os.getenv(name)
|
|
if value is None:
|
|
return default
|
|
return int(value)
|
|
|
|
|
|
def _env_float(name: str, default: float) -> float:
|
|
value = os.getenv(name)
|
|
if value is None:
|
|
return default
|
|
return float(value)
|
|
|
|
|
|
def _env_str(name: str, default: str) -> str:
|
|
value = os.getenv(name)
|
|
if value is None:
|
|
return default
|
|
return value.strip() or default
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Settings:
|
|
app_name: str = os.getenv("APP_NAME", "scholarr")
|
|
database_url: str = os.getenv(
|
|
"DATABASE_URL",
|
|
"postgresql+asyncpg://scholar:scholar@db:5432/scholar",
|
|
)
|
|
session_secret_key: str = os.getenv("SESSION_SECRET_KEY", "dev-insecure-session-key")
|
|
session_cookie_secure: bool = _env_bool("SESSION_COOKIE_SECURE", False)
|
|
security_headers_enabled: bool = _env_bool("SECURITY_HEADERS_ENABLED", True)
|
|
security_x_content_type_options: str = _env_str("SECURITY_X_CONTENT_TYPE_OPTIONS", "nosniff")
|
|
security_x_frame_options: str = _env_str("SECURITY_X_FRAME_OPTIONS", "DENY")
|
|
security_referrer_policy: str = _env_str("SECURITY_REFERRER_POLICY", "strict-origin-when-cross-origin")
|
|
security_permissions_policy: str = _env_str(
|
|
"SECURITY_PERMISSIONS_POLICY",
|
|
DEFAULT_SECURITY_PERMISSIONS_POLICY,
|
|
)
|
|
security_cross_origin_opener_policy: str = _env_str(
|
|
"SECURITY_CROSS_ORIGIN_OPENER_POLICY",
|
|
"same-origin",
|
|
)
|
|
security_cross_origin_resource_policy: str = _env_str(
|
|
"SECURITY_CROSS_ORIGIN_RESOURCE_POLICY",
|
|
"same-origin",
|
|
)
|
|
security_csp_enabled: bool = _env_bool("SECURITY_CSP_ENABLED", True)
|
|
security_csp_policy: str = _env_str("SECURITY_CSP_POLICY", DEFAULT_SECURITY_CSP_POLICY)
|
|
security_csp_docs_policy: str = _env_str("SECURITY_CSP_DOCS_POLICY", DEFAULT_SECURITY_CSP_DOCS_POLICY)
|
|
security_csp_report_only: bool = _env_bool("SECURITY_CSP_REPORT_ONLY", False)
|
|
security_strict_transport_security_enabled: bool = _env_bool(
|
|
"SECURITY_STRICT_TRANSPORT_SECURITY_ENABLED",
|
|
False,
|
|
)
|
|
security_strict_transport_security_max_age: int = _env_int(
|
|
"SECURITY_STRICT_TRANSPORT_SECURITY_MAX_AGE",
|
|
31_536_000,
|
|
)
|
|
security_strict_transport_security_include_subdomains: bool = _env_bool(
|
|
"SECURITY_STRICT_TRANSPORT_SECURITY_INCLUDE_SUBDOMAINS",
|
|
True,
|
|
)
|
|
security_strict_transport_security_preload: bool = _env_bool(
|
|
"SECURITY_STRICT_TRANSPORT_SECURITY_PRELOAD",
|
|
False,
|
|
)
|
|
login_rate_limit_attempts: int = _env_int("LOGIN_RATE_LIMIT_ATTEMPTS", 5)
|
|
login_rate_limit_window_seconds: int = _env_int(
|
|
"LOGIN_RATE_LIMIT_WINDOW_SECONDS",
|
|
60,
|
|
)
|
|
log_level: str = _env_str("LOG_LEVEL", "INFO")
|
|
log_format: str = _env_str("LOG_FORMAT", "console")
|
|
log_requests: bool = _env_bool("LOG_REQUESTS", True)
|
|
log_uvicorn_access: bool = _env_bool("LOG_UVICORN_ACCESS", False)
|
|
log_request_skip_paths: str = _env_str("LOG_REQUEST_SKIP_PATHS", "/healthz")
|
|
log_redact_fields: str = os.getenv("LOG_REDACT_FIELDS", "")
|
|
scheduler_enabled: bool = _env_bool("SCHEDULER_ENABLED", True)
|
|
scheduler_tick_seconds: int = _env_int("SCHEDULER_TICK_SECONDS", 60)
|
|
ingestion_automation_allowed: bool = _env_bool(
|
|
"INGESTION_AUTOMATION_ALLOWED",
|
|
True,
|
|
)
|
|
ingestion_manual_run_allowed: bool = _env_bool(
|
|
"INGESTION_MANUAL_RUN_ALLOWED",
|
|
True,
|
|
)
|
|
ingestion_min_run_interval_minutes: int = _env_int(
|
|
"INGESTION_MIN_RUN_INTERVAL_MINUTES",
|
|
15,
|
|
)
|
|
ingestion_min_request_delay_seconds: int = _env_int(
|
|
"INGESTION_MIN_REQUEST_DELAY_SECONDS",
|
|
2,
|
|
)
|
|
ingestion_network_error_retries: int = _env_int("INGESTION_NETWORK_ERROR_RETRIES", 1)
|
|
ingestion_retry_backoff_seconds: float = _env_float(
|
|
"INGESTION_RETRY_BACKOFF_SECONDS",
|
|
1.0,
|
|
)
|
|
ingestion_max_pages_per_scholar: int = _env_int(
|
|
"INGESTION_MAX_PAGES_PER_SCHOLAR",
|
|
30,
|
|
)
|
|
ingestion_page_size: int = _env_int("INGESTION_PAGE_SIZE", 100)
|
|
ingestion_alert_blocked_failure_threshold: int = _env_int(
|
|
"INGESTION_ALERT_BLOCKED_FAILURE_THRESHOLD",
|
|
1,
|
|
)
|
|
ingestion_alert_network_failure_threshold: int = _env_int(
|
|
"INGESTION_ALERT_NETWORK_FAILURE_THRESHOLD",
|
|
2,
|
|
)
|
|
ingestion_alert_retry_scheduled_threshold: int = _env_int(
|
|
"INGESTION_ALERT_RETRY_SCHEDULED_THRESHOLD",
|
|
3,
|
|
)
|
|
ingestion_safety_cooldown_blocked_seconds: int = _env_int(
|
|
"INGESTION_SAFETY_COOLDOWN_BLOCKED_SECONDS",
|
|
1800,
|
|
)
|
|
ingestion_safety_cooldown_network_seconds: int = _env_int(
|
|
"INGESTION_SAFETY_COOLDOWN_NETWORK_SECONDS",
|
|
900,
|
|
)
|
|
ingestion_continuation_queue_enabled: bool = _env_bool(
|
|
"INGESTION_CONTINUATION_QUEUE_ENABLED",
|
|
True,
|
|
)
|
|
ingestion_continuation_base_delay_seconds: int = _env_int(
|
|
"INGESTION_CONTINUATION_BASE_DELAY_SECONDS",
|
|
120,
|
|
)
|
|
ingestion_continuation_max_delay_seconds: int = _env_int(
|
|
"INGESTION_CONTINUATION_MAX_DELAY_SECONDS",
|
|
3600,
|
|
)
|
|
ingestion_continuation_max_attempts: int = _env_int(
|
|
"INGESTION_CONTINUATION_MAX_ATTEMPTS",
|
|
6,
|
|
)
|
|
scheduler_queue_batch_size: int = _env_int("SCHEDULER_QUEUE_BATCH_SIZE", 10)
|
|
frontend_enabled: bool = _env_bool("FRONTEND_ENABLED", True)
|
|
frontend_dist_dir: str = _env_str("FRONTEND_DIST_DIR", "/app/frontend/dist")
|
|
scholar_image_upload_dir: str = _env_str(
|
|
"SCHOLAR_IMAGE_UPLOAD_DIR",
|
|
"/tmp/scholarr_uploads/scholar_images",
|
|
)
|
|
scholar_image_upload_max_bytes: int = _env_int(
|
|
"SCHOLAR_IMAGE_UPLOAD_MAX_BYTES",
|
|
2_000_000,
|
|
)
|
|
scholar_name_search_enabled: bool = _env_bool("SCHOLAR_NAME_SEARCH_ENABLED", True)
|
|
scholar_name_search_cache_ttl_seconds: int = _env_int(
|
|
"SCHOLAR_NAME_SEARCH_CACHE_TTL_SECONDS",
|
|
21_600,
|
|
)
|
|
scholar_name_search_blocked_cache_ttl_seconds: int = _env_int(
|
|
"SCHOLAR_NAME_SEARCH_BLOCKED_CACHE_TTL_SECONDS",
|
|
300,
|
|
)
|
|
scholar_name_search_cache_max_entries: int = _env_int(
|
|
"SCHOLAR_NAME_SEARCH_CACHE_MAX_ENTRIES",
|
|
512,
|
|
)
|
|
scholar_name_search_min_interval_seconds: float = _env_float(
|
|
"SCHOLAR_NAME_SEARCH_MIN_INTERVAL_SECONDS",
|
|
8.0,
|
|
)
|
|
scholar_name_search_interval_jitter_seconds: float = _env_float(
|
|
"SCHOLAR_NAME_SEARCH_INTERVAL_JITTER_SECONDS",
|
|
2.0,
|
|
)
|
|
scholar_name_search_cooldown_block_threshold: int = _env_int(
|
|
"SCHOLAR_NAME_SEARCH_COOLDOWN_BLOCK_THRESHOLD",
|
|
1,
|
|
)
|
|
scholar_name_search_cooldown_seconds: int = _env_int(
|
|
"SCHOLAR_NAME_SEARCH_COOLDOWN_SECONDS",
|
|
1800,
|
|
)
|
|
scholar_name_search_alert_retry_count_threshold: int = _env_int(
|
|
"SCHOLAR_NAME_SEARCH_ALERT_RETRY_COUNT_THRESHOLD",
|
|
2,
|
|
)
|
|
scholar_name_search_alert_cooldown_rejections_threshold: int = _env_int(
|
|
"SCHOLAR_NAME_SEARCH_ALERT_COOLDOWN_REJECTIONS_THRESHOLD",
|
|
3,
|
|
)
|
|
|
|
|
|
settings = Settings()
|