Make APP_URLS accept list

This commit is contained in:
2026-06-18 21:10:20 +02:00
parent 6f58eb3fde
commit d0d6b3f999
4 changed files with 33 additions and 22 deletions
+4 -4
View File
@@ -15,13 +15,13 @@ DEBUG=false
SECRET_KEY=change-me-to-a-long-random-string SECRET_KEY=change-me-to-a-long-random-string
# SECRET_KEY__FILE=/run/secrets/timetracker_secret_key # SECRET_KEY__FILE=/run/secrets/timetracker_secret_key
# Public URL of the site. Derives ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS. # Public URL(s) of the site — one URL or comma-separated list of full URLs.
# Derives ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS from all listed URLs.
APP_URL=https://tracker.kucharczyk.xyz APP_URL=https://tracker.kucharczyk.xyz
# APP_URL=https://tracker.kucharczyk.xyz,https://www.tracker.kucharczyk.xyz
# Optional explicit overrides (comma-separated). When set they win over APP_URL. # Override ALLOWED_HOSTS directly for edge cases (e.g. behind a reverse proxy).
# Useful behind a reverse proxy, e.g. ALLOWED_HOSTS=*
# ALLOWED_HOSTS=* # ALLOWED_HOSTS=*
# CSRF_TRUSTED_ORIGINS=https://tracker.kucharczyk.xyz
# Container timezone. # Container timezone.
TZ=Europe/Prague TZ=Europe/Prague
+1 -1
View File
@@ -151,7 +151,7 @@ All configurable Django settings are read through `config()` in `timetracker/con
- `config(name, *, default, cast, allow_file, required_in_prod)`: `cast` handles `bool`/`list`/`int`/`Path`/callable; `allow_file=True` honors `NAME__FILE` (contents `.strip()`-ed); `required_in_prod=True` hard-fails when missing and DEBUG is off. - `config(name, *, default, cast, allow_file, required_in_prod)`: `cast` handles `bool`/`list`/`int`/`Path`/callable; `allow_file=True` honors `NAME__FILE` (contents `.strip()`-ed); `required_in_prod=True` hard-fails when missing and DEBUG is off.
- `DEBUG` defaults `True` (dev), turned off with `DEBUG=false`. `PROD` is a **deprecated alias** kept for one release. - `DEBUG` defaults `True` (dev), turned off with `DEBUG=false`. `PROD` is a **deprecated alias** kept for one release.
- `SECRET_KEY` is required in production (insecure default only in DEBUG); supports `SECRET_KEY__FILE`. - `SECRET_KEY` is required in production (insecure default only in DEBUG); supports `SECRET_KEY__FILE`.
- `APP_URL` derives `ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS` when those aren't set explicitly; the two are never merged (different security checks) and each can be overridden directly. - `APP_URL` accepts one full URL or a comma-separated list of full URLs; `ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS` are derived from all listed URLs. `ALLOWED_HOSTS` can still be overridden directly (e.g. `ALLOWED_HOSTS=*` behind a reverse proxy); `CSRF_TRUSTED_ORIGINS` is always derived from `APP_URL`.
- `TIME_ZONE` reads `TZ` (defaults `Europe/Prague` in debug, `UTC` in prod). - `TIME_ZONE` reads `TZ` (defaults `Europe/Prague` in debug, `UTC` in prod).
- Django Admin, Debug Toolbar, and `django_extensions` are only available in `DEBUG` mode. - Django Admin, Debug Toolbar, and `django_extensions` are only available in `DEBUG` mode.
- **Container/entrypoint-only** flags (`PUID`, `PGID`, `CREATE_DEFAULT_SUPERUSER`, `STAGING`, `LOAD_SAMPLE_DATA`) live in `entrypoint.sh`, not the Python config — they are bootstrap concerns, not Django settings. - **Container/entrypoint-only** flags (`PUID`, `PGID`, `CREATE_DEFAULT_SUPERUSER`, `STAGING`, `LOAD_SAMPLE_DATA`) live in `entrypoint.sh`, not the Python config — they are bootstrap concerns, not Django settings.
+17 -8
View File
@@ -31,9 +31,8 @@ remove that and `settings.ini` wins; remove that and the code default applies.
|---------|------|---------|:---------:|-------------| |---------|------|---------|:---------:|-------------|
| `SECRET_KEY` | str | insecure dev key | yes | Django secret key. **Required in production** (DEBUG off) — a missing value is a hard error, not a silent insecure fallback. | | `SECRET_KEY` | str | insecure dev key | yes | Django secret key. **Required in production** (DEBUG off) — a missing value is a hard error, not a silent insecure fallback. |
| `DEBUG` | bool | `true` (dev) | no | Debug mode. Turn **off** in production. Defaults on for local development. | | `DEBUG` | bool | `true` (dev) | no | Debug mode. Turn **off** in production. Defaults on for local development. |
| `APP_URL` | str | `http://localhost:8000` | no | Public URL of the site. Derives `ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS` when those are not set explicitly. | | `APP_URL` | str (or comma-separated URLs) | `http://localhost:8000` | no | Public URL(s) of the site. One full URL or a comma-separated list. Derives `ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS` from all listed URLs. |
| `ALLOWED_HOSTS` | list | derived from `APP_URL` | no | Comma-separated hostnames. Overrides the `APP_URL` derivation. | | `ALLOWED_HOSTS` | list | derived from `APP_URL` | no | Comma-separated hostnames. Overrides the `APP_URL` derivation (useful for `ALLOWED_HOSTS=*` behind a reverse proxy). |
| `CSRF_TRUSTED_ORIGINS` | list | derived from `APP_URL` | no | Comma-separated full origins (`https://host`). Overrides the `APP_URL` derivation. |
| `TZ` | str | `Europe/Prague` (dev) / `UTC` (prod) | no | Time zone. | | `TZ` | str | `Europe/Prague` (dev) / `UTC` (prod) | no | Time zone. |
| `DATA_DIR` | path | project root | no | Directory holding the SQLite database. Also read by `entrypoint.sh`. | | `DATA_DIR` | path | project root | no | Directory holding the SQLite database. Also read by `entrypoint.sh`. |
@@ -42,9 +41,11 @@ whitespace-trimmed, empty items dropped), `int`, `Path`, or any callable.
## APP_URL, ALLOWED_HOSTS and CSRF ## APP_URL, ALLOWED_HOSTS and CSRF
`ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS` guard different things — the `Host` `APP_URL` accepts one full URL or a comma-separated list of full URLs. Both
header versus cross-origin requests — so they are **never merged**. For the `ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS` are derived from all listed URLs —
common case you set only `APP_URL` and both are derived: no need to repeat the same information in separate variables.
Single domain (common case):
``` ```
APP_URL=https://tracker.example.com APP_URL=https://tracker.example.com
@@ -52,11 +53,19 @@ APP_URL=https://tracker.example.com
# -> CSRF_TRUSTED_ORIGINS = ["https://tracker.example.com"] # -> CSRF_TRUSTED_ORIGINS = ["https://tracker.example.com"]
``` ```
Power users override either independently. A typical reverse-proxy setup: Multiple domains:
```
APP_URL=https://tracker.example.com,https://www.tracker.example.com
# -> ALLOWED_HOSTS = ["tracker.example.com", "www.tracker.example.com"]
# -> CSRF_TRUSTED_ORIGINS = ["https://tracker.example.com", "https://www.tracker.example.com"]
```
`ALLOWED_HOSTS` can still be overridden directly for edge cases. A typical
reverse-proxy setup where the proxy validates the host:
``` ```
ALLOWED_HOSTS=* ALLOWED_HOSTS=*
CSRF_TRUSTED_ORIGINS=https://tracker.example.com
``` ```
## Secrets and `__FILE` ## Secrets and `__FILE`
+10 -8
View File
@@ -51,16 +51,18 @@ SECRET_KEY = config(
required_in_prod=True, required_in_prod=True,
) )
# ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS are configured independently (they # APP_URL accepts one or more comma-separated full URLs (single URL is the
# guard different things), but both default off a single user-facing APP_URL # common case). Both ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS are derived from
# when not set explicitly. Power users override either one directly — e.g. # all listed URLs. ALLOWED_HOSTS can still be overridden directly for edge
# ALLOWED_HOSTS=* behind a reverse proxy while CSRF stays locked to the domain. # cases like ALLOWED_HOSTS=* behind a reverse proxy.
APP_URL = config("APP_URL", default="http://localhost:8000") APP_URL = config("APP_URL", default="http://localhost:8000")
_app_url = urlparse(APP_URL) _parsed_app_urls = [urlparse(raw_url.strip()) for raw_url in APP_URL.split(",")]
ALLOWED_HOSTS = config("ALLOWED_HOSTS", default=None, cast=list) or [_app_url.hostname] ALLOWED_HOSTS = config("ALLOWED_HOSTS", default=None, cast=list) or [
CSRF_TRUSTED_ORIGINS = config("CSRF_TRUSTED_ORIGINS", default=None, cast=list) or [ parsed_url.hostname for parsed_url in _parsed_app_urls
f"{_app_url.scheme}://{_app_url.netloc}" ]
CSRF_TRUSTED_ORIGINS = [
f"{parsed_url.scheme}://{parsed_url.netloc}" for parsed_url in _parsed_app_urls
] ]