Point the panel link at the installation, not at the browser's origin
Both links out of the dashboard editor were built root-relative, so a portal
serving the app under `/i/{id}` got a URL to itself: the hub has no route
there and answers a bare 404. That is what a device link and "open what a
wall panel sees" both landed on.
They want different answers. The view link is for the person already looking,
so it takes the router's basepath — `appPath` in `lib/portal` is the same
prefix the router applies to every `Link`, for the places that step outside
it. The device link is for a screen, which cannot go through the portal at
all: the shell is served only to a portal session, and the credential that
page carries is the portal's rather than the panel's. So the server now says
where it answers, and `FRONTEND_HOST` is that answer — the same setting the
password-reset links already use.
Also fixes the panel branch in the query error handler, which compared a raw
pathname and so never fired under a portal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHpLJHozysQXjsxAyU1WHj
This commit is contained in:
@@ -18,10 +18,11 @@ from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.api.deps import CurrentUser, get_current_active_superuser, get_current_user
|
||||
from app.core import security
|
||||
from app.core.config import settings
|
||||
from app.flow.panels import PanelDef, PanelsConfig, find, read_config, write_config
|
||||
from app.models import Message
|
||||
|
||||
@@ -87,15 +88,38 @@ class PairRequest(BaseModel):
|
||||
code: str
|
||||
|
||||
|
||||
@router.get("/", response_model=PanelsConfig, dependencies=[Depends(get_current_user)])
|
||||
class PanelsPublic(BaseModel):
|
||||
"""The panels, and the address a device should be pointed at.
|
||||
|
||||
The address is the server's own, because the browser's origin is not a
|
||||
reliable answer to it: an admin working through the portal is on the
|
||||
portal's origin, and a screen cannot be sent there — the portal serves a
|
||||
page only to someone holding a portal session, and the credential it hands
|
||||
that page is the portal's rather than the panel's.
|
||||
"""
|
||||
|
||||
panels: list[PanelDef] = Field(default_factory=list)
|
||||
#: Whatever this installation was told it is reachable at. The same setting
|
||||
#: the password-reset links are built from, so an installation that has it
|
||||
#: wrong has it wrong in both places.
|
||||
frontend_host: str = ""
|
||||
|
||||
|
||||
def _public(config: PanelsConfig) -> PanelsPublic:
|
||||
return PanelsPublic(
|
||||
panels=config.panels, frontend_host=settings.FRONTEND_HOST.rstrip("/")
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_model=PanelsPublic, dependencies=[Depends(get_current_user)])
|
||||
async def read_panels() -> Any:
|
||||
"""Every panel, and what each one shows."""
|
||||
return await run_in_threadpool(read_config)
|
||||
"""Every panel, what each one shows, and where to point a device."""
|
||||
return _public(await run_in_threadpool(read_config))
|
||||
|
||||
|
||||
@router.put(
|
||||
"/",
|
||||
response_model=PanelsConfig,
|
||||
response_model=PanelsPublic,
|
||||
dependencies=[Depends(get_current_active_superuser)],
|
||||
)
|
||||
async def save_panels(body: PanelsConfig) -> Any:
|
||||
@@ -113,7 +137,7 @@ async def save_panels(body: PanelsConfig) -> Any:
|
||||
seen.add(panel.id)
|
||||
|
||||
await run_in_threadpool(write_config, body)
|
||||
return body
|
||||
return _public(body)
|
||||
|
||||
|
||||
@router.post("/pair", response_model=PairStarted)
|
||||
|
||||
@@ -56,6 +56,9 @@ def test_assign_and_read_back(
|
||||
|
||||
stored = client.get(f"{PREFIX}/", headers=superuser_token_headers).json()
|
||||
assert stored["panels"][0]["dashboards"] == ["hall_a", "hall_b"]
|
||||
# The address a device is pointed at comes from the server, because the
|
||||
# browser's own origin is the portal's when someone administers remotely.
|
||||
assert stored["frontend_host"] == settings.FRONTEND_HOST.rstrip("/")
|
||||
assert (
|
||||
client.get(f"{PREFIX}/hall", headers=superuser_token_headers).json()["title"]
|
||||
== "Hall"
|
||||
|
||||
Reference in New Issue
Block a user