Merge pull request #573 from fudiwei/main

Support configuring independent CA for each workflow
This commit is contained in:
Yoan.liu
2025-04-03 17:42:42 +08:00
committed by GitHub
63 changed files with 1996 additions and 682 deletions

View File

@@ -0,0 +1,118 @@
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useControllableValue } from "ahooks";
import { Button, Drawer, Space, notification } from "antd";
import { type AccessModel } from "@/domain/access";
import { useTriggerElement, useZustandShallowSelector } from "@/hooks";
import { useAccessesStore } from "@/stores/access";
import { getErrMsg } from "@/utils/error";
import AccessForm, { type AccessFormInstance, type AccessFormProps } from "./AccessForm";
export type AccessEditDrawerProps = {
data?: AccessFormProps["initialValues"];
loading?: boolean;
open?: boolean;
range?: AccessFormProps["range"];
scene: AccessFormProps["scene"];
trigger?: React.ReactNode;
onOpenChange?: (open: boolean) => void;
afterSubmit?: (record: AccessModel) => void;
};
const AccessEditDrawer = ({ data, loading, trigger, scene, range, afterSubmit, ...props }: AccessEditDrawerProps) => {
const { t } = useTranslation();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
const { createAccess, updateAccess } = useAccessesStore(useZustandShallowSelector(["createAccess", "updateAccess"]));
const [open, setOpen] = useControllableValue<boolean>(props, {
valuePropName: "open",
defaultValuePropName: "defaultOpen",
trigger: "onOpenChange",
});
const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });
const formRef = useRef<AccessFormInstance>(null);
const [formPending, setFormPending] = useState(false);
const handleOkClick = async () => {
setFormPending(true);
try {
await formRef.current!.validateFields();
} catch (err) {
setFormPending(false);
throw err;
}
try {
let values: AccessModel = formRef.current!.getFieldsValue();
if (scene === "add") {
if (data?.id) {
throw "Invalid props: `data`";
}
values = await createAccess(values);
} else if (scene === "edit") {
if (!data?.id) {
throw "Invalid props: `data`";
}
values = await updateAccess({ ...data, ...values });
} else {
throw "Invalid props: `scene`";
}
afterSubmit?.(values);
setOpen(false);
} catch (err) {
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
throw err;
} finally {
setFormPending(false);
}
};
const handleCancelClick = () => {
if (formPending) return;
setOpen(false);
};
return (
<>
{NotificationContextHolder}
{triggerEl}
<Drawer
afterOpenChange={setOpen}
closable={!formPending}
destroyOnClose
footer={
<Space className="w-full justify-end">
<Button onClick={handleCancelClick}>{t("common.button.cancel")}</Button>
<Button loading={formPending} type="primary" onClick={handleOkClick}>
{scene === "edit" ? t("common.button.save") : t("common.button.submit")}
</Button>
</Space>
}
loading={loading}
maskClosable={!formPending}
open={open}
title={t(`access.action.${scene}`)}
width={720}
onClose={() => setOpen(false)}
>
<AccessForm ref={formRef} initialValues={data} range={range} scene={scene === "add" ? "add" : "edit"} />
</Drawer>
</>
);
};
export default AccessEditDrawer;

View File

@@ -14,13 +14,14 @@ export type AccessEditModalProps = {
data?: AccessFormProps["initialValues"];
loading?: boolean;
open?: boolean;
preset: AccessFormProps["preset"];
range?: AccessFormProps["range"];
scene: AccessFormProps["scene"];
trigger?: React.ReactNode;
onOpenChange?: (open: boolean) => void;
afterSubmit?: (record: AccessModel) => void;
};
const AccessEditModal = ({ data, loading, trigger, preset, afterSubmit, ...props }: AccessEditModalProps) => {
const AccessEditModal = ({ data, loading, trigger, scene, range, afterSubmit, ...props }: AccessEditModalProps) => {
const { t } = useTranslation();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
@@ -50,13 +51,13 @@ const AccessEditModal = ({ data, loading, trigger, preset, afterSubmit, ...props
try {
let values: AccessModel = formRef.current!.getFieldsValue();
if (preset === "add") {
if (scene === "add") {
if (data?.id) {
throw "Invalid props: `data`";
}
values = await createAccess(values);
} else if (preset === "edit") {
} else if (scene === "edit") {
if (!data?.id) {
throw "Invalid props: `data`";
}
@@ -96,15 +97,15 @@ const AccessEditModal = ({ data, loading, trigger, preset, afterSubmit, ...props
confirmLoading={formPending}
destroyOnClose
loading={loading}
okText={preset === "edit" ? t("common.button.save") : t("common.button.submit")}
okText={scene === "edit" ? t("common.button.save") : t("common.button.submit")}
open={open}
title={t(`access.action.${preset}`)}
title={t(`access.action.${scene}`)}
width={480}
onOk={handleOkClick}
onCancel={handleCancelClick}
>
<div className="pb-2 pt-4">
<AccessForm ref={formRef} initialValues={data} preset={preset === "add" ? "add" : "edit"} />
<AccessForm ref={formRef} initialValues={data} range={range} scene={scene === "add" ? "add" : "edit"} />
</div>
</Modal>
</>

View File

@@ -6,7 +6,7 @@ import { z } from "zod";
import AccessProviderSelect from "@/components/provider/AccessProviderSelect";
import { type AccessModel } from "@/domain/access";
import { ACCESS_PROVIDERS } from "@/domain/provider";
import { ACCESS_PROVIDERS, ACCESS_USAGES } from "@/domain/provider";
import { useAntdForm, useAntdFormName } from "@/hooks";
import AccessForm1PanelConfig from "./AccessForm1PanelConfig";
@@ -31,10 +31,10 @@ import AccessFormEdgioConfig from "./AccessFormEdgioConfig";
import AccessFormGcoreConfig from "./AccessFormGcoreConfig";
import AccessFormGnameConfig from "./AccessFormGnameConfig";
import AccessFormGoDaddyConfig from "./AccessFormGoDaddyConfig";
import AccessFormGoogleTrustServicesConfig from "./AccessFormGoogleTrustServicesConfig";
import AccessFormHuaweiCloudConfig from "./AccessFormHuaweiCloudConfig";
import AccessFormJDCloudConfig from "./AccessFormJDCloudConfig";
import AccessFormKubernetesConfig from "./AccessFormKubernetesConfig";
import AccessFormLocalConfig from "./AccessFormLocalConfig";
import AccessFormNamecheapConfig from "./AccessFormNamecheapConfig";
import AccessFormNameDotComConfig from "./AccessFormNameDotComConfig";
import AccessFormNameSiloConfig from "./AccessFormNameSiloConfig";
@@ -45,6 +45,7 @@ import AccessFormQiniuConfig from "./AccessFormQiniuConfig";
import AccessFormRainYunConfig from "./AccessFormRainYunConfig";
import AccessFormSafeLineConfig from "./AccessFormSafeLineConfig";
import AccessFormSSHConfig from "./AccessFormSSHConfig";
import AccessFormSSLComConfig from "./AccessFormSSLComConfig";
import AccessFormTencentCloudConfig from "./AccessFormTencentCloudConfig";
import AccessFormUCloudConfig from "./AccessFormUCloudConfig";
import AccessFormUpyunConfig from "./AccessFormUpyunConfig";
@@ -52,16 +53,19 @@ import AccessFormVercelConfig from "./AccessFormVercelConfig";
import AccessFormVolcEngineConfig from "./AccessFormVolcEngineConfig";
import AccessFormWebhookConfig from "./AccessFormWebhookConfig";
import AccessFormWestcnConfig from "./AccessFormWestcnConfig";
import AccessFormZeroSSLConfig from "./AccessFormZeroSSLConfig";
type AccessFormFieldValues = Partial<MaybeModelRecord<AccessModel>>;
type AccessFormPresets = "add" | "edit";
type AccessFormRanges = "both-dns-hosting" | "ca-only" | "notify-only";
type AccessFormScenes = "add" | "edit";
export type AccessFormProps = {
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
initialValues?: AccessFormFieldValues;
preset: AccessFormPresets;
range?: AccessFormRanges;
scene: AccessFormScenes;
onValuesChange?: (values: AccessFormFieldValues) => void;
};
@@ -71,7 +75,7 @@ export type AccessFormInstance = {
validateFields: FormInstance<AccessFormFieldValues>["validateFields"];
};
const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className, style, disabled, initialValues, preset, onValuesChange }, ref) => {
const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className, style, disabled, initialValues, range, scene, onValuesChange }, ref) => {
const { t } = useTranslation();
const formSchema = z.object({
@@ -80,7 +84,14 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
.min(1, t("access.form.name.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim(),
provider: z.nativeEnum(ACCESS_PROVIDERS, { message: t("access.form.provider.placeholder") }),
provider: z.nativeEnum(ACCESS_PROVIDERS, {
message:
range === "ca-only"
? t("access.form.certificate_authority.placeholder")
: range === "notify-only"
? t("access.form.notification_channel.placeholder")
: t("access.form.provider.placeholder"),
}),
config: z.any(),
});
const formRule = createSchemaFieldRule(formSchema);
@@ -88,6 +99,35 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
initialValues: initialValues,
});
const providerLabel = useMemo(() => {
switch (range) {
case "ca-only":
return t("access.form.certificate_authority.label");
case "notify-only":
return t("access.form.notification_channel.label");
}
return t("access.form.provider.label");
}, [range]);
const providerPlaceholder = useMemo(() => {
switch (range) {
case "ca-only":
return t("access.form.certificate_authority.placeholder");
case "notify-only":
return t("access.form.notification_channel.placeholder");
}
return t("access.form.provider.placeholder");
}, [range]);
const providerTooltip = useMemo(() => {
switch (range) {
case "both-dns-hosting":
return <span dangerouslySetInnerHTML={{ __html: t("access.form.provider.tooltip") }}></span>;
}
return undefined;
}, [range]);
const fieldProvider = Form.useWatch("provider", formInst);
const [nestedFormInst] = Form.useForm();
@@ -147,6 +187,8 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormGnameConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.GODADDY:
return <AccessFormGoDaddyConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.GOOGLETRUSTSERVICES:
return <AccessFormGoogleTrustServicesConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.EDGIO:
return <AccessFormEdgioConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.HUAWEICLOUD:
@@ -155,8 +197,6 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormJDCloudConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.KUBERNETES:
return <AccessFormKubernetesConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.LOCAL:
return <AccessFormLocalConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.NAMECHEAP:
return <AccessFormNamecheapConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.NAMEDOTCOM:
@@ -177,6 +217,8 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormSafeLineConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.SSH:
return <AccessFormSSHConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.SSLCOM:
return <AccessFormSSLComConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.TENCENTCLOUD:
return <AccessFormTencentCloudConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.UCLOUD:
@@ -191,6 +233,8 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormWebhookConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.WESTCN:
return <AccessFormWestcnConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.ZEROSSL:
return <AccessFormZeroSSLConfig {...nestedFormProps} />;
}
}, [disabled, initialValues?.config, fieldProvider, nestedFormInst, nestedFormName]);
@@ -235,13 +279,25 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
<Input placeholder={t("access.form.name.placeholder")} />
</Form.Item>
<Form.Item
name="provider"
label={t("access.form.provider.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.provider.tooltip") }}></span>}
>
<AccessProviderSelect disabled={preset !== "add"} placeholder={t("access.form.provider.placeholder")} showSearch={!disabled} />
<Form.Item name="provider" label={providerLabel} rules={[formRule]} tooltip={providerTooltip}>
<AccessProviderSelect
filter={(record) => {
if (range == null) return true;
switch (range) {
case "both-dns-hosting":
return record.usages.includes(ACCESS_USAGES.DNS) || record.usages.includes(ACCESS_USAGES.HOSTING);
case "ca-only":
return record.usages.includes(ACCESS_USAGES.CA);
case "notify-only":
return record.usages.includes(ACCESS_USAGES.NOTIFICATION);
}
}}
disabled={scene !== "add"}
placeholder={providerPlaceholder}
showOptionTags={range == null || (range === "both-dns-hosting" ? { [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : false)}
showSearch={!disabled}
/>
</Form.Item>
</Form>

View File

@@ -0,0 +1,82 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForGoogleTrustServices } from "@/domain/access";
type AccessFormGoogleTrustServicesConfigFieldValues = Nullish<AccessConfigForGoogleTrustServices>;
export type AccessFormGoogleTrustServicesConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormGoogleTrustServicesConfigFieldValues;
onValuesChange?: (values: AccessFormGoogleTrustServicesConfigFieldValues) => void;
};
const initFormModel = (): AccessFormGoogleTrustServicesConfigFieldValues => {
return {
eabKid: "",
eabHmacKey: "",
};
};
const AccessFormGoogleTrustServicesConfig = ({
form: formInst,
formName,
disabled,
initialValues,
onValuesChange,
}: AccessFormGoogleTrustServicesConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
eabKid: z
.string()
.min(1, t("access.form.googletrustservices_eab_kid.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
eabHmacKey: z
.string()
.min(1, t("access.form.googletrustservices_eab_hmac_key.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
});
const formRule = createSchemaFieldRule(formSchema);
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="eabKid"
label={t("access.form.googletrustservices_eab_kid.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.googletrustservices_eab_kid.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.googletrustservices_eab_kid.placeholder")} />
</Form.Item>
<Form.Item
name="eabHmacKey"
label={t("access.form.googletrustservices_eab_hmac_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.googletrustservices_eab_hmac_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.googletrustservices_eab_hmac_key.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormGoogleTrustServicesConfig;

View File

@@ -1,36 +0,0 @@
import { Form, type FormInstance } from "antd";
import { type AccessConfigForLocal } from "@/domain/access";
type AccessFormLocalConfigFieldValues = Nullish<AccessConfigForLocal>;
export type AccessFormLocalConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormLocalConfigFieldValues;
onValuesChange?: (values: AccessFormLocalConfigFieldValues) => void;
};
const initFormModel = (): AccessFormLocalConfigFieldValues => {
return {};
};
const AccessFormLocalConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormLocalConfigProps) => {
const handleFormChange = (_: unknown, values: any) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
></Form>
);
};
export default AccessFormLocalConfig;

View File

@@ -7,7 +7,7 @@ import { z } from "zod";
import { type AccessConfigForSSH } from "@/domain/access";
import { readFileContent } from "@/utils/file";
import { validDomainName, validIPv4Address, validIPv6Address } from "@/utils/validators";
import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators";
type AccessFormSSHConfigFieldValues = Nullish<AccessConfigForSSH>;
@@ -34,11 +34,13 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
host: z
.string({ message: t("access.form.ssh_host.placeholder") })
.refine((v) => validDomainName(v) || validIPv4Address(v) || validIPv6Address(v), t("common.errmsg.host_invalid")),
port: z
.number({ message: t("access.form.ssh_port.placeholder") })
.int()
.gte(1, t("common.errmsg.port_invalid"))
.lte(65535, t("common.errmsg.port_invalid")),
port: z.preprocess(
(v) => Number(v),
z
.number({ message: t("access.form.ssh_port.placeholder") })
.int(t("access.form.ssh_port.placeholder"))
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
),
username: z
.string()
.min(1, "access.form.ssh_username.placeholder")

View File

@@ -0,0 +1,76 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForSSLCom } from "@/domain/access";
type AccessFormSSLComConfigFieldValues = Nullish<AccessConfigForSSLCom>;
export type AccessFormSSLComConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormSSLComConfigFieldValues;
onValuesChange?: (values: AccessFormSSLComConfigFieldValues) => void;
};
const initFormModel = (): AccessFormSSLComConfigFieldValues => {
return {
eabKid: "",
eabHmacKey: "",
};
};
const AccessFormSSLComConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormSSLComConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
eabKid: z
.string()
.min(1, t("access.form.sslcom_eab_kid.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
eabHmacKey: z
.string()
.min(1, t("access.form.sslcom_eab_hmac_key.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
});
const formRule = createSchemaFieldRule(formSchema);
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="eabKid"
label={t("access.form.sslcom_eab_kid.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.sslcom_eab_kid.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.sslcom_eab_kid.placeholder")} />
</Form.Item>
<Form.Item
name="eabHmacKey"
label={t("access.form.sslcom_eab_hmac_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.sslcom_eab_hmac_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.sslcom_eab_hmac_key.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormSSLComConfig;

View File

@@ -0,0 +1,76 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForZeroSSL } from "@/domain/access";
type AccessFormZeroSSLConfigFieldValues = Nullish<AccessConfigForZeroSSL>;
export type AccessFormZeroSSLConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormZeroSSLConfigFieldValues;
onValuesChange?: (values: AccessFormZeroSSLConfigFieldValues) => void;
};
const initFormModel = (): AccessFormZeroSSLConfigFieldValues => {
return {
eabKid: "",
eabHmacKey: "",
};
};
const AccessFormZeroSSLConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormZeroSSLConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
eabKid: z
.string()
.min(1, t("access.form.zerossl_eab_kid.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
eabHmacKey: z
.string()
.min(1, t("access.form.zerossl_eab_hmac_key.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
});
const formRule = createSchemaFieldRule(formSchema);
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="eabKid"
label={t("access.form.zerossl_eab_kid.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.zerossl_eab_kid.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.zerossl_eab_kid.placeholder")} />
</Form.Item>
<Form.Item
name="eabHmacKey"
label={t("access.form.zerossl_eab_hmac_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.zerossl_eab_hmac_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.zerossl_eab_hmac_key.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormZeroSSLConfig;