2023-06-07 17:45:13 +10:00
|
|
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
|
|
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
|
|
|
2023-11-29 05:15:04 -05:00
|
|
|
use std::env;
|
|
|
|
use std::fs;
|
2023-06-19 10:16:45 +10:00
|
|
|
use std::io::Read;
|
2024-04-09 08:04:36 -04:00
|
|
|
use std::path::Path;
|
|
|
|
use std::path::PathBuf;
|
2023-06-19 10:16:45 +10:00
|
|
|
|
|
|
|
use anyhow::Result;
|
2023-07-13 19:05:56 -07:00
|
|
|
use oauth2::TokenResponse;
|
2024-04-09 08:04:36 -04:00
|
|
|
use serde::Serialize;
|
2024-03-14 15:56:45 -04:00
|
|
|
use std::process::Command;
|
2024-04-09 08:04:36 -04:00
|
|
|
use tauri::ipc::InvokeError;
|
|
|
|
use tauri_plugin_shell::ShellExt;
|
2023-09-19 14:08:26 -04:00
|
|
|
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
2023-06-19 10:16:45 +10:00
|
|
|
|
|
|
|
/// This command returns the a json string parse from a toml file at the path.
|
|
|
|
#[tauri::command]
|
|
|
|
fn read_toml(path: &str) -> Result<String, InvokeError> {
|
2023-07-13 19:05:56 -07:00
|
|
|
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
|
|
let mut contents = String::new();
|
|
|
|
file.read_to_string(&mut contents)
|
|
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
|
|
let value =
|
|
|
|
toml::from_str::<toml::Value>(&contents).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
|
|
let value = serde_json::to_string(&value).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
|
|
Ok(value)
|
2023-06-19 10:16:45 +10:00
|
|
|
}
|
|
|
|
|
2024-04-09 08:04:36 -04:00
|
|
|
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
|
|
|
/// Removed from tauri v2
|
|
|
|
#[derive(Debug, Serialize)]
|
|
|
|
pub struct DiskEntry {
|
|
|
|
/// The path to the entry.
|
|
|
|
pub path: PathBuf,
|
|
|
|
/// The name of the entry (file name with extension or directory name).
|
|
|
|
pub name: Option<String>,
|
|
|
|
/// The children of this entry if it's a directory.
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
pub children: Option<Vec<DiskEntry>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
|
|
|
/// Removed from tauri v2
|
|
|
|
fn is_dir<P: AsRef<Path>>(path: P) -> Result<bool> {
|
|
|
|
std::fs::metadata(path)
|
|
|
|
.map(|md| md.is_dir())
|
|
|
|
.map_err(Into::into)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
|
|
|
/// Removed from tauri v2
|
|
|
|
#[tauri::command]
|
|
|
|
fn read_dir_recursive(path: &str) -> Result<Vec<DiskEntry>, InvokeError> {
|
|
|
|
let mut files_and_dirs: Vec<DiskEntry> = vec![];
|
|
|
|
// let path = path.as_ref();
|
|
|
|
for entry in fs::read_dir(path).map_err(|e| InvokeError::from_anyhow(e.into()))? {
|
|
|
|
let path = entry
|
|
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?
|
|
|
|
.path();
|
|
|
|
|
|
|
|
if let Ok(flag) = is_dir(&path) {
|
|
|
|
files_and_dirs.push(DiskEntry {
|
|
|
|
path: path.clone(),
|
|
|
|
children: if flag {
|
|
|
|
Some(read_dir_recursive(path.to_str().expect("No path"))?)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
},
|
|
|
|
name: path
|
|
|
|
.file_name()
|
|
|
|
.map(|name| name.to_string_lossy())
|
|
|
|
.map(|name| name.to_string()),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(files_and_dirs)
|
|
|
|
}
|
|
|
|
|
2023-07-13 19:05:56 -07:00
|
|
|
/// This command returns a string that is the contents of a file at the path.
|
2023-06-19 10:16:45 +10:00
|
|
|
#[tauri::command]
|
|
|
|
fn read_txt_file(path: &str) -> Result<String, InvokeError> {
|
2023-07-13 19:05:56 -07:00
|
|
|
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
|
|
let mut contents = String::new();
|
|
|
|
file.read_to_string(&mut contents)
|
|
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
|
|
Ok(contents)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// This command instantiates a new window with auth.
|
|
|
|
/// The string returned from this method is the access token.
|
|
|
|
#[tauri::command]
|
2023-08-08 09:06:14 +10:00
|
|
|
async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError> {
|
2023-07-13 19:05:56 -07:00
|
|
|
println!("Logging in...");
|
|
|
|
// Do an OAuth 2.0 Device Authorization Grant dance to get a token.
|
2023-08-08 09:06:14 +10:00
|
|
|
let device_auth_url = oauth2::DeviceAuthorizationUrl::new(format!("{host}/oauth2/device/auth"))
|
2023-07-13 19:05:56 -07:00
|
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
|
|
// We can hardcode the client ID.
|
|
|
|
// This value is safe to be embedded in version control.
|
|
|
|
// This is the client ID of the KittyCAD app.
|
|
|
|
let client_id = "2af127fb-e14e-400a-9c57-a9ed08d1a5b7".to_string();
|
|
|
|
let auth_client = oauth2::basic::BasicClient::new(
|
|
|
|
oauth2::ClientId::new(client_id),
|
|
|
|
None,
|
2023-08-08 09:06:14 +10:00
|
|
|
oauth2::AuthUrl::new(format!("{host}/authorize"))
|
2023-07-13 19:05:56 -07:00
|
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
|
|
|
Some(
|
2023-08-08 09:06:14 +10:00
|
|
|
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
|
2023-07-13 19:05:56 -07:00
|
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.set_auth_type(oauth2::AuthType::RequestBody)
|
|
|
|
.set_device_authorization_url(device_auth_url);
|
|
|
|
|
|
|
|
let details: oauth2::devicecode::StandardDeviceAuthorizationResponse = auth_client
|
|
|
|
.exchange_device_code()
|
|
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?
|
|
|
|
.request_async(oauth2::reqwest::async_http_client)
|
|
|
|
.await
|
|
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
|
|
|
|
|
|
let Some(auth_uri) = details.verification_uri_complete() else {
|
|
|
|
return Err(InvokeError::from("getting the verification uri failed"));
|
|
|
|
};
|
|
|
|
|
|
|
|
// Open the system browser with the auth_uri.
|
2023-11-01 17:34:54 -05:00
|
|
|
// We do this in the browser and not a separate window because we want 1password and
|
2023-07-13 19:05:56 -07:00
|
|
|
// other crap to work well.
|
2023-11-29 05:15:04 -05:00
|
|
|
// TODO: find a better way to share this value with tauri e2e tests
|
|
|
|
// Here we're using an env var to enable the /tmp file (windows not supported for now)
|
|
|
|
// and bypass the shell::open call as it fails on GitHub Actions.
|
|
|
|
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
|
2023-12-18 23:31:19 -06:00
|
|
|
if e2e_tauri_enabled {
|
2023-11-29 05:15:04 -05:00
|
|
|
println!(
|
|
|
|
"E2E_TAURI_ENABLED is set, won't open {} externally",
|
|
|
|
auth_uri.secret()
|
|
|
|
);
|
2023-12-18 23:31:19 -06:00
|
|
|
fs::write("/tmp/kittycad_user_code", details.user_code().secret())
|
|
|
|
.expect("Unable to write /tmp/kittycad_user_code file");
|
2023-11-29 05:15:04 -05:00
|
|
|
} else {
|
2024-04-09 08:04:36 -04:00
|
|
|
app.shell()
|
|
|
|
.open(auth_uri.secret(), None)
|
2023-11-29 05:15:04 -05:00
|
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
|
|
}
|
2023-07-13 19:05:56 -07:00
|
|
|
|
|
|
|
// Wait for the user to login.
|
|
|
|
let token = auth_client
|
|
|
|
.exchange_device_access_token(&details)
|
|
|
|
.request_async(oauth2::reqwest::async_http_client, tokio::time::sleep, None)
|
|
|
|
.await
|
|
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?
|
|
|
|
.access_token()
|
|
|
|
.secret()
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
Ok(token)
|
2023-06-19 10:16:45 +10:00
|
|
|
}
|
|
|
|
|
2023-09-14 19:31:16 -04:00
|
|
|
///This command returns the KittyCAD user info given a token.
|
|
|
|
/// The string returned from this method is the user info as a json string.
|
|
|
|
#[tauri::command]
|
2023-09-19 14:08:26 -04:00
|
|
|
async fn get_user(
|
|
|
|
token: Option<String>,
|
|
|
|
hostname: &str,
|
|
|
|
) -> Result<kittycad::types::User, InvokeError> {
|
|
|
|
// Use the host passed in if it's set.
|
|
|
|
// Otherwise, use the default host.
|
|
|
|
let host = if hostname.is_empty() {
|
|
|
|
DEFAULT_HOST.to_string()
|
|
|
|
} else {
|
|
|
|
hostname.to_string()
|
|
|
|
};
|
|
|
|
|
|
|
|
// Change the baseURL to the one we want.
|
|
|
|
let mut baseurl = host.to_string();
|
|
|
|
if !host.starts_with("http://") && !host.starts_with("https://") {
|
|
|
|
baseurl = format!("https://{host}");
|
|
|
|
if host.starts_with("localhost") {
|
|
|
|
baseurl = format!("http://{host}")
|
|
|
|
}
|
|
|
|
}
|
2023-09-14 19:31:16 -04:00
|
|
|
println!("Getting user info...");
|
|
|
|
|
|
|
|
// use kittycad library to fetch the user info from /user/me
|
2023-09-19 14:08:26 -04:00
|
|
|
let mut client = kittycad::Client::new(token.unwrap());
|
|
|
|
|
|
|
|
if baseurl != DEFAULT_HOST {
|
|
|
|
client.set_base_url(&baseurl);
|
|
|
|
}
|
2023-09-14 19:31:16 -04:00
|
|
|
|
|
|
|
let user_info: kittycad::types::User = client
|
|
|
|
.users()
|
|
|
|
.get_self()
|
|
|
|
.await
|
|
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
|
|
|
|
|
|
Ok(user_info)
|
|
|
|
}
|
|
|
|
|
2024-03-14 15:56:45 -04:00
|
|
|
/// Open the selected path in the system file manager.
|
|
|
|
/// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169
|
|
|
|
/// But with the Linux support removed since we don't need it for now.
|
|
|
|
#[tauri::command]
|
|
|
|
fn show_in_folder(path: String) {
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
{
|
|
|
|
Command::new("explorer")
|
|
|
|
.args(["/select,", &path]) // The comma after select is not a typo
|
|
|
|
.spawn()
|
|
|
|
.unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
{
|
|
|
|
Command::new("open").args(["-R", &path]).spawn().unwrap();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-07 17:45:13 +10:00
|
|
|
fn main() {
|
2023-07-13 19:05:56 -07:00
|
|
|
tauri::Builder::default()
|
2023-11-01 07:39:31 -04:00
|
|
|
.setup(|_app| {
|
2024-04-09 08:04:36 -04:00
|
|
|
#[cfg(debug_assertions)]
|
|
|
|
{
|
|
|
|
use tauri::Manager;
|
|
|
|
_app.get_webview("main").unwrap().open_devtools();
|
|
|
|
}
|
|
|
|
#[cfg(not(debug_assertions))]
|
2023-07-13 19:05:56 -07:00
|
|
|
{
|
2024-04-09 08:04:36 -04:00
|
|
|
_app.handle()
|
|
|
|
.plugin(tauri_plugin_updater::Builder::new().build())?;
|
2023-07-13 19:05:56 -07:00
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
})
|
2023-09-14 19:31:16 -04:00
|
|
|
.invoke_handler(tauri::generate_handler![
|
|
|
|
get_user,
|
|
|
|
login,
|
|
|
|
read_toml,
|
2024-03-14 15:56:45 -04:00
|
|
|
read_txt_file,
|
2024-04-09 08:04:36 -04:00
|
|
|
read_dir_recursive,
|
2024-03-14 15:56:45 -04:00
|
|
|
show_in_folder,
|
2023-09-14 19:31:16 -04:00
|
|
|
])
|
2024-04-09 08:04:36 -04:00
|
|
|
.plugin(tauri_plugin_dialog::init())
|
|
|
|
.plugin(tauri_plugin_fs::init())
|
|
|
|
.plugin(tauri_plugin_http::init())
|
|
|
|
.plugin(tauri_plugin_os::init())
|
2024-04-17 10:30:23 -04:00
|
|
|
.plugin(tauri_plugin_process::init())
|
2024-04-09 08:04:36 -04:00
|
|
|
.plugin(tauri_plugin_shell::init())
|
2023-07-13 19:05:56 -07:00
|
|
|
.run(tauri::generate_context!())
|
|
|
|
.expect("error while running tauri application");
|
2023-06-07 17:45:13 +10:00
|
|
|
}
|