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:
Generated
+29
@@ -2305,6 +2305,7 @@ dependencies = [
|
||||
"tauri-build",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-store",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-websocket",
|
||||
]
|
||||
@@ -3793,6 +3794,22 @@ dependencies = [
|
||||
"tauri-plugin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-store"
|
||||
version = "2.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6708afbe549f176b712066e71648ba8fafba20789453718260c7ca356733cb0c"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.10.1"
|
||||
@@ -4301,9 +4318,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
|
||||
@@ -33,6 +33,10 @@ tauri-plugin-http = "2"
|
||||
# (ws://127.0.0.1:3000 from the secure tauri://localhost origin) — HTTP and WS
|
||||
# are separate browser checks, so this needs its own plugin.
|
||||
tauri-plugin-websocket = "2"
|
||||
# Persists the operator-configured backend URL (host:port of the Fastify
|
||||
# server this install talks to) across restarts. Read before any API call —
|
||||
# see apps/web/src/lib/backend-config.ts.
|
||||
tauri-plugin-store = "2"
|
||||
|
||||
[features]
|
||||
# Used by `tauri dev`/CLI for hot-reload of the Rust side.
|
||||
|
||||
@@ -8,11 +8,15 @@
|
||||
"updater:default",
|
||||
"process:default",
|
||||
"websocket:default",
|
||||
"store:default",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"//": "Backend address is operator-configured at runtime (backend-config.ts) so the exact host:port can't be allow-listed at build time. Wildcarded to any host — the CSP forces ALL backend traffic through this plugin (see tauri.conf.json), so this scope is the real boundary; a compromised/malicious page still can't reach anything the operator hasn't pointed the app at, since the app only ever calls the one configured origin. All 4 forms needed: a known Tauri scope-matching quirk drops http://*:PORT unless both bare and :* variants are listed.",
|
||||
"allow": [
|
||||
{ "url": "http://127.0.0.1:3000" },
|
||||
{ "url": "http://localhost:3000" }
|
||||
{ "url": "http://*" },
|
||||
{ "url": "https://*" },
|
||||
{ "url": "http://*:*" },
|
||||
{ "url": "https://*:*" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
// Intentionally minimal: build the default Tauri app and run it. The window
|
||||
// config (kiosk, fullscreen, which URL/assets to load) lives in tauri.conf.json.
|
||||
// No custom commands are registered — the renderer (the @parking/web SPA) reaches
|
||||
// the backend over HTTP to the local Fastify server, NOT through Tauri IPC. This
|
||||
// keeps the shell a thin presentation wrapper with a deny-by-default native
|
||||
// surface (see wiki/decisions/desktop-shell-tauri.md).
|
||||
// the backend over HTTP to a Fastify server (address operator-configured at
|
||||
// runtime, not baked in — see apps/web/src/lib/backend-config.ts), NOT through
|
||||
// Tauri IPC. This keeps the shell a thin presentation wrapper with a
|
||||
// deny-by-default native surface (see wiki/decisions/desktop-shell-tauri.md).
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
@@ -16,13 +17,16 @@ pub fn run() {
|
||||
// endpoint + signing pubkey live in tauri.conf.json.
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
// Routes the SPA's fetch()/WS calls to the local Fastify server through
|
||||
// Tauri's native HTTP client — see the Cargo.toml comment on why the
|
||||
// webview's own fetch() can't reach http://127.0.0.1:3000 directly.
|
||||
// Routes the SPA's fetch()/WS calls to the operator-configured Fastify
|
||||
// server through Tauri's native HTTP client — see the Cargo.toml
|
||||
// comment on why the webview's own fetch() can't reach it directly.
|
||||
.plugin(tauri_plugin_http::init())
|
||||
// Live-feed WebSocket — same mixed-content reason as the HTTP plugin
|
||||
// above, but WS needs its own plugin (separate browser check).
|
||||
.plugin(tauri_plugin_websocket::init())
|
||||
// Persists the operator-configured backend URL across restarts (JSON
|
||||
// file in the app's config dir) — see backend-config.ts.
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running the Parking System desktop shell");
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"devUrl": "http://localhost:5173",
|
||||
"frontendDist": "../../web/dist",
|
||||
"beforeDevCommand": "pnpm --filter @parking/web dev",
|
||||
"beforeBuildCommand": "VITE_API_BASE=http://127.0.0.1:3000 pnpm --filter @parking/web build"
|
||||
"beforeBuildCommand": "pnpm --filter @parking/web build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
@@ -24,7 +24,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; connect-src 'self' http://127.0.0.1:3000 http://localhost:3000 ws://127.0.0.1:3000 ws://localhost:3000"
|
||||
"csp": "default-src 'self'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; connect-src 'self'"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
|
||||
Reference in New Issue
Block a user