5c6a21e2c3
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.
87 lines
3.9 KiB
TypeScript
87 lines
3.9 KiB
TypeScript
// Desktop auto-update — prompt-on-update flow.
|
|
//
|
|
// Runs ONLY inside the Tauri desktop shell; a plain browser has no updater, so
|
|
// this is a guarded no-op there. On launch it checks the configured update
|
|
// endpoint (tauri.conf.json → plugins.updater); if a signed newer version is
|
|
// available it asks the operator, then downloads + installs and relaunches.
|
|
//
|
|
// The plugins are imported dynamically so the browser build never bundles them
|
|
// and never tries to resolve the Tauri APIs. Offline-first: a failed check (no
|
|
// network — the appliance is usually offline) is swallowed; updates only happen
|
|
// when someone has brought the box online (e.g. a phone hotspot) on purpose.
|
|
//
|
|
// A release build's console.error is invisible with no way to attach devtools
|
|
// in the field (kiosk mode blocks the context menu; this WebKitGTK build's
|
|
// remote inspector doesn't answer standard discovery endpoints either — both
|
|
// confirmed dead ends 2026-09-03). logClient() ships straight to the
|
|
// server-side app_logs store regardless of the client's console-forward log
|
|
// level (that gate is meant for noisy console chatter, not this), so a real
|
|
// post-accept install failure is visible via wiki/concepts/app-logs.md /
|
|
// LogsViewer.tsx without needing a terminal or devtools at all.
|
|
|
|
import { logClient } from "./logger.js";
|
|
import { inTauri } from "./tauri-env.js";
|
|
|
|
export interface UpdatePrompt {
|
|
/** Newer version string offered by the server. */
|
|
version: string;
|
|
/** Release notes, if the server provided them. */
|
|
notes?: string;
|
|
}
|
|
|
|
/**
|
|
* Check for an update. If one is available, calls `confirm` (your UI) with the
|
|
* version/notes; when it resolves true, downloads + installs and relaunches.
|
|
* No-op (resolves silently) in the browser or when no update / offline.
|
|
*/
|
|
export async function checkForDesktopUpdate(
|
|
confirm: (info: UpdatePrompt) => Promise<boolean>,
|
|
): Promise<void> {
|
|
if (!inTauri()) return;
|
|
try {
|
|
const { check } = await import("@tauri-apps/plugin-updater");
|
|
const update = await check();
|
|
if (!update) return; // up to date
|
|
|
|
const accepted = await confirm({ version: update.version, notes: update.body });
|
|
if (!accepted) return;
|
|
|
|
// Download + install the signed update (signature verified against the
|
|
// pubkey in tauri.conf.json), then relaunch into the new version.
|
|
try {
|
|
await update.downloadAndInstall((progress) => {
|
|
logClient({
|
|
level: "info",
|
|
message: `desktop update download progress: ${progress.event}`,
|
|
context: { kind: "desktop_update_progress", version: update.version, event: progress.event },
|
|
});
|
|
});
|
|
} catch (err) {
|
|
// A real update WAS found and accepted — this is a genuine install
|
|
// failure (bad signature, corrupted download, disk/permission issue),
|
|
// not "offline". Surface it instead of silently reverting to the old
|
|
// version with no explanation.
|
|
logClient({
|
|
level: "error",
|
|
message: `desktop update download/install failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
stack: err instanceof Error ? err.stack : undefined,
|
|
context: { kind: "desktop_update_install_failed", version: update.version },
|
|
});
|
|
throw err;
|
|
}
|
|
const { relaunch } = await import("@tauri-apps/plugin-process");
|
|
await relaunch();
|
|
} catch (err) {
|
|
// Offline / endpoint unreachable / no update server yet → ignore. The app
|
|
// keeps running on the current version; checking again next launch. Still
|
|
// log it (info, not error — this path is expected/normal far more often
|
|
// than it's a real problem) so a real install failure (rethrown above,
|
|
// logged as error) isn't lost among routine offline checks.
|
|
logClient({
|
|
level: "info",
|
|
message: `desktop update check/apply skipped: ${err instanceof Error ? err.message : String(err)}`,
|
|
context: { kind: "desktop_update_skipped" },
|
|
});
|
|
}
|
|
}
|