mirror of
https://github.com/usual2970/certimate.git
synced 2025-10-04 21:44:54 +00:00
feat: support ssh challenge-response
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowDownOutlined, ArrowUpOutlined, CloseOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { Button, Collapse, Form, type FormInstance, Input, InputNumber, Space } from "antd";
|
||||
import { Button, Collapse, Form, type FormInstance, Input, InputNumber, Select, Space } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import TextFileInput from "@/components/TextFileInput";
|
||||
import { type AccessConfigForSSH } from "@/domain/access";
|
||||
import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators";
|
||||
@@ -18,10 +19,15 @@ export type AccessFormSSHConfigProps = {
|
||||
onValuesChange?: (values: AccessFormSSHConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const AUTH_METHOD_NONE = "none" as const;
|
||||
const AUTH_METHOD_PASSWORD = "password" as const;
|
||||
const AUTH_METHOD_KEY = "key" as const;
|
||||
|
||||
const initFormModel = (): AccessFormSSHConfigFieldValues => {
|
||||
return {
|
||||
host: "127.0.0.1",
|
||||
port: 22,
|
||||
authMethod: AUTH_METHOD_PASSWORD,
|
||||
username: "root",
|
||||
};
|
||||
};
|
||||
@@ -38,6 +44,9 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
|
||||
.int(t("access.form.ssh_port.placeholder"))
|
||||
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
|
||||
),
|
||||
authMethod: z.union([z.literal(AUTH_METHOD_NONE), z.literal(AUTH_METHOD_PASSWORD), z.literal(AUTH_METHOD_KEY)], {
|
||||
message: t("access.form.ssh_auth_method.placeholder"),
|
||||
}),
|
||||
username: z
|
||||
.string()
|
||||
.min(1, t("access.form.ssh_username.placeholder"))
|
||||
@@ -45,11 +54,13 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
|
||||
password: z
|
||||
.string()
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 }))
|
||||
.nullish(),
|
||||
.nullish()
|
||||
.refine((v) => fieldAuthMethod !== AUTH_METHOD_PASSWORD || !!v?.trim(), t("access.form.ssh_password.placeholder")),
|
||||
key: z
|
||||
.string()
|
||||
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
|
||||
.nullish(),
|
||||
.nullish()
|
||||
.refine((v) => fieldAuthMethod !== AUTH_METHOD_KEY || !!v?.trim(), t("access.form.ssh_key.placeholder")),
|
||||
keyPassphrase: z
|
||||
.string()
|
||||
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
|
||||
@@ -57,47 +68,43 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
|
||||
.refine((v) => !v || formInst.getFieldValue("key"), t("access.form.ssh_key.placeholder")),
|
||||
jumpServers: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
host: z.string().refine((v) => validDomainName(v) || validIPv4Address(v) || validIPv6Address(v), t("common.errmsg.host_invalid")),
|
||||
port: z.preprocess(
|
||||
(v) => Number(v),
|
||||
z
|
||||
.number()
|
||||
.int(t("access.form.ssh_port.placeholder"))
|
||||
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
|
||||
),
|
||||
username: z
|
||||
.string()
|
||||
.min(1, t("access.form.ssh_username.placeholder"))
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 })),
|
||||
password: z
|
||||
.string()
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 }))
|
||||
.nullish(),
|
||||
key: z
|
||||
.string()
|
||||
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
|
||||
.nullish(),
|
||||
keyPassphrase: z
|
||||
.string()
|
||||
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
|
||||
.nullish(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.keyPassphrase && !data.key) {
|
||||
ctx.addIssue({
|
||||
path: ["keyPassphrase"],
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t("access.form.ssh_key.placeholder"),
|
||||
});
|
||||
}
|
||||
})
|
||||
z.object({
|
||||
host: z.string().refine((v) => validDomainName(v) || validIPv4Address(v) || validIPv6Address(v), t("common.errmsg.host_invalid")),
|
||||
port: z.preprocess(
|
||||
(v) => Number(v),
|
||||
z
|
||||
.number()
|
||||
.int(t("access.form.ssh_port.placeholder"))
|
||||
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
|
||||
),
|
||||
authMethod: z.union([z.literal(AUTH_METHOD_NONE), z.literal(AUTH_METHOD_PASSWORD), z.literal(AUTH_METHOD_KEY)], {
|
||||
message: t("access.form.ssh_auth_method.placeholder"),
|
||||
}),
|
||||
username: z
|
||||
.string()
|
||||
.min(1, t("access.form.ssh_username.placeholder"))
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 })),
|
||||
password: z
|
||||
.string()
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 }))
|
||||
.nullish(),
|
||||
key: z
|
||||
.string()
|
||||
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
|
||||
.nullish(),
|
||||
keyPassphrase: z
|
||||
.string()
|
||||
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
|
||||
.nullish(),
|
||||
}),
|
||||
{ message: t("access.form.ssh_jump_servers.errmsg.invalid") }
|
||||
)
|
||||
.nullish(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
const fieldAuthMethod = Form.useWatch("authMethod", formInst);
|
||||
|
||||
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
|
||||
onValuesChange?.(values);
|
||||
};
|
||||
@@ -125,36 +132,39 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.Item name="authMethod" label={t("access.form.ssh_auth_method.label")} rules={[formRule]}>
|
||||
<Select placeholder={t("access.form.ssh_auth_method.placeholder")}>
|
||||
<Select.Option key={AUTH_METHOD_NONE} value={AUTH_METHOD_NONE}>
|
||||
{t("access.form.ssh_auth_method.option.none.label")}
|
||||
</Select.Option>
|
||||
<Select.Option key={AUTH_METHOD_PASSWORD} value={AUTH_METHOD_PASSWORD}>
|
||||
{t("access.form.ssh_auth_method.option.password.label")}
|
||||
</Select.Option>
|
||||
<Select.Option key={AUTH_METHOD_KEY} value={AUTH_METHOD_KEY}>
|
||||
{t("access.form.ssh_auth_method.option.key.label")}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="username" label={t("access.form.ssh_username.label")} rules={[formRule]}>
|
||||
<Input autoComplete="new-password" placeholder={t("access.form.ssh_username.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={t("access.form.ssh_password.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_password.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_password.placeholder")} />
|
||||
</Form.Item>
|
||||
<Show when={fieldAuthMethod === AUTH_METHOD_PASSWORD}>
|
||||
<Form.Item name="password" label={t("access.form.ssh_password.label")} rules={[formRule]}>
|
||||
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_password.placeholder")} />
|
||||
</Form.Item>
|
||||
</Show>
|
||||
|
||||
<Form.Item
|
||||
name="key"
|
||||
label={t("access.form.ssh_key.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key.tooltip") }}></span>}
|
||||
>
|
||||
<TextFileInput allowClear autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("access.form.ssh_key.placeholder")} />
|
||||
</Form.Item>
|
||||
<Show when={fieldAuthMethod === AUTH_METHOD_KEY}>
|
||||
<Form.Item name="key" label={t("access.form.ssh_key.label")} rules={[formRule]}>
|
||||
<TextFileInput allowClear autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("access.form.ssh_key.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="keyPassphrase"
|
||||
label={t("access.form.ssh_key_passphrase.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key_passphrase.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_key_passphrase.placeholder")} />
|
||||
</Form.Item>
|
||||
<Form.Item name="keyPassphrase" label={t("access.form.ssh_key_passphrase.label")} rules={[formRule]}>
|
||||
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_key_passphrase.placeholder")} />
|
||||
</Form.Item>
|
||||
</Show>
|
||||
|
||||
<Form.Item name="jumpServers" label={t("access.form.ssh_jump_servers.label")} rules={[formRule]}>
|
||||
<Form.List name="jumpServers">
|
||||
@@ -174,6 +184,60 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
|
||||
);
|
||||
};
|
||||
|
||||
const Fields = () => {
|
||||
const authMethod = Form.useWatch(["jumpServers", field.name, "authMethod"], formInst);
|
||||
return (
|
||||
<>
|
||||
<div className="flex space-x-2">
|
||||
<div className="w-2/3">
|
||||
<Form.Item name={[field.name, "host"]} label={t("access.form.ssh_host.label")} rules={[formRule]}>
|
||||
<Input placeholder={t("access.form.ssh_host.placeholder")} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="w-1/3">
|
||||
<Form.Item name={[field.name, "port"]} label={t("access.form.ssh_port.label")} rules={[formRule]}>
|
||||
<InputNumber className="w-full" placeholder={t("access.form.ssh_port.placeholder")} min={1} max={65535} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.Item name={[field.name, "authMethod"]} label={t("access.form.ssh_auth_method.label")} rules={[formRule]}>
|
||||
<Select placeholder={t("access.form.ssh_auth_method.placeholder")}>
|
||||
<Select.Option key={AUTH_METHOD_NONE} value={AUTH_METHOD_NONE}>
|
||||
{t("access.form.ssh_auth_method.option.none.label")}
|
||||
</Select.Option>
|
||||
<Select.Option key={AUTH_METHOD_PASSWORD} value={AUTH_METHOD_PASSWORD}>
|
||||
{t("access.form.ssh_auth_method.option.password.label")}
|
||||
</Select.Option>
|
||||
<Select.Option key={AUTH_METHOD_KEY} value={AUTH_METHOD_KEY}>
|
||||
{t("access.form.ssh_auth_method.option.key.label")}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name={[field.name, "username"]} label={t("access.form.ssh_username.label")} rules={[formRule]}>
|
||||
<Input autoComplete="new-password" placeholder={t("access.form.ssh_username.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Show when={authMethod === AUTH_METHOD_PASSWORD}>
|
||||
<Form.Item name={[field.name, "password"]} label={t("access.form.ssh_password.label")} rules={[formRule]}>
|
||||
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_password.placeholder")} />
|
||||
</Form.Item>
|
||||
</Show>
|
||||
|
||||
<Show when={authMethod === AUTH_METHOD_KEY}>
|
||||
<Form.Item name={[field.name, "key"]} label={t("access.form.ssh_key.label")} rules={[formRule]}>
|
||||
<TextFileInput allowClear autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("access.form.ssh_key.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name={[field.name, "keyPassphrase"]} label={t("access.form.ssh_key_passphrase.label")} rules={[formRule]}>
|
||||
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_key_passphrase.placeholder")} />
|
||||
</Form.Item>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
key: field.key,
|
||||
label: <Label />,
|
||||
@@ -214,58 +278,12 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
|
||||
/>
|
||||
</Space.Compact>
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
<div className="flex space-x-2">
|
||||
<div className="w-2/3">
|
||||
<Form.Item name={[field.name, "host"]} label={t("access.form.ssh_host.label")} rules={[formRule]}>
|
||||
<Input placeholder={t("access.form.ssh_host.placeholder")} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="w-1/3">
|
||||
<Form.Item name={[field.name, "port"]} label={t("access.form.ssh_port.label")} rules={[formRule]}>
|
||||
<InputNumber className="w-full" placeholder={t("access.form.ssh_port.placeholder")} min={1} max={65535} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.Item name={[field.name, "username"]} label={t("access.form.ssh_username.label")} rules={[formRule]}>
|
||||
<Input autoComplete="new-password" placeholder={t("access.form.ssh_username.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[field.name, "password"]}
|
||||
label={t("access.form.ssh_password.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_password.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_password.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[field.name, "key"]}
|
||||
label={t("access.form.ssh_key.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key.tooltip") }}></span>}
|
||||
>
|
||||
<TextFileInput allowClear autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("access.form.ssh_key.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[field.name, "keyPassphrase"]}
|
||||
label={t("access.form.ssh_key_passphrase.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key_passphrase.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_key_passphrase.placeholder")} />
|
||||
</Form.Item>
|
||||
</>
|
||||
),
|
||||
children: <Fields />,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
<Button className="w-full" type="dashed" icon={<PlusOutlined />} onClick={() => add()}>
|
||||
<Button className="w-full" type="dashed" icon={<PlusOutlined />} onClick={() => add(initFormModel())}>
|
||||
{t("access.form.ssh_jump_servers.add")}
|
||||
</Button>
|
||||
</Space>
|
||||
|
@@ -379,7 +379,8 @@ export type AccessConfigForSlackBot = {
|
||||
export type AccessConfigForSSH = {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
authMethod?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
key?: string;
|
||||
keyPassphrase?: string;
|
||||
|
@@ -378,18 +378,21 @@
|
||||
"access.form.ssh_host.placeholder": "Please enter server host",
|
||||
"access.form.ssh_port.label": "Server port",
|
||||
"access.form.ssh_port.placeholder": "Please enter server port",
|
||||
"access.form.ssh_auth_method.label": "Authentication method",
|
||||
"access.form.ssh_auth_method.placeholder": "Please select authentication method",
|
||||
"access.form.ssh_auth_method.option.none.label": "None",
|
||||
"access.form.ssh_auth_method.option.password.label": "Password",
|
||||
"access.form.ssh_auth_method.option.key.label": "SSH key",
|
||||
"access.form.ssh_username.label": "Username",
|
||||
"access.form.ssh_username.placeholder": "Please enter username",
|
||||
"access.form.ssh_password.label": "Password (Optional)",
|
||||
"access.form.ssh_password.label": "Password",
|
||||
"access.form.ssh_password.placeholder": "Please enter password",
|
||||
"access.form.ssh_password.tooltip": "Required when using password to connect to SSH.",
|
||||
"access.form.ssh_key.label": "SSH key (Optional)",
|
||||
"access.form.ssh_key.label": "SSH key",
|
||||
"access.form.ssh_key.placeholder": "Please enter SSH key",
|
||||
"access.form.ssh_key.tooltip": "Required when using key to connect to SSH.",
|
||||
"access.form.ssh_key_passphrase.label": "SSH key passphrase (Optional)",
|
||||
"access.form.ssh_key_passphrase.placeholder": "Please enter SSH key passphrase",
|
||||
"access.form.ssh_key_passphrase.tooltip": "Optional when using key to connect to SSH.",
|
||||
"access.form.ssh_jump_servers.label": "SSH jump server (Optional)",
|
||||
"access.form.ssh_jump_servers.errmsg.invalid": "Please configure a valid jump server",
|
||||
"access.form.ssh_jump_servers.item.label": "Jump server",
|
||||
"access.form.ssh_jump_servers.add": "Add jump server",
|
||||
"access.form.sslcom_eab_kid.label": "ACME EAB KID",
|
||||
|
@@ -378,18 +378,21 @@
|
||||
"access.form.ssh_host.placeholder": "请输入服务器地址",
|
||||
"access.form.ssh_port.label": "服务器端口",
|
||||
"access.form.ssh_port.placeholder": "请输入服务器端口",
|
||||
"access.form.ssh_auth_method.label": "认证方式",
|
||||
"access.form.ssh_auth_method.placeholder": "请选择认证方式",
|
||||
"access.form.ssh_auth_method.option.none.label": "无",
|
||||
"access.form.ssh_auth_method.option.password.label": "密码",
|
||||
"access.form.ssh_auth_method.option.key.label": "密钥",
|
||||
"access.form.ssh_username.label": "用户名",
|
||||
"access.form.ssh_username.placeholder": "请输入用户名",
|
||||
"access.form.ssh_password.label": "密码(可选)",
|
||||
"access.form.ssh_password.label": "密码",
|
||||
"access.form.ssh_password.placeholder": "请输入密码",
|
||||
"access.form.ssh_password.tooltip": "使用密码连接到 SSH 时必填。<br>该字段与密钥文件字段二选一,如果同时填写优先使用 SSH 密钥登录。",
|
||||
"access.form.ssh_key.label": "SSH 密钥(可选)",
|
||||
"access.form.ssh_key.label": "SSH 密钥",
|
||||
"access.form.ssh_key.placeholder": "请输入 SSH 密钥文件内容",
|
||||
"access.form.ssh_key.tooltip": "使用 SSH 密钥连接到 SSH 时必填。<br>该字段与密码字段二选一,如果同时填写优先使用 SSH 密钥登录。",
|
||||
"access.form.ssh_key_passphrase.label": "SSH 密钥口令(可选)",
|
||||
"access.form.ssh_key_passphrase.placeholder": "请输入 SSH 密钥口令",
|
||||
"access.form.ssh_key_passphrase.tooltip": "使用 SSH 密钥连接到 SSH 时选填。",
|
||||
"access.form.ssh_jump_servers.label": "SSH 跳板机(可选)",
|
||||
"access.form.ssh_jump_servers.errmsg.invalid": "请配置有效的 SSH 跳板机",
|
||||
"access.form.ssh_jump_servers.item.label": "跳板机",
|
||||
"access.form.ssh_jump_servers.add": "添加跳板机",
|
||||
"access.form.sslcom_eab_kid.label": "ACME EAB KID",
|
||||
|
Reference in New Issue
Block a user