diff --git a/scripts/local_with_uvicorn/.env.example b/scripts/local_with_uvicorn/.env.example index c4bf803..6090ee2 100644 --- a/scripts/local_with_uvicorn/.env.example +++ b/scripts/local_with_uvicorn/.env.example @@ -16,6 +16,8 @@ APP_NAME="My Project" APP_DESCRIPTION="My Project Description" APP_VERSION="0.1" +APP_BACKEND_HOST="http://localhost:8000" +APP_FRONTEND_HOST="http://localhost:3000" CONTACT_NAME="Me" CONTACT_EMAIL="my.email@example.com" LICENSE_NAME="MIT" diff --git a/src/app/core/config.py b/src/app/core/config.py index e67169a..ae280be 100644 --- a/src/app/core/config.py +++ b/src/app/core/config.py @@ -1,7 +1,9 @@ import os +import warnings from enum import Enum +from typing import Self -from pydantic import SecretStr, computed_field +from pydantic import SecretStr, computed_field, field_validator, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -9,10 +11,21 @@ class AppSettings(BaseSettings): APP_NAME: str = "FastAPI app" APP_DESCRIPTION: str | None = None APP_VERSION: str | None = None + APP_BACKEND_HOST: str = "http://localhost:8000" + APP_FRONTEND_HOST: str | None = None LICENSE_NAME: str | None = None CONTACT_NAME: str | None = None CONTACT_EMAIL: str | None = None + @field_validator("APP_BACKEND_HOST", "APP_FRONTEND_HOST", mode="after") + @classmethod + def validate_hosts(cls, host: str) -> str: + if host is not None and not (host.startswith("http://") or host.startswith("https://")): + raise ValueError( + f"HOSTS must define their protocol and start with http:// or https://. Received the host '{host}'." + ) + return host + class CryptSettings(BaseSettings): SECRET_KEY: SecretStr = SecretStr("secret-key") @@ -172,5 +185,30 @@ class Settings( extra="ignore", ) + @model_validator(mode="after") + def validate_environment_settings(self) -> Self: + "The validation should not modify any of the settings. It should provide" + "feedback to the user if any misconfiguration is detected." + if self.ENVIRONMENT == EnvironmentOption.LOCAL: + pass + elif self.ENVIRONMENT == EnvironmentOption.STAGING: + if "*" in self.CORS_ORIGINS: + warnings.warn( + "For security, in a staging environment CORS_ORIGINS should not include '*'. " + "It's recommended to specify explicit origins (e.g., ['https://staging.example.com'])." + ) + elif self.ENVIRONMENT == EnvironmentOption.PRODUCTION: + if "*" in self.CORS_ORIGINS: + raise ValueError( + "For security, in a production environment CORS_ORIGINS cannot include '*'. " + "You must specify explicit allowed origins (e.g., ['https://example.com', 'https://www.example.com'])." + ) + if self.APP_FRONTEND_HOST and not self.APP_FRONTEND_HOST.startswith("https://"): + raise ValueError( + "In production, APP_FRONTEND_HOST must start with the https:// protocol. " + f"Received the host '{self.APP_FRONTEND_HOST}'." + ) + return self + settings = Settings()