Metadata-Version: 2.4
Name: django-heluca-themis
Version: 1.23.0
Summary: Django app providing user preferences, theme management, API key management, Casdoor SSO adapters, and standard navigation templates
Author-email: Robert Helewka <r@helu.ca>
License: MIT
Project-URL: Homepage, https://helu.ca
Project-URL: Repository, ssh://git@git.helu.ca:22022/r/themis.git
Keywords: django,user-profile,themes,daisyui,preferences,heluca
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django<6.0,>=5.2
Requires-Dist: djangorestframework<4.0,>=3.14
Requires-Dist: cryptography<51.0,>=41.0
Requires-Dist: django-allauth[socialaccount]<66.0,>=65.0
Requires-Dist: whitenoise<7,>=6.6
Provides-Extra: dev
Requires-Dist: drf-spectacular>=0.26.0; extra == "dev"
Requires-Dist: django-filter>=23.0; extra == "dev"
Requires-Dist: pyyaml>=6.0; extra == "dev"
Requires-Dist: prometheus-client>=0.19; extra == "dev"
Provides-Extra: docs
Requires-Dist: sphinx<10,>=9.0; extra == "docs"
Requires-Dist: heluca-sphinx<1.0,>=0.3; extra == "docs"
Requires-Dist: myst-parser<6,>=5.0; extra == "docs"
Provides-Extra: unfold
Requires-Dist: django-unfold<1.0,>=0.98; extra == "unfold"
Dynamic: license-file

# Themis

Reusable Django app providing user preferences, DaisyUI theme management, API key management, and standard navigation templates for all Heluca applications.

*Themis — titan of order, custom, and law.*

## Features

- **User Preferences** — timezone (home + traveling), date/time/number formatting, week start day
- **Heluca Design System** — the V2 system vendored from `@heluca/svelte`: token layer + daisyUI 5 bridge, `light` / `dark` plus the five temperaments (consulting, business, business-dark, terminal, terminal-light), self-hosted Marcellus, Literata, Public Sans, IBM Plex Sans, JetBrains Mono and Atkinson Hyperlegible; separate light/dark selection, auto (system) mode
- **Notifications** — in-app notification bell, JS polling, browser desktop notifications, user preferences
- **API Key Management** — encrypted storage with per-key instructions and documentation links
- **Standard Navigation** — consistent navbar, user menu, notification bell, theme toggle, and bottom nav across all apps
- **Themed Auth Pages** — every django-allauth page (login, signup, logout, password reset/change, email management, …) rendered in the Themis DaisyUI chrome, with no per-app templates, and a single-click SSO sign-in: the login page POSTs to the provider, and `themis.views.login` redirects straight to the IdP when SSO is the only method
- **Middleware** — automatic timezone activation and theme context
- **Formatting Utilities** — date, time, number formatting respecting user preferences
- **Health Checks** — Kubernetes-ready `/ready/` and `/live/` endpoints
- **Observability Wiring** — an `<app>_build_info{version="..."}` metric self-reporting the deployed release, and structured `event=login_success` / `event=login_failure` lines on the `themis.auth` logger, so one estate-wide Loki rule covers every app
- **REST API** — complete API for profiles, keys, and notifications

## Installation

Releases are wheels on the estate's Gitea PyPI registry (anonymous read):

```bash
pip install django-heluca-themis --extra-index-url https://git.helu.ca/api/packages/r/pypi/simple/
```

Consumers pin a range in `pyproject.toml` (`django-heluca-themis>=1.13,<2.0`)
and set `PIP_EXTRA_INDEX_URL` in the Dockerfile rather than passing the flag.

## Quick Start

1. Add to `INSTALLED_APPS`:
```python
INSTALLED_APPS = [
    ...
    "rest_framework",
    "themis",
    ...
]
```

2. Configure middleware:
```python
MIDDLEWARE = [
    ...
    "themis.middleware.TimezoneMiddleware",
    "themis.middleware.ThemeMiddleware",
    ...
]
```

3. Configure context processors:
```python
TEMPLATES = [{
    "OPTIONS": {
        "context_processors": [
            ...
            "themis.context_processors.themis_settings",
            "themis.context_processors.user_preferences",
            "themis.context_processors.notifications",
            "themis.context_processors.navigation",
        ],
    },
}]
```

4. Include URLs:
```python
urlpatterns = [
    ...
    path("", include("themis.urls")),
    path("api/v1/", include("themis.api.urls")),
    ...
]
```

5. Configure app settings:
```python
THEMIS_APP_NAME = "My Application"

# Register navigation entries (block-based extension can't cross {% include %},
# so the chrome iterates these instead). Each entry: label, named URL, optional
# SVG icon `d` path. Bad url_names are skipped with a warning, not a 500.
THEMIS_NAV_ITEMS = [
    {"label": "Home", "url_name": "core:home"},
    {"label": "Mail", "url_name": "mail:account_list"},
]
THEMIS_USER_MENU_ITEMS = [
    {"label": "Mail Accounts", "url_name": "mail:account_list"},
]
```

6. Run migrations:
```bash
python manage.py migrate
```

7. Extend the base template:
```html
{% extends "themis/base.html" %}

{% block nav_items %}
<li><a href="{% url 'dashboard' %}">Dashboard</a></li>
{% endblock %}

{% block content %}
<h1 class="text-2xl font-bold">My App</h1>
{% endblock %}
```

## Casdoor SSO (optional)

Themis ships generic django-allauth adapters for Casdoor OIDC. They map the
Casdoor `groups` claim to Django groups, set `is_staff` from configurable
staff groups, and block superusers from SSO (they must use local auth).

```python
SOCIALACCOUNT_ADAPTER = "themis.adapters.CasdoorAccountAdapter"
ACCOUNT_ADAPTER = "themis.adapters.LocalAccountAdapter"
```

With `ALLOW_LOCAL_LOGIN` off the login page holds one button and nothing else,
so Themis ships a pass-through that skips it. Wire it ahead of the allauth
include — both answer the `account_login` name and Django takes the first match
— and turn off allauth's "Continue" interstitial, or the redirect lands on it
(system check `themis.W006`):

```python
from themis import views as themis_views

urlpatterns = [
    path("accounts/login/", themis_views.login, name="account_login"),
    path("accounts/", include("allauth.urls")),
]

SOCIALACCOUNT_LOGIN_ON_GET = True
```

Optional settings:

- `THEMIS_STAFF_GROUPS` — Casdoor groups granting `is_staff`
  (default `["staff", "sme", "admin"]`).
- `THEMIS_GROUP_MAPPING` — Casdoor group name → Django group name.
- `THEMIS_SSO_PROVIDER_ID` — the `provider_id` the login pass-through redirects
  to (default `"casdoor"`, the estate-standard value in every app's
  `SOCIALACCOUNT_PROVIDERS`).
- `THEMIS_ORG_ADAPTER` — dotted path to `fn(user, org_identifier)` invoked
  with the Casdoor `organization` claim, for apps with an Organization model.
  Omit it (the default) to skip organization mapping.
- `THEMIS_TENANT_ADAPTER` — store mode (Agora ADR-0004): dotted path to
  `fn(user, tenant_id, groups, tenant_name="")` invoked with the `tenant_id`
  claim, the bare own-org group names and the subscriber's display name from
  the `tenant_name` claim, instead of the org adapter, whenever the
  `tenant_id` claim is present. An adapter written against the
  three-argument contract of 1.16.0 is detected by its signature and still
  called with three arguments. Helpers for the projected groups live in
  `themis.tenancy`.
- `THEMIS_REQUIRED_GROUP_PREFIX` — a product slug; an SSO login holding no
  `<prefix>` / `<prefix>-*` group is refused before provisioning and sent to
  `themis:no-subscription`, which links to `THEMIS_STORE_URL` (required with
  the prefix; system check `themis.E001`). See
  `docs/Pattern_SaaS-Tenancy_V1-04.md` § Store mode.
- `THEMIS_ENCRYPTION_KEY` — any non-empty string; Themis hashes it, so no
  format is required, but it is the passphrase for every stored `UserAPIKey`
  and a long random value is what counts. Generate one with
  `python -c "import secrets; print(secrets.token_urlsafe(48))"`. Required,
  from the environment, no default (system check `themis.E006`). Identical
  across every process/container of one deployment, and never changed once
  `UserAPIKey` rows exist: rows are readable only under the value that wrote
  them, a row that no longer opens shows as `undecryptable` in the admin list
  and on the profile page (and logs `event=api_key_undecryptable`), and the
  only remedy is reissuing the keys. Independent of `SECRET_KEY` by design;
  upgrading from a release that derived the key from `SECRET_KEY`: set it to
  the `SECRET_KEY` value in force at the time to keep stored keys readable.
  Since 1.17.0 the value is stretched with PBKDF2-HMAC-SHA256 instead of being
  hashed once, so a deployment upgrading from an earlier Themis must run
  `python manage.py rekey_api_keys` once — the setting is unchanged but the key
  it derives is, and until the command runs every stored key reads
  `undecryptable`.
  See `docs/Themis_V1-00.md` § UserAPIKey.
- `THEMIS_PROFILE_SOURCE` — `"local"` (default) keeps preferences on the
  app's own settings page; `"claims"` links the app to the estate's Agora
  (ADR-0018): the projected preference claims (`timezone`, `date_format`,
  `theme_mode`, …) are copied into `UserProfile` at every login — a missing
  claim leaves its field alone — and the settings page turns read-only with
  a "Manage in Agora" link to `{AGORA_URL}/profile/`. `AGORA_URL` is
  required with `"claims"` (system check `themis.E002`; environment value,
  no default). See `docs/Pattern_SSO-Allauth-Casdoor_V1-05.md` § Profile
  Claims.
- `AGORA_URL`, `AGORA_PRODUCT_API_KEY` — the entitlements client
  (`themis.entitlements`: `get_limits`, `check_quota`, `quota_blocked`,
  `report_usage`, `flush_usage`). Both from the environment, no defaults, set
  together (system check `themis.E004`); the usage PUT is named by
  `THEMIS_REQUIRED_GROUP_PREFIX` (`themis.E005`). Optional:
  `THEMIS_TENANT_RESOLVER` (dotted path `fn(request) -> tenant id`; default
  reads `request.tenant.uuid`), `THEMIS_QUOTA_WARN_RATIO` (0.8). Add
  `themis.context_processors.entitlements` for the warn banner and schedule
  `manage.py flush_usage` every minute. See `docs/Pattern_Entitlements_V1-00.md`.
- `THEMIS_DISTRIBUTION` — this app's `[project] name` from its own
  `pyproject.toml` (e.g. `"kairos"`). Themis exports
  `<app>_build_info{version="..."} 1` on the Prometheus registry at startup,
  reading the version from the installed package metadata, so each
  environment self-reports the release it runs. Unset, nothing is registered
  and system check `themis.W004` says so (only where `prometheus_client` is
  installed). An in-repo constant, not an environment value. See
  `docs/Pattern_Observability_V2-02.md` § Required Interface.
- `THEMIS_GIT_SHA` — the commit the image was built from; adds a `git_sha`
  label to `<app>_build_info`. From the image build
  (`--build-arg GIT_SHA=$(git rev-parse HEAD)`), never from the deploy, no
  default. Empty with `DEBUG` off, system check `themis.W005` says so.

### Reconnecting an account after an IdP reset

OIDC matches a returning user on the subject id allauth stored as
`SocialAccount.uid` — for Casdoor, the user's `id`. Re-provisioning Casdoor
re-issues those ids, so every stored connection is orphaned and SSO login
falls through to auto-signup. Re-point it:

```bash
# Preview the change (old -> new uid), write nothing
python manage.py reconnect_socialaccount user@example.com --uid <new-casdoor-id> --dry-run

python manage.py reconnect_socialaccount user@example.com --uid <new-casdoor-id>
```

Resolves the user by username or email, creates the connection when none
exists, no-ops when the uid already matches, and refuses when that uid
belongs to another user. `--provider` defaults to `casdoor` — the
`provider_id` allauth stores, not `openid_connect`. Full procedure, including
how to do it without this command: [SSO with Allauth & Casdoor](docs/Pattern_SSO-Allauth-Casdoor_V1-05.md#recovering-from-an-idp-reset).

For sandbox environments with self-signed Casdoor certs, import the SSL bypass
at the top of `settings.py` **before** any requests are made (activates only
when `CASDOOR_SSL_VERIFY=false`):

```python
import themis.ssl_patch  # noqa: F401
```

## Development

Run the test suite from the repo root:

```bash
source ~/env/themis/bin/activate
DJANGO_SETTINGS_MODULE=themis.tests.settings python -m django test themis
```

Sweep every repo under `~/git` for stale or diverged copies of the pattern
library (patterns live only in this repo's `docs/` — themis#67):

```bash
tools/pattern_drift.sh
# or: tools/pattern_drift.sh /path/to/root
# or: PATTERN_DRIFT_ROOT=/path/to/root tools/pattern_drift.sh
```

Exits non-zero and lists every `repo:path` hit, tagged `IDENTICAL`,
`DIVERGED` (same filename and version as the Themis original, different
content — the dangerous case), `STALE-VERSION` (an old version copied in),
or `UNKNOWN-PATTERN`. Clean (no hits) exits 0.

## Documentation

- **[Themis App](docs/Themis_V1-00.md)** — Full documentation
- **[SSO with Allauth & Casdoor](docs/Pattern_SSO-Allauth-Casdoor_V1-05.md)** — Adapters, claim mapping, and IdP-reset recovery
- **[Notification Trigger Pattern](docs/Pattern_Notification_V1-00.md)** — How to trigger notifications from your app
- **[Organization Pattern](docs/Pattern_Organization_V1-00.md)** — Standard Organization model pattern
- **[Ansible Deployment Pattern](docs/Pattern_Ansible-Deploy_V1-00.md)** — Compose + nginx on the private estates
- **[Kubernetes Deployment Pattern](docs/Pattern_Kubernetes_V1-00.md)** — `charts/themis-app` for the public tier; WhiteNoise static standard (`themis.settings.static`)
- **[Red Panda Standards](docs/Red%20Panda%20Standards_Django_V1-00.md)** — Django development standards
- **[Documentation Style Guide](docs/DocumentationStyleGuide_Markdown_V1-00.md)** — Documentation standards

## 🐾 Red Panda Approval™

This project follows Red Panda Approval standards — our gold standard for Django application quality.

### The 5 Sacred Django Criteria
1. **Fresh Migration Test** — Clean migrations from empty database
2. **Elegant Simplicity** — No unnecessary complexity
3. **Observable & Debuggable** — Proper logging and error handling
4. **Consistent Patterns** — Follow Django conventions
5. **Actually Works** — Passes all checks and serves real user needs

## License

MIT License — see [LICENSE](LICENSE) file for details.

## Author

Robert Helewka <r@helu.ca>

## Package Information

- **Package Name:** django-heluca-themis
- **Django:** >=5.2, <6.0
- **Python:** >=3.10
