db867dcbe5
Deploy to Staging / deploy (push) Has been cancelled
Conflict detector / main (push) Has been cancelled
Lint Backend / lint-backend (push) Has been cancelled
Playwright Tests / changes (push) Has been cancelled
Test Backend / test-backend (push) Has been cancelled
Test Docker Compose / test-docker-compose (push) Has been cancelled
Playwright Tests / test-playwright (1, 4) (push) Has been cancelled
Playwright Tests / test-playwright (2, 4) (push) Has been cancelled
Playwright Tests / test-playwright (3, 4) (push) Has been cancelled
Playwright Tests / test-playwright (4, 4) (push) Has been cancelled
Playwright Tests / merge-playwright-reports (push) Has been cancelled
Playwright Tests / alls-green-playwright (push) Has been cancelled
Issue Manager / issue-manager (push) Has been cancelled
120 lines
3.6 KiB
Python
120 lines
3.6 KiB
Python
import secrets
|
||
import warnings
|
||
from typing import Annotated, Any, Literal
|
||
|
||
from pydantic import (
|
||
AnyUrl,
|
||
BeforeValidator,
|
||
EmailStr,
|
||
HttpUrl,
|
||
PostgresDsn,
|
||
computed_field,
|
||
model_validator,
|
||
)
|
||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||
from typing_extensions import Self
|
||
|
||
|
||
def parse_cors(v: Any) -> list[str] | str:
|
||
if isinstance(v, str) and not v.startswith("["):
|
||
return [i.strip() for i in v.split(",") if i.strip()]
|
||
elif isinstance(v, list | str):
|
||
return v
|
||
raise ValueError(v)
|
||
|
||
|
||
class Settings(BaseSettings):
|
||
model_config = SettingsConfigDict(
|
||
# 使用顶层的 .env 文件(在 ./backend/ 上一级目录)
|
||
env_file="../.env",
|
||
env_ignore_empty=True,
|
||
extra="ignore",
|
||
)
|
||
API_V1_STR: str = "/api/v1"
|
||
SECRET_KEY: str = secrets.token_urlsafe(32)
|
||
# 60 分钟 * 24 小时 * 8 天 = 8 天
|
||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
|
||
FRONTEND_HOST: str = "http://localhost:5173"
|
||
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
|
||
|
||
BACKEND_CORS_ORIGINS: Annotated[
|
||
list[AnyUrl] | str, BeforeValidator(parse_cors)
|
||
] = []
|
||
|
||
@computed_field # type: ignore[prop-decorator]
|
||
@property
|
||
def all_cors_origins(self) -> list[str]:
|
||
return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [
|
||
self.FRONTEND_HOST
|
||
]
|
||
|
||
PROJECT_NAME: str
|
||
SENTRY_DSN: HttpUrl | None = None
|
||
POSTGRES_SERVER: str
|
||
POSTGRES_PORT: int = 5432
|
||
POSTGRES_USER: str
|
||
POSTGRES_PASSWORD: str = ""
|
||
POSTGRES_DB: str = ""
|
||
|
||
@computed_field # type: ignore[prop-decorator]
|
||
@property
|
||
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
|
||
return PostgresDsn.build(
|
||
scheme="postgresql+psycopg",
|
||
username=self.POSTGRES_USER,
|
||
password=self.POSTGRES_PASSWORD,
|
||
host=self.POSTGRES_SERVER,
|
||
port=self.POSTGRES_PORT,
|
||
path=self.POSTGRES_DB,
|
||
)
|
||
|
||
SMTP_TLS: bool = True
|
||
SMTP_SSL: bool = False
|
||
SMTP_PORT: int = 587
|
||
SMTP_HOST: str | None = None
|
||
SMTP_USER: str | None = None
|
||
SMTP_PASSWORD: str | None = None
|
||
EMAILS_FROM_EMAIL: EmailStr | None = None
|
||
EMAILS_FROM_NAME: str | None = None
|
||
|
||
@model_validator(mode="after")
|
||
def _set_default_emails_from(self) -> Self:
|
||
if not self.EMAILS_FROM_NAME:
|
||
self.EMAILS_FROM_NAME = self.PROJECT_NAME
|
||
return self
|
||
|
||
EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
|
||
|
||
@computed_field # type: ignore[prop-decorator]
|
||
@property
|
||
def emails_enabled(self) -> bool:
|
||
return bool(self.SMTP_HOST and self.EMAILS_FROM_EMAIL)
|
||
|
||
EMAIL_TEST_USER: EmailStr = "test@example.com"
|
||
FIRST_SUPERUSER: EmailStr
|
||
FIRST_SUPERUSER_PASSWORD: str
|
||
|
||
def _check_default_secret(self, var_name: str, value: str | None) -> None:
|
||
if value == "changethis":
|
||
message = (
|
||
f'{var_name} 的值为 "changethis",'
|
||
"出于安全考虑,请修改它,至少在部署时需要修改。"
|
||
)
|
||
if self.ENVIRONMENT == "local":
|
||
warnings.warn(message, stacklevel=1)
|
||
else:
|
||
raise ValueError(message)
|
||
|
||
@model_validator(mode="after")
|
||
def _enforce_non_default_secrets(self) -> Self:
|
||
self._check_default_secret("SECRET_KEY", self.SECRET_KEY)
|
||
self._check_default_secret("POSTGRES_PASSWORD", self.POSTGRES_PASSWORD)
|
||
self._check_default_secret(
|
||
"FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD
|
||
)
|
||
|
||
return self
|
||
|
||
|
||
settings = Settings() # type: ignore
|