Make APP_URLS accept list
This commit is contained in:
+4
-4
@@ -15,13 +15,13 @@ DEBUG=false
|
||||
SECRET_KEY=change-me-to-a-long-random-string
|
||||
# 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,https://www.tracker.kucharczyk.xyz
|
||||
|
||||
# Optional explicit overrides (comma-separated). When set they win over APP_URL.
|
||||
# Useful behind a reverse proxy, e.g. ALLOWED_HOSTS=*
|
||||
# Override ALLOWED_HOSTS directly for edge cases (e.g. behind a reverse proxy).
|
||||
# ALLOWED_HOSTS=*
|
||||
# CSRF_TRUSTED_ORIGINS=https://tracker.kucharczyk.xyz
|
||||
|
||||
# Container timezone.
|
||||
TZ=Europe/Prague
|
||||
|
||||
@@ -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.
|
||||
- `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`.
|
||||
- `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).
|
||||
- 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.
|
||||
|
||||
+18
-9
@@ -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. |
|
||||
| `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. |
|
||||
| `ALLOWED_HOSTS` | list | derived from `APP_URL` | no | Comma-separated hostnames. Overrides the `APP_URL` derivation. |
|
||||
| `CSRF_TRUSTED_ORIGINS` | list | derived from `APP_URL` | no | Comma-separated full origins (`https://host`). Overrides the `APP_URL` derivation. |
|
||||
| `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 (useful for `ALLOWED_HOSTS=*` behind a reverse proxy). |
|
||||
| `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`. |
|
||||
|
||||
@@ -42,21 +41,31 @@ whitespace-trimmed, empty items dropped), `int`, `Path`, or any callable.
|
||||
|
||||
## APP_URL, ALLOWED_HOSTS and CSRF
|
||||
|
||||
`ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS` guard different things — the `Host`
|
||||
header versus cross-origin requests — so they are **never merged**. For the
|
||||
common case you set only `APP_URL` and both are derived:
|
||||
`APP_URL` accepts one full URL or a comma-separated list of full URLs. Both
|
||||
`ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS` are derived from all listed URLs —
|
||||
no need to repeat the same information in separate variables.
|
||||
|
||||
Single domain (common case):
|
||||
|
||||
```
|
||||
APP_URL=https://tracker.example.com
|
||||
# -> ALLOWED_HOSTS = ["tracker.example.com"]
|
||||
# -> ALLOWED_HOSTS = ["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=*
|
||||
CSRF_TRUSTED_ORIGINS=https://tracker.example.com
|
||||
```
|
||||
|
||||
## Secrets and `__FILE`
|
||||
|
||||
+10
-8
@@ -51,16 +51,18 @@ SECRET_KEY = config(
|
||||
required_in_prod=True,
|
||||
)
|
||||
|
||||
# ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS are configured independently (they
|
||||
# guard different things), but both default off a single user-facing APP_URL
|
||||
# when not set explicitly. Power users override either one directly — e.g.
|
||||
# ALLOWED_HOSTS=* behind a reverse proxy while CSRF stays locked to the domain.
|
||||
# APP_URL accepts one or more comma-separated full URLs (single URL is the
|
||||
# common case). Both ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS are derived from
|
||||
# all listed URLs. ALLOWED_HOSTS can still be overridden directly for edge
|
||||
# cases like ALLOWED_HOSTS=* behind a reverse proxy.
|
||||
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]
|
||||
CSRF_TRUSTED_ORIGINS = config("CSRF_TRUSTED_ORIGINS", default=None, cast=list) or [
|
||||
f"{_app_url.scheme}://{_app_url.netloc}"
|
||||
ALLOWED_HOSTS = config("ALLOWED_HOSTS", default=None, cast=list) or [
|
||||
parsed_url.hostname for parsed_url in _parsed_app_urls
|
||||
]
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
f"{parsed_url.scheme}://{parsed_url.netloc}" for parsed_url in _parsed_app_urls
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user