From 9320712ea9ac6d0ceb3dc39c28b702a66727b703 Mon Sep 17 00:00:00 2001 From: Rodrigo Agundez Date: Fri, 21 Nov 2025 13:23:11 +0700 Subject: [PATCH 1/3] Setting for defining the frontend and backend host --- src/app/core/config.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/app/core/config.py b/src/app/core/config.py index e67169a..10558f3 100644 --- a/src/app/core/config.py +++ b/src/app/core/config.py @@ -1,7 +1,7 @@ import os from enum import Enum -from pydantic import SecretStr, computed_field +from pydantic import SecretStr, computed_field, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -9,10 +9,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 +183,14 @@ class Settings( extra="ignore", ) + @field_validator("APP_FRONTEND_HOST") + @classmethod + def validate_app_frontend_host_protocol(cls, host: str) -> str: + if EnvironmentSettings.ENVIRONMENT == EnvironmentOption.PRODUCTION and not host.startswith("https://"): + raise ValueError( + f"In production, APP_FRONTEND_HOST must start with the https:// protocol. Received the host {host}." + ) + return host + settings = Settings() From db1e17296afb6adad0966a154441d7df54ab4414 Mon Sep 17 00:00:00 2001 From: Rodrigo Agundez Date: Fri, 21 Nov 2025 14:22:32 +0700 Subject: [PATCH 2/3] Add validation logic based on environment and validate hosts protocols and SSL in production --- scripts/local_with_uvicorn/.env.example | 2 ++ src/app/core/config.py | 36 ++++++++++++++++++------- 2 files changed, 28 insertions(+), 10 deletions(-) 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 10558f3..b83eef9 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, field_validator +from pydantic import SecretStr, computed_field, field_validator, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -20,7 +22,7 @@ class AppSettings(BaseSettings): 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}." + f"HOSTS must define their protocol and start with http:// or https://. Received the host '{host}'." ) return host @@ -183,14 +185,28 @@ class Settings( extra="ignore", ) - @field_validator("APP_FRONTEND_HOST") - @classmethod - def validate_app_frontend_host_protocol(cls, host: str) -> str: - if EnvironmentSettings.ENVIRONMENT == EnvironmentOption.PRODUCTION and not host.startswith("https://"): - raise ValueError( - f"In production, APP_FRONTEND_HOST must start with the https:// protocol. Received the host {host}." - ) - return host + @model_validator(mode="after") + def validate_environment_settings(self) -> Self: + 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() From b1fdc520ebd0eb0f06ee4d6f7acccd00920668ea Mon Sep 17 00:00:00 2001 From: Rodrigo Agundez Date: Fri, 21 Nov 2025 14:36:35 +0700 Subject: [PATCH 3/3] Add validation message to avoid obscure changes that the user will not expect --- src/app/core/config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/core/config.py b/src/app/core/config.py index b83eef9..ae280be 100644 --- a/src/app/core/config.py +++ b/src/app/core/config.py @@ -187,6 +187,8 @@ class Settings( @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: