mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-02-05 01:55:05 +08:00
### What problem does this PR solve? Feat: Remove HMAC from the webhook #10427 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -43,6 +43,7 @@ function BeginForm({ node }: INextOperatorForm) {
|
||||
const form = useForm({
|
||||
defaultValues: values,
|
||||
resolver: zodResolver(BeginFormSchema),
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { WebhookAlgorithmList } from '@/constants/agent';
|
||||
import { WebhookJWTAlgorithmList } from '@/constants/agent';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const BeginFormSchema = z.object({
|
||||
@ -30,7 +30,14 @@ export const BeginFormSchema = z.object({
|
||||
max_body_size: z.string(),
|
||||
jwt: z
|
||||
.object({
|
||||
algorithm: z.string().default(WebhookAlgorithmList[0]).optional(),
|
||||
algorithm: z.string().default(WebhookJWTAlgorithmList[0]).optional(),
|
||||
required_claims: z.array(z.object({ value: z.string() })),
|
||||
})
|
||||
.optional(),
|
||||
hmac: z
|
||||
.object({
|
||||
header: z.string().optional(),
|
||||
secret: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
@ -2,11 +2,11 @@ import { useCallback } from 'react';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import {
|
||||
AgentDialogueMode,
|
||||
RateLimitPerList,
|
||||
WebhookContentType,
|
||||
WebhookExecutionMode,
|
||||
WebhookMaxBodySize,
|
||||
WebhookMethod,
|
||||
WebhookRateLimitPer,
|
||||
WebhookSecurityAuthType,
|
||||
} from '../../constant';
|
||||
|
||||
@ -14,7 +14,7 @@ const initialFormValuesMap = {
|
||||
methods: [WebhookMethod.Get],
|
||||
schema: {},
|
||||
'security.auth_type': WebhookSecurityAuthType.Basic,
|
||||
'security.rate_limit.per': RateLimitPerList[0],
|
||||
'security.rate_limit.per': WebhookRateLimitPer.Second,
|
||||
'security.rate_limit.limit': 10,
|
||||
'security.max_body_size': WebhookMaxBodySize[0],
|
||||
'response.status': 200,
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { WebhookAlgorithmList } from '@/constants/agent';
|
||||
import { WebhookJWTAlgorithmList } from '@/constants/agent';
|
||||
import { WebhookSecurityAuthType } from '@/pages/agent/constant';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { useCallback } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DynamicStringForm } from '../../components/dynamic-string-form';
|
||||
|
||||
const AlgorithmOptions = buildOptions(WebhookAlgorithmList);
|
||||
|
||||
const RequiredClaimsOptions = buildOptions(['exp', 'sub']);
|
||||
const AlgorithmOptions = buildOptions(WebhookJWTAlgorithmList);
|
||||
|
||||
export function Auth() {
|
||||
const { t } = useTranslation();
|
||||
@ -88,38 +87,10 @@ export function Auth() {
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
<DynamicStringForm
|
||||
name="security.jwt.required_claims"
|
||||
label={t('flow.webhook.requiredClaims')}
|
||||
>
|
||||
<SelectWithSearch options={RequiredClaimsOptions}></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
</>
|
||||
),
|
||||
[t],
|
||||
);
|
||||
|
||||
const renderHmacAuth = useCallback(
|
||||
() => (
|
||||
<>
|
||||
<RAGFlowFormItem
|
||||
name="security.hmac.header"
|
||||
label={t('flow.webhook.header')}
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.hmac.secret"
|
||||
label={t('flow.webhook.secret')}
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.hmac.algorithm"
|
||||
label={t('flow.webhook.algorithm')}
|
||||
>
|
||||
<SelectWithSearch options={AlgorithmOptions}></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
></DynamicStringForm>
|
||||
</>
|
||||
),
|
||||
[t],
|
||||
@ -129,11 +100,14 @@ export function Auth() {
|
||||
[WebhookSecurityAuthType.Token]: renderTokenAuth,
|
||||
[WebhookSecurityAuthType.Basic]: renderBasicAuth,
|
||||
[WebhookSecurityAuthType.Jwt]: renderJwtAuth,
|
||||
[WebhookSecurityAuthType.Hmac]: renderHmacAuth,
|
||||
[WebhookSecurityAuthType.None]: () => null,
|
||||
};
|
||||
|
||||
return AuthMap[
|
||||
(authType ?? WebhookSecurityAuthType.None) as WebhookSecurityAuthType
|
||||
]();
|
||||
return (
|
||||
<div key={`auth-${authType}`} className="space-y-5">
|
||||
{AuthMap[
|
||||
(authType ?? WebhookSecurityAuthType.None) as WebhookSecurityAuthType
|
||||
]()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,15 +6,10 @@ import { Separator } from '@/components/ui/separator';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { loader } from '@monaco-editor/react';
|
||||
import { omit } from 'lodash';
|
||||
import { X } from 'lucide-react';
|
||||
import { ReactNode } from 'react';
|
||||
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
||||
import {
|
||||
TypesWithArray,
|
||||
WebhookContentType,
|
||||
WebhookRequestParameters,
|
||||
} from '../../../constant';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { TypesWithArray, WebhookRequestParameters } from '../../../constant';
|
||||
import { DynamicFormHeader } from '../../components/dynamic-fom-header';
|
||||
|
||||
loader.config({ paths: { vs: '/vs' } });
|
||||
@ -28,16 +23,9 @@ type SelectKeysProps = {
|
||||
requiredField?: string;
|
||||
nodeId?: string;
|
||||
isObject?: boolean;
|
||||
operatorList: WebhookRequestParameters[];
|
||||
};
|
||||
|
||||
function buildParametersOptions(isObject: boolean) {
|
||||
const list = isObject
|
||||
? WebhookRequestParameters
|
||||
: omit(WebhookRequestParameters, ['File']);
|
||||
|
||||
return buildOptions(list);
|
||||
}
|
||||
|
||||
export function DynamicRequest({
|
||||
name,
|
||||
label,
|
||||
@ -45,15 +33,9 @@ export function DynamicRequest({
|
||||
keyField = 'key',
|
||||
operatorField = 'type',
|
||||
requiredField = 'required',
|
||||
isObject = false,
|
||||
operatorList,
|
||||
}: SelectKeysProps) {
|
||||
const form = useFormContext();
|
||||
const contentType = useWatch({
|
||||
name: 'content_types',
|
||||
control: form.control,
|
||||
});
|
||||
const isFormDataContentType =
|
||||
contentType === WebhookContentType.MultipartFormData;
|
||||
|
||||
const { fields, remove, append } = useFieldArray({
|
||||
name: name,
|
||||
@ -94,9 +76,7 @@ export function DynamicRequest({
|
||||
onChange={(val) => {
|
||||
field.onChange(val);
|
||||
}}
|
||||
options={buildParametersOptions(
|
||||
isObject && isFormDataContentType,
|
||||
)}
|
||||
options={buildOptions(operatorList)}
|
||||
></SelectWithSearch>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
import { Collapse } from '@/components/collapse';
|
||||
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||
import { CopyToClipboardWithText } from '@/components/copy-to-clipboard';
|
||||
import NumberInput from '@/components/originui/number-input';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { MultiSelect } from '@/components/ui/multi-select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { useCallback } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'umi';
|
||||
import {
|
||||
RateLimitPerList,
|
||||
WebhookMaxBodySize,
|
||||
WebhookMethod,
|
||||
WebhookRateLimitPer,
|
||||
WebhookSecurityAuthType,
|
||||
} from '../../../constant';
|
||||
import { DynamicStringForm } from '../../components/dynamic-string-form';
|
||||
@ -21,18 +24,32 @@ import { WebhookResponse } from './response';
|
||||
|
||||
const RateLimitPerOptions = buildOptions(RateLimitPerList);
|
||||
|
||||
const RequestLimitMap = {
|
||||
[WebhookRateLimitPer.Second]: 100,
|
||||
[WebhookRateLimitPer.Minute]: 1000,
|
||||
[WebhookRateLimitPer.Hour]: 10000,
|
||||
[WebhookRateLimitPer.Day]: 100000,
|
||||
};
|
||||
|
||||
export function WebHook() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
const form = useFormContext();
|
||||
|
||||
const rateLimitPer = useWatch({
|
||||
name: 'security.rate_limit.per',
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const getLimitRateLimitPerMax = useCallback((rateLimitPer: string) => {
|
||||
return RequestLimitMap[rateLimitPer as keyof typeof RequestLimitMap] ?? 100;
|
||||
}, []);
|
||||
|
||||
const text = `${location.protocol}//${location.host}/api/v1/webhook/${id}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-bg-card p-1 rounded-md flex gap-2">
|
||||
<span className="flex-1 truncate">{text}</span>
|
||||
<CopyToClipboard text={text}></CopyToClipboard>
|
||||
</div>
|
||||
<CopyToClipboardWithText text={text}></CopyToClipboardWithText>
|
||||
<RAGFlowFormItem name="methods" label={t('flow.webhook.methods')}>
|
||||
{(field) => (
|
||||
<MultiSelect
|
||||
@ -61,13 +78,28 @@ export function WebHook() {
|
||||
name="security.rate_limit.limit"
|
||||
label={t('flow.webhook.limit')}
|
||||
>
|
||||
<Input type="number"></Input>
|
||||
<NumberInput
|
||||
max={getLimitRateLimitPerMax(rateLimitPer)}
|
||||
className="w-full"
|
||||
></NumberInput>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.rate_limit.per"
|
||||
label={t('flow.webhook.per')}
|
||||
>
|
||||
<SelectWithSearch options={RateLimitPerOptions}></SelectWithSearch>
|
||||
{(field) => (
|
||||
<SelectWithSearch
|
||||
options={RateLimitPerOptions}
|
||||
value={field.value}
|
||||
onChange={(val) => {
|
||||
field.onChange(val);
|
||||
form.setValue(
|
||||
'security.rate_limit.limit',
|
||||
getLimitRateLimitPerMax(val),
|
||||
);
|
||||
}}
|
||||
></SelectWithSearch>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.max_body_size"
|
||||
|
||||
@ -1,13 +1,40 @@
|
||||
import { Collapse } from '@/components/collapse';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { WebhookContentType } from '@/pages/agent/constant';
|
||||
import {
|
||||
WebhookContentType,
|
||||
WebhookRequestParameters,
|
||||
} from '@/pages/agent/constant';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { useMemo } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DynamicRequest } from './dynamic-request';
|
||||
|
||||
export function WebhookRequestSchema() {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
const contentType = useWatch({
|
||||
name: 'content_types',
|
||||
control: form.control,
|
||||
});
|
||||
const isFormDataContentType =
|
||||
contentType === WebhookContentType.MultipartFormData;
|
||||
|
||||
const bodyOperatorList = useMemo(() => {
|
||||
return isFormDataContentType
|
||||
? [
|
||||
WebhookRequestParameters.String,
|
||||
WebhookRequestParameters.Number,
|
||||
WebhookRequestParameters.Boolean,
|
||||
WebhookRequestParameters.File,
|
||||
]
|
||||
: [
|
||||
WebhookRequestParameters.String,
|
||||
WebhookRequestParameters.Number,
|
||||
WebhookRequestParameters.Boolean,
|
||||
];
|
||||
}, [isFormDataContentType]);
|
||||
|
||||
return (
|
||||
<Collapse title={<div>{t('flow.webhook.schema')}</div>}>
|
||||
@ -23,14 +50,20 @@ export function WebhookRequestSchema() {
|
||||
<DynamicRequest
|
||||
name="schema.query"
|
||||
label={t('flow.webhook.queryParameters')}
|
||||
operatorList={[
|
||||
WebhookRequestParameters.String,
|
||||
WebhookRequestParameters.Number,
|
||||
WebhookRequestParameters.Boolean,
|
||||
]}
|
||||
></DynamicRequest>
|
||||
<DynamicRequest
|
||||
name="schema.headers"
|
||||
label={t('flow.webhook.headerParameters')}
|
||||
operatorList={[WebhookRequestParameters.String]}
|
||||
></DynamicRequest>
|
||||
<DynamicRequest
|
||||
name="schema.body"
|
||||
isObject
|
||||
operatorList={bodyOperatorList}
|
||||
label={t('flow.webhook.requestBodyParameters')}
|
||||
></DynamicRequest>
|
||||
</section>
|
||||
|
||||
@ -49,7 +49,7 @@ export function WebhookResponse() {
|
||||
name="response.body_template"
|
||||
label={t('flow.webhook.bodyTemplate')}
|
||||
>
|
||||
<Textarea></Textarea>
|
||||
<Textarea className="overflow-auto"></Textarea>
|
||||
</RAGFlowFormItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
21
web/src/pages/agent/form/components/json-viewer.tsx
Normal file
21
web/src/pages/agent/form/components/json-viewer.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import JsonView from 'react18-json-view';
|
||||
|
||||
export function JsonViewer({
|
||||
data,
|
||||
title,
|
||||
}: {
|
||||
data: Record<string, any>;
|
||||
title: string;
|
||||
}) {
|
||||
return (
|
||||
<section className="space-y-2">
|
||||
<div>{title}</div>
|
||||
<JsonView
|
||||
src={data}
|
||||
displaySize
|
||||
collapseStringsAfterLength={100000000000}
|
||||
className="w-full h-[200px] break-words overflow-auto scrollbar-auto p-2 bg-muted"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user