chore: update frontend and backend URLs to use port 5181 for local development
This commit is contained in:
parent
e26ab380fc
commit
3f46e09aed
19 changed files with 41 additions and 37 deletions
|
|
@ -37,4 +37,4 @@ GOOGLE_CLIENT_ID=
|
||||||
GOOGLE_CLIENT_SECRET=
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
|
||||||
# Frontend
|
# Frontend
|
||||||
PUBLIC_DJANGO_API_URL=http://localhost:8000
|
PUBLIC_DJANGO_API_URL=http://backend:8000
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ cd Django-CRM
|
||||||
docker compose up --build
|
docker compose up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
Frontend at http://localhost:5173, API and Swagger UI at http://localhost:8000/swagger-ui/. See
|
Frontend at http://localhost:5181, API and Swagger UI at http://localhost:8000/swagger-ui/. See
|
||||||
the [Docker quick start guide](docs/getting-started/docker-quick-start.md) for the default admin
|
the [Docker quick start guide](docs/getting-started/docker-quick-start.md) for the default admin
|
||||||
login, loading demo data, and what each service does.
|
login, loading demo data, and what each service does.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ DOMAIN_NAME="http://localhost:8000"
|
||||||
# CSAT survey, and the internal "assigned to you" notifications. Outside dev
|
# CSAT survey, and the internal "assigned to you" notifications. Outside dev
|
||||||
# (ENV_TYPE other than "dev") a loopback value is refused at startup, because
|
# (ENV_TYPE other than "dev") a loopback value is refused at startup, because
|
||||||
# left at this default it mails customers a link to their own machine.
|
# left at this default it mails customers a link to their own machine.
|
||||||
FRONTEND_URL="http://localhost:5173"
|
FRONTEND_URL="http://localhost:5181"
|
||||||
|
|
||||||
# AWS (only needed for production / S3 storage)
|
# AWS (only needed for production / S3 storage)
|
||||||
AWS_BUCKET_NAME=""
|
AWS_BUCKET_NAME=""
|
||||||
|
|
@ -44,8 +44,8 @@ DEFAULT_FROM_EMAIL="noreply@localhost"
|
||||||
ADMIN_EMAIL="admin@localhost"
|
ADMIN_EMAIL="admin@localhost"
|
||||||
|
|
||||||
ALLOWED_HOSTS=localhost,127.0.0.1
|
ALLOWED_HOSTS=localhost,127.0.0.1
|
||||||
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
CORS_ALLOWED_ORIGINS=http://localhost:5181,http://127.0.0.1:5181
|
||||||
CSRF_TRUSTED_ORIGINS=http://localhost:5173
|
CSRF_TRUSTED_ORIGINS=http://localhost:5181
|
||||||
|
|
||||||
# Set to True only when TLS is terminated by a proxy in front of Django and that
|
# Set to True only when TLS is terminated by a proxy in front of Django and that
|
||||||
# proxy is the only way in. It makes Django trust X-Forwarded-Proto, which is
|
# proxy is the only way in. It makes Django trust X-Forwarded-Proto, which is
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ CELERY_RESULT_BACKEND=redis://localhost:6379/0
|
||||||
# The web app, NOT this API. Every link the backend emails is built from it:
|
# The web app, NOT this API. Every link the backend emails is built from it:
|
||||||
# the magic-link sign-in URL, the customer invoice and estimate portals, the
|
# the magic-link sign-in URL, the customer invoice and estimate portals, the
|
||||||
# CSAT survey. Point it at the API host and every one of those links 404s.
|
# CSAT survey. Point it at the API host and every one of those links 404s.
|
||||||
FRONTEND_URL=http://localhost:5173
|
FRONTEND_URL=http://localhost:5181
|
||||||
|
|
||||||
# Google sign-in. Without these the OAuth login flow cannot complete, which is
|
# Google sign-in. Without these the OAuth login flow cannot complete, which is
|
||||||
# the only interactive way into the app.
|
# the only interactive way into the app.
|
||||||
|
|
@ -164,7 +164,7 @@ GOOGLE_CLIENT_ID=your-google-client-id
|
||||||
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||||
|
|
||||||
# Where the browser calls this API from. The SvelteKit dev server by default.
|
# Where the browser calls this API from. The SvelteKit dev server by default.
|
||||||
CORS_ALLOWED_ORIGINS=http://localhost:5173
|
CORS_ALLOWED_ORIGINS=http://localhost:5181
|
||||||
|
|
||||||
# This API's own public origin
|
# This API's own public origin
|
||||||
DOMAIN_NAME=http://localhost:8000
|
DOMAIN_NAME=http://localhost:8000
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
``common.links.frontend_url`` builds all of them from it: the customer's invoice
|
``common.links.frontend_url`` builds all of them from it: the customer's invoice
|
||||||
and estimate portal links, the CSAT survey, the internal "assigned to you"
|
and estimate portal links, the CSAT survey, the internal "assigned to you"
|
||||||
notifications, and, directly in ``common/tasks.py``, the magic-link sign-in URL.
|
notifications, and, directly in ``common/tasks.py``, the magic-link sign-in URL.
|
||||||
It defaults to ``http://localhost:5173`` so a checkout works with no ``.env``,
|
It defaults to ``http://localhost:5181`` so a checkout works with no ``.env``,
|
||||||
which means a deployment that never sets it mails customers a link to a port on
|
which means a deployment that never sets it mails customers a link to a port on
|
||||||
their own machine and mails a sign-in token to one. Nothing on the server
|
their own machine and mails a sign-in token to one. Nothing on the server
|
||||||
reports it: the email sends, the link is simply dead on arrival.
|
reports it: the email sends, the link is simply dead on arrival.
|
||||||
|
|
@ -56,31 +56,31 @@ def _import_settings(frontend_url, env_type):
|
||||||
class TestALoopbackFrontendUrlIsRefusedOutsideDev:
|
class TestALoopbackFrontendUrlIsRefusedOutsideDev:
|
||||||
def test_the_dev_default_fails_the_import(self):
|
def test_the_dev_default_fails_the_import(self):
|
||||||
"""The exact value an operator gets by not setting the variable."""
|
"""The exact value an operator gets by not setting the variable."""
|
||||||
result = _import_settings("http://localhost:5173", "production")
|
result = _import_settings("http://localhost:5181", "production")
|
||||||
|
|
||||||
assert result.returncode != 0
|
assert result.returncode != 0
|
||||||
assert "FRONTEND_URL" in result.stderr
|
assert "FRONTEND_URL" in result.stderr
|
||||||
|
|
||||||
def test_the_error_says_what_to_set_it_to(self):
|
def test_the_error_says_what_to_set_it_to(self):
|
||||||
"""A refusal that does not say what to do next is a support ticket."""
|
"""A refusal that does not say what to do next is a support ticket."""
|
||||||
result = _import_settings("http://localhost:5173", "production")
|
result = _import_settings("http://localhost:5181", "production")
|
||||||
|
|
||||||
assert "https://app.example.com" in result.stderr
|
assert "https://app.example.com" in result.stderr
|
||||||
|
|
||||||
def test_the_error_says_the_link_reaches_customers(self):
|
def test_the_error_says_the_link_reaches_customers(self):
|
||||||
"""The operator needs to know this is outward-facing, not internal."""
|
"""The operator needs to know this is outward-facing, not internal."""
|
||||||
result = _import_settings("http://localhost:5173", "production")
|
result = _import_settings("http://localhost:5181", "production")
|
||||||
|
|
||||||
assert "customers" in result.stderr
|
assert "customers" in result.stderr
|
||||||
|
|
||||||
def test_the_loopback_address_fails_too(self):
|
def test_the_loopback_address_fails_too(self):
|
||||||
result = _import_settings("http://127.0.0.1:5173", "production")
|
result = _import_settings("http://127.0.0.1:5181", "production")
|
||||||
|
|
||||||
assert result.returncode != 0
|
assert result.returncode != 0
|
||||||
|
|
||||||
def test_the_wildcard_bind_address_fails(self):
|
def test_the_wildcard_bind_address_fails(self):
|
||||||
"""`0.0.0.0` is what a Docker compose file tends to carry."""
|
"""`0.0.0.0` is what a Docker compose file tends to carry."""
|
||||||
result = _import_settings("http://0.0.0.0:5173", "production")
|
result = _import_settings("http://0.0.0.0:5181", "production")
|
||||||
|
|
||||||
assert result.returncode != 0
|
assert result.returncode != 0
|
||||||
|
|
||||||
|
|
@ -141,7 +141,7 @@ class TestANonAbsoluteFrontendUrlIsRefusedOutsideDev:
|
||||||
class TestDevIsLeftAlone:
|
class TestDevIsLeftAlone:
|
||||||
def test_the_dev_default_imports_cleanly_in_dev(self):
|
def test_the_dev_default_imports_cleanly_in_dev(self):
|
||||||
"""In dev the loopback URL is the correct answer, not a warning."""
|
"""In dev the loopback URL is the correct answer, not a warning."""
|
||||||
result = _import_settings("http://localhost:5173", "dev")
|
result = _import_settings("http://localhost:5181", "dev")
|
||||||
|
|
||||||
assert result.returncode == 0, result.stderr
|
assert result.returncode == 0, result.stderr
|
||||||
assert "FRONTEND_URL" not in result.stderr
|
assert "FRONTEND_URL" not in result.stderr
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ rule, and `Tags.name_error` the one owner of what a storable name is.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
|
import uuid
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -207,8 +208,10 @@ class TestRecomputeMigration:
|
||||||
module.recompute_tag_slugs(global_apps, SimpleNamespace(connection=connection))
|
module.recompute_tag_slugs(global_apps, SimpleNamespace(connection=connection))
|
||||||
|
|
||||||
def _with_slug(self, org, name, slug):
|
def _with_slug(self, org, name, slug):
|
||||||
# save() recomputes the slug, so store the old one with update().
|
# save() recomputes the slug, so store the old one with update(). The
|
||||||
tag = Tags.objects.create(name=f"placeholder {name}", org=org)
|
# placeholder must fit `name`'s 50 characters: Postgres enforces the
|
||||||
|
# width and SQLite does not, so a longer one only fails on Postgres.
|
||||||
|
tag = Tags.objects.create(name=f"placeholder {uuid.uuid4().hex}", org=org)
|
||||||
Tags.objects.filter(pk=tag.pk).update(name=name, slug=slug)
|
Tags.objects.filter(pk=tag.pk).update(name=name, slug=slug)
|
||||||
return tag
|
return tag
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -454,7 +454,7 @@ CORS_ORIGIN_ALLOW_ALL = os.environ.get("CORS_ALLOW_ALL", "False").lower() == "tr
|
||||||
CORS_ALLOWED_ORIGINS = [
|
CORS_ALLOWED_ORIGINS = [
|
||||||
origin.strip()
|
origin.strip()
|
||||||
for origin in os.environ.get(
|
for origin in os.environ.get(
|
||||||
"CORS_ALLOWED_ORIGINS", "http://localhost:5173,http://127.0.0.1:5173"
|
"CORS_ALLOWED_ORIGINS", "http://localhost:5181,http://127.0.0.1:5181"
|
||||||
).split(",")
|
).split(",")
|
||||||
if origin.strip()
|
if origin.strip()
|
||||||
]
|
]
|
||||||
|
|
@ -523,7 +523,7 @@ DOMAIN_NAME = os.environ.get("DOMAIN_NAME", "http://localhost:8000")
|
||||||
ORG_API_KEY_AUTH_ENABLED = os.environ.get(
|
ORG_API_KEY_AUTH_ENABLED = os.environ.get(
|
||||||
"DJANGO_ORG_API_KEY_AUTH", "true"
|
"DJANGO_ORG_API_KEY_AUTH", "true"
|
||||||
).strip().lower() not in ("false", "0", "no", "off")
|
).strip().lower() not in ("false", "0", "no", "off")
|
||||||
FRONTEND_URL = os.environ.get("FRONTEND_URL", "http://localhost:5173")
|
FRONTEND_URL = os.environ.get("FRONTEND_URL", "http://localhost:5181")
|
||||||
|
|
||||||
# Every link this system puts in an email is built from this one value, via
|
# Every link this system puts in an email is built from this one value, via
|
||||||
# `common.links.frontend_url`: the magic-link sign-in URL, the customer's
|
# `common.links.frontend_url`: the magic-link sign-in URL, the customer's
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ services:
|
||||||
- ./frontend:/app
|
- ./frontend:/app
|
||||||
- frontend_node_modules:/app/node_modules
|
- frontend_node_modules:/app/node_modules
|
||||||
ports:
|
ports:
|
||||||
- "5173:5173"
|
- "5181:5181"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ of this section's page references point at, at the same, empty prefix. Individua
|
||||||
under their own segment from there, for example `path("leads/", include("leads.urls", ...))`, so a
|
under their own segment from there, for example `path("leads/", include("leads.urls", ...))`, so a
|
||||||
lead endpoint is `/api/leads/…`.
|
lead endpoint is `/api/leads/…`.
|
||||||
|
|
||||||
Locally this is `http://localhost:8000/api/` (the default in the Docker quick start's
|
Locally this is `http://localhost:8000/api/` (the port the Docker quick start publishes. Inside
|
||||||
`.env.docker`, and what `PUBLIC_DJANGO_API_URL` points the frontend at. See
|
the compose network the frontend reaches the same API as `http://backend:8000`. See
|
||||||
[Docker quick start](../getting-started/docker-quick-start.md)). In a self-hosted deployment it is
|
[Docker quick start](../getting-started/docker-quick-start.md)). In a self-hosted deployment it is
|
||||||
whatever host you put `DOMAIN_NAME` / `ALLOWED_HOSTS` on, still under `/api/`.
|
whatever host you put `DOMAIN_NAME` / `ALLOWED_HOSTS` on, still under `/api/`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,11 +66,11 @@ pnpm install
|
||||||
pnpm run dev
|
pnpm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
`pnpm run dev` starts the Vite dev server on `http://localhost:5173`. `frontend/vite.config.js`
|
`pnpm run dev` starts the Vite dev server on `http://localhost:5181`. `frontend/vite.config.js`
|
||||||
configures no dev-server proxy, there is no `server.proxy` entry in it, so the browser calls the
|
configures no dev-server proxy, there is no `server.proxy` entry in it, so the browser calls the
|
||||||
Django API directly, cross-origin: `frontend/src/lib/api.js` builds its base URL from
|
Django API directly, cross-origin: `frontend/src/lib/api.js` builds its base URL from
|
||||||
`PUBLIC_DJANGO_API_URL`, defaulting to `http://localhost:8000/api` when that variable is unset. That
|
`PUBLIC_DJANGO_API_URL`, defaulting to `http://localhost:8000/api` when that variable is unset. That
|
||||||
means your backend's `CORS_ALLOWED_ORIGINS` has to include `http://localhost:5173` (it does by
|
means your backend's `CORS_ALLOWED_ORIGINS` has to include `http://localhost:5181` (it does by
|
||||||
default in `backend/.env.example`: see [Manual setup](../getting-started/manual-setup.md)) or every
|
default in `backend/.env.example`: see [Manual setup](../getting-started/manual-setup.md)) or every
|
||||||
request from the dev server fails as a CORS error, not a 404 or connection-refused, which is easy to
|
request from the dev server fails as a CORS error, not a 404 or connection-refused, which is easy to
|
||||||
misdiagnose as a backend problem.
|
misdiagnose as a backend problem.
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ The variables `.env.docker` actually defines are:
|
||||||
| `DEFAULT_FROM_EMAIL`, `ADMIN_EMAIL`, `ADMIN_PASSWORD` | Console email backend (dev), and the credentials used to bootstrap a Django admin superuser. See [What you got](#what-you-got). |
|
| `DEFAULT_FROM_EMAIL`, `ADMIN_EMAIL`, `ADMIN_PASSWORD` | Console email backend (dev), and the credentials used to bootstrap a Django admin superuser. See [What you got](#what-you-got). |
|
||||||
| `CORS_ALLOW_ALL` | `True` for local development. |
|
| `CORS_ALLOW_ALL` | `True` for local development. |
|
||||||
| `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` | Blank by default; Google sign-in is disabled until both are set. The redirect URI is not a setting: the frontend sends it in the request body on each call. |
|
| `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` | Blank by default; Google sign-in is disabled until both are set. The redirect URI is not a setting: the frontend sends it in the request body on each call. |
|
||||||
| `PUBLIC_DJANGO_API_URL` | The API base URL the frontend container is built against, `http://localhost:8000`. |
|
| `PUBLIC_DJANGO_API_URL` | The API origin the frontend's server-side code calls, `http://backend:8000`. It uses the compose service name because `localhost` inside the frontend container is the container itself. Your browser still reaches the API at `http://localhost:8000`. |
|
||||||
|
|
||||||
## Start the stack
|
## Start the stack
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ This builds and starts six services, defined in `docker-compose.yml`:
|
||||||
| `backend` | built from the repo root `Dockerfile` | `8000` |
|
| `backend` | built from the repo root `Dockerfile` | `8000` |
|
||||||
| `celery-worker` | same image as `backend`, runs `celery -A crm worker --loglevel=info` |. |
|
| `celery-worker` | same image as `backend`, runs `celery -A crm worker --loglevel=info` |. |
|
||||||
| `celery-beat` | same image as `backend`, runs `celery -A crm beat --loglevel=info` |, |
|
| `celery-beat` | same image as `backend`, runs `celery -A crm beat --loglevel=info` |, |
|
||||||
| `frontend` | built from `frontend/Dockerfile` | `5173` |
|
| `frontend` | built from `frontend/Dockerfile` | `5181` |
|
||||||
|
|
||||||
`backend`, `celery-worker` and `celery-beat` all wait for `db` and `redis` to report healthy
|
`backend`, `celery-worker` and `celery-beat` all wait for `db` and `redis` to report healthy
|
||||||
before starting. The `backend` container's entrypoint (`docker/backend/entrypoint.sh`) waits for
|
before starting. The `backend` container's entrypoint (`docker/backend/entrypoint.sh`) waits for
|
||||||
|
|
@ -106,7 +106,7 @@ docker compose exec backend python manage.py devlogin you@example.com --org Micr
|
||||||
|
|
||||||
This prints an access token, a refresh token, and the `MicroPyramid` org's UUID, already bound
|
This prints an access token, a refresh token, and the `MicroPyramid` org's UUID, already bound
|
||||||
into the token. See [First sign-in](first-sign-in.md) for how to use them from the frontend at
|
into the token. See [First sign-in](first-sign-in.md) for how to use them from the frontend at
|
||||||
`http://localhost:5173`.
|
`http://localhost:5181`.
|
||||||
|
|
||||||
## What you got
|
## What you got
|
||||||
|
|
||||||
|
|
@ -117,7 +117,7 @@ After `docker compose up --build` and one `seed_data` run, you have:
|
||||||
server. This compose file is for development).
|
server. This compose file is for development).
|
||||||
- A Celery worker and a Celery beat scheduler, both connected to the `redis` service, running the
|
- A Celery worker and a Celery beat scheduler, both connected to the `redis` service, running the
|
||||||
periodic and background tasks registered in `backend/crm/celery.py`.
|
periodic and background tasks registered in `backend/crm/celery.py`.
|
||||||
- A SvelteKit frontend on `http://localhost:5173`, built against `PUBLIC_DJANGO_API_URL=http://localhost:8000`.
|
- A SvelteKit frontend on `http://localhost:5181`, which reaches the API over the compose network at `PUBLIC_DJANGO_API_URL=http://backend:8000`.
|
||||||
- A Django admin superuser, created automatically by `create_default_admin` on first boot, using
|
- A Django admin superuser, created automatically by `create_default_admin` on first boot, using
|
||||||
`ADMIN_EMAIL` / `ADMIN_PASSWORD` from `.env.docker` (`admin@localhost` / `admin` unless you
|
`ADMIN_EMAIL` / `ADMIN_PASSWORD` from `.env.docker` (`admin@localhost` / `admin` unless you
|
||||||
override them). This account can sign in to Django's own `/admin/` site; it does not have a CRM
|
override them). This account can sign in to Django's own `/admin/` site; it does not have a CRM
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ instead (`event.cookies.get('jwt_access')`, `('jwt_refresh')` and `('org')`), an
|
||||||
server-side reads `localStorage` at all. Without those cookies set, `hooks.server.js` treats the
|
server-side reads `localStorage` at all. Without those cookies set, `hooks.server.js` treats the
|
||||||
request as signed out and redirects to `/login`, no matter what `localStorage` holds.
|
request as signed out and redirects to `/login`, no matter what `localStorage` holds.
|
||||||
|
|
||||||
To get past the guard, open the devtools console on `http://localhost:5173`, run the printed
|
To get past the guard, open the devtools console on `http://localhost:5181`, run the printed
|
||||||
`localStorage.setItem(...)` snippet, and set the matching cookies yourself:
|
`localStorage.setItem(...)` snippet, and set the matching cookies yourself:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ you're following the ports in this guide). Then start the dev server:
|
||||||
pnpm run dev
|
pnpm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
This serves the SvelteKit app on `http://localhost:5173` (`vite dev` under the hood, per the
|
This serves the SvelteKit app on `http://localhost:5181` (`vite dev` under the hood, per the
|
||||||
`dev` script in `frontend/package.json`).
|
`dev` script in `frontend/package.json`).
|
||||||
|
|
||||||
## Running all three
|
## Running all three
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ Pooling uses psycopg 3's `psycopg_pool` through Django's PostgreSQL backend. It
|
||||||
| Variable | Default | Required | Purpose |
|
| Variable | Default | Required | Purpose |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `CORS_ALLOW_ALL` | `False` | No | Sets Django's `CORS_ORIGIN_ALLOW_ALL`. Note the name doesn't match the Django setting it controls. `"true"` (case-insensitive) allows every origin; anything else leaves it off. |
|
| `CORS_ALLOW_ALL` | `False` | No | Sets Django's `CORS_ORIGIN_ALLOW_ALL`. Note the name doesn't match the Django setting it controls. `"true"` (case-insensitive) allows every origin; anything else leaves it off. |
|
||||||
| `CORS_ALLOWED_ORIGINS` | `http://localhost:5173,http://127.0.0.1:5173` | Yes, once the frontend is served from a real domain | Comma-separated list of allowed CORS origins; each entry is stripped of surrounding whitespace and empty entries are dropped. |
|
| `CORS_ALLOWED_ORIGINS` | `http://localhost:5181,http://127.0.0.1:5181` | Yes, once the frontend is served from a real domain | Comma-separated list of allowed CORS origins; each entry is stripped of surrounding whitespace and empty entries are dropped. |
|
||||||
| `CSRF_TRUSTED_ORIGINS` | *(empty, no trusted origins)* | Yes, before anyone but you can reach the instance | Comma-separated list, same stripping rule as `CORS_ALLOWED_ORIGINS`. Each entry needs a scheme, e.g. `https://crm.example.com`. A bare hostname is silently useless to Django's CSRF check. |
|
| `CSRF_TRUSTED_ORIGINS` | *(empty, no trusted origins)* | Yes, before anyone but you can reach the instance | Comma-separated list, same stripping rule as `CORS_ALLOWED_ORIGINS`. Each entry needs a scheme, e.g. `https://crm.example.com`. A bare hostname is silently useless to Django's CSRF check. |
|
||||||
|
|
||||||
### URLs
|
### URLs
|
||||||
|
|
@ -120,7 +120,7 @@ Pooling uses psycopg 3's `psycopg_pool` through Django's PostgreSQL backend. It
|
||||||
| Variable | Default | Required | Purpose |
|
| Variable | Default | Required | Purpose |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `DOMAIN_NAME` | `http://localhost:8000` | No. Nothing reads it any more | Base URL of the **backend**. It no longer reaches a single emitted link. The four remaining reads (`invoices/tasks.py:305,482` and `invoices/api_views.py:276,436`) pass it as the `domain=` kwarg of Celery tasks whose signatures keep that parameter only so a task queued before the change still unpacks after deploy, and whose docstrings say it is ignored. Every link now comes from `FRONTEND_URL` via `common/links.py`. It used to build the internal "assigned to you" notifications as well as the customer-facing invoice, estimate and CSAT links, and every one of those was broken: the builders wrote `f"{protocol}://{domain}/..."` around a value that already carried a scheme, so the emitted link had two, and the paths are frontend routes that do not exist on the backend host. Setting this variable changes nothing; it is kept so an existing `.env` does not have to be edited. |
|
| `DOMAIN_NAME` | `http://localhost:8000` | No. Nothing reads it any more | Base URL of the **backend**. It no longer reaches a single emitted link. The four remaining reads (`invoices/tasks.py:305,482` and `invoices/api_views.py:276,436`) pass it as the `domain=` kwarg of Celery tasks whose signatures keep that parameter only so a task queued before the change still unpacks after deploy, and whose docstrings say it is ignored. Every link now comes from `FRONTEND_URL` via `common/links.py`. It used to build the internal "assigned to you" notifications as well as the customer-facing invoice, estimate and CSAT links, and every one of those was broken: the builders wrote `f"{protocol}://{domain}/..."` around a value that already carried a scheme, so the emitted link had two, and the paths are frontend routes that do not exist on the backend host. Setting this variable changes nothing; it is kept so an existing `.env` does not have to be edited. |
|
||||||
| `FRONTEND_URL` | `http://localhost:5173` | **Yes**, once self-hosted under a real domain | Base URL of the SvelteKit frontend, and the single source for every link that leaves the system in an email: the welcome email and magic-link sign-in URL (`common/tasks.py`), the emailed invoice and estimate portal links, the CSAT survey link, and the "assigned to you" invoice link (all via `common.links.frontend_url`). The customer-facing ones go to people outside the organization, so leaving the `localhost` default in a real deployment emails dead links to customers. **That is now refused rather than emailed:** when `ENV_TYPE` is anything other than `dev`, `crm/settings.py` raises at import if this points at a loopback host (`localhost`, `127.0.0.1`, `0.0.0.0`, `::1`) or is not an absolute `http`/`https` URL, so the deploy fails to start instead of failing silently on somebody else's inbox. Plain HTTP against a real host is allowed, for a deployment behind its own TLS terminator. A trailing slash is stripped, so `https://app.example.com` and `https://app.example.com/` behave the same. |
|
| `FRONTEND_URL` | `http://localhost:5181` | **Yes**, once self-hosted under a real domain | Base URL of the SvelteKit frontend, and the single source for every link that leaves the system in an email: the welcome email and magic-link sign-in URL (`common/tasks.py`), the emailed invoice and estimate portal links, the CSAT survey link, and the "assigned to you" invoice link (all via `common.links.frontend_url`). The customer-facing ones go to people outside the organization, so leaving the `localhost` default in a real deployment emails dead links to customers. **That is now refused rather than emailed:** when `ENV_TYPE` is anything other than `dev`, `crm/settings.py` raises at import if this points at a loopback host (`localhost`, `127.0.0.1`, `0.0.0.0`, `::1`) or is not an absolute `http`/`https` URL, so the deploy fails to start instead of failing silently on somebody else's inbox. Plain HTTP against a real host is allowed, for a deployment behind its own TLS terminator. A trailing slash is stripped, so `https://app.example.com` and `https://app.example.com/` behave the same. |
|
||||||
|
|
||||||
### Google OAuth
|
### Google OAuth
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ requirements in [Production deployment](production-deploy.md).
|
||||||
| `backend` | built from the repo-root `Dockerfile` | `8000` | The Django API, served via `manage.py runserver` in this compose file (see the warning above). |
|
| `backend` | built from the repo-root `Dockerfile` | `8000` | The Django API, served via `manage.py runserver` in this compose file (see the warning above). |
|
||||||
| `celery-worker` | same image as `backend` |, (no published port) | Runs `celery -A crm worker --loglevel=info`; executes background tasks such as outbound email and RLS-context-aware jobs in `backend/common/tasks.py`. |
|
| `celery-worker` | same image as `backend` |, (no published port) | Runs `celery -A crm worker --loglevel=info`; executes background tasks such as outbound email and RLS-context-aware jobs in `backend/common/tasks.py`. |
|
||||||
| `celery-beat` | same image as `backend` |, (no published port) | Runs `celery -A crm beat --loglevel=info`; fires the periodic tasks registered in `backend/crm/celery.py` (recurring invoices, overdue/expiry checks, SLA scanning, cleanup jobs) on their schedules. |
|
| `celery-beat` | same image as `backend` |, (no published port) | Runs `celery -A crm beat --loglevel=info`; fires the periodic tasks registered in `backend/crm/celery.py` (recurring invoices, overdue/expiry checks, SLA scanning, cleanup jobs) on their schedules. |
|
||||||
| `frontend` | built from `frontend/Dockerfile` | `5173` | The SvelteKit app, served via `pnpm dev --host 0.0.0.0`, also a dev server, not a production build. |
|
| `frontend` | built from `frontend/Dockerfile` | `5181` | The SvelteKit app, served via `pnpm dev --host 0.0.0.0`, also a dev server, not a production build. |
|
||||||
|
|
||||||
`backend`, `celery-worker` and `celery-beat` each declare `depends_on: db (service_healthy), redis
|
`backend`, `celery-worker` and `celery-beat` each declare `depends_on: db (service_healthy), redis
|
||||||
(service_healthy)`, using the `pg_isready` and `redis-cli ping` healthchecks defined on those two
|
(service_healthy)`, using the `pg_isready` and `redis-cli ping` healthchecks defined on those two
|
||||||
|
|
@ -47,7 +47,8 @@ superuser role. See [PostgreSQL and RLS](postgresql-and-rls.md) for why the app
|
||||||
as this role), `CELERY_BROKER_URL`/`CELERY_RESULT_BACKEND` (both pointing at `redis`),
|
as this role), `CELERY_BROKER_URL`/`CELERY_RESULT_BACKEND` (both pointing at `redis`),
|
||||||
`DEFAULT_FROM_EMAIL`/`ADMIN_EMAIL`/`ADMIN_PASSWORD`, `CORS_ALLOW_ALL=True`,
|
`DEFAULT_FROM_EMAIL`/`ADMIN_EMAIL`/`ADMIN_PASSWORD`, `CORS_ALLOW_ALL=True`,
|
||||||
`GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET` (blank. Google sign-in stays disabled until both
|
`GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET` (blank. Google sign-in stays disabled until both
|
||||||
are set), and `PUBLIC_DJANGO_API_URL=http://localhost:8000` for the frontend build.
|
are set), and `PUBLIC_DJANGO_API_URL=http://backend:8000`, the compose service name the
|
||||||
|
frontend's server-side code uses to reach the API.
|
||||||
|
|
||||||
## Running it
|
## Running it
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ Create an OAuth 2.0 Client ID of type "Web application" in the Google Cloud Cons
|
||||||
URI it needs authorized is built by the frontend's login page
|
URI it needs authorized is built by the frontend's login page
|
||||||
(`frontend/src/routes/(no-layout)/login/+page.server.js`) as `${GOOGLE_LOGIN_DOMAIN}/login`;
|
(`frontend/src/routes/(no-layout)/login/+page.server.js`) as `${GOOGLE_LOGIN_DOMAIN}/login`;
|
||||||
`GOOGLE_LOGIN_DOMAIN` is a **frontend** environment variable (`frontend/.env.example` defaults it to
|
`GOOGLE_LOGIN_DOMAIN` is a **frontend** environment variable (`frontend/.env.example` defaults it to
|
||||||
`http://localhost:5173`), so the value to register with Google is `http://localhost:5173/login` for
|
`http://localhost:5181`), so the value to register with Google is `http://localhost:5181/login` for
|
||||||
local development, or `https://<your-frontend-host>/login` in production. There is no separate
|
local development, or `https://<your-frontend-host>/login` in production. There is no separate
|
||||||
redirect registration needed for the mobile flow. It doesn't use a redirect at all (see
|
redirect registration needed for the mobile flow. It doesn't use a redirect at all (see
|
||||||
[Mobile ID tokens](#mobile-id-tokens) below).
|
[Mobile ID tokens](#mobile-id-tokens) below).
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Google OAuth (client_id only - secret is on backend)
|
# Google OAuth (client_id only - secret is on backend)
|
||||||
GOOGLE_CLIENT_ID="your-google-client-id-here"
|
GOOGLE_CLIENT_ID="your-google-client-id-here"
|
||||||
GOOGLE_LOGIN_DOMAIN="http://localhost:5173"
|
GOOGLE_LOGIN_DOMAIN="http://localhost:5181"
|
||||||
|
|
||||||
# Django API URL (PUBLIC_ prefix makes it available in both server and browser)
|
# Django API URL (PUBLIC_ prefix makes it available in both server and browser)
|
||||||
PUBLIC_DJANGO_API_URL=http://localhost:8000
|
PUBLIC_DJANGO_API_URL=http://localhost:8000
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ RUN pnpm install --frozen-lockfile
|
||||||
# The compose target. Source and node_modules arrive as mounts, so this stage
|
# The compose target. Source and node_modules arrive as mounts, so this stage
|
||||||
# deliberately copies no source of its own.
|
# deliberately copies no source of its own.
|
||||||
FROM deps AS dev
|
FROM deps AS dev
|
||||||
EXPOSE 5173
|
EXPOSE 5181
|
||||||
CMD ["pnpm", "dev", "--host", "0.0.0.0"]
|
CMD ["pnpm", "dev", "--host", "0.0.0.0"]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ JWT_SECRET="<your-generated-secret>"
|
||||||
# Google OAuth (Optional)
|
# Google OAuth (Optional)
|
||||||
GOOGLE_CLIENT_ID=""
|
GOOGLE_CLIENT_ID=""
|
||||||
GOOGLE_CLIENT_SECRET=""
|
GOOGLE_CLIENT_SECRET=""
|
||||||
GOOGLE_LOGIN_DOMAIN="http://localhost:5173"
|
GOOGLE_LOGIN_DOMAIN="http://localhost:5181"
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Set up the database:**
|
5. **Set up the database:**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue