Feat: Remove HMAC from the webhook #10427

This commit is contained in:
bill 2025-12-15 16:38:03 +08:00
parent 2a0f835ffe
commit 6eedd0a237
12 changed files with 140 additions and 82 deletions

View file

@ -7,6 +7,7 @@ interface NumberInputProps {
onChange?: (value: number) => void;
height?: number | string;
min?: number;
max?: number;
}
const NumberInput: React.FC<NumberInputProps> = ({
@ -15,6 +16,7 @@ const NumberInput: React.FC<NumberInputProps> = ({
onChange,
height,
min = 0,
max = Infinity,
}) => {
const [value, setValue] = useState<number>(() => {
return initialValue ?? 0;
@ -34,6 +36,9 @@ const NumberInput: React.FC<NumberInputProps> = ({
};
const handleIncrement = () => {
if (value > max - 1) {
return;
}
setValue(value + 1);
onChange?.(value + 1);
};
@ -41,6 +46,9 @@ const NumberInput: React.FC<NumberInputProps> = ({
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = Number(e.target.value);
if (!isNaN(newValue)) {
if (newValue > max) {
return;
}
setValue(newValue);
onChange?.(newValue);
}

View file

@ -7,7 +7,11 @@ import {
} from '@/components/ui/form';
import { cn } from '@/lib/utils';
import { ReactNode, cloneElement, isValidElement } from 'react';
import { ControllerRenderProps, useFormContext } from 'react-hook-form';
import {
ControllerRenderProps,
UseControllerProps,
useFormContext,
} from 'react-hook-form';
type RAGFlowFormItemProps = {
name: string;
@ -18,7 +22,7 @@ type RAGFlowFormItemProps = {
required?: boolean;
labelClassName?: string;
className?: string;
};
} & Pick<UseControllerProps<any>, 'rules'>;
export function RAGFlowFormItem({
name,
@ -29,11 +33,13 @@ export function RAGFlowFormItem({
required = false,
labelClassName,
className,
rules,
}: RAGFlowFormItemProps) {
const form = useFormContext();
return (
<FormField
control={form.control}
rules={rules}
name={name}
render={({ field }) => (
<FormItem

View file

@ -195,7 +195,7 @@ export enum SwitchLogicOperator {
Or = 'or',
}
export const WebhookAlgorithmList = [
export const WebhookJWTAlgorithmList = [
'hs256',
'hs384',
'hs512',

View file

@ -1024,12 +1024,18 @@ export enum WebhookSecurityAuthType {
Token = 'token',
Basic = 'basic',
Jwt = 'jwt',
Hmac = 'hmac',
}
export const RateLimitPerList = ['minute', 'hour', 'day'];
export enum WebhookRateLimitPer {
Second = 'second',
Minute = 'minute',
Hour = 'hour',
Day = 'day',
}
export const WebhookMaxBodySize = ['10MB', '50MB', '100MB', '1000MB'];
export const RateLimitPerList = Object.values(WebhookRateLimitPer);
export const WebhookMaxBodySize = ['1MB', '5MB', '10MB'];
export enum WebhookRequestParameters {
File = VariableType.File,

View file

@ -43,6 +43,7 @@ function BeginForm({ node }: INextOperatorForm) {
const form = useForm({
defaultValues: values,
resolver: zodResolver(BeginFormSchema),
mode: 'onChange',
});
useWatchFormChange(node?.id, form);

View file

@ -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(),
})

View file

@ -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,

View file

@ -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>
);
}

View file

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

View file

@ -1,17 +1,20 @@
import { Collapse } from '@/components/collapse';
import CopyToClipboard 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,9 +24,26 @@ 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}`;
@ -61,13 +81,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"

View file

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

View file

@ -34,6 +34,7 @@ import {
NodeHandleId,
Operator,
TypesWithArray,
WebhookSecurityAuthType,
} from './constant';
import { BeginFormSchemaType } from './form/begin-form/schema';
import { DataOperationsFormSchemaType } from './form/data-operations-form';
@ -348,13 +349,20 @@ function transformRequestSchemaToJsonschema(
function transformBeginParams(params: BeginFormSchemaType) {
if (params.mode === AgentDialogueMode.Webhook) {
const nextSecurity: Record<string, any> = {
...params.security,
ip_whitelist: params.security?.ip_whitelist.map((x) => x.value),
};
if (params.security?.auth_type === WebhookSecurityAuthType.Jwt) {
nextSecurity.jwt = {
...nextSecurity.jwt,
required_claims: nextSecurity.jwt?.required_claims.map((x) => x.value),
};
}
return {
...params,
schema: transformRequestSchemaToJsonschema(params.schema),
security: {
...params.security,
ip_whitelist: params.security?.ip_whitelist.map((x) => x.value),
},
security: nextSecurity,
};
}