Compare commits
2 Commits
80dd1e7c7d
...
d90dda1df5
| Author | SHA1 | Date | |
|---|---|---|---|
| d90dda1df5 | |||
| 425acebe0d |
@@ -2016,7 +2016,16 @@ impl Launcher {
|
|||||||
|
|
||||||
if let Some(url) = config.web_url() {
|
if let Some(url) = config.web_url() {
|
||||||
self.log(app, "info", format!("The web client is at {url}"));
|
self.log(app, "info", format!("The web client is at {url}"));
|
||||||
if let Err(e) = app.opener().open_url(url.as_str(), None::<&str>) {
|
// Signed in the way the .exe is, so live mode reaches the browser too. The token
|
||||||
|
// rides only in the address handed to the browser, never in the line logged above.
|
||||||
|
let opened = match stack::mint_web_token(&self.http, &config).await {
|
||||||
|
Ok(token) => config.web_url_signed(&token).unwrap_or(url),
|
||||||
|
Err(e) => {
|
||||||
|
self.log(app, "warn", format!("The web client opens without a session ({e}), so it shows this PC's data only."));
|
||||||
|
url
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = app.opener().open_url(opened.as_str(), None::<&str>) {
|
||||||
// Not a failure: the address is in the log and in the panel, and a developer can
|
// Not a failure: the address is in the log and in the panel, and a developer can
|
||||||
// paste it. Refusing the whole launch over a browser that would not open would be.
|
// paste it. Refusing the whole launch over a browser that would not open would be.
|
||||||
self.log(app, "warn", format!("Could not open a browser: {e}. Open the address above yourself."));
|
self.log(app, "warn", format!("Could not open a browser: {e}. Open the address above yourself."));
|
||||||
|
|||||||
@@ -282,6 +282,17 @@ impl StackConfig {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `web_url` with a session token for the web client, in the FRAGMENT.
|
||||||
|
///
|
||||||
|
/// The local API relays to the DHP only a request carrying a token it minted itself -
|
||||||
|
/// the forwarder's guard against being an open proxy - so a tokenless web client stays
|
||||||
|
/// on this PC's database while the .exe beside it is live. A fragment never leaves the
|
||||||
|
/// browser: not in the request line, not in Vite's log. Never log this address;
|
||||||
|
/// `web_url` is the one to print.
|
||||||
|
pub fn web_url_signed(&self, token: &str) -> Option<String> {
|
||||||
|
self.web_url().map(|url| format!("{url}#token={}", url_value(token)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Everything this run takes from the checkout rather than the install, for the log.
|
/// Everything this run takes from the checkout rather than the install, for the log.
|
||||||
pub fn dev_parts(&self) -> Vec<String> {
|
pub fn dev_parts(&self) -> Vec<String> {
|
||||||
match &self.dev_engine {
|
match &self.dev_engine {
|
||||||
@@ -655,6 +666,31 @@ async fn start_web(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A session token from THIS stack's API for the web client, minted with the secret the
|
||||||
|
/// .exe signs in with and for the same operator - so both clients reach the DHP alike.
|
||||||
|
pub async fn mint_web_token(http: &reqwest::Client, config: &StackConfig) -> Result<String, String> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Minted {
|
||||||
|
access_token: String,
|
||||||
|
}
|
||||||
|
let response = http
|
||||||
|
.post(format!("http://127.0.0.1:{API_PORT}/auth/mint"))
|
||||||
|
.header("x-mint-secret", &config.mint_secret)
|
||||||
|
.json(&serde_json::json!({ "username": config.operator }))
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("the local API did not answer: {e}"))?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(format!("the local API refused it (HTTP {})", response.status().as_u16()));
|
||||||
|
}
|
||||||
|
response
|
||||||
|
.json::<Minted>()
|
||||||
|
.await
|
||||||
|
.map(|minted| minted.access_token)
|
||||||
|
.map_err(|e| format!("the local API's answer made no sense: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
fn failed(emit: Emit<'_>, component: Component, message: String) -> String {
|
fn failed(emit: Emit<'_>, component: Component, message: String) -> String {
|
||||||
emit(StackEvent { component, state: ComponentState::Failed, message: Some(message.clone()) });
|
emit(StackEvent { component, state: ComponentState::Failed, message: Some(message.clone()) });
|
||||||
message
|
message
|
||||||
@@ -1337,6 +1373,22 @@ mod tests {
|
|||||||
assert!(cfg.web_url().unwrap().ends_with("&user=m2op"));
|
assert!(cfg.web_url().unwrap().ends_with("&user=m2op"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_web_token_rides_in_the_fragment_and_never_in_the_printed_address() {
|
||||||
|
let mut cfg = config(&[("dark", "protomaps_20260423")]);
|
||||||
|
cfg.dev_engine = Some(PathBuf::from(r"E:\dev\ODIAN_Engine"));
|
||||||
|
cfg.dev_target = DevTarget::Web;
|
||||||
|
let plain = cfg.web_url().unwrap();
|
||||||
|
let signed = cfg.web_url_signed("a+b/c=").unwrap();
|
||||||
|
// The query is untouched - the token is after `#`, so the browser never sends it.
|
||||||
|
assert_eq!(signed, format!("{plain}#token=a%2Bb%2Fc%3D"));
|
||||||
|
assert!(!plain.contains("token"), "the address the launcher logs carries no token");
|
||||||
|
|
||||||
|
// No web client this run, no address at all.
|
||||||
|
cfg.dev_target = DevTarget::Client;
|
||||||
|
assert_eq!(cfg.web_url_signed("t"), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_web_server_is_started_on_the_port_the_launcher_advertises() {
|
fn the_web_server_is_started_on_the_port_the_launcher_advertises() {
|
||||||
// Checked as ARGUMENTS rather than by reading the binary: these are short literals that
|
// Checked as ARGUMENTS rather than by reading the binary: these are short literals that
|
||||||
|
|||||||
+4
-2
@@ -2,14 +2,16 @@ import { defineConfig } from "vite";
|
|||||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
|
||||||
// Tauri loads the dev server from a fixed port and fails if it is taken, and
|
// Tauri loads the dev server from a fixed port and fails if it is taken, and
|
||||||
// rebuilds the Rust side itself, so Vite must not watch src-tauri.
|
// rebuilds the Rust side itself, so Vite must not watch src-tauri - nor the
|
||||||
|
// workspace's target/ at the repo root, whose DLLs the build holds locked
|
||||||
|
// (watching one crashes Vite with EBUSY).
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [svelte()],
|
plugins: [svelte()],
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
server: {
|
server: {
|
||||||
port: 1420,
|
port: 1420,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
watch: { ignored: ["**/src-tauri/**"] },
|
watch: { ignored: ["**/src-tauri/**", "**/target/**"] },
|
||||||
},
|
},
|
||||||
envPrefix: ["VITE_", "TAURI_ENV_"],
|
envPrefix: ["VITE_", "TAURI_ENV_"],
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
Reference in New Issue
Block a user