Compare commits

..

41 Commits

Author SHA1 Message Date
RHQYZ
12d0b66c61 Merge pull request #691 from fudiwei/main
refactor
2025-05-16 11:15:38 +08:00
Fu Diwei
f3bbb9e8b2 refactor: optimize third-party sdks 2025-05-16 11:10:43 +08:00
RHQYZ
dcd646b465 Merge branch 'usual2970:main' into main 2025-05-16 02:52:51 +08:00
Fu Diwei
b122bbced9 build: config goreleaser 2025-05-16 02:26:38 +08:00
Fu Diwei
5edae845cb build: config goreleaser 2025-05-16 02:03:49 +08:00
Fu Diwei
b7af3c10e4 build: config goreleaser 2025-05-16 01:58:53 +08:00
Fu Diwei
2453048288 build: config goreleaser 2025-05-16 01:50:43 +08:00
Fu Diwei
221b6a6fc6 feat(ui): improve i18n 2025-05-16 01:34:05 +08:00
Fu Diwei
851ad70a6c bump version to v0.3.12 2025-05-15 23:52:21 +08:00
Fu Diwei
1844e9a9db update README 2025-05-15 23:41:25 +08:00
RHQYZ
04e9f4e909 Merge pull request #689 from fudiwei/feat/providers
new providers
2025-05-15 23:35:48 +08:00
Fu Diwei
e6e2587bfc feat: new deployment provider: netlify site 2025-05-15 23:25:48 +08:00
Fu Diwei
70bd2f0581 feat: new acme dns-01 provider: netlify 2025-05-15 23:25:48 +08:00
Fu Diwei
9e08cfd1d1 feat: support replacing old certificate on deployment to aws acm 2025-05-15 23:25:48 +08:00
Fu Diwei
cd93a2d72c feat: new acme dns-01 provider: netcup 2025-05-15 23:25:42 +08:00
RHQYZ
11a4d4f55c Merge pull request #684 from fudiwei/main
enhance & bugfix
2025-05-15 21:16:17 +08:00
RHQYZ
f33570d514 Merge branch 'usual2970:main' into main 2025-05-15 21:15:49 +08:00
Fu Diwei
1ee3b64a19 feat: config goedge api user role 2025-05-15 21:05:22 +08:00
Fu Diwei
a3a56f3346 feat: add preset scripts for synologydsm and fnos on deployment to ssh 2025-05-15 20:52:43 +08:00
Fu Diwei
564ed8f0d3 fix: #686 2025-05-15 17:52:24 +08:00
Fu Diwei
268ec4bd7f feat(ui): browser happy detecting 2025-05-15 02:20:47 +08:00
Fu Diwei
e55e6cc512 fix(ui): tsc build error 2025-05-15 01:57:00 +08:00
Fu Diwei
6c6bb78568 feat: deploy server certificate or intermedia certificate 2025-05-15 01:40:37 +08:00
Fu Diwei
178c62512d fix(ui): fix incorrect webhook guide 2025-05-15 01:16:29 +08:00
Fu Diwei
9eaf9fd933 feat(ui): CodeInput 2025-05-15 00:58:02 +08:00
Fu Diwei
04abf9dd76 feat(ui): TextFileInput 2025-05-14 00:42:59 +08:00
Fu Diwei
355059df3c fix(ui): disallow to create access of builtin providers 2025-05-13 22:32:33 +08:00
Fu Diwei
4a6c32877f chore: github issue templates 2025-05-13 22:00:15 +08:00
Fu Diwei
258f6b5001 fix: resolve #681 2025-05-13 21:33:59 +08:00
Fu Diwei
78d9501fce fix: fix typo 2025-05-13 21:22:01 +08:00
RHQYZ
15975bb92c Merge branch 'usual2970:main' into main 2025-05-13 21:19:13 +08:00
RHQYZ
4b226d7730 Merge branch 'usual2970:main' into main 2025-05-13 01:00:07 +08:00
Fu Diwei
2ec4adf7d5 fix(ui): tsc build error 2025-05-13 00:52:05 +08:00
Fu Diwei
709684d00f fix(ui): scale origin 2025-05-13 00:43:58 +08:00
Fu Diwei
989fd1ec6d feat(ui): max content height on AccessEditModal 2025-05-13 00:39:18 +08:00
Fu Diwei
7d57c5abc0 feat(ui): show search on AccessSelect 2025-05-13 00:33:48 +08:00
Fu Diwei
0c42bb845d feat(ui): AccessProviderPicker 2025-05-13 00:28:58 +08:00
Fu Diwei
07037e8d49 feat: support replacing old certificate on deployment to gcore cdn 2025-05-12 23:19:13 +08:00
Fu Diwei
a1fc3841df chore(deps): upgrade gomod dependencies 2025-05-12 23:00:39 +08:00
Fu Diwei
1fee156457 chore(deps): upgrade npm dependencies 2025-05-12 22:12:59 +08:00
Fu Diwei
82bdccf7e7 feat(ui): move settings page entry from topbar to sider-menu 2025-05-12 21:34:00 +08:00
115 changed files with 2005 additions and 437 deletions

View File

@@ -10,21 +10,23 @@ body:
## Welcome!
**在提交 Issue 之前,请确认以下事项**
1. 我**确认**已尝试过使用当前最新版本,并能复现问题。
1. 我**确认**已尝试过使用当前最新版本,并能复现问题。由于开发者精力有限,非当前最新版本的问题将被直接关闭,感谢理解。
2. 我**确认**已搜索过[已有的 Issues](https://github.com/usual2970/certimate/issues)(包括已关闭的),没有类似的问题。
3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。
4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。
5. 请保持每个 Issue 只包含一个缺陷报告。如果有多个缺陷,请分别提交 Issue。
**Before you submit the issue, please make sure of the following checklist**:
1. Yes, I'm using the latest release and can reproduce the issue.
1. Yes, I'm using the latest release and can reproduce the issue. Issues that are not in the latest version will be closed directly.
2. Yes, I've searched for [existing issues](https://github.com/usual2970/certimate/issues) (including closed ones) on GitHub and didn't find any similar.
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.
5. Please limit one report per issue.
- type: input
attributes:
label: 软件版本 / Release Version
description: 请提供 Certimate 的具体版本。 / Please provide the specific version of Certimate.
description: 请提供 Certimate 的具体版本(请不要填写 `latest` 之类的无效版本号)。 / Please provide the specific version of Certimate.
placeholder: (e.g. v1.0.0)
validations:
required: true
@@ -70,8 +72,9 @@ body:
validations:
required: false
- type: markdown
- type: checkboxes
attributes:
value: |
请保持每个 Issue 只包含一个缺陷报告。
Please limit one report per issue.
label: 贡献 / Contribution
options:
- label: 我乐意为此贡献代码! / I am interested in contributing to this issue!
required: false

View File

@@ -14,12 +14,14 @@ body:
2. 我**确认**已搜索过[已有的 Issues](https://github.com/usual2970/certimate/issues)(包括已关闭的),没有类似的问题。
3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。
4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。
5. 请保持每个 Issue 只包含一个功能请求。如果有多个需求,请分别提交 Issue。
**Before you submit the issue, please make sure of the following checklist**:
1. Yes, I'm using the latest release.
2. Yes, I've searched for [existing issues](https://github.com/usual2970/certimate/issues) (including closed ones) on GitHub and didn't find any similar.
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.
5. Please limit one request per issue.
- type: textarea
attributes:
@@ -38,7 +40,7 @@ body:
- type: textarea
attributes:
label: 其他 / Miscellaneous
description: 在此处添加关于该 Issue 的任何其他信息。 / Add any other context about the problem here.
description: 在此处添加关于该 Issue 的任何其他信息(新增提供商请求请补充 API 文档链接等资料)。 / Add any other context about the problem here.
validations:
required: false
@@ -46,11 +48,5 @@ body:
attributes:
label: 贡献 / Contribution
options:
- label: 我乐意为此贡献代码! / I am interested in contributing to this feature!
- label: 我乐意为此贡献代码! / I am interested in contributing to this issue!
required: false
- type: markdown
attributes:
value: |
请保持每个 Issue 只包含一个功能请求。
Please limit one request per issue.

View File

@@ -3,7 +3,7 @@ description: "遇到了困难需要求助? / Have problem in use and need help
title: "简要描述你遇到的问题"
body:
- type: markdown
attributes:
attributes:
value: |
## Welcome!
@@ -12,17 +12,19 @@ body:
2. 我**确认**已搜索过[已有的 Issues](https://github.com/usual2970/certimate/issues)(包括已关闭的),没有类似的问题。
3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。
4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。
5. 请保持每个 Issue 只包含一个问题求助。如果有多个问题,请分别提交 Issue。
**Before you submit the issue, please make sure of the following checklist**:
1. Yes, I'm using the latest release.
2. Yes, I've searched for [existing issues](https://github.com/usual2970/certimate/issues) (including closed ones) on GitHub and didn't find any similar.
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.
5. Please limit one question per issue.
- type: input
attributes:
label: 软件版本 / Release Version
description: 请提供 Certimate 的具体版本。 / Please provide the specific version of Certimate.
description: 请提供 Certimate 的具体版本(请不要填写 `latest` 之类的无效版本号)。 / Please provide the specific version of Certimate.
placeholder: (e.g. v1.0.0)
validations:
required: true
@@ -40,9 +42,3 @@ body:
description: 在此处添加关于该问题的任何其他信息。 / Add any other context about the problem here.
validations:
required: false
- type: markdown
attributes:
value: |
请保持每个 Issue 只包含一个问题求助。
Please limit one question per issue.

View File

@@ -1,4 +1,4 @@
name: Base Build
name: Release
on:
push:
@@ -22,13 +22,22 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ">=1.24.0"
go-version-file: "go.mod"
- name: Build WebUI
run: npm --prefix=./ui ci && npm --prefix=./ui run build
run: |
npm --prefix=./ui ci
npm --prefix=./ui run build
npm cache clean --force
rm -rf ./ui/node_modules
- name: Check disk usage
run: |
df -h
du -sh /opt/hostedtoolcache/go/*
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v3
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest

View File

@@ -36,7 +36,7 @@ release:
archives:
- id: archive_noncgo
builds: [build_noncgo]
format: zip
format: "zip"
files:
- CHANGELOG.md
- LICENSE.md

View File

@@ -9,7 +9,7 @@
## 前提条件
- Go 1.22+ (用于修改 Go 代码)
- Go 1.24+ (用于修改 Go 代码)
- Node 20+ (用于修改 UI)
如果还没有这样做,你可以 fork Certimate 的主仓库,并克隆到本地以便进行修改:

View File

@@ -9,7 +9,7 @@ Thank you for taking the time to improve Certimate! Below is a guide for submitt
## Prerequisites
- Go 1.22+ (for Go code changes)
- Go 1.24+ (for Go code changes)
- Node 20+ (for Admin UI changes)
If you haven't done so already, you can fork the Certimate repository and clone your fork to work locally:

View File

@@ -38,8 +38,8 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
- 灵活的工作流编排方式,证书从申请到部署完全自动化;
- 支持单域名、多域名、泛域名证书,可选 RSA、ECC 签名算法;
- 支持 PEM、PFX、JKS 等多种格式输出证书;
- 支持 20+ 域名托管商如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers)
- 支持 70+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-host-providers)
- 支持 30+ 域名托管商如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers)
- 支持 80+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-host-providers)
- 支持邮件、钉钉、飞书、企业微信、Webhook 等多种通知渠道;
- 支持 Let's Encrypt、Buypass、Google Trust Services、SSL.com、ZeroSSL 等多种 ACME 证书颁发机构;
- 更多特性等待探索。

View File

@@ -38,8 +38,8 @@ Certimate aims to provide users with a secure and user-friendly SSL certificate
- Flexible workflow orchestration, fully automation from certificate application to deployment;
- Supports single-domain, multi-domain, wildcard certificates, with options for RSA or ECC.
- Supports various certificate formats such as PEM, PFX, JKS.
- Supports more than 20+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-dns-providers));
- Supports more than 70+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-host-providers));
- Supports more than 30+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-dns-providers));
- Supports more than 80+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-host-providers));
- Supports multiple notification channels including email, DingTalk, Feishu, WeCom, Webhook, and more;
- Supports multiple ACME CAs including Let's Encrypt, Buypass, Google Trust ServicesSSL.com, ZeroSSL, and more;
- More features waiting to be discovered.

View File

@@ -27,6 +27,8 @@ import (
pNamecheap "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namecheap"
pNameDotCom "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namedotcom"
pNameSilo "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namesilo"
pNetcup "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/netcup"
pNetlify "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/netlify"
pNS1 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ns1"
pPorkbun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/porkbun"
pPowerDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns"
@@ -402,6 +404,38 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi
return applicant, err
}
case domain.ACMEDns01ProviderTypeNetcup:
{
access := domain.AccessConfigForNetcup{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pNetcup.NewChallengeProvider(&pNetcup.ChallengeProviderConfig{
CustomerNumber: access.CustomerNumber,
ApiKey: access.ApiKey,
ApiPassword: access.ApiPassword,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeNetlify:
{
access := domain.AccessConfigForNetlify{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pNetlify.NewChallengeProvider(&pNetlify.ChallengeProviderConfig{
ApiToken: access.ApiToken,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeNS1:
{
access := domain.AccessConfigForNS1{}

View File

@@ -52,6 +52,7 @@ import (
pJDCloudVOD "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/jdcloud-vod"
pK8sSecret "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/k8s-secret"
pLocal "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local"
pNetlifySite "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/netlify-site"
pProxmoxVE "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/proxmoxve"
pQiniuCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn"
pQiniuPili "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-pili"
@@ -306,6 +307,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Region: maputil.GetString(options.ProviderExtendedConfig, "region"),
CertificateArn: maputil.GetString(options.ProviderExtendedConfig, "certificateArn"),
})
return deployer, err
@@ -581,6 +583,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
deployer, err := pGoEdge.NewDeployer(&pGoEdge.DeployerConfig{
ApiUrl: access.ApiUrl,
ApiRole: access.ApiRole,
AccessKeyId: access.AccessKeyId,
AccessKey: access.AccessKey,
AllowInsecureConnections: access.AllowInsecureConnections,
@@ -693,16 +696,18 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
case domain.DeploymentProviderTypeLocal:
{
deployer, err := pLocal.NewDeployer(&pLocal.DeployerConfig{
ShellEnv: pLocal.ShellEnvType(maputil.GetString(options.ProviderExtendedConfig, "shellEnv")),
PreCommand: maputil.GetString(options.ProviderExtendedConfig, "preCommand"),
PostCommand: maputil.GetString(options.ProviderExtendedConfig, "postCommand"),
OutputFormat: pLocal.OutputFormatType(maputil.GetOrDefaultString(options.ProviderExtendedConfig, "format", string(pLocal.OUTPUT_FORMAT_PEM))),
OutputCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPath"),
OutputKeyPath: maputil.GetString(options.ProviderExtendedConfig, "keyPath"),
PfxPassword: maputil.GetString(options.ProviderExtendedConfig, "pfxPassword"),
JksAlias: maputil.GetString(options.ProviderExtendedConfig, "jksAlias"),
JksKeypass: maputil.GetString(options.ProviderExtendedConfig, "jksKeypass"),
JksStorepass: maputil.GetString(options.ProviderExtendedConfig, "jksStorepass"),
ShellEnv: pLocal.ShellEnvType(maputil.GetString(options.ProviderExtendedConfig, "shellEnv")),
PreCommand: maputil.GetString(options.ProviderExtendedConfig, "preCommand"),
PostCommand: maputil.GetString(options.ProviderExtendedConfig, "postCommand"),
OutputFormat: pLocal.OutputFormatType(maputil.GetOrDefaultString(options.ProviderExtendedConfig, "format", string(pLocal.OUTPUT_FORMAT_PEM))),
OutputCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPath"),
OutputServerCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPathForServerOnly"),
OutputIntermediaCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPathForIntermediaOnly"),
OutputKeyPath: maputil.GetString(options.ProviderExtendedConfig, "keyPath"),
PfxPassword: maputil.GetString(options.ProviderExtendedConfig, "pfxPassword"),
JksAlias: maputil.GetString(options.ProviderExtendedConfig, "jksAlias"),
JksKeypass: maputil.GetString(options.ProviderExtendedConfig, "jksKeypass"),
JksStorepass: maputil.GetString(options.ProviderExtendedConfig, "jksStorepass"),
})
return deployer, err
}
@@ -725,6 +730,20 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
return deployer, err
}
case domain.DeploymentProviderTypeNetlifySite:
{
access := domain.AccessConfigForNetlify{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
deployer, err := pNetlifySite.NewDeployer(&pNetlifySite.DeployerConfig{
ApiToken: access.ApiToken,
SiteId: maputil.GetString(options.ProviderExtendedConfig, "siteId"),
})
return deployer, err
}
case domain.DeploymentProviderTypeProxmoxVE:
{
access := domain.AccessConfigForProxmoxVE{}
@@ -819,22 +838,24 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
}
deployer, err := pSSH.NewDeployer(&pSSH.DeployerConfig{
SshHost: access.Host,
SshPort: access.Port,
SshUsername: access.Username,
SshPassword: access.Password,
SshKey: access.Key,
SshKeyPassphrase: access.KeyPassphrase,
UseSCP: maputil.GetBool(options.ProviderExtendedConfig, "useSCP"),
PreCommand: maputil.GetString(options.ProviderExtendedConfig, "preCommand"),
PostCommand: maputil.GetString(options.ProviderExtendedConfig, "postCommand"),
OutputFormat: pSSH.OutputFormatType(maputil.GetOrDefaultString(options.ProviderExtendedConfig, "format", string(pSSH.OUTPUT_FORMAT_PEM))),
OutputCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPath"),
OutputKeyPath: maputil.GetString(options.ProviderExtendedConfig, "keyPath"),
PfxPassword: maputil.GetString(options.ProviderExtendedConfig, "pfxPassword"),
JksAlias: maputil.GetString(options.ProviderExtendedConfig, "jksAlias"),
JksKeypass: maputil.GetString(options.ProviderExtendedConfig, "jksKeypass"),
JksStorepass: maputil.GetString(options.ProviderExtendedConfig, "jksStorepass"),
SshHost: access.Host,
SshPort: access.Port,
SshUsername: access.Username,
SshPassword: access.Password,
SshKey: access.Key,
SshKeyPassphrase: access.KeyPassphrase,
UseSCP: maputil.GetBool(options.ProviderExtendedConfig, "useSCP"),
PreCommand: maputil.GetString(options.ProviderExtendedConfig, "preCommand"),
PostCommand: maputil.GetString(options.ProviderExtendedConfig, "postCommand"),
OutputFormat: pSSH.OutputFormatType(maputil.GetOrDefaultString(options.ProviderExtendedConfig, "format", string(pSSH.OUTPUT_FORMAT_PEM))),
OutputCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPath"),
OutputServerCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPathForServerOnly"),
OutputIntermediaCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPathForIntermediaOnly"),
OutputKeyPath: maputil.GetString(options.ProviderExtendedConfig, "keyPath"),
PfxPassword: maputil.GetString(options.ProviderExtendedConfig, "pfxPassword"),
JksAlias: maputil.GetString(options.ProviderExtendedConfig, "jksAlias"),
JksKeypass: maputil.GetString(options.ProviderExtendedConfig, "jksKeypass"),
JksStorepass: maputil.GetString(options.ProviderExtendedConfig, "jksStorepass"),
})
return deployer, err
}

View File

@@ -149,6 +149,7 @@ type AccessConfigForGoDaddy struct {
type AccessConfigForGoEdge struct {
ApiUrl string `json:"apiUrl"`
ApiRole string `json:"apiRole"`
AccessKeyId string `json:"accessKeyId"`
AccessKey string `json:"accessKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
@@ -198,6 +199,16 @@ type AccessConfigForNameSilo struct {
ApiKey string `json:"apiKey"`
}
type AccessConfigForNetcup struct {
CustomerNumber string `json:"customerNumber"`
ApiKey string `json:"apiKey"`
ApiPassword string `json:"apiPassword"`
}
type AccessConfigForNetlify struct {
ApiToken string `json:"apiToken"`
}
type AccessConfigForNS1 struct {
ApiKey string `json:"apiKey"`
}

View File

@@ -10,6 +10,7 @@ type AccessProviderType string
*/
const (
AccessProviderType1Panel = AccessProviderType("1panel")
AccessProviderTypeACMECA = AccessProviderType("acmeca") // ACME CA预留
AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq")
AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai预留
AccessProviderTypeAliyun = AccessProviderType("aliyun")
@@ -35,7 +36,8 @@ const (
AccessProviderTypeDynv6 = AccessProviderType("dynv6")
AccessProviderTypeEdgio = AccessProviderType("edgio")
AccessProviderTypeEmail = AccessProviderType("email")
AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly预留
AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly预留
AccessProviderTypeFlexCDN = AccessProviderType("flexcdn") // FlexCDN预留
AccessProviderTypeGname = AccessProviderType("gname")
AccessProviderTypeGcore = AccessProviderType("gcore")
AccessProviderTypeGoDaddy = AccessProviderType("godaddy")
@@ -47,11 +49,14 @@ const (
AccessProviderTypeLarkBot = AccessProviderType("larkbot")
AccessProviderTypeLetsEncrypt = AccessProviderType("letsencrypt")
AccessProviderTypeLetsEncryptStaging = AccessProviderType("letsencryptstaging")
AccessProviderTypeLeCDN = AccessProviderType("lecdn") // LeCDN预留
AccessProviderTypeLocal = AccessProviderType("local")
AccessProviderTypeMattermost = AccessProviderType("mattermost")
AccessProviderTypeNamecheap = AccessProviderType("namecheap")
AccessProviderTypeNameDotCom = AccessProviderType("namedotcom")
AccessProviderTypeNameSilo = AccessProviderType("namesilo")
AccessProviderTypeNetcup = AccessProviderType("netcup")
AccessProviderTypeNetlify = AccessProviderType("netlify")
AccessProviderTypeNS1 = AccessProviderType("ns1")
AccessProviderTypePorkbun = AccessProviderType("porkbun")
AccessProviderTypePowerDNS = AccessProviderType("powerdns")
@@ -130,6 +135,8 @@ const (
ACMEDns01ProviderTypeNamecheap = ACMEDns01ProviderType(AccessProviderTypeNamecheap)
ACMEDns01ProviderTypeNameDotCom = ACMEDns01ProviderType(AccessProviderTypeNameDotCom)
ACMEDns01ProviderTypeNameSilo = ACMEDns01ProviderType(AccessProviderTypeNameSilo)
ACMEDns01ProviderTypeNetcup = ACMEDns01ProviderType(AccessProviderTypeNetcup)
ACMEDns01ProviderTypeNetlify = ACMEDns01ProviderType(AccessProviderTypeNetlify)
ACMEDns01ProviderTypeNS1 = ACMEDns01ProviderType(AccessProviderTypeNS1)
ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun)
ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS)
@@ -165,6 +172,7 @@ const (
DeploymentProviderTypeAliyunDDoS = DeploymentProviderType(AccessProviderTypeAliyun + "-ddos")
DeploymentProviderTypeAliyunESA = DeploymentProviderType(AccessProviderTypeAliyun + "-esa")
DeploymentProviderTypeAliyunFC = DeploymentProviderType(AccessProviderTypeAliyun + "-fc")
DeploymentProviderTypeAliyunGA = DeploymentProviderType(AccessProviderTypeAliyun + "-ga") // 阿里云全球加速(预留)
DeploymentProviderTypeAliyunLive = DeploymentProviderType(AccessProviderTypeAliyun + "-live")
DeploymentProviderTypeAliyunNLB = DeploymentProviderType(AccessProviderTypeAliyun + "-nlb")
DeploymentProviderTypeAliyunOSS = DeploymentProviderType(AccessProviderTypeAliyun + "-oss")
@@ -186,6 +194,7 @@ const (
DeploymentProviderTypeCdnfly = DeploymentProviderType(AccessProviderTypeCdnfly)
DeploymentProviderTypeDogeCloudCDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-cdn")
DeploymentProviderTypeEdgioApplications = DeploymentProviderType(AccessProviderTypeEdgio + "-applications")
DeploymentProviderTypeFlexCDN = DeploymentProviderType(AccessProviderTypeFlexCDN) // FlexCDN预留
DeploymentProviderTypeGcoreCDN = DeploymentProviderType(AccessProviderTypeGcore + "-cdn")
DeploymentProviderTypeGoEdge = DeploymentProviderType(AccessProviderTypeGoEdge)
DeploymentProviderTypeHuaweiCloudCDN = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-cdn")
@@ -197,7 +206,9 @@ const (
DeploymentProviderTypeJDCloudLive = DeploymentProviderType(AccessProviderTypeJDCloud + "-live")
DeploymentProviderTypeJDCloudVOD = DeploymentProviderType(AccessProviderTypeJDCloud + "-vod")
DeploymentProviderTypeKubernetesSecret = DeploymentProviderType(AccessProviderTypeKubernetes + "-secret")
DeploymentProviderTypeLeCDN = DeploymentProviderType(AccessProviderTypeLeCDN) // LeCDN预留
DeploymentProviderTypeLocal = DeploymentProviderType(AccessProviderTypeLocal)
DeploymentProviderTypeNetlifySite = DeploymentProviderType(AccessProviderTypeNetlify + "-site")
DeploymentProviderTypeProxmoxVE = DeploymentProviderType(AccessProviderTypeProxmoxVE)
DeploymentProviderTypeQiniuCDN = DeploymentProviderType(AccessProviderTypeQiniu + "-cdn")
DeploymentProviderTypeQiniuKodo = DeploymentProviderType(AccessProviderTypeQiniu + "-kodo")
@@ -228,7 +239,9 @@ const (
DeploymentProviderTypeVolcEngineImageX = DeploymentProviderType(AccessProviderTypeVolcEngine + "-imagex")
DeploymentProviderTypeVolcEngineLive = DeploymentProviderType(AccessProviderTypeVolcEngine + "-live")
DeploymentProviderTypeVolcEngineTOS = DeploymentProviderType(AccessProviderTypeVolcEngine + "-tos")
DeploymentProviderTypeWangsuCDN = DeploymentProviderType(AccessProviderTypeWangsu + "-cdn") // 网宿 CDN预留
DeploymentProviderTypeWangsuCDNPro = DeploymentProviderType(AccessProviderTypeWangsu + "-cdnpro")
DeploymentProviderTypeWangsuCert = DeploymentProviderType(AccessProviderTypeWangsu + "-cert") // 网宿证书管理(预留)
DeploymentProviderTypeWebhook = DeploymentProviderType(AccessProviderTypeWebhook)
)

View File

@@ -0,0 +1,40 @@
package netcup
import (
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/providers/dns/netcup"
)
type ChallengeProviderConfig struct {
CustomerNumber string `json:"customerNumber"`
ApiKey string `json:"apiKey"`
ApiPassword string `json:"apiPassword"`
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
DnsTTL int32 `json:"dnsTTL,omitempty"`
}
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
if config == nil {
panic("config is nil")
}
providerConfig := netcup.NewDefaultConfig()
providerConfig.Customer = config.CustomerNumber
providerConfig.Key = config.ApiKey
providerConfig.Password = config.ApiPassword
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
}
if config.DnsTTL != 0 {
providerConfig.TTL = int(config.DnsTTL)
}
provider, err := netcup.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -0,0 +1,36 @@
package netcup
import (
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/providers/dns/netlify"
)
type ChallengeProviderConfig struct {
ApiToken string `json:"apiToken"`
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
DnsTTL int32 `json:"dnsTTL,omitempty"`
}
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
if config == nil {
panic("config is nil")
}
providerConfig := netlify.NewDefaultConfig()
providerConfig.Token = config.ApiToken
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
}
if config.DnsTTL != 0 {
providerConfig.TTL = int(config.DnsTTL)
}
provider, err := netlify.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"log/slog"
"strings"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
alislb "github.com/alibabacloud-go/slb-20140515/v4/client"
@@ -310,22 +309,10 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alislb.Clien
}
func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) {
casRegion := region
if casRegion != "" {
// 阿里云 CAS 服务接入点是独立于 CLB 服务的
// 国内版固定接入点:华东一杭州
// 国际版固定接入点:亚太东南一新加坡
if !strings.HasPrefix(casRegion, "cn-") {
casRegion = "ap-southeast-1"
} else {
casRegion = "cn-hangzhou"
}
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: accessKeyId,
AccessKeySecret: accessKeySecret,
Region: casRegion,
Region: region,
})
return uploader, err
}

View File

@@ -5,9 +5,15 @@ import (
"fmt"
"log/slog"
aws "github.com/aws/aws-sdk-go-v2/aws"
awscfg "github.com/aws/aws-sdk-go-v2/config"
awscred "github.com/aws/aws-sdk-go-v2/credentials"
awsacm "github.com/aws/aws-sdk-go-v2/service/acm"
"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/aws-acm"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
)
type DeployerConfig struct {
@@ -17,11 +23,15 @@ type DeployerConfig struct {
SecretAccessKey string `json:"secretAccessKey"`
// AWS 区域。
Region string `json:"region"`
// ACM 证书 ARN。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateArn string `json:"certificateArn,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *awsacm.Client
sslUploader uploader.Uploader
}
@@ -32,6 +42,11 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
@@ -44,6 +59,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslUploader: uploader,
}, nil
}
@@ -59,13 +75,48 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 上传证书到 ACM
upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
if d.config.CertificateArn == "" {
// 上传证书到 ACM
upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
// 提取服务器证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 导入证书
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ImportCertificate.html
importCertificateReq := &awsacm.ImportCertificateInput{
CertificateArn: aws.String(d.config.CertificateArn),
Certificate: ([]byte)(serverCertPEM),
CertificateChain: ([]byte)(intermediaCertPEM),
PrivateKey: ([]byte)(privkeyPEM),
}
importCertificateResp, err := d.sdkClient.ImportCertificate(context.TODO(), importCertificateReq)
d.logger.Debug("sdk request 'acm.ImportCertificate'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'acm.ImportCertificate': %w", err)
}
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, secretAccessKey, region string) (*awsacm.Client, error) {
cfg, err := awscfg.LoadDefaultConfig(context.TODO())
if err != nil {
return nil, err
}
client := awsacm.NewFromConfig(cfg, func(o *awsacm.Options) {
o.Region = region
o.Credentials = aws.NewCredentialsCache(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, ""))
})
return client, nil
}

View File

@@ -32,7 +32,7 @@ type DeployerConfig struct {
// Key Vault 名称。
KeyVaultName string `json:"keyvaultName"`
// Key Vault 证书名称。
// 选填。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateName string `json:"certificateName,omitempty"`
}

View File

@@ -20,7 +20,7 @@ type DeployerConfig struct {
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
// 证书 ID。
// 选填。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateId string `json:"certificateId,omitempty"`
}

View File

@@ -20,11 +20,11 @@ type DeployerConfig struct {
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 站类型。
// 站类型。
SiteType string `json:"siteType"`
// 站名称(单个)。
// 站名称(单个)。
SiteName string `json:"siteName,omitempty"`
// 站名称(多个)。
// 站名称(多个)。
SiteNames []string `json:"siteNames,omitempty"`
}

View File

@@ -51,6 +51,7 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 上传证书
// REF: https://api.cachefly.com/api/2.5/docs#tag/Certificates/paths/~1certificates/post
createCertificateReq := &cfsdk.CreateCertificateRequest{
Certificate: certPEM,
CertificateKey: privkeyPEM,

View File

@@ -56,18 +56,18 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 提取 Edgio 所需的服务证书和中间证书内容
privateCertPEM, intermediateCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
// 提取服务证书和中间证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 上传 TLS 证书
// REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts
uploadTlsCertReq := edgiodtos.UploadTlsCertRequest{
EnvironmentID: d.config.EnvironmentId,
PrimaryCert: privateCertPEM,
IntermediateCert: intermediateCertPEM,
PrimaryCert: serverCertPEM,
IntermediateCert: intermediaCertPEM,
PrivateKey: privkeyPEM,
}
uploadTlsCertResp, err := d.sdkClient.UploadTlsCert(uploadTlsCertReq)

View File

@@ -24,7 +24,7 @@ type DeployerConfig struct {
// CDN 资源 ID。
ResourceId int64 `json:"resourceId"`
// 证书 ID。
// 选填。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateId int64 `json:"certificateId,omitempty"`
}
@@ -112,7 +112,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
ValidateRootCA: false,
}
changeCertificateResp, err := d.sdkClients.SSLCerts.Update(context.TODO(), getCertificateDetailResp.ID, changeCertificateReq)
d.logger.Debug("sdk request 'sslcerts.Create'", slog.Any("request", changeCertificateReq), slog.Any("response", changeCertificateResp))
d.logger.Debug("sdk request 'sslcerts.Update'", slog.Any("sslId", getCertificateDetailResp.ID), slog.Any("request", changeCertificateReq), slog.Any("response", changeCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'sslcerts.Update': %w", err)
}

View File

@@ -18,9 +18,11 @@ import (
type DeployerConfig struct {
// GoEdge URL。
ApiUrl string `json:"apiUrl"`
// GoEdge 用户 AccessKeyId
// GoEdge 用户角色
ApiRole string `json:"apiRole"`
// GoEdge AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// GoEdge 用户 AccessKey。
// GoEdge AccessKey。
AccessKey string `json:"accessKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
@@ -44,7 +46,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
panic("config is nil")
}
client, err := createSdkClient(config.ApiUrl, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections)
client, err := createSdkClient(config.ApiUrl, config.ApiRole, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
@@ -116,11 +118,15 @@ func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM stri
return nil
}
func createSdkClient(apiUrl, accessKeyId, accessKey string, skipTlsVerify bool) (*goedgesdk.Client, error) {
func createSdkClient(apiUrl, apiRole, accessKeyId, accessKey string, skipTlsVerify bool) (*goedgesdk.Client, error) {
if _, err := url.Parse(apiUrl); err != nil {
return nil, errors.New("invalid goedge api url")
}
if apiRole != "user" && apiRole != "admin" {
return nil, errors.New("invalid goedge api role")
}
if accessKeyId == "" {
return nil, errors.New("invalid goedge access key id")
}
@@ -129,7 +135,7 @@ func createSdkClient(apiUrl, accessKeyId, accessKey string, skipTlsVerify bool)
return nil, errors.New("invalid goedge access key")
}
client := goedgesdk.NewClient(apiUrl, "user", accessKeyId, accessKey)
client := goedgesdk.NewClient(apiUrl, apiRole, accessKeyId, accessKey)
if skipTlsVerify {
client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
}

View File

@@ -25,6 +25,12 @@ type DeployerConfig struct {
OutputFormat OutputFormatType `json:"outputFormat,omitempty"`
// 输出证书文件路径。
OutputCertPath string `json:"outputCertPath,omitempty"`
// 输出服务器证书文件路径。
// 选填。
OutputServerCertPath string `json:"outputServerCertPath,omitempty"`
// 输出中间证书文件路径。
// 选填。
OutputIntermediaCertPath string `json:"outputIntermediaCertPath,omitempty"`
// 输出私钥文件路径。
OutputKeyPath string `json:"outputKeyPath,omitempty"`
// PFX 导出密码。
@@ -69,6 +75,12 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 提取服务器证书和中间证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 执行前置命令
if d.config.PreCommand != "" {
stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PreCommand)
@@ -86,6 +98,20 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
}
d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath))
if d.config.OutputServerCertPath != "" {
if err := fileutil.WriteString(d.config.OutputServerCertPath, serverCertPEM); err != nil {
return nil, fmt.Errorf("failed to save server certificate file: %w", err)
}
d.logger.Info("ssl server certificate file saved", slog.String("path", d.config.OutputServerCertPath))
}
if d.config.OutputIntermediaCertPath != "" {
if err := fileutil.WriteString(d.config.OutputIntermediaCertPath, intermediaCertPEM); err != nil {
return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err)
}
d.logger.Info("ssl intermedia certificate file saved", slog.String("path", d.config.OutputIntermediaCertPath))
}
if err := fileutil.WriteString(d.config.OutputKeyPath, privkeyPEM); err != nil {
return nil, fmt.Errorf("failed to save private key file: %w", err)
}

View File

@@ -0,0 +1,89 @@
package netlifysite
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
netlifysdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/netlify"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
)
type DeployerConfig struct {
// netlify API Token。
ApiToken string `json:"apiToken"`
// netlify 网站 ID。
SiteId string `json:"siteId"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *netlifysdk.Client
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.ApiToken)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
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.SiteId == "" {
return nil, errors.New("config `siteId` is required")
}
// 提取服务器证书和中间证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 上传网站证书
// REF: https://open-api.netlify.com/#tag/sniCertificate/operation/provisionSiteTLSCertificate
provisionSiteTLSCertificateReq := &netlifysdk.ProvisionSiteTLSCertificateParams{
Certificate: serverCertPEM,
CACertificates: intermediaCertPEM,
Key: privkeyPEM,
}
provisionSiteTLSCertificateResp, err := d.sdkClient.ProvisionSiteTLSCertificate(d.config.SiteId, provisionSiteTLSCertificateReq)
d.logger.Debug("sdk request 'netlify.provisionSiteTLSCertificate'", slog.String("siteId", d.config.SiteId), slog.Any("request", provisionSiteTLSCertificateReq), slog.Any("response", provisionSiteTLSCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'netlify.provisionSiteTLSCertificate': %w", err)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(apiToken string) (*netlifysdk.Client, error) {
if apiToken == "" {
return nil, errors.New("invalid netlify api token")
}
client := netlifysdk.NewClient(apiToken)
return client, nil
}

View File

@@ -0,0 +1,70 @@
package netlifysite_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/netlify-site"
)
var (
fInputCertPath string
fInputKeyPath string
fApiToken string
fSiteId int64
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_NETLIFYSITE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "")
flag.Int64Var(&fSiteId, argsPrefix+"SITEID", 0, "")
}
/*
Shell command to run this test:
go test -v ./netlify_site_test.go -args \
--CERTIMATE_DEPLOYER_NETLIFYSITE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_NETLIFYSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_NETLIFYSITE_APITOKEN="your-api-token" \
--CERTIMATE_DEPLOYER_NETLIFYSITE_SITEID="your-site-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("APITOKEN: %v", fApiToken),
fmt.Sprintf("SITEID: %v", fSiteId),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
ApiToken: fApiToken,
SiteId: fSiteId,
})
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)
})
}

View File

@@ -41,6 +41,12 @@ type DeployerConfig struct {
OutputFormat OutputFormatType `json:"outputFormat,omitempty"`
// 输出证书文件路径。
OutputCertPath string `json:"outputCertPath,omitempty"`
// 输出服务器证书文件路径。
// 选填。
OutputServerCertPath string `json:"outputServerCertPath,omitempty"`
// 输出中间证书文件路径。
// 选填。
OutputIntermediaCertPath string `json:"outputIntermediaCertPath,omitempty"`
// 输出私钥文件路径。
OutputKeyPath string `json:"outputKeyPath,omitempty"`
// PFX 导出密码。
@@ -85,6 +91,12 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 提取服务器证书和中间证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 连接
client, err := createSshClient(
d.config.SshHost,
@@ -118,6 +130,20 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
}
d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath))
if d.config.OutputServerCertPath != "" {
if err := writeFileString(client, d.config.UseSCP, d.config.OutputServerCertPath, serverCertPEM); err != nil {
return nil, fmt.Errorf("failed to save server certificate file: %w", err)
}
d.logger.Info("ssl server certificate file uploaded", slog.String("path", d.config.OutputServerCertPath))
}
if d.config.OutputIntermediaCertPath != "" {
if err := writeFileString(client, d.config.UseSCP, d.config.OutputIntermediaCertPath, intermediaCertPEM); err != nil {
return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err)
}
d.logger.Info("ssl intermedia certificate file uploaded", slog.String("path", d.config.OutputIntermediaCertPath))
}
if err := writeFileString(client, d.config.UseSCP, d.config.OutputKeyPath, privkeyPEM); err != nil {
return nil, fmt.Errorf("failed to upload private key file: %w", err)
}

View File

@@ -34,7 +34,7 @@ type DeployerConfig struct {
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
// 证书 ID。
// 选填。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateId string `json:"certificateId,omitempty"`
// Webhook ID。
// 选填。

View File

@@ -75,6 +75,12 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
return nil, fmt.Errorf("failed to parse x509: %w", err)
}
// 提取服务器证书和中间证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 处理 Webhook URL
webhookUrl, err := url.Parse(d.config.WebhookUrl)
if err != nil {
@@ -134,6 +140,8 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
replaceJsonValueRecursively(webhookData, "${DOMAIN}", certX509.Subject.CommonName)
replaceJsonValueRecursively(webhookData, "${DOMAINS}", strings.Join(certX509.DNSNames, ";"))
replaceJsonValueRecursively(webhookData, "${CERTIFICATE}", certPEM)
replaceJsonValueRecursively(webhookData, "${SERVER_CERTIFICATE}", serverCertPEM)
replaceJsonValueRecursively(webhookData, "${INTERMEDIA_CERTIFICATE}", intermediaCertPEM)
replaceJsonValueRecursively(webhookData, "${PRIVATE_KEY}", privkeyPEM)
if webhookMethod == http.MethodGet || webhookContentType == CONTENT_TYPE_FORM || webhookContentType == CONTENT_TYPE_MULTIPART {

View File

@@ -65,9 +65,11 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
return nil, err
}
// 生成 AWS 业务参数
scertPEM, _ := certutil.ConvertCertificateToPEM(certX509)
bcertPEM := certPEM
// 提取服务器证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 获取证书列表,避免重复上传
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ListCertificates.html
@@ -145,8 +147,8 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
// 导入证书
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ImportCertificate.html
importCertificateReq := &awsacm.ImportCertificateInput{
Certificate: ([]byte)(scertPEM),
CertificateChain: ([]byte)(bcertPEM),
Certificate: ([]byte)(serverCertPEM),
CertificateChain: ([]byte)(intermediaCertPEM),
PrivateKey: ([]byte)(privkeyPEM),
}
importCertificateResp, err := u.sdkClient.ImportCertificate(context.TODO(), importCertificateReq)

View File

@@ -1,4 +1,4 @@
package onepanelsdk
package onepanel
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package onepanelsdk
package onepanel
import (
"crypto/md5"
@@ -97,7 +97,7 @@ func (c *Client) sendRequestWithResult(method string, path string, params interf
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return fmt.Errorf("1panel api error: failed to parse response: %w", err)
} else if errcode := result.GetCode(); errcode/100 != 2 {
return fmt.Errorf("1panel api error: %d - %s", errcode, result.GetMessage())
return fmt.Errorf("1panel api error: code='%d', message='%s'", errcode, result.GetMessage())
}
return nil

View File

@@ -1,4 +1,4 @@
package onepanelsdk
package onepanel
type BaseResponse interface {
GetCode() int32

View File

@@ -1,4 +1,4 @@
package baishansdk
package baishan
import (
"net/http"

View File

@@ -1,4 +1,4 @@
package baishansdk
package baishan
import (
"encoding/json"
@@ -93,7 +93,7 @@ func (c *Client) sendRequestWithResult(method string, path string, params interf
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return fmt.Errorf("baishan api error: failed to parse response: %w", err)
} else if errcode := result.GetCode(); errcode != 0 {
return fmt.Errorf("baishan api error: %d - %s", errcode, result.GetMessage())
return fmt.Errorf("baishan api error: code='%d', message='%s'", errcode, result.GetMessage())
}
return nil

View File

@@ -1,4 +1,4 @@
package baishansdk
package baishan
import "encoding/json"

View File

@@ -1,4 +1,4 @@
package btpanelsdk
package btpanel
func (c *Client) ConfigSavePanelSSL(req *ConfigSavePanelSSLRequest) (*ConfigSavePanelSSLResponse, error) {
resp := &ConfigSavePanelSSLResponse{}

View File

@@ -1,4 +1,4 @@
package btpanelsdk
package btpanel
import (
"crypto/md5"
@@ -104,7 +104,7 @@ func (c *Client) sendRequestWithResult(path string, params interface{}, result B
if result.GetMessage() == nil {
return fmt.Errorf("baota api error: unknown error")
} else {
return fmt.Errorf("baota api error: %s", *result.GetMessage())
return fmt.Errorf("baota api error: message='%s'", *result.GetMessage())
}
}

View File

@@ -1,4 +1,4 @@
package btpanelsdk
package btpanel
type BaseResponse interface {
GetStatus() *bool

View File

@@ -1,4 +1,4 @@
package bunnysdk
package bunny
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package bunnysdk
package bunny
import (
"encoding/json"

View File

@@ -1,4 +1,4 @@
package bunnysdk
package bunny
type AddCustomCertificateRequest struct {
Hostname string `json:"Hostname"`

View File

@@ -1,4 +1,4 @@
package cacheflysdk
package cachefly
import (
"net/http"

View File

@@ -1,4 +1,4 @@
package cacheflysdk
package cachefly
import (
"encoding/json"

View File

@@ -1,4 +1,4 @@
package cacheflysdk
package cachefly
type BaseResponse interface {
GetMessage() string

View File

@@ -1,4 +1,4 @@
package cdnflysdk
package cdnfly
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package cdnflysdk
package cdnfly
import (
"crypto/tls"
@@ -89,7 +89,7 @@ func (c *Client) sendRequestWithResult(method string, path string, params interf
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return fmt.Errorf("cdnfly api error: failed to parse response: %w", err)
} else if errcode := result.GetCode(); errcode != "" && errcode != "0" {
return fmt.Errorf("cdnfly api error: %s - %s", errcode, result.GetMessage())
return fmt.Errorf("cdnfly api error: code='%s', message='%s'", errcode, result.GetMessage())
}
return nil

View File

@@ -1,4 +1,4 @@
package cdnflysdk
package cdnfly
import "fmt"

View File

@@ -1,4 +1,4 @@
package dnslasdk
package dnsla
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package dnslasdk
package dnsla
import (
"encoding/json"
@@ -78,7 +78,7 @@ func (c *Client) sendRequestWithResult(method string, path string, params interf
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return fmt.Errorf("dnsla api error: failed to parse response: %w", err)
} else if errcode := result.GetCode(); errcode/100 != 2 {
return fmt.Errorf("dnsla api error: %d - %s", errcode, result.GetMessage())
return fmt.Errorf("dnsla api error: code='%d', message='%s'", errcode, result.GetMessage())
}
return nil

View File

@@ -1,4 +1,4 @@
package dnslasdk
package dnsla
type BaseResponse interface {
GetCode() int32

View File

@@ -1,4 +1,4 @@
package dogecloudsdk
package dogecloud
import (
"crypto/hmac"

View File

@@ -1,4 +1,4 @@
package dogecloudsdk
package dogecloud
type BaseResponse struct {
Code *int `json:"code,omitempty"`

View File

@@ -1,4 +1,4 @@
package gnamesdk
package gname
func (c *Client) AddDomainResolution(req *AddDomainResolutionRequest) (*AddDomainResolutionResponse, error) {
resp := &AddDomainResolutionResponse{}

View File

@@ -1,4 +1,4 @@
package gnamesdk
package gname
import (
"crypto/md5"
@@ -97,7 +97,7 @@ func (c *Client) sendRequestWithResult(path string, params interface{}, result B
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return fmt.Errorf("gname api error: failed to parse response: %w", err)
} else if errcode := result.GetCode(); errcode != 1 {
return fmt.Errorf("gname api error: %d - %s", errcode, result.GetMessage())
return fmt.Errorf("gname api error: code='%d', message='%s'", errcode, result.GetMessage())
}
return nil

View File

@@ -1,4 +1,4 @@
package gnamesdk
package gname
import "encoding/json"

View File

@@ -9,7 +9,7 @@ import (
func (c *Client) getAccessToken() error {
req := &getAPIAccessTokenRequest{
Type: c.apiUserType,
Type: c.apiRole,
AccessKeyId: c.accessKeyId,
AccessKey: c.accessKey,
}

View File

@@ -14,7 +14,7 @@ import (
type Client struct {
apiHost string
apiUserType string
apiRole string
accessKeyId string
accessKey string
@@ -25,12 +25,12 @@ type Client struct {
client *resty.Client
}
func NewClient(apiHost, apiUserType, accessKeyId, accessKey string) *Client {
func NewClient(apiHost, apiRole, accessKeyId, accessKey string) *Client {
client := resty.New()
return &Client{
apiHost: strings.TrimRight(apiHost, "/"),
apiUserType: apiUserType,
apiRole: apiRole,
accessKeyId: accessKeyId,
accessKey: accessKey,
client: client,
@@ -96,7 +96,7 @@ func (c *Client) sendRequestWithResult(method string, path string, params interf
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return fmt.Errorf("goedge api error: failed to parse response: %w", err)
} else if errcode := result.GetCode(); errcode != 200 {
return fmt.Errorf("goedge api error: %d - %s", errcode, result.GetMessage())
return fmt.Errorf("goedge api error: code='%d', message='%s'", errcode, result.GetMessage())
}
return nil

View File

@@ -0,0 +1,17 @@
package netlify
import (
"fmt"
"net/http"
"net/url"
)
func (c *Client) ProvisionSiteTLSCertificate(siteId string, params *ProvisionSiteTLSCertificateParams) (*ProvisionSiteTLSCertificateResponse, error) {
if siteId == "" {
return nil, fmt.Errorf("netlify api error: invalid parameter: SiteId")
}
resp := &ProvisionSiteTLSCertificateResponse{}
err := c.sendRequestWithResult(http.MethodPost, fmt.Sprintf("/sites/%s/ssl", url.PathEscape(siteId)), params, nil, resp)
return resp, err
}

View File

@@ -0,0 +1,97 @@
package netlify
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-resty/resty/v2"
)
type Client struct {
apiToken string
client *resty.Client
}
func NewClient(apiToken string) *Client {
client := resty.New()
return &Client{
apiToken: apiToken,
client: client,
}
}
func (c *Client) WithTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) sendRequest(method string, path string, queryParams interface{}, payloadParams interface{}) (*resty.Response, error) {
req := c.client.R().SetHeader("Authorization", "Bearer "+c.apiToken)
req.Method = method
req.URL = "https://api.netlify.com/api/v1" + path
if queryParams != nil {
qs := make(map[string]string)
temp := make(map[string]any)
jsonb, _ := json.Marshal(queryParams)
json.Unmarshal(jsonb, &temp)
for k, v := range temp {
if v != nil {
qs[k] = fmt.Sprintf("%v", v)
}
}
req = req.SetQueryParams(qs)
}
if strings.EqualFold(method, http.MethodGet) {
qs := make(map[string]string)
if payloadParams != nil {
temp := make(map[string]any)
jsonb, _ := json.Marshal(payloadParams)
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.
SetHeader("Content-Type", "application/json").
SetBody(payloadParams)
}
resp, err := req.Send()
if err != nil {
return resp, fmt.Errorf("netlify api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("netlify api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
}
return resp, nil
}
func (c *Client) sendRequestWithResult(method string, path string, queryParams interface{}, payloadParams interface{}, result BaseResponse) error {
resp, err := c.sendRequest(method, path, queryParams, payloadParams)
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("netlify api error: failed to parse response: %w", err)
} else if errcode := result.GetCode(); errcode != 0 {
return fmt.Errorf("netlify api error: code='%d', message='%s'", errcode, result.GetMessage())
}
return nil
}

View File

@@ -0,0 +1,40 @@
package netlify
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 ProvisionSiteTLSCertificateParams struct {
Certificate string `json:"certificate"`
CACertificates string `json:"key"`
Key string `json:"ca_certificates"`
}
type ProvisionSiteTLSCertificateResponse struct {
baseResponse
Domains []string `json:"domains,omitempty"`
State string `json:"state,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
}

View File

@@ -1,4 +1,4 @@
package qiniusdk
package qiniu
import (
"net/http"

View File

@@ -1,4 +1,4 @@
package qiniusdk
package qiniu
import (
"context"

View File

@@ -1,4 +1,4 @@
package qiniusdk
package qiniu
type BaseResponse struct {
Code *int `json:"code,omitempty"`

View File

@@ -1,4 +1,4 @@
package rainyunsdk
package rainyun
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package rainyunsdk
package rainyun
import (
"encoding/json"
@@ -67,7 +67,7 @@ func (c *Client) sendRequestWithResult(method string, path string, params interf
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 fmt.Errorf("rainyun api error: code='%d', message='%s'", errcode, result.GetMessage())
}
return nil

View File

@@ -1,4 +1,4 @@
package rainyunsdk
package rainyun
type BaseResponse interface {
GetCode() int32

View File

@@ -1,4 +1,4 @@
package safelinesdk
package safeline
func (c *Client) UpdateCertificate(req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {
resp := &UpdateCertificateResponse{}

View File

@@ -1,4 +1,4 @@
package safelinesdk
package safeline
import (
"crypto/tls"
@@ -66,9 +66,9 @@ func (c *Client) sendRequestWithResult(path string, params interface{}, result B
return fmt.Errorf("safeline api error: failed to parse response: %w", err)
} else if errcode := result.GetErrCode(); errcode != nil && *errcode != "" {
if result.GetErrMsg() == nil {
return fmt.Errorf("safeline api error: %s", *errcode)
return fmt.Errorf("safeline api error: code='%s'", *errcode)
} else {
return fmt.Errorf("safeline api error: %s - %s", *errcode, *result.GetErrMsg())
return fmt.Errorf("safeline api error: code='%s', message='%s'", *errcode, *result.GetErrMsg())
}
}

View File

@@ -1,4 +1,4 @@
package safelinesdk
package safeline
type BaseResponse interface {
GetErrCode() *string

View File

@@ -90,7 +90,7 @@ func (c *Client) sendRequestWithResult(method string, path string, params interf
} else if tdata := tresp.GetData(); tdata == nil {
return fmt.Errorf("upyun api error: empty data")
} else if errcode := tdata.GetErrorCode(); errcode > 0 {
return fmt.Errorf("upyun api error: %d - %s", errcode, tdata.GetErrorMessage())
return fmt.Errorf("upyun api error: code='%d', message='%s'", errcode, tdata.GetErrorMessage())
}
return nil

View File

@@ -12,9 +12,9 @@ import (
//
// 出参:
// - serverCertPEM: 服务器证书的 PEM 内容。
// - interCertPEM: 中间证书的 PEM 内容。
// - intermediaCertPEM: 中间证书的 PEM 内容。
// - err: 错误。
func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, interCertPEM string, err error) {
func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, intermediaCertPEM string, err error) {
pemBlocks := make([]*pem.Block, 0)
pemData := []byte(certPEM)
for {
@@ -28,7 +28,7 @@ func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, interCert
}
serverCertPEM = ""
interCertPEM = ""
intermediaCertPEM = ""
if len(pemBlocks) == 0 {
return "", "", errors.New("failed to decode PEM block")
@@ -40,9 +40,9 @@ func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, interCert
if len(pemBlocks) > 1 {
for i := 1; i < len(pemBlocks); i++ {
interCertPEM += string(pem.EncodeToMemory(pemBlocks[i]))
intermediaCertPEM += string(pem.EncodeToMemory(pemBlocks[i]))
}
}
return serverCertPEM, interCertPEM, nil
return serverCertPEM, intermediaCertPEM, nil
}

View File

@@ -0,0 +1,44 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
// migrate data
{
accesses, err := app.FindAllRecords("access")
if err != nil {
return err
}
for _, access := range accesses {
changed := false
if access.GetString("provider") == "goedge" {
config := make(map[string]any)
if err := access.UnmarshalJSONField("config", &config); err != nil {
return err
}
config["apiRole"] = "user"
access.Set("config", config)
changed = true
}
if changed {
err = app.Save(access)
if err != nil {
return err
}
}
}
}
return nil
}, func(app core.App) error {
return nil
})
}

277
ui/package-lock.json generated
View File

@@ -10,6 +10,13 @@
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@ant-design/pro-components": "^2.8.7",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.0",
"@codemirror/legacy-modes": "^6.5.1",
"@uiw/codemirror-extensions-basic-setup": "^4.23.12",
"@uiw/codemirror-theme-vscode": "^4.23.12",
"@uiw/react-codemirror": "^4.23.12",
"ahooks": "^3.8.4",
"antd": "^5.25.1",
"antd-zod": "^6.1.0",
@@ -2107,6 +2114,121 @@
"react": ">=16.12.0"
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.18.6",
"resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
"integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.8.1",
"resolved": "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.8.1.tgz",
"integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-json": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/lang-yaml": {
"version": "6.1.2",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz",
"integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.0.0",
"@lezer/yaml": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.11.0",
"resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.11.0.tgz",
"integrity": "sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/legacy-modes": {
"version": "6.5.1",
"resolved": "https://registry.npmmirror.com/@codemirror/legacy-modes/-/legacy-modes-6.5.1.tgz",
"integrity": "sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==",
"dependencies": {
"@codemirror/language": "^6.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.8.5",
"resolved": "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.8.5.tgz",
"integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.5.10",
"resolved": "https://registry.npmmirror.com/@codemirror/search/-/search-6.5.10.tgz",
"integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.2",
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.2",
"resolved": "https://registry.npmmirror.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
"integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.36.8",
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.36.8.tgz",
"integrity": "sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==",
"dependencies": {
"@codemirror/state": "^6.5.0",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
@@ -2839,6 +2961,52 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="
},
"node_modules/@lezer/highlight": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/json": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.2",
"resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/yaml": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@lezer/yaml/-/yaml-1.0.3.tgz",
"integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.4.0"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3628,6 +3796,86 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@uiw/codemirror-extensions-basic-setup": {
"version": "4.23.12",
"resolved": "https://registry.npmmirror.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.12.tgz",
"integrity": "sha512-l9vuiXOTFDBetYrRLDmz3jDxQHDsrVAZ2Y6dVfmrqi2AsulsDu+y7csW0JsvaMqo79rYkaIZg8yeqmDgMb7VyQ==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@codemirror/autocomplete": ">=6.0.0",
"@codemirror/commands": ">=6.0.0",
"@codemirror/language": ">=6.0.0",
"@codemirror/lint": ">=6.0.0",
"@codemirror/search": ">=6.0.0",
"@codemirror/state": ">=6.0.0",
"@codemirror/view": ">=6.0.0"
}
},
"node_modules/@uiw/codemirror-theme-vscode": {
"version": "4.23.12",
"resolved": "https://registry.npmmirror.com/@uiw/codemirror-theme-vscode/-/codemirror-theme-vscode-4.23.12.tgz",
"integrity": "sha512-ePBaUQiixrpmSoZJWCGXUStKmcM8G0VBv3UqwPR+kNGBjqDife76Gbhv77izSeEI3zRPzL+683BOdclkvWnsMg==",
"dependencies": {
"@uiw/codemirror-themes": "4.23.12"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
}
},
"node_modules/@uiw/codemirror-themes": {
"version": "4.23.12",
"resolved": "https://registry.npmmirror.com/@uiw/codemirror-themes/-/codemirror-themes-4.23.12.tgz",
"integrity": "sha512-8etEByfS9yttFZW0rcWhdZc7/JXJKRWlU5lHmJCI3GydZNGCzydNA+HtK9nWKpJUndVc58Q2sqSC5OIcwq8y6A==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@codemirror/language": ">=6.0.0",
"@codemirror/state": ">=6.0.0",
"@codemirror/view": ">=6.0.0"
}
},
"node_modules/@uiw/react-codemirror": {
"version": "4.23.12",
"resolved": "https://registry.npmmirror.com/@uiw/react-codemirror/-/react-codemirror-4.23.12.tgz",
"integrity": "sha512-yseqWdzoAAGAW7i/NiU8YrfSLVOEBjQvSx1KpDTFVV/nn0AlAZoDVTIPEBgdXrPlVUQoCrwgpEaj3uZCklk9QA==",
"dependencies": {
"@babel/runtime": "^7.18.6",
"@codemirror/commands": "^6.1.0",
"@codemirror/state": "^6.1.1",
"@codemirror/theme-one-dark": "^6.0.0",
"@uiw/codemirror-extensions-basic-setup": "4.23.12",
"codemirror": "^6.0.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@babel/runtime": ">=7.11.0",
"@codemirror/state": ">=6.0.0",
"@codemirror/theme-one-dark": ">=6.0.0",
"@codemirror/view": ">=6.0.0",
"codemirror": ">=6.0.0",
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@umijs/route-utils": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/@umijs/route-utils/-/route-utils-4.0.1.tgz",
@@ -4337,6 +4585,20 @@
"node": ">=6"
}
},
"node_modules/codemirror": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.1.tgz",
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
@@ -4403,6 +4665,11 @@
"url": "https://opencollective.com/core-js"
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
},
"node_modules/cron-parser": {
"version": "5.2.0",
"resolved": "https://registry.npmmirror.com/cron-parser/-/cron-parser-5.2.0.tgz",
@@ -8689,6 +8956,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
},
"node_modules/stylis": {
"version": "4.3.4",
"resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.4.tgz",
@@ -9363,6 +9635,11 @@
"node": ">=0.10.0"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz",

View File

@@ -12,6 +12,13 @@
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@ant-design/pro-components": "^2.8.7",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.0",
"@codemirror/legacy-modes": "^6.5.1",
"@uiw/codemirror-extensions-basic-setup": "^4.23.12",
"@uiw/codemirror-theme-vscode": "^4.23.12",
"@uiw/react-codemirror": "^4.23.12",
"ahooks": "^3.8.4",
"antd": "^5.25.1",
"antd-zod": "^6.1.0",

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,97 @@
import { useMemo, useRef } from "react";
import { json } from "@codemirror/lang-json";
import { yaml } from "@codemirror/lang-yaml";
import { StreamLanguage } from "@codemirror/language";
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { basicSetup } from "@uiw/codemirror-extensions-basic-setup";
import { vscodeDark, vscodeLight } from "@uiw/codemirror-theme-vscode";
import CodeMirror, { type ReactCodeMirrorProps, type ReactCodeMirrorRef } from "@uiw/react-codemirror";
import { useFocusWithin } from "ahooks";
import { theme } from "antd";
import { useBrowserTheme } from "@/hooks";
import { mergeCls } from "@/utils/css";
export interface CodeInputProps extends Omit<ReactCodeMirrorProps, "extensions" | "lang" | "theme"> {
disabled?: boolean;
language?: string | string[];
}
const CodeInput = ({ className, style, disabled, language, ...props }: CodeInputProps) => {
const { token: themeToken } = theme.useToken();
const { theme: browserTheme } = useBrowserTheme();
const cmRef = useRef<ReactCodeMirrorRef>(null);
const isFocusWithin = useFocusWithin(cmRef.current?.editor);
const cmTheme = useMemo(() => {
if (browserTheme === "dark") {
return vscodeDark;
}
return vscodeLight;
}, [browserTheme]);
const cmExtensions = useMemo(() => {
const temp: NonNullable<ReactCodeMirrorProps["extensions"]> = [
basicSetup({
foldGutter: false,
dropCursor: false,
allowMultipleSelections: false,
indentOnInput: false,
}),
];
const langs = Array.isArray(language) ? language : [language];
langs.forEach((lang) => {
switch (lang) {
case "shell":
temp.push(StreamLanguage.define(shell));
break;
case "json":
temp.push(json());
break;
case "powershell":
temp.push(StreamLanguage.define(powerShell));
break;
case "yaml":
temp.push(yaml());
break;
}
});
return temp;
}, [language]);
return (
<div
className={mergeCls(className, `hover:border-[${themeToken.colorPrimaryBorderHover}]`)}
style={{
...(style ?? {}),
border: `1px solid ${isFocusWithin ? (themeToken.Input?.activeBorderColor ?? themeToken.colorPrimaryBorder) : themeToken.colorBorder}`,
borderRadius: `${themeToken.borderRadius}px`,
backgroundColor: disabled ? themeToken.colorBgContainerDisabled : themeToken.colorBgContainer,
boxShadow: isFocusWithin ? themeToken.Input?.activeShadow : undefined,
overflow: "hidden",
}}
>
<CodeMirror
ref={cmRef}
height="100%"
style={{ height: "100%" }}
{...props}
basicSetup={{
foldGutter: false,
dropCursor: false,
allowMultipleSelections: false,
indentOnInput: false,
}}
extensions={cmExtensions}
theme={cmTheme}
/>
</div>
);
};
export default CodeInput;

View File

@@ -0,0 +1,51 @@
import { type ChangeEvent, useRef } from "react";
import { useTranslation } from "react-i18next";
import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons";
import { Button, type ButtonProps, Input, Space, type UploadProps } from "antd";
import { type TextAreaProps } from "antd/es/input/TextArea";
import { mergeCls } from "@/utils/css";
import { readFileContent } from "@/utils/file";
export interface TextFileInputProps extends Omit<TextAreaProps, "onChange"> {
accept?: UploadProps["accept"];
uploadButtonProps?: Omit<ButtonProps, "disabled" | "onClick">;
uploadText?: string;
onChange?: (value: string) => void;
}
const TextFileInput = ({ className, style, accept, disabled, readOnly, uploadText, uploadButtonProps, onChange, ...props }: TextFileInputProps) => {
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const handleButtonClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
};
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
const { files } = e.target as HTMLInputElement;
if (files?.length) {
const value = await readFileContent(files[0]);
onChange?.(value);
}
};
return (
<Space className={mergeCls("w-full", className)} style={style} direction="vertical" size="small">
<Input.TextArea {...props} disabled={disabled} readOnly={readOnly} onChange={(e) => onChange?.(e.target.value)} />
{!readOnly && (
<>
<Button {...uploadButtonProps} block disabled={disabled} icon={<UploadOutlinedIcon />} onClick={handleButtonClick}>
{uploadText ?? t("common.text.import_from_file")}
</Button>
<input ref={fileInputRef} type="file" style={{ display: "none" }} accept={accept} onChange={handleFileChange} />
</>
)}
</Space>
);
};
export default TextFileInput;

View File

@@ -46,6 +46,8 @@ import AccessFormMattermostConfig from "./AccessFormMattermostConfig";
import AccessFormNamecheapConfig from "./AccessFormNamecheapConfig";
import AccessFormNameDotComConfig from "./AccessFormNameDotComConfig";
import AccessFormNameSiloConfig from "./AccessFormNameSiloConfig";
import AccessFormNetcupConfig from "./AccessFormNetcupConfig";
import AccessFormNetlifyConfig from "./AccessFormNetlifyConfig";
import AccessFormNS1Config from "./AccessFormNS1Config";
import AccessFormPorkbunConfig from "./AccessFormPorkbunConfig";
import AccessFormPowerDNSConfig from "./AccessFormPowerDNSConfig";
@@ -242,6 +244,10 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormNameDotComConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.NAMESILO:
return <AccessFormNameSiloConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.NETCUP:
return <AccessFormNetcupConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.NETLIFY:
return <AccessFormNetlifyConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.NS1:
return <AccessFormNS1Config {...nestedFormProps} />;
case ACCESS_PROVIDERS.PORKBUN:

View File

@@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input, Switch } from "antd";
import { Form, type FormInstance, Input, Radio, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
@@ -18,6 +18,7 @@ export type AccessFormGoEdgeConfigProps = {
const initFormModel = (): AccessFormGoEdgeConfigFieldValues => {
return {
apiUrl: "http://<your-host-addr>:7788/",
apiRole: "user",
accessKeyId: "",
accessKey: "",
};
@@ -28,6 +29,9 @@ const AccessFormGoEdgeConfig = ({ form: formInst, formName, disabled, initialVal
const formSchema = z.object({
apiUrl: z.string().url(t("common.errmsg.url_invalid")),
role: z.union([z.literal("user"), z.literal("admin")], {
message: t("access.form.goedge_api_role.placeholder"),
}),
accessKeyId: z
.string()
.min(1, t("access.form.goedge_access_key_id.placeholder"))
@@ -59,6 +63,10 @@ const AccessFormGoEdgeConfig = ({ form: formInst, formName, disabled, initialVal
<Input placeholder={t("access.form.goedge_api_url.placeholder")} />
</Form.Item>
<Form.Item name="apiRole" label={t("access.form.goedge_api_role.label")} rules={[formRule]}>
<Radio.Group options={["user", "admin"].map((s) => ({ label: t(`access.form.goedge_api_role.option.${s}.label`), value: s }))} />
</Form.Item>
<Form.Item
name="accessKeyId"
label={t("access.form.goedge_access_key_id.label")}

View File

@@ -1,12 +1,10 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons";
import { Button, Form, type FormInstance, Input, Upload, type UploadFile, type UploadProps } from "antd";
import { Form, type FormInstance } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import TextFileInput from "@/components/TextFileInput";
import { type AccessConfigForKubernetes } from "@/domain/access";
import { readFileContent } from "@/utils/file";
type AccessFormKubernetesConfigFieldValues = Nullish<AccessConfigForKubernetes>;
@@ -34,24 +32,6 @@ const AccessFormKubernetesConfig = ({ form: formInst, formName, disabled, initia
});
const formRule = createSchemaFieldRule(formSchema);
const fieldKubeConfig = Form.useWatch("kubeConfig", formInst);
const [fieldKubeFileList, setFieldKubeFileList] = useState<UploadFile[]>([]);
useEffect(() => {
setFieldKubeFileList(initialValues?.kubeConfig?.trim() ? [{ uid: "-1", name: "kubeconfig", status: "done" }] : []);
}, [initialValues?.kubeConfig]);
const handleKubeFileChange: UploadProps["onChange"] = async ({ file }) => {
if (file && file.status !== "removed") {
formInst.setFieldValue("kubeConfig", await readFileContent(file.originFileObj ?? (file as unknown as File)));
setFieldKubeFileList([file]);
} else {
formInst.setFieldValue("kubeConfig", "");
setFieldKubeFileList([]);
}
onValuesChange?.(formInst.getFieldsValue(true));
};
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
@@ -65,16 +45,13 @@ const AccessFormKubernetesConfig = ({ form: formInst, formName, disabled, initia
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item name="kubeConfig" noStyle rules={[formRule]}>
<Input.TextArea autoComplete="new-password" hidden placeholder={t("access.form.k8s_kubeconfig.placeholder")} value={fieldKubeConfig} />
</Form.Item>
<Form.Item
name="kubeConfig"
label={t("access.form.k8s_kubeconfig.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.k8s_kubeconfig.tooltip") }}></span>}
>
<Upload beforeUpload={() => false} fileList={fieldKubeFileList} maxCount={1} onChange={handleKubeFileChange}>
<Button icon={<UploadOutlinedIcon />}>{t("access.form.k8s_kubeconfig.upload")}</Button>
</Upload>
<TextFileInput allowClear autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("access.form.k8s_kubeconfig.placeholder")} />
</Form.Item>
</Form>
);

View File

@@ -0,0 +1,79 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForNetcup } from "@/domain/access";
type AccessFormNetcupConfigFieldValues = Nullish<AccessConfigForNetcup>;
export type AccessFormNetcupConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormNetcupConfigFieldValues;
onValuesChange?: (values: AccessFormNetcupConfigFieldValues) => void;
};
const initFormModel = (): AccessFormNetcupConfigFieldValues => {
return {
customerNumber: "",
apiKey: "",
apiPassword: "",
};
};
const AccessFormNetcupConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormNetcupConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
customerNumber: z.string().nonempty(t("access.form.netcup_customer_number.placeholder")).trim(),
apiKey: z.string().nonempty(t("access.form.netcup_api_key.placeholder")).trim(),
apiPassword: z.string().nonempty(t("access.form.netcup_api_password.placeholder")).trim(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="customerNumber"
label={t("access.form.netcup_customer_number.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.netcup_customer_number.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.netcup_customer_number.placeholder")} />
</Form.Item>
<Form.Item
name="apiKey"
label={t("access.form.netcup_api_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.netcup_api_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.netcup_api_key.placeholder")} />
</Form.Item>
<Form.Item
name="apiPassword"
label={t("access.form.netcup_api_password.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.netcup_api_password.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.netcup_api_password.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormNetcupConfig;

View File

@@ -0,0 +1,57 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForNetlify } from "@/domain/access";
type AccessFormNetlifyConfigFieldValues = Nullish<AccessConfigForNetlify>;
export type AccessFormNetlifyConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormNetlifyConfigFieldValues;
onValuesChange?: (values: AccessFormNetlifyConfigFieldValues) => void;
};
const initFormModel = (): AccessFormNetlifyConfigFieldValues => {
return {
apiToken: "",
};
};
const AccessFormNetlifyConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormNetlifyConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
apiToken: z.string().nonempty(t("access.form.netlify_api_token.placeholder")).trim(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="apiToken"
label={t("access.form.netlify_api_token.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.netlify_api_token.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.netlify_api_token.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormNetlifyConfig;

View File

@@ -1,12 +1,10 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons";
import { Button, Form, type FormInstance, Input, InputNumber, Upload, type UploadFile, type UploadProps } from "antd";
import { Form, type FormInstance, Input, InputNumber } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import TextFileInput from "@/components/TextFileInput";
import { type AccessConfigForSSH } from "@/domain/access";
import { readFileContent } from "@/utils/file";
import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators";
type AccessFormSSHConfigFieldValues = Nullish<AccessConfigForSSH>;
@@ -59,24 +57,6 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
});
const formRule = createSchemaFieldRule(formSchema);
const fieldKey = Form.useWatch("key", formInst);
const [fieldKeyFileList, setFieldKeyFileList] = useState<UploadFile[]>([]);
useEffect(() => {
setFieldKeyFileList(initialValues?.key?.trim() ? [{ uid: "-1", name: "sshkey", status: "done" }] : []);
}, [initialValues?.key]);
const handleKeyFileChange: UploadProps["onChange"] = async ({ file }) => {
if (file && file.status !== "removed") {
formInst.setFieldValue("key", await readFileContent(file.originFileObj ?? (file as unknown as File)));
setFieldKeyFileList([file]);
} else {
formInst.setFieldValue("key", "");
setFieldKeyFileList([]);
}
onValuesChange?.(formInst.getFieldsValue(true));
};
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
@@ -104,48 +84,36 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
</div>
</div>
<div className="flex space-x-2">
<div className="w-1/2">
<Form.Item name="username" label={t("access.form.ssh_username.label")} rules={[formRule]}>
<Input autoComplete="new-password" placeholder={t("access.form.ssh_username.placeholder")} />
</Form.Item>
</div>
<Form.Item name="username" label={t("access.form.ssh_username.label")} rules={[formRule]}>
<Input autoComplete="new-password" placeholder={t("access.form.ssh_username.placeholder")} />
</Form.Item>
<div className="w-1/2">
<Form.Item
name="password"
label={t("access.form.ssh_password.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_password.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.ssh_password.placeholder")} />
</Form.Item>
</div>
</div>
<Form.Item
name="password"
label={t("access.form.ssh_password.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_password.tooltip") }}></span>}
>
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_password.placeholder")} />
</Form.Item>
<div className="flex space-x-2">
<div className="w-1/2">
<Form.Item name="key" noStyle rules={[formRule]}>
<Input.TextArea autoComplete="new-password" hidden placeholder={t("access.form.ssh_key.placeholder")} value={fieldKey} />
</Form.Item>
<Form.Item label={t("access.form.ssh_key.label")} tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key.tooltip") }}></span>}>
<Upload beforeUpload={() => false} fileList={fieldKeyFileList} maxCount={1} onChange={handleKeyFileChange}>
<Button icon={<UploadOutlinedIcon />}>{t("access.form.ssh_key.upload")}</Button>
</Upload>
</Form.Item>
</div>
<Form.Item
name="key"
label={t("access.form.ssh_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key.tooltip") }}></span>}
>
<TextFileInput allowClear autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("access.form.ssh_key.placeholder")} />
</Form.Item>
<div className="w-1/2">
<Form.Item
name="keyPassphrase"
label={t("access.form.ssh_key_passphrase.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key_passphrase.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.ssh_key_passphrase.placeholder")} />
</Form.Item>
</div>
</div>
<Form.Item
name="keyPassphrase"
label={t("access.form.ssh_key_passphrase.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key_passphrase.tooltip") }}></span>}
>
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_key_passphrase.placeholder")} />
</Form.Item>
</Form>
);
};

View File

@@ -4,6 +4,7 @@ import { Alert, Button, Dropdown, Form, type FormInstance, Input, Select, Switch
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import CodeInput from "@/components/CodeInput";
import Show from "@/components/Show";
import { type AccessConfigForWebhook } from "@/domain/access";
@@ -105,8 +106,8 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa
formInst.setFieldValue("headers", value);
};
const handleWebhookDataForDeploymentBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
const handleWebhookDataForDeploymentBlur = () => {
const value = formInst.getFieldValue("defaultDataForDeployment");
try {
const json = JSON.stringify(JSON.parse(value), null, 2);
formInst.setFieldValue("defaultDataForDeployment", json);
@@ -115,8 +116,8 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa
}
};
const handleWebhookDataForNotificationBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
const handleWebhookDataForNotificationBlur = () => {
const value = formInst.getFieldValue("defaultDataForNotification");
try {
const json = JSON.stringify(JSON.parse(value), null, 2);
formInst.setFieldValue("defaultDataForNotification", json);
@@ -279,7 +280,7 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.webhook_headers.tooltip") }}></span>}
>
<Input.TextArea autoSize={{ minRows: 3, maxRows: 5 }} placeholder={t("access.form.webhook_headers.placeholder")} onBlur={handleWebhookHeadersBlur} />
<Input.TextArea autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("access.form.webhook_headers.placeholder")} onBlur={handleWebhookHeadersBlur} />
</Form.Item>
<Show when={!usage || usage === "deployment"}>
@@ -297,9 +298,11 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa
</div>
</label>
<Form.Item name="defaultDataForDeployment" rules={[formRule]}>
<Input.TextArea
allowClear
autoSize={{ minRows: 3, maxRows: 10 }}
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language="json"
placeholder={t("access.form.webhook_default_data_for_deployment.placeholder")}
onBlur={handleWebhookDataForDeploymentBlur}
/>
@@ -338,9 +341,11 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa
</div>
</label>
<Form.Item name="defaultDataForNotification" rules={[formRule]}>
<Input.TextArea
allowClear
autoSize={{ minRows: 3, maxRows: 10 }}
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language="json"
placeholder={t("access.form.webhook_default_data_for_notification.placeholder")}
onBlur={handleWebhookDataForNotificationBlur}
/>

View File

@@ -75,7 +75,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
</CopyToClipboard>
</Tooltip>
</div>
<Input.TextArea value={data.certificate} variant="filled" rows={5} autoSize={{ maxRows: 5 }} readOnly />
<Input.TextArea value={data.certificate} variant="filled" autoSize={{ minRows: 5, maxRows: 5 }} readOnly />
</Form.Item>
<Form.Item>
@@ -92,7 +92,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
</CopyToClipboard>
</Tooltip>
</div>
<Input.TextArea value={data.privateKey} variant="filled" rows={5} autoSize={{ maxRows: 5 }} readOnly />
<Input.TextArea value={data.privateKey} variant="filled" autoSize={{ minRows: 5, maxRows: 5 }} readOnly />
</Form.Item>
</Form>

View File

@@ -108,7 +108,7 @@ const NotifyTemplateForm = ({ className, style }: NotifyTemplateFormProps) => {
rules={[formRule]}
>
<Input.TextArea
autoSize={{ minRows: 3, maxRows: 5 }}
autoSize={{ minRows: 3, maxRows: 10 }}
placeholder={t("settings.notification.template.form.message.placeholder")}
onChange={handleInputChange}
/>

View File

@@ -4,6 +4,7 @@ import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Tag, Typogra
import Show from "@/components/Show";
import { ACCESS_USAGES, type AccessProvider, type AccessUsageType, accessProvidersMap } from "@/domain/provider";
import { mergeCls } from "@/utils/css";
export type AccessProviderPickerProps = {
className?: string;
@@ -73,17 +74,23 @@ const AccessProviderPicker = ({ className, style, autoFocus, filter, placeholder
return (
<Col key={index} xs={24} md={12} span={8}>
<Card
className="h-20 w-full overflow-hidden shadow-sm"
className={mergeCls("h-20 w-full overflow-hidden shadow-sm", provider.builtin ? " cursor-not-allowed" : "")}
styles={{ body: { height: "100%", padding: "0.5rem 1rem" } }}
hoverable
onClick={() => {
if (provider.builtin) {
return;
}
handleProviderTypeSelect(provider.type);
}}
>
<Flex className="size-full overflow-hidden" align="center" gap={8}>
<Avatar src={provider.icon} size="small" />
<div className="flex-1 overflow-hidden">
<Typography.Text className="mb-1 line-clamp-1">{t(provider.name)}</Typography.Text>
<Typography.Text className="mb-1 line-clamp-1" type={provider.builtin ? "secondary" : undefined}>
{t(provider.name)}
</Typography.Text>
<div className="origin-left scale-[80%]">
<Show when={provider.builtin}>
<Tag>{t("access.props.provider.builtin")}</Tag>

View File

@@ -57,6 +57,7 @@ import DeployNodeConfigFormJDCloudLiveConfig from "./DeployNodeConfigFormJDCloud
import DeployNodeConfigFormJDCloudVODConfig from "./DeployNodeConfigFormJDCloudVODConfig";
import DeployNodeConfigFormKubernetesSecretConfig from "./DeployNodeConfigFormKubernetesSecretConfig";
import DeployNodeConfigFormLocalConfig from "./DeployNodeConfigFormLocalConfig";
import DeployNodeConfigFormNetlifySiteConfig from "./DeployNodeConfigFormNetlifySiteConfig";
import DeployNodeConfigFormProxmoxVEConfig from "./DeployNodeConfigFormProxmoxVEConfig";
import DeployNodeConfigFormQiniuCDNConfig from "./DeployNodeConfigFormQiniuCDNConfig";
import DeployNodeConfigFormQiniuKodoConfig from "./DeployNodeConfigFormQiniuKodoConfig";
@@ -260,6 +261,8 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
return <DeployNodeConfigFormKubernetesSecretConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.LOCAL:
return <DeployNodeConfigFormLocalConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.NETLIFY_SITE:
return <DeployNodeConfigFormNetlifySiteConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.PROXMOXVE:
return <DeployNodeConfigFormProxmoxVEConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.QINIU_CDN:

View File

@@ -5,6 +5,7 @@ import { z } from "zod";
type DeployNodeConfigFormAWSACMConfigFieldValues = Nullish<{
region: string;
certificateArn?: string;
}>;
export type DeployNodeConfigFormAWSACMConfigProps = {
@@ -27,6 +28,7 @@ const DeployNodeConfigFormAWSACMConfig = ({ form: formInst, formName, disabled,
.string({ message: t("workflow_node.deploy.form.aws_acm_region.placeholder") })
.nonempty(t("workflow_node.deploy.form.aws_acm_region.placeholder"))
.trim(),
certificateArn: z.string({ message: t("workflow_node.deploy.form.aws_acm_certificate_arn.placeholder") }).nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
@@ -51,6 +53,15 @@ const DeployNodeConfigFormAWSACMConfig = ({ form: formInst, formName, disabled,
>
<Input placeholder={t("workflow_node.deploy.form.aws_acm_region.placeholder")} />
</Form.Item>
<Form.Item
name="certificateArn"
label={t("workflow_node.deploy.form.aws_acm_certificate_arn.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aws_acm_certificate_arn.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aws_acm_certificate_arn.placeholder")} />
</Form.Item>
</Form>
);
};

View File

@@ -37,7 +37,7 @@ const DeployNodeConfigFormAzureKeyVaultConfig = ({
certificateName: z
.string({ message: t("workflow_node.deploy.form.azure_keyvault_certificate_name.placeholder") })
.nullish()
.refine((v) =>{
.refine((v) => {
if (!v) return true;
return /^[a-zA-Z0-9-]{1,127}$/.test(v);
}, t("workflow_node.deploy.form.azure_keyvault_certificate_name.errmsg.invalid")),

View File

@@ -4,20 +4,23 @@ import { Alert, Button, Dropdown, Form, type FormInstance, Input, Select } from
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import CodeInput from "@/components/CodeInput";
import Show from "@/components/Show";
import { CERTIFICATE_FORMATS } from "@/domain/certificate";
type DeployNodeConfigFormLocalConfigFieldValues = Nullish<{
format: string;
certPath: string;
keyPath?: string | null;
pfxPassword?: string | null;
jksAlias?: string | null;
jksKeypass?: string | null;
jksStorepass?: string | null;
shellEnv?: string | null;
preCommand?: string | null;
postCommand?: string | null;
certPathForServerOnly?: string;
certPathForIntermediaOnly?: string;
keyPath?: string;
pfxPassword?: string;
jksAlias?: string;
jksKeypass?: string;
jksStorepass?: string;
shellEnv?: string;
preCommand?: string;
postCommand?: string;
}>;
export type DeployNodeConfigFormLocalConfigProps = {
@@ -49,6 +52,8 @@ export const initPresetScript = (
key: "sh_backup_files" | "ps_backup_files" | "sh_reload_nginx" | "ps_binding_iis" | "ps_binding_netsh" | "ps_binding_rdp",
params?: {
certPath?: string;
certPathForServerOnly?: string;
certPathForIntermediaOnly?: string;
keyPath?: string;
pfxPassword?: string;
jksAlias?: string;
@@ -74,19 +79,22 @@ if (Test-Path -Path "${params?.keyPath || "<your-key-path>"}" -PathType Leaf) {
`.trim();
case "sh_reload_nginx":
return `sudo service nginx reload`;
return `# *** 需要 root 权限 ***
sudo service nginx reload
`.trim();
case "ps_binding_iis":
return `# 需要管理员权限
return `# *** 需要管理员权限 ***
# 请将以下变量替换为实际值
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径(与表单中保持一致)
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码(与表单中保持一致)
$siteName = "<your-site-name>" # IIS 网站名称
$domain = "<your-domain-name>" # 域名
$ipaddr = "<your-binding-ip>" # 绑定 IP“*”表示所有 IP 绑定
$port = "<your-binding-port>" # 绑定端口
# 导入证书到本地计算机的个人存储区
$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable
# 获取 Thumbprint
@@ -108,16 +116,16 @@ Remove-Item -Path "$pfxPath" -Force
`.trim();
case "ps_binding_netsh":
return `# 需要管理员权限
return `# *** 需要管理员权限 ***
# 请将以下变量替换为实际值
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码
$ipaddr = "<your-binding-ip>" # 绑定 IP“0.0.0.0”表示所有 IP 绑定,可填入域名
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径(与表单中保持一致)
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码(与表单中保持一致)
$ipaddr = "<your-binding-ip>" # 绑定 IP“0.0.0.0”表示所有 IP 绑定,可填入域名
$port = "<your-binding-port>" # 绑定端口
$addr = $ipaddr + ":" + $port
# 导入证书到本地计算机的个人存储区
$addr = $ipaddr + ":" + $port
$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable
# 获取 Thumbprint
$thumbprint = $cert.Thumbprint
@@ -131,10 +139,11 @@ Remove-Item -Path "$pfxPath" -Force
`.trim();
case "ps_binding_rdp":
return `# 需要管理员权限
return `# *** 需要管理员权限 ***
# 请将以下变量替换为实际值
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX 文件路径(与表单中保持一致)
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX 密码(与表单中保持一致)
# 导入证书到本地计算机的个人存储区
$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable
@@ -159,6 +168,16 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
.min(1, t("workflow_node.deploy.form.local_cert_path.tooltip"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
certPathForServerOnly: z
.string()
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish(),
certPathForIntermediaOnly: z
.string()
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish(),
keyPath: z
.string()
.max(256, t("common.errmsg.string_max", { max: 256 }))
@@ -325,6 +344,24 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
>
<Input placeholder={t("workflow_node.deploy.form.local_key_path.placeholder")} />
</Form.Item>
<Form.Item
name="certPathForServerOnly"
label={t("workflow_node.deploy.form.local_servercert_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_servercert_path.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.local_servercert_path.placeholder")} />
</Form.Item>
<Form.Item
name="certPathForIntermediaOnly"
label={t("workflow_node.deploy.form.local_intermediacert_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_intermediacert_path.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.local_intermediacert_path.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldFormat === FORMAT_PFX}>
@@ -407,7 +444,13 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
</div>
</label>
<Form.Item name="preCommand" rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.local_pre_command.placeholder")} />
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language={["shell", "powershell"]}
placeholder={t("workflow_node.deploy.form.local_pre_command.placeholder")}
/>
</Form.Item>
</Form.Item>
@@ -437,7 +480,13 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
</div>
</label>
<Form.Item name="postCommand" rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.local_post_command.placeholder")} />
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language={["shell", "powershell"]}
placeholder={t("workflow_node.deploy.form.local_post_command.placeholder")}
/>
</Form.Item>
</Form.Item>
</Form>

View File

@@ -0,0 +1,63 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
type DeployNodeConfigFormNetlifySiteConfigFieldValues = Nullish<{
siteId: string;
}>;
export type DeployNodeConfigFormNetlifySiteConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: DeployNodeConfigFormNetlifySiteConfigFieldValues;
onValuesChange?: (values: DeployNodeConfigFormNetlifySiteConfigFieldValues) => void;
};
const initFormModel = (): DeployNodeConfigFormNetlifySiteConfigFieldValues => {
return {
siteId: "",
};
};
const DeployNodeConfigFormNetlifySiteConfig = ({
form: formInst,
formName,
disabled,
initialValues,
onValuesChange,
}: DeployNodeConfigFormNetlifySiteConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
siteId: z.string().nonempty(t("workflow_node.deploy.form.netlify_site_id.placeholder")),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="siteId"
label={t("workflow_node.deploy.form.netlify_site_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.netlify_site_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.netlify_site_id.placeholder")} />
</Form.Item>
</Form>
);
};
export default DeployNodeConfigFormNetlifySiteConfig;

View File

@@ -4,21 +4,24 @@ import { Button, Dropdown, Form, type FormInstance, Input, Select, Switch } from
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import CodeInput from "@/components/CodeInput";
import Show from "@/components/Show";
import { CERTIFICATE_FORMATS } from "@/domain/certificate";
import { initPresetScript } from "./DeployNodeConfigFormLocalConfig";
import { initPresetScript as _initPresetScript } from "./DeployNodeConfigFormLocalConfig";
type DeployNodeConfigFormSSHConfigFieldValues = Nullish<{
format: string;
certPath: string;
keyPath?: string | null;
pfxPassword?: string | null;
jksAlias?: string | null;
jksKeypass?: string | null;
jksStorepass?: string | null;
preCommand?: string | null;
postCommand?: string | null;
certPathForServerOnly?: string;
certPathForIntermediaOnly?: string;
keyPath?: string;
pfxPassword?: string;
jksAlias?: string;
jksKeypass?: string;
jksStorepass?: string;
preCommand?: string;
postCommand?: string;
useSCP?: boolean;
}>;
@@ -42,6 +45,125 @@ const initFormModel = (): DeployNodeConfigFormSSHConfigFieldValues => {
};
};
const initPresetScript = (
key: Parameters<typeof _initPresetScript>[0] | "sh_replace_synologydsm_ssl" | "sh_replace_fnos_ssl",
params?: Parameters<typeof _initPresetScript>[1]
) => {
switch (key) {
case "sh_replace_synologydsm_ssl":
return `# *** 需要 root 权限 ***
# 脚本参考 https://github.com/catchdave/ssl-certs/blob/main/replace_synology_ssl_certs.sh
# 请将以下变量替换为实际值
$tmpFullchainPath = "${params?.certPath || "<your-fullchain-cert-path>"}" # 证书文件路径(与表单中保持一致)
$tmpCertPath = "${params?.certPathForServerOnly || "<your-server-cert-path>"}" # 服务器证书文件路径(与表单中保持一致)
$tmpKeyPath = "${params?.keyPath || "<your-key-path>"}" # 私钥文件路径(与表单中保持一致)
DEBUG=1
error_exit() { echo "[ERROR] $1"; exit 1; }
warn() { echo "[WARN] $1"; }
info() { echo "[INFO] $1"; }
debug() { [[ "\${DEBUG}" ]] && echo "[DEBUG] $1"; }
certs_src_dir="/usr/syno/etc/certificate/system/default"
target_cert_dirs=(
"/usr/syno/etc/certificate/system/FQDN"
"/usr/local/etc/certificate/ScsiTarget/pkg-scsi-plugin-server/"
"/usr/local/etc/certificate/SynologyDrive/SynologyDrive/"
"/usr/local/etc/certificate/WebDAVServer/webdav/"
"/usr/local/etc/certificate/ActiveBackup/ActiveBackup/"
"/usr/syno/etc/certificate/smbftpd/ftpd/")
# 获取证书目录
default_dir_name=$(</usr/syno/etc/certificate/_archive/DEFAULT)
if [[ -n "$default_dir_name" ]]; then
target_cert_dirs+=("/usr/syno/etc/certificate/_archive/\${default_dir_name}")
debug "Default cert directory found: '/usr/syno/etc/certificate/_archive/\${default_dir_name}'"
else
warn "No default directory found. Probably unusual? Check: 'cat /usr/syno/etc/certificate/_archive/DEFAULT'"
fi
# 获取反向代理证书目录
for proxy in /usr/syno/etc/certificate/ReverseProxy/*/; do
debug "Found proxy dir: \${proxy}"
target_cert_dirs+=("\${proxy}")
done
[[ "\${DEBUG}" ]] && set -x
# 复制文件
cp -rf "$tmpFullchainPath" "\${certs_src_dir}/fullchain.pem" || error_exit "Halting because of error moving fullchain file"
cp -rf "$tmpCertPath" "\${certs_src_dir}/cert.pem" || error_exit "Halting because of error moving cert file"
cp -rf "$tmpKeyPath" "\${certs_src_dir}/privkey.pem" || error_exit "Halting because of error moving privkey file"
chown root:root "\${certs_src_dir}/"{privkey,fullchain,cert}.pem || error_exit "Halting because of error chowning files"
info "Certs moved from /tmp & chowned."
# 替换证书
for target_dir in "\${target_cert_dirs[@]}"; do
if [[ ! -d "$target_dir" ]]; then
debug "Target cert directory '$target_dir' not found, skipping..."
continue
fi
info "Copying certificates to '$target_dir'"
if ! (cp "\${certs_src_dir}/"{privkey,fullchain,cert}.pem "$target_dir/" && \
chown root:root "$target_dir/"{privkey,fullchain,cert}.pem); then
warn "Error copying or chowning certs to \${target_dir}"
fi
done
# 重启服务
info "Rebooting all the things..."
/usr/syno/bin/synosystemctl restart nmbd
/usr/syno/bin/synosystemctl restart avahi
/usr/syno/bin/synosystemctl restart ldap-server
/usr/syno/bin/synopkg is_onoff ScsiTarget 1>/dev/null && /usr/syno/bin/synopkg restart ScsiTarget
/usr/syno/bin/synopkg is_onoff SynologyDrive 1>/dev/null && /usr/syno/bin/synopkg restart SynologyDrive
/usr/syno/bin/synopkg is_onoff WebDAVServer 1>/dev/null && /usr/syno/bin/synopkg restart WebDAVServer
/usr/syno/bin/synopkg is_onoff ActiveBackup 1>/dev/null && /usr/syno/bin/synopkg restart ActiveBackup
if ! /usr/syno/bin/synow3tool --gen-all && sudo /usr/syno/bin/synosystemctl restart nginx; then
warn "nginx failed to restart"
fi
info "Completed"
`.trim();
case "sh_replace_fnos_ssl":
return `# *** 需要 root 权限 ***
# 脚本参考 https://github.com/lfgyx/fnos_certificate_update/blob/main/src/update_cert.sh
# 请将以下变量替换为实际值
# 飞牛证书实际存放路径请在 \`/usr/trim/etc/network_cert_all.conf\` 中查看,注意不要修改文件名
$tmpFullchainPath = "${params?.certPath || "<your-fullchain-cert-path>"}" # 证书文件路径(与表单中保持一致)
$tmpCertPath = "${params?.certPathForServerOnly || "<your-server-cert-path>"}" # 服务器证书文件路径(与表单中保持一致)
$tmpKeyPath = "${params?.keyPath || "<your-key-path>"}" # 私钥文件路径(与表单中保持一致)
$fnFullchainPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/fullchain.crt" # 飞牛证书文件路径
$fnCertPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/example.com.crt" # 飞牛服务器证书文件路径
$fnKeyPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/example.com.key" # 飞牛私钥文件路径
$domain = "<your-domain-name>" # 域名
# 复制文件
cp -rf "$tmpFullchainPath" "$fnFullchainPath"
cp -rf "$tmpCertPath" "$fnCertPath"
cp -rf "$tmpKeyPath" "$fnKeyPath"
chmod 755 "$fnCertPath"
chmod 755 "$fnKeyPath"
chmod 755 "$fnFullchainPath"
# 更新数据库
NEW_EXPIRY_DATE=$(openssl x509 -enddate -noout -in "$fnCertPath" | sed "s/^.*=\\(.*\\)$/\\1/")
NEW_EXPIRY_TIMESTAMP=$(date -d "$NEW_EXPIRY_DATE" +%s%3N)
psql -U postgres -d trim_connect -c "UPDATE cert SET valid_to=$NEW_EXPIRY_TIMESTAMP WHERE domain='$domain'"
# 重启服务
systemctl restart webdav.service
systemctl restart smbftpd.service
systemctl restart trim_nginx.service
`.trim();
}
return _initPresetScript(key as Parameters<typeof _initPresetScript>[0], params);
};
const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormSSHConfigProps) => {
const { t } = useTranslation();
@@ -60,6 +182,16 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
.trim()
.nullish()
.refine((v) => fieldFormat !== FORMAT_PEM || !!v?.trim(), { message: t("workflow_node.deploy.form.ssh_key_path.tooltip") }),
certPathForServerOnly: z
.string()
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish(),
certPathForIntermediaOnly: z
.string()
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim()
.nullish(),
pfxPassword: z
.string()
.max(64, t("common.errmsg.string_max", { max: 256 }))
@@ -147,6 +279,24 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
const handlePresetPostScriptClick = (key: string) => {
switch (key) {
case "sh_reload_nginx":
{
formInst.setFieldValue("postCommand", initPresetScript(key));
}
break;
case "sh_replace_synologydsm_ssl":
case "sh_replace_fnos_ssl":
{
const presetScriptParams = {
certPath: formInst.getFieldValue("certPath"),
certPathForServerOnly: formInst.getFieldValue("certPathForServerOnly"),
certPathForIntermediaOnly: formInst.getFieldValue("certPathForIntermediaOnly"),
keyPath: formInst.getFieldValue("keyPath"),
};
formInst.setFieldValue("postCommand", initPresetScript(key, presetScriptParams));
}
break;
case "ps_binding_iis":
case "ps_binding_netsh":
case "ps_binding_rdp":
@@ -206,6 +356,24 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
>
<Input placeholder={t("workflow_node.deploy.form.ssh_key_path.placeholder")} />
</Form.Item>
<Form.Item
name="certPathForServerOnly"
label={t("workflow_node.deploy.form.ssh_servercert_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.ssh_servercert_path.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.ssh_servercert_path.placeholder")} />
</Form.Item>
<Form.Item
name="certPathForIntermediaOnly"
label={t("workflow_node.deploy.form.ssh_intermediacert_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.ssh_intermediacert_path.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.ssh_intermediacert_path.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldFormat === FORMAT_PFX}>
@@ -248,10 +416,6 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
</Form.Item>
</Show>
<Form.Item label={t("workflow_node.deploy.form.ssh_shell_env.label")}>
<Select options={[{ value: t("workflow_node.deploy.form.ssh_shell_env.value") }]} value={t("workflow_node.deploy.form.ssh_shell_env.value")} />
</Form.Item>
<Form.Item className="mb-0" htmlFor="null">
<label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4">
@@ -278,7 +442,13 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
</div>
</label>
<Form.Item name="preCommand" rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.ssh_pre_command.placeholder")} />
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language={["shell", "powershell"]}
placeholder={t("workflow_node.deploy.form.ssh_pre_command.placeholder")}
/>
</Form.Item>
</Form.Item>
@@ -291,11 +461,13 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
<div className="text-right">
<Dropdown
menu={{
items: ["sh_reload_nginx", "ps_binding_iis", "ps_binding_netsh", "ps_binding_rdp"].map((key) => ({
key,
label: t(`workflow_node.deploy.form.ssh_preset_scripts.option.${key}.label`),
onClick: () => handlePresetPostScriptClick(key),
})),
items: ["sh_reload_nginx", "sh_replace_synologydsm_ssl", "sh_replace_fnos_ssl", "ps_binding_iis", "ps_binding_netsh", "ps_binding_rdp"].map(
(key) => ({
key,
label: t(`workflow_node.deploy.form.ssh_preset_scripts.option.${key}.label`),
onClick: () => handlePresetPostScriptClick(key),
})
),
}}
trigger={["click"]}
>
@@ -308,7 +480,13 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
</div>
</label>
<Form.Item name="postCommand" rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.ssh_post_command.placeholder")} />
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language={["shell", "powershell"]}
placeholder={t("workflow_node.deploy.form.ssh_post_command.placeholder")}
/>
</Form.Item>
</Form.Item>

View File

@@ -51,7 +51,7 @@ const DeployNodeConfigFormTencentCloudSSLDeployConfig = ({
if (!v) return false;
return String(v)
.split(MULTIPLE_INPUT_DELIMITER)
.every((e) => /^[A-Za-z0-9*._-]+$/.test(e));
.every((e) => /^[A-Za-z0-9*._-|]+$/.test(e));
}, t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.errmsg.invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
@@ -138,7 +138,7 @@ const ResourceIdsModalInput = memo(({ value, trigger, onChange }: { value?: stri
const formSchema = z.object({
resourceIds: z.array(z.string()).refine((v) => {
return v.every((e) => !e?.trim() || /^[A-Za-z0-9*._-]+$/.test(e));
return v.every((e) => !e?.trim() || /^[A-Za-z0-9*._-|]+$/.test(e));
}, t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.errmsg.invalid")),
});
const formRule = createSchemaFieldRule(formSchema);

View File

@@ -1,8 +1,10 @@
import { useTranslation } from "react-i18next";
import { Alert, Form, type FormInstance, Input } from "antd";
import { Alert, Form, type FormInstance } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import CodeInput from "@/components/CodeInput";
type DeployNodeConfigFormWebhookConfigFieldValues = Nullish<{
webhookData: string;
}>;
@@ -39,8 +41,8 @@ const DeployNodeConfigFormWebhookConfig = ({ form: formInst, formName, disabled,
});
const formRule = createSchemaFieldRule(formSchema);
const handleWebhookDataBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
const handleWebhookDataBlur = () => {
const value = formInst.getFieldValue("webhookData");
try {
const json = JSON.stringify(JSON.parse(value), null, 2);
formInst.setFieldValue("webhookData", json);
@@ -68,9 +70,11 @@ const DeployNodeConfigFormWebhookConfig = ({ form: formInst, formName, disabled,
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.webhook_data.tooltip") }}></span>}
>
<Input.TextArea
allowClear
autoSize={{ minRows: 3, maxRows: 10 }}
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language="json"
placeholder={t("workflow_node.deploy.form.webhook_data.placeholder")}
onBlur={handleWebhookDataBlur}
/>

View File

@@ -177,7 +177,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
</Form.Item>
<Form.Item name="message" label={t("workflow_node.notify.form.message.label")} rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 3, maxRows: 5 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
<Input.TextArea autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
</Form.Item>
<Form.Item className="mb-0" htmlFor="null">

View File

@@ -1,8 +1,10 @@
import { useTranslation } from "react-i18next";
import { Alert, Form, type FormInstance, Input } from "antd";
import { Alert, Form, type FormInstance } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import CodeInput from "@/components/CodeInput";
type NotifyNodeConfigFormWebhookConfigFieldValues = Nullish<{
webhookData: string;
}>;
@@ -39,8 +41,8 @@ const NotifyNodeConfigFormWebhookConfig = ({ form: formInst, formName, disabled,
});
const formRule = createSchemaFieldRule(formSchema);
const handleWebhookDataBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
const handleWebhookDataBlur = () => {
const value = formInst.getFieldValue("webhookData");
try {
const json = JSON.stringify(JSON.parse(value), null, 2);
formInst.setFieldValue("webhookData", json);
@@ -68,9 +70,11 @@ const NotifyNodeConfigFormWebhookConfig = ({ form: formInst, formName, disabled,
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.notify.form.webhook_data.tooltip") }}></span>}
>
<Input.TextArea
allowClear
autoSize={{ minRows: 3, maxRows: 10 }}
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language="json"
placeholder={t("workflow_node.notify.form.webhook_data.placeholder")}
onBlur={handleWebhookDataBlur}
/>

Some files were not shown because too many files have changed in this diff Show More