Ebbe BaßandClaude Sonnet 5.5 3fd8f27127 feat: add profile edit
Profile page at /profile with first name, last name, display name, telephone and mobile read from AD and shown with the username and e-mail. Only changed fields are written through the directory adapter in one transaction with the audit entry (field names only); numbers are accepted in any common notation and stored as +country code and digits; names are required. Plain-language errors per field, German text and tests; review findings logged in the plan.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FocPJ75XPma7TcuYEMy8zm
2026-10-19 09:03:24 +02:00
2026-10-19 09:03:24 +02:00
2026-10-19 09:03:24 +02:00
2026-10-18 09:18:42 +02:00
2026-10-18 09:18:42 +02:00
2026-10-15 08:57:23 +02:00

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
SMTP_HOST yes Host name of the mail server (no port, no URL)
SMTP_SECURITY no starttls starttls (port 587) or ssl (port 465); mail is never sent unencrypted and the server certificate is verified
SMTP_PORT no 587 or 465 Overrides the port
SMTP_USERNAME yes Mailbox user name
SMTP_PASSWORD yes Mailbox password
MAIL_FROM yes Sender address of every e-mail, optionally with a display name
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.

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.

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.

Scheduler and e-mail

One scheduler thread runs inside the single gunicorn worker and ticks every 30 seconds (docker/gunicorn.conf.py). Each tick runs the jobs in portal/scheduler/services.py; every run is recorded in JobRun. Other code never talks to the mail server: it calls portal.mailer.services.enqueue(template, recipient, context) inside its own transaction, and the mail.flush job sends from the outbox with retries (1, 5, 15 and 60 minutes between attempts, 5 attempts), at most 20 mails a minute. A mail that was being sent when the process stopped is marked failed and is not sent again.

Mail templates live in portal/templates/mail/<name>/subject.txt and body.txt. They are rendered when queued, in the portal-wide language (MAIL_LANGUAGE, German formal), and the text is removed from the database once the mail is sent.

To check the SMTP settings, queue and send a test mail from the running container:

docker compose exec portal python manage.py send_test_mail someone@example.test

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:

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.

S
Description
The ping-mee self-servicing platform for all users that use the ping-mee network and its resources
Readme
1.6 MiB
Languages
Python 80.9%
HTML 16.3%
PowerShell 2%
CSS 0.5%
JavaScript 0.2%
Other 0.1%