diff --git a/.dockerignore b/.dockerignore index ae5ac42..4db934b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,3 +4,4 @@ notes.md .git .venv .pre-commit-config.yaml +.env diff --git a/.venv.dev.example b/development.env similarity index 67% rename from .venv.dev.example rename to development.env index 3114ecf..fbe112f 100644 --- a/.venv.dev.example +++ b/development.env @@ -1,8 +1,8 @@ -TAKAHE_DATABASE_URL="postgres://postgres:insecure_password@db/takahe" +TAKAHE_DATABASE_SERVER="sqlite://takahe.db" TAKAHE_DEBUG=true TAKAHE_SECRET_KEY="insecure_secret" TAKAHE_CSRF_TRUSTED_ORIGINS=["http://127.0.0.1:8000", "https://127.0.0.1:8000"] TAKAHE_USE_PROXY_HEADERS=true -TAKAHE_EMAIL_BACKEND="console://console" +TAKAHE_EMAIL_SERVER="console://console" TAKAHE_MAIN_DOMAIN="example.com" TAKAHE_ENVIRONMENT="development" diff --git a/docker/Dockerfile b/docker/Dockerfile index b84a391..9fd54c4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,7 +11,7 @@ COPY . /takahe WORKDIR /takahe -RUN TAKAHE_DATABASE_URL="postgres://dummy:dummy@localhost/postgres" python3 manage.py collectstatic +RUN TAKAHE_DATABASE_SERVER="postgres://x@example.com/x" python3 manage.py collectstatic EXPOSE 8000 diff --git a/docs/installation.rst b/docs/installation.rst index 9263e43..c332d9c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -61,36 +61,33 @@ Environment Variables All of these variables are *required* for a working installation, and should be provided from the first boot. -* ``PGHOST``, ``PGPORT``, ``PGUSER``, ``PGDATABASE``, and ``PGPASSWORD`` are the - standard PostgreSQL environment variables for configuring your database. +* ``TAKAHE_DATABASE_SERVER`` should be a database DSN for your database (you can use + the standard ``PG*`` variables too if you want) * ``TAKAHE_SECRET_KEY`` must be a fixed, random value (it's used for internal cryptography). Don't change this unless you want to invalidate all sessions. -* ``TAKAHE_MEDIA_BACKEND`` must be one of ``local``, ``s3`` or ``gcs``. +* ``TAKAHE_MEDIA_BACKEND`` must be a URI starting with ``local://``, ``s3://`` or ``gcs://``. - * If it is set to ``local``, you must also provide ``TAKAHE_MEDIA_ROOT``, + * If it is set to ``local://``, you must also provide ``TAKAHE_MEDIA_ROOT``, the path to the local media directory, and ``TAKAHE_MEDIA_URL``, a fully-qualified URL prefix that serves that directory. - * If it is set to ``gcs``, you must also provide ``TAKAHE_MEDIA_BUCKET``, - the name of the bucket to store files in. The bucket must be publicly - readable and have "uniform access control" enabled. + * If it is set to ``gcs://``, it must be in the form ``gcs://bucket-name`` + (note the two slashes if you just want a bucket name) - * If it is set to ``s3``, you must also provide ``TAKAHE_MEDIA_BUCKET``, - the name of the bucket to store files in. + * If it is set to ``s3://``, it must be in the form ``s3://access-key:secret-key@endpoint-url/bucket-name`` * ``TAKAHE_MAIN_DOMAIN`` should be the domain name (without ``https://``) that will be used for default links (such as in emails). It does *not* need to be the same as any domain you are hosting user accounts on. -* ``TAKAHE_EMAIL_HOST`` and ``TAKAHE_EMAIL_PORT`` (along with - ``TAKAHE_EMAIL_USER`` and ``TAKAHE_EMAIL_PASSWORD``, if needed) should point - to an SMTP server Takahe can use for sending email. Email is *required*, to - allow account creation and password resets. +* ``TAKAHE_EMAIL_SERVER`` should be set to an ``smtp://`` or ``sendgrid://`` URI - * If you are using SendGrid, you can just set an API key in - ``TAKAHE_EMAIL_SENDGRID_KEY`` instead. + * If you are using SMTP, it is ``smtp://username:password@host:port/``. You + can also put ``?tls=true`` or ``?ssl=true`` on the end to enable encryption. + + * If you are using SendGrid, you should set the URI to ``sendgrid://api-key`` * ``TAKAHE_EMAIL_FROM`` is the email address that emails from the system will appear to come from. @@ -99,12 +96,13 @@ be provided from the first boot. be automatically promoted to administrator when it signs up. You only need this for initial setup, and can unset it after that if you like. -* ``TAKAHE_STATOR_TOKEN`` should be a random string that you are using to - protect the stator (task runner) endpoint. You'll use this value later. +* If you don't want to run Stator as a background process but as a view, + set ``TAKAHE_STATOR_TOKEN`` to a random string that you are using to + protect it; you'll use this when setting up the URL to be called. * If your installation is behind a HTTPS endpoint that is proxying it, set - ``TAKAHE_SECURE_HEADER`` to the header name used to signify that HTTPS is - being used (usually ``X-Forwarded-Proto``) + ``TAKAHE_USE_PROXY_HEADERS`` to ``true``. (The HTTPS proxy header must be called + ``X-Forwarded-Proto``). * If you want to receive emails about internal site errors, set ``TAKAHE_ERROR_EMAILS`` to a comma-separated list of email addresses that diff --git a/takahe/settings.py b/takahe/settings.py index 76c8f8b..bc55711 100644 --- a/takahe/settings.py +++ b/takahe/settings.py @@ -31,25 +31,32 @@ class Settings(BaseSettings): """ #: The default database. - DATABASE_URL: Optional[PostgresDsn] + DATABASE_SERVER: Optional[PostgresDsn] + #: The currently running environment, used for things such as sentry #: error reporting. ENVIRONMENT: Environments = "development" + #: Should django run in debug mode? DEBUG: bool = False + #: Set a secret key used for signing values such as sessions. Randomized #: by default, so you'll logout everytime the process restarts. SECRET_KEY: str = Field(default_factory=lambda: secrets.token_hex(128)) + #: Set a secret key used to protect the stator. Randomized by default. STATOR_TOKEN: str = Field(default_factory=lambda: secrets.token_hex(128)) #: If set, a list of allowed values for the HOST header. The default value #: of '*' means any host will be accepted. ALLOWED_HOSTS: List[str] = Field(default_factory=lambda: ["*"]) + #: If set, a list of hosts to accept for CORS. CORS_HOSTS: List[str] = Field(default_factory=list) + #: If set, a list of hosts to accept for CSRF. CSRF_HOSTS: List[str] = Field(default_factory=list) + #: If enabled, trust the HTTP_X_FORWARDED_FOR header. USE_PROXY_HEADERS: bool = False @@ -59,25 +66,25 @@ class Settings(BaseSettings): #: Fallback domain for links. MAIN_DOMAIN: str = "example.com" - EMAIL_DSN: AnyUrl = "console://localhost" + EMAIL_SERVER: AnyUrl = "console://localhost" EMAIL_FROM: EmailStr = "test@example.com" AUTO_ADMIN_EMAIL: Optional[EmailStr] = None ERROR_EMAILS: Optional[List[EmailStr]] = None MEDIA_URL: str = "/media/" - MEDIA_ROOT: str = str(BASE_DIR / "MEDIA") + MEDIA_ROOT: str = str(BASE_DIR / "media") MEDIA_BACKEND: Optional[AnyUrl] = None PGHOST: Optional[str] = None - PGPORT: int = 5432 + PGPORT: Optional[int] = 5432 PGNAME: str = "takahe" PGUSER: str = "postgres" PGPASSWORD: Optional[str] = None @validator("PGHOST", always=True) def validate_db(cls, PGHOST, values): # noqa - if not values.get("DATABASE_URL") and not PGHOST: - raise ValueError("Either DATABASE_URL or PGHOST are required.") + if not values.get("DATABASE_SERVER") and not PGHOST: + raise ValueError("Either DATABASE_SERVER or PGHOST are required.") return PGHOST class Config: @@ -154,8 +161,10 @@ TEMPLATES = [ WSGI_APPLICATION = "takahe.wsgi.application" -if SETUP.DATABASE_URL: - DATABASES = {"default": dj_database_url.parse(SETUP.DATABASE_URL, conn_max_age=600)} +if SETUP.DATABASE_SERVER: + DATABASES = { + "default": dj_database_url.parse(SETUP.DATABASE_SERVER, conn_max_age=600) + } else: DATABASES = { "default": { @@ -243,11 +252,17 @@ if SETUP.SENTRY_DSN: ) SERVER_EMAIL = SETUP.EMAIL_FROM -if SETUP.EMAIL_DSN: - parsed = urllib.parse.urlparse(SETUP.EMAIL_DSN) +if SETUP.EMAIL_SERVER: + parsed = urllib.parse.urlparse(SETUP.EMAIL_SERVER) query = urllib.parse.parse_qs(parsed.query) if parsed.scheme == "console": EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + elif parsed.scheme == "sendgrid": + EMAIL_HOST = "smtp.sendgrid.net" + EMAIL_PORT = 587 + EMAIL_HOST_USER = "apikey" + EMAIL_HOST_PASSWORD = parsed.hostname + EMAIL_USE_TLS = True elif parsed.scheme == "smtp": EMAIL_HOST = parsed.hostname EMAIL_PORT = parsed.port @@ -256,7 +271,7 @@ if SETUP.EMAIL_DSN: EMAIL_USE_TLS = as_bool(query.get("tls")) EMAIL_USE_SSL = as_bool(query.get("ssl")) else: - raise ValueError("Unknown schema for EMAIL_DSN.") + raise ValueError("Unknown schema for EMAIL_SERVER.") if SETUP.MEDIA_BACKEND: @@ -264,7 +279,10 @@ if SETUP.MEDIA_BACKEND: query = urllib.parse.parse_qs(parsed.query) if parsed.scheme == "gcs": DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" - GS_BUCKET_NAME = parsed.path.lstrip("/") + if parsed.path.lstrip("/"): + GS_BUCKET_NAME = parsed.path.lstrip("/") + else: + GS_BUCKET_NAME = parsed.hostname GS_QUERYSTRING_AUTH = False elif parsed.scheme == "s3": DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" @@ -273,6 +291,8 @@ if SETUP.MEDIA_BACKEND: AWS_SECRET_ACCESS_KEY = parsed.password port = parsed.port or 443 AWS_S3_ENDPOINT_URL = f"{parsed.hostname}:{port}" + else: + raise ValueError(f"Unsupported media backend {parsed.scheme}") if SETUP.ERROR_EMAILS: ADMINS = [("Admin", e) for e in SETUP.ERROR_EMAILS]