certimate/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx

497 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useTranslation } from "react-i18next";
import { DownOutlined as DownOutlinedIcon } from "@ant-design/icons";
import { Alert, Button, Dropdown, Form, type FormInstance, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import CodeInput from "@/components/CodeInput";
import Show from "@/components/Show";
import { CERTIFICATE_FORMATS } from "@/domain/certificate";
type DeployNodeConfigFormLocalConfigFieldValues = Nullish<{
format: string;
certPath: string;
certPathForServerOnly?: string;
certPathForIntermediaOnly?: string;
keyPath?: string;
pfxPassword?: string;
jksAlias?: string;
jksKeypass?: string;
jksStorepass?: string;
shellEnv?: string;
preCommand?: string;
postCommand?: string;
}>;
export type DeployNodeConfigFormLocalConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: DeployNodeConfigFormLocalConfigFieldValues;
onValuesChange?: (values: DeployNodeConfigFormLocalConfigFieldValues) => void;
};
const FORMAT_PEM = CERTIFICATE_FORMATS.PEM;
const FORMAT_PFX = CERTIFICATE_FORMATS.PFX;
const FORMAT_JKS = CERTIFICATE_FORMATS.JKS;
const SHELLENV_SH = "sh" as const;
const SHELLENV_CMD = "cmd" as const;
const SHELLENV_POWERSHELL = "powershell" as const;
const initFormModel = (): DeployNodeConfigFormLocalConfigFieldValues => {
return {
format: FORMAT_PEM,
certPath: "/etc/ssl/certimate/cert.crt",
keyPath: "/etc/ssl/certimate/cert.key",
shellEnv: SHELLENV_SH,
};
};
export const initPresetScript = (
key: "sh_backup_files" | "ps_backup_files" | "sh_reload_nginx" | "ps_binding_iis" | "ps_binding_netsh" | "ps_binding_rdp",
params?: {
certPath?: string;
certPathForServerOnly?: string;
certPathForIntermediaOnly?: string;
keyPath?: string;
pfxPassword?: string;
jksAlias?: string;
jksKeypass?: string;
jksStorepass?: string;
}
) => {
switch (key) {
case "sh_backup_files":
return `# 请将以下路径替换为实际值
cp "${params?.certPath || "<your-cert-path>"}" "${params?.certPath || "<your-cert-path>"}.bak" 2>/dev/null || :
cp "${params?.keyPath || "<your-key-path>"}" "${params?.keyPath || "<your-key-path>"}.bak" 2>/dev/null || :
`.trim();
case "ps_backup_files":
return `# 请将以下路径替换为实际值
if (Test-Path -Path "${params?.certPath || "<your-cert-path>"}" -PathType Leaf) {
Copy-Item -Path "${params?.certPath || "<your-cert-path>"}" -Destination "${params?.certPath || "<your-cert-path>"}.bak" -Force
}
if (Test-Path -Path "${params?.keyPath || "<your-key-path>"}" -PathType Leaf) {
Copy-Item -Path "${params?.keyPath || "<your-key-path>"}" -Destination "${params?.keyPath || "<your-key-path>"}.bak" -Force
}
`.trim();
case "sh_reload_nginx":
return `# *** 需要 root 权限 ***
sudo service nginx reload
`.trim();
case "ps_binding_iis":
return `# *** 需要管理员权限 ***
# 请将以下变量替换为实际值
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径(与表单中保持一致)
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码(与表单中保持一致)
$siteName = "<your-site-name>" # IIS 网站名称
$domain = "<your-domain-name>" # 域名
$ipaddr = "<your-binding-ip>" # 绑定 IP“*”表示所有 IP 绑定
$port = "<your-binding-port>" # 绑定端口
# 导入证书到本地计算机的个人存储区
$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable
# 获取 Thumbprint
$thumbprint = $cert.Thumbprint
# 导入 WebAdministration 模块
Import-Module WebAdministration
# 检查是否已存在 HTTPS 绑定
$existingBinding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -HostHeader "$domain" -ErrorAction SilentlyContinue
if (!$existingBinding) {
# 添加新的 HTTPS 绑定
New-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain"
}
# 获取绑定对象
$binding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain"
# 绑定 SSL 证书
$binding.AddSslCertificate($thumbprint, "My")
# 删除目录下的证书文件
Remove-Item -Path "$pfxPath" -Force
`.trim();
case "ps_binding_netsh":
return `# *** 需要管理员权限 ***
# 请将以下变量替换为实际值
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径(与表单中保持一致)
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码(与表单中保持一致)
$ipaddr = "<your-binding-ip>" # 绑定 IP“0.0.0.0”表示所有 IP 绑定,可填入域名
$port = "<your-binding-port>" # 绑定端口
# 导入证书到本地计算机的个人存储区
$addr = $ipaddr + ":" + $port
$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable
# 获取 Thumbprint
$thumbprint = $cert.Thumbprint
# 检测端口是否绑定证书,如绑定则删除绑定
$isExist = netsh http show sslcert ipport=$addr
if ($isExist -like "*$addr*"){ netsh http delete sslcert ipport=$addr }
# 绑定到端口
netsh http add sslcert ipport=$addr certhash=$thumbprint
# 删除目录下的证书文件
Remove-Item -Path "$pfxPath" -Force
`.trim();
case "ps_binding_rdp":
return `# *** 需要管理员权限 ***
# 请将以下变量替换为实际值
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径(与表单中保持一致)
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码(与表单中保持一致)
# 导入证书到本地计算机的个人存储区
$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable
# 获取 Thumbprint
$thumbprint = $cert.Thumbprint
# 绑定到 RDP
$rdpCertPath = "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp"
Set-ItemProperty -Path $rdpCertPath -Name "SSLCertificateSHA1Hash" -Value "$thumbprint"
`.trim();
}
};
const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormLocalConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
format: z.union([z.literal(FORMAT_PEM), z.literal(FORMAT_PFX), z.literal(FORMAT_JKS)], {
message: t("workflow_node.deploy.form.local_format.placeholder"),
}),
certPath: z
.string()
.min(1, t("workflow_node.deploy.form.local_cert_path.tooltip"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
certPathForServerOnly: z
.string()
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish(),
certPathForIntermediaOnly: z
.string()
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish(),
keyPath: z
.string()
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_PEM || !!v?.trim(), { message: t("workflow_node.deploy.form.local_key_path.tooltip") }),
pfxPassword: z
.string()
.max(64, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_PFX || !!v?.trim(), { message: t("workflow_node.deploy.form.local_pfx_password.tooltip") }),
jksAlias: z
.string()
.max(64, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), { message: t("workflow_node.deploy.form.local_jks_alias.tooltip") }),
jksKeypass: z
.string()
.max(64, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), { message: t("workflow_node.deploy.form.local_jks_keypass.tooltip") }),
jksStorepass: z
.string()
.max(64, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_JKS || !!v?.trim(), { message: t("workflow_node.deploy.form.local_jks_storepass.tooltip") }),
shellEnv: z.union([z.literal(SHELLENV_SH), z.literal(SHELLENV_CMD), z.literal(SHELLENV_POWERSHELL)], {
message: t("workflow_node.deploy.form.local_shell_env.placeholder"),
}),
preCommand: z
.string()
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
.nullish(),
postCommand: z
.string()
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
.nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
const fieldFormat = Form.useWatch("format", formInst);
const fieldCertPath = Form.useWatch("certPath", formInst);
const handleFormatSelect = (value: string) => {
if (fieldFormat === value) return;
switch (value) {
case FORMAT_PEM:
{
if (/(.pfx|.jks)$/.test(fieldCertPath)) {
formInst.setFieldValue("certPath", fieldCertPath.replace(/(.pfx|.jks)$/, ".crt"));
}
}
break;
case FORMAT_PFX:
{
if (/(.crt|.jks)$/.test(fieldCertPath)) {
formInst.setFieldValue("certPath", fieldCertPath.replace(/(.crt|.jks)$/, ".pfx"));
}
}
break;
case FORMAT_JKS:
{
if (/(.crt|.pfx)$/.test(fieldCertPath)) {
formInst.setFieldValue("certPath", fieldCertPath.replace(/(.crt|.pfx)$/, ".jks"));
}
}
break;
}
};
const handlePresetPreScriptClick = (key: string) => {
switch (key) {
case "sh_backup_files":
case "ps_backup_files":
{
const presetScriptParams = {
certPath: formInst.getFieldValue("certPath"),
keyPath: formInst.getFieldValue("keyPath"),
};
formInst.setFieldValue("shellEnv", SHELLENV_SH);
formInst.setFieldValue("preCommand", initPresetScript(key, presetScriptParams));
}
break;
}
};
const handlePresetPostScriptClick = (key: string) => {
switch (key) {
case "sh_reload_nginx":
{
formInst.setFieldValue("shellEnv", SHELLENV_SH);
formInst.setFieldValue("postCommand", initPresetScript(key));
}
break;
case "ps_binding_iis":
case "ps_binding_netsh":
case "ps_binding_rdp":
{
const presetScriptParams = {
certPath: formInst.getFieldValue("certPath"),
pfxPassword: formInst.getFieldValue("pfxPassword"),
};
formInst.setFieldValue("shellEnv", SHELLENV_POWERSHELL);
formInst.setFieldValue("postCommand", initPresetScript(key, presetScriptParams));
}
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>
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local.guide") }}></span>} />
</Form.Item>
<Form.Item name="format" label={t("workflow_node.deploy.form.local_format.label")} rules={[formRule]}>
<Select placeholder={t("workflow_node.deploy.form.local_format.placeholder")} onSelect={handleFormatSelect}>
<Select.Option key={FORMAT_PEM} value={FORMAT_PEM}>
{t("workflow_node.deploy.form.local_format.option.pem.label")}
</Select.Option>
<Select.Option key={FORMAT_PFX} value={FORMAT_PFX}>
{t("workflow_node.deploy.form.local_format.option.pfx.label")}
</Select.Option>
<Select.Option key={FORMAT_JKS} value={FORMAT_JKS}>
{t("workflow_node.deploy.form.local_format.option.jks.label")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="certPath"
label={t("workflow_node.deploy.form.local_cert_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_cert_path.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.local_cert_path.placeholder")} />
</Form.Item>
<Show when={fieldFormat === FORMAT_PEM}>
<Form.Item
name="keyPath"
label={t("workflow_node.deploy.form.local_key_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_key_path.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.local_key_path.placeholder")} />
</Form.Item>
<Form.Item
name="certPathForServerOnly"
label={t("workflow_node.deploy.form.local_servercert_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_servercert_path.tooltip") }}></span>}
>
<Input allowClear placeholder={t("workflow_node.deploy.form.local_servercert_path.placeholder")} />
</Form.Item>
<Form.Item
name="certPathForIntermediaOnly"
label={t("workflow_node.deploy.form.local_intermediacert_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_intermediacert_path.tooltip") }}></span>}
>
<Input allowClear placeholder={t("workflow_node.deploy.form.local_intermediacert_path.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldFormat === FORMAT_PFX}>
<Form.Item
name="pfxPassword"
label={t("workflow_node.deploy.form.local_pfx_password.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_pfx_password.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.local_pfx_password.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldFormat === FORMAT_JKS}>
<Form.Item
name="jksAlias"
label={t("workflow_node.deploy.form.local_jks_alias.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_jks_alias.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.local_jks_alias.placeholder")} />
</Form.Item>
<Form.Item
name="jksKeypass"
label={t("workflow_node.deploy.form.local_jks_keypass.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_jks_keypass.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.local_jks_keypass.placeholder")} />
</Form.Item>
<Form.Item
name="jksStorepass"
label={t("workflow_node.deploy.form.local_jks_storepass.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_jks_storepass.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.local_jks_storepass.placeholder")} />
</Form.Item>
</Show>
<Form.Item name="shellEnv" label={t("workflow_node.deploy.form.local_shell_env.label")} rules={[formRule]}>
<Select placeholder={t("workflow_node.deploy.form.local_shell_env.placeholder")}>
<Select.Option key={SHELLENV_SH} value={SHELLENV_SH}>
{t("workflow_node.deploy.form.local_shell_env.option.sh.label")}
</Select.Option>
<Select.Option key={SHELLENV_CMD} value={SHELLENV_CMD}>
{t("workflow_node.deploy.form.local_shell_env.option.cmd.label")}
</Select.Option>
<Select.Option key={SHELLENV_POWERSHELL} value={SHELLENV_POWERSHELL}>
{t("workflow_node.deploy.form.local_shell_env.option.powershell.label")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item className="mb-0" htmlFor="null">
<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("workflow_node.deploy.form.local_pre_command.label")}</span>
</div>
<div className="text-right">
<Dropdown
menu={{
items: ["sh_backup_files", "ps_backup_files"].map((key) => ({
key,
label: t(`workflow_node.deploy.form.local_preset_scripts.option.${key}.label`),
onClick: () => handlePresetPreScriptClick(key),
})),
}}
trigger={["click"]}
>
<Button size="small" type="link">
{t("workflow_node.deploy.form.local_preset_scripts.button")}
<DownOutlinedIcon />
</Button>
</Dropdown>
</div>
</div>
</label>
<Form.Item name="preCommand" rules={[formRule]}>
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language={["shell", "powershell"]}
placeholder={t("workflow_node.deploy.form.local_pre_command.placeholder")}
/>
</Form.Item>
</Form.Item>
<Form.Item className="mb-0" htmlFor="null">
<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("workflow_node.deploy.form.local_post_command.label")}</span>
</div>
<div className="text-right">
<Dropdown
menu={{
items: ["sh_reload_nginx", "ps_binding_iis", "ps_binding_netsh", "ps_binding_rdp"].map((key) => ({
key,
label: t(`workflow_node.deploy.form.local_preset_scripts.option.${key}.label`),
onClick: () => handlePresetPostScriptClick(key),
})),
}}
trigger={["click"]}
>
<Button size="small" type="link">
{t("workflow_node.deploy.form.local_preset_scripts.button")}
<DownOutlinedIcon />
</Button>
</Dropdown>
</div>
</div>
</label>
<Form.Item name="postCommand" rules={[formRule]}>
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language={["shell", "powershell"]}
placeholder={t("workflow_node.deploy.form.local_post_command.placeholder")}
/>
</Form.Item>
</Form.Item>
</Form>
);
};
export default DeployNodeConfigFormLocalConfig;