mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-11 22:40:26 +01:00
[Plugins] [Auth] [JWT] Add addtional JWT headers input (#247)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
This commit is contained in:
33
crates/yaak-plugins/bindings/gen_events.ts
generated
33
crates/yaak-plugins/bindings/gen_events.ts
generated
@@ -92,7 +92,7 @@ export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
|
|||||||
|
|
||||||
export type FolderAction = { label: string, icon?: Icon, };
|
export type FolderAction = { label: string, icon?: Icon, };
|
||||||
|
|
||||||
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "h_stack" } & FormInputHStack | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown;
|
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "h_stack" } & FormInputHStack | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown | { "type": "key_value" } & FormInputKeyValue;
|
||||||
|
|
||||||
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
|
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
|
||||||
|
|
||||||
@@ -275,6 +275,37 @@ defaultValue?: string, disabled?: boolean,
|
|||||||
*/
|
*/
|
||||||
description?: string, };
|
description?: string, };
|
||||||
|
|
||||||
|
export type FormInputKeyValue = {
|
||||||
|
/**
|
||||||
|
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||||
|
*/
|
||||||
|
name: string,
|
||||||
|
/**
|
||||||
|
* Whether this input is visible for the given configuration. Use this to
|
||||||
|
* make branching forms.
|
||||||
|
*/
|
||||||
|
hidden?: boolean,
|
||||||
|
/**
|
||||||
|
* Whether the user must fill in the argument
|
||||||
|
*/
|
||||||
|
optional?: boolean,
|
||||||
|
/**
|
||||||
|
* The label of the input
|
||||||
|
*/
|
||||||
|
label?: string,
|
||||||
|
/**
|
||||||
|
* Visually hide the label of the input
|
||||||
|
*/
|
||||||
|
hideLabel?: boolean,
|
||||||
|
/**
|
||||||
|
* The default value
|
||||||
|
*/
|
||||||
|
defaultValue?: string, disabled?: boolean,
|
||||||
|
/**
|
||||||
|
* Longer description of the input, likely shown in a tooltip
|
||||||
|
*/
|
||||||
|
description?: string, };
|
||||||
|
|
||||||
export type FormInputMarkdown = { content: string, hidden?: boolean, };
|
export type FormInputMarkdown = { content: string, hidden?: boolean, };
|
||||||
|
|
||||||
export type FormInputSelect = {
|
export type FormInputSelect = {
|
||||||
|
|||||||
@@ -48,11 +48,7 @@ impl PluginContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(label: Option<String>, workspace_id: Option<String>) -> Self {
|
pub fn new(label: Option<String>, workspace_id: Option<String>) -> Self {
|
||||||
Self {
|
Self { label, workspace_id, id: generate_prefixed_id("pctx") }
|
||||||
label,
|
|
||||||
workspace_id,
|
|
||||||
id: generate_prefixed_id("pctx"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,6 +838,7 @@ pub enum FormInput {
|
|||||||
HStack(FormInputHStack),
|
HStack(FormInputHStack),
|
||||||
Banner(FormInputBanner),
|
Banner(FormInputBanner),
|
||||||
Markdown(FormInputMarkdown),
|
Markdown(FormInputMarkdown),
|
||||||
|
KeyValue(FormInputKeyValue),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
@@ -1095,6 +1092,14 @@ pub struct FormInputMarkdown {
|
|||||||
pub hidden: Option<bool>,
|
pub hidden: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(default, rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "gen_events.ts")]
|
||||||
|
pub struct FormInputKeyValue {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub base: FormInputBase,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
#[serde(rename_all = "snake_case", tag = "type")]
|
#[serde(rename_all = "snake_case", tag = "type")]
|
||||||
#[ts(export, export_to = "gen_events.ts")]
|
#[ts(export, export_to = "gen_events.ts")]
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
|
|||||||
|
|
||||||
export type FolderAction = { label: string, icon?: Icon, };
|
export type FolderAction = { label: string, icon?: Icon, };
|
||||||
|
|
||||||
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "h_stack" } & FormInputHStack | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown;
|
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "h_stack" } & FormInputHStack | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown | { "type": "key_value" } & FormInputKeyValue;
|
||||||
|
|
||||||
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
|
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
|
||||||
|
|
||||||
@@ -275,6 +275,37 @@ defaultValue?: string, disabled?: boolean,
|
|||||||
*/
|
*/
|
||||||
description?: string, };
|
description?: string, };
|
||||||
|
|
||||||
|
export type FormInputKeyValue = {
|
||||||
|
/**
|
||||||
|
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||||
|
*/
|
||||||
|
name: string,
|
||||||
|
/**
|
||||||
|
* Whether this input is visible for the given configuration. Use this to
|
||||||
|
* make branching forms.
|
||||||
|
*/
|
||||||
|
hidden?: boolean,
|
||||||
|
/**
|
||||||
|
* Whether the user must fill in the argument
|
||||||
|
*/
|
||||||
|
optional?: boolean,
|
||||||
|
/**
|
||||||
|
* The label of the input
|
||||||
|
*/
|
||||||
|
label?: string,
|
||||||
|
/**
|
||||||
|
* Visually hide the label of the input
|
||||||
|
*/
|
||||||
|
hideLabel?: boolean,
|
||||||
|
/**
|
||||||
|
* The default value
|
||||||
|
*/
|
||||||
|
defaultValue?: string, disabled?: boolean,
|
||||||
|
/**
|
||||||
|
* Longer description of the input, likely shown in a tooltip
|
||||||
|
*/
|
||||||
|
description?: string, };
|
||||||
|
|
||||||
export type FormInputMarkdown = { content: string, hidden?: boolean, };
|
export type FormInputMarkdown = { content: string, hidden?: boolean, };
|
||||||
|
|
||||||
export type FormInputSelect = {
|
export type FormInputSelect = {
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ A JWT consists of three parts separated by dots:
|
|||||||
|
|
||||||
1. Configure the request, folder, or workspace to use JWT Authentication
|
1. Configure the request, folder, or workspace to use JWT Authentication
|
||||||
2. Set up your signing algorithm and secret/key
|
2. Set up your signing algorithm and secret/key
|
||||||
3. Configure the required claims for your JWT
|
3. Add custom JWT header fields if needed
|
||||||
4. The plugin will generate, sign, and include the JWT in your requests
|
4. Configure the required claims for your JWT payload
|
||||||
|
5. The plugin will generate, sign, and include the JWT in your requests
|
||||||
|
|
||||||
## Common Use Cases
|
## Common Use Cases
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,28 @@ export const plugin: PluginDefinition = {
|
|||||||
name: 'secretBase64',
|
name: 'secretBase64',
|
||||||
label: 'Secret is base64 encoded',
|
label: 'Secret is base64 encoded',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'editor',
|
||||||
|
name: 'payload',
|
||||||
|
label: 'JWT Payload',
|
||||||
|
language: 'json',
|
||||||
|
defaultValue: '{\n "foo": "bar"\n}',
|
||||||
|
placeholder: '{ }',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'accordion',
|
||||||
|
label: 'Advanced',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
type: 'editor',
|
||||||
|
name: 'headers',
|
||||||
|
label: 'JWT Header',
|
||||||
|
description: 'Merged with auto-generated header fields like alg (e.g., kid)',
|
||||||
|
language: 'json',
|
||||||
|
defaultValue: '{}',
|
||||||
|
placeholder: '{ }',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'select',
|
type: 'select',
|
||||||
name: 'location',
|
name: 'location',
|
||||||
@@ -56,24 +78,16 @@ export const plugin: PluginDefinition = {
|
|||||||
{ label: 'Append Query Parameter', value: 'query' },
|
{ label: 'Append Query Parameter', value: 'query' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'h_stack',
|
||||||
|
inputs: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: 'Header Name',
|
label: 'Header Name',
|
||||||
defaultValue: 'Authorization',
|
defaultValue: 'Authorization',
|
||||||
optional: true,
|
optional: true,
|
||||||
dynamic(_ctx, args) {
|
|
||||||
if (args.values.location === 'query') {
|
|
||||||
return {
|
|
||||||
label: 'Parameter Name',
|
|
||||||
description: 'The name of the query parameter to add to the request',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
label: 'Header Name',
|
|
||||||
description: 'The name of the header to add to the request',
|
description: 'The name of the header to add to the request',
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -81,6 +95,8 @@ export const plugin: PluginDefinition = {
|
|||||||
label: 'Header Prefix',
|
label: 'Header Prefix',
|
||||||
optional: true,
|
optional: true,
|
||||||
defaultValue: 'Bearer',
|
defaultValue: 'Bearer',
|
||||||
|
},
|
||||||
|
],
|
||||||
dynamic(_ctx, args) {
|
dynamic(_ctx, args) {
|
||||||
if (args.values.location === 'query') {
|
if (args.values.location === 'query') {
|
||||||
return {
|
return {
|
||||||
@@ -90,25 +106,38 @@ export const plugin: PluginDefinition = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'editor',
|
type: 'text',
|
||||||
name: 'payload',
|
name: 'name',
|
||||||
label: 'Payload',
|
label: 'Parameter Name',
|
||||||
language: 'json',
|
description: 'The name of the query parameter to add to the request',
|
||||||
defaultValue: '{\n "foo": "bar"\n}',
|
defaultValue: 'token',
|
||||||
placeholder: '{ }',
|
optional: true,
|
||||||
|
dynamic(_ctx, args) {
|
||||||
|
if (args.values.location !== 'query') {
|
||||||
|
return {
|
||||||
|
hidden: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
async onApply(_ctx, { values }) {
|
async onApply(_ctx, { values }) {
|
||||||
const { algorithm, secret: _secret, secretBase64, payload } = values;
|
const { algorithm, secret: _secret, secretBase64, payload, headers } = values;
|
||||||
const secret = secretBase64 ? Buffer.from(`${_secret}`, 'base64') : `${_secret}`;
|
const secret = secretBase64 ? Buffer.from(`${_secret}`, 'base64') : `${_secret}`;
|
||||||
|
|
||||||
|
const parsedHeaders = headers ? JSON.parse(`${headers}`) : undefined;
|
||||||
|
|
||||||
const token = jwt.sign(`${payload}`, secret, {
|
const token = jwt.sign(`${payload}`, secret, {
|
||||||
algorithm: algorithm as (typeof algorithms)[number],
|
algorithm: algorithm as (typeof algorithms)[number],
|
||||||
|
// Extra header fields are merged with the auto-generated header (which includes alg)
|
||||||
|
header: parsedHeaders as jwt.JwtHeader | undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (values.location === 'query') {
|
if (values.location === 'query') {
|
||||||
const paramName = String(values.name || 'token');
|
const paramName = String(values.name || 'token');
|
||||||
const paramValue = String(values.value || '');
|
return { setQueryParameters: [{ name: paramName, value: token }] };
|
||||||
return { setQueryParameters: [{ name: paramName, value: paramValue }] };
|
|
||||||
}
|
}
|
||||||
const headerPrefix = values.headerPrefix != null ? values.headerPrefix : 'Bearer';
|
const headerPrefix = values.headerPrefix != null ? values.headerPrefix : 'Bearer';
|
||||||
const headerName = String(values.name || 'Authorization');
|
const headerName = String(values.name || 'Authorization');
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ import type {
|
|||||||
FormInputEditor,
|
FormInputEditor,
|
||||||
FormInputFile,
|
FormInputFile,
|
||||||
FormInputHttpRequest,
|
FormInputHttpRequest,
|
||||||
|
FormInputKeyValue,
|
||||||
FormInputSelect,
|
FormInputSelect,
|
||||||
FormInputText,
|
FormInputText,
|
||||||
JsonPrimitive,
|
JsonPrimitive,
|
||||||
} from '@yaakapp-internal/plugins';
|
} from '@yaakapp-internal/plugins';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
import { useRandomKey } from '../hooks/useRandomKey';
|
import { useRandomKey } from '../hooks/useRandomKey';
|
||||||
import { capitalize } from '../lib/capitalize';
|
import { capitalize } from '../lib/capitalize';
|
||||||
@@ -26,6 +27,8 @@ import { IconButton } from './core/IconButton';
|
|||||||
import type { InputProps } from './core/Input';
|
import type { InputProps } from './core/Input';
|
||||||
import { Input } from './core/Input';
|
import { Input } from './core/Input';
|
||||||
import { Label } from './core/Label';
|
import { Label } from './core/Label';
|
||||||
|
import type { Pair } from './core/PairEditor';
|
||||||
|
import { PairEditor } from './core/PairEditor';
|
||||||
import { PlainInput } from './core/PlainInput';
|
import { PlainInput } from './core/PlainInput';
|
||||||
import { Select } from './core/Select';
|
import { Select } from './core/Select';
|
||||||
import { VStack } from './core/Stacks';
|
import { VStack } from './core/Stacks';
|
||||||
@@ -201,7 +204,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
|||||||
summary={input.label}
|
summary={input.label}
|
||||||
className={classNames('!mb-auto', disabled && 'opacity-disabled')}
|
className={classNames('!mb-auto', disabled && 'opacity-disabled')}
|
||||||
>
|
>
|
||||||
<div className="my-3">
|
<div className="mt-3">
|
||||||
<FormInputsStack
|
<FormInputsStack
|
||||||
data={data}
|
data={data}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -249,6 +252,18 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
|||||||
);
|
);
|
||||||
case 'markdown':
|
case 'markdown':
|
||||||
return <Markdown key={i + stateKey}>{input.content}</Markdown>;
|
return <Markdown key={i + stateKey}>{input.content}</Markdown>;
|
||||||
|
case 'key_value':
|
||||||
|
return (
|
||||||
|
<KeyValueArg
|
||||||
|
key={i + stateKey}
|
||||||
|
arg={input}
|
||||||
|
stateKey={stateKey}
|
||||||
|
onChange={(v) => setDataAttr(input.name, v)}
|
||||||
|
value={
|
||||||
|
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? '[]')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
throw new Error(`Invalid input type: ${input.type}`);
|
throw new Error(`Invalid input type: ${input.type}`);
|
||||||
@@ -539,3 +554,52 @@ function CheckboxArg({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function KeyValueArg({
|
||||||
|
arg,
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
stateKey,
|
||||||
|
}: {
|
||||||
|
arg: FormInputKeyValue;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
stateKey: string;
|
||||||
|
}) {
|
||||||
|
const pairs: Pair[] = useMemo(() => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(newPairs: Pair[]) => {
|
||||||
|
onChange(JSON.stringify(newPairs));
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full grid grid-cols-1 grid-rows-[auto_minmax(0,1fr)] overflow-hidden">
|
||||||
|
<Label
|
||||||
|
htmlFor={`input-${arg.name}`}
|
||||||
|
required={!arg.optional}
|
||||||
|
visuallyHidden={arg.hideLabel}
|
||||||
|
help={arg.description}
|
||||||
|
>
|
||||||
|
{arg.label ?? arg.name}
|
||||||
|
</Label>
|
||||||
|
<PairEditor
|
||||||
|
pairs={pairs}
|
||||||
|
onChange={handleChange}
|
||||||
|
stateKey={stateKey}
|
||||||
|
namePlaceholder="name"
|
||||||
|
valuePlaceholder="value"
|
||||||
|
noScroll
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -557,7 +557,7 @@ export function PairEditorRow({
|
|||||||
ref={handleSetRef}
|
ref={handleSetRef}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
'group grid grid-cols-[auto_auto_minmax(0,1fr)_auto]',
|
'group/pair-row grid grid-cols-[auto_auto_minmax(0,1fr)_auto]',
|
||||||
'grid-rows-1 items-center',
|
'grid-rows-1 items-center',
|
||||||
!pair.enabled && 'opacity-60',
|
!pair.enabled && 'opacity-60',
|
||||||
)}
|
)}
|
||||||
@@ -576,7 +576,7 @@ export function PairEditorRow({
|
|||||||
{...listeners}
|
{...listeners}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'py-2 h-7 w-4 flex items-center',
|
'py-2 h-7 w-4 flex items-center',
|
||||||
'justify-center opacity-0 group-hover:opacity-70',
|
'justify-center opacity-0 group-hover/pair-row:opacity-70',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon size="sm" icon="grip_vertical" className="pointer-events-none" />
|
<Icon size="sm" icon="grip_vertical" className="pointer-events-none" />
|
||||||
|
|||||||
Reference in New Issue
Block a user