Release command now runs migrations + seeds test users when SEED_ON_DEPLOY=true. Tree seeding runs as a background task on startup via HTTP API. Everything is idempotent and non-fatal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
108 lines
3.8 KiB
Python
108 lines
3.8 KiB
Python
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
|
||
|
||
# Email (Resend)
|
||
RESEND_API_KEY: Optional[str] = None
|
||
FROM_EMAIL: str = "ResolutionFlow <invites@resolutionflow.com>"
|
||
FEEDBACK_EMAIL: Optional[str] = None
|
||
|
||
@property
|
||
def email_enabled(self) -> bool:
|
||
"""Check if email sending is configured."""
|
||
return self.RESEND_API_KEY is not None
|
||
|
||
# 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
|
||
|
||
# Deployment – auto-seed test data on PR environments
|
||
SEED_ON_DEPLOY: bool = False
|
||
|
||
# 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()
|