From 8d29fad2617d54ceddc68ecbce02ff6972668cb1 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sat, 17 Feb 2024 23:47:28 -0800 Subject: [PATCH] gRPC authentication --- ...828deebd684fc9f78e9f8595a550f90749fe.json} | 18 +++++- ...c9f1b50f3fe53d7735194d91927b5d14a436.json} | 18 +++++- ...4c6060c3d2a209689a879824dea4d26e5497e.json | 12 ++++ ...fc30eaeffeed6883e712bda4b4d6ca49cf740.json | 12 ---- src-tauri/grpc/src/manager.rs | 45 ++++++++++++--- .../20240218071057_grpc-authentication.sql | 2 + src-tauri/src/main.rs | 56 +++++++++++++++++-- src-tauri/src/models.rs | 18 ++++-- src-web/components/BasicAuth.tsx | 51 +++++++++++------ src-web/components/BearerAuth.tsx | 30 ++++++---- src-web/components/GrpcConnectionLayout.tsx | 11 ++-- .../components/GrpcConnectionSetupPane.tsx | 55 ++++++++++-------- src-web/components/RequestPane.tsx | 14 +---- src-web/components/core/Tabs/Tabs.tsx | 2 +- src-web/hooks/useGrpc.ts | 23 ++------ src-web/lib/models.ts | 2 + 16 files changed, 246 insertions(+), 123 deletions(-) rename src-tauri/.sqlx/{query-7398403d3de2dc5c5b4b6392f083041d9a55194bb97819225a2612fdeb60ad42.json => query-0d9e685f878fc2a0e1803c6aaae3828deebd684fc9f78e9f8595a550f90749fe.json} (72%) rename src-tauri/.sqlx/{query-761d27c3ec425c37ad9abe9c732a9c1746c81ca50d2c413e540b74c8c8e908b7.json => query-545d21ff21bd02468be86746325dc9f1b50f3fe53d7735194d91927b5d14a436.json} (71%) create mode 100644 src-tauri/.sqlx/query-c554305252cb21e34aa1e3c1f204c6060c3d2a209689a879824dea4d26e5497e.json delete mode 100644 src-tauri/.sqlx/query-ee562f85ec28c554c607adde670fc30eaeffeed6883e712bda4b4d6ca49cf740.json create mode 100644 src-tauri/migrations/20240218071057_grpc-authentication.sql diff --git a/src-tauri/.sqlx/query-7398403d3de2dc5c5b4b6392f083041d9a55194bb97819225a2612fdeb60ad42.json b/src-tauri/.sqlx/query-0d9e685f878fc2a0e1803c6aaae3828deebd684fc9f78e9f8595a550f90749fe.json similarity index 72% rename from src-tauri/.sqlx/query-7398403d3de2dc5c5b4b6392f083041d9a55194bb97819225a2612fdeb60ad42.json rename to src-tauri/.sqlx/query-0d9e685f878fc2a0e1803c6aaae3828deebd684fc9f78e9f8595a550f90749fe.json index 15e4f800..4b82c0a6 100644 --- a/src-tauri/.sqlx/query-7398403d3de2dc5c5b4b6392f083041d9a55194bb97819225a2612fdeb60ad42.json +++ b/src-tauri/.sqlx/query-0d9e685f878fc2a0e1803c6aaae3828deebd684fc9f78e9f8595a550f90749fe.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,\n url, service, method, message,\n proto_files AS \"proto_files!: sqlx::types::Json>\"\n FROM grpc_requests\n WHERE id = ?\n ", + "query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,\n url, service, method, message, authentication_type,\n authentication AS \"authentication!: Json>\",\n proto_files AS \"proto_files!: sqlx::types::Json>\"\n FROM grpc_requests\n WHERE id = ?\n ", "describe": { "columns": [ { @@ -64,9 +64,19 @@ "type_info": "Text" }, { - "name": "proto_files!: sqlx::types::Json>", + "name": "authentication_type", "ordinal": 12, "type_info": "Text" + }, + { + "name": "authentication!: Json>", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "proto_files!: sqlx::types::Json>", + "ordinal": 14, + "type_info": "Text" } ], "parameters": { @@ -85,8 +95,10 @@ true, true, false, + true, + false, false ] }, - "hash": "7398403d3de2dc5c5b4b6392f083041d9a55194bb97819225a2612fdeb60ad42" + "hash": "0d9e685f878fc2a0e1803c6aaae3828deebd684fc9f78e9f8595a550f90749fe" } diff --git a/src-tauri/.sqlx/query-761d27c3ec425c37ad9abe9c732a9c1746c81ca50d2c413e540b74c8c8e908b7.json b/src-tauri/.sqlx/query-545d21ff21bd02468be86746325dc9f1b50f3fe53d7735194d91927b5d14a436.json similarity index 71% rename from src-tauri/.sqlx/query-761d27c3ec425c37ad9abe9c732a9c1746c81ca50d2c413e540b74c8c8e908b7.json rename to src-tauri/.sqlx/query-545d21ff21bd02468be86746325dc9f1b50f3fe53d7735194d91927b5d14a436.json index c24f110b..d2e4bdb7 100644 --- a/src-tauri/.sqlx/query-761d27c3ec425c37ad9abe9c732a9c1746c81ca50d2c413e540b74c8c8e908b7.json +++ b/src-tauri/.sqlx/query-545d21ff21bd02468be86746325dc9f1b50f3fe53d7735194d91927b5d14a436.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,\n url, service, method, message,\n proto_files AS \"proto_files!: sqlx::types::Json>\"\n FROM grpc_requests\n WHERE workspace_id = ?\n ", + "query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,\n url, service, method, message, authentication_type,\n authentication AS \"authentication!: Json>\",\n proto_files AS \"proto_files!: sqlx::types::Json>\"\n FROM grpc_requests\n WHERE workspace_id = ?\n ", "describe": { "columns": [ { @@ -64,9 +64,19 @@ "type_info": "Text" }, { - "name": "proto_files!: sqlx::types::Json>", + "name": "authentication_type", "ordinal": 12, "type_info": "Text" + }, + { + "name": "authentication!: Json>", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "proto_files!: sqlx::types::Json>", + "ordinal": 14, + "type_info": "Text" } ], "parameters": { @@ -85,8 +95,10 @@ true, true, false, + true, + false, false ] }, - "hash": "761d27c3ec425c37ad9abe9c732a9c1746c81ca50d2c413e540b74c8c8e908b7" + "hash": "545d21ff21bd02468be86746325dc9f1b50f3fe53d7735194d91927b5d14a436" } diff --git a/src-tauri/.sqlx/query-c554305252cb21e34aa1e3c1f204c6060c3d2a209689a879824dea4d26e5497e.json b/src-tauri/.sqlx/query-c554305252cb21e34aa1e3c1f204c6060c3d2a209689a879824dea4d26e5497e.json new file mode 100644 index 00000000..c5db2a1c --- /dev/null +++ b/src-tauri/.sqlx/query-c554305252cb21e34aa1e3c1f204c6060c3d2a209689a879824dea4d26e5497e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO grpc_requests (\n id, name, workspace_id, folder_id, sort_priority, url, service, method, message,\n proto_files, authentication_type, authentication\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 sort_priority = excluded.sort_priority,\n url = excluded.url,\n service = excluded.service,\n method = excluded.method,\n message = excluded.message,\n proto_files = excluded.proto_files,\n authentication_type = excluded.authentication_type,\n authentication = excluded.authentication\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 12 + }, + "nullable": [] + }, + "hash": "c554305252cb21e34aa1e3c1f204c6060c3d2a209689a879824dea4d26e5497e" +} diff --git a/src-tauri/.sqlx/query-ee562f85ec28c554c607adde670fc30eaeffeed6883e712bda4b4d6ca49cf740.json b/src-tauri/.sqlx/query-ee562f85ec28c554c607adde670fc30eaeffeed6883e712bda4b4d6ca49cf740.json deleted file mode 100644 index c1183863..00000000 --- a/src-tauri/.sqlx/query-ee562f85ec28c554c607adde670fc30eaeffeed6883e712bda4b4d6ca49cf740.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO grpc_requests (\n id, name, workspace_id, folder_id, sort_priority, url, service, method, message,\n proto_files\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 sort_priority = excluded.sort_priority,\n url = excluded.url,\n service = excluded.service,\n method = excluded.method,\n message = excluded.message,\n proto_files = excluded.proto_files\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 10 - }, - "nullable": [] - }, - "hash": "ee562f85ec28c554c607adde670fc30eaeffeed6883e712bda4b4d6ca49cf740" -} diff --git a/src-tauri/grpc/src/manager.rs b/src-tauri/grpc/src/manager.rs index f9c98533..85eecc30 100644 --- a/src-tauri/grpc/src/manager.rs +++ b/src-tauri/grpc/src/manager.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::path::PathBuf; +use std::str::FromStr; use hyper::client::HttpConnector; use hyper::Client; @@ -10,8 +11,9 @@ use serde_json::Deserializer; use tokio_stream::wrappers::ReceiverStream; use tokio_stream::StreamExt; use tonic::body::BoxBody; +use tonic::metadata::{MetadataKey, MetadataValue}; use tonic::transport::Uri; -use tonic::{IntoRequest, IntoStreamingRequest, Response, Status, Streaming}; +use tonic::{IntoRequest, IntoStreamingRequest, Request, Response, Status, Streaming}; use crate::codec::DynamicCodec; use crate::proto::{fill_pool, fill_pool_from_files, get_transport, method_desc_to_path}; @@ -47,6 +49,7 @@ impl GrpcConnection { service: &str, method: &str, message: &str, + metadata: HashMap, ) -> Result { let method = &self.method(&service, &method)?; let input_message = method.input(); @@ -58,7 +61,9 @@ impl GrpcConnection { let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()); - let req = req_message.into_request(); + let mut req = req_message.into_request(); + decorate_req(metadata, &mut req).map_err(|e| e.to_string())?; + let path = method_desc_to_path(method); let codec = DynamicCodec::new(method.clone()); client.ready().await.unwrap(); @@ -75,12 +80,13 @@ impl GrpcConnection { service: &str, method: &str, stream: ReceiverStream, + metadata: HashMap, ) -> Result>, Status>, String> { let method = &self.method(&service, &method)?; let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()); let method2 = method.clone(); - let req = stream + let mut req = stream .map(move |s| { let mut deserializer = Deserializer::from_str(&s); let req_message = DynamicMessage::deserialize(method2.input(), &mut deserializer) @@ -90,6 +96,9 @@ impl GrpcConnection { req_message }) .into_streaming_request(); + + decorate_req(metadata, &mut req).map_err(|e| e.to_string())?; + let path = method_desc_to_path(method); let codec = DynamicCodec::new(method.clone()); client.ready().await.map_err(|e| e.to_string())?; @@ -101,11 +110,12 @@ impl GrpcConnection { service: &str, method: &str, stream: ReceiverStream, + metadata: HashMap, ) -> Result { let method = &self.method(&service, &method)?; let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()); - let req = { + let mut req = { let method = method.clone(); stream .map(move |s| { @@ -119,6 +129,9 @@ impl GrpcConnection { }) .into_streaming_request() }; + + decorate_req(metadata, &mut req).map_err(|e| e.to_string())?; + let path = method_desc_to_path(method); let codec = DynamicCodec::new(method.clone()); client.ready().await.unwrap(); @@ -134,6 +147,7 @@ impl GrpcConnection { service: &str, method: &str, message: &str, + metadata: HashMap, ) -> Result>, Status>, String> { let method = &self.method(&service, &method)?; let input_message = method.input(); @@ -145,7 +159,9 @@ impl GrpcConnection { let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()); - let req = req_message.into_request(); + let mut req = req_message.into_request(); + decorate_req(metadata, &mut req).map_err(|e| e.to_string())?; + let path = method_desc_to_path(method); let codec = DynamicCodec::new(method.clone()); client.ready().await.map_err(|e| e.to_string())?; @@ -222,10 +238,11 @@ impl GrpcHandle { service: &str, method: &str, message: &str, + metadata: HashMap, ) -> Result>, Status>, String> { self.connect(id, uri, proto_files) .await? - .server_streaming(service, method, message) + .server_streaming(service, method, message, metadata) .await } @@ -237,10 +254,11 @@ impl GrpcHandle { service: &str, method: &str, stream: ReceiverStream, + metadata: HashMap, ) -> Result { self.connect(id, uri, proto_files) .await? - .client_streaming(service, method, stream) + .client_streaming(service, method, stream, metadata) .await } @@ -252,10 +270,11 @@ impl GrpcHandle { service: &str, method: &str, stream: ReceiverStream, + metadata: HashMap, ) -> Result>, Status>, String> { self.connect(id, uri, proto_files) .await? - .streaming(service, method, stream) + .streaming(service, method, stream, metadata) .await } @@ -282,3 +301,13 @@ impl GrpcHandle { Ok(connection) } } + +fn decorate_req(metadata: HashMap, req: &mut Request) -> Result<(), String> { + for (k, v) in metadata { + req.metadata_mut().insert( + MetadataKey::from_str(k.as_str()).map_err(|e| e.to_string())?, + MetadataValue::from_str(v.as_str()).map_err(|e| e.to_string())?, + ); + } + Ok(()) +} diff --git a/src-tauri/migrations/20240218071057_grpc-authentication.sql b/src-tauri/migrations/20240218071057_grpc-authentication.sql new file mode 100644 index 00000000..2b2be63a --- /dev/null +++ b/src-tauri/migrations/20240218071057_grpc-authentication.sql @@ -0,0 +1,2 @@ +ALTER TABLE grpc_requests ADD COLUMN authentication TEXT NOT NULL DEFAULT '{}'; +ALTER TABLE grpc_requests ADD COLUMN authentication_type TEXT; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index bf21a0ab..e176bf56 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -17,6 +17,7 @@ use std::str::FromStr; use ::http::uri::InvalidUri; use ::http::Uri; +use base64::Engine; use fern::colors::ColoredLevelConfig; use log::{debug, error, info, warn}; use rand::random; @@ -128,12 +129,20 @@ async fn cmd_grpc_reflect( #[tauri::command] async fn cmd_grpc_go( request_id: &str, + environment_id: Option<&str>, w: Window, grpc_handle: State<'_, Mutex>, ) -> Result { let req = get_grpc_request(&w, request_id) .await .map_err(|e| e.to_string())?; + let environment = match environment_id { + Some(id) => Some(get_environment(&w, id).await.map_err(|e| e.to_string())?), + None => None, + }; + let workspace = get_workspace(&w, &req.workspace_id) + .await + .map_err(|e| e.to_string())?; let conn = { let req = req.clone(); upsert_grpc_connection( @@ -261,6 +270,37 @@ async fn cmd_grpc_go( } }; let event_handler = w.listen_global(format!("grpc_client_msg_{}", conn.id).as_str(), cb); + let mut metadata = HashMap::new(); + if let Some(b) = &req.authentication_type { + let req = req.clone(); + let environment_ref = environment.as_ref(); + let empty_value = &serde_json::to_value("").unwrap(); + let a = req.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); + metadata.insert("Authorization".to_string(), format!("Basic {}", encoded)); + } 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); + metadata.insert("Authorization".to_string(), format!("Bearer {token}")); + } + } + println!("METADATA: {:?}", metadata); let grpc_listen = { let w = w.clone(); @@ -272,28 +312,36 @@ async fn cmd_grpc_go( method_desc.is_server_streaming(), ) { (true, true) => ( - Some(connection.streaming(&service, &method, in_msg_stream).await), + Some( + connection + .streaming(&service, &method, in_msg_stream, metadata) + .await, + ), None, ), (true, false) => ( None, Some( connection - .client_streaming(&service, &method, in_msg_stream) + .client_streaming(&service, &method, in_msg_stream, metadata) .await, ), ), (false, true) => ( Some( connection - .server_streaming(&service, &method, &req.message) + .server_streaming(&service, &method, &req.message, metadata) .await, ), None, ), (false, false) => ( None, - Some(connection.unary(&service, &method, &req.message).await), + Some( + connection + .unary(&service, &method, &req.message, metadata) + .await, + ), ), }; diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index fcd9efcd..5f7a6a40 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -205,6 +205,8 @@ pub struct GrpcRequest { pub method: Option, pub message: String, pub proto_files: Json>, + pub authentication_type: Option, + pub authentication: Json>, } #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] @@ -502,9 +504,9 @@ pub async fn upsert_grpc_request( r#" INSERT INTO grpc_requests ( id, name, workspace_id, folder_id, sort_priority, url, service, method, message, - proto_files + proto_files, authentication_type, authentication ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET updated_at = CURRENT_TIMESTAMP, name = excluded.name, @@ -514,7 +516,9 @@ pub async fn upsert_grpc_request( service = excluded.service, method = excluded.method, message = excluded.message, - proto_files = excluded.proto_files + proto_files = excluded.proto_files, + authentication_type = excluded.authentication_type, + authentication = excluded.authentication "#, id, trimmed_name, @@ -526,6 +530,8 @@ pub async fn upsert_grpc_request( request.method, request.message, request.proto_files, + request.authentication_type, + request.authentication, ) .execute(&db) .await?; @@ -546,7 +552,8 @@ pub async fn get_grpc_request( r#" SELECT id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority, - url, service, method, message, + url, service, method, message, authentication_type, + authentication AS "authentication!: Json>", proto_files AS "proto_files!: sqlx::types::Json>" FROM grpc_requests WHERE id = ? @@ -567,7 +574,8 @@ pub async fn list_grpc_requests( r#" SELECT id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority, - url, service, method, message, + url, service, method, message, authentication_type, + authentication AS "authentication!: Json>", proto_files AS "proto_files!: sqlx::types::Json>" FROM grpc_requests WHERE workspace_id = ? diff --git a/src-web/components/BasicAuth.tsx b/src-web/components/BasicAuth.tsx index 2724739c..b0d090df 100644 --- a/src-web/components/BasicAuth.tsx +++ b/src-web/components/BasicAuth.tsx @@ -1,49 +1,64 @@ +import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest'; -import type { HttpRequest } from '../lib/models'; +import type { GrpcRequest, HttpRequest } from '../lib/models'; import { Input } from './core/Input'; import { VStack } from './core/Stacks'; -interface Props { - requestId: string; - authentication: HttpRequest['authentication']; +interface Props { + request: T; } -export function BasicAuth({ requestId, authentication }: Props) { - const updateRequest = useUpdateHttpRequest(requestId); +export function BasicAuth({ request }: Props) { + const updateHttpRequest = useUpdateHttpRequest(request.id); + const updateGrpcRequest = useUpdateGrpcRequest(request.id); return ( { - updateRequest.mutate((r) => ({ - ...r, - authentication: { password: r.authentication.password, username }, - })); + if (request.model === 'http_request') { + updateHttpRequest.mutate((r) => ({ + ...r, + authentication: { password: r.authentication.password, username }, + })); + } else { + updateGrpcRequest.mutate((r) => ({ + ...r, + authentication: { password: r.authentication.password, username }, + })); + } }} /> { - updateRequest.mutate((r) => ({ - ...r, - authentication: { username: r.authentication.username, password }, - })); + if (request.model === 'http_request') { + updateHttpRequest.mutate((r) => ({ + ...r, + authentication: { username: r.authentication.username, password }, + })); + } else { + updateGrpcRequest.mutate((r) => ({ + ...r, + authentication: { username: r.authentication.username, password }, + })); + } }} /> diff --git a/src-web/components/BearerAuth.tsx b/src-web/components/BearerAuth.tsx index 5e6db78b..2c81c5d9 100644 --- a/src-web/components/BearerAuth.tsx +++ b/src-web/components/BearerAuth.tsx @@ -1,15 +1,16 @@ +import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest'; -import type { HttpRequest } from '../lib/models'; +import type { GrpcRequest, HttpRequest } from '../lib/models'; import { Input } from './core/Input'; import { VStack } from './core/Stacks'; -interface Props { - requestId: string; - authentication: HttpRequest['authentication']; +interface Props { + request: T; } -export function BearerAuth({ requestId, authentication }: Props) { - const updateRequest = useUpdateHttpRequest(requestId); +export function BearerAuth({ request }: Props) { + const updateHttpRequest = useUpdateHttpRequest(request?.id ?? null); + const updateGrpcRequest = useUpdateGrpcRequest(request?.id ?? null); return ( @@ -21,12 +22,19 @@ export function BearerAuth({ requestId, authentication }: Props) { label="Token" name="token" size="sm" - defaultValue={`${authentication.token}`} + defaultValue={`${request.authentication.token}`} onChange={(token: string) => { - updateRequest.mutate((r) => ({ - ...r, - authentication: { token }, - })); + if (request.model === 'http_request') { + updateHttpRequest.mutate((r) => ({ + ...r, + authentication: { token }, + })); + } else { + updateGrpcRequest.mutate((r) => ({ + ...r, + authentication: { token }, + })); + } }} /> diff --git a/src-web/components/GrpcConnectionLayout.tsx b/src-web/components/GrpcConnectionLayout.tsx index 7d0aeaf6..9703d492 100644 --- a/src-web/components/GrpcConnectionLayout.tsx +++ b/src-web/components/GrpcConnectionLayout.tsx @@ -80,10 +80,7 @@ export function GrpcConnectionLayout({ style }: Props) { style={style} activeRequest={activeRequest} methodType={methodType} - onUnary={grpc.unary.mutate} - onServerStreaming={grpc.serverStreaming.mutate} - onClientStreaming={grpc.clientStreaming.mutate} - onStreaming={grpc.streaming.mutate} + onGo={grpc.go.mutate} onCommit={grpc.commit.mutate} onCancel={grpc.cancel.mutate} onSend={grpc.send.mutate} @@ -93,7 +90,7 @@ export function GrpcConnectionLayout({ style }: Props) { /> )} secondSlot={({ style }) => - !grpc.unary.isLoading && ( + !grpc.go.isLoading && (
- {grpc.unary.error ? ( + {grpc.go.error ? ( - {grpc.unary.error} + {grpc.go.error} ) : messages.length >= 0 ? ( diff --git a/src-web/components/GrpcConnectionSetupPane.tsx b/src-web/components/GrpcConnectionSetupPane.tsx index 53709ae4..9639ffe4 100644 --- a/src-web/components/GrpcConnectionSetupPane.tsx +++ b/src-web/components/GrpcConnectionSetupPane.tsx @@ -5,9 +5,12 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { createGlobalState } from 'react-use'; import type { ReflectResponseService } from '../hooks/useGrpc'; import { useGrpcConnections } from '../hooks/useGrpcConnections'; +import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; import type { GrpcRequest } from '../lib/models'; import { AUTH_TYPE_BASIC, AUTH_TYPE_BEARER, AUTH_TYPE_NONE } from '../lib/models'; +import { BasicAuth } from './BasicAuth'; +import { BearerAuth } from './BearerAuth'; import { Button } from './core/Button'; import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; @@ -15,6 +18,7 @@ import { RadioDropdown } from './core/RadioDropdown'; import { HStack, VStack } from './core/Stacks'; import type { TabItem } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs'; +import { EmptyStateText } from './EmptyStateText'; import { GrpcEditor } from './GrpcEditor'; import { UrlBar } from './UrlBar'; @@ -31,13 +35,10 @@ interface Props { | 'streaming' | 'no-schema' | 'no-method'; - onUnary: () => void; onCommit: () => void; onCancel: () => void; onSend: (v: { message: string }) => void; - onClientStreaming: () => void; - onServerStreaming: () => void; - onStreaming: () => void; + onGo: () => void; services: ReflectResponseService[] | null; } @@ -50,19 +51,17 @@ export function GrpcConnectionSetupPane({ activeRequest, reflectionError, reflectionLoading, - onStreaming, - onClientStreaming, - onServerStreaming, + onGo, onCommit, onCancel, onSend, - onUnary, }: Props) { const connections = useGrpcConnections(activeRequest.id ?? null); const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null); const activeConnection = connections[0] ?? null; const isStreaming = activeConnection?.elapsed === 0; const [activeTab, setActiveTab] = useActiveTab(); + const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null); const [paneSize, setPaneSize] = useState(99999); const urlContainerEl = useRef(null); @@ -116,17 +115,9 @@ export function GrpcConnectionSetupPane({ body: 'Service or method not selected', }); } - if (methodType === 'streaming') { - onStreaming(); - } else if (methodType === 'server_streaming') { - onServerStreaming(); - } else if (methodType === 'client_streaming') { - onClientStreaming(); - } else { - onUnary(); - } + onGo(); }, - [activeRequest, methodType, onStreaming, onServerStreaming, onClientStreaming, onUnary], + [activeRequest, onGo], ); const tabs: TabItem[] = useMemo( @@ -136,7 +127,7 @@ export function GrpcConnectionSetupPane({ value: 'auth', label: 'Auth', options: { - value: AUTH_TYPE_NONE, // TODO + value: activeRequest.authenticationType, items: [ { label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC }, { label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER }, @@ -144,13 +135,24 @@ export function GrpcConnectionSetupPane({ { label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE }, ], onChange: async (authenticationType) => { - // TODO + let authentication: GrpcRequest['authentication'] = activeRequest.authentication; + if (authenticationType === AUTH_TYPE_BASIC) { + authentication = { + username: authentication.username ?? '', + password: authentication.password ?? '', + }; + } else if (authenticationType === AUTH_TYPE_BEARER) { + authentication = { + token: authentication.token ?? '', + }; + } + await updateRequest.mutateAsync({ authenticationType, authentication }); }, }, }, { value: 'metadata', label: 'Metadata' }, ], - [], + [activeRequest.authentication, activeRequest.authenticationType, updateRequest], ); return ( @@ -166,7 +168,7 @@ export function GrpcConnectionSetupPane({ url={activeRequest.url ?? ''} method={null} submitIcon={null} - forceUpdateKey={activeRequest?.id ?? ''} + forceUpdateKey={forceUpdateKey} placeholder="localhost:50051" onSubmit={handleConnect} onUrlChange={handleChangeUrl} @@ -270,6 +272,15 @@ export function GrpcConnectionSetupPane({ request={activeRequest} /> + + {activeRequest.authenticationType === AUTH_TYPE_BASIC ? ( + + ) : activeRequest.authenticationType === AUTH_TYPE_BEARER ? ( + + ) : ( + No Authentication {activeRequest.authenticationType} + )} + ); diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index 680928f9..3ef14846 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -154,7 +154,7 @@ export const RequestPane = memo(function RequestPane({ token: authentication.token ?? '', }; } - updateRequest.mutate({ authenticationType, authentication }); + await updateRequest.mutateAsync({ authenticationType, authentication }); }, }, }, @@ -226,17 +226,9 @@ export const RequestPane = memo(function RequestPane({ > {activeRequest.authenticationType === AUTH_TYPE_BASIC ? ( - + ) : activeRequest.authenticationType === AUTH_TYPE_BEARER ? ( - + ) : ( No Authentication {activeRequest.authenticationType} diff --git a/src-web/components/core/Tabs/Tabs.tsx b/src-web/components/core/Tabs/Tabs.tsx index 2125bd2d..a834b759 100644 --- a/src-web/components/core/Tabs/Tabs.tsx +++ b/src-web/components/core/Tabs/Tabs.tsx @@ -82,7 +82,7 @@ export function Tabs({ {tabs.map((t) => { const isActive = t.value === value; const btnClassName = classNames( - isActive ? '' : 'text-gray-600 hover:text-gray-800', + isActive ? 'text-gray-800' : 'text-gray-600 hover:text-gray-700', '!px-2 ml-[1px]', ); diff --git a/src-web/hooks/useGrpc.ts b/src-web/hooks/useGrpc.ts index c6d74856..35db6967 100644 --- a/src-web/hooks/useGrpc.ts +++ b/src-web/hooks/useGrpc.ts @@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api'; import { emit } from '@tauri-apps/api/event'; import { minPromiseMillis } from '../lib/minPromiseMillis'; import type { GrpcConnection, GrpcRequest } from '../lib/models'; +import { useActiveEnvironmentId } from './useActiveEnvironmentId'; import { useDebouncedValue } from './useDebouncedValue'; export interface ReflectResponseService { @@ -12,21 +13,10 @@ export interface ReflectResponseService { export function useGrpc(req: GrpcRequest | null, conn: GrpcConnection | null) { const requestId = req?.id ?? 'n/a'; + const environmentId = useActiveEnvironmentId(); - const unary = useMutation({ - mutationFn: async () => await invoke('cmd_grpc_go', { requestId }), - }); - - const clientStreaming = useMutation({ - mutationFn: async () => await invoke('cmd_grpc_go', { requestId }), - }); - - const serverStreaming = useMutation({ - mutationFn: async () => await invoke('cmd_grpc_go', { requestId }), - }); - - const streaming = useMutation({ - mutationFn: async () => await invoke('cmd_grpc_go', { requestId }), + const go = useMutation({ + mutationFn: async () => await invoke('cmd_grpc_go', { requestId, environmentId }), }); const send = useMutation({ @@ -58,10 +48,7 @@ export function useGrpc(req: GrpcRequest | null, conn: GrpcConnection | null) { }); return { - unary, - clientStreaming, - serverStreaming, - streaming, + go, reflect, cancel, commit, diff --git a/src-web/lib/models.ts b/src-web/lib/models.ts index 56acf0e7..6935866b 100644 --- a/src-web/lib/models.ts +++ b/src-web/lib/models.ts @@ -115,6 +115,8 @@ export interface GrpcRequest extends BaseModel { method: string | null; message: string; protoFiles: string[]; + authentication: Record; + authenticationType: string | null; } export interface GrpcMessage extends BaseModel {