From 26028bb1ebac806868f5adc25a305e19035cb6fe Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 3 Apr 2025 20:30:44 +0800 Subject: [PATCH 01/22] chore(ui): improve i18n --- ui/src/i18n/locales/en/nls.settings.json | 14 +++++++------- ui/src/i18n/locales/zh/nls.common.json | 2 +- ui/src/i18n/locales/zh/nls.settings.json | 20 ++++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/ui/src/i18n/locales/en/nls.settings.json b/ui/src/i18n/locales/en/nls.settings.json index d436665c..85c17f49 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -53,21 +53,21 @@ "settings.notification.channel.form.email_sender_address.placeholder": "Please enter sender email address", "settings.notification.channel.form.email_receiver_address.label": "Receiver email address", "settings.notification.channel.form.email_receiver_address.placeholder": "Please enter receiver email address", - "settings.notification.channel.form.gotify_url.placeholder": "Please enter Service URL", "settings.notification.channel.form.gotify_url.label": "Service URL", - "settings.notification.channel.form.gotify_url.tooltip": "Example: https://gotify.exmaple.com, the protocol needs to be included but the trailing '/' should not be included.
For more information, see https://gotify.net/docs/pushmsg", - "settings.notification.channel.form.gotify_token.placeholder": "Please enter Application Token", - "settings.notification.channel.form.gotify_token.label": "Application Token", + "settings.notification.channel.form.gotify_url.placeholder": "Please enter Service URL", + "settings.notification.channel.form.gotify_url.tooltip": "For more information, see https://gotify.net/docs/pushmsg

Example: https://gotify.exmaple.com, the trailing '/' should not be included.", + "settings.notification.channel.form.gotify_token.label": "Application token", + "settings.notification.channel.form.gotify_token.placeholder": "Please enter Application token", "settings.notification.channel.form.gotify_token.tooltip": "For more information, see https://gotify.net/docs/pushmsg", "settings.notification.channel.form.gotify_priority.placeholder": "Please enter message priority", - "settings.notification.channel.form.gotify_priority.label": "Message Priority", - "settings.notification.channel.form.gotify_priority.tooltip": "Message Priority, you can set it to 1 as default.
For more information, see https://gotify.net/docs/pushmsg
https://github.com/gotify/android/issues/18#issuecomment-437403888", + "settings.notification.channel.form.gotify_priority.label": "Message priority", + "settings.notification.channel.form.gotify_priority.tooltip": "For more information, see https://gotify.net/docs/pushmsg, https://github.com/gotify/android/issues/18#issuecomment-437403888", "settings.notification.channel.form.gotify_priority.error.gte0": "Message Priority must be greater than or equal to 0.", "settings.notification.channel.form.lark_webhook_url.label": "Webhook URL", "settings.notification.channel.form.lark_webhook_url.placeholder": "Please enter Webhook URL", "settings.notification.channel.form.lark_webhook_url.tooltip": "For more information, see https://www.feishu.cn/hc/en-US/articles/807992406756", - "settings.notification.channel.form.pushplus_token.placeholder": "Please enter Token", "settings.notification.channel.form.pushplus_token.label": "Token", + "settings.notification.channel.form.pushplus_token.placeholder": "Please enter token", "settings.notification.channel.form.pushplus_token.tooltip": "For more information, see https://www.pushplus.plus/push1.html", "settings.notification.channel.form.serverchan_url.label": "Server URL", "settings.notification.channel.form.serverchan_url.placeholder": "Please enter ServerChan server URL (e.g. https://sctapi.ftqq.com/*****.send)", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index 726c5ca2..4f279ba0 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -41,7 +41,7 @@ "common.notifier.email": "邮件", "common.notifier.gotify": "Gotify", "common.notifier.lark": "飞书", - "common.notifier.pushplus": "PushPlus推送加", + "common.notifier.pushplus": "PushPlus 推送加", "common.notifier.serverchan": "Server 酱", "common.notifier.telegram": "Telegram", "common.notifier.webhook": "Webhook", diff --git a/ui/src/i18n/locales/zh/nls.settings.json b/ui/src/i18n/locales/zh/nls.settings.json index c00d158a..292a5704 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -53,22 +53,22 @@ "settings.notification.channel.form.email_sender_address.placeholder": "请输入发送邮箱地址", "settings.notification.channel.form.email_receiver_address.label": "接收邮箱地址", "settings.notification.channel.form.email_receiver_address.placeholder": "请输入接收邮箱地址", - "settings.notification.channel.form.gotify_url.placeholder": "请输入服务地址", "settings.notification.channel.form.gotify_url.label": "服务地址", - "settings.notification.channel.form.gotify_url.tooltip": "示例: https://gotify.exmaple.com,需要包含协议但不要包含末尾的'/'。
请参阅 https://gotify.net/docs/pushmsg", - "settings.notification.channel.form.gotify_token.placeholder": "请输入应用Token", - "settings.notification.channel.form.gotify_token.label": "应用Token", - "settings.notification.channel.form.gotify_token.tooltip": "应用Token。
请参阅 https://gotify.net/docs/pushmsg", + "settings.notification.channel.form.gotify_url.placeholder": "请输入服务地址", + "settings.notification.channel.form.gotify_url.tooltip": "这是什么?请参阅 https://gotify.net/docs/pushmsg

示例: https://gotify.exmaple.com,不要包含末尾的'/'。", + "settings.notification.channel.form.gotify_token.label": "应用 Token", + "settings.notification.channel.form.gotify_token.placeholder": "请输入应用 Token", + "settings.notification.channel.form.gotify_token.tooltip": "这是什么?请参阅 https://gotify.net/docs/pushmsg", + "settings.notification.channel.form.gotify_priority.label": "消息优先级(可选)", "settings.notification.channel.form.gotify_priority.placeholder": "请输入消息优先级", - "settings.notification.channel.form.gotify_priority.label": "消息优先级", - "settings.notification.channel.form.gotify_priority.tooltip": "消息优先级, 可以设置为1作为默认值。
请参阅 https://gotify.net/docs/pushmsg
https://github.com/gotify/android/issues/18#issuecomment-437403888", - "settings.notification.channel.form.gotify_priority.error.gte0": "消息优先级需要大于等于0", + "settings.notification.channel.form.gotify_priority.tooltip": "这是什么?请参阅 https://gotify.net/docs/pushmsghttps://github.com/gotify/android/issues/18#issuecomment-437403888", + "settings.notification.channel.form.gotify_priority.error.gte0": "消息优先级需要大于等于 0", "settings.notification.channel.form.lark_webhook_url.label": "机器人 Webhook 地址", "settings.notification.channel.form.lark_webhook_url.placeholder": "请输入机器人 Webhook 地址", "settings.notification.channel.form.lark_webhook_url.tooltip": "这是什么?请参阅 https://www.feishu.cn/hc/zh-CN/articles/807992406756", - "settings.notification.channel.form.pushplus_token.placeholder": "请输入Token", "settings.notification.channel.form.pushplus_token.label": "Token", - "settings.notification.channel.form.pushplus_token.tooltip": "请参阅 https://www.pushplus.plus/push1.html", + "settings.notification.channel.form.pushplus_token.placeholder": "请输入 Token", + "settings.notification.channel.form.pushplus_token.tooltip": "这是什么?请参阅 https://www.pushplus.plus/push1.html", "settings.notification.channel.form.serverchan_url.label": "服务器地址", "settings.notification.channel.form.serverchan_url.placeholder": "请输入服务器地址(形如: https://sctapi.ftqq.com/*****.send)", "settings.notification.channel.form.serverchan_url.tooltip": "这是什么?请参阅 https://sct.ftqq.com/forward", From 6ff738144aa34215ef12b1a945145de4304f3957 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 3 Apr 2025 20:33:58 +0800 Subject: [PATCH 02/22] fix: #585 #586 --- .../core/deployer/providers/volcengine-alb/volcengine_alb.go | 2 +- .../core/deployer/providers/volcengine-clb/volcengine_clb.go | 2 +- .../workflow/node/DeployNodeConfigFormVolcEngineALBConfig.tsx | 2 +- .../workflow/node/DeployNodeConfigFormVolcEngineCLBConfig.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go b/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go index 0c6ba1b4..ceba0952 100644 --- a/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go +++ b/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go @@ -182,7 +182,7 @@ func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId str return errors.New("config `listenerId` is required") } - if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, cloudCertId); err != nil { + if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil { return err } diff --git a/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go b/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go index 37481a3f..752b0f85 100644 --- a/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go +++ b/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go @@ -178,7 +178,7 @@ func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId str return errors.New("config `listenerId` is required") } - if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, cloudCertId); err != nil { + if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil { return err } diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineALBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineALBConfig.tsx index d831fd7f..348f4d8d 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineALBConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineALBConfig.tsx @@ -113,7 +113,7 @@ const DeployNodeConfigFormVolcEngineALBConfig = ({ } diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCLBConfig.tsx index c3ddfd03..99263044 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCLBConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCLBConfig.tsx @@ -104,7 +104,7 @@ const DeployNodeConfigFormVolcEngineCLBConfig = ({ } From 47c4ba9dd6f9b745d3054882cda9ce85bbf180a1 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sat, 5 Apr 2025 21:23:55 +0800 Subject: [PATCH 03/22] feat(ui): workflow runs deleting warning --- ui/src/components/workflow/WorkflowRuns.tsx | 4 +++- ui/src/i18n/locales/en/nls.workflow.runs.json | 2 ++ ui/src/i18n/locales/zh/nls.workflow.runs.json | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/src/components/workflow/WorkflowRuns.tsx b/ui/src/components/workflow/WorkflowRuns.tsx index 25f3891a..718a7913 100644 --- a/ui/src/components/workflow/WorkflowRuns.tsx +++ b/ui/src/components/workflow/WorkflowRuns.tsx @@ -11,7 +11,7 @@ import { SyncOutlined as SyncOutlinedIcon, } from "@ant-design/icons"; import { useRequest } from "ahooks"; -import { Button, Empty, Modal, Space, Table, type TableProps, Tag, Tooltip, notification } from "antd"; +import { Alert, Button, Empty, Modal, Space, Table, type TableProps, Tag, Tooltip, notification } from "antd"; import dayjs from "dayjs"; import { ClientResponseError } from "pocketbase"; @@ -284,6 +284,8 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { {NotificationContextHolder}
+ } /> + columns={tableColumns} dataSource={tableData} diff --git a/ui/src/i18n/locales/en/nls.workflow.runs.json b/ui/src/i18n/locales/en/nls.workflow.runs.json index c48e54b7..551a1e90 100644 --- a/ui/src/i18n/locales/en/nls.workflow.runs.json +++ b/ui/src/i18n/locales/en/nls.workflow.runs.json @@ -5,6 +5,8 @@ "workflow_run.action.delete": "Delete run", "workflow_run.action.delete.confirm": "Are you sure to delete this run?", + "workflow_run.table.alert": "Attention: The workflow run contains the execution results of each node. Deleting it may trigger re-application or re-deployment of certificates due to the inability to find the previous execution result. Please do not delete unless necessary. It is recommended to keep it for at least 180 days.", + "workflow_run.props.id": "ID", "workflow_run.props.status": "Status", "workflow_run.props.status.pending": "Pending", diff --git a/ui/src/i18n/locales/zh/nls.workflow.runs.json b/ui/src/i18n/locales/zh/nls.workflow.runs.json index ce3ac15d..4f8640e6 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.runs.json +++ b/ui/src/i18n/locales/zh/nls.workflow.runs.json @@ -5,6 +5,8 @@ "workflow_run.action.delete": "删除执行", "workflow_run.action.delete.confirm": "确定要删除此执行吗?请注意此操作仅清除日志历史,但不会影响签发的证书。", + "workflow_run.table.alert": "注意:执行记录中包含工作流各节点的执行结果,删除后可能导致因找不到前次执行结果而触发重新申请或部署证书。如无必要请勿提前删除,建议保留至少 180 天。", + "workflow_run.props.id": "ID", "workflow_run.props.status": "状态", "workflow_run.props.status.pending": "等待执行", From 111ef97d9ce957464d1f67643b2e1d3e35bd7682 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 7 Apr 2025 15:31:20 +0800 Subject: [PATCH 04/22] fix: migration error --- migrations/1742209200_upgrade.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/migrations/1742209200_upgrade.go b/migrations/1742209200_upgrade.go index d2ed7f9d..da509468 100644 --- a/migrations/1742209200_upgrade.go +++ b/migrations/1742209200_upgrade.go @@ -258,15 +258,14 @@ func init() { } type dWorkflowNode struct { - Id string `json:"id"` - Type string `json:"type"` - Name string `json:"name"` - Config map[string]any `json:"config"` - Inputs map[string]any `json:"inputs"` - Outputs map[string]any `json:"outputs"` - Next *dWorkflowNode `json:"next,omitempty"` - Branches []dWorkflowNode `json:"branches,omitempty"` - Validated bool `json:"validated"` + Id string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Inputs []map[string]any `json:"inputs"` + Outputs []map[string]any `json:"outputs"` + Next *dWorkflowNode `json:"next,omitempty"` + Branches []dWorkflowNode `json:"branches,omitempty"` + Validated bool `json:"validated"` } for _, workflowRun := range workflowRuns { From 37b9ae30e2259b44578d566021bedbd79285c541 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 8 Apr 2025 09:41:16 +0800 Subject: [PATCH 05/22] fix: #595 --- .../core/deployer/providers/gcore-cdn/gcore_cdn.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go b/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go index a4d1c33e..c0a9e5ba 100644 --- a/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go +++ b/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go @@ -100,9 +100,15 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe SSlEnabled: true, SSLData: int(updateResourceCertId), ProxySSLEnabled: getResourceResp.ProxySSLEnabled, - ProxySSLCA: &getResourceResp.ProxySSLCA, - ProxySSLData: &getResourceResp.ProxySSLData, - Options: getResourceResp.Options, + } + if getResourceResp.ProxySSLCA != 0 { + updateResourceReq.ProxySSLCA = &getResourceResp.ProxySSLCA + } + if getResourceResp.ProxySSLData != 0 { + updateResourceReq.ProxySSLData = &getResourceResp.ProxySSLData + } + if getResourceResp.Options != nil { + updateResourceReq.Options = getResourceResp.Options } updateResourceResp, err := d.sdkClient.Update(context.TODO(), d.config.ResourceId, updateResourceReq) d.logger.Debug("sdk request 'resources.Update'", slog.Int64("resourceId", d.config.ResourceId), slog.Any("request", updateResourceReq), slog.Any("response", updateResourceResp)) From 02dd11f1963bad6416fbaa6a2d05a756f317e052 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 8 Apr 2025 10:15:00 +0800 Subject: [PATCH 06/22] chore(ui): improve i18n --- migrations/1742209200_upgrade.go | 1 + ui/src/domain/provider.ts | 10 +++++----- ui/src/i18n/locales/en/nls.provider.json | 12 ++++++------ ui/src/i18n/locales/zh/nls.provider.json | 12 ++++++------ 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/migrations/1742209200_upgrade.go b/migrations/1742209200_upgrade.go index da509468..5cda0c35 100644 --- a/migrations/1742209200_upgrade.go +++ b/migrations/1742209200_upgrade.go @@ -261,6 +261,7 @@ func init() { Id string `json:"id"` Type string `json:"type"` Name string `json:"name"` + Config map[string]any `json:"config"` Inputs []map[string]any `json:"inputs"` Outputs []map[string]any `json:"outputs"` Next *dWorkflowNode `json:"next,omitempty"` diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 74296917..3546d71e 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -418,7 +418,7 @@ export const deployProvidersMap: Map Date: Tue, 8 Apr 2025 16:44:10 +0800 Subject: [PATCH 07/22] feat(ui): builtin providers tag --- .../provider/AccessProviderSelect.tsx | 35 ++++++++++--------- ui/src/i18n/locales/en/nls.access.json | 3 +- ui/src/i18n/locales/zh/nls.access.json | 3 +- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/ui/src/components/provider/AccessProviderSelect.tsx b/ui/src/components/provider/AccessProviderSelect.tsx index fbb4099f..79f1539e 100644 --- a/ui/src/components/provider/AccessProviderSelect.tsx +++ b/ui/src/components/provider/AccessProviderSelect.tsx @@ -50,26 +50,27 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid
- + {t(provider.name)} - {showOptionTags && ( -
- - {t("access.props.provider.usage.dns")} - - - {t("access.props.provider.usage.hosting")} - - - {t("access.props.provider.usage.ca")} - - - {t("access.props.provider.usage.notification")} - -
- )} +
+ + {t("access.props.provider.builtin")} + + + {t("access.props.provider.usage.dns")} + + + {t("access.props.provider.usage.hosting")} + + + {t("access.props.provider.usage.ca")} + + + {t("access.props.provider.usage.notification")} + +
); }; diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index 791b81fc..e474517a 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -17,6 +17,7 @@ "access.props.provider.usage.hosting": "Hosting provider", "access.props.provider.usage.ca": "Certificate authority", "access.props.provider.usage.notification": "Notification channel", + "access.props.provider.builtin": "Built-in", "access.props.range.both_dns_hosting": "Provider", "access.props.range.ca_only": "Certificate authority", "access.props.range.notify_only": "Notification channel", @@ -237,7 +238,7 @@ "access.form.qiniu_secret_key.tooltip": "For more information, see https://portal.qiniu.com/", "access.form.rainyun_api_key.label": "Rain Yun API key", "access.form.rainyun_api_key.placeholder": "Please enter Rain Yun API key", - "access.form.rainyun_api_key.tooltip": "For more information, see https://www.rainyun.com/docs/account/racc/setting", + "access.form.rainyun_api_key.tooltip": "For more information, see https://app.rainyun.com/account/settings/api-key", "access.form.safeline_api_url.label": "SafeLine URL", "access.form.safeline_api_url.placeholder": "Please enter SafeLine URL", "access.form.safeline_api_url.tooltip": "For more information, see https://docs.waf.chaitin.com/en/tutorials/install", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 657c2d68..6022fa15 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -17,6 +17,7 @@ "access.props.provider.usage.hosting": "主机提供商", "access.props.provider.usage.ca": "证书颁发机构", "access.props.provider.usage.notification": "通知渠道", + "access.props.provider.builtin": "内置", "access.props.range.both_dns_hosting": "提供商", "access.props.range.ca_only": "证书颁发机构", "access.props.range.notify_only": "通知渠道", @@ -231,7 +232,7 @@ "access.form.qiniu_secret_key.tooltip": "这是什么?请参阅 https://portal.qiniu.com/", "access.form.rainyun_api_key.label": "雨云 API 密钥", "access.form.rainyun_api_key.placeholder": "请输入雨云 API 密钥", - "access.form.rainyun_api_key.tooltip": "这是什么?请参阅 https://www.rainyun.com/docs/account/racc/setting", + "access.form.rainyun_api_key.tooltip": "这是什么?请参阅 https://app.rainyun.com/account/settings/api-key", "access.form.safeline_api_url.label": "雷池 URL", "access.form.safeline_api_url.placeholder": "请输入雷池 URL", "access.form.safeline_api_url.tooltip": "这是什么?请参阅 https://docs.waf-ce.chaitin.cn/zh/上手指南/安装雷池", From 2525f54dc31731cf58f185d779b647d6d8c47dab Mon Sep 17 00:00:00 2001 From: redzl Date: Tue, 8 Apr 2025 18:06:51 +0800 Subject: [PATCH 08/22] =?UTF-8?q?=E8=A7=A3=E5=86=B3=E8=85=BE=E8=AE=AF?= =?UTF-8?q?=E4=BA=91ECDN=E9=83=A8=E7=BD=B2=E6=8A=A5=E9=94=99=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ECDN部署的时候报错:failed to execute sdk request 'ssl.DeployCertificateInstance':[TencentCloudSDKError] Code=FailedOperation.CertificateHostResourceTypeInvalid, Message=云资源类型无效。 经排查'ssl.DeployCertificateInstance接口的ResourceType不支持ecdn类型,ecdn和cdn都需要传入cdn --- .../deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go b/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go index bd2aebe2..beb5a043 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go @@ -107,7 +107,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe // REF: https://cloud.tencent.com/document/product/400/91667 deployCertificateInstanceReq := tcssl.NewDeployCertificateInstanceRequest() deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) - deployCertificateInstanceReq.ResourceType = common.StringPtr("ecdn") + deployCertificateInstanceReq.ResourceType = common.StringPtr("cdn") deployCertificateInstanceReq.Status = common.Int64Ptr(1) deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(instanceIds) deployCertificateInstanceResp, err := d.sdkClients.SSL.DeployCertificateInstance(deployCertificateInstanceReq) From 25bd17dc6e25d3e3c800547ca3f3c1ba7b3e01a8 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 8 Apr 2025 21:53:05 +0800 Subject: [PATCH 09/22] feat: add rainyun ssl center uploader --- .../providers/1panel-ssl/1panel_ssl.go | 6 +- .../rainyun-sslcenter/rainyun_sslcenter.go | 169 ++++++++++++++++++ .../rainyun_sslcenter_test.go | 67 +++++++ .../providers/ucloud-ussl/ucloud_ussl.go | 6 +- internal/pkg/vendors/rainyun-sdk/api.go | 30 ++++ internal/pkg/vendors/rainyun-sdk/client.go | 74 ++++++++ internal/pkg/vendors/rainyun-sdk/models.go | 83 +++++++++ 7 files changed, 429 insertions(+), 6 deletions(-) create mode 100644 internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go create mode 100644 internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter_test.go create mode 100644 internal/pkg/vendors/rainyun-sdk/api.go create mode 100644 internal/pkg/vendors/rainyun-sdk/client.go create mode 100644 internal/pkg/vendors/rainyun-sdk/models.go diff --git a/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go b/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go index ee00c06a..4eeec679 100644 --- a/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go +++ b/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go @@ -58,7 +58,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { // 遍历证书列表,避免重复上传 - if res, err := u.getExistCert(ctx, certPem, privkeyPem); err != nil { + if res, err := u.getCertIfExists(ctx, certPem, privkeyPem); err != nil { return nil, err } else if res != nil { u.logger.Info("ssl certificate already exists") @@ -82,7 +82,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe } // 遍历证书列表,获取刚刚上传证书 ID - if res, err := u.getExistCert(ctx, certPem, privkeyPem); err != nil { + if res, err := u.getCertIfExists(ctx, certPem, privkeyPem); err != nil { return nil, err } else if res == nil { return nil, fmt.Errorf("no ssl certificate found, may be upload failed (code: %d, message: %s)", uploadWebsiteSSLResp.GetCode(), uploadWebsiteSSLResp.GetMessage()) @@ -91,7 +91,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe } } -func (u *UploaderProvider) getExistCert(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { searchWebsiteSSLPageNumber := int32(1) searchWebsiteSSLPageSize := int32(100) for { diff --git a/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go new file mode 100644 index 00000000..f2ee4bde --- /dev/null +++ b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go @@ -0,0 +1,169 @@ +package rainyunsslcenter + +import ( + "context" + "errors" + "fmt" + "log/slog" + "strings" + + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/uploader" + "github.com/usual2970/certimate/internal/pkg/utils/certutil" + rainyunsdk "github.com/usual2970/certimate/internal/pkg/vendors/rainyun-sdk" +) + +type UploaderConfig struct { + // 雨云 API 密钥。 + ApiKey string `json:"ApiKey"` +} + +type UploaderProvider struct { + config *UploaderConfig + logger *slog.Logger + sdkClient *rainyunsdk.Client +} + +var _ uploader.Uploader = (*UploaderProvider)(nil) + +func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.ApiKey) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + return &UploaderProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, nil +} + +func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { + if logger == nil { + u.logger = slog.Default() + } else { + u.logger = logger + } + return u +} + +func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { + if res, err := u.getCertIfExists(ctx, certPem); err != nil { + return nil, err + } else if res != nil { + u.logger.Info("ssl certificate already exists") + return res, nil + } + + // SSL 证书上传 + // REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046 + sslCenterCreateReq := &rainyunsdk.SslCenterCreateRequest{ + Cert: certPem, + Key: privkeyPem, + } + sslCenterCreateResp, err := u.sdkClient.SslCenterCreate(sslCenterCreateReq) + u.logger.Debug("sdk request 'sslcenter.Create'", slog.Any("request", sslCenterCreateReq), slog.Any("response", sslCenterCreateResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.Create'") + } + + if res, err := u.getCertIfExists(ctx, certPem); err != nil { + return nil, err + } else if res == nil { + return nil, errors.New("rainyun sslcenter: no certificate found") + } else { + return res, nil + } +} + +func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) { + // 解析证书内容 + certX509, err := certutil.ParseCertificateFromPEM(certPem) + if err != nil { + return nil, err + } + + // 遍历 SSL 证书列表,避免重复上传 + // REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046 + // REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943048 + sslCenterListPage := int32(1) + sslCenterListPerPage := int32(100) + for { + sslCenterListReq := &rainyunsdk.SslCenterListRequest{ + Filters: &rainyunsdk.SslCenterListFilters{ + Domain: &certX509.Subject.CommonName, + }, + Page: &sslCenterListPage, + PerPage: &sslCenterListPerPage, + } + sslCenterListResp, err := u.sdkClient.SslCenterList(sslCenterListReq) + u.logger.Debug("sdk request 'sslcenter.List'", slog.Any("request", sslCenterListReq), slog.Any("response", sslCenterListResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.List'") + } + + if sslCenterListResp.Data != nil && sslCenterListResp.Data.Records != nil { + for _, sslItem := range sslCenterListResp.Data.Records { + // 先对比证书的多域名 + if sslItem.Domain != strings.Join(certX509.DNSNames, ", ") { + continue + } + + // 再对比证书的有效期 + if sslItem.StartDate != certX509.NotBefore.Unix() || sslItem.ExpireDate != certX509.NotAfter.Unix() { + continue + } + + // 最后对比证书内容 + sslCenterGetResp, err := u.sdkClient.SslCenterGet(sslItem.ID) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.Get'") + } + + var isSameCert bool + if sslCenterGetResp.Data != nil { + if sslCenterGetResp.Data.Cert == certPem { + isSameCert = true + } else { + oldCertX509, err := certutil.ParseCertificateFromPEM(sslCenterGetResp.Data.Cert) + if err != nil { + continue + } + + isSameCert = certutil.EqualCertificate(certX509, oldCertX509) + } + } + + // 如果已存在相同证书,直接返回 + if isSameCert { + return &uploader.UploadResult{ + CertId: fmt.Sprintf("%d", sslItem.ID), + }, nil + } + } + } + + if sslCenterListResp.Data == nil || len(sslCenterListResp.Data.Records) < int(sslCenterListPerPage) { + break + } else { + sslCenterListPage++ + } + } + + return nil, nil +} + +func createSdkClient(apiKey string) (*rainyunsdk.Client, error) { + if apiKey == "" { + return nil, errors.New("invalid rainyun api key") + } + + client := rainyunsdk.NewClient(apiKey) + return client, nil +} diff --git a/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter_test.go b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter_test.go new file mode 100644 index 00000000..41619401 --- /dev/null +++ b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter_test.go @@ -0,0 +1,67 @@ +package rainyunsslcenter_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/rainyun-sslcenter" +) + +var ( + fInputCertPath string + fInputKeyPath string + fApiKey string +) + +func init() { + argsPrefix := "CERTIMATE_UPLOADER_RAINYUNSSLCENTER_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") +} + +/* +Shell command to run this test: + + go test -v ./rainyun_sslcenter_test.go -args \ + --CERTIMATE_UPLOADER_RAINYUNSSLCENTER_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_UPLOADER_RAINYUNSSLCENTER_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_UPLOADER_RAINYUNSSLCENTER_APIKEY="your-api-key" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("APIKEY: %v", fApiKey), + }, "\n")) + + uploader, err := provider.NewUploader(&provider.UploaderConfig{ + ApiKey: fApiKey, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + sres, _ := json.Marshal(res) + t.Logf("ok: %s", string(sres)) + }) +} diff --git a/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go b/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go index b8639bf3..4649c454 100644 --- a/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go +++ b/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go @@ -89,10 +89,10 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe u.logger.Debug("sdk request 'ussl.UploadNormalCertificate'", slog.Any("request", uploadNormalCertificateReq), slog.Any("response", uploadNormalCertificateResp)) if err != nil { if uploadNormalCertificateResp != nil && uploadNormalCertificateResp.GetRetCode() == 80035 { - if res, err := u.getExistCert(ctx, certPem); err != nil { + if res, err := u.getCertIfExists(ctx, certPem); err != nil { return nil, err } else if res == nil { - return nil, errors.New("no certificate found") + return nil, errors.New("ucloud ssl: no certificate found") } else { u.logger.Info("ssl certificate already exists") return res, nil @@ -112,7 +112,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe }, nil } -func (u *UploaderProvider) getExistCert(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPem) if err != nil { diff --git a/internal/pkg/vendors/rainyun-sdk/api.go b/internal/pkg/vendors/rainyun-sdk/api.go new file mode 100644 index 00000000..50f3279d --- /dev/null +++ b/internal/pkg/vendors/rainyun-sdk/api.go @@ -0,0 +1,30 @@ +package rainyunsdk + +import ( + "fmt" + "net/http" +) + +func (c *Client) SslCenterList(req *SslCenterListRequest) (*SslCenterListResponse, error) { + resp := &SslCenterListResponse{} + err := c.sendRequestWithResult(http.MethodGet, "/product/sslcenter", req, resp) + return resp, err +} + +func (c *Client) SslCenterGet(id int32) (*SslCenterGetResponse, error) { + resp := &SslCenterGetResponse{} + err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/product/sslcenter/%d", id), nil, resp) + return resp, err +} + +func (c *Client) SslCenterCreate(req *SslCenterCreateRequest) (*SslCenterCreateResponse, error) { + resp := &SslCenterCreateResponse{} + err := c.sendRequestWithResult(http.MethodPost, "/product/sslcenter/", req, resp) + return resp, err +} + +func (c *Client) RcdnInstanceSslBind(id int32, req *RcdnInstanceSslBindRequest) (*RcdnInstanceSslBindResponse, error) { + resp := &RcdnInstanceSslBindResponse{} + err := c.sendRequestWithResult(http.MethodPost, fmt.Sprintf("/product/rcdn/instance/%d/ssl_bind", id), req, resp) + return resp, err +} diff --git a/internal/pkg/vendors/rainyun-sdk/client.go b/internal/pkg/vendors/rainyun-sdk/client.go new file mode 100644 index 00000000..b35f38e0 --- /dev/null +++ b/internal/pkg/vendors/rainyun-sdk/client.go @@ -0,0 +1,74 @@ +package rainyunsdk + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-resty/resty/v2" +) + +type Client struct { + apiKey string + + client *resty.Client +} + +func NewClient(apiKey string) *Client { + client := resty.New() + + return &Client{ + apiKey: apiKey, + client: client, + } +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c.client.SetTimeout(timeout) + return c +} + +func (c *Client) sendRequest(method string, path string, params interface{}) (*resty.Response, error) { + req := c.client.R().SetHeader("x-api-key", c.apiKey) + req.Method = method + req.URL = "https://api.v2.rainyun.com" + path + if strings.EqualFold(method, http.MethodGet) { + if params != nil { + jsonb, _ := json.Marshal(params) + req = req.SetQueryParam("options", string(jsonb)) + } + } else { + req = req. + SetHeader("Content-Type", "application/json"). + SetBody(params) + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("rainyun api error: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("rainyun api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + } + + return resp, nil +} + +func (c *Client) sendRequestWithResult(method string, path string, params interface{}, result BaseResponse) error { + resp, err := c.sendRequest(method, path, params) + if err != nil { + if resp != nil { + json.Unmarshal(resp.Body(), &result) + } + return err + } + + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return fmt.Errorf("rainyun api error: failed to parse response: %w", err) + } else if errcode := result.GetCode(); errcode/100 != 2 { + return fmt.Errorf("rainyun api error: %d - %s", errcode, result.GetMessage()) + } + + return nil +} diff --git a/internal/pkg/vendors/rainyun-sdk/models.go b/internal/pkg/vendors/rainyun-sdk/models.go new file mode 100644 index 00000000..bcc44963 --- /dev/null +++ b/internal/pkg/vendors/rainyun-sdk/models.go @@ -0,0 +1,83 @@ +package rainyunsdk + +type BaseResponse interface { + GetCode() int32 + GetMessage() string +} + +type baseResponse struct { + Code *int32 `json:"code,omitempty"` + Message *string `json:"message,omitempty"` +} + +func (r *baseResponse) GetCode() int32 { + if r.Code != nil { + return *r.Code + } + return 0 +} + +func (r *baseResponse) GetMessage() string { + if r.Message != nil { + return *r.Message + } + return "" +} + +type SslCenterListFilters struct { + Domain *string `json:"Domain,omitempty"` +} + +type SslCenterListRequest struct { + Filters *SslCenterListFilters `json:"columnFilters,omitempty"` + Sort []*string `json:"sort,omitempty"` + Page *int32 `json:"page,omitempty"` + PerPage *int32 `json:"perPage,omitempty"` +} + +type SslCenterListResponse struct { + baseResponse + Data *struct { + TotalRecords int32 `json:"TotalRecords"` + Records []*struct { + ID int32 `json:"ID"` + UID int32 `json:"UID"` + Domain string `json:"Domain"` + Issuer string `json:"Issuer"` + StartDate int64 `json:"StartDate"` + ExpireDate int64 `json:"ExpDate"` + UploadTime int64 `json:"UploadTime"` + } `json:"Records"` + } `json:"data,omitempty"` +} + +type SslCenterGetResponse struct { + baseResponse + Data *struct { + Cert string `json:"Cert"` + Key string `json:"Key"` + Domain string `json:"DomainName"` + Issuer string `json:"Issuer"` + StartDate int64 `json:"StartDate"` + ExpireDate int64 `json:"ExpDate"` + RemainDays int32 `json:"RemainDays"` + } `json:"data,omitempty"` +} + +type SslCenterCreateRequest struct { + Cert string `json:"cert"` + Key string `json:"key"` +} + +type SslCenterCreateResponse struct { + baseResponse +} + +type RcdnInstanceSslBindRequest struct { + CertId int32 `json:"cert_id"` + Domains []string `json:"domains"` +} + +type RcdnInstanceSslBindResponse struct { + baseResponse +} From 3cebe51796bf7480b3f95a4107c80b8298e25b24 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 8 Apr 2025 21:53:16 +0800 Subject: [PATCH 10/22] feat: add rainyun rcdn deployer --- internal/deployer/providers.go | 22 ++++ internal/domain/provider.go | 1 + .../providers/rainyun-rcdn/rainyun_rcdn.go | 102 ++++++++++++++++++ .../rainyun-rcdn/rainyun_rcdn_test.go | 75 +++++++++++++ internal/pkg/utils/maputil/getter.go | 18 ++++ .../workflow/node/DeployNodeConfigForm.tsx | 3 + .../DeployNodeConfigFormGcoreCDNConfig.tsx | 4 +- .../DeployNodeConfigFormRainYunRCDNConfig.tsx | 80 ++++++++++++++ ui/src/domain/provider.ts | 4 +- ui/src/i18n/locales/en/nls.provider.json | 1 + .../i18n/locales/en/nls.workflow.nodes.json | 6 ++ ui/src/i18n/locales/zh/nls.provider.json | 1 + .../i18n/locales/zh/nls.workflow.nodes.json | 8 +- 13 files changed, 321 insertions(+), 4 deletions(-) create mode 100644 internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go create mode 100644 internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn_test.go create mode 100644 ui/src/components/workflow/node/DeployNodeConfigFormRainYunRCDNConfig.tsx diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index fc3c7a37..292c34f7 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -49,6 +49,7 @@ import ( pLocal "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local" pQiniuCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn" pQiniuPili "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-pili" + pRainYunRCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/rainyun-rcdn" pSafeLine "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/safeline" pSSH "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh" pTencentCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn" @@ -681,6 +682,27 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { } } + case domain.DeployProviderTypeRainYunRCDN: + { + access := domain.AccessConfigForRainYun{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + switch options.Provider { + case domain.DeployProviderTypeTencentCloudCDN: + deployer, err := pRainYunRCDN.NewDeployer(&pRainYunRCDN.DeployerConfig{ + ApiKey: access.ApiKey, + InstanceId: maputil.GetInt32(options.ProviderDeployConfig, "instanceId"), + Domain: maputil.GetString(options.ProviderDeployConfig, "domain"), + }) + return deployer, err + + default: + break + } + } + case domain.DeployProviderTypeSafeLine: { access := domain.AccessConfigForSafeLine{} diff --git a/internal/domain/provider.go b/internal/domain/provider.go index d8726034..18ee73b9 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -186,6 +186,7 @@ const ( DeployProviderTypeQiniuCDN = DeployProviderType("qiniu-cdn") DeployProviderTypeQiniuKodo = DeployProviderType("qiniu-kodo") DeployProviderTypeQiniuPili = DeployProviderType("qiniu-pili") + DeployProviderTypeRainYunRCDN = DeployProviderType("rainyun-rcdn") DeployProviderTypeSafeLine = DeployProviderType("safeline") DeployProviderTypeSSH = DeployProviderType("ssh") DeployProviderTypeTencentCloudCDN = DeployProviderType("tencentcloud-cdn") diff --git a/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go b/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go new file mode 100644 index 00000000..d2b56e07 --- /dev/null +++ b/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go @@ -0,0 +1,102 @@ +package rainyunrcdn + +import ( + "context" + "errors" + "log/slog" + "strconv" + + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/rainyun-sslcenter" + rainyunsdk "github.com/usual2970/certimate/internal/pkg/vendors/rainyun-sdk" +) + +type DeployerConfig struct { + // 雨云 API 密钥。 + ApiKey string `json:"apiKey"` + // RCDN 实例 ID。 + InstanceId int32 `json:"instanceId"` + // 加速域名(支持泛域名)。 + Domain string `json:"domain"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *rainyunsdk.Client + sslUploader uploader.Uploader +} + +var _ deployer.Deployer = (*DeployerProvider)(nil) + +func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.ApiKey) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ + ApiKey: config.ApiKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } + + return &DeployerProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + sslUploader: uploader, + }, nil +} + +func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { + if logger == nil { + d.logger = slog.Default() + } else { + d.logger = logger + } + d.sslUploader.WithLogger(logger) + return d +} + +func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { + // 上传证书到 SSL 证书 + upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem) + if err != nil { + return nil, xerrors.Wrap(err, "failed to upload certificate file") + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + + // RCDN SSL 绑定域名 + // REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-184214120 + certId, _ := strconv.Atoi(upres.CertId) + rcdnInstanceSslBindReq := &rainyunsdk.RcdnInstanceSslBindRequest{ + CertId: int32(certId), + Domains: []string{d.config.Domain}, + } + rcdnInstanceSslBindResp, err := d.sdkClient.RcdnInstanceSslBind(d.config.InstanceId, rcdnInstanceSslBindReq) + d.logger.Debug("sdk request 'rcdn.InstanceSslBind'", slog.Any("instanceId", d.config.InstanceId), slog.Any("request", rcdnInstanceSslBindReq), slog.Any("response", rcdnInstanceSslBindResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'rcdn.InstanceSslBind'") + } + + return &deployer.DeployResult{}, nil +} + +func createSdkClient(apiKey string) (*rainyunsdk.Client, error) { + if apiKey == "" { + return nil, errors.New("invalid rainyun api key") + } + + client := rainyunsdk.NewClient(apiKey) + return client, nil +} diff --git a/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn_test.go b/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn_test.go new file mode 100644 index 00000000..7c3e90f7 --- /dev/null +++ b/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn_test.go @@ -0,0 +1,75 @@ +package rainyunrcdn_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/rainyun-rcdn" +) + +var ( + fInputCertPath string + fInputKeyPath string + fApiKey string + fInstanceId int64 + fDomain string +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_RAINYUNRCDN_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") + flag.Int64Var(&fInstanceId, argsPrefix+"INSTANCEID", 0, "") + flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") +} + +/* +Shell command to run this test: + + go test -v ./ucloud_ucdn_test.go -args \ + --CERTIMATE_DEPLOYER_RAINYUNRCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_RAINYUNRCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_RAINYUNRCDN_APIKEY="your-api-key" \ + --CERTIMATE_DEPLOYER_RAINYUNRCDN_INSTANCEID="your-rcdn-instance-id" \ + --CERTIMATE_DEPLOYER_RAINYUNRCDN_DOMAIN="example.com" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("APIKEY: %v", fApiKey), + fmt.Sprintf("INSTANCEID: %v", fInstanceId), + fmt.Sprintf("DOMAIN: %v", fDomain), + }, "\n")) + + deployer, err := provider.NewDeployer(&provider.DeployerConfig{ + PrivateKey: fApiKey, + InstanceId: fInstanceId, + Domain: fDomain, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/internal/pkg/utils/maputil/getter.go b/internal/pkg/utils/maputil/getter.go index 9ba22875..c1126496 100644 --- a/internal/pkg/utils/maputil/getter.go +++ b/internal/pkg/utils/maputil/getter.go @@ -74,6 +74,18 @@ func GetOrDefaultInt32(dict map[string]any, key string, defaultValue int32) int3 } } + if result, ok := value.(int64); ok { + if result != 0 { + return int32(result) + } + } + + if result, ok := value.(int); ok { + if result != 0 { + return int32(result) + } + } + // 兼容字符串类型的值 if str, ok := value.(string); ok { if result, err := strconv.ParseInt(str, 10, 32); err == nil { @@ -126,6 +138,12 @@ func GetOrDefaultInt64(dict map[string]any, key string, defaultValue int64) int6 } } + if result, ok := value.(int); ok { + if result != 0 { + return int64(result) + } + } + // 兼容字符串类型的值 if str, ok := value.(string); ok { if result, err := strconv.ParseInt(str, 10, 64); err == nil { diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index 33d1012c..cd1a0fe7 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -56,6 +56,7 @@ import DeployNodeConfigFormLocalConfig from "./DeployNodeConfigFormLocalConfig"; import DeployNodeConfigFormQiniuCDNConfig from "./DeployNodeConfigFormQiniuCDNConfig"; import DeployNodeConfigFormQiniuKodoConfig from "./DeployNodeConfigFormQiniuKodoConfig"; import DeployNodeConfigFormQiniuPiliConfig from "./DeployNodeConfigFormQiniuPiliConfig"; +import DeployNodeConfigFormRainYunRCDNConfig from "./DeployNodeConfigFormRainYunRCDNConfig"; import DeployNodeConfigFormSafeLineConfig from "./DeployNodeConfigFormSafeLineConfig"; import DeployNodeConfigFormSSHConfig from "./DeployNodeConfigFormSSHConfig.tsx"; import DeployNodeConfigFormTencentCloudCDNConfig from "./DeployNodeConfigFormTencentCloudCDNConfig.tsx"; @@ -251,6 +252,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOY_PROVIDERS.QINIU_PILI: return ; + case DEPLOY_PROVIDERS.RAINYUN_RCDN: + return ; case DEPLOY_PROVIDERS.SAFELINE: return ; case DEPLOY_PROVIDERS.SSH: diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormGcoreCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormGcoreCDNConfig.tsx index 00dc48dd..c06087de 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormGcoreCDNConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormGcoreCDNConfig.tsx @@ -4,7 +4,7 @@ import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; type DeployNodeConfigFormGcoreCDNConfigFieldValues = Nullish<{ - resourceId?: string | number; + resourceId: string | number; }>; export type DeployNodeConfigFormGcoreCDNConfigProps = { @@ -27,7 +27,7 @@ const DeployNodeConfigFormGcoreCDNConfig = ({ form: formInst, formName, disabled const formSchema = z.object({ resourceId: z.union([z.string(), z.number()]).refine((v) => { return /^\d+$/.test(v + "") && +v > 0; - }, t("workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder")), + }, t("workflow_node.deploy.form.gcore_cdn_resource_id.placeholder")), }); const formRule = createSchemaFieldRule(formSchema); diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormRainYunRCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormRainYunRCDNConfig.tsx new file mode 100644 index 00000000..b13ad5cb --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormRainYunRCDNConfig.tsx @@ -0,0 +1,80 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { validDomainName } from "@/utils/validators"; + +type DeployNodeConfigFormRainYunRCDNConfigFieldValues = Nullish<{ + instanceId: string | number; + domain: string; +}>; + +export type DeployNodeConfigFormRainYunRCDNConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormRainYunRCDNConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormRainYunRCDNConfigFieldValues) => void; +}; + +const initFormModel = (): DeployNodeConfigFormRainYunRCDNConfigFieldValues => { + return { + instanceId: "", + }; +}; + +const DeployNodeConfigFormRainYunRCDNConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: DeployNodeConfigFormRainYunRCDNConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + instanceId: z.union([z.string(), z.number()]).refine((v) => { + return /^\d+$/.test(v + "") && +v > 0; + }, t("workflow_node.deploy.form.rainyun_rcdn_instance_id.placeholder")), + domain: z + .string({ message: t("workflow_node.deploy.form.rainyun_rcdn_domain.placeholder") }) + .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default DeployNodeConfigFormRainYunRCDNConfig; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 74296917..c101b0e3 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -94,6 +94,7 @@ export const accessProvidersMap: Maphttps://portal.qiniu.com/hub", + "workflow_node.deploy.form.rainyun_rcdn_instance_id.label": "Rain Yun RCDN instance ID", + "workflow_node.deploy.form.rainyun_rcdn_instance_id.placeholder": "Please enter Rain Yun RCDN instance ID", + "workflow_node.deploy.form.rainyun_rcdn_instance_id.tooltip": "For more information, see https://app.rainyun.com/apps/rcdn/list", + "workflow_node.deploy.form.rainyun_rcdn_domain.label": "Rain Yun RCDN domain", + "workflow_node.deploy.form.rainyun_rcdn_domain.placeholder": "Please enter Rain Yun RCDN domain name", + "workflow_node.deploy.form.rainyun_rcdn_domain.tooltip": "For more information, see https://app.rainyun.com/apps/rcdn/list", "workflow_node.deploy.form.safeline_resource_type.label": "Resource type", "workflow_node.deploy.form.safeline_resource_type.placeholder": "Please select resource type", "workflow_node.deploy.form.safeline_resource_type.option.certificate.label": "Certificate", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 3d7c1e58..34ef3fb2 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -91,6 +91,7 @@ "provider.qiniu.kodo": "七牛云 - 对象存储 Kodo", "provider.qiniu.pili": "七牛云 - 视频直播 Pili", "provider.rainyun": "雨云", + "provider.rainyun.rcdn": "雨云 - 雨盾 CDN", "provider.safeline": "雷池", "provider.ssh": "SSH 部署", "provider.sslcom": "SSL.com", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 7338912f..7e50cc5b 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -435,6 +435,12 @@ "workflow_node.deploy.form.qiniu_pili_domain.label": "七牛云视频直播流域名", "workflow_node.deploy.form.qiniu_pili_domain.placeholder": "请输入七牛云视频直播流域名", "workflow_node.deploy.form.qiniu_pili_domain.tooltip": "这是什么?请参阅 https://portal.qiniu.com/hub", + "workflow_node.deploy.form.rainyun_rcdn_instance_id.label": "雨云 RCDN 实例 ID", + "workflow_node.deploy.form.rainyun_rcdn_instance_id.placeholder": "请输入雨云 RCDN 实例 ID", + "workflow_node.deploy.form.rainyun_rcdn_instance_id.tooltip": "这是什么?请参阅 https://app.rainyun.com/apps/rcdn/list", + "workflow_node.deploy.form.rainyun_rcdn_domain.label": "雨云 RCDN 加速域名", + "workflow_node.deploy.form.rainyun_rcdn_domain.placeholder": "请输入雨云 RCDN 加速域名(支持泛域名)", + "workflow_node.deploy.form.rainyun_rcdn_domain.tooltip": "这是什么?请参阅 https://app.rainyun.com/apps/rcdn/list", "workflow_node.deploy.form.safeline_resource_type.label": "证书替换方式", "workflow_node.deploy.form.safeline_resource_type.placeholder": "请选择证书替换方式", "workflow_node.deploy.form.safeline_resource_type.option.certificate.label": "替换指定证书", @@ -639,7 +645,7 @@ "workflow_node.deploy.form.webhook_data_preset.button": "使用预设模板", "workflow_node.deploy.form.strategy_config.label": "执行策略", "workflow_node.deploy.form.skip_on_last_succeeded.label": "重复部署", - "workflow_node.deploy.form.skip_on_last_succeeded.prefix": "当上次部署相同证书已成功时", + "workflow_node.deploy.form.skip_on_last_succeeded.prefix": "当上次部署相同证书成功时,", "workflow_node.deploy.form.skip_on_last_succeeded.suffix": "重新部署。", "workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "跳过", "workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "不跳过", From 4e3f499d7634487e23046443242e0917bac9e300 Mon Sep 17 00:00:00 2001 From: RHQYZ Date: Wed, 9 Apr 2025 10:55:53 +0800 Subject: [PATCH 11/22] chore: github issue templates --- .github/ISSUE_TEMPLATE/3-questions.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/3-questions.yml b/.github/ISSUE_TEMPLATE/3-questions.yml index fd847486..f4918034 100644 --- a/.github/ISSUE_TEMPLATE/3-questions.yml +++ b/.github/ISSUE_TEMPLATE/3-questions.yml @@ -1,6 +1,6 @@ name: "❓ Questions" description: "遇到了困难需要求助? / Have problem in use and need help?" -title: "[Feature] 简要描述你遇到的问题" +title: "简要描述你遇到的问题" body: - type: markdown attributes: @@ -19,6 +19,14 @@ body: 3. Yes, I've read the [documentation](https://docs.certimate.me/en/) and didn't find any similar. 4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly. + - type: input + attributes: + label: 软件版本 / Release Version + description: 请提供 Certimate 的具体版本。 / Please provide the specific version of Certimate. + placeholder: (e.g. v1.0.0) + validations: + required: true + - type: textarea attributes: label: 问题描述 / Description From c5409c78ba7461f16945e28520241250bf38e082 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 9 Apr 2025 23:12:11 +0800 Subject: [PATCH 12/22] refactor: edgio api sdk --- go.mod | 3 +++ .../edgio-applications/edgio_applications.go | 12 ++++++------ internal/pkg/vendors/cdnfly-sdk/api.go | 7 ++++--- .../vendors/edgio-sdk/{applications => }/README.md | 0 .../applications/v7/dtos/cdn_configuration.go | 0 .../applications/v7/dtos/environment.go | 0 .../applications/v7/dtos/property.go | 0 .../applications/v7/dtos/purge.go | 0 .../applications/v7/dtos/tls_cert.go | 0 .../applications/v7/edgio_client.go | 3 +-- .../applications/v7/edgio_client_interface.go | 2 +- .../edgio-sdk/edgio-api@v0.0.0-workspace/go.mod | 3 +++ 12 files changed, 18 insertions(+), 12 deletions(-) rename internal/pkg/vendors/edgio-sdk/{applications => }/README.md (100%) rename internal/pkg/vendors/edgio-sdk/{ => edgio-api@v0.0.0-workspace}/applications/v7/dtos/cdn_configuration.go (100%) rename internal/pkg/vendors/edgio-sdk/{ => edgio-api@v0.0.0-workspace}/applications/v7/dtos/environment.go (100%) rename internal/pkg/vendors/edgio-sdk/{ => edgio-api@v0.0.0-workspace}/applications/v7/dtos/property.go (100%) rename internal/pkg/vendors/edgio-sdk/{ => edgio-api@v0.0.0-workspace}/applications/v7/dtos/purge.go (100%) rename internal/pkg/vendors/edgio-sdk/{ => edgio-api@v0.0.0-workspace}/applications/v7/dtos/tls_cert.go (100%) rename internal/pkg/vendors/edgio-sdk/{ => edgio-api@v0.0.0-workspace}/applications/v7/edgio_client.go (99%) rename internal/pkg/vendors/edgio-sdk/{ => edgio-api@v0.0.0-workspace}/applications/v7/edgio_client_interface.go (94%) create mode 100644 internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/go.mod diff --git a/go.mod b/go.mod index 7c8723af..196b186b 100644 --- a/go.mod +++ b/go.mod @@ -71,6 +71,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect + github.com/Edgio/edgio-api v0.0.0-workspace // indirect github.com/alibabacloud-go/alibabacloud-gateway-fc-util v0.0.7 // indirect github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect @@ -211,6 +212,8 @@ require ( modernc.org/sqlite v1.36.1 // indirect ) +replace github.com/Edgio/edgio-api v0.0.0-workspace => ./internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace + replace gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkcore@v1.0.0 replace gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkclouddns@v1.0.1 diff --git a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go index 8ce6c73d..8dea5555 100644 --- a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go +++ b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go @@ -4,12 +4,12 @@ import ( "context" "log/slog" + edgio "github.com/Edgio/edgio-api/applications/v7" + edgiodtos "github.com/Edgio/edgio-api/applications/v7/dtos" xerrors "github.com/pkg/errors" "github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/utils/certutil" - edgsdk "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7" - edgsdkdtos "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos" ) type DeployerConfig struct { @@ -24,7 +24,7 @@ type DeployerConfig struct { type DeployerProvider struct { config *DeployerConfig logger *slog.Logger - sdkClient *edgsdk.EdgioClient + sdkClient *edgio.EdgioClient } var _ deployer.Deployer = (*DeployerProvider)(nil) @@ -64,7 +64,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe // 上传 TLS 证书 // REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts - uploadTlsCertReq := edgsdkdtos.UploadTlsCertRequest{ + uploadTlsCertReq := edgiodtos.UploadTlsCertRequest{ EnvironmentID: d.config.EnvironmentId, PrimaryCert: privateCertPem, IntermediateCert: intermediateCertPem, @@ -79,7 +79,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe return &deployer.DeployResult{}, nil } -func createSdkClient(clientId, clientSecret string) (*edgsdk.EdgioClient, error) { - client := edgsdk.NewEdgioClient(clientId, clientSecret, "", "") +func createSdkClient(clientId, clientSecret string) (*edgio.EdgioClient, error) { + client := edgio.NewEdgioClient(clientId, clientSecret, "", "") return client, nil } diff --git a/internal/pkg/vendors/cdnfly-sdk/api.go b/internal/pkg/vendors/cdnfly-sdk/api.go index 2387f6d8..263bf2cd 100644 --- a/internal/pkg/vendors/cdnfly-sdk/api.go +++ b/internal/pkg/vendors/cdnfly-sdk/api.go @@ -3,17 +3,18 @@ package cdnflysdk import ( "fmt" "net/http" + "net/url" ) func (c *Client) GetSite(req *GetSiteRequest) (*GetSiteResponse, error) { resp := &GetSiteResponse{} - err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/v1/sites/%s", req.Id), req, resp) + err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/v1/sites/%s", url.PathEscape(req.Id)), req, resp) return resp, err } func (c *Client) UpdateSite(req *UpdateSiteRequest) (*UpdateSiteResponse, error) { resp := &UpdateSiteResponse{} - err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/sites/%s", req.Id), req, resp) + err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/sites/%s", url.PathEscape(req.Id)), req, resp) return resp, err } @@ -25,6 +26,6 @@ func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertif func (c *Client) UpdateCertificate(req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { resp := &UpdateCertificateResponse{} - err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/certs/%s", req.Id), req, resp) + err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/certs/%s", url.PathEscape(req.Id)), req, resp) return resp, err } diff --git a/internal/pkg/vendors/edgio-sdk/applications/README.md b/internal/pkg/vendors/edgio-sdk/README.md similarity index 100% rename from internal/pkg/vendors/edgio-sdk/applications/README.md rename to internal/pkg/vendors/edgio-sdk/README.md diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/cdn_configuration.go b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/cdn_configuration.go similarity index 100% rename from internal/pkg/vendors/edgio-sdk/applications/v7/dtos/cdn_configuration.go rename to internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/cdn_configuration.go diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/environment.go b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/environment.go similarity index 100% rename from internal/pkg/vendors/edgio-sdk/applications/v7/dtos/environment.go rename to internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/environment.go diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/property.go b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/property.go similarity index 100% rename from internal/pkg/vendors/edgio-sdk/applications/v7/dtos/property.go rename to internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/property.go diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/purge.go b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/purge.go similarity index 100% rename from internal/pkg/vendors/edgio-sdk/applications/v7/dtos/purge.go rename to internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/purge.go diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/tls_cert.go b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/tls_cert.go similarity index 100% rename from internal/pkg/vendors/edgio-sdk/applications/v7/dtos/tls_cert.go rename to internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/tls_cert.go diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client.go b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/edgio_client.go similarity index 99% rename from internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client.go rename to internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/edgio_client.go index a03436fc..fb7b7cf7 100644 --- a/internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client.go +++ b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/edgio_client.go @@ -6,9 +6,8 @@ import ( "fmt" "time" + "github.com/Edgio/edgio-api/applications/v7/dtos" "github.com/go-resty/resty/v2" - - "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos" ) // AccessTokenResponse represents the response from the token endpoint. diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client_interface.go b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/edgio_client_interface.go similarity index 94% rename from internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client_interface.go rename to internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/edgio_client_interface.go index ea5fa958..645d73aa 100644 --- a/internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client_interface.go +++ b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/edgio_client_interface.go @@ -3,7 +3,7 @@ package edgio_api import ( "context" - "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos" + "github.com/Edgio/edgio-api/applications/v7/dtos" ) type EdgioClientInterface interface { diff --git a/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/go.mod b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/go.mod new file mode 100644 index 00000000..2c127948 --- /dev/null +++ b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/go.mod @@ -0,0 +1,3 @@ +module github.com/Edgio/edgio-api + +go 1.23.0 From 2a6cc01eedaa08f804081f4b0016b9ff5d85dd7c Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 10 Apr 2025 21:57:22 +0800 Subject: [PATCH 13/22] feat(ui): adjust table scroll width in Dashboard --- ui/src/pages/dashboard/Dashboard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx index 83f8cd47..9915a8a7 100644 --- a/ui/src/pages/dashboard/Dashboard.tsx +++ b/ui/src/pages/dashboard/Dashboard.tsx @@ -285,7 +285,7 @@ const Dashboard = () => { }} pagination={false} rowKey={(record) => record.id} - scroll={{ x: "max(100%, 960px)" }} + scroll={{ x: "max(100%, 720px)" }} size="small" /> From ec0cdf8b96f52e99152d9ad17d2891e587dbe36b Mon Sep 17 00:00:00 2001 From: banto <13196831+banto6@users.noreply.github.com> Date: Fri, 11 Apr 2025 22:55:47 +0800 Subject: [PATCH 14/22] feat(notify): add mattermost --- internal/domain/notify.go | 1 + internal/notify/providers.go | 9 ++ .../providers/mattermost/mattermost.go | 89 +++++++++++++++++++ .../providers/mattermost/mattermost_test.go | 74 +++++++++++++++ .../notification/NotifyChannelEditForm.tsx | 11 ++- .../NotifyChannelEditFormMattermostFields.tsx | 63 +++++++++++++ ui/src/domain/settings.ts | 11 +++ ui/src/i18n/locales/en/nls.common.json | 1 + ui/src/i18n/locales/en/nls.settings.json | 9 ++ ui/src/i18n/locales/zh/nls.common.json | 1 + ui/src/i18n/locales/zh/nls.settings.json | 9 ++ 11 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 internal/pkg/core/notifier/providers/mattermost/mattermost.go create mode 100644 internal/pkg/core/notifier/providers/mattermost/mattermost_test.go create mode 100644 ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx diff --git a/internal/domain/notify.go b/internal/domain/notify.go index 4bc57b85..06c8bbae 100644 --- a/internal/domain/notify.go +++ b/internal/domain/notify.go @@ -14,6 +14,7 @@ const ( NotifyChannelTypeEmail = NotifyChannelType("email") NotifyChannelTypeGotify = NotifyChannelType("gotify") NotifyChannelTypeLark = NotifyChannelType("lark") + NotifyChannelTypeMattermost = NotifyChannelType("mattermost") NotifyChannelTypePushPlus = NotifyChannelType("pushplus") NotifyChannelTypeServerChan = NotifyChannelType("serverchan") NotifyChannelTypeTelegram = NotifyChannelType("telegram") diff --git a/internal/notify/providers.go b/internal/notify/providers.go index 3a7cadf9..6e5a6aef 100644 --- a/internal/notify/providers.go +++ b/internal/notify/providers.go @@ -10,6 +10,7 @@ import ( pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email" pGotify "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify" pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark" + pMattermost "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost" pPushPlus "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus" pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan" pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram" @@ -59,6 +60,14 @@ func createNotifier(channel domain.NotifyChannelType, channelConfig map[string]a WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"), }) + case domain.NotifyChannelTypeMattermost: + return pMattermost.NewNotifier(&pMattermost.NotifierConfig{ + ServerUrl: maputil.GetString(channelConfig, "serverUrl"), + ChannelId: maputil.GetString(channelConfig, "channelId"), + Username: maputil.GetString(channelConfig, "username"), + Password: maputil.GetString(channelConfig, "password"), + }) + case domain.NotifyChannelTypePushPlus: return pPushPlus.NewNotifier(&pPushPlus.NotifierConfig{ Token: maputil.GetString(channelConfig, "token"), diff --git a/internal/pkg/core/notifier/providers/mattermost/mattermost.go b/internal/pkg/core/notifier/providers/mattermost/mattermost.go new file mode 100644 index 00000000..24890794 --- /dev/null +++ b/internal/pkg/core/notifier/providers/mattermost/mattermost.go @@ -0,0 +1,89 @@ +package mattermost + +import ( + "bytes" + "context" + "encoding/json" + "github.com/nikoksr/notify/service/mattermost" + "github.com/usual2970/certimate/internal/pkg/core/notifier" + "io" + "log/slog" + "net/http" +) + +type NotifierConfig struct { + // Mattermost 服务地址。 + ServerUrl string `json:"serverUrl"` + // 频道ID + ChannelId string `json:"channelId"` + // 用户名 + Username string `json:"username"` + // 密码 + Password string `json:"password"` +} + +type NotifierProvider struct { + config *NotifierConfig + logger *slog.Logger +} + +var _ notifier.Notifier = (*NotifierProvider)(nil) + +func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { + if config == nil { + panic("config is nil") + } + + return &NotifierProvider{ + config: config, + }, nil +} + +func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { + if logger == nil { + n.logger = slog.Default() + } else { + n.logger = logger + } + return n +} + +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { + srv := mattermost.New(n.config.ServerUrl) + + if err := srv.LoginWithCredentials(ctx, n.config.Username, n.config.Password); err != nil { + return nil, err + } + + srv.AddReceivers(n.config.ChannelId) + + // 复写消息样式 + srv.PreSend(func(req *http.Request) error { + m := map[string]interface{}{ + "channel_id": n.config.ChannelId, + "props": map[string]interface{}{ + "attachments": []map[string]interface{}{ + { + "title": subject, + "text": message, + }, + }, + }, + } + + if body, err := json.Marshal(m); err != nil { + return err + } else { + req.ContentLength = int64(len(body)) + req.Body = io.NopCloser(bytes.NewReader(body)) + } + + return nil + }) + + if err = srv.Send(ctx, subject, message); err != nil { + return nil, err + } + + return ¬ifier.NotifyResult{}, nil +} diff --git a/internal/pkg/core/notifier/providers/mattermost/mattermost_test.go b/internal/pkg/core/notifier/providers/mattermost/mattermost_test.go new file mode 100644 index 00000000..6db6cc42 --- /dev/null +++ b/internal/pkg/core/notifier/providers/mattermost/mattermost_test.go @@ -0,0 +1,74 @@ +package mattermost_test + +import ( + "context" + "flag" + "fmt" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost" +) + +const ( + mockSubject = "test_subject" + mockMessage = "test_message" +) + +var ( + fServerUrl string + fChannelId string + fUsername string + fPassword string +) + +func init() { + argsPrefix := "CERTIMATE_NOTIFIER_MATTERMOST_" + + flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") + flag.StringVar(&fChannelId, argsPrefix+"CHANNELID", "", "") + flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") + flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") +} + +/* +Shell command to run this test: + + go test -v ./mattermost_test.go -args \ + --CERTIMATE_NOTIFIER_MATTERMOST_SERVERURL="https://example.com/your-server-url" \ + --CERTIMATE_NOTIFIER_MATTERMOST_CHANNELID="your-chanel-id" \ + --CERTIMATE_NOTIFIER_MATTERMOST_USERNAME="your-username" \ + --CERTIMATE_NOTIFIER_MATTERMOST_PASSWORD="your-password" +*/ +func TestNotify(t *testing.T) { + flag.Parse() + + t.Run("Notify", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("SERVERURL: %v", fServerUrl), + fmt.Sprintf("CHANNELID: %v", fChannelId), + fmt.Sprintf("USERNAME: %v", fUsername), + fmt.Sprintf("PASSWORD: %v", fPassword), + }, "\n")) + + notifier, err := provider.NewNotifier(&provider.NotifierConfig{ + ServerUrl: fServerUrl, + ChannelId: fChannelId, + Username: fUsername, + Password: fPassword, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + res, err := notifier.Notify(context.Background(), mockSubject, mockMessage) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/ui/src/components/notification/NotifyChannelEditForm.tsx b/ui/src/components/notification/NotifyChannelEditForm.tsx index aa3f4f12..fb87c5a8 100644 --- a/ui/src/components/notification/NotifyChannelEditForm.tsx +++ b/ui/src/components/notification/NotifyChannelEditForm.tsx @@ -1,8 +1,8 @@ -import { forwardRef, useImperativeHandle, useMemo } from "react"; -import { Form, type FormInstance } from "antd"; +import {forwardRef, useImperativeHandle, useMemo} from "react"; +import {Form, type FormInstance} from "antd"; -import { NOTIFY_CHANNELS, type NotifyChannelsSettingsContent } from "@/domain/settings"; -import { useAntdForm } from "@/hooks"; +import {NOTIFY_CHANNELS, type NotifyChannelsSettingsContent} from "@/domain/settings"; +import {useAntdForm} from "@/hooks"; import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields"; import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields"; @@ -14,6 +14,7 @@ import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServer import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields"; import NotifyChannelEditFormWebhookFields from "./NotifyChannelEditFormWebhookFields"; import NotifyChannelEditFormWeComFields from "./NotifyChannelEditFormWeComFields"; +import NotifyChannelEditFormMattermostFields from "@/components/notification/NotifyChannelEditFormMattermostFields.tsx"; type NotifyChannelEditFormFieldValues = NotifyChannelsSettingsContent[keyof NotifyChannelsSettingsContent]; @@ -54,6 +55,8 @@ const NotifyChannelEditForm = forwardRef; case NOTIFY_CHANNELS.LARK: return ; + case NOTIFY_CHANNELS.MATTERMOST: + return ; case NOTIFY_CHANNELS.PUSHPLUS: return ; case NOTIFY_CHANNELS.SERVERCHAN: diff --git a/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx b/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx new file mode 100644 index 00000000..de8a0b08 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx @@ -0,0 +1,63 @@ +import {useTranslation} from "react-i18next"; +import {Form, Input} from "antd"; +import {createSchemaFieldRule} from "antd-zod"; +import {z} from "zod"; + +const NotifyChannelEditFormMattermostFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + serverUrl: z + .string({ message: t("settings.notification.channel.form.mattermost_server_url.placeholder") }) + .url(t("common.errmsg.url_invalid")), + channelId: z + .string({ message: t("settings.notification.channel.form.mattermost_channel_id.placeholder") }) + .nonempty(t("settings.notification.channel.form.mattermost_channel_id.placeholder")), + username: z + .string({ message: t("settings.notification.channel.form.mattermost_username.placeholder") }) + .nonempty(t("settings.notification.channel.form.mattermost_username.placeholder")), + password: z + .string({ message: t("settings.notification.channel.form.mattermost_password.placeholder") }) + .nonempty(t("settings.notification.channel.form.mattermost_password.placeholder")), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + + + + + + + + + + + + + ); +}; + +export default NotifyChannelEditFormMattermostFields; diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts index 5dc4da80..35d7f8e4 100644 --- a/ui/src/domain/settings.ts +++ b/ui/src/domain/settings.ts @@ -44,6 +44,7 @@ export const NOTIFY_CHANNELS = Object.freeze({ EMAIL: "email", GOTIFY: "gotify", LARK: "lark", + MATTERMOST: "mattermost", PUSHPLUS: "pushplus", SERVERCHAN: "serverchan", TELEGRAM: "telegram", @@ -64,6 +65,7 @@ export type NotifyChannelsSettingsContent = { [NOTIFY_CHANNELS.EMAIL]?: EmailNotifyChannelConfig; [NOTIFY_CHANNELS.GOTIFY]?: GotifyNotifyChannelConfig; [NOTIFY_CHANNELS.LARK]?: LarkNotifyChannelConfig; + [NOTIFY_CHANNELS.MATTERMOST]?: MattermostNotifyChannelConfig; [NOTIFY_CHANNELS.PUSHPLUS]?: PushPlusNotifyChannelConfig; [NOTIFY_CHANNELS.SERVERCHAN]?: ServerChanNotifyChannelConfig; [NOTIFY_CHANNELS.TELEGRAM]?: TelegramNotifyChannelConfig; @@ -106,6 +108,14 @@ export type LarkNotifyChannelConfig = { enabled?: boolean; }; +export type MattermostNotifyChannelConfig = { + serverUrl: string; + channel: string; + username: string; + password: string; + enabled?: boolean; +} + export type PushPlusNotifyChannelConfig = { token: string; enabled?: boolean; @@ -143,6 +153,7 @@ export const notifyChannelsMap: Map = new [NOTIFY_CHANNELS.DINGTALK, "common.notifier.dingtalk"], [NOTIFY_CHANNELS.GOTIFY, "common.notifier.gotify"], [NOTIFY_CHANNELS.LARK, "common.notifier.lark"], + [NOTIFY_CHANNELS.MATTERMOST, "common.notifier.mattermost"], [NOTIFY_CHANNELS.PUSHPLUS, "common.notifier.pushplus"], [NOTIFY_CHANNELS.WECOM, "common.notifier.wecom"], [NOTIFY_CHANNELS.TELEGRAM, "common.notifier.telegram"], diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index c5949d28..2587bdb3 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -41,6 +41,7 @@ "common.notifier.email": "Email", "common.notifier.gotify": "Gotify", "common.notifier.lark": "Lark", + "common.notifier.mattermost": "Mattermost", "common.notifier.pushplus": "PushPlus", "common.notifier.serverchan": "ServerChan", "common.notifier.telegram": "Telegram", diff --git a/ui/src/i18n/locales/en/nls.settings.json b/ui/src/i18n/locales/en/nls.settings.json index d436665c..8b7c15cc 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -66,6 +66,15 @@ "settings.notification.channel.form.lark_webhook_url.label": "Webhook URL", "settings.notification.channel.form.lark_webhook_url.placeholder": "Please enter Webhook URL", "settings.notification.channel.form.lark_webhook_url.tooltip": "For more information, see https://www.feishu.cn/hc/en-US/articles/807992406756", + "settings.notification.channel.form.mattermost_server_url.label": "Service URL", + "settings.notification.channel.form.mattermost_server_url.placeholder": "Please enter service URL", + "settings.notification.channel.form.mattermost_server_url.tooltip": "Example: https://exmaple.com, the protocol needs to be included but the trailing '/' should not be included.", + "settings.notification.channel.form.mattermost_channel_id.label": "Channel ID", + "settings.notification.channel.form.mattermost_channel_id.placeholder": "Please enter channel ID", + "settings.notification.channel.form.mattermost_username.label": "Username", + "settings.notification.channel.form.mattermost_username.placeholder": "Please enter username", + "settings.notification.channel.form.mattermost_password.label": "Password", + "settings.notification.channel.form.mattermost_password.placeholder": "Please enter password", "settings.notification.channel.form.pushplus_token.placeholder": "Please enter Token", "settings.notification.channel.form.pushplus_token.label": "Token", "settings.notification.channel.form.pushplus_token.tooltip": "For more information, see https://www.pushplus.plus/push1.html", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index 726c5ca2..333c5f12 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -41,6 +41,7 @@ "common.notifier.email": "邮件", "common.notifier.gotify": "Gotify", "common.notifier.lark": "飞书", + "common.notifier.mattermost": "Mattermost", "common.notifier.pushplus": "PushPlus推送加", "common.notifier.serverchan": "Server 酱", "common.notifier.telegram": "Telegram", diff --git a/ui/src/i18n/locales/zh/nls.settings.json b/ui/src/i18n/locales/zh/nls.settings.json index c00d158a..bc0b3c7b 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -66,6 +66,15 @@ "settings.notification.channel.form.lark_webhook_url.label": "机器人 Webhook 地址", "settings.notification.channel.form.lark_webhook_url.placeholder": "请输入机器人 Webhook 地址", "settings.notification.channel.form.lark_webhook_url.tooltip": "这是什么?请参阅 https://www.feishu.cn/hc/zh-CN/articles/807992406756", + "settings.notification.channel.form.mattermost_server_url.label": "服务地址", + "settings.notification.channel.form.mattermost_server_url.placeholder": "请输入服务地址", + "settings.notification.channel.form.mattermost_server_url.tooltip": "示例: https://exmaple.com,需要包含协议但不要包含末尾的'/'", + "settings.notification.channel.form.mattermost_channel_id.label": "频道ID", + "settings.notification.channel.form.mattermost_channel_id.placeholder": "请输入频道ID", + "settings.notification.channel.form.mattermost_username.label": "用户名", + "settings.notification.channel.form.mattermost_username.placeholder": "请输入用户名", + "settings.notification.channel.form.mattermost_password.label": "密码", + "settings.notification.channel.form.mattermost_password.placeholder": "请输入密码", "settings.notification.channel.form.pushplus_token.placeholder": "请输入Token", "settings.notification.channel.form.pushplus_token.label": "Token", "settings.notification.channel.form.pushplus_token.tooltip": "请参阅 https://www.pushplus.plus/push1.html", From 48f698e84bb12554f579fc863951478c772edba9 Mon Sep 17 00:00:00 2001 From: banto <13196831+banto6@users.noreply.github.com> Date: Sat, 12 Apr 2025 12:45:03 +0800 Subject: [PATCH 15/22] style: fix code style --- ui/src/components/notification/NotifyChannelEditForm.tsx | 8 ++++---- .../NotifyChannelEditFormMattermostFields.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ui/src/components/notification/NotifyChannelEditForm.tsx b/ui/src/components/notification/NotifyChannelEditForm.tsx index fb87c5a8..8a282229 100644 --- a/ui/src/components/notification/NotifyChannelEditForm.tsx +++ b/ui/src/components/notification/NotifyChannelEditForm.tsx @@ -1,8 +1,8 @@ -import {forwardRef, useImperativeHandle, useMemo} from "react"; -import {Form, type FormInstance} from "antd"; +import { forwardRef, useImperativeHandle, useMemo } from "react"; +import { Form, type FormInstance } from "antd"; -import {NOTIFY_CHANNELS, type NotifyChannelsSettingsContent} from "@/domain/settings"; -import {useAntdForm} from "@/hooks"; +import { NOTIFY_CHANNELS, type NotifyChannelsSettingsContent } from "@/domain/settings"; +import { useAntdForm } from "@/hooks"; import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields"; import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields"; diff --git a/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx b/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx index de8a0b08..3a2c5b8b 100644 --- a/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx +++ b/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx @@ -1,7 +1,7 @@ -import {useTranslation} from "react-i18next"; -import {Form, Input} from "antd"; -import {createSchemaFieldRule} from "antd-zod"; -import {z} from "zod"; +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; const NotifyChannelEditFormMattermostFields = () => { const { t } = useTranslation(); From 6b8dbf52352cd5a6c297819aaedc37fd219e13ae Mon Sep 17 00:00:00 2001 From: imlonghao Date: Sat, 12 Apr 2025 11:07:32 +0800 Subject: [PATCH 16/22] feat: support pushover as notification --- internal/domain/notify.go | 1 + internal/notify/providers.go | 7 ++ .../notifier/providers/pushover/pushover.go | 102 ++++++++++++++++++ .../providers/pushover/pushover_test.go | 62 +++++++++++ .../notification/NotifyChannelEditForm.tsx | 3 + .../NotifyChannelEditFormPushoverFields.tsx | 41 +++++++ ui/src/domain/settings.ts | 9 ++ ui/src/i18n/locales/en/nls.common.json | 1 + ui/src/i18n/locales/en/nls.settings.json | 6 ++ ui/src/i18n/locales/zh/nls.common.json | 1 + ui/src/i18n/locales/zh/nls.settings.json | 6 ++ 11 files changed, 239 insertions(+) create mode 100644 internal/pkg/core/notifier/providers/pushover/pushover.go create mode 100644 internal/pkg/core/notifier/providers/pushover/pushover_test.go create mode 100644 ui/src/components/notification/NotifyChannelEditFormPushoverFields.tsx diff --git a/internal/domain/notify.go b/internal/domain/notify.go index 4bc57b85..7be8b59b 100644 --- a/internal/domain/notify.go +++ b/internal/domain/notify.go @@ -14,6 +14,7 @@ const ( NotifyChannelTypeEmail = NotifyChannelType("email") NotifyChannelTypeGotify = NotifyChannelType("gotify") NotifyChannelTypeLark = NotifyChannelType("lark") + NotifyChannelTypePushover = NotifyChannelType("pushover") NotifyChannelTypePushPlus = NotifyChannelType("pushplus") NotifyChannelTypeServerChan = NotifyChannelType("serverchan") NotifyChannelTypeTelegram = NotifyChannelType("telegram") diff --git a/internal/notify/providers.go b/internal/notify/providers.go index 3a7cadf9..808892b3 100644 --- a/internal/notify/providers.go +++ b/internal/notify/providers.go @@ -10,6 +10,7 @@ import ( pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email" pGotify "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify" pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark" + pPushover "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover" pPushPlus "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus" pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan" pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram" @@ -59,6 +60,12 @@ func createNotifier(channel domain.NotifyChannelType, channelConfig map[string]a WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"), }) + case domain.NotifyChannelTypePushover: + return pPushover.NewNotifier(&pPushover.NotifierConfig{ + Token: maputil.GetString(channelConfig, "token"), + User: maputil.GetString(channelConfig, "user"), + }) + case domain.NotifyChannelTypePushPlus: return pPushPlus.NewNotifier(&pPushPlus.NotifierConfig{ Token: maputil.GetString(channelConfig, "token"), diff --git a/internal/pkg/core/notifier/providers/pushover/pushover.go b/internal/pkg/core/notifier/providers/pushover/pushover.go new file mode 100644 index 00000000..8f84dfd2 --- /dev/null +++ b/internal/pkg/core/notifier/providers/pushover/pushover.go @@ -0,0 +1,102 @@ +package pushover + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + + "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/notifier" +) + +type NotifierConfig struct { + Token string `json:"token"` // 应用 API Token + User string `json:"user"` // 用户/分组 Key +} + +type NotifierProvider struct { + config *NotifierConfig + logger *slog.Logger + // 未来将移除 + httpClient *http.Client +} + +var _ notifier.Notifier = (*NotifierProvider)(nil) + +func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { + if config == nil { + panic("config is nil") + } + + return &NotifierProvider{ + config: config, + httpClient: http.DefaultClient, + }, nil +} + +func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { + if logger == nil { + n.logger = slog.Default() + } else { + n.logger = logger + } + return n +} + +// Notify 发送通知 +// 参考文档:https://pushover.net/api +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { + // 请求体 + reqBody := &struct { + Token string `json:"token"` + User string `json:"user"` + Title string `json:"title"` + Message string `json:"message"` + }{ + Token: n.config.Token, + User: n.config.User, + Title: subject, + Message: message, + } + + // Make request + body, err := json.Marshal(reqBody) + if err != nil { + return nil, errors.Wrap(err, "encode message body") + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + "https://api.pushover.net/1/messages.json", + bytes.NewReader(body), + ) + if err != nil { + return nil, errors.Wrap(err, "create new request") + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + // Send request to pushover service + resp, err := n.httpClient.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "send request to pushover server") + } + defer resp.Body.Close() + + result, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "read response") + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("pushover returned status code %d: %s", resp.StatusCode, string(result)) + } + + return ¬ifier.NotifyResult{}, nil +} diff --git a/internal/pkg/core/notifier/providers/pushover/pushover_test.go b/internal/pkg/core/notifier/providers/pushover/pushover_test.go new file mode 100644 index 00000000..450beac1 --- /dev/null +++ b/internal/pkg/core/notifier/providers/pushover/pushover_test.go @@ -0,0 +1,62 @@ +package pushover_test + +import ( + "context" + "flag" + "fmt" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover" +) + +const ( + mockSubject = "test_subject" + mockMessage = "test_message" +) + +var ( + fToken string + fUser string +) + +func init() { + argsPrefix := "CERTIMATE_NOTIFIER_PUSHOVER_" + flag.StringVar(&fToken, argsPrefix+"TOKEN", "", "") + flag.StringVar(&fUser, argsPrefix+"USER", "", "") +} + +/* +Shell command to run this test: + + go test -v ./pushover_test.go -args \ + --CERTIMATE_NOTIFIER_PUSHOVER_TOKEN="your-pushover-token" \ + --CERTIMATE_NOTIFIER_PUSHOVER_USER="your-pushover-user" \ +*/ +func TestNotify(t *testing.T) { + flag.Parse() + + t.Run("Notify", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("TOKEN: %v", fToken), + }, "\n")) + + notifier, err := provider.NewNotifier(&provider.NotifierConfig{ + Token: fToken, + User: fUser, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + res, err := notifier.Notify(context.Background(), mockSubject, mockMessage) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/ui/src/components/notification/NotifyChannelEditForm.tsx b/ui/src/components/notification/NotifyChannelEditForm.tsx index aa3f4f12..1aa7eb87 100644 --- a/ui/src/components/notification/NotifyChannelEditForm.tsx +++ b/ui/src/components/notification/NotifyChannelEditForm.tsx @@ -9,6 +9,7 @@ import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalk import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields"; import NotifyChannelEditFormGotifyFields from "./NotifyChannelEditFormGotifyFields.tsx"; import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields"; +import NotifyChannelEditFormPushoverFields from "./NotifyChannelEditFormPushoverFields"; import NotifyChannelEditFormPushPlusFields from "./NotifyChannelEditFormPushPlusFields"; import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields"; import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields"; @@ -54,6 +55,8 @@ const NotifyChannelEditForm = forwardRef; case NOTIFY_CHANNELS.LARK: return ; + case NOTIFY_CHANNELS.PUSHOVER: + return ; case NOTIFY_CHANNELS.PUSHPLUS: return ; case NOTIFY_CHANNELS.SERVERCHAN: diff --git a/ui/src/components/notification/NotifyChannelEditFormPushoverFields.tsx b/ui/src/components/notification/NotifyChannelEditFormPushoverFields.tsx new file mode 100644 index 00000000..449c98fa --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormPushoverFields.tsx @@ -0,0 +1,41 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormPushoverFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + token: z + .string({ message: t("settings.notification.channel.form.pushover_token.placeholder") }) + .nonempty(t("settings.notification.channel.form.pushover_token.placeholder")), + user: z + .string({ message: t("settings.notification.channel.form.pushover_user.placeholder") }) + .nonempty(t("settings.notification.channel.form.pushover_user.placeholder")), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + } + > + + + + ); +}; + +export default NotifyChannelEditFormPushoverFields; diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts index 5dc4da80..11633789 100644 --- a/ui/src/domain/settings.ts +++ b/ui/src/domain/settings.ts @@ -44,6 +44,7 @@ export const NOTIFY_CHANNELS = Object.freeze({ EMAIL: "email", GOTIFY: "gotify", LARK: "lark", + PUSHOVER: "pushover", PUSHPLUS: "pushplus", SERVERCHAN: "serverchan", TELEGRAM: "telegram", @@ -64,6 +65,7 @@ export type NotifyChannelsSettingsContent = { [NOTIFY_CHANNELS.EMAIL]?: EmailNotifyChannelConfig; [NOTIFY_CHANNELS.GOTIFY]?: GotifyNotifyChannelConfig; [NOTIFY_CHANNELS.LARK]?: LarkNotifyChannelConfig; + [NOTIFY_CHANNELS.PUSHOVER]?: PushoverNotifyChannelConfig; [NOTIFY_CHANNELS.PUSHPLUS]?: PushPlusNotifyChannelConfig; [NOTIFY_CHANNELS.SERVERCHAN]?: ServerChanNotifyChannelConfig; [NOTIFY_CHANNELS.TELEGRAM]?: TelegramNotifyChannelConfig; @@ -106,6 +108,12 @@ export type LarkNotifyChannelConfig = { enabled?: boolean; }; +export type PushoverNotifyChannelConfig = { + token: string; + user: string; + enabled?: boolean; +}; + export type PushPlusNotifyChannelConfig = { token: string; enabled?: boolean; @@ -143,6 +151,7 @@ export const notifyChannelsMap: Map = new [NOTIFY_CHANNELS.DINGTALK, "common.notifier.dingtalk"], [NOTIFY_CHANNELS.GOTIFY, "common.notifier.gotify"], [NOTIFY_CHANNELS.LARK, "common.notifier.lark"], + [NOTIFY_CHANNELS.PUSHOVER, "common.notifier.pushover"], [NOTIFY_CHANNELS.PUSHPLUS, "common.notifier.pushplus"], [NOTIFY_CHANNELS.WECOM, "common.notifier.wecom"], [NOTIFY_CHANNELS.TELEGRAM, "common.notifier.telegram"], diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index c5949d28..5ca42847 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -41,6 +41,7 @@ "common.notifier.email": "Email", "common.notifier.gotify": "Gotify", "common.notifier.lark": "Lark", + "common.notifier.pushover": "Pushover", "common.notifier.pushplus": "PushPlus", "common.notifier.serverchan": "ServerChan", "common.notifier.telegram": "Telegram", diff --git a/ui/src/i18n/locales/en/nls.settings.json b/ui/src/i18n/locales/en/nls.settings.json index d436665c..eb3aeb95 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -66,6 +66,12 @@ "settings.notification.channel.form.lark_webhook_url.label": "Webhook URL", "settings.notification.channel.form.lark_webhook_url.placeholder": "Please enter Webhook URL", "settings.notification.channel.form.lark_webhook_url.tooltip": "For more information, see https://www.feishu.cn/hc/en-US/articles/807992406756", + "settings.notification.channel.form.pushover_token.placeholder": "Please enter Application API Token", + "settings.notification.channel.form.pushover_token.label": "Application API Token", + "settings.notification.channel.form.pushover_token.tooltip": "For more information, see https://pushover.net/api#registration", + "settings.notification.channel.form.pushover_user.placeholder": "Please enter User/Group Key", + "settings.notification.channel.form.pushover_user.label": "User/Group Key", + "settings.notification.channel.form.pushover_user.tooltip": "For more information, see https://pushover.net/api#identifiers", "settings.notification.channel.form.pushplus_token.placeholder": "Please enter Token", "settings.notification.channel.form.pushplus_token.label": "Token", "settings.notification.channel.form.pushplus_token.tooltip": "For more information, see https://www.pushplus.plus/push1.html", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index 726c5ca2..19f0a94a 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -41,6 +41,7 @@ "common.notifier.email": "邮件", "common.notifier.gotify": "Gotify", "common.notifier.lark": "飞书", + "common.notifier.pushover": "Pushover", "common.notifier.pushplus": "PushPlus推送加", "common.notifier.serverchan": "Server 酱", "common.notifier.telegram": "Telegram", diff --git a/ui/src/i18n/locales/zh/nls.settings.json b/ui/src/i18n/locales/zh/nls.settings.json index c00d158a..b892d50c 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -66,6 +66,12 @@ "settings.notification.channel.form.lark_webhook_url.label": "机器人 Webhook 地址", "settings.notification.channel.form.lark_webhook_url.placeholder": "请输入机器人 Webhook 地址", "settings.notification.channel.form.lark_webhook_url.tooltip": "这是什么?请参阅 https://www.feishu.cn/hc/zh-CN/articles/807992406756", + "settings.notification.channel.form.pushover_token.placeholder": "请输入应用 API Token", + "settings.notification.channel.form.pushover_token.label": "应用 API Token", + "settings.notification.channel.form.pushover_token.tooltip": "这是什么?请参阅 https://pushover.net/api#registration", + "settings.notification.channel.form.pushover_user.placeholder": "请输入用户/分组 Key", + "settings.notification.channel.form.pushover_user.label": "用户/分组 Key", + "settings.notification.channel.form.pushover_user.tooltip": "这是什么?请参阅 https://pushover.net/api#identifiers", "settings.notification.channel.form.pushplus_token.placeholder": "请输入Token", "settings.notification.channel.form.pushplus_token.label": "Token", "settings.notification.channel.form.pushplus_token.tooltip": "请参阅 https://www.pushplus.plus/push1.html", From 4784bf9dba3e638d0907dacba9836e8118fd893f Mon Sep 17 00:00:00 2001 From: banto <13196831+banto6@users.noreply.github.com> Date: Sat, 12 Apr 2025 20:01:03 +0800 Subject: [PATCH 17/22] feat: add channelId tooltip --- ui/src/components/notification/NotifyChannelEditForm.tsx | 2 +- .../notification/NotifyChannelEditFormMattermostFields.tsx | 1 + ui/src/i18n/locales/en/nls.settings.json | 1 + ui/src/i18n/locales/zh/nls.settings.json | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/src/components/notification/NotifyChannelEditForm.tsx b/ui/src/components/notification/NotifyChannelEditForm.tsx index 8a282229..b8b1888a 100644 --- a/ui/src/components/notification/NotifyChannelEditForm.tsx +++ b/ui/src/components/notification/NotifyChannelEditForm.tsx @@ -9,12 +9,12 @@ import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalk import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields"; import NotifyChannelEditFormGotifyFields from "./NotifyChannelEditFormGotifyFields.tsx"; import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields"; +import NotifyChannelEditFormMattermostFields from "./NotifyChannelEditFormMattermostFields.tsx"; import NotifyChannelEditFormPushPlusFields from "./NotifyChannelEditFormPushPlusFields"; import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields"; import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields"; import NotifyChannelEditFormWebhookFields from "./NotifyChannelEditFormWebhookFields"; import NotifyChannelEditFormWeComFields from "./NotifyChannelEditFormWeComFields"; -import NotifyChannelEditFormMattermostFields from "@/components/notification/NotifyChannelEditFormMattermostFields.tsx"; type NotifyChannelEditFormFieldValues = NotifyChannelsSettingsContent[keyof NotifyChannelsSettingsContent]; diff --git a/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx b/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx index 3a2c5b8b..a847be75 100644 --- a/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx +++ b/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx @@ -37,6 +37,7 @@ const NotifyChannelEditFormMattermostFields = () => { name="channelId" label={t("settings.notification.channel.form.mattermost_channel_id.label")} rules={[formRule]} + tooltip={} > diff --git a/ui/src/i18n/locales/en/nls.settings.json b/ui/src/i18n/locales/en/nls.settings.json index 8b7c15cc..2edcfe72 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -71,6 +71,7 @@ "settings.notification.channel.form.mattermost_server_url.tooltip": "Example: https://exmaple.com, the protocol needs to be included but the trailing '/' should not be included.", "settings.notification.channel.form.mattermost_channel_id.label": "Channel ID", "settings.notification.channel.form.mattermost_channel_id.placeholder": "Please enter channel ID", + "settings.notification.channel.form.mattermost_channel_id.tooltip": "How to get the channel ID? Select the target channel from the left sidebar, click on the channel name at the top, and choose ”Channel Details.” You can directly see the channel ID on the pop-up page.", "settings.notification.channel.form.mattermost_username.label": "Username", "settings.notification.channel.form.mattermost_username.placeholder": "Please enter username", "settings.notification.channel.form.mattermost_password.label": "Password", diff --git a/ui/src/i18n/locales/zh/nls.settings.json b/ui/src/i18n/locales/zh/nls.settings.json index bc0b3c7b..6967792b 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -71,6 +71,7 @@ "settings.notification.channel.form.mattermost_server_url.tooltip": "示例: https://exmaple.com,需要包含协议但不要包含末尾的'/'", "settings.notification.channel.form.mattermost_channel_id.label": "频道ID", "settings.notification.channel.form.mattermost_channel_id.placeholder": "请输入频道ID", + "settings.notification.channel.form.mattermost_channel_id.tooltip": "频道ID怎么获取?从左侧边栏中选择目标频道,点击顶部的频道名称,选择“频道详情”,即可在弹出页面中直接看到频道ID", "settings.notification.channel.form.mattermost_username.label": "用户名", "settings.notification.channel.form.mattermost_username.placeholder": "请输入用户名", "settings.notification.channel.form.mattermost_password.label": "密码", From b0973b5ca8dcad509f44028c93e8dda6a904a8c9 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sat, 12 Apr 2025 20:54:02 +0800 Subject: [PATCH 18/22] refactor: clean code --- internal/pkg/vendors/1panel-sdk/client.go | 2 +- internal/pkg/vendors/baishan-sdk/client.go | 2 +- internal/pkg/vendors/btpanel-sdk/client.go | 2 +- internal/pkg/vendors/cachefly-sdk/client.go | 2 +- internal/pkg/vendors/cdnfly-sdk/client.go | 2 +- internal/pkg/vendors/dnsla-sdk/client.go | 2 +- internal/pkg/vendors/gname-sdk/client.go | 2 +- internal/pkg/vendors/rainyun-sdk/client.go | 2 +- internal/pkg/vendors/safeline-sdk/client.go | 2 +- internal/pkg/vendors/upyun-sdk/console/client.go | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/pkg/vendors/1panel-sdk/client.go b/internal/pkg/vendors/1panel-sdk/client.go index 653e3a69..02dc8f58 100644 --- a/internal/pkg/vendors/1panel-sdk/client.go +++ b/internal/pkg/vendors/1panel-sdk/client.go @@ -79,7 +79,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r if err != nil { return resp, fmt.Errorf("1panel api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("1panel api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("1panel api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/baishan-sdk/client.go b/internal/pkg/vendors/baishan-sdk/client.go index 400e1ae1..ad906cbe 100644 --- a/internal/pkg/vendors/baishan-sdk/client.go +++ b/internal/pkg/vendors/baishan-sdk/client.go @@ -75,7 +75,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r if err != nil { return resp, fmt.Errorf("baishan api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("baishan api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("baishan api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/btpanel-sdk/client.go b/internal/pkg/vendors/btpanel-sdk/client.go index 8fb4ad32..1e48f734 100644 --- a/internal/pkg/vendors/btpanel-sdk/client.go +++ b/internal/pkg/vendors/btpanel-sdk/client.go @@ -86,7 +86,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, if err != nil { return resp, fmt.Errorf("baota api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("baota api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("baota api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/cachefly-sdk/client.go b/internal/pkg/vendors/cachefly-sdk/client.go index 0b11f6d2..a460ae96 100644 --- a/internal/pkg/vendors/cachefly-sdk/client.go +++ b/internal/pkg/vendors/cachefly-sdk/client.go @@ -59,7 +59,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r if err != nil { return resp, fmt.Errorf("cachefly api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("cachefly api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("cachefly api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/cdnfly-sdk/client.go b/internal/pkg/vendors/cdnfly-sdk/client.go index b43a04db..47738f29 100644 --- a/internal/pkg/vendors/cdnfly-sdk/client.go +++ b/internal/pkg/vendors/cdnfly-sdk/client.go @@ -65,7 +65,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r if err != nil { return resp, fmt.Errorf("cdnfly api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("cdnfly api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("cdnfly api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/dnsla-sdk/client.go b/internal/pkg/vendors/dnsla-sdk/client.go index 72b0ed3d..d557635b 100644 --- a/internal/pkg/vendors/dnsla-sdk/client.go +++ b/internal/pkg/vendors/dnsla-sdk/client.go @@ -60,7 +60,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r if err != nil { return resp, fmt.Errorf("dnsla api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("dnsla api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("dnsla api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/gname-sdk/client.go b/internal/pkg/vendors/gname-sdk/client.go index 0a2238f2..017a3315 100644 --- a/internal/pkg/vendors/gname-sdk/client.go +++ b/internal/pkg/vendors/gname-sdk/client.go @@ -82,7 +82,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, if err != nil { return resp, fmt.Errorf("gname api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("gname api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("gname api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/rainyun-sdk/client.go b/internal/pkg/vendors/rainyun-sdk/client.go index b35f38e0..e710128b 100644 --- a/internal/pkg/vendors/rainyun-sdk/client.go +++ b/internal/pkg/vendors/rainyun-sdk/client.go @@ -49,7 +49,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r if err != nil { return resp, fmt.Errorf("rainyun api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("rainyun api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("rainyun api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/safeline-sdk/client.go b/internal/pkg/vendors/safeline-sdk/client.go index ade8acfa..c56e3485 100644 --- a/internal/pkg/vendors/safeline-sdk/client.go +++ b/internal/pkg/vendors/safeline-sdk/client.go @@ -47,7 +47,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, if err != nil { return resp, fmt.Errorf("safeline api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("safeline api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("safeline api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/upyun-sdk/console/client.go b/internal/pkg/vendors/upyun-sdk/console/client.go index 7b968f53..cf431c2c 100644 --- a/internal/pkg/vendors/upyun-sdk/console/client.go +++ b/internal/pkg/vendors/upyun-sdk/console/client.go @@ -64,7 +64,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r if err != nil { return resp, fmt.Errorf("upyun api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("upyun api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("upyun api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil From f970ae752993ff2e69a27e7b6058781ac52f03ef Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sat, 12 Apr 2025 21:28:33 +0800 Subject: [PATCH 19/22] feat: add wangsu cdnpro deployer --- internal/deployer/providers.go | 25 ++ internal/domain/access.go | 5 + internal/domain/provider.go | 2 + .../providers/wangsu-cdnpro/wangsu_cdnpro.go | 276 ++++++++++++++++++ .../wangsu-cdnpro/wangsu_cdnpro_test.go | 90 ++++++ internal/pkg/vendors/wangsu-sdk/cdn/api.go | 58 ++++ internal/pkg/vendors/wangsu-sdk/cdn/client.go | 20 ++ internal/pkg/vendors/wangsu-sdk/cdn/models.go | 107 +++++++ .../pkg/vendors/wangsu-sdk/openapi/client.go | 187 ++++++++++++ migrations/1744192800_upgrade.go | 91 ++++++ ui/public/imgs/providers/wangsu.svg | 1 + ui/src/components/access/AccessForm.tsx | 3 + .../access/AccessFormWangsuConfig.tsx | 76 +++++ .../workflow/node/DeployNodeConfigForm.tsx | 3 + ...DeployNodeConfigFormWangsuCDNProConfig.tsx | 107 +++++++ ui/src/domain/access.ts | 6 + ui/src/domain/provider.ts | 4 + ui/src/i18n/locales/en/nls.access.json | 6 + ui/src/i18n/locales/en/nls.provider.json | 2 + .../i18n/locales/en/nls.workflow.nodes.json | 23 +- ui/src/i18n/locales/zh/nls.access.json | 6 + ui/src/i18n/locales/zh/nls.provider.json | 2 + .../i18n/locales/zh/nls.workflow.nodes.json | 15 +- 23 files changed, 1109 insertions(+), 6 deletions(-) create mode 100644 internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go create mode 100644 internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro_test.go create mode 100644 internal/pkg/vendors/wangsu-sdk/cdn/api.go create mode 100644 internal/pkg/vendors/wangsu-sdk/cdn/client.go create mode 100644 internal/pkg/vendors/wangsu-sdk/cdn/models.go create mode 100644 internal/pkg/vendors/wangsu-sdk/openapi/client.go create mode 100644 migrations/1744192800_upgrade.go create mode 100644 ui/public/imgs/providers/wangsu.svg create mode 100644 ui/src/components/access/AccessFormWangsuConfig.tsx create mode 100644 ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 292c34f7..c2136d20 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -74,6 +74,7 @@ import ( pVolcEngineImageX "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-imagex" pVolcEngineLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live" pVolcEngineTOS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-tos" + pWangsuCDNPro "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-cdnpro" pWebhook "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook" "github.com/usual2970/certimate/internal/pkg/utils/maputil" "github.com/usual2970/certimate/internal/pkg/utils/sliceutil" @@ -1003,6 +1004,30 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { } } + case domain.DeployProviderTypeWangsuCDNPro: + { + access := domain.AccessConfigForWangsu{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + switch options.Provider { + case domain.DeployProviderTypeWangsuCDNPro: + deployer, err := pWangsuCDNPro.NewDeployer(&pWangsuCDNPro.DeployerConfig{ + AccessKeyId: access.AccessKeyId, + AccessKeySecret: access.AccessKeySecret, + Environment: maputil.GetOrDefaultString(options.ProviderDeployConfig, "environment", "production"), + Domain: maputil.GetString(options.ProviderDeployConfig, "domain"), + CertificateId: maputil.GetString(options.ProviderDeployConfig, "certificateId"), + WebhookId: maputil.GetString(options.ProviderDeployConfig, "webhookId"), + }) + return deployer, err + + default: + break + } + } + case domain.DeployProviderTypeWebhook: { access := domain.AccessConfigForWebhook{} diff --git a/internal/domain/access.go b/internal/domain/access.go index 0d3528ab..9e419eaa 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -228,6 +228,11 @@ type AccessConfigForVolcEngine struct { SecretAccessKey string `json:"secretAccessKey"` } +type AccessConfigForWangsu struct { + AccessKeyId string `json:"accessKeyId"` + AccessKeySecret string `json:"accessKeySecret"` +} + type AccessConfigForWebhook struct { Url string `json:"url"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 18ee73b9..668612f7 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -61,6 +61,7 @@ const ( AccessProviderTypeUpyun = AccessProviderType("upyun") AccessProviderTypeVercel = AccessProviderType("vercel") AccessProviderTypeVolcEngine = AccessProviderType("volcengine") + AccessProviderTypeWangsu = AccessProviderType("wangsu") AccessProviderTypeWebhook = AccessProviderType("webhook") AccessProviderTypeWestcn = AccessProviderType("westcn") AccessProviderTypeZeroSSL = AccessProviderType("zerossl") @@ -212,5 +213,6 @@ const ( DeployProviderTypeVolcEngineImageX = DeployProviderType("volcengine-imagex") DeployProviderTypeVolcEngineLive = DeployProviderType("volcengine-live") DeployProviderTypeVolcEngineTOS = DeployProviderType("volcengine-tos") + DeployProviderTypeWangsuCDNPro = DeployProviderType("wangsu-cdnpro") DeployProviderTypeWebhook = DeployProviderType("webhook") ) diff --git a/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go new file mode 100644 index 00000000..c5ac15b9 --- /dev/null +++ b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go @@ -0,0 +1,276 @@ +package wangsucdnpro + +import ( + "bytes" + "context" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "log/slog" + "regexp" + "time" + + "github.com/alibabacloud-go/tea/tea" + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + "github.com/usual2970/certimate/internal/pkg/utils/certutil" + wangsucdn "github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/cdn" +) + +type DeployerConfig struct { + // 网宿云 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 网宿云 AccessKeySecret。 + AccessKeySecret string `json:"accessKeySecret"` + // 网宿云环境。 + Environment string `json:"environment"` + // 加速域名(支持泛域名)。 + Domain string `json:"domain"` + // 证书 ID。 + // 选填。 + CertificateId string `json:"certificateId,omitempty"` + // Webhook ID。 + // 选填。 + WebhookId string `json:"webhookId,omitempty"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *wangsucdn.Client +} + +var _ deployer.Deployer = (*DeployerProvider)(nil) + +func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + return &DeployerProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, nil +} + +func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { + if logger == nil { + d.logger = slog.Default() + } else { + d.logger = logger + } + return d +} + +func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { + if d.config.Domain == "" { + return nil, errors.New("config `domain` is required") + } + + // 解析证书内容 + certX509, err := certutil.ParseCertificateFromPEM(certPem) + if err != nil { + return nil, err + } + + // 查询已部署加速域名的详情 + getHostnameDetailResp, err := d.sdkClient.GetHostnameDetail(d.config.Domain) + d.logger.Debug("sdk request 'cdn.GetHostnameDetail'", slog.String("hostname", d.config.Domain), slog.Any("response", getHostnameDetailResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetHostnameDetail'") + } + + // 生成网宿云证书参数 + encryptedPrivateKey, err := encryptPrivateKey(privkeyPem, d.config.AccessKeySecret, time.Now().Unix()) + if err != nil { + return nil, xerrors.Wrap(err, "failed to encrypt private key") + } + certificateNewVersionInfo := &wangsucdn.CertificateVersion{ + PrivateKey: tea.String(encryptedPrivateKey), + Certificate: tea.String(certPem), + IdentificationInfo: &wangsucdn.CertificateVersionIdentificationInfo{ + CommonName: tea.String(certX509.Subject.CommonName), + SubjectAlternativeNames: &certX509.DNSNames, + }, + } + + // 网宿云证书 URL 中包含证书 ID 及版本号 + // 格式: + // http://open.chinanetcenter.com/cdn/certificates/5dca2205f9e9cc0001df7b33 + // http://open.chinanetcenter.com/cdn/certificates/329f12c1fe6708c23c31e91f/versions/5 + var wangsuCertUrl string + var wangsuCertId, wangsuCertVer string + + // 如果原证书 ID 为空,则创建证书;否则更新证书。 + timestamp := time.Now().Unix() + if d.config.CertificateId == "" { + // 创建证书 + createCertificateReq := &wangsucdn.CreateCertificateRequest{ + Timestamp: timestamp, + Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())), + AutoRenew: tea.String("Off"), + NewVersion: certificateNewVersionInfo, + } + createCertificateResp, err := d.sdkClient.CreateCertificate(createCertificateReq) + d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.CreateCertificate'") + } + + wangsuCertUrl = createCertificateResp.CertificateUrl + d.logger.Info("ssl certificate uploaded", slog.Any("certUrl", wangsuCertUrl)) + + wangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl) + if len(wangsuCertIdMatches) > 1 { + wangsuCertId = wangsuCertIdMatches[1] + } + + wangsuCertVer = "1" + } else { + // 更新证书 + updateCertificateReq := &wangsucdn.UpdateCertificateRequest{ + Timestamp: timestamp, + Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())), + AutoRenew: tea.String("Off"), + NewVersion: certificateNewVersionInfo, + } + updateCertificateResp, err := d.sdkClient.UpdateCertificate(d.config.CertificateId, updateCertificateReq) + d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("certificateId", d.config.CertificateId), slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UpdateCertificate'") + } + + wangsuCertUrl = updateCertificateResp.CertificateUrl + d.logger.Info("ssl certificate uploaded", slog.Any("certUrl", wangsuCertUrl)) + + wangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl) + if len(wangsuCertIdMatches) > 1 { + wangsuCertId = wangsuCertIdMatches[1] + } + + wangsuCertVerMatches := regexp.MustCompile(`/versions/(\d+)`).FindStringSubmatch(wangsuCertUrl) + if len(wangsuCertVerMatches) > 1 { + wangsuCertVer = wangsuCertVerMatches[1] + } + } + + // 创建部署任务 + // REF: https://www.wangsu.com/document/api-doc/27034 + createDeploymentTaskReq := &wangsucdn.CreateDeploymentTaskRequest{ + Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())), + Target: tea.String(d.config.Environment), + Actions: &[]wangsucdn.DeploymentTaskAction{ + { + Action: tea.String("deploy_cert"), + CertificateId: tea.String(wangsuCertId), + Version: tea.String(wangsuCertVer), + }, + }, + } + if d.config.WebhookId != "" { + createDeploymentTaskReq.Webhook = tea.String(d.config.WebhookId) + } + createDeploymentTaskResp, err := d.sdkClient.CreateDeploymentTask(createDeploymentTaskReq) + d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("request", createDeploymentTaskReq), slog.Any("response", createDeploymentTaskResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.CreateDeploymentTask'") + } + + // 循环获取部署任务详细信息,等待任务状态变更 + // REF: https://www.wangsu.com/document/api-doc/27038 + var wangsuTaskId string + wangsuTaskMatches := regexp.MustCompile(`/deploymentTasks/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl) + if len(wangsuTaskMatches) > 1 { + wangsuTaskId = wangsuTaskMatches[1] + } + for { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + getDeploymentTaskDetailResp, err := d.sdkClient.GetDeploymentTaskDetail(wangsuTaskId) + d.logger.Debug("sdk request 'cdn.GetDeploymentTaskDetail'", slog.Any("taskId", wangsuTaskId), slog.Any("response", getDeploymentTaskDetailResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDeploymentTaskDetail'") + } + + if getDeploymentTaskDetailResp.Status == "failed" { + return nil, errors.New("unexpected deployment task status") + } else if getDeploymentTaskDetailResp.Status == "succeeded" { + break + } + + d.logger.Info("waiting for deployment task completion ...") + time.Sleep(time.Second * 15) + } + + return &deployer.DeployResult{}, nil +} + +func createSdkClient(accessKeyId, accessKeySecret string) (*wangsucdn.Client, error) { + if accessKeyId == "" { + return nil, errors.New("invalid wangsu access key id") + } + + if accessKeySecret == "" { + return nil, errors.New("invalid wangsu access key secret") + } + + return wangsucdn.NewClient(accessKeyId, accessKeySecret), nil +} + +func encryptPrivateKey(privkeyPem string, secretKey string, timestamp int64) (string, error) { + date := time.Unix(timestamp, 0).UTC() + dateStr := date.Format("Mon, 02 Jan 2006 15:04:05 GMT") + + mac := hmac.New(sha256.New, []byte(secretKey)) + mac.Write([]byte(dateStr)) + aesivkey := mac.Sum(nil) + aesivkeyHex := hex.EncodeToString(aesivkey) + + if len(aesivkeyHex) != 64 { + return "", fmt.Errorf("invalid hmac length: %d", len(aesivkeyHex)) + } + ivHex := aesivkeyHex[:32] + keyHex := aesivkeyHex[32:64] + + iv, err := hex.DecodeString(ivHex) + if err != nil { + return "", fmt.Errorf("failed to decode iv: %w", err) + } + + key, err := hex.DecodeString(keyHex) + if err != nil { + return "", fmt.Errorf("failed to decode key: %w", err) + } + + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + plainBytes := []byte(privkeyPem) + padlen := aes.BlockSize - len(plainBytes)%aes.BlockSize + if padlen > 0 { + paddata := bytes.Repeat([]byte{byte(padlen)}, padlen) + plainBytes = append(plainBytes, paddata...) + } + + encBytes := make([]byte, len(plainBytes)) + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(encBytes, plainBytes) + + return base64.StdEncoding.EncodeToString(encBytes), nil +} diff --git a/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro_test.go b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro_test.go new file mode 100644 index 00000000..25dd7b1e --- /dev/null +++ b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro_test.go @@ -0,0 +1,90 @@ +package wangsucdnpro_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-cdnpro" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fAccessKeySecret string + fEnvironment string + fDomain string + fCertificateId string + fWebhookId string +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_WANGSUCDNPRO_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") + flag.StringVar(&fEnvironment, argsPrefix+"ENVIRONMENT", "production", "") + flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") + flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "") + flag.StringVar(&fWebhookId, argsPrefix+"WEBHOOKID", "", "") +} + +/* +Shell command to run this test: + + go test -v ./wangsu_cdnpro_test.go -args \ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_ACCESSKEYSECRET="your-access-key-secret" \ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_ENVIRONMENT="production" \ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_DOMAIN="example.com" \ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_CERTIFICATEID="your-certificate-id"\ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_WEBHOOKID="your-webhook-id" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), + fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), + fmt.Sprintf("ENVIRONMENT: %v", fEnvironment), + fmt.Sprintf("DOMAIN: %v", fDomain), + fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), + fmt.Sprintf("WEBHOOKID: %v", fWebhookId), + }, "\n")) + + deployer, err := provider.NewDeployer(&provider.DeployerConfig{ + AccessKeyId: fAccessKeyId, + AccessKeySecret: fAccessKeySecret, + Environment: fEnvironment, + Domain: fDomain, + CertificateId: fCertificateId, + WebhookId: fWebhookId, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/internal/pkg/vendors/wangsu-sdk/cdn/api.go b/internal/pkg/vendors/wangsu-sdk/cdn/api.go new file mode 100644 index 00000000..fd96ba2f --- /dev/null +++ b/internal/pkg/vendors/wangsu-sdk/cdn/api.go @@ -0,0 +1,58 @@ +package cdn + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/go-resty/resty/v2" +) + +func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) { + resp := &CreateCertificateResponse{} + r, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/certificates", req, resp, func(r *resty.Request) { + r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp)) + }) + if err != nil { + return resp, err + } + + resp.CertificateUrl = r.Header().Get("Location") + return resp, err +} + +func (c *Client) UpdateCertificate(certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { + resp := &UpdateCertificateResponse{} + r, err := c.client.SendRequestWithResult(http.MethodPatch, fmt.Sprintf("/cdn/certificates/%s", url.PathEscape(certificateId)), req, resp, func(r *resty.Request) { + r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp)) + }) + if err != nil { + return resp, err + } + + resp.CertificateUrl = r.Header().Get("Location") + return resp, err +} + +func (c *Client) GetHostnameDetail(hostname string) (*GetHostnameDetailResponse, error) { + resp := &GetHostnameDetailResponse{} + _, err := c.client.SendRequestWithResult(http.MethodGet, fmt.Sprintf("/cdn/hostnames/%s", url.PathEscape(hostname)), nil, resp) + return resp, err +} + +func (c *Client) CreateDeploymentTask(req *CreateDeploymentTaskRequest) (*CreateDeploymentTaskResponse, error) { + resp := &CreateDeploymentTaskResponse{} + r, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/deploymentTasks", req, resp) + if err != nil { + return resp, err + } + + resp.DeploymentTaskUrl = r.Header().Get("Location") + return resp, err +} + +func (c *Client) GetDeploymentTaskDetail(deploymentTaskId string) (*GetDeploymentTaskDetailResponse, error) { + resp := &GetDeploymentTaskDetailResponse{} + _, err := c.client.SendRequestWithResult(http.MethodGet, fmt.Sprintf("/cdn/deploymentTasks/%s", url.PathEscape(hostname)), nil, resp) + return resp, err +} diff --git a/internal/pkg/vendors/wangsu-sdk/cdn/client.go b/internal/pkg/vendors/wangsu-sdk/cdn/client.go new file mode 100644 index 00000000..e1831960 --- /dev/null +++ b/internal/pkg/vendors/wangsu-sdk/cdn/client.go @@ -0,0 +1,20 @@ +package cdn + +import ( + "time" + + "github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/openapi" +) + +type Client struct { + client *openapi.Client +} + +func NewClient(accessKey, secretKey string) *Client { + return &Client{client: openapi.NewClient(accessKey, secretKey)} +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c.client.WithTimeout(timeout) + return c +} diff --git a/internal/pkg/vendors/wangsu-sdk/cdn/models.go b/internal/pkg/vendors/wangsu-sdk/cdn/models.go new file mode 100644 index 00000000..0126418a --- /dev/null +++ b/internal/pkg/vendors/wangsu-sdk/cdn/models.go @@ -0,0 +1,107 @@ +package cdn + +import ( + "github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/openapi" +) + +type baseResponse struct { + RequestId *string `json:"-"` + Code *string `json:"code,omitempty"` + Message *string `json:"message,omitempty"` +} + +var _ openapi.Result = (*baseResponse)(nil) + +func (r *baseResponse) SetRequestId(requestId string) { + r.RequestId = &requestId +} + +type CertificateVersion struct { + Comments *string `json:"comments,omitempty"` + PrivateKey *string `json:"privateKey,omitempty"` + Certificate *string `json:"certificate,omitempty"` + ChainCert *string `json:"chainCert,omitempty"` + IdentificationInfo *CertificateVersionIdentificationInfo `json:"identificationInfo,omitempty"` +} + +type CertificateVersionIdentificationInfo struct { + Country *string `json:"country,omitempty"` + State *string `json:"state,omitempty"` + City *string `json:"city,omitempty"` + Company *string `json:"company,omitempty"` + Department *string `json:"department,omitempty"` + CommonName *string `json:"commonName,omitempty" required:"true"` + Email *string `json:"email,omitempty"` + SubjectAlternativeNames *[]string `json:"subjectAlternativeNames,omitempty" required:"true"` +} + +type CreateCertificateRequest struct { + Timestamp int64 `json:"-"` + Name *string `json:"name,omitempty" required:"true"` + Description *string `json:"description,omitempty"` + AutoRenew *string `json:"autoRenew,omitempty"` + ForceRenew *bool `json:"forceRenew,omitempty"` + NewVersion *CertificateVersion `json:"newVersion,omitempty" required:"true"` +} + +type CreateCertificateResponse struct { + baseResponse + CertificateUrl string `json:"-"` +} + +type UpdateCertificateRequest struct { + Timestamp int64 `json:"-"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + AutoRenew *string `json:"autoRenew,omitempty"` + ForceRenew *bool `json:"forceRenew,omitempty"` + NewVersion *CertificateVersion `json:"newVersion,omitempty" required:"true"` +} + +type UpdateCertificateResponse struct { + baseResponse + CertificateUrl string `json:"-"` +} + +type HostnameProperty struct { + PropertyId string `json:"propertyId"` + Version int32 `json:"version"` + CertificateId *string `json:"certificateId,omitempty"` +} + +type GetHostnameDetailResponse struct { + baseResponse + Hostname string `json:"hostname"` + PropertyInProduction *HostnameProperty `json:"propertyInProduction,omitempty"` + PropertyInStaging *HostnameProperty `json:"propertyInStaging,omitempty"` +} + +type DeploymentTaskAction struct { + Action *string `json:"action,omitempty" required:"true"` + PropertyId *string `json:"propertyId,omitempty"` + CertificateId *string `json:"certificateId,omitempty"` + Version *string `json:"version,omitempty"` +} + +type CreateDeploymentTaskRequest struct { + Name *string `json:"name,omitempty"` + Target *string `json:"target,omitempty" required:"true"` + Actions *[]DeploymentTaskAction `json:"actions,omitempty" required:"true"` + Webhook *string `json:"webhook,omitempty"` +} + +type CreateDeploymentTaskResponse struct { + baseResponse + DeploymentTaskUrl string `json:"-"` +} + +type GetDeploymentTaskDetailResponse struct { + baseResponse + Target string `json:"target"` + Actions []DeploymentTaskAction `json:"actions"` + Status string `json:"status"` + StatusDetails string `json:"statusDetails"` + SubmissionTime string `json:"submissionTime"` + FinishTime string `json:"finishTime"` + ApiRequestId string `json:"apiRequestId"` +} diff --git a/internal/pkg/vendors/wangsu-sdk/openapi/client.go b/internal/pkg/vendors/wangsu-sdk/openapi/client.go new file mode 100644 index 00000000..6492aba8 --- /dev/null +++ b/internal/pkg/vendors/wangsu-sdk/openapi/client.go @@ -0,0 +1,187 @@ +package openapi + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/go-resty/resty/v2" +) + +type Client struct { + accessKey string + secretKey string + + client *resty.Client +} + +type Result interface { + SetRequestId(requestId string) +} + +func NewClient(accessKey, secretKey string) *Client { + client := resty.New(). + SetBaseURL("https://open.chinanetcenter.com"). + SetHeader("Host", "open.chinanetcenter.com"). + SetHeader("Accept", "application/json"). + SetHeader("Content-Type", "application/json"). + SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + // Step 1: Get request method + method := req.Method + method = strings.ToUpper(method) + + // Step 2: Get request path + path := "/" + if req.URL != nil { + path = req.URL.Path + } + + // Step 3: Get unencoded query string + queryString := "" + if method != http.MethodPost && req.URL != nil { + queryString = req.URL.RawQuery + + s, err := url.QueryUnescape(queryString) + if err != nil { + return err + } + + queryString = s + } + + // Step 4: Get canonical headers & signed headers + canonicalHeaders := "" + + "content-type:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Content-Type"))) + "\n" + + "host:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Host"))) + "\n" + signedHeaders := "content-type;host" + + // Step 5: Get request payload + payload := "" + if method != http.MethodGet && req.Body != nil { + reader, err := req.GetBody() + if err != nil { + return err + } + + defer reader.Close() + + payloadb, err := io.ReadAll(reader) + if err != nil { + return err + } + + payload = string(payloadb) + } + hashedPayload := sha256.Sum256([]byte(payload)) + hashedPayloadHex := strings.ToLower(hex.EncodeToString(hashedPayload[:])) + + // Step 6: Get timestamp + var reqtime time.Time + timestampString := req.Header.Get("x-cnc-timestamp") + if timestampString == "" { + reqtime = time.Now().UTC() + timestampString = fmt.Sprintf("%d", reqtime.Unix()) + } else { + timestamp, err := strconv.ParseInt(timestampString, 10, 64) + if err != nil { + return err + } + reqtime = time.Unix(timestamp, 0).UTC() + } + + // Step 7: Get canonical request string + canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, queryString, canonicalHeaders, signedHeaders, hashedPayloadHex) + hashedCanonicalRequest := sha256.Sum256([]byte(canonicalRequest)) + hashedCanonicalRequestHex := strings.ToLower(hex.EncodeToString(hashedCanonicalRequest[:])) + + // Step 8: String to sign + const SignAlgorithmHeader = "CNC-HMAC-SHA256" + stringToSign := fmt.Sprintf("%s\n%s\n%s", SignAlgorithmHeader, timestampString, hashedCanonicalRequestHex) + hmac := hmac.New(sha256.New, []byte(secretKey)) + hmac.Write([]byte(stringToSign)) + sign := hmac.Sum(nil) + signHex := strings.ToLower(hex.EncodeToString(sign)) + + // Step 9: Add headers to request + req.Header.Set("x-cnc-accessKey", accessKey) + req.Header.Set("x-cnc-timestamp", timestampString) + req.Header.Set("x-cnc-auth-method", "AKSK") + req.Header.Set("Authorization", fmt.Sprintf("%s Credential=%s, SignedHeaders=%s, Signature=%s", SignAlgorithmHeader, accessKey, signedHeaders, signHex)) + req.Header.Set("Date", reqtime.Format("Mon, 02 Jan 2006 15:04:05 GMT")) + + return nil + }) + + return &Client{ + accessKey: accessKey, + secretKey: secretKey, + client: client, + } +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c.client.SetTimeout(timeout) + return c +} + +func (c *Client) sendRequest(method string, path string, params interface{}, configureReq ...func(req *resty.Request)) (*resty.Response, error) { + req := c.client.R() + req.Method = method + req.URL = path + if strings.EqualFold(method, http.MethodGet) { + qs := make(map[string]string) + if params != nil { + temp := make(map[string]any) + jsonb, _ := json.Marshal(params) + json.Unmarshal(jsonb, &temp) + for k, v := range temp { + if v != nil { + qs[k] = fmt.Sprintf("%v", v) + } + } + } + + req = req.SetQueryParams(qs) + } else { + req = req.SetBody(params) + } + + for _, fn := range configureReq { + fn(req) + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("wangsu api error: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("wangsu api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) + } + + return resp, nil +} + +func (c *Client) SendRequestWithResult(method string, path string, params interface{}, result Result, configureReq ...func(req *resty.Request)) (*resty.Response, error) { + resp, err := c.sendRequest(method, path, params, configureReq...) + if err != nil { + if resp != nil { + json.Unmarshal(resp.Body(), &result) + result.SetRequestId(resp.Header().Get("x-cnc-request-id")) + } + return resp, err + } + + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return resp, fmt.Errorf("wangsu api error: failed to parse response: %w", err) + } + + result.SetRequestId(resp.Header().Get("x-cnc-request-id")) + return resp, nil +} diff --git a/migrations/1744192800_upgrade.go b/migrations/1744192800_upgrade.go new file mode 100644 index 00000000..83e83ee6 --- /dev/null +++ b/migrations/1744192800_upgrade.go @@ -0,0 +1,91 @@ +package migrations + +import ( + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e") + if err != nil { + return err + } + + // update field + if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ + "hidden": false, + "id": "hwy7m03o", + "maxSelect": 1, + "name": "provider", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "1panel", + "acmehttpreq", + "akamai", + "aliyun", + "aws", + "azure", + "baiducloud", + "baishan", + "baotapanel", + "byteplus", + "buypass", + "cachefly", + "cdnfly", + "cloudflare", + "cloudns", + "cmcccloud", + "ctcccloud", + "cucccloud", + "desec", + "dnsla", + "dogecloud", + "dynv6", + "edgio", + "fastly", + "gname", + "gcore", + "godaddy", + "goedge", + "googletrustservices", + "huaweicloud", + "jdcloud", + "k8s", + "letsencrypt", + "letsencryptstaging", + "local", + "namecheap", + "namedotcom", + "namesilo", + "ns1", + "porkbun", + "powerdns", + "qiniu", + "qingcloud", + "rainyun", + "safeline", + "ssh", + "sslcom", + "tencentcloud", + "ucloud", + "upyun", + "vercel", + "volcengine", + "wangsu", + "webhook", + "westcn", + "zerossl" + ] + }`)); err != nil { + return err + } + + return app.Save(collection) + }, func(app core.App) error { + return nil + }) +} diff --git a/ui/public/imgs/providers/wangsu.svg b/ui/public/imgs/providers/wangsu.svg new file mode 100644 index 00000000..276ec1cc --- /dev/null +++ b/ui/public/imgs/providers/wangsu.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index d5906434..bffb1f49 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -51,6 +51,7 @@ import AccessFormUCloudConfig from "./AccessFormUCloudConfig"; import AccessFormUpyunConfig from "./AccessFormUpyunConfig"; import AccessFormVercelConfig from "./AccessFormVercelConfig"; import AccessFormVolcEngineConfig from "./AccessFormVolcEngineConfig"; +import AccessFormWangsuConfig from "./AccessFormWangsuConfig"; import AccessFormWebhookConfig from "./AccessFormWebhookConfig"; import AccessFormWestcnConfig from "./AccessFormWestcnConfig"; import AccessFormZeroSSLConfig from "./AccessFormZeroSSLConfig"; @@ -229,6 +230,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.VOLCENGINE: return ; + case ACCESS_PROVIDERS.WANGSU: + return ; case ACCESS_PROVIDERS.WEBHOOK: return ; case ACCESS_PROVIDERS.WESTCN: diff --git a/ui/src/components/access/AccessFormWangsuConfig.tsx b/ui/src/components/access/AccessFormWangsuConfig.tsx new file mode 100644 index 00000000..f9676829 --- /dev/null +++ b/ui/src/components/access/AccessFormWangsuConfig.tsx @@ -0,0 +1,76 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForWangsu } from "@/domain/access"; + +type AccessFormWangsuConfigFieldValues = Nullish; + +export type AccessFormWangsuConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormWangsuConfigFieldValues; + onValuesChange?: (values: AccessFormWangsuConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormWangsuConfigFieldValues => { + return { + accessKeyId: "", + accessKeySecret: "", + }; +}; + +const AccessFormWangsuConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange: onValuesChange }: AccessFormWangsuConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKeyId: z + .string() + .min(1, t("access.form.wangsu_access_key_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .trim(), + accessKeySecret: z + .string() + .min(1, t("access.form.wangsu_access_key_secret.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessFormWangsuConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index cd1a0fe7..5565c985 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -81,6 +81,7 @@ import DeployNodeConfigFormVolcEngineDCDNConfig from "./DeployNodeConfigFormVolc import DeployNodeConfigFormVolcEngineImageXConfig from "./DeployNodeConfigFormVolcEngineImageXConfig.tsx"; import DeployNodeConfigFormVolcEngineLiveConfig from "./DeployNodeConfigFormVolcEngineLiveConfig.tsx"; import DeployNodeConfigFormVolcEngineTOSConfig from "./DeployNodeConfigFormVolcEngineTOSConfig.tsx"; +import DeployNodeConfigFormWangsuCDNProConfig from "./DeployNodeConfigFormWangsuCDNProConfig.tsx"; import DeployNodeConfigFormWebhookConfig from "./DeployNodeConfigFormWebhookConfig.tsx"; type DeployNodeConfigFormFieldValues = Partial; @@ -302,6 +303,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOY_PROVIDERS.VOLCENGINE_TOS: return ; + case DEPLOY_PROVIDERS.WANGSU_CDNPRO: + return ; case DEPLOY_PROVIDERS.WEBHOOK: return ; } diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx new file mode 100644 index 00000000..90bdb064 --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx @@ -0,0 +1,107 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input, Select } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { validDomainName } from "@/utils/validators"; + +type DeployNodeConfigFormBaishanCDNConfigFieldValues = Nullish<{ + environment: string; + domain: string; + certificateId?: string; + webhookId?: string; +}>; + +export type DeployNodeConfigFormBaishanCDNConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormBaishanCDNConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormBaishanCDNConfigFieldValues) => void; +}; + +const ENVIRONMENT_PRODUCTION = "production" as const; +const ENVIRONMENT_STAGING = "stating" as const; + +const initFormModel = (): DeployNodeConfigFormBaishanCDNConfigFieldValues => { + return { + environment: ENVIRONMENT_PRODUCTION, + }; +}; + +const DeployNodeConfigFormBaishanCDNConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: DeployNodeConfigFormBaishanCDNConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + resourceType: z.union([z.literal(ENVIRONMENT_PRODUCTION), z.literal(ENVIRONMENT_STAGING)], { + message: t("workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder"), + }), + domain: z + .string({ message: t("workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder") }) + .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), + certificateId: z.string().nullish(), + webhookId: z.string().nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ + + + + } + > + + + + } + > + + + + } + > + + +
+ ); +}; + +export default DeployNodeConfigFormBaishanCDNConfig; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index 86d644fa..3015e4d0 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -47,6 +47,7 @@ export interface AccessModel extends BaseModel { | AccessConfigForUpyun | AccessConfigForVercel | AccessConfigForVolcEngine + | AccessConfigForWangsu | AccessConfigForWebhook | AccessConfigForWestcn | AccessConfigForZeroSSL @@ -268,6 +269,11 @@ export type AccessConfigForVolcEngine = { secretAccessKey: string; }; +export type AccessConfigForWangsu = { + accessKeyId: string; + accessKeySecret: string; +}; + export type AccessConfigForWebhook = { url: string; allowInsecureConnections?: boolean; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index c101b0e3..9c59a30e 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -50,6 +50,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ UPYUN: "upyun", VERCEL: "vercel", VOLCENGINE: "volcengine", + WANGSU: "wangsu", WEBHOOK: "webhook", WESTCN: "westcn", ZEROSSL: "zerossl", @@ -99,6 +100,7 @@ export const accessProvidersMap: Maphttps://www.volcengine.com/docs/6291/216571", + "access.form.wangsu_access_key_id.label": "Wangsu Cloud AccessKeyId", + "access.form.wangsu_access_key_id.placeholder": "Please enter Wangsu Cloud AccessKeyId", + "access.form.wangsu_access_key_id.tooltip": "For more information, see https://en.wangsu.com/document/account-manage/15775", + "access.form.wangsu_access_key_secret.label": "Wangsu Cloud AccessKeySecret", + "access.form.wangsu_access_key_secret.placeholder": "Please enter Wangsu Cloud AccessKeySecret", + "access.form.wangsu_access_key_secret.tooltip": "For more information, see https://en.wangsu.com/document/account-manage/15775", "access.form.webhook_url.label": "Webhook URL", "access.form.webhook_url.placeholder": "Please enter Webhook URL", "access.form.webhook_allow_insecure_conns.label": "Insecure SSL/TLS connections", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index 5df6c3b3..76680957 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -125,6 +125,8 @@ "provider.volcengine.imagex": "Volcengine - ImageX", "provider.volcengine.live": "Volcengine - Live", "provider.volcengine.tos": "Volcengine - TOS (Tinder Object Storage)", + "provider.wangsu": "Wangsu Cloud", + "provider.wangsu.cdnpro": "Wangsu Cloud - CDN Pro", "provider.webhook": "Webhook", "provider.westcn": "West.cn", "provider.zerossl": "ZeroSSL", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index d6728544..4ac796b3 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -95,7 +95,7 @@ "workflow_node.deploy.form.provider.placeholder": "Please select deploy target", "workflow_node.deploy.form.provider_access.label": "Host provider authorization", "workflow_node.deploy.form.provider_access.placeholder": "Please select an authorization of host provider", - "workflow_node.deploy.form.provider_access.tooltip": "Used to deploy certificates.", + "workflow_node.deploy.form.provider_access.tooltip": "Used to invoke API during deployment.", "workflow_node.deploy.form.provider_access.button": "Create", "workflow_node.deploy.form.provider_access.guide_for_local": "Tips: If you are running Certimate in Docker, the \"Local\" refers to the container rather than the host.", "workflow_node.deploy.form.certificate.label": "Certificate", @@ -269,11 +269,11 @@ "workflow_node.deploy.form.baiducloud_cdn_domain.label": "Baidu Cloud CDN domain", "workflow_node.deploy.form.baiducloud_cdn_domain.placeholder": "Please enter Baidu Cloud CDN domain name", "workflow_node.deploy.form.baiducloud_cdn_domain.tooltip": "For more information, see https://console.bce.baidu.com/cdn", - "workflow_node.deploy.form.baishan_cdn_domain.label": "Baishan CDN domain", - "workflow_node.deploy.form.baishan_cdn_domain.placeholder": "Please enter Baishan CDN domain name", + "workflow_node.deploy.form.baishan_cdn_domain.label": "Baishan Cloud CDN domain", + "workflow_node.deploy.form.baishan_cdn_domain.placeholder": "Please enter Baishan Cloud CDN domain name", "workflow_node.deploy.form.baishan_cdn_domain.tooltip": "For more information, see https://cdnx.console.baishan.com", - "workflow_node.deploy.form.baishan_cdn_certificate_id.label": "Baishan CDN certificate ID (Optional", - "workflow_node.deploy.form.baishan_cdn_certificate_id.placeholder": "Please enter Baishan CDN certificate ID", + "workflow_node.deploy.form.baishan_cdn_certificate_id.label": "Baishan Cloud CDN certificate ID (Optional)", + "workflow_node.deploy.form.baishan_cdn_certificate_id.placeholder": "Please enter Baishan Cloud CDN certificate ID", "workflow_node.deploy.form.baishan_cdn_certificate_id.tooltip": "For more information, see https://cdnx.console.baishan.com/#/cdn/cert", "workflow_node.deploy.form.baotapanel_console_auto_restart.label": "Auto restart after deployment", "workflow_node.deploy.form.baotapanel_site_type.label": "aaPanel site type", @@ -639,6 +639,19 @@ "workflow_node.deploy.form.volcengine_tos_domain.label": "VolcEngine TOS domain", "workflow_node.deploy.form.volcengine_tos_domain.placeholder": "Please enter VolcEngine TOS domain name", "workflow_node.deploy.form.volcengine_tos_domain.tooltip": "For more information, see https://console.volcengine.com/tos", + "workflow_node.deploy.form.wangsu_cdnpro_environment.label": "Wangsu Cloud environment", + "workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder": "Please select Wangsu Cloud environment", + "workflow_node.deploy.form.wangsu_cdnpro_environment.option.production.label": "Production environment", + "workflow_node.deploy.form.wangsu_cdnpro_environment.option.staging.label": "Staging environment", + "workflow_node.deploy.form.wangsu_cdnpro_domain.label": "Wangsu Cloud CDN domain", + "workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder": "Please enter Wangsu Cloud CDN domain name", + "workflow_node.deploy.form.wangsu_cdnpro_domain.tooltip": "For more information, see https://cdnpro.console.wangsu.com/v2/index/#/properties", + "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.label": "Wangsu Cloud CDN certificate ID (Optional)", + "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.placeholder": "Please enter Wangsu Cloud CDN certificate ID", + "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.tooltip": "For more information, see https://cdnpro.console.wangsu.com/v2/index/#/certificate", + "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.label": "Wangsu Cloud CDN Webhook ID (Optional)", + "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.placeholder": "Please enter Wangsu Cloud CDN Webhook ID", + "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.tooltip": "For more information, see https://cdnpro.console.wangsu.com/v2/index/#/certificate", "workflow_node.deploy.form.webhook_data.label": "Webhook data (JSON format)", "workflow_node.deploy.form.webhook_data.placeholder": "Please enter Webhook data", "workflow_node.deploy.form.webhook_data.guide": "Tips: The Webhook data should be a key-value pair in JSON format. The values in JSON support template variables, which will be replaced by actual values when sent to the Webhook URL.

Supported variables:
${DOMAIN}: The primary domain of the certificate (CommonName).
${DOMAINS}: The domain list of the certificate (SubjectAltNames).
${CERTIFICATE}: The PEM format content of the certificate file.
${PRIVATE_KEY}: The PEM format content of the private key file.", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 6022fa15..bf068260 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -298,6 +298,12 @@ "access.form.volcengine_secret_access_key.label": "火山引擎 SecretAccessKey", "access.form.volcengine_secret_access_key.placeholder": "请输入火山引擎 SecretAccessKey", "access.form.volcengine_secret_access_key.tooltip": "这是什么?请参阅 https://www.volcengine.com/docs/6291/216571", + "access.form.wangsu_access_key_id.label": "网宿云 AccessKeyId", + "access.form.wangsu_access_key_id.placeholder": "请输入网宿科技 AccessKeyId", + "access.form.wangsu_access_key_id.tooltip": "这是什么?请参阅 https://www.wangsu.com/document/account-manage/15775", + "access.form.wangsu_access_key_secret.label": "网宿科技 AccessKeySecret", + "access.form.wangsu_access_key_secret.placeholder": "请输入网宿科技 AccessKeySecret", + "access.form.wangsu_access_key_secret.tooltip": "这是什么?请参阅 https://www.wangsu.com/document/account-manage/15775", "access.form.webhook_url.label": "Webhook 回调地址", "access.form.webhook_url.placeholder": "请输入 Webhook 回调地址", "access.form.webhook_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 34ef3fb2..d6be56a2 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -125,6 +125,8 @@ "provider.volcengine.imagex": "火山引擎 - 图片服务 ImageX", "provider.volcengine.live": "火山引擎 - 视频直播 Live", "provider.volcengine.tos": "火山引擎 - 对象存储 TOS", + "provider.wangsu": "网宿云", + "provider.wangsu.cdnpro": "网宿云 - CDN Pro", "provider.webhook": "Webhook", "provider.westcn": "西部数码", "provider.zerossl": "ZeroSSL", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 7e50cc5b..5fcb201d 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -94,7 +94,7 @@ "workflow_node.deploy.form.provider.placeholder": "请选择部署目标", "workflow_node.deploy.form.provider_access.label": "主机提供商授权", "workflow_node.deploy.form.provider_access.placeholder": "请选择主机提供商授权", - "workflow_node.deploy.form.provider_access.tooltip": "用于部署证书,注意与申请阶段所需的 DNS 提供商相区分。", + "workflow_node.deploy.form.provider_access.tooltip": "用于部署证书时调用相关 API,注意与申请阶段所需的 DNS 提供商相区分。", "workflow_node.deploy.form.provider_access.button": "新建", "workflow_node.deploy.form.provider_access.guide_for_local": "小贴士:如果你正在使用 Docker 运行 Certimate,“本地”指的是容器内而非宿主机。", "workflow_node.deploy.form.certificate.label": "待部署证书", @@ -638,6 +638,19 @@ "workflow_node.deploy.form.volcengine_tos_domain.label": "火山引擎 TOS 自定义域名", "workflow_node.deploy.form.volcengine_tos_domain.placeholder": "请输入火山引擎 TOS 自定义域名", "workflow_node.deploy.form.volcengine_tos_domain.tooltip": "这是什么?请参阅 see https://console.volcengine.com/tos", + "workflow_node.deploy.form.wangsu_cdnpro_environment.label": "网宿云环境", + "workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder": "请选择网宿云环境", + "workflow_node.deploy.form.wangsu_cdnpro_environment.option.production.label": "生产环境", + "workflow_node.deploy.form.wangsu_cdnpro_environment.option.staging.label": "演练环境", + "workflow_node.deploy.form.wangsu_cdnpro_domain.label": "网宿云 CDN Pro 加速域名", + "workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder": "请输入网宿云 CDN Pro 加速域名(支持泛域名)", + "workflow_node.deploy.form.wangsu_cdnpro_domain.tooltip": "这是什么?请参阅 https://cdnpro.console.wangsu.com/v2/index/#/properties", + "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.label": "网宿云 CDN Pro 原证书 ID(可选)", + "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.placeholder": "请输入网宿云 CDN Pro 原证书 ID", + "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.tooltip": "这是什么?请参阅 https://cdnpro.console.wangsu.com/v2/index/#/certificate

不填写时,将上传新证书;否则,将替换原证书。", + "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.label": "网宿云 CDN Pro 部署任务 Webhook ID(可选)", + "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.placeholder": "请输入网宿云 CDN Pro 部署任务 Webhook ID", + "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.tooltip": "这是什么?请参阅 https://cdnpro.console.wangsu.com/v2/index/#/certificate", "workflow_node.deploy.form.webhook_data.label": "Webhook 回调数据(JSON 格式)", "workflow_node.deploy.form.webhook_data.placeholder": "请输入 Webhook 回调数据", "workflow_node.deploy.form.webhook_data.guide": "小贴士:回调数据是一个 JSON 格式的键值对。其中值支持模板变量,将在被发送到指定的 Webhook URL 时被替换为实际值;其他内容将保持原样。

支持的变量:
${DOMAIN}:证书的主域名(即 CommonName
${DOMAINS}:证书的多域名列表(即 SubjectAltNames
${CERTIFICATE}:证书文件 PEM 格式内容
${PRIVATE_KEY}:私钥文件 PEM 格式内容", From 4475ed0dea2540542c191d5bbad513a13c726780 Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Sun, 13 Apr 2025 08:54:05 +0800 Subject: [PATCH 20/22] resolve build error --- go.mod | 2 +- go.sum | 2 ++ internal/pkg/vendors/wangsu-sdk/cdn/api.go | 2 +- ui/src/pages/dashboard/Dashboard.tsx | 2 +- ui/src/utils/cron.ts | 8 ++++---- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 196b186b..c9cdb902 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 github.com/Azure/azure-sdk-for-go/sdk/keyvault/azcertificates v0.9.0 + github.com/Edgio/edgio-api v0.0.0-workspace github.com/G-Core/gcorelabscdn-go v1.0.28 github.com/alibabacloud-go/alb-20200616/v2 v2.2.8 github.com/alibabacloud-go/cas-20200407/v3 v3.0.4 @@ -71,7 +72,6 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect - github.com/Edgio/edgio-api v0.0.0-workspace // indirect github.com/alibabacloud-go/alibabacloud-gateway-fc-util v0.0.7 // indirect github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect diff --git a/go.sum b/go.sum index 98e8211f..f7267783 100644 --- a/go.sum +++ b/go.sum @@ -498,6 +498,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= diff --git a/internal/pkg/vendors/wangsu-sdk/cdn/api.go b/internal/pkg/vendors/wangsu-sdk/cdn/api.go index fd96ba2f..dff719f1 100644 --- a/internal/pkg/vendors/wangsu-sdk/cdn/api.go +++ b/internal/pkg/vendors/wangsu-sdk/cdn/api.go @@ -53,6 +53,6 @@ func (c *Client) CreateDeploymentTask(req *CreateDeploymentTaskRequest) (*Create func (c *Client) GetDeploymentTaskDetail(deploymentTaskId string) (*GetDeploymentTaskDetailResponse, error) { resp := &GetDeploymentTaskDetailResponse{} - _, err := c.client.SendRequestWithResult(http.MethodGet, fmt.Sprintf("/cdn/deploymentTasks/%s", url.PathEscape(hostname)), nil, resp) + _, err := c.client.SendRequestWithResult(http.MethodGet, fmt.Sprintf("/cdn/deploymentTasks/%s", deploymentTaskId), nil, resp) return resp, err } diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx index 9915a8a7..919b53d3 100644 --- a/ui/src/pages/dashboard/Dashboard.tsx +++ b/ui/src/pages/dashboard/Dashboard.tsx @@ -310,7 +310,7 @@ const StatisticCard = ({ onClick?: () => void; }) => { return ( - + {icon} { try { - CronExpressionParser.parse(expr); + parseExpression(expr); if (expr.trim().split(" ").length !== 5) return false; // pocketbase 后端仅支持五段式的表达式 return true; @@ -15,7 +15,7 @@ export const getNextCronExecutions = (expr: string, times = 1): Date[] => { if (!validCronExpression(expr)) return []; const now = new Date(); - const cron = CronExpressionParser.parse(expr, { currentDate: now }); + const cron = parseExpression(expr, { currentDate: now }); - return cron.take(times).map((date) => date.toDate()); + return cron.iterate(times).map((date) => date.toDate()); }; From 44a6190e1759cbb69e8d54ce1cfd238ee37179b9 Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Sun, 13 Apr 2025 09:14:08 +0800 Subject: [PATCH 21/22] resolve build error --- ui/src/utils/cron.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/src/utils/cron.ts b/ui/src/utils/cron.ts index 6bccd729..46d806e3 100644 --- a/ui/src/utils/cron.ts +++ b/ui/src/utils/cron.ts @@ -1,8 +1,8 @@ -import { parseExpression } from "cron-parser"; +import { CronExpressionParser } from "cron-parser"; export const validCronExpression = (expr: string): boolean => { try { - parseExpression(expr); + CronExpressionParser.parse(expr); if (expr.trim().split(" ").length !== 5) return false; // pocketbase 后端仅支持五段式的表达式 return true; @@ -15,7 +15,7 @@ export const getNextCronExecutions = (expr: string, times = 1): Date[] => { if (!validCronExpression(expr)) return []; const now = new Date(); - const cron = parseExpression(expr, { currentDate: now }); + const cron = CronExpressionParser.parse(expr, { currentDate: now }); - return cron.iterate(times).map((date) => date.toDate()); + return cron.take(times).map((date) => date.toDate()); }; From 88b90986b11dc5f535bdd32fbc8c4984dd3dc4f5 Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Sun, 13 Apr 2025 20:55:33 +0800 Subject: [PATCH 22/22] update to version v0.3.8 --- ui/src/domain/version.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/domain/version.ts b/ui/src/domain/version.ts index f437dc74..8818dd83 100644 --- a/ui/src/domain/version.ts +++ b/ui/src/domain/version.ts @@ -1 +1 @@ -export const version = "v0.3.7"; +export const version = "v0.3.8";