From 6798331ce5569d671582e8beefd59e47429ea401 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sun, 28 Jan 2024 14:39:51 -0800 Subject: [PATCH] Cookie Support (#19) --- ...7821bdb51fcfe747170bea41e7a366d736bda.json | 12 + ...2840a6d5d13fec1bbc0aa61ca4f60de98a09c.json | 12 + ...1bdc59fb3dffbd44ff99bc15adb34ea7093f7.json | 56 +++++ ...040bd7883e9a5c0eb79ef0d8a6782a8eae299.json | 56 +++++ src-tauri/Cargo.lock | 129 ++++++++++- src-tauri/Cargo.toml | 4 +- .../migrations/20240127013915_cookies.sql | 10 + src-tauri/src/analytics.rs | 9 + src-tauri/src/http.rs | 101 ++++++++- src-tauri/src/main.rs | 206 +++++++++++++++--- src-tauri/src/models.rs | 109 ++++++++- src-tauri/tauri.conf.json | 2 +- src-web/components/CookieDialog.tsx | 74 +++++++ src-web/components/CookieDropdown.tsx | 93 ++++++++ .../components/EnvironmentActionsDropdown.tsx | 35 +-- src-web/components/EnvironmentEditDialog.tsx | 4 +- src-web/components/GlobalHooks.tsx | 7 + src-web/components/GraphQLEditor.tsx | 68 +++--- .../components/KeyboardShortcutsDialog.tsx | 2 +- src-web/components/ResponsePane.tsx | 6 +- src-web/components/SettingsDialog.tsx | 2 +- src-web/components/SettingsDropdown.tsx | 2 +- .../components/WorkspaceActionsDropdown.tsx | 4 +- src-web/components/WorkspaceHeader.tsx | 2 + src-web/components/core/Banner.tsx | 8 +- src-web/components/core/Button.tsx | 27 ++- src-web/components/core/Dialog.tsx | 19 +- src-web/components/core/FormattedError.tsx | 2 +- src-web/components/core/Icon.tsx | 1 + src-web/hooks/Alert.tsx | 2 +- src-web/hooks/Confirm.tsx | 2 +- src-web/hooks/Prompt.tsx | 15 +- src-web/hooks/useActiveCookieJar.ts | 22 ++ src-web/hooks/useCookieJars.ts | 22 ++ src-web/hooks/useCreateCookieJar.ts | 35 +++ src-web/hooks/useDeleteCookieJar.tsx | 37 ++++ src-web/hooks/useImportData.tsx | 2 +- src-web/hooks/usePrompt.ts | 7 +- src-web/hooks/useSendAnyRequest.ts | 3 + src-web/hooks/useSyncWindowTitle.ts | 1 - src-web/hooks/useUpdateCookieJar.ts | 30 +++ src-web/hooks/useUpdateWorkspace.ts | 1 - src-web/lib/analytics.ts | 1 + src-web/lib/models.ts | 36 ++- src-web/lib/store.ts | 19 +- 45 files changed, 1152 insertions(+), 145 deletions(-) create mode 100644 src-tauri/.sqlx/query-b5ed4dc77f8cf21de1a06f146e47821bdb51fcfe747170bea41e7a366d736bda.json create mode 100644 src-tauri/.sqlx/query-b98609f65dd3a6bbd1ea8dc8bed2840a6d5d13fec1bbc0aa61ca4f60de98a09c.json create mode 100644 src-tauri/.sqlx/query-cb939b45a715d91f7631dea6b2d1bdc59fb3dffbd44ff99bc15adb34ea7093f7.json create mode 100644 src-tauri/.sqlx/query-f2ba4708d4a9ff9ce74c407a730040bd7883e9a5c0eb79ef0d8a6782a8eae299.json create mode 100644 src-tauri/migrations/20240127013915_cookies.sql create mode 100644 src-web/components/CookieDialog.tsx create mode 100644 src-web/components/CookieDropdown.tsx create mode 100644 src-web/hooks/useActiveCookieJar.ts create mode 100644 src-web/hooks/useCookieJars.ts create mode 100644 src-web/hooks/useCreateCookieJar.ts create mode 100644 src-web/hooks/useDeleteCookieJar.tsx create mode 100644 src-web/hooks/useUpdateCookieJar.ts diff --git a/src-tauri/.sqlx/query-b5ed4dc77f8cf21de1a06f146e47821bdb51fcfe747170bea41e7a366d736bda.json b/src-tauri/.sqlx/query-b5ed4dc77f8cf21de1a06f146e47821bdb51fcfe747170bea41e7a366d736bda.json new file mode 100644 index 00000000..f038039f --- /dev/null +++ b/src-tauri/.sqlx/query-b5ed4dc77f8cf21de1a06f146e47821bdb51fcfe747170bea41e7a366d736bda.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO cookie_jars (\n id,\n workspace_id,\n name,\n cookies\n )\n VALUES (?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n cookies = excluded.cookies\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "b5ed4dc77f8cf21de1a06f146e47821bdb51fcfe747170bea41e7a366d736bda" +} diff --git a/src-tauri/.sqlx/query-b98609f65dd3a6bbd1ea8dc8bed2840a6d5d13fec1bbc0aa61ca4f60de98a09c.json b/src-tauri/.sqlx/query-b98609f65dd3a6bbd1ea8dc8bed2840a6d5d13fec1bbc0aa61ca4f60de98a09c.json new file mode 100644 index 00000000..95b8b28f --- /dev/null +++ b/src-tauri/.sqlx/query-b98609f65dd3a6bbd1ea8dc8bed2840a6d5d13fec1bbc0aa61ca4f60de98a09c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM cookie_jars\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "b98609f65dd3a6bbd1ea8dc8bed2840a6d5d13fec1bbc0aa61ca4f60de98a09c" +} diff --git a/src-tauri/.sqlx/query-cb939b45a715d91f7631dea6b2d1bdc59fb3dffbd44ff99bc15adb34ea7093f7.json b/src-tauri/.sqlx/query-cb939b45a715d91f7631dea6b2d1bdc59fb3dffbd44ff99bc15adb34ea7093f7.json new file mode 100644 index 00000000..ddd49a3b --- /dev/null +++ b/src-tauri/.sqlx/query-cb939b45a715d91f7631dea6b2d1bdc59fb3dffbd44ff99bc15adb34ea7093f7.json @@ -0,0 +1,56 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n workspace_id,\n name,\n cookies AS \"cookies!: sqlx::types::Json>\"\n FROM cookie_jars WHERE workspace_id = ?\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "model", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 2, + "type_info": "Datetime" + }, + { + "name": "updated_at", + "ordinal": 3, + "type_info": "Datetime" + }, + { + "name": "workspace_id", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "cookies!: sqlx::types::Json>", + "ordinal": 6, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "cb939b45a715d91f7631dea6b2d1bdc59fb3dffbd44ff99bc15adb34ea7093f7" +} diff --git a/src-tauri/.sqlx/query-f2ba4708d4a9ff9ce74c407a730040bd7883e9a5c0eb79ef0d8a6782a8eae299.json b/src-tauri/.sqlx/query-f2ba4708d4a9ff9ce74c407a730040bd7883e9a5c0eb79ef0d8a6782a8eae299.json new file mode 100644 index 00000000..4fb2112a --- /dev/null +++ b/src-tauri/.sqlx/query-f2ba4708d4a9ff9ce74c407a730040bd7883e9a5c0eb79ef0d8a6782a8eae299.json @@ -0,0 +1,56 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n workspace_id,\n name,\n cookies AS \"cookies!: sqlx::types::Json>\"\n FROM cookie_jars WHERE id = ?\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "model", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 2, + "type_info": "Datetime" + }, + { + "name": "updated_at", + "ordinal": 3, + "type_info": "Datetime" + }, + { + "name": "workspace_id", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "cookies!: sqlx::types::Json>", + "ordinal": 6, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "f2ba4708d4a9ff9ce74c407a730040bd7883e9a5c0eb79ef0d8a6782a8eae299" +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6dcd0484..88fa03eb 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -83,9 +83,9 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "async-compression" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5" +checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" dependencies = [ "brotli", "flate2", @@ -614,6 +614,72 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check 0.9.4", +] + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time", + "version_check 0.9.4", +] + +[[package]] +name = "cookie" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" +dependencies = [ + "time", + "version_check 0.9.4", +] + +[[package]] +name = "cookie_store" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d606d0fba62e13cf04db20536c05cb7f13673c161cb47a47a82b9b9e7d3f1daa" +dependencies = [ + "cookie 0.16.2", + "idna 0.2.3", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "cookie_store" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" +dependencies = [ + "cookie 0.17.0", + "idna 0.3.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -1955,6 +2021,27 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.4.0" @@ -3143,6 +3230,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + [[package]] name = "quick-xml" version = "0.31.0" @@ -3339,13 +3442,15 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.22" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "async-compression", "base64 0.21.5", "bytes", + "cookie 0.16.2", + "cookie_store 0.16.2", "encoding_rs", "futures-core", "futures-util", @@ -3379,6 +3484,18 @@ dependencies = [ "winreg 0.50.0", ] +[[package]] +name = "reqwest_cookie_store" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba529055ea150e42e4eb9c11dcd380a41025ad4d594b0cb4904ef28b037e1061" +dependencies = [ + "bytes", + "cookie_store 0.20.0", + "reqwest", + "url", +] + [[package]] name = "rfd" version = "0.10.0" @@ -4974,7 +5091,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", - "idna", + "idna 0.4.0", "percent-encoding", "serde", ] @@ -5805,6 +5922,7 @@ dependencies = [ "boa_runtime", "chrono", "cocoa 0.25.0", + "cookie 0.18.0", "datetime", "futures", "http", @@ -5813,6 +5931,7 @@ dependencies = [ "openssl-sys", "rand 0.8.5", "reqwest", + "reqwest_cookie_store", "serde", "serde_json", "sqlx", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f549d590..f1f1d448 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -28,7 +28,8 @@ chrono = { version = "0.4.31", features = ["serde"] } futures = "0.3.26" http = "0.2.8" rand = "0.8.5" -reqwest = { version = "0.11.14", features = ["json", "multipart", "gzip", "brotli", "deflate"] } +reqwest = { version = "0.11.23", features = ["multipart", "cookies", "gzip", "brotli", "deflate"] } +cookie = { version = "0.18.0" } serde = { version = "1.0.195", features = ["derive"] } serde_json = { version = "1.0.111", features = ["raw_value"] } sqlx = { version = "0.7.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] } @@ -57,6 +58,7 @@ uuid = "1.3.0" log = "0.4.20" datetime = "0.5.2" window-shadows = "0.2.2" +reqwest_cookie_store = "0.6.0" [features] # by default Tauri runs in production mode diff --git a/src-tauri/migrations/20240127013915_cookies.sql b/src-tauri/migrations/20240127013915_cookies.sql new file mode 100644 index 00000000..bd513c46 --- /dev/null +++ b/src-tauri/migrations/20240127013915_cookies.sql @@ -0,0 +1,10 @@ +CREATE TABLE cookie_jars +( + id TEXT NOT NULL PRIMARY KEY, + model TEXT DEFAULT 'cookie_jar' NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + name TEXT NOT NULL, + cookies TEXT DEFAULT '[]' NOT NULL, + workspace_id TEXT NOT NULL +); diff --git a/src-tauri/src/analytics.rs b/src-tauri/src/analytics.rs index 0222875d..3f38e375 100644 --- a/src-tauri/src/analytics.rs +++ b/src-tauri/src/analytics.rs @@ -14,6 +14,7 @@ pub enum AnalyticsResource { App, Sidebar, Workspace, + CookieJar, Environment, Folder, HttpRequest, @@ -28,6 +29,7 @@ impl AnalyticsResource { "Sidebar" => Some(AnalyticsResource::Sidebar), "Workspace" => Some(AnalyticsResource::Workspace), "Environment" => Some(AnalyticsResource::Environment), + "CookieJar" => Some(AnalyticsResource::CookieJar), "Folder" => Some(AnalyticsResource::Folder), "HttpRequest" => Some(AnalyticsResource::HttpRequest), "HttpResponse" => Some(AnalyticsResource::HttpResponse), @@ -50,6 +52,8 @@ pub enum AnalyticsAction { Send, Toggle, Duplicate, + Import, + Export, } impl AnalyticsAction { @@ -66,6 +70,8 @@ impl AnalyticsAction { "Send" => Some(AnalyticsAction::Send), "Duplicate" => Some(AnalyticsAction::Duplicate), "Toggle" => Some(AnalyticsAction::Toggle), + "Import" => Some(AnalyticsAction::Import), + "Export" => Some(AnalyticsAction::Export), _ => None, } } @@ -77,6 +83,7 @@ fn resource_name(resource: AnalyticsResource) -> &'static str { AnalyticsResource::Sidebar => "sidebar", AnalyticsResource::Workspace => "workspace", AnalyticsResource::Environment => "environment", + AnalyticsResource::CookieJar => "cookie_jar", AnalyticsResource::Folder => "folder", AnalyticsResource::HttpRequest => "http_request", AnalyticsResource::HttpResponse => "http_response", @@ -97,6 +104,8 @@ fn action_name(action: AnalyticsAction) -> &'static str { AnalyticsAction::Send => "send", AnalyticsAction::Duplicate => "duplicate", AnalyticsAction::Toggle => "toggle", + AnalyticsAction::Import => "import", + AnalyticsAction::Export => "export", } } diff --git a/src-tauri/src/http.rs b/src-tauri/src/http.rs index 1cf2b351..04481fe9 100644 --- a/src-tauri/src/http.rs +++ b/src-tauri/src/http.rs @@ -2,16 +2,18 @@ use std::fs; use std::fs::{create_dir_all, File}; use std::io::Write; use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; use std::time::Duration; use base64::Engine; -use http::{HeaderMap, HeaderName, HeaderValue, Method}; use http::header::{ACCEPT, USER_AGENT}; +use http::{HeaderMap, HeaderName, HeaderValue, Method}; use log::{error, info, warn}; -use reqwest::multipart; use reqwest::redirect::Policy; +use reqwest::{multipart, Url}; +use sqlx::types::{Json, JsonValue}; use sqlx::{Pool, Sqlite}; -use sqlx::types::Json; use tauri::{AppHandle, Wry}; use crate::{emit_side_effect, models, render, response_err}; @@ -19,13 +21,13 @@ use crate::{emit_side_effect, models, render, response_err}; pub async fn send_http_request( request: models::HttpRequest, response: &models::HttpResponse, - environment_id: &str, + environment: Option, + cookie_jar: Option, app_handle: &AppHandle, pool: &Pool, download_path: Option, ) -> Result { let start = std::time::Instant::now(); - let environment = models::get_environment(environment_id, pool).await.ok(); let environment_ref = environment.as_ref(); let workspace = models::get_workspace(&request.workspace_id, pool) .await @@ -49,6 +51,32 @@ pub async fn send_http_request( .danger_accept_invalid_certs(!workspace.setting_validate_certificates) .tls_info(true); + // Add cookie store if specified + let maybe_cookie_manager = match cookie_jar.clone() { + Some(cj) => { + // HACK: Can't construct Cookie without serde, so we have to do this + let cookies = cj + .cookies + .0 + .iter() + .map(|json_cookie| { + serde_json::from_value(json_cookie.clone()) + .expect("Failed to deserialize cookie") + }) + .map(|c| Ok(c)) + .collect::>>(); + + let store = reqwest_cookie_store::CookieStore::from_cookies(cookies, true) + .expect("Failed to create cookie store"); + let cookie_store = reqwest_cookie_store::CookieStoreMutex::new(store); + let cookie_store = Arc::new(cookie_store); + client_builder = client_builder.cookie_provider(Arc::clone(&cookie_store)); + + Some((cookie_store, cj)) + } + None => None, + }; + if workspace.setting_request_timeout > 0 { client_builder = client_builder.timeout(Duration::from_millis( workspace.setting_request_timeout.unsigned_abs(), @@ -58,14 +86,37 @@ pub async fn send_http_request( // .use_rustls_tls() // TODO: Make this configurable (maybe) let client = client_builder.build().expect("Failed to build client"); + let url = match Url::from_str(url_string.as_str()) { + Ok(u) => u, + Err(e) => { + return response_err(response, e.to_string(), app_handle, pool).await; + } + }; + let m = Method::from_bytes(request.method.to_uppercase().as_bytes()) .expect("Failed to create method"); - let mut request_builder = client.request(m, url_string.to_string()); + let mut request_builder = client.request(m, url.clone()); let mut headers = HeaderMap::new(); headers.insert(USER_AGENT, HeaderValue::from_static("yaak")); headers.insert(ACCEPT, HeaderValue::from_static("*/*")); + // TODO: Set cookie header ourselves once we also handle redirects. We need to do this + // because reqwest doesn't give us a way to inspect the headers it sent (we have to do + // everything manually to know that). + // if let Some(cookie_store) = maybe_cookie_store.clone() { + // let values1 = cookie_store.get_request_values(&url); + // println!("COOKIE VLUAES: {:?}", values1.collect::>()); + // let raw_value = cookie_store.get_request_values(&url) + // .map(|(name, value)| format!("{}={}", name, value)) + // .collect::>() + // .join("; "); + // headers.insert( + // COOKIE, + // HeaderValue::from_str(&raw_value).expect("Failed to create cookie header"), + // ); + // } + for h in request.headers.0 { if h.name.is_empty() && h.value.is_empty() { continue; @@ -252,10 +303,11 @@ pub async fn send_http_request( match raw_response { Ok(v) => { let mut response = response.clone(); + let response_headers = v.headers().clone(); response.status = v.status().as_u16() as i64; response.status_reason = v.status().canonical_reason().map(|s| s.to_string()); response.headers = Json( - v.headers() + response_headers .iter() .map(|(k, v)| models::HttpResponseHeader { name: k.as_str().to_string(), @@ -304,15 +356,42 @@ pub async fn send_http_request( match (download_path, response.body_path.clone()) { (Some(dl_path), Some(body_path)) => { info!("Downloading response body to {}", dl_path.display()); - fs::copy(body_path, dl_path).expect("Failed to copy file for response download"); + fs::copy(body_path, dl_path) + .expect("Failed to copy file for response download"); } _ => {} }; + // Add cookie store if specified + if let Some((cookie_store, mut cookie_jar)) = maybe_cookie_manager { + // let cookies = response_headers.get_all(SET_COOKIE).iter().map(|h| { + // println!("RESPONSE COOKIE: {}", h.to_str().unwrap()); + // cookie_store::RawCookie::from_str(h.to_str().unwrap()) + // .expect("Failed to parse cookie") + // }); + // store.store_response_cookies(cookies, &url); + + let json_cookies: Json> = Json( + cookie_store + .lock() + .unwrap() + .iter_any() + .map(|c| serde_json::to_value(&c).expect("Failed to serialize cookie")) + .collect::>(), + ); + cookie_jar.cookies = json_cookies; + match models::upsert_cookie_jar(pool, &cookie_jar).await { + Ok(updated_jar) => { + emit_side_effect(app_handle, "updated_model", &updated_jar); + } + Err(e) => { + error!("Failed to update cookie jar: {}", e); + } + }; + } + Ok(response) } - Err(e) => { - response_err(response, e.to_string(), app_handle, pool).await - } + Err(e) => response_err(response, e.to_string(), app_handle, pool).await, } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 662f7c30..6d87b59c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,27 +3,28 @@ windows_subsystem = "windows" )] +extern crate core; #[cfg(target_os = "macos")] #[macro_use] extern crate objc; use std::collections::HashMap; use std::env::current_dir; -use std::fs::{create_dir_all, read_to_string, File}; +use std::fs::{create_dir_all, File, read_to_string}; use std::process::exit; use fern::colors::ColoredLevelConfig; use log::{debug, error, info, warn}; use rand::random; use serde::Serialize; -use serde_json::Value; +use serde_json::{json, Value}; +use sqlx::{Pool, Sqlite, SqlitePool}; use sqlx::migrate::Migrator; use sqlx::types::Json; -use sqlx::{Pool, Sqlite, SqlitePool}; -#[cfg(target_os = "macos")] -use tauri::TitleBarStyle; use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry}; use tauri::{Manager, WindowEvent}; +#[cfg(target_os = "macos")] +use tauri::TitleBarStyle; use tauri_plugin_log::{fern, LogTarget}; use tauri_plugin_window_state::{StateFlags, WindowExt}; use tokio::sync::Mutex; @@ -78,17 +79,36 @@ async fn migrate_db( async fn send_ephemeral_request( mut request: models::HttpRequest, environment_id: Option<&str>, + cookie_jar_id: Option<&str>, app_handle: AppHandle, db_instance: State<'_, Mutex>>, ) -> Result { let pool = &*db_instance.lock().await; let response = models::HttpResponse::new(); - let environment_id2 = environment_id.unwrap_or("n/a").to_string(); request.id = "".to_string(); + let environment = match environment_id { + Some(id) => Some( + models::get_environment(id, pool) + .await + .expect("Failed to get environment"), + ), + None => None, + }; + let cookie_jar = match cookie_jar_id { + Some(id) => Some( + models::get_cookie_jar(id, pool) + .await + .expect("Failed to get cookie jar"), + ), + None => None, + }; + + // let cookie_jar_id2 = cookie_jar_id.unwrap_or("").to_string(); send_http_request( request, &response, - &environment_id2, + environment, + cookie_jar, &app_handle, pool, None, @@ -151,6 +171,13 @@ async fn import_data( ) .await { + analytics::track_event( + &window.app_handle(), + AnalyticsResource::App, + AnalyticsAction::Import, + Some(json!({ "plugin": plugin_name })), + ) + .await; result = Some(r); break; } @@ -217,8 +244,17 @@ async fn export_data( serde_json::to_writer_pretty(&f, &export_data) .map_err(|e| e.to_string()) .expect("Failed to write"); + f.sync_all().expect("Failed to sync"); - info!("Exported Yaak workspace to {:?}", export_path); + + analytics::track_event( + &app_handle, + AnalyticsResource::App, + AnalyticsAction::Export, + None, + ) + .await; + Ok(()) } @@ -228,47 +264,56 @@ async fn send_request( db_instance: State<'_, Mutex>>, request_id: &str, environment_id: Option<&str>, + cookie_jar_id: Option<&str>, download_dir: Option<&str>, ) -> Result { let pool = &*db_instance.lock().await; + let app_handle = window.app_handle(); - let req = models::get_request(request_id, pool) + let request = models::get_request(request_id, pool) .await .expect("Failed to get request"); - let response = models::create_response(&req.id, 0, "", 0, None, None, None, vec![], pool) + let environment = match environment_id { + Some(id) => Some( + models::get_environment(id, pool) + .await + .expect("Failed to get environment"), + ), + None => None, + }; + + let cookie_jar = match cookie_jar_id { + Some(id) => Some( + models::get_cookie_jar(id, pool) + .await + .expect("Failed to get cookie jar"), + ), + None => None, + }; + + let response = models::create_response(&request.id, 0, "", 0, None, None, None, vec![], pool) .await .expect("Failed to create response"); - let response2 = response.clone(); - let environment_id2 = environment_id.unwrap_or("n/a").to_string(); - let app_handle2 = window.app_handle().clone(); - let pool2 = pool.clone(); - let download_path = if let Some(p) = download_dir { Some(std::path::Path::new(p).to_path_buf()) } else { None }; - tokio::spawn(async move { - if let Err(e) = send_http_request( - req, - &response2, - &environment_id2, - &app_handle2, - &pool2, - download_path, - ) - .await - { - response_err(&response2, e, &app_handle2, &pool2) - .await - .expect("Failed to update response"); - } - }); + emit_side_effect(&app_handle, "created_model", response.clone()); - emit_and_return(&window, "created_model", response) + send_http_request( + request.clone(), + &response, + environment, + cookie_jar, + &app_handle, + &pool, + download_path, + ) + .await } async fn response_err( @@ -362,6 +407,57 @@ async fn create_workspace( emit_and_return(&window, "created_model", created_workspace) } +#[tauri::command] +async fn update_cookie_jar( + cookie_jar: models::CookieJar, + window: Window, + db_instance: State<'_, Mutex>>, +) -> Result { + let pool = &*db_instance.lock().await; + println!("Updating cookie jar {}", cookie_jar.cookies.len()); + + let updated = models::upsert_cookie_jar(pool, &cookie_jar) + .await + .expect("Failed to update cookie jar"); + + emit_and_return(&window, "updated_model", updated) +} + +#[tauri::command] +async fn delete_cookie_jar( + window: Window, + db_instance: State<'_, Mutex>>, + cookie_jar_id: &str, +) -> Result { + let pool = &*db_instance.lock().await; + let req = models::delete_cookie_jar(cookie_jar_id, pool) + .await + .expect("Failed to delete cookie jar"); + emit_and_return(&window, "deleted_model", req) +} + +#[tauri::command] +async fn create_cookie_jar( + workspace_id: &str, + name: &str, + window: Window, + db_instance: State<'_, Mutex>>, +) -> Result { + let pool = &*db_instance.lock().await; + let created_cookie_jar = models::upsert_cookie_jar( + pool, + &models::CookieJar { + name: name.to_string(), + workspace_id: workspace_id.to_string(), + ..Default::default() + }, + ) + .await + .expect("Failed to create cookie jar"); + + emit_and_return(&window, "created_model", created_cookie_jar) +} + #[tauri::command] async fn create_environment( workspace_id: &str, @@ -627,6 +723,44 @@ async fn get_request( .map_err(|e| e.to_string()) } +#[tauri::command] +async fn get_cookie_jar( + id: &str, + db_instance: State<'_, Mutex>>, +) -> Result { + let pool = &*db_instance.lock().await; + models::get_cookie_jar(id, pool) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn list_cookie_jars( + workspace_id: &str, + db_instance: State<'_, Mutex>>, +) -> Result, String> { + let pool = &*db_instance.lock().await; + let cookie_jars = models::find_cookie_jars(workspace_id, pool) + .await + .expect("Failed to find cookie jars"); + + if cookie_jars.is_empty() { + let cookie_jar = models::upsert_cookie_jar( + pool, + &models::CookieJar { + name: "Default".to_string(), + workspace_id: workspace_id.to_string(), + ..Default::default() + }, + ) + .await + .expect("Failed to create CookieJar"); + Ok(vec![cookie_jar]) + } else { + Ok(cookie_jars) + } +} + #[tauri::command] async fn get_environment( id: &str, @@ -755,6 +889,7 @@ fn main() { .level_for("tracing", log::LevelFilter::Info) .level_for("reqwest", log::LevelFilter::Info) .level_for("tokio_util", log::LevelFilter::Info) + .level_for("cookie_store", log::LevelFilter::Info) .with_colors(ColoredLevelConfig::default()) .level(log::LevelFilter::Trace) .build(), @@ -807,11 +942,13 @@ fn main() { }) .invoke_handler(tauri::generate_handler![ check_for_updates, + create_cookie_jar, create_environment, create_folder, create_request, create_workspace, delete_all_responses, + delete_cookie_jar, delete_environment, delete_folder, delete_request, @@ -820,13 +957,15 @@ fn main() { duplicate_request, export_data, filter_response, - get_key_value, + get_cookie_jar, get_environment, get_folder, + get_key_value, get_request, get_settings, get_workspace, import_data, + list_cookie_jars, list_environments, list_folders, list_requests, @@ -838,6 +977,7 @@ fn main() { set_key_value, set_update_mode, track_event, + update_cookie_jar, update_environment, update_folder, update_request, diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 7dec5dd7..532666f2 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::fs; -use log::error; +use log::error; use rand::distributions::{Alphanumeric, DistString}; use serde::{Deserialize, Serialize}; use sqlx::{Pool, Sqlite}; @@ -57,6 +57,23 @@ impl Workspace { } } +#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] +pub struct CookieX { + +} + +#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default, rename_all = "camelCase")] +pub struct CookieJar { + pub id: String, + pub model: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub workspace_id: String, + pub name: String, + pub cookies: Json>, +} + #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct Environment { @@ -351,6 +368,96 @@ pub async fn delete_workspace(id: &str, pool: &Pool) -> Result) -> Result { + sqlx::query_as!( + CookieJar, + r#" + SELECT + id, + model, + created_at, + updated_at, + workspace_id, + name, + cookies AS "cookies!: sqlx::types::Json>" + FROM cookie_jars WHERE id = ? + "#, + id, + ) + .fetch_one(pool) + .await +} + +pub async fn find_cookie_jars(workspace_id: &str, pool: &Pool) -> Result, sqlx::Error> { + sqlx::query_as!( + CookieJar, + r#" + SELECT + id, + model, + created_at, + updated_at, + workspace_id, + name, + cookies AS "cookies!: sqlx::types::Json>" + FROM cookie_jars WHERE workspace_id = ? + "#, + workspace_id, + ) + .fetch_all(pool) + .await +} + +pub async fn delete_cookie_jar(id: &str, pool: &Pool) -> Result { + let cookie_jar = get_cookie_jar(id, pool).await?; + + let _ = sqlx::query!( + r#" + DELETE FROM cookie_jars + WHERE id = ? + "#, + id, + ) + .execute(pool) + .await; + + Ok(cookie_jar) +} + +pub async fn upsert_cookie_jar( + pool: &Pool, + cookie_jar: &CookieJar, +) -> Result { + let id = match cookie_jar.id.as_str() { + "" => generate_id(Some("cj")), + _ => cookie_jar.id.to_string(), + }; + let trimmed_name = cookie_jar.name.trim(); + sqlx::query!( + r#" + INSERT INTO cookie_jars ( + id, + workspace_id, + name, + cookies + ) + VALUES (?, ?, ?, ?) + ON CONFLICT (id) DO UPDATE SET + updated_at = CURRENT_TIMESTAMP, + name = excluded.name, + cookies = excluded.cookies + "#, + id, + cookie_jar.workspace_id, + trimmed_name, + cookie_jar.cookies, + ) + .execute(pool) + .await?; + + get_cookie_jar(&id, pool).await +} + pub async fn find_environments( workspace_id: &str, pool: &Pool, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f4e27ee2..1722a25a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "Yaak", - "version": "2024.1.0" + "version": "2024.2.0" }, "tauri": { "windows": [], diff --git a/src-web/components/CookieDialog.tsx b/src-web/components/CookieDialog.tsx new file mode 100644 index 00000000..10c1f798 --- /dev/null +++ b/src-web/components/CookieDialog.tsx @@ -0,0 +1,74 @@ +import { useCookieJars } from '../hooks/useCookieJars'; +import { useUpdateCookieJar } from '../hooks/useUpdateCookieJar'; +import { cookieDomain } from '../lib/models'; +import { Banner } from './core/Banner'; +import { IconButton } from './core/IconButton'; +import { InlineCode } from './core/InlineCode'; + +interface Props { + cookieJarId: string | null; +} + +export const CookieDialog = function ({ cookieJarId }: Props) { + const updateCookieJar = useUpdateCookieJar(cookieJarId ?? null); + const cookieJars = useCookieJars(); + const cookieJar = cookieJars.find((c) => c.id === cookieJarId); + + if (cookieJar == null) { + return
No cookie jar selected
; + } + + if (cookieJar.cookies.length === 0) { + return ( + + Cookies will appear when a response contains the Set-Cookie header + + ); + } + + return ( +
+ + + + + + + + + + {cookieJar?.cookies.map((c) => ( + + + + + + ))} + +
DomainCookie
+ {cookieDomain(c)} + + {c.raw_cookie} + + { + console.log( + 'DELETE COOKIE', + c, + cookieJar.cookies.filter((c2) => c2 !== c).length, + ); + await updateCookieJar.mutateAsync({ + ...cookieJar, + cookies: cookieJar.cookies.filter((c2) => c2 !== c), + }); + }} + /> +
+
+ ); +}; diff --git a/src-web/components/CookieDropdown.tsx b/src-web/components/CookieDropdown.tsx new file mode 100644 index 00000000..11b757a5 --- /dev/null +++ b/src-web/components/CookieDropdown.tsx @@ -0,0 +1,93 @@ +import { useActiveCookieJar } from '../hooks/useActiveCookieJar'; +import { useCookieJars } from '../hooks/useCookieJars'; +import { useCreateCookieJar } from '../hooks/useCreateCookieJar'; +import { useDeleteCookieJar } from '../hooks/useDeleteCookieJar'; +import { usePrompt } from '../hooks/usePrompt'; +import { useUpdateCookieJar } from '../hooks/useUpdateCookieJar'; +import { CookieDialog } from './CookieDialog'; +import { Dropdown, type DropdownItem } from './core/Dropdown'; +import { Icon } from './core/Icon'; +import { IconButton } from './core/IconButton'; +import { InlineCode } from './core/InlineCode'; +import { useDialog } from './DialogContext'; + +export function CookieDropdown() { + const cookieJars = useCookieJars(); + const { activeCookieJar, setActiveCookieJarId } = useActiveCookieJar(); + const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null); + const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null); + const createCookieJar = useCreateCookieJar(); + const dialog = useDialog(); + const prompt = usePrompt(); + + return ( + ({ + key: j.id, + label: j.name, + leftSlot: , + onSelect: () => setActiveCookieJarId(j.id), + })), + ...((cookieJars.length > 0 && activeCookieJar != null + ? [ + { type: 'separator', label: activeCookieJar.name }, + { + key: 'manage', + label: 'Manage Cookies', + leftSlot: , + onSelect: () => { + if (activeCookieJar == null) return; + dialog.show({ + id: 'cookies', + title: 'Manage Cookies', + size: 'full', + render: () => , + }); + }, + }, + { + key: 'rename', + label: 'Rename', + leftSlot: , + onSelect: async () => { + const name = await prompt({ + title: 'Rename Cookie Jar', + description: ( + <> + Enter a new name for {activeCookieJar?.name} + + ), + name: 'name', + label: 'Name', + defaultValue: activeCookieJar?.name, + }); + updateCookieJar.mutate({ name }); + }, + }, + ...((cookieJars.length > 1 // Never delete the last one + ? [ + { + key: 'delete', + label: 'Delete', + leftSlot: , + variant: 'danger', + onSelect: () => deleteCookieJar.mutateAsync(), + }, + ] + : []) as DropdownItem[]), + ] + : []) as DropdownItem[]), + { type: 'separator' }, + { + key: 'create-cookie-jar', + label: 'New Cookie Jar', + leftSlot: , + onSelect: () => createCookieJar.mutate(), + }, + ]} + > + + + ); +} diff --git a/src-web/components/EnvironmentActionsDropdown.tsx b/src-web/components/EnvironmentActionsDropdown.tsx index 5768ca4c..837b45b4 100644 --- a/src-web/components/EnvironmentActionsDropdown.tsx +++ b/src-web/components/EnvironmentActionsDropdown.tsx @@ -54,23 +54,26 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo ...((environments.length > 0 ? [{ type: 'separator', label: 'Environments' }] : []) as DropdownItem[]), - environments.length - ? { - key: 'edit', - label: 'Manage Environments', - hotKeyAction: 'environmentEditor.toggle', - leftSlot: , - onSelect: showEnvironmentDialog, - } - : { - key: 'new', - label: 'New Environment', - leftSlot: , - onSelect: async () => { - await createEnvironment.mutateAsync(); - showEnvironmentDialog(); + ...((environments.length > 0 + ? [ + { + key: 'edit', + label: 'Manage Environments', + hotKeyAction: 'environmentEditor.toggle', + leftSlot: , + onSelect: showEnvironmentDialog, }, - }, + ] + : []) as DropdownItem[]), + { + key: 'new', + label: 'New Environment', + leftSlot: , + onSelect: async () => { + await createEnvironment.mutateAsync(); + showEnvironmentDialog(); + }, + }, ], [activeEnvironment?.id, createEnvironment, environments, routes, showEnvironmentDialog], ); diff --git a/src-web/components/EnvironmentEditDialog.tsx b/src-web/components/EnvironmentEditDialog.tsx index 0db0170c..95392ab8 100644 --- a/src-web/components/EnvironmentEditDialog.tsx +++ b/src-web/components/EnvironmentEditDialog.tsx @@ -51,12 +51,12 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) { return (
{showSidebar && ( -
, ] : [] } diff --git a/src-web/components/KeyboardShortcutsDialog.tsx b/src-web/components/KeyboardShortcutsDialog.tsx index 323d07f3..1e40dabd 100644 --- a/src-web/components/KeyboardShortcutsDialog.tsx +++ b/src-web/components/KeyboardShortcutsDialog.tsx @@ -3,7 +3,7 @@ import { HotKeyList } from './core/HotKeyList'; export const KeyboardShortcutsDialog = () => { return ( -
+
); diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index 3bbf9d4a..fb9a2a9c 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -99,7 +99,11 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro 'shadow shadow-gray-100 dark:shadow-gray-0 relative', )} > - {activeResponse?.error && {activeResponse.error}} + {activeResponse?.error && ( + + {activeResponse.error} + + )} {!activeResponse && ( <> diff --git a/src-web/components/SettingsDialog.tsx b/src-web/components/SettingsDialog.tsx index 7ce19a0c..602dabf5 100644 --- a/src-web/components/SettingsDialog.tsx +++ b/src-web/components/SettingsDialog.tsx @@ -20,7 +20,7 @@ export const SettingsDialog = () => { } return ( - + diff --git a/src-web/hooks/useActiveCookieJar.ts b/src-web/hooks/useActiveCookieJar.ts new file mode 100644 index 00000000..f5150f35 --- /dev/null +++ b/src-web/hooks/useActiveCookieJar.ts @@ -0,0 +1,22 @@ +import { NAMESPACE_GLOBAL } from '../lib/keyValueStore'; +import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useCookieJars } from './useCookieJars'; +import { useKeyValue } from './useKeyValue'; + +export function useActiveCookieJar() { + const workspaceId = useActiveWorkspaceId(); + const cookieJars = useCookieJars(); + + const kv = useKeyValue({ + namespace: NAMESPACE_GLOBAL, + key: ['activeCookieJar', workspaceId ?? 'n/a'], + defaultValue: null, + }); + + const activeCookieJar = cookieJars.find((cookieJar) => cookieJar.id === kv.value); + + return { + activeCookieJar: activeCookieJar ?? null, + setActiveCookieJarId: kv.set, + }; +} diff --git a/src-web/hooks/useCookieJars.ts b/src-web/hooks/useCookieJars.ts new file mode 100644 index 00000000..d87fec2d --- /dev/null +++ b/src-web/hooks/useCookieJars.ts @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import type { CookieJar } from '../lib/models'; +import { useActiveWorkspaceId } from './useActiveWorkspaceId'; + +export function cookieJarsQueryKey({ workspaceId }: { workspaceId: string }) { + return ['cookie_jars', { workspaceId }]; +} + +export function useCookieJars() { + const workspaceId = useActiveWorkspaceId(); + return ( + useQuery({ + enabled: workspaceId != null, + queryKey: cookieJarsQueryKey({ workspaceId: workspaceId ?? 'n/a' }), + queryFn: async () => { + if (workspaceId == null) return []; + return (await invoke('list_cookie_jars', { workspaceId })) as CookieJar[]; + }, + }).data ?? [] + ); +} diff --git a/src-web/hooks/useCreateCookieJar.ts b/src-web/hooks/useCreateCookieJar.ts new file mode 100644 index 00000000..3f919423 --- /dev/null +++ b/src-web/hooks/useCreateCookieJar.ts @@ -0,0 +1,35 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import { trackEvent } from '../lib/analytics'; +import type { CookieJar, HttpRequest } from '../lib/models'; +import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { usePrompt } from './usePrompt'; +import { requestsQueryKey } from './useRequests'; + +export function useCreateCookieJar() { + const workspaceId = useActiveWorkspaceId(); + const queryClient = useQueryClient(); + const prompt = usePrompt(); + + return useMutation({ + mutationFn: async () => { + if (workspaceId === null) { + throw new Error("Cannot create cookie jar when there's no active workspace"); + } + const name = await prompt({ + name: 'name', + title: 'New CookieJar', + label: 'Name', + defaultValue: 'My Jar', + }); + return invoke('create_cookie_jar', { workspaceId, name }); + }, + onSettled: () => trackEvent('CookieJar', 'Create'), + onSuccess: async (request) => { + queryClient.setQueryData( + requestsQueryKey({ workspaceId: request.workspaceId }), + (requests) => [...(requests ?? []), request], + ); + }, + }); +} diff --git a/src-web/hooks/useDeleteCookieJar.tsx b/src-web/hooks/useDeleteCookieJar.tsx new file mode 100644 index 00000000..4ee9b46b --- /dev/null +++ b/src-web/hooks/useDeleteCookieJar.tsx @@ -0,0 +1,37 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import { InlineCode } from '../components/core/InlineCode'; +import { trackEvent } from '../lib/analytics'; +import type { CookieJar, Workspace } from '../lib/models'; +import { useConfirm } from './useConfirm'; +import { cookieJarsQueryKey } from './useCookieJars'; + +export function useDeleteCookieJar(cookieJar: CookieJar | null) { + const queryClient = useQueryClient(); + const confirm = useConfirm(); + + return useMutation({ + mutationFn: async () => { + const confirmed = await confirm({ + title: 'Delete CookieJar', + variant: 'delete', + description: ( + <> + Permanently delete {cookieJar?.name}? + + ), + }); + if (!confirmed) return null; + return invoke('delete_cookie_jar', { cookieJarId: cookieJar?.id }); + }, + onSettled: () => trackEvent('CookieJar', 'Delete'), + onSuccess: async (cookieJar) => { + if (cookieJar === null) return; + + const { id: cookieJarId, workspaceId } = cookieJar; + queryClient.setQueryData(cookieJarsQueryKey({ workspaceId }), (cookieJars) => + cookieJars?.filter((e) => e.id !== cookieJarId), + ); + }, + }); +} diff --git a/src-web/hooks/useImportData.tsx b/src-web/hooks/useImportData.tsx index 4a380f78..47f2e0a0 100644 --- a/src-web/hooks/useImportData.tsx +++ b/src-web/hooks/useImportData.tsx @@ -47,7 +47,7 @@ export function useImportData() { render: ({ hide }) => { const { workspaces, environments, folders, requests } = imported; return ( - +
  • {count('Workspace', workspaces.length)}
  • {count('Environment', environments.length)}
  • diff --git a/src-web/hooks/usePrompt.ts b/src-web/hooks/usePrompt.ts index 25261b61..29494db9 100644 --- a/src-web/hooks/usePrompt.ts +++ b/src-web/hooks/usePrompt.ts @@ -1,4 +1,3 @@ -import { dialog } from '@tauri-apps/api'; import type { DialogProps } from '../components/core/Dialog'; import { useDialog } from '../components/DialogContext'; import type { PromptProps } from './Prompt'; @@ -13,8 +12,8 @@ export function usePrompt() { label, defaultValue, placeholder, - }: Pick & - Pick) => + confirmLabel, + }: Pick & Omit) => new Promise((onResult: PromptProps['onResult']) => { dialog.show({ title, @@ -22,7 +21,7 @@ export function usePrompt() { hideX: true, size: 'sm', render: ({ hide }) => - Prompt({ onHide: hide, onResult, name, label, defaultValue, placeholder }), + Prompt({ onHide: hide, onResult, name, label, defaultValue, placeholder, confirmLabel }), }); }); } diff --git a/src-web/hooks/useSendAnyRequest.ts b/src-web/hooks/useSendAnyRequest.ts index d6a6a9d5..157eec0a 100644 --- a/src-web/hooks/useSendAnyRequest.ts +++ b/src-web/hooks/useSendAnyRequest.ts @@ -5,12 +5,14 @@ import slugify from 'slugify'; import { trackEvent } from '../lib/analytics'; import type { HttpResponse } from '../lib/models'; import { getRequest } from '../lib/store'; +import { useActiveCookieJar } from './useActiveCookieJar'; import { useActiveEnvironmentId } from './useActiveEnvironmentId'; import { useAlert } from './useAlert'; export function useSendAnyRequest(options: { download?: boolean } = {}) { const environmentId = useActiveEnvironmentId(); const alert = useAlert(); + const { activeCookieJar } = useActiveCookieJar(); return useMutation({ mutationFn: async (id) => { const request = await getRequest(id); @@ -33,6 +35,7 @@ export function useSendAnyRequest(options: { download?: boolean } = {}) { requestId: id, environmentId, downloadDir: downloadDir, + cookieJarId: activeCookieJar?.id, }); }, onSettled: () => trackEvent('HttpRequest', 'Send'), diff --git a/src-web/hooks/useSyncWindowTitle.ts b/src-web/hooks/useSyncWindowTitle.ts index 78017f3b..9b902f33 100644 --- a/src-web/hooks/useSyncWindowTitle.ts +++ b/src-web/hooks/useSyncWindowTitle.ts @@ -17,7 +17,6 @@ export function useSyncWindowTitle() { newTitle += ` – ${fallbackRequestName(activeRequest)}`; } - console.log('Skipping setting window title to ', newTitle); // TODO: This resets the stoplight position so we can't use it yet // appWindow.setTitle(newTitle).catch(console.error); }, [activeEnvironment, activeRequest, activeWorkspace]); diff --git a/src-web/hooks/useUpdateCookieJar.ts b/src-web/hooks/useUpdateCookieJar.ts new file mode 100644 index 00000000..f5406ec8 --- /dev/null +++ b/src-web/hooks/useUpdateCookieJar.ts @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import type { CookieJar } from '../lib/models'; +import { getCookieJar } from '../lib/store'; +import { cookieJarsQueryKey } from './useCookieJars'; + +export function useUpdateCookieJar(id: string | null) { + const queryClient = useQueryClient(); + return useMutation | ((j: CookieJar) => CookieJar)>({ + mutationFn: async (v) => { + const cookieJar = await getCookieJar(id); + if (cookieJar == null) { + throw new Error("Can't update a null workspace"); + } + + const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v }; + console.log('NEW COOKIE JAR', newCookieJar.cookies.length); + await invoke('update_cookie_jar', { cookieJar: newCookieJar }); + }, + onMutate: async (v) => { + const cookieJar = await getCookieJar(id); + if (cookieJar === null) return; + + const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v }; + queryClient.setQueryData(cookieJarsQueryKey(cookieJar), (cookieJars) => + (cookieJars ?? []).map((j) => (j.id === newCookieJar.id ? newCookieJar : j)), + ); + }, + }); +} diff --git a/src-web/hooks/useUpdateWorkspace.ts b/src-web/hooks/useUpdateWorkspace.ts index 482f8fe4..3b71ee00 100644 --- a/src-web/hooks/useUpdateWorkspace.ts +++ b/src-web/hooks/useUpdateWorkspace.ts @@ -21,7 +21,6 @@ export function useUpdateWorkspace(id: string | null) { if (workspace === null) return; const newWorkspace = typeof v === 'function' ? v(workspace) : { ...workspace, ...v }; - console.log('NEW WORKSPACE', newWorkspace); queryClient.setQueryData(workspacesQueryKey(workspace), (workspaces) => (workspaces ?? []).map((w) => (w.id === newWorkspace.id ? newWorkspace : w)), ); diff --git a/src-web/lib/analytics.ts b/src-web/lib/analytics.ts index 03f9a74b..f7e47bef 100644 --- a/src-web/lib/analytics.ts +++ b/src-web/lib/analytics.ts @@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api'; export function trackEvent( resource: | 'App' + | 'CookieJar' | 'Sidebar' | 'Workspace' | 'Environment' diff --git a/src-web/lib/models.ts b/src-web/lib/models.ts index 7047543a..a68ea24f 100644 --- a/src-web/lib/models.ts +++ b/src-web/lib/models.ts @@ -9,7 +9,14 @@ export const AUTH_TYPE_NONE = null; export const AUTH_TYPE_BASIC = 'basic'; export const AUTH_TYPE_BEARER = 'bearer'; -export type Model = Settings | Workspace | HttpRequest | HttpResponse | KeyValue | Environment; +export type Model = + | Settings + | Workspace + | HttpRequest + | HttpResponse + | KeyValue + | Environment + | CookieJar; export interface BaseModel { readonly id: string; @@ -34,6 +41,33 @@ export interface Workspace extends BaseModel { settingRequestTimeout: number; } +export interface CookieJar extends BaseModel { + readonly model: 'cookie_jar'; + workspaceId: string; + name: string; + cookies: Cookie[]; +} + +export interface Cookie { + raw_cookie: string; + domain: { HostOnly: string } | { Suffix: string } | 'NotPresent' | 'Empty'; + expires: { AtUtc: string } | 'SessionEnd'; + path: [string, boolean]; +} + +export function cookieDomain(cookie: Cookie): string { + if (cookie.domain === 'NotPresent' || cookie.domain === 'Empty') { + return 'n/a'; + } + if ('HostOnly' in cookie.domain) { + return cookie.domain.HostOnly; + } + if ('Suffix' in cookie.domain) { + return cookie.domain.Suffix; + } + return 'unknown'; +} + export interface EnvironmentVariable { name: string; value: string; diff --git a/src-web/lib/store.ts b/src-web/lib/store.ts index a5ecfa6e..103e19c6 100644 --- a/src-web/lib/store.ts +++ b/src-web/lib/store.ts @@ -1,5 +1,13 @@ import { invoke } from '@tauri-apps/api'; -import type { Environment, Folder, HttpRequest, Settings, Workspace } from './models'; +import type { + Cookie, + CookieJar, + Environment, + Folder, + HttpRequest, + Settings, + Workspace, +} from './models'; export async function getSettings(): Promise { return invoke('get_settings', {}); @@ -40,3 +48,12 @@ export async function getWorkspace(id: string | null): Promise } return workspace; } + +export async function getCookieJar(id: string | null): Promise { + if (id === null) return null; + const cookieJar: CookieJar = (await invoke('get_cookie_jar', { id })) ?? null; + if (cookieJar == null) { + return null; + } + return cookieJar; +}