from pydantic_settings import BaseSettings from pydantic import field_validator from typing import Optional _DEFAULT_SECRET_KEY = "your-secret-key-change-in-production-use-openssl-rand-hex-32" class Settings(BaseSettings): # Application APP_NAME: str = "ResolutionFlow" DEBUG: bool = False API_V1_PREFIX: str = "/api/v1" # Database - Railway provides DATABASE_URL, we convert it for asyncpg DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/patherly" @field_validator("DATABASE_URL", mode="before") @classmethod def convert_database_url(cls, v: str) -> str: """Convert standard postgres URL to asyncpg format.""" if v.startswith("postgresql://"): return v.replace("postgresql://", "postgresql+asyncpg://", 1) return v @property def DATABASE_URL_SYNC(self) -> str: """Get sync URL by removing asyncpg prefix from DATABASE_URL.""" return self.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql://", 1) # JWT Settings SECRET_KEY: str = _DEFAULT_SECRET_KEY @field_validator("SECRET_KEY", mode="after") @classmethod def reject_default_secret_in_production(cls, v: str, info) -> str: """Fail loudly if the default secret key is used outside of DEBUG mode.""" debug = info.data.get("DEBUG", False) if v == _DEFAULT_SECRET_KEY and not debug: raise ValueError( "SECRET_KEY must be set to a secure value in production. " "Generate one with: openssl rand -hex 32" ) return v ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 5 REFRESH_TOKEN_EXPIRE_DAYS: int = 7 # Security BCRYPT_ROUNDS: int = 12 # Registration REQUIRE_INVITE_CODE: bool = True # Set to False to allow open registration # Stripe STRIPE_SECRET_KEY: Optional[str] = None STRIPE_PUBLISHABLE_KEY: Optional[str] = None STRIPE_WEBHOOK_SECRET: Optional[str] = None @property def stripe_enabled(self) -> bool: """Check if Stripe is configured.""" return self.STRIPE_SECRET_KEY is not None and self.STRIPE_WEBHOOK_SECRET is not None # CORS - set FRONTEND_URL in production (e.g., https://patherly.up.railway.app) CORS_ORIGINS: list[str] = ["http://localhost:3000", "http://localhost:5173", "http://localhost:5174"] FRONTEND_URL: Optional[str] = None # Allow all Railway PR environments (set to True in Railway env vars) ALLOW_RAILWAY_ORIGINS: bool = False @property def allowed_origins(self) -> list[str]: """Get all allowed CORS origins including FRONTEND_URL if set.""" origins = self.CORS_ORIGINS.copy() if self.FRONTEND_URL and self.FRONTEND_URL not in origins: origins.append(self.FRONTEND_URL) return origins def is_origin_allowed(self, origin: str) -> bool: """Check if an origin is allowed, including Railway wildcard pattern.""" if origin in self.allowed_origins: return True # Allow any *.up.railway.app origin for PR environments if self.ALLOW_RAILWAY_ORIGINS and origin.endswith(".up.railway.app"): return True return False class Config: env_file = ".env" case_sensitive = True extra = "ignore" # Ignore extra env vars like DATABASE_URL_SYNC settings = Settings()