Add an accent-colored new-node button that opens independent login windows with scoped Tauri permissions

Hop-State: A_06FPHE7W9CXE3QZY70FNMH8
Hop-Proposal: R_06FPHE77PJN045T67QKDFA0
Hop-Task: T_06FPHCF4TTA40Z7GKD237Z8
Hop-Attempt: AT_06FPHCF4TRY8K16J9218WER
This commit is contained in:
2026-07-15 19:19:41 -07:00
committed by Hop
parent e41da3e6c8
commit 7bdd460a52
13 changed files with 250 additions and 27 deletions
+12 -1
View File
@@ -1,3 +1,14 @@
fn main() {
tauri_build::build()
tauri_build::try_build(tauri_build::Attributes::new().app_manifest(
tauri_build::AppManifest::new().commands(&[
"connect",
"login",
"register",
"logout",
"has_web_session",
"open_web_app",
"open_login_window",
]),
))
.unwrap();
}
+4 -3
View File
@@ -1,10 +1,11 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"description": "Capability for local Synapsis windows",
"windows": ["main", "node-*"],
"permissions": [
"core:default",
"notification:default"
"notification:default",
"local-app"
]
}
@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-connect"
description = "Enables the connect command without any pre-configured scope."
commands.allow = ["connect"]
[[permission]]
identifier = "deny-connect"
description = "Denies the connect command without any pre-configured scope."
commands.deny = ["connect"]
@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-has-web-session"
description = "Enables the has_web_session command without any pre-configured scope."
commands.allow = ["has_web_session"]
[[permission]]
identifier = "deny-has-web-session"
description = "Denies the has_web_session command without any pre-configured scope."
commands.deny = ["has_web_session"]
@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-login"
description = "Enables the login command without any pre-configured scope."
commands.allow = ["login"]
[[permission]]
identifier = "deny-login"
description = "Denies the login command without any pre-configured scope."
commands.deny = ["login"]
@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-logout"
description = "Enables the logout command without any pre-configured scope."
commands.allow = ["logout"]
[[permission]]
identifier = "deny-logout"
description = "Denies the logout command without any pre-configured scope."
commands.deny = ["logout"]
@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-open-login-window"
description = "Enables the open_login_window command without any pre-configured scope."
commands.allow = ["open_login_window"]
[[permission]]
identifier = "deny-open-login-window"
description = "Denies the open_login_window command without any pre-configured scope."
commands.deny = ["open_login_window"]
@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-open-web-app"
description = "Enables the open_web_app command without any pre-configured scope."
commands.allow = ["open_web_app"]
[[permission]]
identifier = "deny-open-web-app"
description = "Denies the open_web_app command without any pre-configured scope."
commands.deny = ["open_web_app"]
@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-register"
description = "Enables the register command without any pre-configured scope."
commands.allow = ["register"]
[[permission]]
identifier = "deny-register"
description = "Denies the register command without any pre-configured scope."
commands.deny = ["register"]
+12
View File
@@ -0,0 +1,12 @@
[[permission]]
identifier = "local-app"
description = "Allows bundled Synapsis screens to connect, authenticate, and hand off to a node."
commands.allow = [
"connect",
"login",
"register",
"logout",
"has_web_session",
"open_web_app",
"open_login_window",
]
@@ -0,0 +1,4 @@
[[permission]]
identifier = "allow-open-login-window"
description = "Allows an authenticated node page to open a fresh Synapsis login window."
commands.allow = ["open_login_window"]
+135 -23
View File
@@ -1,15 +1,17 @@
use reqwest::{Client, Method, StatusCode, Url};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::Duration;
use tauri::webview::{Cookie, PageLoadEvent};
use tauri::{Manager, State, WebviewWindow};
use tauri::{Manager, State, Webview, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
struct AppState {
client: Client,
notification_capability_sequence: AtomicU64,
entry_url: Mutex<Option<Url>>,
window_sequence: AtomicU64,
entry_urls: Mutex<HashMap<String, Url>>,
}
#[derive(Debug, Default, Deserialize, Serialize)]
@@ -270,23 +272,21 @@ async fn open_web_app(
) -> Result<(), String> {
let base = normalize_node(&node_url)?;
{
let mut entry_url = state
.entry_url
let mut entry_urls = state
.entry_urls
.lock()
.map_err(|_| "Couldnt remember the app entry screen.".to_string())?;
if entry_url.is_none() {
*entry_url = Some(
webview
.url()
.map_err(|error| format!("Couldnt read the app entry address: {error}"))?,
);
}
entry_urls.entry(webview.label().to_owned()).or_insert(
webview
.url()
.map_err(|error| format!("Couldnt read the app entry address: {error}"))?,
);
}
let origin = base.origin().ascii_serialization();
let capability_id = state
.notification_capability_sequence
.fetch_add(1, Ordering::Relaxed);
let capability = remote_notification_capability(&origin, capability_id);
let capability = remote_node_capability(&origin, webview.label(), capability_id);
webview
.add_capability(capability)
.map_err(|error| format!("Couldnt enable native notifications for this node: {error}"))?;
@@ -295,20 +295,113 @@ async fn open_web_app(
.map_err(|error| format!("Couldnt open the Synapsis web app: {error}"))
}
fn remote_notification_capability(origin: &str, sequence: u64) -> String {
fn remote_node_capability(origin: &str, window_label: &str, sequence: u64) -> String {
serde_json::json!({
"identifier": format!("synapsis-node-notifications-{sequence}"),
"description": "Allow the active Synapsis node to send native desktop notifications",
"identifier": format!("synapsis-node-desktop-{sequence}"),
"description": "Allow an active Synapsis node to use its desktop window controls",
"local": false,
"windows": ["main"],
"windows": [window_label],
"remote": {
"urls": [format!("{origin}/*")]
},
"permissions": ["notification:default"]
"permissions": ["notification:default", "allow-open-login-window"]
})
.to_string()
}
fn new_window_button_script(accent_color: Option<&str>) -> String {
let accent = serde_json::to_string(accent_color.unwrap_or("#7c5cff"))
.expect("serializing an accent color cannot fail");
r##"
(() => {
const id = "synapsis-new-node-window";
const styleId = `${id}-style`;
document.getElementById(id)?.remove();
document.getElementById(styleId)?.remove();
const requestedAccent = __ACCENT__;
const accent = CSS.supports("color", requestedAccent) ? requestedAccent : "#7c5cff";
const style = document.createElement("style");
style.id = styleId;
style.textContent = `
#${id} {
position: fixed; right: 28px; bottom: 28px; z-index: 2147483647;
width: 54px; height: 54px; display: grid; place-items: center;
padding: 0; border: 1px solid rgba(255,255,255,.22); border-radius: 50%;
color: #fff; background: ${accent};
box-shadow: 0 13px 34px rgba(0,0,0,.38), 0 0 22px color-mix(in srgb, ${accent} 38%, transparent);
font: 300 32px/1 -apple-system, BlinkMacSystemFont, sans-serif;
cursor: pointer; -webkit-font-smoothing: antialiased;
transition: transform 160ms ease, filter 160ms ease, opacity 160ms ease;
}
#${id}:hover { transform: translateY(-2px) scale(1.04); filter: brightness(1.08); }
#${id}:active { transform: scale(.96); }
#${id}:focus-visible { outline: 2px solid #fff; outline-offset: 4px; }
#${id}:disabled { cursor: wait; opacity: .62; }
@media (prefers-reduced-motion: reduce) { #${id} { transition: none; } }
`;
const button = document.createElement("button");
button.id = id;
button.type = "button";
button.textContent = "+";
button.setAttribute("aria-label", "Open another Synapsis node window");
button.title = "Open another node";
button.addEventListener("click", async () => {
button.disabled = true;
try {
await window.__TAURI_INTERNALS__.invoke("open_login_window");
} catch (error) {
console.error("Could not open another Synapsis window", error);
} finally {
button.disabled = false;
}
});
document.head.appendChild(style);
document.documentElement.appendChild(button);
})();
"##
.replace("__ACCENT__", &accent)
}
async fn install_new_window_button(client: Client, webview: Webview, page_url: Url) {
let accent_color = if let Ok(base) = normalize_node(page_url.as_str()) {
get::<NodeInfo>(&client, &base, "/api/node")
.await
.ok()
.and_then(|node| node.accent_color)
} else {
None
};
let _ = webview.eval(new_window_button_script(accent_color.as_deref()));
}
#[tauri::command]
async fn open_login_window(
app: tauri::AppHandle,
state: State<'_, AppState>,
) -> Result<(), String> {
let sequence = state.window_sequence.fetch_add(1, Ordering::Relaxed);
let label = format!("node-{sequence}");
WebviewWindowBuilder::new(
&app,
label,
WebviewUrl::App("index.html?newWindow=1".into()),
)
.title("Synapsis")
.inner_size(1280.0, 820.0)
.min_inner_size(720.0, 560.0)
.center()
.hidden_title(true)
.title_bar_style(tauri::TitleBarStyle::Visible)
.theme(Some(tauri::Theme::Dark))
.background_color(tauri::webview::Color(8, 9, 9, 255))
.build()
.map(|_| ())
.map_err(|error| format!("Couldnt open another Synapsis window: {error}"))
}
#[tauri::command]
async fn logout(state: State<'_, AppState>, node_url: String) -> Result<(), String> {
let base = normalize_node(&node_url)?;
@@ -340,7 +433,8 @@ pub fn run() {
.manage(AppState {
client,
notification_capability_sequence: AtomicU64::new(0),
entry_url: Mutex::new(None),
window_sequence: AtomicU64::new(1),
entry_urls: Mutex::new(HashMap::new()),
})
.on_page_load(|webview, payload| {
if payload.event() != PageLoadEvent::Finished
@@ -352,16 +446,22 @@ pub fn run() {
let Ok(cookies) = webview.cookies_for_url(payload.url().clone()) else {
return;
};
if has_synapsis_session(&cookies) {
let client = webview.state::<AppState>().client.clone();
let target = webview.clone();
let page_url = payload.url().clone();
tauri::async_runtime::spawn(install_new_window_button(client, target, page_url));
}
if !is_logged_out_explore(payload.url(), &cookies) {
return;
}
let state = webview.state::<AppState>();
let mut entry_url = {
let Ok(saved_entry_url) = state.entry_url.lock() else {
let Ok(entry_urls) = state.entry_urls.lock() else {
return;
};
let Some(entry_url) = saved_entry_url.clone() else {
let Some(entry_url) = entry_urls.get(webview.label()).cloned() else {
return;
};
entry_url
@@ -390,7 +490,8 @@ pub fn run() {
register,
logout,
has_web_session,
open_web_app
open_web_app,
open_login_window
])
.run(tauri::generate_context!())
.expect("error while running Synapsis");
@@ -447,13 +548,24 @@ mod tests {
#[test]
fn scopes_native_notifications_to_the_selected_node_origin() {
let capability = remote_notification_capability("https://example.com", 7);
let capability = remote_node_capability("https://example.com", "node-3", 7);
let value: serde_json::Value = serde_json::from_str(&capability).unwrap();
assert_eq!(value["identifier"], "synapsis-node-notifications-7");
assert_eq!(value["identifier"], "synapsis-node-desktop-7");
assert_eq!(value["local"], false);
assert_eq!(value["windows"][0], "node-3");
assert_eq!(value["remote"]["urls"][0], "https://example.com/*");
assert_eq!(value["permissions"][0], "notification:default");
assert_eq!(value["permissions"][1], "allow-open-login-window");
}
#[test]
fn escapes_node_accents_in_the_injected_button_script() {
let script = new_window_button_script(Some("#12abef\"; alert(1); //"));
assert!(script.contains(r##"const requestedAccent = "#12abef\"; alert(1); //";"##));
assert!(script.contains("CSS.supports"));
assert!(script.contains("open_login_window"));
}
#[test]