|
1 | 1 | import os |
| 2 | +import warnings |
2 | 3 | from enum import Enum |
| 4 | +from typing import Self |
3 | 5 |
|
4 | | -from pydantic import SecretStr, computed_field |
| 6 | +from pydantic import SecretStr, computed_field, field_validator, model_validator |
5 | 7 | from pydantic_settings import BaseSettings, SettingsConfigDict |
6 | 8 |
|
7 | 9 |
|
8 | 10 | class AppSettings(BaseSettings): |
9 | 11 | APP_NAME: str = "FastAPI app" |
10 | 12 | APP_DESCRIPTION: str | None = None |
11 | 13 | APP_VERSION: str | None = None |
| 14 | + APP_BACKEND_HOST: str = "http://localhost:8000" |
| 15 | + APP_FRONTEND_HOST: str | None = None |
12 | 16 | LICENSE_NAME: str | None = None |
13 | 17 | CONTACT_NAME: str | None = None |
14 | 18 | CONTACT_EMAIL: str | None = None |
15 | 19 |
|
| 20 | + @field_validator("APP_BACKEND_HOST", "APP_FRONTEND_HOST", mode="after") |
| 21 | + @classmethod |
| 22 | + def validate_hosts(cls, host: str) -> str: |
| 23 | + if host is not None and not (host.startswith("http://") or host.startswith("https://")): |
| 24 | + raise ValueError( |
| 25 | + f"HOSTS must define their protocol and start with http:// or https://. Received the host '{host}'." |
| 26 | + ) |
| 27 | + return host |
| 28 | + |
16 | 29 |
|
17 | 30 | class CryptSettings(BaseSettings): |
18 | 31 | SECRET_KEY: SecretStr = SecretStr("secret-key") |
@@ -150,5 +163,30 @@ class Settings( |
150 | 163 | extra="ignore", |
151 | 164 | ) |
152 | 165 |
|
| 166 | + @model_validator(mode="after") |
| 167 | + def validate_environment_settings(self) -> Self: |
| 168 | + "The validation should not modify any of the settings. It should provide" |
| 169 | + "feedback to the user if any misconfiguration is detected." |
| 170 | + if self.ENVIRONMENT == EnvironmentOption.LOCAL: |
| 171 | + pass |
| 172 | + elif self.ENVIRONMENT == EnvironmentOption.STAGING: |
| 173 | + if "*" in self.CORS_ORIGINS: |
| 174 | + warnings.warn( |
| 175 | + "For security, in a staging environment CORS_ORIGINS should not include '*'. " |
| 176 | + "It's recommended to specify explicit origins (e.g., ['https://staging.example.com'])." |
| 177 | + ) |
| 178 | + elif self.ENVIRONMENT == EnvironmentOption.PRODUCTION: |
| 179 | + if "*" in self.CORS_ORIGINS: |
| 180 | + raise ValueError( |
| 181 | + "For security, in a production environment CORS_ORIGINS cannot include '*'. " |
| 182 | + "You must specify explicit allowed origins (e.g., ['https://example.com', 'https://www.example.com'])." |
| 183 | + ) |
| 184 | + if self.APP_FRONTEND_HOST and not self.APP_FRONTEND_HOST.startswith("https://"): |
| 185 | + raise ValueError( |
| 186 | + "In production, APP_FRONTEND_HOST must start with the https:// protocol. " |
| 187 | + f"Received the host '{self.APP_FRONTEND_HOST}'." |
| 188 | + ) |
| 189 | + return self |
| 190 | + |
153 | 191 |
|
154 | 192 | settings = Settings() |
0 commit comments