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.Mode = access.Mode
config.Username = access.Username config.Username = access.Username
config.Password = access.Password config.Password = access.Password
if a.option.Timeout != 0 { if a.option.PropagationTimeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second config.PropagationTimeout = time.Duration(a.option.PropagationTimeout) * time.Second
} }
provider, err := httpreq.NewDNSProviderConfig(config) provider, err := httpreq.NewDNSProviderConfig(config)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,9 +15,10 @@ export type AccessEditModalProps = {
preset: AccessEditFormProps["preset"]; preset: AccessEditFormProps["preset"];
trigger?: React.ReactElement; trigger?: React.ReactElement;
onOpenChange?: (open: boolean) => void; 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 { t } = useTranslation();
const [notificationApi, NotificationContextHolder] = notification.useNotification(); const [notificationApi, NotificationContextHolder] = notification.useNotification();
@ -57,20 +58,24 @@ const AccessEditModal = ({ data, loading, trigger, preset, ...props }: AccessEdi
} }
try { try {
let res: AccessModel;
if (preset === "add") { if (preset === "add") {
if (data?.id) { if (data?.id) {
throw "Invalid props: `data`"; throw "Invalid props: `data`";
} }
await createAccess(formRef.current!.getFieldsValue() as AccessModel); res = await createAccess(formRef.current!.getFieldsValue() as AccessModel);
} else if (preset === "edit") { } else if (preset === "edit") {
if (!data?.id) { if (!data?.id) {
throw "Invalid props: `data`"; 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); setOpen(false);
} catch (err) { } catch (err) {
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(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 { 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 AddNode = ({ data }: NodeProps | BrandNodeProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -56,7 +57,7 @@ const AddNode = ({ data }: NodeProps | BrandNodeProps) => {
trigger={["click"]} trigger={["click"]}
> >
<div className="bg-stone-400 hover:bg-stone-500 rounded-full z-10 relative outline-none"> <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> </div>
</Dropdown> </Dropdown>
</div> </div>

View File

@ -1,9 +1,9 @@
import { memo, useCallback, useEffect, useState } from "react"; import { memo, useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useControllableValue } from "ahooks"; 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 { 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 z from "zod";
import AccessEditModal from "@/components/access/AccessEditModal"; import AccessEditModal from "@/components/access/AccessEditModal";
@ -24,7 +24,8 @@ const initFormModel = (): WorkflowNodeConfig => {
return { return {
domain: "", domain: "",
keyAlgorithm: "RSA2048", keyAlgorithm: "RSA2048",
timeout: 60, nameservers: "",
propagationTimeout: 60,
disableFollowCNAME: true, disableFollowCNAME: true,
}; };
}; };
@ -32,6 +33,7 @@ const initFormModel = (): WorkflowNodeConfig => {
const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => { const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { addEmail } = useContactStore();
const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"]));
const { hidePanel } = usePanel(); const { hidePanel } = usePanel();
@ -59,7 +61,7 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
{ message: t("common.errmsg.host_invalid") } { message: t("common.errmsg.host_invalid") }
) )
.nullish(), .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(), disableFollowCNAME: z.boolean().nullish(),
}); });
const formRule = createSchemaFieldRule(formSchema); const formRule = createSchemaFieldRule(formSchema);
@ -71,6 +73,7 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
initialValues: data?.config ?? initFormModel(), initialValues: data?.config ?? initFormModel(),
onSubmit: async (values) => { onSubmit: async (values) => {
await updateNode({ ...data, config: { ...values }, validated: true }); await updateNode({ ...data, config: { ...values }, validated: true });
await addEmail(values.email);
hidePanel(); hidePanel();
}, },
}); });
@ -96,18 +99,31 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
</Form.Item> </Form.Item>
<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 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"> <div className="text-right">
<AccessEditModal <AccessEditModal
preset="add" preset="add"
trigger={ trigger={
<Button className="p-0" type="link"> <Button size="small" type="link">
<PlusOutlinedIcon /> <PlusOutlinedIcon />
{t("workflow.nodes.apply.form.access.button")} {t("workflow.nodes.apply.form.access.button")}
</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>
</div> </div>
@ -149,17 +165,17 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="timeout" name="propagationTimeout"
label={t("workflow.nodes.apply.form.timeout.label")} label={t("workflow.nodes.apply.form.propagation_timeout.label")}
rules={[formRule]} 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 <InputNumber
className="w-full" className="w-full"
min={0} min={0}
max={3600} max={3600}
placeholder={t("workflow.nodes.apply.form.timeout.placeholder")} placeholder={t("workflow.nodes.apply.form.propagation_timeout.placeholder")}
addonAfter={t("workflow.nodes.apply.form.timeout.suffix")} addonAfter={t("workflow.nodes.apply.form.propagation_timeout.suffix")}
/> />
</Form.Item> </Form.Item>

View File

@ -70,7 +70,7 @@ const NotifyNodeForm = ({ data }: NotifyNodeFormProps) => {
</Form.Item> </Form.Item>
<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 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="flex-grow max-w-full truncate">{t("workflow.nodes.notify.form.channel.label")}</div>
<div className="text-right"> <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", key: "startedAt",
title: t("workflow_run.props.started_at"), 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.label": "Deploy Method",
"domain.deployment.form.type.placeholder": "Please select deploy method", "domain.deployment.form.type.placeholder": "Please select deploy method",
"domain.deployment.form.type.list": "Deploy Method List", "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.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.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.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.domain.tooltip": "Wildcard domain: *.example.com",
"workflow.nodes.apply.form.email.label": "Contact Email", "workflow.nodes.apply.form.email.label": "Contact Email",
"workflow.nodes.apply.form.email.placeholder": "Please enter 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.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.label": "DNS Provider Authorization",
"workflow.nodes.apply.form.access.placeholder": "Please select an authorization of DNS provider", "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.access.button": "Create",
"workflow.nodes.apply.form.advanced_settings.label": "Advanced Settings", "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.label": "Certificate Key Algorithm",
"workflow.nodes.apply.form.key_algorithm.placeholder": "Please select 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.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.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.propagation_timeout.label": "DNS Propagation Timeout",
"workflow.nodes.apply.form.timeout.placeholder": "Please enter DNS propagation timeout", "workflow.nodes.apply.form.propagation_timeout.placeholder": "Please enter DNS propagation timeout",
"workflow.nodes.apply.form.timeout.suffix": "Seconds", "workflow.nodes.apply.form.propagation_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.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.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.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", "workflow.nodes.notify.form.subject.label": "Subject",
@ -73,6 +74,7 @@
"workflow_run.props.status": "Status", "workflow_run.props.status": "Status",
"workflow_run.props.status.succeeded": "Succeeded", "workflow_run.props.status.succeeded": "Succeeded",
"workflow_run.props.status.failed": "Failed", "workflow_run.props.status.failed": "Failed",
"workflow_run.props.trigger": "Trigger",
"workflow_run.props.started_at": "Started At", "workflow_run.props.started_at": "Started At",
"workflow_run.props.completed_at": "Completed 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.label": "部署方式",
"domain.deployment.form.type.placeholder": "请选择部署方式", "domain.deployment.form.type.placeholder": "请选择部署方式",
"domain.deployment.form.type.list": "部署方式列表", "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.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.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.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.domain.tooltip": "泛域名表示形式为:*.example.com",
"workflow.nodes.apply.form.email.label": "联系邮箱", "workflow.nodes.apply.form.email.label": "联系邮箱",
"workflow.nodes.apply.form.email.placeholder": "请输入联系邮箱", "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.label": "DNS 提供商授权",
"workflow.nodes.apply.form.access.placeholder": "请选择 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.access.button": "新建",
"workflow.nodes.apply.form.advanced_settings.label": "高级设置", "workflow.nodes.apply.form.advanced_settings.label": "高级设置",
"workflow.nodes.apply.form.key_algorithm.label": "数字证书算法", "workflow.nodes.apply.form.key_algorithm.label": "数字证书算法",
"workflow.nodes.apply.form.key_algorithm.placeholder": "请选择数字证书算法", "workflow.nodes.apply.form.key_algorithm.placeholder": "请选择数字证书算法",
"workflow.nodes.apply.form.nameservers.label": "DNS 递归服务器", "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.nameservers.tooltip": "在 ACME DNS-01 认证时使用自定义的 DNS 递归服务器。如果你不了解该选项的用途,保持默认即可。",
"workflow.nodes.apply.form.timeout.label": "DNS 传播检查超时时间", "workflow.nodes.apply.form.propagation_timeout.label": "DNS 传播检查超时时间",
"workflow.nodes.apply.form.timeout.placeholder": "请输入 DNS 传播检查超时时间", "workflow.nodes.apply.form.propagation_timeout.placeholder": "请输入 DNS 传播检查超时时间",
"workflow.nodes.apply.form.timeout.suffix": "秒", "workflow.nodes.apply.form.propagation_timeout.suffix": "秒",
"workflow.nodes.apply.form.timeout.tooltip": "在 ACME DNS-01 认证时等待 DNS 传播检查的最长时间。如果你不了解此选项的用途,保持默认即可。", "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.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.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": "通知主题", "workflow.nodes.notify.form.subject.label": "通知主题",
@ -73,6 +74,7 @@
"workflow_run.props.status": "状态", "workflow_run.props.status": "状态",
"workflow_run.props.status.succeeded": "成功", "workflow_run.props.status.succeeded": "成功",
"workflow_run.props.status.failed": "失败", "workflow_run.props.status.failed": "失败",
"workflow_run.props.trigger": "触发方式",
"workflow_run.props.started_at": "开始时间", "workflow_run.props.started_at": "开始时间",
"workflow_run.props.completed_at": "完成时间", "workflow_run.props.completed_at": "完成时间",

View File

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

View File

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