feat(platform): ship AI fallback stack and entity-scoped course planning

Unifies the new LangGraph-driven course-plan/media flow with robust provider fallbacks, admin AI provider settings, editable book-style materials, and strict entity isolation across LMS/course-plan APIs. Adds admin-only entity membership management in the Entities UI so users can switch linked entities directly from the platform.

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-26 02:34:52 +04:00
parent afd1662a60
commit cd47d01f53
26 changed files with 2795 additions and 338 deletions

View File

@@ -94,12 +94,39 @@ def _get_jwt_secret():
return secret
def validate_token():
"""Decode JWT Bearer token and return the corresponding ``res.users`` record or None."""
def _extract_bearer_token(allow_query_param: bool = False) -> str | None:
"""Pull a JWT off the current request.
By default we only honour the ``Authorization: Bearer …`` header. Some
endpoints (notably media streams that get embedded in ``<img>`` /
``<audio>`` / ``<video>`` tags, where the browser cannot attach custom
headers) opt in to ``allow_query_param=True`` so callers can send the
token via ``?token=<jwt>`` or ``?access_token=<jwt>``. We deliberately
keep this off by default so a leaked URL never gives access to JSON
APIs — only to the specific raw-media route that opts in.
"""
auth_header = request.httprequest.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
if auth_header.startswith("Bearer "):
return auth_header[7:].strip() or None
if allow_query_param:
try:
args = request.httprequest.args
except Exception:
args = {}
token = (args.get("token") or args.get("access_token") or "").strip()
if token:
return token
return None
def validate_token(allow_query_param: bool = False):
"""Decode JWT Bearer token and return the corresponding ``res.users`` record or None.
See :func:`_extract_bearer_token` for the ``allow_query_param`` flag.
"""
token = _extract_bearer_token(allow_query_param=allow_query_param)
if not token:
return None
token = auth_header[7:]
secret = _get_jwt_secret()
if not secret:
_logger.error("System parameter 'encoach.jwt_secret' is not configured")