mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-11 14:30:24 +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 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, };
|
||||
|
||||
@@ -275,6 +275,37 @@ defaultValue?: string, disabled?: boolean,
|
||||
*/
|
||||
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 FormInputSelect = {
|
||||
|
||||
@@ -48,11 +48,7 @@ impl PluginContext {
|
||||
}
|
||||
|
||||
pub fn new(label: Option<String>, workspace_id: Option<String>) -> Self {
|
||||
Self {
|
||||
label,
|
||||
workspace_id,
|
||||
id: generate_prefixed_id("pctx"),
|
||||
}
|
||||
Self { label, workspace_id, id: generate_prefixed_id("pctx") }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -842,6 +838,7 @@ pub enum FormInput {
|
||||
HStack(FormInputHStack),
|
||||
Banner(FormInputBanner),
|
||||
Markdown(FormInputMarkdown),
|
||||
KeyValue(FormInputKeyValue),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
@@ -1095,6 +1092,14 @@ pub struct FormInputMarkdown {
|
||||
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)]
|
||||
#[serde(rename_all = "snake_case", tag = "type")]
|
||||
#[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 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, };
|
||||
|
||||
@@ -275,6 +275,37 @@ defaultValue?: string, disabled?: boolean,
|
||||
*/
|
||||
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 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
|
||||
2. Set up your signing algorithm and secret/key
|
||||
3. Configure the required claims for your JWT
|
||||
4. The plugin will generate, sign, and include the JWT in your requests
|
||||
3. Add custom JWT header fields if needed
|
||||
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
|
||||
|
||||
|
||||
@@ -46,69 +46,98 @@ export const plugin: PluginDefinition = {
|
||||
name: 'secretBase64',
|
||||
label: 'Secret is base64 encoded',
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'location',
|
||||
label: 'Behavior',
|
||||
defaultValue: 'header',
|
||||
options: [
|
||||
{ label: 'Insert Header', value: 'header' },
|
||||
{ label: 'Append Query Parameter', value: 'query' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
label: 'Header Name',
|
||||
defaultValue: 'Authorization',
|
||||
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',
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'headerPrefix',
|
||||
label: 'Header Prefix',
|
||||
optional: true,
|
||||
defaultValue: 'Bearer',
|
||||
dynamic(_ctx, args) {
|
||||
if (args.values.location === 'query') {
|
||||
return {
|
||||
hidden: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'editor',
|
||||
name: 'payload',
|
||||
label: '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',
|
||||
name: 'location',
|
||||
label: 'Behavior',
|
||||
defaultValue: 'header',
|
||||
options: [
|
||||
{ label: 'Insert Header', value: 'header' },
|
||||
{ label: 'Append Query Parameter', value: 'query' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'h_stack',
|
||||
inputs: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
label: 'Header Name',
|
||||
defaultValue: 'Authorization',
|
||||
optional: true,
|
||||
description: 'The name of the header to add to the request',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'headerPrefix',
|
||||
label: 'Header Prefix',
|
||||
optional: true,
|
||||
defaultValue: 'Bearer',
|
||||
},
|
||||
],
|
||||
dynamic(_ctx, args) {
|
||||
if (args.values.location === 'query') {
|
||||
return {
|
||||
hidden: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
label: 'Parameter Name',
|
||||
description: 'The name of the query parameter to add to the request',
|
||||
defaultValue: 'token',
|
||||
optional: true,
|
||||
dynamic(_ctx, args) {
|
||||
if (args.values.location !== 'query') {
|
||||
return {
|
||||
hidden: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
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 parsedHeaders = headers ? JSON.parse(`${headers}`) : undefined;
|
||||
|
||||
const token = jwt.sign(`${payload}`, secret, {
|
||||
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') {
|
||||
const paramName = String(values.name || 'token');
|
||||
const paramValue = String(values.value || '');
|
||||
return { setQueryParameters: [{ name: paramName, value: paramValue }] };
|
||||
return { setQueryParameters: [{ name: paramName, value: token }] };
|
||||
}
|
||||
const headerPrefix = values.headerPrefix != null ? values.headerPrefix : 'Bearer';
|
||||
const headerName = String(values.name || 'Authorization');
|
||||
|
||||
@@ -6,13 +6,14 @@ import type {
|
||||
FormInputEditor,
|
||||
FormInputFile,
|
||||
FormInputHttpRequest,
|
||||
FormInputKeyValue,
|
||||
FormInputSelect,
|
||||
FormInputText,
|
||||
JsonPrimitive,
|
||||
} from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useRandomKey } from '../hooks/useRandomKey';
|
||||
import { capitalize } from '../lib/capitalize';
|
||||
@@ -26,6 +27,8 @@ import { IconButton } from './core/IconButton';
|
||||
import type { InputProps } from './core/Input';
|
||||
import { Input } from './core/Input';
|
||||
import { Label } from './core/Label';
|
||||
import type { Pair } from './core/PairEditor';
|
||||
import { PairEditor } from './core/PairEditor';
|
||||
import { PlainInput } from './core/PlainInput';
|
||||
import { Select } from './core/Select';
|
||||
import { VStack } from './core/Stacks';
|
||||
@@ -80,7 +83,7 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
|
||||
function FormInputsStack<T extends Record<string, JsonPrimitive>>({
|
||||
className,
|
||||
...props
|
||||
}: FormInputsProps<T> & { className?: string }) {
|
||||
}: FormInputsProps<T> & { className?: string}) {
|
||||
return (
|
||||
<VStack
|
||||
space={3}
|
||||
@@ -201,7 +204,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
summary={input.label}
|
||||
className={classNames('!mb-auto', disabled && 'opacity-disabled')}
|
||||
>
|
||||
<div className="my-3">
|
||||
<div className="mt-3">
|
||||
<FormInputsStack
|
||||
data={data}
|
||||
disabled={disabled}
|
||||
@@ -249,6 +252,18 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
);
|
||||
case '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:
|
||||
// @ts-expect-error
|
||||
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}
|
||||
className={classNames(
|
||||
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',
|
||||
!pair.enabled && 'opacity-60',
|
||||
)}
|
||||
@@ -576,7 +576,7 @@ export function PairEditorRow({
|
||||
{...listeners}
|
||||
className={classNames(
|
||||
'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" />
|
||||
|
||||
Reference in New Issue
Block a user