Compare commits
2 Commits
80dd1e7c7d
...
d90dda1df5
| Author | SHA1 | Date | |
|---|---|---|---|
| d90dda1df5 | |||
| 425acebe0d |
@@ -2016,7 +2016,16 @@ impl Launcher {
|
||||
|
||||
if let Some(url) = config.web_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
|
||||
// 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."));
|
||||
|
||||
@@ -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.
|
||||
pub fn dev_parts(&self) -> Vec<String> {
|
||||
match &self.dev_engine {
|
||||
@@ -655,6 +666,31 @@ async fn start_web(
|
||||
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 {
|
||||
emit(StackEvent { component, state: ComponentState::Failed, message: Some(message.clone()) });
|
||||
message
|
||||
@@ -1337,6 +1373,22 @@ mod tests {
|
||||
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]
|
||||
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
|
||||
|
||||
+4
-2
@@ -2,14 +2,16 @@ import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
// 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({
|
||||
plugins: [svelte()],
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
watch: { ignored: ["**/src-tauri/**"] },
|
||||
watch: { ignored: ["**/src-tauri/**", "**/target/**"] },
|
||||
},
|
||||
envPrefix: ["VITE_", "TAURI_ENV_"],
|
||||
build: {
|
||||
|
||||
Reference in New Issue
Block a user