feat(desktop): runtime-configurable backend server address
The desktop shell is one generic .deb/.AppImage distributed via mca/public_releases, not built per-booth, but the backend origin was baked in at build time (VITE_API_BASE, hardcoded to http://127.0.0.1:3000) — the same installer could never point at a different appliance without a rebuild. Adds ConnectScreen (shown before Login in Tauri when no backend is saved), backed by tauri-plugin-store persisting the operator-entered URL across restarts. CSP's connect-src tightens to 'self' only — all backend traffic already routes through tauri-plugin-http/websocket, which run Rust-side and are outside connect-src's reach anyway — and the real access boundary moves to capabilities/default.json's http:default scope, wildcarded so an operator-chosen host is actually reachable. Adds a "Change server" control in Setup (desktop-only) to repoint an already-configured install. While tracing the desktop auth path for this: tauri-plugin-http's fetch() runs through Rust's reqwest, which keeps its own cookie jar separate from the webview, so document.cookie on tauri://localhost never sees the parking_csrf cookie the server sets (open upstream bug, tauri-apps/tauri#13045/#11518). This means the desktop app has likely been silently sending no CSRF header on every mutation since the shell was first built — pre-existing, independent of this change. Fixed by having sessionView() (routes/auth.ts) also echo the CSRF value in the login/me JSON body; the desktop client stashes it in memory and echoes that instead of reading document.cookie. assertCsrf() itself is untouched. Verified end-to-end against a real LAN-bound dev server: login returns a csrfToken matching the cookie, a mutation using the body-sourced token in X-CSRF-Token succeeds (200), and the same mutation without it still correctly 403s.
This commit is contained in:
@@ -130,6 +130,55 @@ function DesktopVersionBadge() {
|
||||
return <span className="ml-auto shrink-0 pl-3 text-[0.7rem] text-term-muted">app v{version}</span>;
|
||||
}
|
||||
|
||||
/** Desktop-only "change which server this install talks to" control. No-op /
|
||||
* renders nothing in a browser (the concept doesn't apply — same-origin).
|
||||
* Simplest correct action: clear the saved backend URL and reload, which
|
||||
* drops the app back to ConnectScreen (see App.tsx) to re-enter it — this
|
||||
* mirrors clearing the session (logout → back to Login), not an inline
|
||||
* editor, since repointing the app is a rare, deliberate admin action. */
|
||||
function DesktopServerButton() {
|
||||
const { t } = useTranslation();
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
if (!inTauri()) return null;
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-sm ml-2"
|
||||
onClick={() => setConfirming(true)}
|
||||
>
|
||||
{t("connect.changeServer")}
|
||||
</button>
|
||||
{confirming && (
|
||||
<Modal open onClose={() => setConfirming(false)} title={t("connect.changeServer")} width="max-w-sm">
|
||||
<div className="text-[0.8125rem]">
|
||||
<p className="text-term-muted">{t("connect.changeServerConfirm")}</p>
|
||||
<div className="mt-3 flex justify-end gap-2">
|
||||
<button type="button" className="btn btn-sm" onClick={() => setConfirming(false)} disabled={busy}>
|
||||
{t("subs.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-danger"
|
||||
disabled={busy}
|
||||
onClick={async () => {
|
||||
setBusy(true);
|
||||
const { clearBackendUrl } = await import("./lib/backend-config.js");
|
||||
await clearBackendUrl();
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
{busy ? <Spinner /> : t("connect.changeServer")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Setup layout — the config hub. Renders a permission-gated tab bar and the active
|
||||
* tab's screen via <Outlet>. Each tab is a child route (its own URL + guard), so
|
||||
* deep links and the back button work and a denied tab redirects to the booth. */
|
||||
@@ -150,6 +199,7 @@ function SetupLayout() {
|
||||
{show("backup:read") && <SetupTab to="/setup/backup" label={t("nav.backup")} />}
|
||||
{show("site:read") && <VersionBadge />}
|
||||
<DesktopVersionBadge />
|
||||
<DesktopServerButton />
|
||||
</nav>
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user