refactor: rename Timeout to PropagationTimeout during ACME DNS-01 authentication

This commit is contained in:
Fu Diwei 2024-12-27 09:50:54 +08:00
parent dae6ad2951
commit 77537e7005
24 changed files with 127 additions and 180 deletions

View File

@ -30,8 +30,8 @@ func (a *acmeHttpReqApplicant) Apply() (*Certificate, error) {
config.Mode = access.Mode
config.Username = access.Username
config.Password = access.Password
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
if a.option.PropagationTimeout != 0 {
config.PropagationTimeout = time.Duration(a.option.PropagationTimeout) * time.Second
}
provider, err := httpreq.NewDNSProviderConfig(config)

View File

@ -26,8 +26,8 @@ func (a *aliyunApplicant) Apply() (*Certificate, error) {
config := alidns.NewDefaultConfig()
config.APIKey = access.AccessKeyId
config.SecretKey = access.AccessKeySecret
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
if a.option.PropagationTimeout != 0 {
config.PropagationTimeout = time.Duration(a.option.PropagationTimeout) * time.Second
}
provider, err := alidns.NewDNSProviderConfig(config)

View File

@ -67,8 +67,6 @@ var sslProviderUrls = map[string]string{
const defaultEmail = "536464346@qq.com"
const defaultTimeout = 60
type Certificate struct {
CertUrl string `json:"certUrl"`
CertStableUrl string `json:"certStableUrl"`
@ -84,7 +82,7 @@ type ApplyOption struct {
Access string `json:"access"`
KeyAlgorithm string `json:"keyAlgorithm"`
Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"`
PropagationTimeout int64 `json:"propagationTimeout"`
DisableFollowCNAME bool `json:"disableFollowCNAME"`
}
@ -164,17 +162,13 @@ func Get(record *models.Record) (Applicant, error) {
applyConfig.Email = defaultEmail
}
if applyConfig.Timeout == 0 {
applyConfig.Timeout = defaultTimeout
}
option := &ApplyOption{
Email: applyConfig.Email,
Domain: record.GetString("domain"),
Access: access.GetString("config"),
KeyAlgorithm: applyConfig.KeyAlgorithm,
Nameservers: applyConfig.Nameservers,
Timeout: applyConfig.Timeout,
PropagationTimeout: applyConfig.PropagationTimeout,
DisableFollowCNAME: applyConfig.DisableFollowCNAME,
}
@ -190,18 +184,13 @@ func GetWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
return nil, fmt.Errorf("access record not found: %w", err)
}
timeout := node.GetConfigInt64("timeout")
if timeout == 0 {
timeout = defaultTimeout
}
applyConfig := &ApplyOption{
Email: node.GetConfigString("email"),
Domain: node.GetConfigString("domain"),
Access: access.Config,
KeyAlgorithm: node.GetConfigString("keyAlgorithm"),
Nameservers: node.GetConfigString("nameservers"),
Timeout: timeout,
PropagationTimeout: node.GetConfigInt64("propagationTimeout"),
DisableFollowCNAME: node.GetConfigBool("disableFollowCNAME"),
}

View File

@ -28,8 +28,8 @@ func (a *awsApplicant) Apply() (*Certificate, error) {
config.SecretAccessKey = access.SecretAccessKey
config.Region = access.Region
config.HostedZoneID = access.HostedZoneId
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
if a.option.PropagationTimeout != 0 {
config.PropagationTimeout = time.Duration(a.option.PropagationTimeout) * time.Second
}
provider, err := route53.NewDNSProviderConfig(config)

View File

@ -25,8 +25,8 @@ func (a *cloudflareApplicant) Apply() (*Certificate, error) {
config := cloudflare.NewDefaultConfig()
config.AuthToken = access.DnsApiToken
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
if a.option.PropagationTimeout != 0 {
config.PropagationTimeout = time.Duration(a.option.PropagationTimeout) * time.Second
}
provider, err := cloudflare.NewDNSProviderConfig(config)

View File

@ -26,8 +26,8 @@ func (a *godaddyApplicant) Apply() (*Certificate, error) {
config := godaddy.NewDefaultConfig()
config.APIKey = access.ApiKey
config.APISecret = access.ApiSecret
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
if a.option.PropagationTimeout != 0 {
config.PropagationTimeout = time.Duration(a.option.PropagationTimeout) * time.Second
}
provider, err := godaddy.NewDNSProviderConfig(config)

View File

@ -33,8 +33,8 @@ func (a *huaweicloudApplicant) Apply() (*Certificate, error) {
config.AccessKeyID = access.AccessKeyId
config.SecretAccessKey = access.SecretAccessKey
config.Region = region
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
if a.option.PropagationTimeout != 0 {
config.PropagationTimeout = time.Duration(a.option.PropagationTimeout) * time.Second
}
provider, err := huaweicloud.NewDNSProviderConfig(config)

View File

@ -25,8 +25,8 @@ func (a *nameDotComApplicant) Apply() (*Certificate, error) {
config := namedotcom.NewDefaultConfig()
config.Username = access.Username
config.APIToken = access.ApiToken
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
if a.option.PropagationTimeout != 0 {
config.PropagationTimeout = time.Duration(a.option.PropagationTimeout) * time.Second
}
provider, err := namedotcom.NewDNSProviderConfig(config)

View File

@ -25,8 +25,8 @@ func (a *namesiloApplicant) Apply() (*Certificate, error) {
config := namesilo.NewDefaultConfig()
config.APIKey = access.ApiKey
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
if a.option.PropagationTimeout != 0 {
config.PropagationTimeout = time.Duration(a.option.PropagationTimeout) * time.Second
}
provider, err := namesilo.NewDNSProviderConfig(config)

View File

@ -28,8 +28,8 @@ func (a *powerdnsApplicant) Apply() (*Certificate, error) {
host, _ := url.Parse(access.ApiUrl)
config.Host = host
config.APIKey = access.ApiKey
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
if a.option.PropagationTimeout != 0 {
config.PropagationTimeout = time.Duration(a.option.PropagationTimeout) * time.Second
}
provider, err := pdns.NewDNSProviderConfig(config)

View File

@ -26,8 +26,8 @@ func (a *tencentcloudApplicant) Apply() (*Certificate, error) {
config := tencentcloud.NewDefaultConfig()
config.SecretID = access.SecretId
config.SecretKey = access.SecretKey
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
if a.option.PropagationTimeout != 0 {
config.PropagationTimeout = time.Duration(a.option.PropagationTimeout) * time.Second
}
provider, err := tencentcloud.NewDNSProviderConfig(config)

View File

@ -25,8 +25,8 @@ func (a *volcengineApplicant) Apply() (*Certificate, error) {
config := volcengine.NewDefaultConfig()
config.AccessKey = access.AccessKeyId
config.SecretKey = access.SecretAccessKey
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
if a.option.PropagationTimeout != 0 {
config.PropagationTimeout = time.Duration(a.option.PropagationTimeout) * time.Second
}
provider, err := volcengine.NewDNSProviderConfig(config)

View File

@ -12,7 +12,7 @@ type ApplyConfig struct {
Access string `json:"access"`
KeyAlgorithm string `json:"keyAlgorithm"`
Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"`
PropagationTimeout int64 `json:"propagationTimeout"`
DisableFollowCNAME bool `json:"disableFollowCNAME"`
}

View File

@ -15,9 +15,10 @@ export type AccessEditModalProps = {
preset: AccessEditFormProps["preset"];
trigger?: React.ReactElement;
onOpenChange?: (open: boolean) => void;
onSubmit?: (record: AccessModel) => void;
};
const AccessEditModal = ({ data, loading, trigger, preset, ...props }: AccessEditModalProps) => {
const AccessEditModal = ({ data, loading, trigger, preset, onSubmit, ...props }: AccessEditModalProps) => {
const { t } = useTranslation();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
@ -57,20 +58,24 @@ const AccessEditModal = ({ data, loading, trigger, preset, ...props }: AccessEdi
}
try {
let res: AccessModel;
if (preset === "add") {
if (data?.id) {
throw "Invalid props: `data`";
}
await createAccess(formRef.current!.getFieldsValue() as AccessModel);
res = await createAccess(formRef.current!.getFieldsValue() as AccessModel);
} else if (preset === "edit") {
if (!data?.id) {
throw "Invalid props: `data`";
}
await updateAccess({ ...data, ...formRef.current!.getFieldsValue() } as AccessModel);
res = await updateAccess({ ...data, ...formRef.current!.getFieldsValue() } as AccessModel);
} else {
throw "Invalid props: `preset`";
}
onSubmit?.(res);
setOpen(false);
} catch (err) {
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });

View File

@ -1,11 +1,12 @@
import { Plus } from "lucide-react";
import { BrandNodeProps, NodeProps } from "./types";
import { newWorkflowNode, workflowNodeDropdownList, WorkflowNodeType } from "@/domain/workflow";
import { useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow";
import { Dropdown } from "antd";
import DropdownMenuItemIcon from "./DropdownMenuItemIcon";
import { useTranslation } from "react-i18next";
import { Dropdown } from "antd";
import { Plus as PlusIcon } from "lucide-react";
import { useZustandShallowSelector } from "@/hooks";
import { newWorkflowNode, workflowNodeDropdownList, WorkflowNodeType } from "@/domain/workflow";
import { useWorkflowStore } from "@/stores/workflow";
import { type BrandNodeProps, type NodeProps } from "./types";
import DropdownMenuItemIcon from "./DropdownMenuItemIcon";
const AddNode = ({ data }: NodeProps | BrandNodeProps) => {
const { t } = useTranslation();
@ -56,7 +57,7 @@ const AddNode = ({ data }: NodeProps | BrandNodeProps) => {
trigger={["click"]}
>
<div className="bg-stone-400 hover:bg-stone-500 rounded-full z-10 relative outline-none">
<Plus size={18} className="text-white" />
<PlusIcon className="text-white" size={18} />
</div>
</Dropdown>
</div>

View File

@ -1,9 +1,9 @@
import { memo, useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useControllableValue } from "ahooks";
import { AutoComplete, Button, Divider, Form, Input, InputNumber, Select, Switch, Typography, type AutoCompleteProps } from "antd";
import { AutoComplete, Button, Divider, Form, Input, InputNumber, Select, Switch, Tooltip, Typography, type AutoCompleteProps } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { PlusOutlined as PlusOutlinedIcon } from "@ant-design/icons";
import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
import z from "zod";
import AccessEditModal from "@/components/access/AccessEditModal";
@ -24,7 +24,8 @@ const initFormModel = (): WorkflowNodeConfig => {
return {
domain: "",
keyAlgorithm: "RSA2048",
timeout: 60,
nameservers: "",
propagationTimeout: 60,
disableFollowCNAME: true,
};
};
@ -32,6 +33,7 @@ const initFormModel = (): WorkflowNodeConfig => {
const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
const { t } = useTranslation();
const { addEmail } = useContactStore();
const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"]));
const { hidePanel } = usePanel();
@ -59,7 +61,7 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
{ message: t("common.errmsg.host_invalid") }
)
.nullish(),
timeout: z.number().gte(1, t("workflow.nodes.apply.form.timeout.placeholder")).nullish(),
timeout: z.number().gte(1, t("workflow.nodes.apply.form.propagation_timeout.placeholder")).nullish(),
disableFollowCNAME: z.boolean().nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
@ -71,6 +73,7 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
initialValues: data?.config ?? initFormModel(),
onSubmit: async (values) => {
await updateNode({ ...data, config: { ...values }, validated: true });
await addEmail(values.email);
hidePanel();
},
});
@ -96,18 +99,31 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
</Form.Item>
<Form.Item>
<label className="block mb-[2px]">
<label className="block mb-1">
<div className="flex items-center justify-between gap-4 w-full overflow-hidden">
<div className="flex-grow max-w-full truncate">{t("workflow.nodes.apply.form.access.label")}</div>
<div className="flex-grow max-w-full truncate">
<span>{t("workflow.nodes.apply.form.access.label")}</span>
<Tooltip title={t("workflow.nodes.apply.form.access.tooltip")}>
<Typography.Text className="ms-1" type="secondary">
<QuestionCircleOutlinedIcon />
</Typography.Text>
</Tooltip>
</div>
<div className="text-right">
<AccessEditModal
preset="add"
trigger={
<Button className="p-0" type="link">
<Button size="small" type="link">
<PlusOutlinedIcon />
{t("workflow.nodes.apply.form.access.button")}
</Button>
}
onSubmit={(record) => {
const provider = accessProvidersMap.get(record.configType);
if (ACCESS_PROVIDER_USAGES.ALL === provider?.usage || ACCESS_PROVIDER_USAGES.APPLY === provider?.usage) {
formInst.setFieldValue("access", record.id);
}
}}
/>
</div>
</div>
@ -149,17 +165,17 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
</Form.Item>
<Form.Item
name="timeout"
label={t("workflow.nodes.apply.form.timeout.label")}
name="propagationTimeout"
label={t("workflow.nodes.apply.form.propagation_timeout.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.timeout.tooltip") }}></span>}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.propagation_timeout.tooltip") }}></span>}
>
<InputNumber
className="w-full"
min={0}
max={3600}
placeholder={t("workflow.nodes.apply.form.timeout.placeholder")}
addonAfter={t("workflow.nodes.apply.form.timeout.suffix")}
placeholder={t("workflow.nodes.apply.form.propagation_timeout.placeholder")}
addonAfter={t("workflow.nodes.apply.form.propagation_timeout.suffix")}
/>
</Form.Item>

View File

@ -70,7 +70,7 @@ const NotifyNodeForm = ({ data }: NotifyNodeFormProps) => {
</Form.Item>
<Form.Item>
<label className="block mb-[2px]">
<label className="block mb-1">
<div className="flex items-center justify-between gap-4 w-full overflow-hidden">
<div className="flex-grow max-w-full truncate">{t("workflow.nodes.notify.form.channel.label")}</div>
<div className="text-right">

View File

@ -63,6 +63,14 @@ const WorkflowRuns = ({ className, style }: WorkflowRunsProps) => {
}
},
},
{
key: "trigger",
title: t("workflow_run.props.trigger"),
ellipsis: true,
render: () => {
return "TODO";
},
},
{
key: "startedAt",
title: t("workflow_run.props.started_at"),

View File

@ -1,53 +1,4 @@
{
"domain.page.title": "Domain List",
"domain.nodata": "Please add a domain to start deploying the certificate.",
"domain.add": "Add Domain",
"domain.edit": "Edit Domain",
"domain.delete": "Delete Domain",
"domain.delete.confirm": "Are you sure you want to delete this domain?",
"domain.history": "Deployment History",
"domain.deploy": "Deploy Now",
"domain.deploy.started.message": "Deploy Started",
"domain.deploy.started.tips": "Deployment initiated, please check the deployment log later.",
"domain.deploy.failed.message": "Execution Failed",
"domain.deploy.failed.tips": "Execution failed, please check the details in <1>Deployment History</1>.",
"domain.deploy_forced": "Force Deploy",
"domain.props.expiry": "Validity Period",
"domain.props.expiry.date1": "Valid for {{date}} days",
"domain.props.expiry.date2": "Expiry on {{date}}",
"domain.props.last_execution_status": "Last Execution Status",
"domain.props.last_execution_stage": "Last Execution Stage",
"domain.props.last_execution_time": "Last Execution Time",
"domain.props.enable": "Enable",
"domain.props.enable.enabled": "Enable",
"domain.props.enable.disabled": "Disable",
"domain.application.tab": "Apply Settings",
"domain.application.form.domain.added.message": "Domain added successfully",
"domain.application.form.domain.changed.message": "Domain updated successfully",
"domain.application.form.email.label": "Email",
"domain.application.form.email.tips": "(A email is required to apply for a certificate)",
"domain.application.form.email.placeholder": "Please select email",
"domain.application.form.email.add": "Add Email",
"domain.application.form.email.list": "Email List",
"domain.application.form.access.label": "DNS Provider Authorization Configuration",
"domain.application.form.access.placeholder": "Please select DNS provider authorization configuration",
"domain.application.form.access.list": "Provider Authorization Configurations",
"domain.application.form.advanced_settings.label": "Advanced Settings",
"domain.application.form.key_algorithm.label": "Certificate Key Algorithm (Default: RSA2048)",
"domain.application.form.key_algorithm.placeholder": "Please select certificate key algorithm",
"domain.application.form.timeout.label": "DNS Propagation Timeout (Seconds)",
"domain.application.form.timeout.placeholder": "Please enter maximum waiting time for DNS propagation",
"domain.application.form.disable_follow_cname.label": "Disable DNS CNAME following",
"domain.application.form.disable_follow_cname.tips": "This option will disable Acme DNS authentication CNAME follow. If you don't understand this option, just keep it by default. ",
"domain.application.form.disable_follow_cname.tips_link": "Learn more",
"domain.application.unsaved.message": "Please save applyment configuration first",
"domain.deployment.tab": "Deploy Settings",
"domain.deployment.nodata": "Deployment not added yet",
"domain.deployment.form.type.label": "Deploy Method",
"domain.deployment.form.type.placeholder": "Please select deploy method",
"domain.deployment.form.type.list": "Deploy Method List",

View File

@ -41,24 +41,25 @@
"workflow.nodes.start.form.trigger_cron.extra": "Expected execution time for the last 5 times:",
"workflow.nodes.start.form.trigger_cron_alert.content": "Tips: If you have multiple workflows, it is recommended to set them to run at multiple times of the day instead of always running at specific times.<br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Lets Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Lets Encrypt (ACME) client run at a random time?</a>",
"workflow.nodes.apply.form.domain.label": "Domain",
"workflow.nodes.apply.form.domain.placeholder": "Please enter domain",
"workflow.nodes.apply.form.domain.placeholder": "Please enter domain (separated by semicolons)",
"workflow.nodes.apply.form.domain.tooltip": "Wildcard domain: *.example.com",
"workflow.nodes.apply.form.email.label": "Contact Email",
"workflow.nodes.apply.form.email.placeholder": "Please enter contact email",
"workflow.nodes.apply.form.email.tooltip": "Contact information required for SSL certificate application. Please pay attention to the <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">rate limits</a>.",
"workflow.nodes.apply.form.access.label": "DNS Provider Authorization",
"workflow.nodes.apply.form.access.placeholder": "Please select an authorization of DNS provider",
"workflow.nodes.apply.form.access.tooltip": "Used to manage DNS records during ACME DNS-01 authentication.",
"workflow.nodes.apply.form.access.button": "Create",
"workflow.nodes.apply.form.advanced_settings.label": "Advanced Settings",
"workflow.nodes.apply.form.key_algorithm.label": "Certificate Key Algorithm",
"workflow.nodes.apply.form.key_algorithm.placeholder": "Please select certificate key algorithm",
"workflow.nodes.apply.form.nameservers.label": "DNS Recursive Nameservers",
"workflow.nodes.apply.form.nameservers.placeholder": "Please enter DNS recursive nameservers",
"workflow.nodes.apply.form.nameservers.placeholder": "Please enter DNS recursive nameservers (separated by semicolons)",
"workflow.nodes.apply.form.nameservers.tooltip": "It determines whether to custom DNS recursive nameservers during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.",
"workflow.nodes.apply.form.timeout.label": "DNS Propagation Timeout",
"workflow.nodes.apply.form.timeout.placeholder": "Please enter DNS propagation timeout",
"workflow.nodes.apply.form.timeout.suffix": "Seconds",
"workflow.nodes.apply.form.timeout.tooltip": "It determines the maximum waiting time for DNS propagation checks during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.",
"workflow.nodes.apply.form.propagation_timeout.label": "DNS Propagation Timeout",
"workflow.nodes.apply.form.propagation_timeout.placeholder": "Please enter DNS propagation timeout",
"workflow.nodes.apply.form.propagation_timeout.suffix": "Seconds",
"workflow.nodes.apply.form.propagation_timeout.tooltip": "It determines the maximum waiting time for DNS propagation checks during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.",
"workflow.nodes.apply.form.disable_follow_cname.label": "Disable CNAME following",
"workflow.nodes.apply.form.disable_follow_cname.tooltip": "It determines whether to disable CNAME following during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.<br><a href=\"https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme/#the-advantages-of-a-cname\" target=\"_blank\">Learn more</a>.",
"workflow.nodes.notify.form.subject.label": "Subject",
@ -73,6 +74,7 @@
"workflow_run.props.status": "Status",
"workflow_run.props.status.succeeded": "Succeeded",
"workflow_run.props.status.failed": "Failed",
"workflow_run.props.trigger": "Trigger",
"workflow_run.props.started_at": "Started At",
"workflow_run.props.completed_at": "Completed At",

View File

@ -1,53 +1,4 @@
{
"domain.page.title": "域名列表",
"domain.nodata": "请添加域名开始部署证书吧。",
"domain.add": "新增域名",
"domain.edit": "编辑域名",
"domain.delete": "删除域名",
"domain.delete.confirm": "确定要删除域名吗?",
"domain.history": "部署历史",
"domain.deploy": "立即部署",
"domain.deploy.started.message": "开始部署",
"domain.deploy.started.tips": "已发起部署,请稍后查看部署日志。",
"domain.deploy.failed.message": "执行失败",
"domain.deploy.failed.tips": "执行失败,请在 <1>部署历史</1> 查看详情。",
"domain.deploy_forced": "强行部署",
"domain.props.expiry": "有效期限",
"domain.props.expiry.date1": "有效期 {{date}} 天",
"domain.props.expiry.date2": "{{date}} 到期",
"domain.props.last_execution_status": "最近执行状态",
"domain.props.last_execution_stage": "最近执行阶段",
"domain.props.last_execution_time": "最近执行时间",
"domain.props.enable": "是否启用",
"domain.props.enable.enabled": "启用",
"domain.props.enable.disabled": "禁用",
"domain.application.tab": "申请配置",
"domain.application.form.domain.added.message": "域名添加成功",
"domain.application.form.domain.changed.message": "域名编辑成功",
"domain.application.form.email.label": "邮箱",
"domain.application.form.email.tips": "(申请证书需要提供邮箱)",
"domain.application.form.email.placeholder": "请选择邮箱",
"domain.application.form.email.add": "添加邮箱",
"domain.application.form.email.list": "邮箱列表",
"domain.application.form.access.label": "DNS 提供商授权配置",
"domain.application.form.access.placeholder": "请选择 DNS 提供商授权配置",
"domain.application.form.access.list": "DNS 提供商授权配置列表",
"domain.application.form.advanced_settings.label": "高级设置",
"domain.application.form.key_algorithm.label": "数字证书算法默认RSA2048",
"domain.application.form.key_algorithm.placeholder": "请选择数字证书算法",
"domain.application.form.timeout.label": "DNS 传播检查超时时间(单位:秒)",
"domain.application.form.timeout.placeholder": "请输入 DNS 传播检查超时时间",
"domain.application.form.disable_follow_cname.label": "禁用 DNS CNAME 跟随",
"domain.application.form.disable_follow_cname.tips": "该选项将禁用 Acme DNS 认证 CNAME 跟随,如果你不了解此选项保持默认即可,",
"domain.application.form.disable_follow_cname.tips_link": "了解更多",
"domain.application.unsaved.message": "请先保存申请配置",
"domain.deployment.tab": "部署配置",
"domain.deployment.nodata": "暂无部署配置,请添加后开始部署证书吧",
"domain.deployment.form.type.label": "部署方式",
"domain.deployment.form.type.placeholder": "请选择部署方式",
"domain.deployment.form.type.list": "部署方式列表",

View File

@ -41,24 +41,25 @@
"workflow.nodes.start.form.trigger_cron.extra": "预计最近 5 次执行时间:",
"workflow.nodes.start.form.trigger_cron_alert.content": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Lets Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Lets Encrypt (ACME) 客户端启动时间应当随机?</a>",
"workflow.nodes.apply.form.domain.label": "域名",
"workflow.nodes.apply.form.domain.placeholder": "请输入域名",
"workflow.nodes.apply.form.domain.placeholder": "请输入域名(多个值请用半角分号隔开)",
"workflow.nodes.apply.form.domain.tooltip": "泛域名表示形式为:*.example.com",
"workflow.nodes.apply.form.email.label": "联系邮箱",
"workflow.nodes.apply.form.email.placeholder": "请输入联系邮箱",
"workflow.nodes.apply.form.email.tooltip": "申请签发 SSL 证书时所需的联系方式。请注意<a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">速率限制</a>。",
"workflow.nodes.apply.form.email.tooltip": "申请签发 SSL 证书时所需的联系方式。请注意 Let's Encrypt 账户注册的<a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">速率限制(点此了解更多)</a>。",
"workflow.nodes.apply.form.access.label": "DNS 提供商授权",
"workflow.nodes.apply.form.access.placeholder": "请选择 DNS 提供商授权",
"workflow.nodes.apply.form.access.tooltip": "用于 ACME DNS-01 认证时操作域名解析记录,注意与部署阶段所需的主机提供商相区分。",
"workflow.nodes.apply.form.access.button": "新建",
"workflow.nodes.apply.form.advanced_settings.label": "高级设置",
"workflow.nodes.apply.form.key_algorithm.label": "数字证书算法",
"workflow.nodes.apply.form.key_algorithm.placeholder": "请选择数字证书算法",
"workflow.nodes.apply.form.nameservers.label": "DNS 递归服务器",
"workflow.nodes.apply.form.nameservers.placeholder": "请输入 DNS 递归服务器",
"workflow.nodes.apply.form.nameservers.placeholder": "请输入 DNS 递归服务器(多个值请用半角分号隔开)",
"workflow.nodes.apply.form.nameservers.tooltip": "在 ACME DNS-01 认证时使用自定义的 DNS 递归服务器。如果你不了解该选项的用途,保持默认即可。",
"workflow.nodes.apply.form.timeout.label": "DNS 传播检查超时时间",
"workflow.nodes.apply.form.timeout.placeholder": "请输入 DNS 传播检查超时时间",
"workflow.nodes.apply.form.timeout.suffix": "秒",
"workflow.nodes.apply.form.timeout.tooltip": "在 ACME DNS-01 认证时等待 DNS 传播检查的最长时间。如果你不了解此选项的用途,保持默认即可。",
"workflow.nodes.apply.form.propagation_timeout.label": "DNS 传播检查超时时间",
"workflow.nodes.apply.form.propagation_timeout.placeholder": "请输入 DNS 传播检查超时时间",
"workflow.nodes.apply.form.propagation_timeout.suffix": "秒",
"workflow.nodes.apply.form.propagation_timeout.tooltip": "在 ACME DNS-01 认证时等待 DNS 传播检查的最长时间。如果你不了解此选项的用途,保持默认即可。",
"workflow.nodes.apply.form.disable_follow_cname.label": "禁止 CNAME 跟随",
"workflow.nodes.apply.form.disable_follow_cname.tooltip": "在 ACME DNS-01 认证时是否禁止 CNAME 跟随。如果你不了解该选项的用途,保持默认即可。<br><a href=\"https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme/#the-advantages-of-a-cname\" target=\"_blank\">点此了解更多</a>。",
"workflow.nodes.notify.form.subject.label": "通知主题",
@ -73,6 +74,7 @@
"workflow_run.props.status": "状态",
"workflow_run.props.status.succeeded": "成功",
"workflow_run.props.status.failed": "失败",
"workflow_run.props.trigger": "触发方式",
"workflow_run.props.started_at": "开始时间",
"workflow_run.props.completed_at": "完成时间",

View File

@ -10,9 +10,9 @@ export interface AccessState {
loadedAtOnce: boolean;
fetchAccesses: () => Promise<void>;
createAccess: (access: MaybeModelRecord<AccessModel>) => Promise<void>;
updateAccess: (access: MaybeModelRecordWithId<AccessModel>) => Promise<void>;
deleteAccess: (access: MaybeModelRecordWithId<AccessModel>) => Promise<void>;
createAccess: (access: MaybeModelRecord<AccessModel>) => Promise<AccessModel>;
updateAccess: (access: MaybeModelRecordWithId<AccessModel>) => Promise<AccessModel>;
deleteAccess: (access: MaybeModelRecordWithId<AccessModel>) => Promise<AccessModel>;
}
export const useAccessStore = create<AccessState>((set) => {
@ -25,17 +25,17 @@ export const useAccessStore = create<AccessState>((set) => {
createAccess: async (access) => {
const record = await saveAccess(access);
set(
produce((state: AccessState) => {
state.accesses.unshift(record);
})
);
return record as AccessModel;
},
updateAccess: async (access) => {
const record = await saveAccess(access);
set(
produce((state: AccessState) => {
const index = state.accesses.findIndex((e) => e.id === record.id);
@ -44,16 +44,19 @@ export const useAccessStore = create<AccessState>((set) => {
}
})
);
return record as AccessModel;
},
deleteAccess: async (access) => {
await removeAccess(access);
set(
produce((state: AccessState) => {
state.accesses = state.accesses.filter((a) => a.id !== access.id);
})
);
return access as AccessModel;
},
fetchAccesses: async () => {

View File

@ -11,9 +11,11 @@ export interface ContactState {
fetchEmails: () => Promise<void>;
setEmails: (emails: string[]) => Promise<void>;
addEmail: (email: string) => Promise<void>;
removeEmail: (email: string) => Promise<void>;
}
export const useContactStore = create<ContactState>((set) => {
export const useContactStore = create<ContactState>((set, get) => {
let fetcher: Promise<SettingsModel<EmailsSettingsContent>> | null = null; // 防止多次重复请求
let settings: SettingsModel<EmailsSettingsContent>; // 记录当前设置的其他字段,保存回数据库时用
@ -34,12 +36,29 @@ export const useContactStore = create<ContactState>((set) => {
set(
produce((state: ContactState) => {
state.emails = settings.content.emails;
state.emails = settings.content.emails?.sort() ?? [];
state.loadedAtOnce = true;
})
);
},
addEmail: async (email) => {
const emails = produce(get().emails, (draft) => {
if (draft.includes(email)) return;
draft.push(email);
draft.sort();
});
get().setEmails(emails);
},
removeEmail: async (email) => {
const emails = produce(get().emails, (draft) => {
draft = draft.filter((e) => e !== email);
draft.sort();
});
get().setEmails(emails);
},
fetchEmails: async () => {
fetcher ??= getSettings<EmailsSettingsContent>(SETTINGS_NAMES.EMAILS);