gRPC authentication

This commit is contained in:
Gregory Schier
2024-02-17 23:47:28 -08:00
parent ab8503d87c
commit 8d29fad261
16 changed files with 246 additions and 123 deletions

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "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<Vec<String>>\"\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<HashMap<String, JsonValue>>\",\n proto_files AS \"proto_files!: sqlx::types::Json<Vec<String>>\"\n FROM grpc_requests\n WHERE id = ?\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -64,9 +64,19 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "proto_files!: sqlx::types::Json<Vec<String>>", "name": "authentication_type",
"ordinal": 12, "ordinal": 12,
"type_info": "Text" "type_info": "Text"
},
{
"name": "authentication!: Json<HashMap<String, JsonValue>>",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "proto_files!: sqlx::types::Json<Vec<String>>",
"ordinal": 14,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@@ -85,8 +95,10 @@
true, true,
true, true,
false, false,
true,
false,
false false
] ]
}, },
"hash": "7398403d3de2dc5c5b4b6392f083041d9a55194bb97819225a2612fdeb60ad42" "hash": "0d9e685f878fc2a0e1803c6aaae3828deebd684fc9f78e9f8595a550f90749fe"
} }

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "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<Vec<String>>\"\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<HashMap<String, JsonValue>>\",\n proto_files AS \"proto_files!: sqlx::types::Json<Vec<String>>\"\n FROM grpc_requests\n WHERE workspace_id = ?\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -64,9 +64,19 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "proto_files!: sqlx::types::Json<Vec<String>>", "name": "authentication_type",
"ordinal": 12, "ordinal": 12,
"type_info": "Text" "type_info": "Text"
},
{
"name": "authentication!: Json<HashMap<String, JsonValue>>",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "proto_files!: sqlx::types::Json<Vec<String>>",
"ordinal": 14,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@@ -85,8 +95,10 @@
true, true,
true, true,
false, false,
true,
false,
false false
] ]
}, },
"hash": "761d27c3ec425c37ad9abe9c732a9c1746c81ca50d2c413e540b74c8c8e908b7" "hash": "545d21ff21bd02468be86746325dc9f1b50f3fe53d7735194d91927b5d14a436"
} }

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -1,5 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr;
use hyper::client::HttpConnector; use hyper::client::HttpConnector;
use hyper::Client; use hyper::Client;
@@ -10,8 +11,9 @@ use serde_json::Deserializer;
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use tonic::body::BoxBody; use tonic::body::BoxBody;
use tonic::metadata::{MetadataKey, MetadataValue};
use tonic::transport::Uri; 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::codec::DynamicCodec;
use crate::proto::{fill_pool, fill_pool_from_files, get_transport, method_desc_to_path}; use crate::proto::{fill_pool, fill_pool_from_files, get_transport, method_desc_to_path};
@@ -47,6 +49,7 @@ impl GrpcConnection {
service: &str, service: &str,
method: &str, method: &str,
message: &str, message: &str,
metadata: HashMap<String, String>,
) -> Result<DynamicMessage, String> { ) -> Result<DynamicMessage, String> {
let method = &self.method(&service, &method)?; let method = &self.method(&service, &method)?;
let input_message = method.input(); 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 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 path = method_desc_to_path(method);
let codec = DynamicCodec::new(method.clone()); let codec = DynamicCodec::new(method.clone());
client.ready().await.unwrap(); client.ready().await.unwrap();
@@ -75,12 +80,13 @@ impl GrpcConnection {
service: &str, service: &str,
method: &str, method: &str,
stream: ReceiverStream<String>, stream: ReceiverStream<String>,
metadata: HashMap<String, String>,
) -> Result<Result<Response<Streaming<DynamicMessage>>, Status>, String> { ) -> Result<Result<Response<Streaming<DynamicMessage>>, Status>, String> {
let method = &self.method(&service, &method)?; let method = &self.method(&service, &method)?;
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()); let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
let method2 = method.clone(); let method2 = method.clone();
let req = stream let mut req = stream
.map(move |s| { .map(move |s| {
let mut deserializer = Deserializer::from_str(&s); let mut deserializer = Deserializer::from_str(&s);
let req_message = DynamicMessage::deserialize(method2.input(), &mut deserializer) let req_message = DynamicMessage::deserialize(method2.input(), &mut deserializer)
@@ -90,6 +96,9 @@ impl GrpcConnection {
req_message req_message
}) })
.into_streaming_request(); .into_streaming_request();
decorate_req(metadata, &mut req).map_err(|e| e.to_string())?;
let path = method_desc_to_path(method); let path = method_desc_to_path(method);
let codec = DynamicCodec::new(method.clone()); let codec = DynamicCodec::new(method.clone());
client.ready().await.map_err(|e| e.to_string())?; client.ready().await.map_err(|e| e.to_string())?;
@@ -101,11 +110,12 @@ impl GrpcConnection {
service: &str, service: &str,
method: &str, method: &str,
stream: ReceiverStream<String>, stream: ReceiverStream<String>,
metadata: HashMap<String, String>,
) -> Result<DynamicMessage, String> { ) -> Result<DynamicMessage, String> {
let method = &self.method(&service, &method)?; let method = &self.method(&service, &method)?;
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()); let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
let req = { let mut req = {
let method = method.clone(); let method = method.clone();
stream stream
.map(move |s| { .map(move |s| {
@@ -119,6 +129,9 @@ impl GrpcConnection {
}) })
.into_streaming_request() .into_streaming_request()
}; };
decorate_req(metadata, &mut req).map_err(|e| e.to_string())?;
let path = method_desc_to_path(method); let path = method_desc_to_path(method);
let codec = DynamicCodec::new(method.clone()); let codec = DynamicCodec::new(method.clone());
client.ready().await.unwrap(); client.ready().await.unwrap();
@@ -134,6 +147,7 @@ impl GrpcConnection {
service: &str, service: &str,
method: &str, method: &str,
message: &str, message: &str,
metadata: HashMap<String, String>,
) -> Result<Result<Response<Streaming<DynamicMessage>>, Status>, String> { ) -> Result<Result<Response<Streaming<DynamicMessage>>, Status>, String> {
let method = &self.method(&service, &method)?; let method = &self.method(&service, &method)?;
let input_message = method.input(); 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 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 path = method_desc_to_path(method);
let codec = DynamicCodec::new(method.clone()); let codec = DynamicCodec::new(method.clone());
client.ready().await.map_err(|e| e.to_string())?; client.ready().await.map_err(|e| e.to_string())?;
@@ -222,10 +238,11 @@ impl GrpcHandle {
service: &str, service: &str,
method: &str, method: &str,
message: &str, message: &str,
metadata: HashMap<String, String>,
) -> Result<Result<Response<Streaming<DynamicMessage>>, Status>, String> { ) -> Result<Result<Response<Streaming<DynamicMessage>>, Status>, String> {
self.connect(id, uri, proto_files) self.connect(id, uri, proto_files)
.await? .await?
.server_streaming(service, method, message) .server_streaming(service, method, message, metadata)
.await .await
} }
@@ -237,10 +254,11 @@ impl GrpcHandle {
service: &str, service: &str,
method: &str, method: &str,
stream: ReceiverStream<String>, stream: ReceiverStream<String>,
metadata: HashMap<String, String>,
) -> Result<DynamicMessage, String> { ) -> Result<DynamicMessage, String> {
self.connect(id, uri, proto_files) self.connect(id, uri, proto_files)
.await? .await?
.client_streaming(service, method, stream) .client_streaming(service, method, stream, metadata)
.await .await
} }
@@ -252,10 +270,11 @@ impl GrpcHandle {
service: &str, service: &str,
method: &str, method: &str,
stream: ReceiverStream<String>, stream: ReceiverStream<String>,
metadata: HashMap<String, String>,
) -> Result<Result<Response<Streaming<DynamicMessage>>, Status>, String> { ) -> Result<Result<Response<Streaming<DynamicMessage>>, Status>, String> {
self.connect(id, uri, proto_files) self.connect(id, uri, proto_files)
.await? .await?
.streaming(service, method, stream) .streaming(service, method, stream, metadata)
.await .await
} }
@@ -282,3 +301,13 @@ impl GrpcHandle {
Ok(connection) Ok(connection)
} }
} }
fn decorate_req<T>(metadata: HashMap<String, String>, req: &mut Request<T>) -> 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(())
}

View File

@@ -0,0 +1,2 @@
ALTER TABLE grpc_requests ADD COLUMN authentication TEXT NOT NULL DEFAULT '{}';
ALTER TABLE grpc_requests ADD COLUMN authentication_type TEXT;

View File

@@ -17,6 +17,7 @@ use std::str::FromStr;
use ::http::uri::InvalidUri; use ::http::uri::InvalidUri;
use ::http::Uri; use ::http::Uri;
use base64::Engine;
use fern::colors::ColoredLevelConfig; use fern::colors::ColoredLevelConfig;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use rand::random; use rand::random;
@@ -128,12 +129,20 @@ async fn cmd_grpc_reflect(
#[tauri::command] #[tauri::command]
async fn cmd_grpc_go( async fn cmd_grpc_go(
request_id: &str, request_id: &str,
environment_id: Option<&str>,
w: Window, w: Window,
grpc_handle: State<'_, Mutex<GrpcHandle>>, grpc_handle: State<'_, Mutex<GrpcHandle>>,
) -> Result<String, String> { ) -> Result<String, String> {
let req = get_grpc_request(&w, request_id) let req = get_grpc_request(&w, request_id)
.await .await
.map_err(|e| e.to_string())?; .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 conn = {
let req = req.clone(); let req = req.clone();
upsert_grpc_connection( 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 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 grpc_listen = {
let w = w.clone(); let w = w.clone();
@@ -272,28 +312,36 @@ async fn cmd_grpc_go(
method_desc.is_server_streaming(), method_desc.is_server_streaming(),
) { ) {
(true, true) => ( (true, true) => (
Some(connection.streaming(&service, &method, in_msg_stream).await), Some(
connection
.streaming(&service, &method, in_msg_stream, metadata)
.await,
),
None, None,
), ),
(true, false) => ( (true, false) => (
None, None,
Some( Some(
connection connection
.client_streaming(&service, &method, in_msg_stream) .client_streaming(&service, &method, in_msg_stream, metadata)
.await, .await,
), ),
), ),
(false, true) => ( (false, true) => (
Some( Some(
connection connection
.server_streaming(&service, &method, &req.message) .server_streaming(&service, &method, &req.message, metadata)
.await, .await,
), ),
None, None,
), ),
(false, false) => ( (false, false) => (
None, None,
Some(connection.unary(&service, &method, &req.message).await), Some(
connection
.unary(&service, &method, &req.message, metadata)
.await,
),
), ),
}; };

View File

@@ -205,6 +205,8 @@ pub struct GrpcRequest {
pub method: Option<String>, pub method: Option<String>,
pub message: String, pub message: String,
pub proto_files: Json<Vec<String>>, pub proto_files: Json<Vec<String>>,
pub authentication_type: Option<String>,
pub authentication: Json<HashMap<String, JsonValue>>,
} }
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
@@ -502,9 +504,9 @@ pub async fn upsert_grpc_request(
r#" r#"
INSERT INTO grpc_requests ( INSERT INTO grpc_requests (
id, name, workspace_id, folder_id, sort_priority, url, service, method, message, 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 ON CONFLICT (id) DO UPDATE SET
updated_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP,
name = excluded.name, name = excluded.name,
@@ -514,7 +516,9 @@ pub async fn upsert_grpc_request(
service = excluded.service, service = excluded.service,
method = excluded.method, method = excluded.method,
message = excluded.message, message = excluded.message,
proto_files = excluded.proto_files proto_files = excluded.proto_files,
authentication_type = excluded.authentication_type,
authentication = excluded.authentication
"#, "#,
id, id,
trimmed_name, trimmed_name,
@@ -526,6 +530,8 @@ pub async fn upsert_grpc_request(
request.method, request.method,
request.message, request.message,
request.proto_files, request.proto_files,
request.authentication_type,
request.authentication,
) )
.execute(&db) .execute(&db)
.await?; .await?;
@@ -546,7 +552,8 @@ pub async fn get_grpc_request(
r#" r#"
SELECT SELECT
id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority, 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<HashMap<String, JsonValue>>",
proto_files AS "proto_files!: sqlx::types::Json<Vec<String>>" proto_files AS "proto_files!: sqlx::types::Json<Vec<String>>"
FROM grpc_requests FROM grpc_requests
WHERE id = ? WHERE id = ?
@@ -567,7 +574,8 @@ pub async fn list_grpc_requests(
r#" r#"
SELECT SELECT
id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority, 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<HashMap<String, JsonValue>>",
proto_files AS "proto_files!: sqlx::types::Json<Vec<String>>" proto_files AS "proto_files!: sqlx::types::Json<Vec<String>>"
FROM grpc_requests FROM grpc_requests
WHERE workspace_id = ? WHERE workspace_id = ?

View File

@@ -1,49 +1,64 @@
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest'; import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
import type { HttpRequest } from '../lib/models'; import type { GrpcRequest, HttpRequest } from '../lib/models';
import { Input } from './core/Input'; import { Input } from './core/Input';
import { VStack } from './core/Stacks'; import { VStack } from './core/Stacks';
interface Props { interface Props<T> {
requestId: string; request: T;
authentication: HttpRequest['authentication'];
} }
export function BasicAuth({ requestId, authentication }: Props) { export function BasicAuth<T extends HttpRequest | GrpcRequest>({ request }: Props<T>) {
const updateRequest = useUpdateHttpRequest(requestId); const updateHttpRequest = useUpdateHttpRequest(request.id);
const updateGrpcRequest = useUpdateGrpcRequest(request.id);
return ( return (
<VStack className="my-2" space={2}> <VStack className="my-2" space={2}>
<Input <Input
useTemplating useTemplating
autocompleteVariables autocompleteVariables
forceUpdateKey={requestId} forceUpdateKey={request.id}
placeholder="username" placeholder="username"
label="Username" label="Username"
name="username" name="username"
size="sm" size="sm"
defaultValue={`${authentication.username}`} defaultValue={`${request.authentication.username}`}
onChange={(username: string) => { onChange={(username: string) => {
updateRequest.mutate((r) => ({ if (request.model === 'http_request') {
...r, updateHttpRequest.mutate((r) => ({
authentication: { password: r.authentication.password, username }, ...r,
})); authentication: { password: r.authentication.password, username },
}));
} else {
updateGrpcRequest.mutate((r) => ({
...r,
authentication: { password: r.authentication.password, username },
}));
}
}} }}
/> />
<Input <Input
useTemplating useTemplating
autocompleteVariables autocompleteVariables
forceUpdateKey={requestId} forceUpdateKey={request?.id}
placeholder="password" placeholder="password"
label="Password" label="Password"
name="password" name="password"
size="sm" size="sm"
type="password" type="password"
defaultValue={`${authentication.password}`} defaultValue={`${request.authentication.password}`}
onChange={(password: string) => { onChange={(password: string) => {
updateRequest.mutate((r) => ({ if (request.model === 'http_request') {
...r, updateHttpRequest.mutate((r) => ({
authentication: { username: r.authentication.username, password }, ...r,
})); authentication: { username: r.authentication.username, password },
}));
} else {
updateGrpcRequest.mutate((r) => ({
...r,
authentication: { username: r.authentication.username, password },
}));
}
}} }}
/> />
</VStack> </VStack>

View File

@@ -1,15 +1,16 @@
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest'; import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
import type { HttpRequest } from '../lib/models'; import type { GrpcRequest, HttpRequest } from '../lib/models';
import { Input } from './core/Input'; import { Input } from './core/Input';
import { VStack } from './core/Stacks'; import { VStack } from './core/Stacks';
interface Props { interface Props<T> {
requestId: string; request: T;
authentication: HttpRequest['authentication'];
} }
export function BearerAuth({ requestId, authentication }: Props) { export function BearerAuth<T extends HttpRequest | GrpcRequest>({ request }: Props<T>) {
const updateRequest = useUpdateHttpRequest(requestId); const updateHttpRequest = useUpdateHttpRequest(request?.id ?? null);
const updateGrpcRequest = useUpdateGrpcRequest(request?.id ?? null);
return ( return (
<VStack className="my-2" space={2}> <VStack className="my-2" space={2}>
@@ -21,12 +22,19 @@ export function BearerAuth({ requestId, authentication }: Props) {
label="Token" label="Token"
name="token" name="token"
size="sm" size="sm"
defaultValue={`${authentication.token}`} defaultValue={`${request.authentication.token}`}
onChange={(token: string) => { onChange={(token: string) => {
updateRequest.mutate((r) => ({ if (request.model === 'http_request') {
...r, updateHttpRequest.mutate((r) => ({
authentication: { token }, ...r,
})); authentication: { token },
}));
} else {
updateGrpcRequest.mutate((r) => ({
...r,
authentication: { token },
}));
}
}} }}
/> />
</VStack> </VStack>

View File

@@ -80,10 +80,7 @@ export function GrpcConnectionLayout({ style }: Props) {
style={style} style={style}
activeRequest={activeRequest} activeRequest={activeRequest}
methodType={methodType} methodType={methodType}
onUnary={grpc.unary.mutate} onGo={grpc.go.mutate}
onServerStreaming={grpc.serverStreaming.mutate}
onClientStreaming={grpc.clientStreaming.mutate}
onStreaming={grpc.streaming.mutate}
onCommit={grpc.commit.mutate} onCommit={grpc.commit.mutate}
onCancel={grpc.cancel.mutate} onCancel={grpc.cancel.mutate}
onSend={grpc.send.mutate} onSend={grpc.send.mutate}
@@ -93,7 +90,7 @@ export function GrpcConnectionLayout({ style }: Props) {
/> />
)} )}
secondSlot={({ style }) => secondSlot={({ style }) =>
!grpc.unary.isLoading && ( !grpc.go.isLoading && (
<div <div
style={style} style={style}
className={classNames( className={classNames(
@@ -102,9 +99,9 @@ export function GrpcConnectionLayout({ style }: Props) {
'shadow shadow-gray-100 dark:shadow-gray-0 relative', 'shadow shadow-gray-100 dark:shadow-gray-0 relative',
)} )}
> >
{grpc.unary.error ? ( {grpc.go.error ? (
<Banner color="danger" className="m-2"> <Banner color="danger" className="m-2">
{grpc.unary.error} {grpc.go.error}
</Banner> </Banner>
) : messages.length >= 0 ? ( ) : messages.length >= 0 ? (
<GrpcConnectionMessagesPane activeRequest={activeRequest} methodType={methodType} /> <GrpcConnectionMessagesPane activeRequest={activeRequest} methodType={methodType} />

View File

@@ -5,9 +5,12 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
import { createGlobalState } from 'react-use'; import { createGlobalState } from 'react-use';
import type { ReflectResponseService } from '../hooks/useGrpc'; import type { ReflectResponseService } from '../hooks/useGrpc';
import { useGrpcConnections } from '../hooks/useGrpcConnections'; import { useGrpcConnections } from '../hooks/useGrpcConnections';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import type { GrpcRequest } from '../lib/models'; import type { GrpcRequest } from '../lib/models';
import { AUTH_TYPE_BASIC, AUTH_TYPE_BEARER, AUTH_TYPE_NONE } 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 { Button } from './core/Button';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
@@ -15,6 +18,7 @@ import { RadioDropdown } from './core/RadioDropdown';
import { HStack, VStack } from './core/Stacks'; import { HStack, VStack } from './core/Stacks';
import type { TabItem } from './core/Tabs/Tabs'; import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
import { GrpcEditor } from './GrpcEditor'; import { GrpcEditor } from './GrpcEditor';
import { UrlBar } from './UrlBar'; import { UrlBar } from './UrlBar';
@@ -31,13 +35,10 @@ interface Props {
| 'streaming' | 'streaming'
| 'no-schema' | 'no-schema'
| 'no-method'; | 'no-method';
onUnary: () => void;
onCommit: () => void; onCommit: () => void;
onCancel: () => void; onCancel: () => void;
onSend: (v: { message: string }) => void; onSend: (v: { message: string }) => void;
onClientStreaming: () => void; onGo: () => void;
onServerStreaming: () => void;
onStreaming: () => void;
services: ReflectResponseService[] | null; services: ReflectResponseService[] | null;
} }
@@ -50,19 +51,17 @@ export function GrpcConnectionSetupPane({
activeRequest, activeRequest,
reflectionError, reflectionError,
reflectionLoading, reflectionLoading,
onStreaming, onGo,
onClientStreaming,
onServerStreaming,
onCommit, onCommit,
onCancel, onCancel,
onSend, onSend,
onUnary,
}: Props) { }: Props) {
const connections = useGrpcConnections(activeRequest.id ?? null); const connections = useGrpcConnections(activeRequest.id ?? null);
const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null); const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null);
const activeConnection = connections[0] ?? null; const activeConnection = connections[0] ?? null;
const isStreaming = activeConnection?.elapsed === 0; const isStreaming = activeConnection?.elapsed === 0;
const [activeTab, setActiveTab] = useActiveTab(); const [activeTab, setActiveTab] = useActiveTab();
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
const [paneSize, setPaneSize] = useState(99999); const [paneSize, setPaneSize] = useState(99999);
const urlContainerEl = useRef<HTMLDivElement>(null); const urlContainerEl = useRef<HTMLDivElement>(null);
@@ -116,17 +115,9 @@ export function GrpcConnectionSetupPane({
body: 'Service or method not selected', body: 'Service or method not selected',
}); });
} }
if (methodType === 'streaming') { onGo();
onStreaming();
} else if (methodType === 'server_streaming') {
onServerStreaming();
} else if (methodType === 'client_streaming') {
onClientStreaming();
} else {
onUnary();
}
}, },
[activeRequest, methodType, onStreaming, onServerStreaming, onClientStreaming, onUnary], [activeRequest, onGo],
); );
const tabs: TabItem[] = useMemo( const tabs: TabItem[] = useMemo(
@@ -136,7 +127,7 @@ export function GrpcConnectionSetupPane({
value: 'auth', value: 'auth',
label: 'Auth', label: 'Auth',
options: { options: {
value: AUTH_TYPE_NONE, // TODO value: activeRequest.authenticationType,
items: [ items: [
{ label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC }, { label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC },
{ label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER }, { 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 }, { label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE },
], ],
onChange: async (authenticationType) => { 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' }, { value: 'metadata', label: 'Metadata' },
], ],
[], [activeRequest.authentication, activeRequest.authenticationType, updateRequest],
); );
return ( return (
@@ -166,7 +168,7 @@ export function GrpcConnectionSetupPane({
url={activeRequest.url ?? ''} url={activeRequest.url ?? ''}
method={null} method={null}
submitIcon={null} submitIcon={null}
forceUpdateKey={activeRequest?.id ?? ''} forceUpdateKey={forceUpdateKey}
placeholder="localhost:50051" placeholder="localhost:50051"
onSubmit={handleConnect} onSubmit={handleConnect}
onUrlChange={handleChangeUrl} onUrlChange={handleChangeUrl}
@@ -270,6 +272,15 @@ export function GrpcConnectionSetupPane({
request={activeRequest} request={activeRequest}
/> />
</TabContent> </TabContent>
<TabContent value="auth">
{activeRequest.authenticationType === AUTH_TYPE_BASIC ? (
<BasicAuth key={forceUpdateKey} request={activeRequest} />
) : activeRequest.authenticationType === AUTH_TYPE_BEARER ? (
<BearerAuth key={forceUpdateKey} request={activeRequest} />
) : (
<EmptyStateText>No Authentication {activeRequest.authenticationType}</EmptyStateText>
)}
</TabContent>
</Tabs> </Tabs>
</VStack> </VStack>
); );

View File

@@ -154,7 +154,7 @@ export const RequestPane = memo(function RequestPane({
token: authentication.token ?? '', token: authentication.token ?? '',
}; };
} }
updateRequest.mutate({ authenticationType, authentication }); await updateRequest.mutateAsync({ authenticationType, authentication });
}, },
}, },
}, },
@@ -226,17 +226,9 @@ export const RequestPane = memo(function RequestPane({
> >
<TabContent value="auth"> <TabContent value="auth">
{activeRequest.authenticationType === AUTH_TYPE_BASIC ? ( {activeRequest.authenticationType === AUTH_TYPE_BASIC ? (
<BasicAuth <BasicAuth key={forceUpdateKey} request={activeRequest} />
key={forceUpdateKey}
requestId={activeRequest.id}
authentication={activeRequest.authentication}
/>
) : activeRequest.authenticationType === AUTH_TYPE_BEARER ? ( ) : activeRequest.authenticationType === AUTH_TYPE_BEARER ? (
<BearerAuth <BearerAuth key={forceUpdateKey} request={activeRequest} />
key={forceUpdateKey}
requestId={activeRequest.id}
authentication={activeRequest.authentication}
/>
) : ( ) : (
<EmptyStateText> <EmptyStateText>
No Authentication {activeRequest.authenticationType} No Authentication {activeRequest.authenticationType}

View File

@@ -82,7 +82,7 @@ export function Tabs({
{tabs.map((t) => { {tabs.map((t) => {
const isActive = t.value === value; const isActive = t.value === value;
const btnClassName = classNames( 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]', '!px-2 ml-[1px]',
); );

View File

@@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api';
import { emit } from '@tauri-apps/api/event'; import { emit } from '@tauri-apps/api/event';
import { minPromiseMillis } from '../lib/minPromiseMillis'; import { minPromiseMillis } from '../lib/minPromiseMillis';
import type { GrpcConnection, GrpcRequest } from '../lib/models'; import type { GrpcConnection, GrpcRequest } from '../lib/models';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useDebouncedValue } from './useDebouncedValue'; import { useDebouncedValue } from './useDebouncedValue';
export interface ReflectResponseService { export interface ReflectResponseService {
@@ -12,21 +13,10 @@ export interface ReflectResponseService {
export function useGrpc(req: GrpcRequest | null, conn: GrpcConnection | null) { export function useGrpc(req: GrpcRequest | null, conn: GrpcConnection | null) {
const requestId = req?.id ?? 'n/a'; const requestId = req?.id ?? 'n/a';
const environmentId = useActiveEnvironmentId();
const unary = useMutation<void, string>({ const go = useMutation<void, string>({
mutationFn: async () => await invoke('cmd_grpc_go', { requestId }), mutationFn: async () => await invoke('cmd_grpc_go', { requestId, environmentId }),
});
const clientStreaming = useMutation<void, string>({
mutationFn: async () => await invoke('cmd_grpc_go', { requestId }),
});
const serverStreaming = useMutation<void, string>({
mutationFn: async () => await invoke('cmd_grpc_go', { requestId }),
});
const streaming = useMutation<void, string>({
mutationFn: async () => await invoke('cmd_grpc_go', { requestId }),
}); });
const send = useMutation({ const send = useMutation({
@@ -58,10 +48,7 @@ export function useGrpc(req: GrpcRequest | null, conn: GrpcConnection | null) {
}); });
return { return {
unary, go,
clientStreaming,
serverStreaming,
streaming,
reflect, reflect,
cancel, cancel,
commit, commit,

View File

@@ -115,6 +115,8 @@ export interface GrpcRequest extends BaseModel {
method: string | null; method: string | null;
message: string; message: string;
protoFiles: string[]; protoFiles: string[];
authentication: Record<string, string | number | boolean | null | undefined>;
authenticationType: string | null;
} }
export interface GrpcMessage extends BaseModel { export interface GrpcMessage extends BaseModel {