import { useTranslation } from "react-i18next"; import { DownOutlined as DownOutlinedIcon } from "@ant-design/icons"; import { Alert, Button, Dropdown, Form, type FormInstance, Input, Select, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { type AccessConfigForWebhook } from "@/domain/access"; type AccessFormWebhookConfigFieldValues = Nullish<AccessConfigForWebhook>; export type AccessFormWebhookConfigProps = { form: FormInstance; formName: string; disabled?: boolean; initialValues?: AccessFormWebhookConfigFieldValues; usage?: "deployment" | "notification" | "none"; onValuesChange?: (values: AccessFormWebhookConfigFieldValues) => void; }; const initFormModel = (): AccessFormWebhookConfigFieldValues => { return { url: "", method: "POST", headers: "Content-Type: application/json", allowInsecureConnections: false, defaultDataForDeployment: JSON.stringify( { name: "${DOMAINS}", cert: "${CERTIFICATE}", privkey: "${PRIVATE_KEY}", }, null, 2 ), defaultDataForNotification: JSON.stringify( { subject: "${SUBJECT}", message: "${MESSAGE}", }, null, 2 ), }; }; const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialValues, usage, onValuesChange }: AccessFormWebhookConfigProps) => { const { t } = useTranslation(); const formSchema = z.object({ url: z.string().url(t("common.errmsg.url_invalid")), method: z.union([z.literal("GET"), z.literal("POST"), z.literal("PUT"), z.literal("PATCH"), z.literal("DELETE")], { message: t("access.form.webhook_method.placeholder"), }), headers: z .string() .nullish() .refine((v) => { if (!v) return true; const lines = v.split(/\r?\n/); for (const line of lines) { if (line.split(":").length < 2) { return false; } } return true; }, t("access.form.webhook_headers.errmsg.invalid")), allowInsecureConnections: z.boolean().nullish(), defaultDataForDeployment: z .string() .nullish() .refine((v) => { if (usage && usage !== "deployment") return true; if (!v) return true; try { const obj = JSON.parse(v); return typeof obj === "object" && !Array.isArray(obj); } catch { return false; } }, t("access.form.webhook_default_data.errmsg.json_invalid")), defaultDataForNotification: z .string() .nullish() .refine((v) => { if (usage && usage !== "notification") return true; if (!v) return true; try { const obj = JSON.parse(v); return typeof obj === "object" && !Array.isArray(obj); } catch { return false; } }, t("access.form.webhook_default_data.errmsg.json_invalid")), }); const formRule = createSchemaFieldRule(formSchema); const handleWebhookHeadersBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => { let value = e.target.value; value = value.trim(); value = value.replace(/(?<!\r)\n/g, "\r\n"); formInst.setFieldValue("headers", value); }; const handleWebhookDataForDeploymentBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => { const value = e.target.value; try { const json = JSON.stringify(JSON.parse(value), null, 2); formInst.setFieldValue("defaultDataForDeployment", json); } catch { return; } }; const handleWebhookDataForNotificationBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => { const value = e.target.value; try { const json = JSON.stringify(JSON.parse(value), null, 2); formInst.setFieldValue("defaultDataForNotification", json); } catch { return; } }; const handlePresetDataForDeploymentClick = () => { formInst.setFieldValue("defaultDataForDeployment", initFormModel().defaultDataForDeployment); }; const handlePresetDataForNotificationClick = (key: string) => { switch (key) { case "bark": formInst.setFieldValue("url", "https://api.day.app/push"); formInst.setFieldValue("method", "POST"); formInst.setFieldValue("headers", "Content-Type: application/json\r\nAuthorization: Bearer <your-gotify-token>"); formInst.setFieldValue( "defaultDataForNotification", JSON.stringify( { title: "${SUBJECT}", body: "${MESSAGE}", group: "<your-bark-group>", device_keys: "<your-bark-device-key>", }, null, 2 ) ); break; case "gotify": formInst.setFieldValue("url", "https://<your-gotify-server>/"); formInst.setFieldValue("method", "POST"); formInst.setFieldValue("headers", "Content-Type: application/json\r\nAuthorization: Bearer <your-gotify-token>"); formInst.setFieldValue( "defaultDataForNotification", JSON.stringify( { title: "${SUBJECT}", message: "${MESSAGE}", priority: 1, }, null, 2 ) ); break; case "ntfy": formInst.setFieldValue("url", "https://<your-ntfy-server>/"); formInst.setFieldValue("method", "POST"); formInst.setFieldValue("headers", "Content-Type: application/json"); formInst.setFieldValue( "defaultDataForNotification", JSON.stringify( { topic: "<your-ntfy-topic>", title: "${SUBJECT}", message: "${MESSAGE}", priority: 1, }, null, 2 ) ); break; case "pushover": formInst.setFieldValue("url", "https://api.pushover.net/1/messages.json"); formInst.setFieldValue("method", "POST"); formInst.setFieldValue("headers", "Content-Type: application/json"); formInst.setFieldValue( "defaultDataForNotification", JSON.stringify( { token: "<your-pushover-token>", user: "<your-pushover-user>", title: "${SUBJECT}", message: "${MESSAGE}", }, null, 2 ) ); break; case "pushplus": formInst.setFieldValue("url", "https://www.pushplus.plus/send"); formInst.setFieldValue("method", "POST"); formInst.setFieldValue("headers", "Content-Type: application/json"); formInst.setFieldValue( "defaultDataForNotification", JSON.stringify( { token: "<your-pushplus-token>", title: "${SUBJECT}", content: "${MESSAGE}", }, null, 2 ) ); break; case "serverchan": formInst.setFieldValue("url", "https://sctapi.ftqq.com/<your-serverchan-key>.send"); formInst.setFieldValue("method", "POST"); formInst.setFieldValue("headers", "Content-Type: application/json"); formInst.setFieldValue( "defaultDataForNotification", JSON.stringify( { text: "${SUBJECT}", desp: "${MESSAGE}", }, null, 2 ) ); break; default: formInst.setFieldValue("method", "POST"); formInst.setFieldValue("headers", "Content-Type: application/json"); formInst.setFieldValue("defaultDataForNotification", initFormModel().defaultDataForNotification); break; } }; const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => { onValuesChange?.(values); }; return ( <Form form={formInst} disabled={disabled} initialValues={initialValues ?? initFormModel()} layout="vertical" name={formName} onValuesChange={handleFormChange} > <Form.Item name="url" label={t("access.form.webhook_url.label")} rules={[formRule]}> <Input placeholder={t("access.form.webhook_url.placeholder")} /> </Form.Item> <Form.Item name="method" label={t("access.form.webhook_method.label")} rules={[formRule]}> <Select options={["GET", "POST", "PUT", "PATCH", "DELETE"].map((s) => ({ label: s, value: s }))} placeholder={t("access.form.webhook_method.placeholder")} /> </Form.Item> <Form.Item name="headers" label={t("access.form.webhook_headers.label")} rules={[formRule]} tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.webhook_headers.tooltip") }}></span>} > <Input.TextArea autoSize={{ minRows: 3, maxRows: 5 }} placeholder={t("access.form.webhook_headers.placeholder")} onBlur={handleWebhookHeadersBlur} /> </Form.Item> <Show when={!usage || usage === "deployment"}> <Form.Item className="mb-0"> <label className="mb-1 block"> <div className="flex w-full items-center justify-between gap-4"> <div className="max-w-full grow truncate"> <span>{t("access.form.webhook_default_data_for_deployment.label")}</span> </div> <div className="text-right"> <Button size="small" type="link" onClick={handlePresetDataForDeploymentClick}> {t("access.form.webhook_preset_data.button")} </Button> </div> </div> </label> <Form.Item name="defaultDataForDeployment" rules={[formRule]}> <Input.TextArea allowClear autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("access.form.webhook_default_data_for_deployment.placeholder")} onBlur={handleWebhookDataForDeploymentBlur} /> </Form.Item> </Form.Item> <Form.Item> <Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("access.form.webhook_default_data_for_deployment.guide") }}></span>} /> </Form.Item> </Show> <Show when={!usage || usage === "notification"}> <Form.Item className="mb-0"> <label className="mb-1 block"> <div className="flex w-full items-center justify-between gap-4"> <div className="max-w-full grow truncate"> <span>{t("access.form.webhook_default_data_for_notification.label")}</span> </div> <div className="text-right"> <Dropdown menu={{ items: ["bark", "ntfy", "gotify", "pushover", "pushplus", "serverchan", "common"].map((key) => ({ key, label: t(`access.form.webhook_preset_data.option.${key}.label`), onClick: () => handlePresetDataForNotificationClick(key), })), }} trigger={["click"]} > <Button size="small" type="link"> {t("access.form.webhook_preset_data.button")} <DownOutlinedIcon /> </Button> </Dropdown> </div> </div> </label> <Form.Item name="defaultDataForNotification" rules={[formRule]}> <Input.TextArea allowClear autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("access.form.webhook_default_data_for_notification.placeholder")} onBlur={handleWebhookDataForNotificationBlur} /> </Form.Item> </Form.Item> <Form.Item> <Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("access.form.webhook_default_data_for_notification.guide") }}></span>} /> </Form.Item> </Show> <Form.Item name="allowInsecureConnections" label={t("access.form.webhook_allow_insecure_conns.label")} rules={[formRule]} tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.webhook_allow_insecure_conns.tooltip") }}></span>} > <Switch checkedChildren={t("access.form.webhook_allow_insecure_conns.switch.on")} unCheckedChildren={t("access.form.webhook_allow_insecure_conns.switch.off")} /> </Form.Item> </Form> ); }; export default AccessFormWebhookConfig;