From df83a61d6f468dfd2644a9ebe4df3f0bc128bc9a Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 13 Nov 2023 10:52:11 -0800 Subject: [PATCH] Url parameters done --- ...6da92cc30919d5bdb0a0226ea5e30d5b30c0f.json | 12 + ...1b8e4fbedfb45751d6cd33bd42e518b634dd.json} | 24 +- ...b46381e2cc16ba0c553713d0b6c64354eb39.json} | 24 +- ...b2af06e9dea4bb24763d717c72a840450a759.json | 12 - .../migrations/20231113183810_url_params.sql | 2 + src-tauri/src/main.rs | 194 +---------------- src-tauri/src/models.rs | 21 +- src-tauri/src/send.rs | 205 ++++++++++++++++++ src-tauri/src/updates.rs | 2 +- .../{HeaderEditor.tsx => HeadersEditor.tsx} | 2 +- src-web/components/ParameterEditor.tsx | 13 +- src-web/components/RequestPane.tsx | 28 ++- src-web/lib/models.ts | 7 + 13 files changed, 308 insertions(+), 238 deletions(-) create mode 100644 src-tauri/.sqlx/query-4a5fd6c81401ccafac64b05cb476da92cc30919d5bdb0a0226ea5e30d5b30c0f.json rename src-tauri/.sqlx/{query-55e4e8b66c18f85d17ada00b302720e5dcd35ec4288006e4a556448d59b63952.json => query-6483f3ffeb90e019e9078d98bb831b8e4fbedfb45751d6cd33bd42e518b634dd.json} (74%) rename src-tauri/.sqlx/{query-ae31827b9576ffba83a9de05e30688df3c83e145860f8dd608410a9a9254659d.json => query-7a7bc4df7e52ad3a987c97af8f43b46381e2cc16ba0c553713d0b6c64354eb39.json} (73%) delete mode 100644 src-tauri/.sqlx/query-e5b410442b00ee354bb58eb0e8fb2af06e9dea4bb24763d717c72a840450a759.json create mode 100644 src-tauri/migrations/20231113183810_url_params.sql create mode 100644 src-tauri/src/send.rs rename src-web/components/{HeaderEditor.tsx => HeadersEditor.tsx} (96%) diff --git a/src-tauri/.sqlx/query-4a5fd6c81401ccafac64b05cb476da92cc30919d5bdb0a0226ea5e30d5b30c0f.json b/src-tauri/.sqlx/query-4a5fd6c81401ccafac64b05cb476da92cc30919d5bdb0a0226ea5e30d5b30c0f.json new file mode 100644 index 00000000..fed65bea --- /dev/null +++ b/src-tauri/.sqlx/query-4a5fd6c81401ccafac64b05cb476da92cc30919d5bdb0a0226ea5e30d5b30c0f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n folder_id,\n name,\n url,\n url_parameters,\n method,\n body,\n body_type,\n authentication,\n authentication_type,\n headers,\n sort_priority\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n authentication = excluded.authentication,\n authentication_type = excluded.authentication_type,\n url = excluded.url,\n url_parameters = excluded.url_parameters,\n sort_priority = excluded.sort_priority\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "4a5fd6c81401ccafac64b05cb476da92cc30919d5bdb0a0226ea5e30d5b30c0f" +} diff --git a/src-tauri/.sqlx/query-55e4e8b66c18f85d17ada00b302720e5dcd35ec4288006e4a556448d59b63952.json b/src-tauri/.sqlx/query-6483f3ffeb90e019e9078d98bb831b8e4fbedfb45751d6cd33bd42e518b634dd.json similarity index 74% rename from src-tauri/.sqlx/query-55e4e8b66c18f85d17ada00b302720e5dcd35ec4288006e4a556448d59b63952.json rename to src-tauri/.sqlx/query-6483f3ffeb90e019e9078d98bb831b8e4fbedfb45751d6cd33bd42e518b634dd.json index 913e802e..9156664c 100644 --- a/src-tauri/.sqlx/query-55e4e8b66c18f85d17ada00b302720e5dcd35ec4288006e4a556448d59b63952.json +++ b/src-tauri/.sqlx/query-6483f3ffeb90e019e9078d98bb831b8e4fbedfb45751d6cd33bd42e518b634dd.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body AS \"body!: Json>\",\n body_type,\n authentication AS \"authentication!: Json>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_requests\n WHERE workspace_id = ?\n ", + "query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n url_parameters AS \"url_parameters!: sqlx::types::Json>\",\n method,\n body AS \"body!: Json>\",\n body_type,\n authentication AS \"authentication!: Json>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_requests\n WHERE id = ?\n ", "describe": { "columns": [ { @@ -44,38 +44,43 @@ "type_info": "Text" }, { - "name": "method", + "name": "url_parameters!: sqlx::types::Json>", "ordinal": 8, "type_info": "Text" }, { - "name": "body!: Json>", + "name": "method", "ordinal": 9, "type_info": "Text" }, { - "name": "body_type", + "name": "body!: Json>", "ordinal": 10, "type_info": "Text" }, { - "name": "authentication!: Json>", + "name": "body_type", "ordinal": 11, "type_info": "Text" }, { - "name": "authentication_type", + "name": "authentication!: Json>", "ordinal": 12, "type_info": "Text" }, { - "name": "sort_priority", + "name": "authentication_type", "ordinal": 13, + "type_info": "Text" + }, + { + "name": "sort_priority", + "ordinal": 14, "type_info": "Float" }, { "name": "headers!: sqlx::types::Json>", - "ordinal": 14, + "ordinal": 15, "type_info": "Text" } ], @@ -93,6 +98,7 @@ false, false, false, + false, true, false, true, @@ -100,5 +106,5 @@ false ] }, - "hash": "55e4e8b66c18f85d17ada00b302720e5dcd35ec4288006e4a556448d59b63952" + "hash": "6483f3ffeb90e019e9078d98bb831b8e4fbedfb45751d6cd33bd42e518b634dd" } diff --git a/src-tauri/.sqlx/query-ae31827b9576ffba83a9de05e30688df3c83e145860f8dd608410a9a9254659d.json b/src-tauri/.sqlx/query-7a7bc4df7e52ad3a987c97af8f43b46381e2cc16ba0c553713d0b6c64354eb39.json similarity index 73% rename from src-tauri/.sqlx/query-ae31827b9576ffba83a9de05e30688df3c83e145860f8dd608410a9a9254659d.json rename to src-tauri/.sqlx/query-7a7bc4df7e52ad3a987c97af8f43b46381e2cc16ba0c553713d0b6c64354eb39.json index f5e3ddd7..e919ca51 100644 --- a/src-tauri/.sqlx/query-ae31827b9576ffba83a9de05e30688df3c83e145860f8dd608410a9a9254659d.json +++ b/src-tauri/.sqlx/query-7a7bc4df7e52ad3a987c97af8f43b46381e2cc16ba0c553713d0b6c64354eb39.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body AS \"body!: Json>\",\n body_type,\n authentication AS \"authentication!: Json>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_requests\n WHERE id = ?\n ", + "query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n url_parameters AS \"url_parameters!: sqlx::types::Json>\",\n method,\n body AS \"body!: Json>\",\n body_type,\n authentication AS \"authentication!: Json>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_requests\n WHERE workspace_id = ?\n ", "describe": { "columns": [ { @@ -44,38 +44,43 @@ "type_info": "Text" }, { - "name": "method", + "name": "url_parameters!: sqlx::types::Json>", "ordinal": 8, "type_info": "Text" }, { - "name": "body!: Json>", + "name": "method", "ordinal": 9, "type_info": "Text" }, { - "name": "body_type", + "name": "body!: Json>", "ordinal": 10, "type_info": "Text" }, { - "name": "authentication!: Json>", + "name": "body_type", "ordinal": 11, "type_info": "Text" }, { - "name": "authentication_type", + "name": "authentication!: Json>", "ordinal": 12, "type_info": "Text" }, { - "name": "sort_priority", + "name": "authentication_type", "ordinal": 13, + "type_info": "Text" + }, + { + "name": "sort_priority", + "ordinal": 14, "type_info": "Float" }, { "name": "headers!: sqlx::types::Json>", - "ordinal": 14, + "ordinal": 15, "type_info": "Text" } ], @@ -93,6 +98,7 @@ false, false, false, + false, true, false, true, @@ -100,5 +106,5 @@ false ] }, - "hash": "ae31827b9576ffba83a9de05e30688df3c83e145860f8dd608410a9a9254659d" + "hash": "7a7bc4df7e52ad3a987c97af8f43b46381e2cc16ba0c553713d0b6c64354eb39" } diff --git a/src-tauri/.sqlx/query-e5b410442b00ee354bb58eb0e8fb2af06e9dea4bb24763d717c72a840450a759.json b/src-tauri/.sqlx/query-e5b410442b00ee354bb58eb0e8fb2af06e9dea4bb24763d717c72a840450a759.json deleted file mode 100644 index 05388c5f..00000000 --- a/src-tauri/.sqlx/query-e5b410442b00ee354bb58eb0e8fb2af06e9dea4bb24763d717c72a840450a759.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n folder_id,\n name,\n url,\n method,\n body,\n body_type,\n authentication,\n authentication_type,\n headers,\n sort_priority\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n authentication = excluded.authentication,\n authentication_type = excluded.authentication_type,\n url = excluded.url,\n sort_priority = excluded.sort_priority\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 12 - }, - "nullable": [] - }, - "hash": "e5b410442b00ee354bb58eb0e8fb2af06e9dea4bb24763d717c72a840450a759" -} diff --git a/src-tauri/migrations/20231113183810_url_params.sql b/src-tauri/migrations/20231113183810_url_params.sql new file mode 100644 index 00000000..acdd827a --- /dev/null +++ b/src-tauri/migrations/20231113183810_url_params.sql @@ -0,0 +1,2 @@ +ALTER TABLE http_requests + ADD COLUMN url_parameters TEXT NOT NULL DEFAULT '[]'; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 540beb0d..d42c583e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -10,16 +10,11 @@ extern crate objc; use std::collections::HashMap; use std::env::current_dir; use std::fs::{create_dir_all, File}; -use std::io::Write; use std::process::exit; -use base64::Engine; use fern::colors::ColoredLevelConfig; -use http::{HeaderMap, HeaderValue, Method}; -use http::header::{ACCEPT, HeaderName, USER_AGENT}; -use log::{debug, error, info, warn}; +use log::{debug, error, info}; use rand::random; -use reqwest::redirect::Policy; use serde::Serialize; use sqlx::{Pool, Sqlite, SqlitePool}; use sqlx::migrate::Migrator; @@ -36,6 +31,7 @@ use window_ext::TrafficLightWindowExt; use crate::analytics::{AnalyticsAction, AnalyticsResource, track_event}; use crate::plugin::{ImportResources, ImportResult}; +use crate::send::actually_send_request; use crate::updates::YaakUpdater; mod analytics; @@ -45,6 +41,7 @@ mod render; mod window_ext; mod window_menu; mod updates; +mod send; #[derive(serde::Serialize)] pub struct CustomResponse { @@ -88,188 +85,6 @@ async fn send_ephemeral_request( actually_send_request(request, &response, &environment_id2, &app_handle, pool).await } -async fn actually_send_request( - request: models::HttpRequest, - response: &models::HttpResponse, - environment_id: &str, - app_handle: &AppHandle, - pool: &Pool, -) -> 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 - .expect("Failed to get Workspace"); - - let mut url_string = render::render(&request.url, &workspace, environment.as_ref()); - - if !url_string.starts_with("http://") && !url_string.starts_with("https://") { - url_string = format!("http://{}", url_string); - } - - let client = reqwest::Client::builder() - .redirect(Policy::none()) - // .danger_accept_invalid_certs(true) - .build() - .expect("Failed to build client"); - - let mut headers = HeaderMap::new(); - headers.insert(USER_AGENT, HeaderValue::from_static("yaak")); - headers.insert(ACCEPT, HeaderValue::from_static("*/*")); - - for h in request.headers.0 { - if h.name.is_empty() && h.value.is_empty() { - continue; - } - - if !h.enabled { - continue; - } - - let name = render::render(&h.name, &workspace, environment_ref); - let value = render::render(&h.value, &workspace, environment_ref); - - let header_name = match HeaderName::from_bytes(name.as_bytes()) { - Ok(n) => n, - Err(e) => { - eprintln!("Failed to create header name: {}", e); - continue; - } - }; - let header_value = match HeaderValue::from_str(value.as_str()) { - Ok(n) => n, - Err(e) => { - eprintln!("Failed to create header value: {}", e); - continue; - } - }; - - headers.insert(header_name, header_value); - } - - if let Some(b) = &request.authentication_type { - let empty_value = &serde_json::to_value("").unwrap(); - let a = request.authentication.0; - - if b == "basic" { - let raw_username = a - .get("username") - .unwrap_or(empty_value) - .as_str() - .unwrap_or(""); - let raw_password = a - .get("password") - .unwrap_or(empty_value) - .as_str() - .unwrap_or(""); - let username = render::render(raw_username, &workspace, environment_ref); - let password = render::render(raw_password, &workspace, environment_ref); - - let auth = format!("{username}:{password}"); - let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth); - headers.insert( - "Authorization", - HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(), - ); - } else if b == "bearer" { - let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or(""); - let token = render::render(raw_token, &workspace, environment_ref); - headers.insert( - "Authorization", - HeaderValue::from_str(&format!("Bearer {token}")).unwrap(), - ); - } - } - - 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()).headers(headers); - - if let Some(t) = &request.body_type { - let empty_value = &serde_json::to_value("").unwrap(); - let b = request.body.0; - - if b.contains_key("text") { - let raw_text = b.get("text").unwrap_or(empty_value).as_str().unwrap_or(""); - let body = render::render(raw_text, &workspace, environment_ref); - request_builder = request_builder.body(body); - } else { - warn!("Unsupported body type: {}", t); - } - } - - let sendable_req = match request_builder.build() { - Ok(r) => r, - Err(e) => { - return response_err(response, e.to_string(), app_handle, pool).await; - } - }; - - let raw_response = client.execute(sendable_req).await; - - match raw_response { - Ok(v) => { - let mut response = response.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() - .iter() - .map(|(k, v)| models::HttpResponseHeader { - name: k.as_str().to_string(), - value: v.to_str().unwrap().to_string(), - }) - .collect(), - ); - response.url = v.url().to_string(); - let body_bytes = v.bytes().await.expect("Failed to get body").to_vec(); - response.content_length = Some(body_bytes.len() as i64); - - { - // Write body to FS - let dir = app_handle.path_resolver().app_data_dir().unwrap(); - let base_dir = dir.join("responses"); - create_dir_all(base_dir.clone()).expect("Failed to create responses dir"); - let body_path = match response.id.is_empty() { - false => base_dir.join(response.id.clone()), - true => base_dir.join(uuid::Uuid::new_v4().to_string()), - }; - let mut f = File::options() - .create(true) - .truncate(true) - .write(true) - .open(&body_path) - .expect("Failed to open file"); - f.write_all(body_bytes.as_slice()) - .expect("Failed to write to file"); - response.body_path = Some( - body_path - .to_str() - .expect("Failed to get body path") - .to_string(), - ); - } - - // Also store body directly on the model, if small enough - if body_bytes.len() < 100_000 { - response.body = Some(body_bytes); - } - - response.elapsed = start.elapsed().as_millis() as i64; - response = models::update_response_if_id(&response, pool) - .await - .expect("Failed to update response"); - if !request.id.is_empty() { - emit_side_effect(app_handle, "updated_model", &response); - } - Ok(response) - } - Err(e) => response_err(response, e.to_string(), app_handle, pool).await, - } -} - #[tauri::command] async fn import_data( window: Window, @@ -925,8 +740,9 @@ fn main() { None, ); } - RunEvent::WindowEvent { label, event: WindowEvent::Focused(true), .. } => { + RunEvent::WindowEvent { label: _label, event: WindowEvent::Focused(true), .. } => { let h = app_handle.clone(); + // Run update check whenever window is focused tauri::async_runtime::spawn(async move { let val: State<'_, Mutex> = h.state(); _ = val.lock().await.check(&h).await; diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index e7ab1fc3..e4d70906 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -3,9 +3,9 @@ use std::fs; use rand::distributions::{Alphanumeric, DistString}; use serde::{Deserialize, Serialize}; -use sqlx::types::chrono::NaiveDateTime; -use sqlx::types::{Json, JsonValue}; use sqlx::{Pool, Sqlite}; +use sqlx::types::{Json, JsonValue}; +use sqlx::types::chrono::NaiveDateTime; use tauri::AppHandle; #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] @@ -54,6 +54,15 @@ pub struct HttpRequestHeader { pub value: String, } +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default, rename_all = "camelCase")] +pub struct HttpUrlParameter { + #[serde(default = "default_enabled")] + pub enabled: bool, + pub name: String, + pub value: String, +} + fn default_http_request_method() -> String { "GET".to_string() } @@ -70,6 +79,7 @@ pub struct HttpRequest { pub sort_priority: f64, pub name: String, pub url: String, + pub url_parameters: Json>, #[serde(default = "default_http_request_method")] pub method: String, pub body: Json>, @@ -439,6 +449,7 @@ pub async fn upsert_request( folder_id, name, url, + url_parameters, method, body, body_type, @@ -447,7 +458,7 @@ pub async fn upsert_request( headers, sort_priority ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET updated_at = CURRENT_TIMESTAMP, name = excluded.name, @@ -459,6 +470,7 @@ pub async fn upsert_request( authentication = excluded.authentication, authentication_type = excluded.authentication_type, url = excluded.url, + url_parameters = excluded.url_parameters, sort_priority = excluded.sort_priority "#, id, @@ -466,6 +478,7 @@ pub async fn upsert_request( r.folder_id, trimmed_name, r.url, + r.url_parameters, r.method, r.body, r.body_type, @@ -496,6 +509,7 @@ pub async fn find_requests( updated_at, name, url, + url_parameters AS "url_parameters!: sqlx::types::Json>", method, body AS "body!: Json>", body_type, @@ -525,6 +539,7 @@ pub async fn get_request(id: &str, pool: &Pool) -> Result>", method, body AS "body!: Json>", body_type, diff --git a/src-tauri/src/send.rs b/src-tauri/src/send.rs new file mode 100644 index 00000000..c49d1a4d --- /dev/null +++ b/src-tauri/src/send.rs @@ -0,0 +1,205 @@ +use std::fs::{create_dir_all, File}; +use std::io::Write; + +use base64::Engine; +use http::{HeaderMap, HeaderName, HeaderValue, Method}; +use http::header::{ACCEPT, USER_AGENT}; +use log::warn; +use reqwest::redirect::Policy; +use sqlx::{Pool, Sqlite}; +use sqlx::types::Json; +use tauri::{AppHandle, Wry}; + +use crate::{emit_side_effect, models, render, response_err}; + +pub async fn actually_send_request( + request: models::HttpRequest, + response: &models::HttpResponse, + environment_id: &str, + app_handle: &AppHandle, + pool: &Pool, +) -> 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 + .expect("Failed to get Workspace"); + + let mut url_string = render::render(&request.url, &workspace, environment.as_ref()); + + if !url_string.starts_with("http://") && !url_string.starts_with("https://") { + url_string = format!("http://{}", url_string); + } + + let client = reqwest::Client::builder() + .redirect(Policy::none()) + // .danger_accept_invalid_certs(true) + .build() + .expect("Failed to build client"); + + let mut headers = HeaderMap::new(); + headers.insert(USER_AGENT, HeaderValue::from_static("yaak")); + headers.insert(ACCEPT, HeaderValue::from_static("*/*")); + + for h in request.headers.0 { + if h.name.is_empty() && h.value.is_empty() { + continue; + } + + if !h.enabled { + continue; + } + + let name = render::render(&h.name, &workspace, environment_ref); + let value = render::render(&h.value, &workspace, environment_ref); + + let header_name = match HeaderName::from_bytes(name.as_bytes()) { + Ok(n) => n, + Err(e) => { + eprintln!("Failed to create header name: {}", e); + continue; + } + }; + let header_value = match HeaderValue::from_str(value.as_str()) { + Ok(n) => n, + Err(e) => { + eprintln!("Failed to create header value: {}", e); + continue; + } + }; + + headers.insert(header_name, header_value); + } + + if let Some(b) = &request.authentication_type { + let empty_value = &serde_json::to_value("").unwrap(); + let a = request.authentication.0; + + if b == "basic" { + let raw_username = a + .get("username") + .unwrap_or(empty_value) + .as_str() + .unwrap_or(""); + let raw_password = a + .get("password") + .unwrap_or(empty_value) + .as_str() + .unwrap_or(""); + let username = render::render(raw_username, &workspace, environment_ref); + let password = render::render(raw_password, &workspace, environment_ref); + + let auth = format!("{username}:{password}"); + let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth); + headers.insert( + "Authorization", + HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(), + ); + } else if b == "bearer" { + let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or(""); + let token = render::render(raw_token, &workspace, environment_ref); + headers.insert( + "Authorization", + HeaderValue::from_str(&format!("Bearer {token}")).unwrap(), + ); + } + } + + 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()).headers(headers); + + let mut query_params = Vec::new(); + for p in request.url_parameters.0 { + if !p.enabled || p.name.is_empty() { continue; } + query_params.push(( + render::render(&p.name, &workspace, environment_ref), + render::render(&p.value, &workspace, environment_ref), + )); + } + request_builder = request_builder.query(&query_params); + + if let Some(t) = &request.body_type { + let empty_value = &serde_json::to_value("").unwrap(); + let b = request.body.0; + + if b.contains_key("text") { + let raw_text = b.get("text").unwrap_or(empty_value).as_str().unwrap_or(""); + let body = render::render(raw_text, &workspace, environment_ref); + request_builder = request_builder.body(body); + } else { + warn!("Unsupported body type: {}", t); + } + } + + let sendable_req = match request_builder.build() { + Ok(r) => r, + Err(e) => { + return response_err(response, e.to_string(), app_handle, pool).await; + } + }; + + let raw_response = client.execute(sendable_req).await; + + match raw_response { + Ok(v) => { + let mut response = response.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() + .iter() + .map(|(k, v)| models::HttpResponseHeader { + name: k.as_str().to_string(), + value: v.to_str().unwrap().to_string(), + }) + .collect(), + ); + response.url = v.url().to_string(); + let body_bytes = v.bytes().await.expect("Failed to get body").to_vec(); + response.content_length = Some(body_bytes.len() as i64); + + { + // Write body to FS + let dir = app_handle.path_resolver().app_data_dir().unwrap(); + let base_dir = dir.join("responses"); + create_dir_all(base_dir.clone()).expect("Failed to create responses dir"); + let body_path = match response.id.is_empty() { + false => base_dir.join(response.id.clone()), + true => base_dir.join(uuid::Uuid::new_v4().to_string()), + }; + let mut f = File::options() + .create(true) + .truncate(true) + .write(true) + .open(&body_path) + .expect("Failed to open file"); + f.write_all(body_bytes.as_slice()) + .expect("Failed to write to file"); + response.body_path = Some( + body_path + .to_str() + .expect("Failed to get body path") + .to_string(), + ); + } + + // Also store body directly on the model, if small enough + if body_bytes.len() < 100_000 { + response.body = Some(body_bytes); + } + + response.elapsed = start.elapsed().as_millis() as i64; + response = models::update_response_if_id(&response, pool) + .await + .expect("Failed to update response"); + if !request.id.is_empty() { + emit_side_effect(app_handle, "updated_model", &response); + } + Ok(response) + } + Err(e) => response_err(response, e.to_string(), app_handle, pool).await, + } +} diff --git a/src-tauri/src/updates.rs b/src-tauri/src/updates.rs index 46333cf7..65bb7444 100644 --- a/src-tauri/src/updates.rs +++ b/src-tauri/src/updates.rs @@ -4,7 +4,7 @@ use tauri::{AppHandle, updater, Window, Wry}; use tauri::api::dialog; // Check for updates every 3 hours -const MAX_UPDATE_CHECK_SECONDS: u64 = 3600 * 3; +const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60 * 3; // Create updater struct pub struct YaakUpdater { diff --git a/src-web/components/HeaderEditor.tsx b/src-web/components/HeadersEditor.tsx similarity index 96% rename from src-web/components/HeaderEditor.tsx rename to src-web/components/HeadersEditor.tsx index 9e42c441..e0565878 100644 --- a/src-web/components/HeaderEditor.tsx +++ b/src-web/components/HeadersEditor.tsx @@ -14,7 +14,7 @@ type Props = { onChange: (headers: HttpRequest['headers']) => void; }; -export function HeaderEditor({ headers, onChange, forceUpdateKey }: Props) { +export function HeadersEditor({ headers, onChange, forceUpdateKey }: Props) { return ( void; + urlParameters: HttpRequest['headers']; + onChange: (headers: HttpRequest['urlParameters']) => void; }; -export function ParametersEditor({ parameters, forceUpdateKey, onChange }: Props) { +export function UrlParametersEditor({ urlParameters, forceUpdateKey, onChange }: Props) { return ( ); } diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index 604de7df..81b691e5 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -10,7 +10,7 @@ import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { tryFormatJson } from '../lib/formatters'; -import type { HttpHeader, HttpRequest } from '../lib/models'; +import type { HttpHeader, HttpRequest, HttpUrlParameter } from '../lib/models'; import { AUTH_TYPE_BASIC, AUTH_TYPE_BEARER, @@ -28,8 +28,8 @@ import type { TabItem } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs'; import { EmptyStateText } from './EmptyStateText'; import { GraphQLEditor } from './GraphQLEditor'; -import { HeaderEditor } from './HeaderEditor'; -import { ParametersEditor } from './ParameterEditor'; +import { HeadersEditor } from './HeadersEditor'; +import { UrlParametersEditor } from './ParameterEditor'; import { UrlBar } from './UrlBar'; interface Props { @@ -92,7 +92,15 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN }, }, }, - // { value: 'params', label: 'URL Params' }, + { + value: 'params', + label: ( +
+ Params + p.name).length} /> +
+ ), + }, { value: 'headers', label: ( @@ -141,6 +149,10 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN (headers: HttpHeader[]) => updateRequest.mutate({ headers }), [updateRequest], ); + const handleUrlParametersChange = useCallback( + (urlParameters: HttpUrlParameter[]) => updateRequest.mutate({ urlParameters }), + [updateRequest], + ); useListenToTauriEvent( 'send_request', @@ -189,17 +201,17 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN )} - - null} + urlParameters={activeRequest.urlParameters} + onChange={handleUrlParametersChange} /> diff --git a/src-web/lib/models.ts b/src-web/lib/models.ts index 6a208c8b..c3bb2ca0 100644 --- a/src-web/lib/models.ts +++ b/src-web/lib/models.ts @@ -49,6 +49,12 @@ export interface HttpHeader { enabled?: boolean; } +export interface HttpUrlParameter { + name: string; + value: string; + enabled?: boolean; +} + export interface HttpRequest extends BaseModel { readonly workspaceId: string; readonly model: 'http_request'; @@ -56,6 +62,7 @@ export interface HttpRequest extends BaseModel { sortPriority: number; name: string; url: string; + urlParameters: HttpUrlParameter[]; body: Record; bodyType: string | null; authentication: Record;