Scoped OAuth 2 tokens

This commit is contained in:
Gregory Schier
2025-07-23 22:03:03 -07:00
parent a258a80fbd
commit 20681e5be3
14 changed files with 232 additions and 86 deletions

View File

@@ -1,19 +0,0 @@
import type { Context } from '@yaakapp/api';
import type { AccessToken } from './store';
import { getToken } from './store';
export async function getAccessTokenIfNotExpired(
ctx: Context,
contextId: string,
): Promise<AccessToken | null> {
const token = await getToken(ctx, contextId);
if (token == null || isTokenExpired(token)) {
return null;
}
return token;
}
export function isTokenExpired(token: AccessToken) {
return token.expiresAt && Date.now() > token.expiresAt;
}

View File

@@ -1,12 +1,12 @@
import type { Context, HttpRequest } from '@yaakapp/api'; import type { Context, HttpRequest } from '@yaakapp/api';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { isTokenExpired } from './getAccessTokenIfNotExpired'; import type { AccessToken, AccessTokenRawResponse, TokenStoreArgs } from './store';
import type { AccessToken, AccessTokenRawResponse } from './store';
import { deleteToken, getToken, storeToken } from './store'; import { deleteToken, getToken, storeToken } from './store';
import { isTokenExpired } from './util';
export async function getOrRefreshAccessToken( export async function getOrRefreshAccessToken(
ctx: Context, ctx: Context,
contextId: string, tokenArgs: TokenStoreArgs,
{ {
scope, scope,
accessTokenUrl, accessTokenUrl,
@@ -23,7 +23,7 @@ export async function getOrRefreshAccessToken(
forceRefresh?: boolean; forceRefresh?: boolean;
}, },
): Promise<AccessToken | null> { ): Promise<AccessToken | null> {
const token = await getToken(ctx, contextId); const token = await getToken(ctx, tokenArgs);
if (token == null) { if (token == null) {
return null; return null;
} }
@@ -75,7 +75,7 @@ export async function getOrRefreshAccessToken(
// Bad refresh token, so we'll force it to fetch a fresh access token by deleting // Bad refresh token, so we'll force it to fetch a fresh access token by deleting
// and returning null; // and returning null;
console.log('[oauth2] Unauthorized refresh_token request'); console.log('[oauth2] Unauthorized refresh_token request');
await deleteToken(ctx, contextId); await deleteToken(ctx, tokenArgs);
return null; return null;
} }
@@ -108,5 +108,5 @@ export async function getOrRefreshAccessToken(
refresh_token: response.refresh_token ?? token.response.refresh_token, refresh_token: response.refresh_token ?? token.response.refresh_token,
}; };
return storeToken(ctx, contextId, newResponse); return storeToken(ctx, tokenArgs, newResponse);
} }

View File

@@ -2,7 +2,7 @@ import type { Context } from '@yaakapp/api';
import { createHash, randomBytes } from 'node:crypto'; import { createHash, randomBytes } from 'node:crypto';
import { fetchAccessToken } from '../fetchAccessToken'; import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken'; import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import type { AccessToken } from '../store'; import type { AccessToken, TokenStoreArgs } from '../store';
import { getDataDirKey, storeToken } from '../store'; import { getDataDirKey, storeToken } from '../store';
export const PKCE_SHA256 = 'S256'; export const PKCE_SHA256 = 'S256';
@@ -41,7 +41,14 @@ export async function getAuthorizationCode(
tokenName: 'access_token' | 'id_token'; tokenName: 'access_token' | 'id_token';
}, },
): Promise<AccessToken> { ): Promise<AccessToken> {
const token = await getOrRefreshAccessToken(ctx, contextId, { const tokenArgs: TokenStoreArgs = {
contextId,
clientId,
accessTokenUrl,
authorizationUrl: authorizationUrlRaw,
};
const token = await getOrRefreshAccessToken(ctx, tokenArgs, {
accessTokenUrl, accessTokenUrl,
scope, scope,
clientId, clientId,
@@ -128,7 +135,7 @@ export async function getAuthorizationCode(
], ],
}); });
return storeToken(ctx, contextId, response, tokenName); return storeToken(ctx, tokenArgs, response, tokenName);
} }
export function genPkceCodeVerifier() { export function genPkceCodeVerifier() {

View File

@@ -1,7 +1,8 @@
import type { Context } from '@yaakapp/api'; import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken'; import { fetchAccessToken } from '../fetchAccessToken';
import { isTokenExpired } from '../getAccessTokenIfNotExpired'; import type { TokenStoreArgs } from '../store';
import { getToken, storeToken } from '../store'; import { getToken, storeToken } from '../store';
import { isTokenExpired } from '../util';
export async function getClientCredentials( export async function getClientCredentials(
ctx: Context, ctx: Context,
@@ -22,7 +23,13 @@ export async function getClientCredentials(
credentialsInBody: boolean; credentialsInBody: boolean;
}, },
) { ) {
const token = await getToken(ctx, contextId); const tokenArgs: TokenStoreArgs = {
contextId,
clientId,
accessTokenUrl,
authorizationUrl: null,
};
const token = await getToken(ctx, tokenArgs);
if (token && !isTokenExpired(token)) { if (token && !isTokenExpired(token)) {
return token; return token;
} }
@@ -38,5 +45,5 @@ export async function getClientCredentials(
params: [], params: [],
}); });
return storeToken(ctx, contextId, response); return storeToken(ctx, tokenArgs, response);
} }

View File

@@ -1,7 +1,7 @@
import type { Context } from '@yaakapp/api'; import type { Context } from '@yaakapp/api';
import { isTokenExpired } from '../getAccessTokenIfNotExpired'; import type { AccessToken, AccessTokenRawResponse } from '../store';
import type { AccessToken, AccessTokenRawResponse} from '../store';
import { getToken, storeToken } from '../store'; import { getToken, storeToken } from '../store';
import { isTokenExpired } from '../util';
export async function getImplicit( export async function getImplicit(
ctx: Context, ctx: Context,
@@ -26,7 +26,13 @@ export async function getImplicit(
tokenName: 'access_token' | 'id_token'; tokenName: 'access_token' | 'id_token';
}, },
): Promise<AccessToken> { ): Promise<AccessToken> {
const token = await getToken(ctx, contextId); const tokenArgs = {
contextId,
clientId,
accessTokenUrl: null,
authorizationUrl: authorizationUrlRaw,
};
const token = await getToken(ctx, tokenArgs);
if (token != null && !isTokenExpired(token)) { if (token != null && !isTokenExpired(token)) {
return token; return token;
} }
@@ -82,7 +88,7 @@ export async function getImplicit(
const response = Object.fromEntries(params) as unknown as AccessTokenRawResponse; const response = Object.fromEntries(params) as unknown as AccessTokenRawResponse;
try { try {
resolve(storeToken(ctx, contextId, response)); resolve(storeToken(ctx, tokenArgs, response));
} catch (err) { } catch (err) {
reject(err); reject(err);
} }

View File

@@ -1,7 +1,7 @@
import type { Context } from '@yaakapp/api'; import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken'; import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken'; import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import type { AccessToken} from '../store'; import type { AccessToken, TokenStoreArgs } from '../store';
import { storeToken } from '../store'; import { storeToken } from '../store';
export async function getPassword( export async function getPassword(
@@ -27,7 +27,13 @@ export async function getPassword(
credentialsInBody: boolean; credentialsInBody: boolean;
}, },
): Promise<AccessToken> { ): Promise<AccessToken> {
const token = await getOrRefreshAccessToken(ctx, contextId, { const tokenArgs: TokenStoreArgs = {
contextId,
clientId,
accessTokenUrl,
authorizationUrl: null,
};
const token = await getOrRefreshAccessToken(ctx, tokenArgs, {
accessTokenUrl, accessTokenUrl,
scope, scope,
clientId, clientId,
@@ -52,5 +58,5 @@ export async function getPassword(
], ],
}); });
return storeToken(ctx, contextId, response); return storeToken(ctx, tokenArgs, response);
} }

View File

@@ -15,7 +15,7 @@ import {
import { getClientCredentials } from './grants/clientCredentials'; import { getClientCredentials } from './grants/clientCredentials';
import { getImplicit } from './grants/implicit'; import { getImplicit } from './grants/implicit';
import { getPassword } from './grants/password'; import { getPassword } from './grants/password';
import type { AccessToken } from './store'; import type { AccessToken, TokenStoreArgs } from './store';
import { deleteToken, getToken, resetDataDirKey } from './store'; import { deleteToken, getToken, resetDataDirKey } from './store';
type GrantType = 'authorization_code' | 'implicit' | 'password' | 'client_credentials'; type GrantType = 'authorization_code' | 'implicit' | 'password' | 'client_credentials';
@@ -83,8 +83,14 @@ export const plugin: PluginDefinition = {
actions: [ actions: [
{ {
label: 'Copy Current Token', label: 'Copy Current Token',
async onSelect(ctx, { contextId }) { async onSelect(ctx, { contextId, values }) {
const token = await getToken(ctx, contextId); const tokenArgs: TokenStoreArgs = {
contextId,
authorizationUrl: stringArg(values, 'authorizationUrl'),
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
clientId: stringArg(values, 'clientId'),
};
const token = await getToken(ctx, tokenArgs);
if (token == null) { if (token == null) {
await ctx.toast.show({ message: 'No token to copy', color: 'warning' }); await ctx.toast.show({ message: 'No token to copy', color: 'warning' });
} else { } else {
@@ -99,8 +105,14 @@ export const plugin: PluginDefinition = {
}, },
{ {
label: 'Delete Token', label: 'Delete Token',
async onSelect(ctx, { contextId }) { async onSelect(ctx, { contextId, values }) {
if (await deleteToken(ctx, contextId)) { const tokenArgs: TokenStoreArgs = {
contextId,
authorizationUrl: stringArg(values, 'authorizationUrl'),
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
clientId: stringArg(values, 'clientId'),
};
if (await deleteToken(ctx, tokenArgs)) {
await ctx.toast.show({ message: 'Token deleted', color: 'success' }); await ctx.toast.show({ message: 'Token deleted', color: 'success' });
} else { } else {
await ctx.toast.show({ message: 'No token to delete', color: 'warning' }); await ctx.toast.show({ message: 'No token to delete', color: 'warning' });
@@ -281,8 +293,14 @@ export const plugin: PluginDefinition = {
{ {
type: 'accordion', type: 'accordion',
label: 'Access Token Response', label: 'Access Token Response',
async dynamic(ctx, { contextId }) { async dynamic(ctx, { contextId, values }) {
const token = await getToken(ctx, contextId); const tokenArgs: TokenStoreArgs = {
contextId,
authorizationUrl: stringArg(values, 'authorizationUrl'),
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
clientId: stringArg(values, 'clientId'),
};
const token = await getToken(ctx, tokenArgs);
if (token == null) { if (token == null) {
return { hidden: true }; return { hidden: true };
} }
@@ -316,9 +334,10 @@ export const plugin: PluginDefinition = {
accessTokenUrl === '' || accessTokenUrl.match(/^https?:\/\//) accessTokenUrl === '' || accessTokenUrl.match(/^https?:\/\//)
? accessTokenUrl ? accessTokenUrl
: `https://${accessTokenUrl}`, : `https://${accessTokenUrl}`,
authorizationUrl: authorizationUrl === '' || authorizationUrl.match(/^https?:\/\//) authorizationUrl:
? authorizationUrl authorizationUrl === '' || authorizationUrl.match(/^https?:\/\//)
: `https://${authorizationUrl}`, ? authorizationUrl
: `https://${authorizationUrl}`,
clientId: stringArg(values, 'clientId'), clientId: stringArg(values, 'clientId'),
clientSecret: stringArg(values, 'clientSecret'), clientSecret: stringArg(values, 'clientSecret'),
redirectUri: stringArgOrNull(values, 'redirectUri'), redirectUri: stringArgOrNull(values, 'redirectUri'),

View File

@@ -1,8 +1,9 @@
import type { Context } from '@yaakapp/api'; import type { Context } from '@yaakapp/api';
import { createHash } from 'node:crypto';
export async function storeToken( export async function storeToken(
ctx: Context, ctx: Context,
contextId: string, args: TokenStoreArgs,
response: AccessTokenRawResponse, response: AccessTokenRawResponse,
tokenName: 'access_token' | 'id_token' = 'access_token', tokenName: 'access_token' | 'id_token' = 'access_token',
) { ) {
@@ -15,16 +16,16 @@ export async function storeToken(
response, response,
expiresAt, expiresAt,
}; };
await ctx.store.set<AccessToken>(tokenStoreKey(contextId), token); await ctx.store.set<AccessToken>(tokenStoreKey(args), token);
return token; return token;
} }
export async function getToken(ctx: Context, contextId: string) { export async function getToken(ctx: Context, args: TokenStoreArgs) {
return ctx.store.get<AccessToken>(tokenStoreKey(contextId)); return ctx.store.get<AccessToken>(tokenStoreKey(args));
} }
export async function deleteToken(ctx: Context, contextId: string) { export async function deleteToken(ctx: Context, args: TokenStoreArgs) {
return ctx.store.delete(tokenStoreKey(contextId)); return ctx.store.delete(tokenStoreKey(args));
} }
export async function resetDataDirKey(ctx: Context, contextId: string) { export async function resetDataDirKey(ctx: Context, contextId: string) {
@@ -37,8 +38,25 @@ export async function getDataDirKey(ctx: Context, contextId: string) {
return `${contextId}::${key}`; return `${contextId}::${key}`;
} }
function tokenStoreKey(contextId: string) { export interface TokenStoreArgs {
return ['token', contextId].join('::'); contextId: string;
clientId: string;
accessTokenUrl: string | null;
authorizationUrl: string | null;
}
/**
* Generate a store key to use based on some arguments. The arguments will be normalized a bit to
* account for slight variations (like domains with and without a protocol scheme).
*/
function tokenStoreKey(args: TokenStoreArgs) {
const hash = createHash('md5');
if (args.contextId) hash.update(args.contextId.trim());
if (args.clientId) hash.update(args.clientId.trim());
if (args.accessTokenUrl) hash.update(args.accessTokenUrl.trim().replace(/^https?:\/\//, ''));
if (args.authorizationUrl) hash.update(args.authorizationUrl.trim().replace(/^https?:\/\//, ''));
const key = hash.digest('hex');
return ['token', key].join('::');
} }
function dataDirStoreKey(contextId: string) { function dataDirStoreKey(contextId: string) {

View File

@@ -0,0 +1,5 @@
import type { AccessToken } from './store';
export function isTokenExpired(token: AccessToken) {
return token.expiresAt && Date.now() > token.expiresAt;
}

View File

@@ -55,11 +55,6 @@ pub async fn send_http_request<R: Runtime>(
let response_id = og_response.id.clone(); let response_id = og_response.id.clone();
let response = Arc::new(Mutex::new(og_response.clone())); let response = Arc::new(Mutex::new(og_response.clone()));
let cb = PluginTemplateCallback::new(
window.app_handle(),
&PluginWindowContext::new(window),
RenderPurpose::Send,
);
let update_source = UpdateSource::from_window(window); let update_source = UpdateSource::from_window(window);
let (resolved_request, auth_context_id) = match resolve_http_request(window, unrendered_request) let (resolved_request, auth_context_id) = match resolve_http_request(window, unrendered_request)
@@ -75,6 +70,12 @@ pub async fn send_http_request<R: Runtime>(
} }
}; };
let cb = PluginTemplateCallback::new(
window.app_handle(),
&PluginWindowContext::new(window),
RenderPurpose::Send,
);
let request = let request =
match render_http_request(&resolved_request, &base_environment, environment.as_ref(), &cb) match render_http_request(&resolved_request, &base_environment, environment.as_ref(), &cb)
.await .await

View File

@@ -35,7 +35,13 @@ use yaak_models::models::{
}; };
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources}; use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
use yaak_plugins::events::{CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest}; use yaak_plugins::events::{
CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs,
CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse,
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent,
InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest,
};
use yaak_plugins::manager::PluginManager; use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_meta::PluginMetadata; use yaak_plugins::plugin_meta::PluginMetadata;
use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_plugins::template_callback::PluginTemplateCallback;
@@ -818,9 +824,29 @@ async fn cmd_get_http_authentication_config<R: Runtime>(
auth_name: &str, auth_name: &str,
values: HashMap<String, JsonPrimitive>, values: HashMap<String, JsonPrimitive>,
request_id: &str, request_id: &str,
environment_id: Option<&str>,
workspace_id: &str,
) -> YaakResult<GetHttpAuthenticationConfigResponse> { ) -> YaakResult<GetHttpAuthenticationConfigResponse> {
let base_environment = window.db().get_base_environment(&workspace_id)?;
let environment = match environment_id {
Some(id) => match window.db().get_environment(id) {
Ok(env) => Some(env),
Err(e) => {
warn!("Failed to find environment by id {id} {}", e);
None
}
},
None => None,
};
Ok(plugin_manager Ok(plugin_manager
.get_http_authentication_config(&window, auth_name, values, request_id) .get_http_authentication_config(
&window,
&base_environment,
environment.as_ref(),
auth_name,
values,
request_id,
)
.await?) .await?)
} }
@@ -872,9 +898,30 @@ async fn cmd_call_http_authentication_action<R: Runtime>(
action_index: i32, action_index: i32,
values: HashMap<String, JsonPrimitive>, values: HashMap<String, JsonPrimitive>,
model_id: &str, model_id: &str,
workspace_id: &str,
environment_id: Option<&str>,
) -> YaakResult<()> { ) -> YaakResult<()> {
let base_environment = window.db().get_base_environment(&workspace_id)?;
let environment = match environment_id {
Some(id) => match window.db().get_environment(id) {
Ok(env) => Some(env),
Err(e) => {
warn!("Failed to find environment by id {id} {}", e);
None
}
},
None => None,
};
Ok(plugin_manager Ok(plugin_manager
.call_http_authentication_action(&window, auth_name, action_index, values, model_id) .call_http_authentication_action(
&window,
&base_environment,
environment.as_ref(),
auth_name,
action_index,
values,
model_id,
)
.await?) .await?)
} }
@@ -1238,7 +1285,10 @@ pub fn run() {
let _ = app_handle.emit( let _ = app_handle.emit(
"show_toast", "show_toast",
ShowToastRequest { ShowToastRequest {
message: format!("Error handling deep link: {}", e.to_string()), message: format!(
"Error handling deep link: {}",
e.to_string()
),
color: Some(Color::Danger), color: Some(Color::Danger),
icon: None, icon: None,
}, },

View File

@@ -18,7 +18,9 @@ use crate::native_template_functions::template_function_secure;
use crate::nodejs::start_nodejs_plugin_runtime; use crate::nodejs::start_nodejs_plugin_runtime;
use crate::plugin_handle::PluginHandle; use crate::plugin_handle::PluginHandle;
use crate::server_ws::PluginRuntimeServerWebsocket; use crate::server_ws::PluginRuntimeServerWebsocket;
use crate::template_callback::PluginTemplateCallback;
use log::{error, info, warn}; use log::{error, info, warn};
use serde_json::json;
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::env;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -30,10 +32,13 @@ use tokio::fs::read_dir;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::sync::{Mutex, mpsc}; use tokio::sync::{Mutex, mpsc};
use tokio::time::{Instant, timeout}; use tokio::time::{Instant, timeout};
use yaak_models::models::Environment;
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::render::make_vars_hashmap;
use yaak_models::util::generate_id; use yaak_models::util::generate_id;
use yaak_templates::error::Error::RenderError; use yaak_templates::error::Error::RenderError;
use yaak_templates::error::Result as TemplateResult; use yaak_templates::error::Result as TemplateResult;
use yaak_templates::render_json_value_raw;
#[derive(Clone)] #[derive(Clone)]
pub struct PluginManager { pub struct PluginManager {
@@ -569,6 +574,8 @@ impl PluginManager {
pub async fn get_http_authentication_config<R: Runtime>( pub async fn get_http_authentication_config<R: Runtime>(
&self, &self,
window: &WebviewWindow<R>, window: &WebviewWindow<R>,
base_environment: &Environment,
environment: Option<&Environment>,
auth_name: &str, auth_name: &str,
values: HashMap<String, JsonPrimitive>, values: HashMap<String, JsonPrimitive>,
request_id: &str, request_id: &str,
@@ -579,13 +586,23 @@ impl PluginManager {
.find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None }) .find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None })
.ok_or(PluginNotFoundErr(auth_name.into()))?; .ok_or(PluginNotFoundErr(auth_name.into()))?;
let vars = &make_vars_hashmap(&base_environment, environment);
let cb = PluginTemplateCallback::new(
window.app_handle(),
&PluginWindowContext::new(&window),
RenderPurpose::Preview,
);
let rendered_values = render_json_value_raw(json!(values), vars, &cb).await?;
let context_id = format!("{:x}", md5::compute(request_id.to_string())); let context_id = format!("{:x}", md5::compute(request_id.to_string()));
let event = self let event = self
.send_to_plugin_and_wait( .send_to_plugin_and_wait(
&PluginWindowContext::new(window), &PluginWindowContext::new(window),
&plugin, &plugin,
&InternalEventPayload::GetHttpAuthenticationConfigRequest( &InternalEventPayload::GetHttpAuthenticationConfigRequest(
GetHttpAuthenticationConfigRequest { values, context_id }, GetHttpAuthenticationConfigRequest {
values: serde_json::from_value(rendered_values)?,
context_id,
},
), ),
) )
.await?; .await?;
@@ -602,11 +619,24 @@ impl PluginManager {
pub async fn call_http_authentication_action<R: Runtime>( pub async fn call_http_authentication_action<R: Runtime>(
&self, &self,
window: &WebviewWindow<R>, window: &WebviewWindow<R>,
base_environment: &Environment,
environment: Option<&Environment>,
auth_name: &str, auth_name: &str,
action_index: i32, action_index: i32,
values: HashMap<String, JsonPrimitive>, values: HashMap<String, JsonPrimitive>,
model_id: &str, model_id: &str,
) -> Result<()> { ) -> Result<()> {
let vars = &make_vars_hashmap(&base_environment, environment);
let rendered_values = render_json_value_raw(
json!(values),
vars,
&PluginTemplateCallback::new(
window.app_handle(),
&PluginWindowContext::new(&window),
RenderPurpose::Preview,
),
)
.await?;
let results = self.get_http_authentication_summaries(window).await?; let results = self.get_http_authentication_summaries(window).await?;
let plugin = results let plugin = results
.iter() .iter()
@@ -621,7 +651,10 @@ impl PluginManager {
CallHttpAuthenticationActionRequest { CallHttpAuthenticationActionRequest {
index: action_index, index: action_index,
plugin_ref_id: plugin.clone().ref_id, plugin_ref_id: plugin.clone().ref_id,
args: CallHttpAuthenticationActionArgs { context_id, values }, args: CallHttpAuthenticationActionArgs {
context_id,
values: serde_json::from_value(rendered_values)?,
},
}, },
), ),
) )

View File

@@ -182,23 +182,24 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
); );
case 'accordion': case 'accordion':
return ( return (
<DetailsBanner <div key={i}>
key={i} <DetailsBanner
summary={input.label} summary={input.label}
className={classNames(disabled && 'opacity-disabled')} className={classNames('!mb-auto', disabled && 'opacity-disabled')}
> >
<div className="mb-3 px-3"> <div className="mb-3 px-3">
<FormInputs <FormInputs
data={data} data={data}
disabled={disabled} disabled={disabled}
inputs={input.inputs} inputs={input.inputs}
setDataAttr={setDataAttr} setDataAttr={setDataAttr}
stateKey={stateKey} stateKey={stateKey}
autocompleteFunctions={autocompleteFunctions || false} autocompleteFunctions={autocompleteFunctions || false}
autocompleteVariables={autocompleteVariables} autocompleteVariables={autocompleteVariables}
/> />
</div> </div>
</DetailsBanner> </DetailsBanner>
</div>
); );
case 'banner': case 'banner':
return ( return (
@@ -309,6 +310,7 @@ function EditorArg({
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined} autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
disabled={arg.disabled} disabled={arg.disabled}
language={arg.language} language={arg.language}
readOnly={arg.readOnly}
onChange={onChange} onChange={onChange}
heightMode="auto" heightMode="auto"
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value} defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
@@ -329,9 +331,9 @@ function EditorArg({
showDialog({ showDialog({
id: 'id', id: 'id',
size: 'full', size: 'full',
title: 'Edit Value', title: arg.readOnly ? 'View Value' : 'Edit Value',
className: '!max-w-[50rem] !max-h-[60rem]', className: '!max-w-[50rem] !max-h-[60rem]',
description: ( description: arg.label && (
<Label <Label
htmlFor={id} htmlFor={id}
required={!arg.optional} required={!arg.optional}
@@ -355,6 +357,7 @@ function EditorArg({
} }
disabled={arg.disabled} disabled={arg.disabled}
language={arg.language} language={arg.language}
readOnly={arg.readOnly}
onChange={onChange} onChange={onChange}
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value} defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
placeholder={arg.placeholder ?? undefined} placeholder={arg.placeholder ?? undefined}

View File

@@ -12,12 +12,16 @@ import { useAtomValue } from 'jotai';
import { md5 } from 'js-md5'; import { md5 } from 'js-md5';
import { useState } from 'react'; import { useState } from 'react';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { activeEnvironmentIdAtom } from './useActiveEnvironment';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
export function useHttpAuthenticationConfig( export function useHttpAuthenticationConfig(
authName: string | null, authName: string | null,
values: Record<string, JsonPrimitive>, values: Record<string, JsonPrimitive>,
requestId: string, requestId: string,
) { ) {
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const environmentId = useAtomValue(activeEnvironmentIdAtom);
const responses = useAtomValue(httpResponsesAtom); const responses = useAtomValue(httpResponsesAtom);
const [forceRefreshCounter, setForceRefreshCounter] = useState<number>(0); const [forceRefreshCounter, setForceRefreshCounter] = useState<number>(0);
@@ -38,6 +42,8 @@ export function useHttpAuthenticationConfig(
values, values,
responseKey, responseKey,
forceRefreshCounter, forceRefreshCounter,
workspaceId,
environmentId,
], ],
placeholderData: (prev) => prev, // Keep previous data on refetch placeholderData: (prev) => prev, // Keep previous data on refetch
queryFn: async () => { queryFn: async () => {
@@ -48,6 +54,8 @@ export function useHttpAuthenticationConfig(
authName, authName,
values, values,
requestId, requestId,
workspaceId,
environmentId,
}, },
); );
@@ -64,6 +72,8 @@ export function useHttpAuthenticationConfig(
authName, authName,
values, values,
modelId, modelId,
environmentId,
workspaceId,
}); });
// Ensure the config is refreshed after the action is done // Ensure the config is refreshed after the action is done