"""Application configuration. All settings can be overridden with environment variables prefixed with ``PROCUREMENT_`` (e.g. ``PROCUREMENT_DATABASE_URL=...``) or a local ``.env`` file. The docker-compose files inject them from the git-ignored ``.env`` — real credentials never live in this repository. ``ENVIRONMENT`` is the safety switch: when it is ``"prod"``, the API runs fail-fast startup checks (see app.main._verify_production_settings) that refuse to boot with wildcard CORS, default credentials or dev login enabled. """ from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict( env_prefix="PROCUREMENT_", env_file=".env", env_file_encoding="utf-8", extra="ignore", ) # "dev" (local defaults allowed) | "prod" (fail-fast checks on startup). ENVIRONMENT: str = "dev" # PostgreSQL DSN — the schema (migration.sql) must already exist. DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/procurement" # Echo SQL statements to the console (dev aid). DEBUG_SQL: bool = False # MinIO object storage — invoice / proforma attachments (S3-compatible). # Endpoint is the compose service name (`minio:9000`) inside Docker. # Defaults are LOCAL DEV ONLY — production values come from .env and the # prod startup checks reject them. MINIO_ENDPOINT: str = "localhost:9000" MINIO_ACCESS_KEY: str = "minioadmin" MINIO_SECRET_KEY: str = "minioadmin" MINIO_BUCKET: str = "procurement-files" MINIO_SECURE: bool = False # Background auto-expiry of offered quotes whose expiry_date has passed. AUTO_EXPIRE_ENABLED: bool = True AUTO_EXPIRE_INTERVAL_SECONDS: int = 3600 # Pagination limits (contract: default 20, max 100). DEFAULT_PAGE_SIZE: int = 20 MAX_PAGE_SIZE: int = 100 # CORS — comma-separated exact browser origins (NOT "*" with # credentials; the prod checks reject wildcard). Example: # PROCUREMENT_CORS_ORIGINS=https://aramesh.bennu-api-bahrestan.ir CORS_ORIGINS: str = "http://localhost,http://localhost:3000,http://localhost:5173" # Developer Mode: in-page dev login panel (frontend build arg) AND the # X-User-Role / X-User-Name-B64 header identity fallback in the # inventory/pm routers. MUST stay false in production. DEV_LOGIN_ENABLED: bool = False # Casdoor (OIDC) — JWT verification against the issuer's JWKS. CASDOOR_ENDPOINT: str = "http://localhost:8080" CASDOOR_ISSUER: str = "" # expected token iss claim (default: CASDOOR_ENDPOINT) CASDOOR_CLIENT_ID: str = "inventory-app-client" CASDOOR_CLIENT_SECRET: str = "" # no default secret — set via env/.env CASDOOR_ORGANIZATION: str = "inventory" CASDOOR_APP_NAME: str = "inventory-app" CASDOOR_JWKS_URL: str = "" # default: {endpoint}/api/public_jwks CASDOOR_AUDIENCE: str = "inventory-app-client" # expected token aud (falls back to client_id) # Casdoor console admin — used by POST /users/sync to mirror Casdoor # identities into the app's users table. No default password. CASDOOR_ADMIN_USER: str = "admin" CASDOOR_ADMIN_PASSWORD: str = "" # Inventory dev fallback defaults (only used when DEV_LOGIN_ENABLED). INVENTORY_DEFAULT_ROLE: str = "pm" INVENTORY_DEFAULT_PROJECT_ID: str = "" # ---- Observability (Phase 2) ------------------------------------------- # "text" (human console) | "json" (JSON lines for Loki/Promtail). LOG_FORMAT: str = "text" # Sentry/GlitchTip DSN — backend exception capture (empty = disabled). SENTRY_DSN: str = "" SENTRY_TRACES_SAMPLE_RATE: float = 0.0 # OpenPanel server-side event ingestion (empty = disabled). OPENPANEL_API_URL: str = "" # e.g. http://openpanel-api:8080 (self-hosted) OPENPANEL_CLIENT_ID: str = "" OPENPANEL_CLIENT_SECRET: str = "" @property def cors_origin_list(self) -> list[str]: """Comma-separated CORS_ORIGINS → list for CORSMiddleware.""" return [o.strip() for o in self.CORS_ORIGINS.split(",") if o.strip()] @property def is_prod(self) -> bool: return self.ENVIRONMENT.lower() == "prod" settings = Settings()