Independent FastAPI backend for Maskan CRM. Owns contacts, organizations, leads, pipelines, activities, products, quotes, users, permissions, audit records and first-party integration credentials, with Alembic migrations against PostgreSQL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
108 lines
3.6 KiB
Python
108 lines
3.6 KiB
Python
from functools import lru_cache
|
|
from urllib.parse import quote_plus
|
|
|
|
from pydantic import Field
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(
|
|
env_file=".env",
|
|
env_file_encoding="utf-8",
|
|
extra="ignore",
|
|
)
|
|
|
|
app_name: str = "Maskan CRM"
|
|
environment: str = Field(default="development", alias="MASKAN_CRM_ENV")
|
|
mode: str = Field(default="standalone", alias="MASKAN_CRM_MODE")
|
|
cors_origins_raw: str = Field(
|
|
default="http://127.0.0.1:5174,http://localhost:5174",
|
|
alias="MASKAN_CRM_CORS_ORIGINS",
|
|
)
|
|
jwt_secret: str = Field(
|
|
default="development-only-change-me-at-least-32-characters",
|
|
alias="MASKAN_CRM_JWT_SECRET",
|
|
)
|
|
access_token_minutes: int = Field(
|
|
default=30,
|
|
alias="MASKAN_CRM_ACCESS_TOKEN_MINUTES",
|
|
)
|
|
jwt_issuer: str = "maskan-crm"
|
|
jwt_audience: str = "maskan-crm-api"
|
|
|
|
database_url_override: str | None = Field(default=None, alias="DATABASE_URL")
|
|
db_host: str = Field(default="127.0.0.1", alias="DB_HOST")
|
|
db_port: int = Field(default=5433, alias="DB_PORT")
|
|
db_name: str = Field(default="maskan_crm", alias="DB_NAME")
|
|
db_user: str = Field(default="postgres", alias="DB_USER")
|
|
db_password: str = Field(default="postgres", alias="DB_PASSWORD")
|
|
db_sslmode: str = Field(default="disable", alias="DB_SSLMODE")
|
|
|
|
bootstrap_workspace: str = Field(
|
|
default="maskan",
|
|
alias="MASKAN_CRM_BOOTSTRAP_WORKSPACE",
|
|
)
|
|
bootstrap_company: str = Field(
|
|
default="Maskan Technologies",
|
|
alias="MASKAN_CRM_BOOTSTRAP_COMPANY",
|
|
)
|
|
bootstrap_admin_email: str = Field(
|
|
default="owner@example.com",
|
|
alias="MASKAN_CRM_BOOTSTRAP_ADMIN_EMAIL",
|
|
)
|
|
bootstrap_admin_password: str = Field(
|
|
default="change-this-before-first-run",
|
|
alias="MASKAN_CRM_BOOTSTRAP_ADMIN_PASSWORD",
|
|
)
|
|
seed_demo: bool = Field(default=True, alias="MASKAN_CRM_SEED_DEMO")
|
|
|
|
@property
|
|
def cors_origins(self) -> list[str]:
|
|
return [item.strip() for item in self.cors_origins_raw.split(",") if item.strip()]
|
|
|
|
@property
|
|
def database_url(self) -> str:
|
|
if self.database_url_override:
|
|
return self.database_url_override
|
|
|
|
user = quote_plus(self.db_user)
|
|
password = quote_plus(self.db_password)
|
|
return (
|
|
f"postgresql+psycopg://{user}:{password}@{self.db_host}:"
|
|
f"{self.db_port}/{self.db_name}?sslmode={self.db_sslmode}"
|
|
)
|
|
|
|
def validate_production(self) -> None:
|
|
if self.environment.lower() != "production":
|
|
self.validate_database_backend()
|
|
return
|
|
|
|
if self.jwt_secret in {
|
|
"development-only-change-me-at-least-32-characters",
|
|
"replace-me",
|
|
}:
|
|
raise RuntimeError("MASKAN_CRM_JWT_SECRET must be set for production.")
|
|
|
|
if len(self.jwt_secret) < 32:
|
|
raise RuntimeError("MASKAN_CRM_JWT_SECRET must contain at least 32 characters.")
|
|
|
|
self.validate_database_backend()
|
|
|
|
def validate_database_backend(self) -> None:
|
|
"""Allow SQLite only in explicit automated-test environments."""
|
|
if not self.database_url.lower().startswith("sqlite"):
|
|
return
|
|
|
|
if self.environment.lower() not in {"test", "testing"}:
|
|
raise RuntimeError(
|
|
"Maskan CRM requires PostgreSQL outside automated tests. "
|
|
"Set DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASSWORD or DATABASE_URL.",
|
|
)
|
|
|
|
|
|
@lru_cache
|
|
def get_settings() -> Settings:
|
|
settings = Settings()
|
|
settings.validate_production()
|
|
return settings
|