KAN-51: add isolated Tauri macOS app scaffold
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
use tauri::{
|
||||
webview::{NewWindowResponse, PageLoadEvent},
|
||||
AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowEvent,
|
||||
};
|
||||
|
||||
const SERVER_URL: &str = "https://kanbangit.de/";
|
||||
|
||||
fn allows_server_navigation(server: &Url, destination: &Url) -> bool {
|
||||
destination.scheme() == "https"
|
||||
&& destination.origin() == server.origin()
|
||||
&& destination.username().is_empty()
|
||||
&& destination.password().is_none()
|
||||
}
|
||||
|
||||
fn allows_launcher_navigation(destination: &Url) -> bool {
|
||||
destination.scheme() == "tauri"
|
||||
&& destination.host_str() == Some("localhost")
|
||||
&& destination.port().is_none()
|
||||
&& destination.username().is_empty()
|
||||
&& destination.password().is_none()
|
||||
}
|
||||
|
||||
fn show_launcher(app: &AppHandle) {
|
||||
if let Some(window) = app.get_webview_window("launcher") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
|
||||
fn open_workspace(app: &AppHandle) -> tauri::Result<()> {
|
||||
if let Some(window) = app.get_webview_window("workspace") {
|
||||
window.show()?;
|
||||
window.set_focus()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let server = Url::parse(SERVER_URL).expect("The built-in server URL must be valid");
|
||||
let window = WebviewWindowBuilder::new(app, "workspace", WebviewUrl::External(server.clone()))
|
||||
.title("KanbanGit")
|
||||
.inner_size(1280.0, 850.0)
|
||||
.min_inner_size(375.0, 600.0)
|
||||
.center()
|
||||
.on_navigation(move |destination| allows_server_navigation(&server, destination))
|
||||
.on_new_window(|_, _| NewWindowResponse::Deny)
|
||||
.on_page_load(|window, payload| {
|
||||
if payload.event() == PageLoadEvent::Finished {
|
||||
if let Some(launcher) = window.app_handle().get_webview_window("launcher") {
|
||||
let _ = launcher.hide();
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!(
|
||||
"KanbanGit workspace page loaded: {}",
|
||||
payload.url().origin().ascii_serialization()
|
||||
);
|
||||
}
|
||||
})
|
||||
.build()?;
|
||||
|
||||
let handle = app.clone();
|
||||
window.on_window_event(move |event| {
|
||||
if matches!(event, WindowEvent::Destroyed) {
|
||||
show_launcher(&handle);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn context<R: tauri::Runtime>() -> tauri::Context<R> {
|
||||
tauri::generate_context!()
|
||||
}
|
||||
|
||||
pub fn builder() -> tauri::Builder<tauri::Wry> {
|
||||
tauri::Builder::default().setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
WebviewWindowBuilder::new(app, "launcher", WebviewUrl::App("index.html".into()))
|
||||
.title("KanbanGit")
|
||||
.inner_size(560.0, 520.0)
|
||||
.min_inner_size(375.0, 480.0)
|
||||
.center()
|
||||
.on_navigation(move |destination| {
|
||||
if allows_launcher_navigation(destination) {
|
||||
return true;
|
||||
}
|
||||
if destination.as_str() == SERVER_URL {
|
||||
let handle = handle.clone();
|
||||
// Defer window creation until the navigation callback has returned.
|
||||
let _ = handle.clone().run_on_main_thread(move || {
|
||||
if let Err(error) = open_workspace(&handle) {
|
||||
eprintln!("Could not open the workspace: {error}");
|
||||
show_launcher(&handle);
|
||||
}
|
||||
});
|
||||
}
|
||||
false
|
||||
})
|
||||
.on_new_window(|_, _| NewWindowResponse::Deny)
|
||||
.build()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
builder().run(context()).expect("Could not start KanbanGit");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn server_allows_application_routes_on_the_same_https_origin() {
|
||||
let server = Url::parse(SERVER_URL).unwrap();
|
||||
for path in [
|
||||
"/",
|
||||
"/login",
|
||||
"/two-factor-challenge",
|
||||
"/projects/1?ticket=51#details",
|
||||
] {
|
||||
assert!(allows_server_navigation(
|
||||
&server,
|
||||
&server.join(path).unwrap()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_rejects_origin_changes_downgrades_credentials_and_local_content() {
|
||||
let server = Url::parse(SERVER_URL).unwrap();
|
||||
for destination in [
|
||||
"http://kanbangit.de/",
|
||||
"https://kanbangit.de:444/",
|
||||
"https://kanbangit.de.example.com/",
|
||||
"https://example.com/",
|
||||
"https://user:password@kanbangit.de/",
|
||||
"https://user@kanbangit.de/",
|
||||
"tauri://localhost/index.html",
|
||||
"file:///etc/passwd",
|
||||
"data:text/html,hello",
|
||||
"javascript:alert(1)",
|
||||
] {
|
||||
assert!(
|
||||
!allows_server_navigation(&server, &Url::parse(destination).unwrap()),
|
||||
"{destination}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launcher_only_loads_bundled_pages() {
|
||||
assert!(allows_launcher_navigation(
|
||||
&Url::parse("tauri://localhost/index.html").unwrap()
|
||||
));
|
||||
for destination in [
|
||||
SERVER_URL,
|
||||
"https://localhost/",
|
||||
"http://localhost:1420/",
|
||||
"tauri://other/index.html",
|
||||
"tauri://user@localhost/index.html",
|
||||
] {
|
||||
assert!(!allows_launcher_navigation(
|
||||
&Url::parse(destination).unwrap()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_pages_cannot_invoke_native_window_webview_or_path_commands() {
|
||||
let app = tauri::test::mock_builder().build(context()).unwrap();
|
||||
let window = WebviewWindowBuilder::new(
|
||||
&app,
|
||||
"workspace",
|
||||
WebviewUrl::External(Url::parse(SERVER_URL).unwrap()),
|
||||
)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
for command in [
|
||||
"plugin:window|close",
|
||||
"plugin:webview|create_webview_window",
|
||||
"plugin:path|resolve_directory",
|
||||
] {
|
||||
let response = tauri::test::get_ipc_response(
|
||||
&window,
|
||||
tauri::webview::InvokeRequest {
|
||||
cmd: command.into(),
|
||||
callback: tauri::ipc::CallbackFn(0),
|
||||
error: tauri::ipc::CallbackFn(1),
|
||||
url: Url::parse(SERVER_URL).unwrap(),
|
||||
body: tauri::ipc::InvokeBody::default(),
|
||||
headers: Default::default(),
|
||||
invoke_key: tauri::test::INVOKE_KEY.into(),
|
||||
},
|
||||
);
|
||||
let error = response.expect_err("Remote native commands must be rejected");
|
||||
assert!(error.as_str().unwrap().contains("not allowed"), "{error}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packaged_app_exposes_no_native_capabilities_or_global_api() {
|
||||
let config: serde_json::Value =
|
||||
serde_json::from_str(include_str!("../tauri.conf.json")).unwrap();
|
||||
assert_eq!(config["app"]["withGlobalTauri"], false);
|
||||
assert_eq!(
|
||||
config["app"]["security"]["capabilities"],
|
||||
serde_json::json!([])
|
||||
);
|
||||
let capabilities = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("capabilities");
|
||||
assert!(
|
||||
!capabilities.exists() || std::fs::read_dir(capabilities).unwrap().next().is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user