From 7a63fb94884c38b807b853b2a886bebf622f9689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Rolf?= Date: Thu, 1 Oct 2026 12:40:58 +0200 Subject: [PATCH] =?UTF-8?q?feat(template):=20auth=200.2.0=20+=20ui=200.7.0?= =?UTF-8?q?=20=E2=80=94=20basicbar=5Fauth.urls,=20whoami=5Fpayload,=20Disc?= =?UTF-8?q?overy-Fail-fast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pins auf auth/v0.2.0 und ui/v0.7.0. config/urls.py bindet basicbar_auth.urls ein (OIDC-Routen inkl. tolerantem Callback, api/whoami/language/); accounts/views.py enthält nur noch whoami als {**whoami_payload(request), …Feature-Flags}; accounts.User erbt language. Settings brechen produktiv mit ImproperlyConfigured ab, wenn OIDC_OP_ISSUER gesetzt ist, die Discovery aber keine Endpunkte lieferte. Nagelprobe: Render (use_lti, github), ruff, Frontend-Build (280 kB ohne TipTap), Backend im Container gegen Postgres: check, makemigrations --check leer, Tests grün, Guard wirft ohne DEBUG und schweigt mit DEBUG. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 9 ++ template/project/backend/accounts/models.py | 7 +- template/project/backend/accounts/views.py | 85 ++++--------------- .../project/backend/config/settings.py.jinja | 11 +++ template/project/backend/config/urls.py.jinja | 14 +-- .../project/backend/requirements.txt.jinja | 2 +- template/project/frontend/package.json.jinja | 2 +- 7 files changed, 42 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8805a93..5027e6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -142,6 +142,15 @@ Uhren zwischen IdP und Tool ggf. `OIDC_BACKCHANNEL_MAX_AGE` erhöhen. ### Template +- **Pins:** basicbar-auth `auth/v0.2.0`, @basicbar/ui `ui/v0.7.0`. +- `config/urls.py` nutzt `include("basicbar_auth.urls")` (OIDC-Routen inkl. + tolerantem Callback, `api/whoami/language/`); `accounts/views.py` enthält + nur noch `whoami` als `{**whoami_payload(request), …Feature-Flags}` — + `logout_view`/`set_language` kommen aus dem Paket. `accounts.User` hat kein + eigenes `language` mehr (geerbt, Migration unverändert). +- Settings brechen produktiv mit `ImproperlyConfigured` ab, wenn + `OIDC_OP_ISSUER` gesetzt ist, die Discovery aber keine Endpunkte lieferte + (sonst scheitert jeder Login später mit unklarem Fehler). - `REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES` nur noch `SessionAuthentication` — DRFs Default aktiviert `BasicAuthentication`, die mit dem Break-glass-Superuser (ModelBackend) jeden Endpunkt für diff --git a/template/project/backend/accounts/models.py b/template/project/backend/accounts/models.py index d0d9932..6a00681 100644 --- a/template/project/backend/accounts/models.py +++ b/template/project/backend/accounts/models.py @@ -8,14 +8,11 @@ normal migrations. """ from basicbar_auth.models import AbstractBasicUser -from django.db import models class User(AbstractBasicUser): - """Application user (``subject``/``claims`` come from AbstractBasicUser).""" - - # Preferred UI language ("en"/"de"), set from the SPA; blank = site default. - language = models.CharField(max_length=10, blank=True) + """Application user (``subject``, ``claims`` and the UI ``language`` come + from AbstractBasicUser).""" def __str__(self): return self.get_username() diff --git a/template/project/backend/accounts/views.py b/template/project/backend/accounts/views.py index ef74a8b..eeae30b 100644 --- a/template/project/backend/accounts/views.py +++ b/template/project/backend/accounts/views.py @@ -1,84 +1,29 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2026 Universität Osnabrück (virtUOS) -"""Session/identity endpoints for the SPA.""" -import json +"""Session/identity endpoints for the SPA. -from basicbar_auth.oidc import provider_logout_url +``logout_view`` and ``set_language`` come from basicbar-auth (wired by +``basicbar_auth.urls``); only ``whoami`` is the tool's own, because it adds +the tool's feature flags to the shared payload. +""" +from basicbar_auth.views import whoami_payload from basicbar_integrations import ai, translation_service from django.conf import settings -from django.contrib.auth import logout as django_logout from django.http import JsonResponse -from django.middleware.csrf import get_token -from django.shortcuts import redirect -from django.views.decorators.http import require_POST def whoami(request): - """Return the current session user (for the SPA to check login state). - - Also returns the CSRF token in the body: ``get_token`` sets the cookie - *and* hands the SPA an authoritative token, so unsafe requests work even - cross-origin in dev, where reading the cookie from JavaScript can be - unreliable (production is same-origin behind Caddy).""" - csrf_token = get_token(request) - user = request.user - # Content-i18n config: the default/canonical authoring language and - # whether machine-translation drafts are available, so the SPA can - # decide which language to show/edit without a second round-trip. - common = { - "csrf_token": csrf_token, - "ai_enabled": ai.is_enabled(), - "content_default_language": settings.MODELTRANSLATION_DEFAULT_LANGUAGE, - "content_translation_enabled": translation_service.is_enabled(), - } - if not user.is_authenticated: - return JsonResponse({"authenticated": False, **common}) + """Return the current session user (for the SPA to check login state), + plus the feature flags the SPA needs before its first real request.""" return JsonResponse( { - "authenticated": True, - "username": user.get_username(), - "first_name": user.first_name, - "last_name": user.last_name, - "email": user.email, - "subject": user.subject, - "is_staff": user.is_staff, - "language": user.language, - **common, + **whoami_payload(request), + "ai_enabled": ai.is_enabled(), + # Content-i18n config: the default/canonical authoring language and + # whether machine-translation drafts are available, so the SPA can + # decide which language to show/edit without a second round-trip. + "content_default_language": settings.MODELTRANSLATION_DEFAULT_LANGUAGE, + "content_translation_enabled": translation_service.is_enabled(), } ) - - -def logout_view(request): - """Log out of Django and (if logged in via OIDC) the identity provider. - - GET-friendly so the SPA can trigger it with a plain redirect. - """ - was_authenticated = request.user.is_authenticated - end_session_url = None - if was_authenticated and settings.OIDC_OP_LOGOUT_ENDPOINT: - end_session_url = provider_logout_url(request) - django_logout(request) - return redirect(end_session_url or settings.LOGOUT_REDIRECT_URL) - - -@require_POST -def set_language(request): - """POST /api/whoami/language/ {language} — remember the user's UI language. - - Plain Django view (matches ``whoami``); the SPA sends its CSRF token from - ``whoami`` so the request passes CSRF as elsewhere. - """ - if not request.user.is_authenticated: - return JsonResponse({"detail": "Not authenticated."}, status=403) - try: - data = json.loads(request.body or b"{}") - except ValueError: - data = {} - raw = data.get("language") if isinstance(data, dict) else "" - language = (str(raw) if raw else "").strip() - if language not in dict(settings.LANGUAGES): - return JsonResponse({"detail": "Unsupported language."}, status=400) - request.user.language = language - request.user.save(update_fields=["language"]) - return JsonResponse({"language": language}) diff --git a/template/project/backend/config/settings.py.jinja b/template/project/backend/config/settings.py.jinja index f8f0fdc..31fed9c 100644 --- a/template/project/backend/config/settings.py.jinja +++ b/template/project/backend/config/settings.py.jinja @@ -262,6 +262,17 @@ if OIDC_OP_ISSUER and not OIDC_OP_AUTHORIZATION_ENDPOINT: OIDC_OP_JWKS_ENDPOINT = OIDC_OP_JWKS_ENDPOINT or _discovered.get("jwks_uri", "") OIDC_OP_LOGOUT_ENDPOINT = OIDC_OP_LOGOUT_ENDPOINT or _discovered.get("end_session_endpoint", "") +# Fail fast in production: with an issuer configured but no endpoints (the +# discovery document was unreachable or incomplete — see the WARNING from +# basicbar_auth.discovery), every login would otherwise fail later with an +# opaque error. In DEBUG the dev stack may simply not have Keycloak up yet. +if OIDC_OP_ISSUER and not DEBUG and not (OIDC_OP_AUTHORIZATION_ENDPOINT and OIDC_OP_TOKEN_ENDPOINT): + raise ImproperlyConfigured( + f"OIDC_OP_ISSUER={OIDC_OP_ISSUER!r} is set, but the OIDC endpoints could not be " + "resolved — the provider's discovery document was unreachable. Set the " + "OIDC_OP_*_ENDPOINT variables explicitly or fix the issuer URL." + ) + OIDC_OP_LOGOUT_URL_METHOD = "basicbar_auth.oidc.provider_logout_url" # Claim mapping (provider-agnostic; defaults are standard OIDC claim names). diff --git a/template/project/backend/config/urls.py.jinja b/template/project/backend/config/urls.py.jinja index 2d96902..dd201e7 100644 --- a/template/project/backend/config/urls.py.jinja +++ b/template/project/backend/config/urls.py.jinja @@ -2,26 +2,18 @@ # Copyright 2026 Universität Osnabrück (virtUOS) """Root URL configuration.""" -from basicbar_auth.oidc import SilentLoginView, backchannel_logout from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path -from accounts.views import logout_view, set_language, whoami +from accounts.views import whoami urlpatterns = [ path("admin/", admin.site.urls), - path("oidc/logout-redirect/", logout_view, name="spa-logout"), - path("oidc/silent/", SilentLoginView.as_view(), name="oidc-silent"), - path( - "oidc/backchannel-logout/", - backchannel_logout, - name="oidc-backchannel-logout", - ), - path("oidc/", include("mozilla_django_oidc.urls")), + # OIDC login/logout/silent/back-channel/callback and api/whoami/language/. + path("", include("basicbar_auth.urls")), path("api/whoami/", whoami), - path("api/whoami/language/", set_language), ] # Serve uploaded media and static files during local development. (Static diff --git a/template/project/backend/requirements.txt.jinja b/template/project/backend/requirements.txt.jinja index 7b9c5db..65869ef 100644 --- a/template/project/backend/requirements.txt.jinja +++ b/template/project/backend/requirements.txt.jinja @@ -3,7 +3,7 @@ djangorestframework==3.15.2 # Gemeinsame Basis der -bar-Tools; Versionswechsel = Tag in der URL heben, # Migrationsschritte im CHANGELOG von https://github.com/virtUOS/basicbar. basicbar-integrations @ https://github.com/virtUOS/basicbar/archive/refs/tags/integrations/v0.2.2.tar.gz#subdirectory=packages/django/basicbar-integrations -basicbar-auth @ https://github.com/virtUOS/basicbar/archive/refs/tags/auth/v0.1.1.tar.gz#subdirectory=packages/django/basicbar-auth +basicbar-auth @ https://github.com/virtUOS/basicbar/archive/refs/tags/auth/v0.2.0.tar.gz#subdirectory=packages/django/basicbar-auth {% if use_lti -%} basicbar-lti @ https://github.com/virtUOS/basicbar/archive/refs/tags/lti/v0.1.4.tar.gz#subdirectory=packages/django/basicbar-lti {% endif -%} diff --git a/template/project/frontend/package.json.jinja b/template/project/frontend/package.json.jinja index 9c85013..e829d9f 100644 --- a/template/project/frontend/package.json.jinja +++ b/template/project/frontend/package.json.jinja @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "@basicbar/ui": "https://github.com/virtUOS/basicbar/releases/download/ui/v0.6.0/basicbar-ui-0.6.0.tgz", + "@basicbar/ui": "https://github.com/virtUOS/basicbar/releases/download/ui/v0.7.0/basicbar-ui-0.7.0.tgz", "@fontsource-variable/plus-jakarta-sans": "^5.2.8", "i18next": "^26.3.1", "i18next-browser-languagedetector": "^8.2.1",