Files
ping-mee-self-service-platform/README.md
T
ebbe.bassandClaude Sonnet 5.5 0dc079a8ac feat: add page shell, preferences and translations
Base layout with top bar, role-based menu, profile menu and phone layout; theme, language and German form of address kept in cookies; German formal, German informal and English text; Fluent avatar bundled in the image by a Node build stage; placeholder pages for the Publisher and Admin groups.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
2026-10-17 21:05:40 +02:00

76 lines
4.8 KiB
Markdown

# Self-Service Portal
A Django 5.2 LTS container for a user self-service portal. This repository holds the project scaffold: configuration, health check, logging, security headers and the container definition.
## Configuration
All configuration comes from environment variables, validated at startup. If any variable is missing or invalid, the process prints one message that lists every problem (never the values) and exits with a non-zero code. Copy `.env.example` to `.env` and edit it.
| Variable | Required | Default | Meaning |
| --- | --- | --- | --- |
| `SECRET_KEY` | yes | | Django secret, at least 32 characters |
| `ALLOWED_HOSTS` | yes | | Comma-separated host names |
| `DATA_DIR` | no | `/data` | Existing directory for the SQLite database |
| `DEBUG` | no | `false` | `true` or `false` |
| `TRUSTED_PROXIES` | no | empty | Comma-separated IP addresses or networks of the reverse proxies. The client address is taken from the last `X-Forwarded-For` entry only when the request comes from one of them |
| `OIDC_ISSUER` | yes | | Issuer address from the provider's OpenID configuration page, exactly as shown (https, keep the trailing slash) |
| `OIDC_CLIENT_ID` | yes | | Client ID of the provider's application |
| `OIDC_CLIENT_SECRET` | yes | | Client secret of the provider's application |
| `PUBLIC_BASE_URL` | yes | | Address users reach the portal at, scheme and host only. The provider's redirect address must be this plus `/auth/callback` |
| `LOG_LEVEL` | no | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
| `LDAP_SERVER` | yes | | Directory server host name (optionally `:port`); LDAPS on 636 is always used and the name must be in the server's certificate |
| `LDAP_USER_OU_DN` | yes | | Distinguished name of the OU whose users the portal may read and change |
| `LDAP_BIND_DN` | yes | | Service account, as distinguished name or user principal name |
| `LDAP_BIND_PASSWORD` | yes | | Password of the service account |
| `LDAP_CA_FILE` | yes | | Path inside the container of the PEM file with the CA chain; certificate verification is never disabled |
| `LDAP_PUBLISHER_GROUP_DN` | yes | | Group whose direct members are Publishers |
| `LDAP_ADMIN_GROUP_DN` | yes | | Group whose direct members are Admins |
| `DEFAULT_TIMEZONE` | no | `Europe/Berlin` | IANA zone used for display; storage is UTC |
## Run in a container
The image is built and run on a Linux host from a checkout of this repository.
```sh
cp .env.example .env # then set SECRET_KEY, ALLOWED_HOSTS, the LDAP_* values and CA_FILE_HOST
docker compose up --build
curl -H "Host: <first ALLOWED_HOSTS entry>" http://localhost:8000/healthz # {"status":"ok"}
```
The container runs as a non-root user with a read-only root file system. Only the `/data` and `/backups` volumes are writable. It starts one gunicorn worker with threads after applying migrations. Session and CSRF cookies are marked secure, so use the portal through an HTTPS reverse proxy that sets `X-Forwarded-Proto`.
`/healthz` checks the database only. It returns 200 `{"status":"ok"}` or 503 `{"status":"unavailable"}`.
## Develop
Requires Python 3.13 and [uv](https://docs.astral.sh/uv/).
```sh
uv sync
uv run pytest
uv run ruff check .
```
`pytest` sets its own test environment (see `pyproject.toml`).
### Page shell, front end and translations
The Fluent web components are bundled by a Node build stage in the `Dockerfile` (`portal/frontend`, pinned by `package-lock.json`) and served from the portal's own origin; the runtime image has no Node.js. To rebuild the bundle locally: `cd portal/frontend && npm ci && npm run build`, then copy `dist/fluent.js` to `portal/static/vendor/`. Pages work without it (the avatar falls back to initials).
Text is German (formal and informal) and English. The English text is the message id. Add the German text to `portal/locale/de/LC_MESSAGES/django.po`, and an informal variant to `portal/locale/de_DU/...` only where it differs. `test_translations.py` fails if a string has no German text. The Docker build compiles the catalogues with `gettext`; the tests compile their own copy.
Theme, language and form of address are stored per device in the cookies `pm_theme`, `pm_lang` and `pm_form`; nothing is stored on the server.
### Directory adapter against the test OU
`uv sync` builds `python-ldap`, which needs the OpenLDAP and SASL development libraries (the Docker image installs them). The automated tests use an in-memory fake and a stand-in for the `ldap` module, so they run anywhere.
To check the real adapter against the test OU (never the production OU), run the opt-in test container on the Linux host. It reads the `LDAP_*` values and `CA_FILE_HOST` from `.env`; the test user's current password comes from your shell:
```sh
LIVE_LDAP_TEST_PASSWORD=<current password of ptest-user1> \
docker compose --profile test run --rm live-test
```
It restores what it writes and does not change any password.