Merge pull request #207 from fudiwei/feat/ecc-certs

feat: certificate key algorithm
This commit is contained in:
usual2970 2024-10-17 07:32:39 +08:00 committed by GitHub
commit d90319bb53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 661 additions and 455 deletions

View File

@ -19,8 +19,8 @@ git clone https://github.com/your_username/certimate.git
```
> **重要提示:**
> 建议为每个 bug 修复或新功能创建一个从 `main` 分支派生的新分支。如果你计划提交多个 PR请保持不同的改动在独立分支中以便更容易进行代码审查并最终合并。
> 保持一个 pr 只实现一个功能。
> 建议为每个 Bug 修复或新功能创建一个从 `main` 分支派生的新分支。如果你计划提交多个 PR请保持不同的改动在独立分支中以便更容易进行代码审查并最终合并。
> 保持一个 PR 只实现一个功能。
## 修改 Go 代码

View File

@ -10,7 +10,7 @@
Certimate 就是为了解决上述问题而产生的,它具有以下特点:
1. 操作简单:自动申请、部署、续期 SSL 证书,全程无需人工干预。
2. 支持私有部署:部署方法简单,只需下载二进制文件执行即可。二进制文件、docker 镜像全部用 github actions 生成,过程透明,可自行计。
2. 支持私有部署:部署方法简单,只需下载二进制文件执行即可。二进制文件、Docker 镜像全部用 Github Actions 生成,过程透明,可自行审计。
3. 数据安全:由于是私有部署,所有数据均存储在本地,不会保存在服务商的服务器,确保数据的安全性。
相关文章:
@ -19,7 +19,7 @@ Certimate 就是为了解决上述问题而产生的,它具有以下特点:
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
- [域名变量及部署授权组介绍](https://docs.certimate.me/blog/multi-deployer)
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。使用文档请访问[https://docs.certimate.me](https://docs.certimate.me)
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。使用文档请访问 [https://docs.certimate.me](https://docs.certimate.me)
## 一、安装
@ -40,7 +40,7 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
```
> [!NOTE]
> MacOS 在执行二进制文件时会提示:无法打开“certimate”因为 Apple 无法检查其是否包含恶意软件。可在系统设置> 隐私与安全性> 安全性 中点击 "仍然允许",然后再次尝试执行二进制文件。
> MacOS 在执行二进制文件时会提示:无法打开“Certimate”因为 Apple 无法检查其是否包含恶意软件。可在“系统设置 > 隐私与安全性 > 安全性”中点击“仍然允许”,然后再次尝试执行二进制文件。
### 2. Docker 安装
@ -72,14 +72,18 @@ go run main.go serve
## 三、支持的服务商列表
| 服务商 | 是否域名服务商 | 是否部署服务 | 备注 |
| ---------- | -------------- | ------------ | -------------------------------------------------------- |
| 阿里云 | 是 | 是 | 支持阿里云注册的域名,支持部署到阿里云 CDN,OSS |
| 腾讯云 | 是 | 是 | 支持腾讯云注册的域名,支持部署到腾讯云 CDN |
| 七牛云 | 否 | 是 | 七牛云没有注册域名服务,支持部署到七牛云 CDN |
| CloudFlare | 是 | 否 | 支持 CloudFlare 注册的域名CloudFlare 服务自带 SSL 证书 |
| SSH | 否 | 是 | 支持部署到 SSH 服务器 |
| WEBHOOK | 否 | 是 | 支持回调到 WEBHOOK |
| 服务商 | 支持申请证书 | 支持部署证书 | 备注 |
| :--------: | :----------: | :----------: | ------------------------------------------------------------ |
| 阿里云 | √ | √ | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN |
| 腾讯云 | √ | √ | 可签发在腾讯云注册的域名;可部署到腾讯云 CDN |
| 华为云 | √ | | 可签发在华为云注册的域名 |
| 七牛云 | | √ | 可部署到七牛云 CDN |
| CloudFlare | √ | | 可签发在 CloudFlare 注册的域名CloudFlare 服务自带 SSL 证书 |
| GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 |
| Namesilo | √ | | 可签发在 Namesilo 注册的域名 |
| 本地部署 | | √ | 可部署到本地服务器 |
| SSH | | √ | 可部署到 SSH 服务器 |
| Webhook | | √ | 可部署时回调到 Webhook |
## 四、系统截图
@ -97,56 +101,60 @@ go run main.go serve
Certimate 的工作流程如下:
- 用户通过 Certimate 管理页面填写申请证书的信息,包括域名、dns 服务商的授权信息、以及要部署到的服务商的授权信息。
- 用户通过 Certimate 管理页面填写申请证书的信息,包括域名、DNS 服务商的授权信息、以及要部署到的服务商的授权信息。
- Certimate 向证书厂商的 API 发起申请请求,获取 SSL 证书。
- Certimate 存储证书信息,包括证书内容、私钥、证书有效期等,并在证书即将过期时自动续期。
- Certimate 向服务商的 API 发起部署请求,将证书部署到服务商的服务器上。
这就涉及域名、dns 服务商的授权信息、部署服务商的授权信息等。
这就涉及域名、DNS 服务商的授权信息、部署服务商的授权信息等。
### 1. 域名
就是要申请证书的域名。
### 2. dns 服务商授权信息
### 2. DNS 服务商授权信息
给域名申请证书需要证明域名是你的,所以我们手动申请证书的时候一般需要在域名服务商的控制台解析记录中添加一个 TXT 记录。
给域名申请证书需要证明域名是你的,所以我们手动申请证书的时候一般需要在域名服务商的控制台解析记录中添加一个 TXT 域名解析记录。
Certimate 会自动添加一个 TXT 记录,你只需要在 Certimate 后台中填写你的域名服务商的授权信息即可。
Certimate 会自动添加一个 TXT 域名解析记录,你只需要在 Certimate 后台中填写你的域名服务商的授权信息即可。
比如你在阿里云购买的域名,授权信息如下:
```bash
accessKeyId: xxx
accessKeySecret: TOKEN
accessKeyId: your-access-key-id
accessKeySecret: your-access-key-secret
```
在腾讯云购买的域名,授权信息如下:
```bash
secretId: xxx
secretKey: TOKEN
secretId: your-secret-id
secretKey: your-secret-key
```
注意,此授权信息需具有访问域名及 DNS 解析的管理权限,具体的权限清单请参阅各服务商自己的技术文档。
### 3. 部署服务商授权信息
Certimate 申请证书后,会自动将证书部署到你指定的目标上,比如阿里云 CDN 这时你需要填写阿里云的授权信息。Certimate 会根据你填写的授权信息及域名找到对应的 CDN 服务,并将证书部署到对应的 CDN 服务上。
Certimate 申请证书后,会自动将证书部署到你指定的目标上,比如阿里云 CDNCertimate 会根据你填写的授权信息及域名找到对应的 CDN 服务,并将证书部署到对应的 CDN 服务上。
部署服务商授权信息和 dns 服务商授权信息一致,区别在于 dns 服务商授权信息用于证明域名是你的,部署服务商授权信息用于提供证书部署的授权信息。
部署服务商授权信息和 DNS 服务商授权信息基本一致,区别在于 DNS 服务商授权信息用于证明域名是你的,部署服务商授权信息用于提供证书部署的授权信息。
注意,此授权信息需具有访问部署目标服务的相关管理权限,具体的权限清单请参阅各服务商自己的技术文档。
## 六、常见问题
Q: 提供 saas 服务吗?
Q: 提供 SaaS 服务吗?
> A: 不提供,目前仅支持 self-hosted私有部署
Q: 数据安全?
> A: 由于仅支持私有部署,各种数据都保存在用户的服务器上。另外 Certimate 源码也开源,二进制包及 Docker 镜像打包过程全部使用 Github actions 进行,过程透明可见,可自行审计。
> A: 由于仅支持私有部署,各种数据都保存在用户的服务器上。另外 Certimate 源码也开源,二进制包及 Docker 镜像打包过程全部使用 Github Actions 进行,过程透明可见,可自行审计。
Q: 自动续期证书?
> A: 已经申请的证书会在过期前 10 天自动续期。每天会检查一次证书是否快要过期,快要过期时会自动重新申请证书并部署到目标服务上。
> A: 已经申请的证书会在**过期前 10 天**自动续期。每天会检查一次证书是否快要过期,快要过期时会自动重新申请证书并部署到目标服务上。
## 七、贡献
@ -154,18 +162,18 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.
你可以通过以下方式来支持 Certimate 的开发:
- [提交代码:如果你发现了 bug 或有新的功能需求,而你又有相关经验,可以提交代码给我们](CONTRIBUTING.md)。
- 提交 issue功能建议或者 bug 可以[提交 issue](https://github.com/usual2970/certimate/issues) 给我们。
- 提交代码:如果你发现了 Bug 或有新的功能需求,而你又有相关经验,可以[提交代码](CONTRIBUTING.md)给我们
- 提交 Issue功能建议或者 Bug 可以[提交 Issue](https://github.com/usual2970/certimate/issues) 给我们。
支持更多服务商、UI 的优化改进、BUG 修复、文档完善等,欢迎大家提交 PR。
支持更多服务商、UI 的优化改进、Bug 修复、文档完善等,欢迎大家提交 PR。
## 八、加入社区
- [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
- 微信群聊(超 200 人需邀请入群,可先加作者好友)
- 微信群聊(超 200 人需邀请入群,可先加作者好友)
<img src="https://i.imgur.com/8xwsLTA.png" width="400"/>
## 九、Star History
## 九、Star 趋势图
[![Stargazers over time](https://starchart.cc/usual2970/certimate.svg?variant=adaptive)](https://starchart.cc/usual2970/certimate)

View File

@ -18,7 +18,7 @@ Related articles:
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
- [Introduction to Domain Variables and Deployment Authorization Groups](https://docs.certimate.me/blog/multi-deployer)
Certimate aims to provide users with a secure and user-friendly SSL certificate management solution. For usage documentation, please visit.[https://docs.certimate.me](https://docs.certimate.me)
Certimate aims to provide users with a secure and user-friendly SSL certificate management solution. For usage documentation, please visit [https://docs.certimate.me](https://docs.certimate.me).
## Installation
@ -71,14 +71,18 @@ password1234567890
## List of Supported Providers
| Provider | Domain Registrar | Deployment Service | Remarks |
| ------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------- |
| Alibaba Cloud | Yes | Yes | Supports domains registered with Alibaba Cloud; supports deployment to Alibaba Cloud CDN and OSS. |
| Tencent Cloud | Yes | Yes | Supports domains registered with Tencent Cloud; supports deployment to Tencent Cloud CDN. |
| Qiniu Cloud | No | Yes | Qiniu Cloud does not offer domain registration services; supports deployment to Qiniu Cloud CDN. |
| Cloudflare | Yes | No | Supports domains registered with Cloudflare; Cloudflare services come with SSL certificates. |
| SSH | No | Yes | Supports deployment to SSH servers. |
| WEBHOOK | No | Yes | Supports callbacks to WEBHOOK. |
| Provider | Registration | Deployment | Remarks |
| :-----------: | :----------: | :--------: | ------------------------------------------------------------------------------------------- |
| Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN |
| Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud CDN |
| Huawei Cloud | √ | | Supports domains registered on Huawei Cloud |
| Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud CDN |
| CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates |
| GoDaddy | √ | | Supports domains registered on GoDaddy |
| Namesilo | √ | | Supports domains registered on Namesilo |
| Local Deploy | | √ | Supports deployment to local servers |
| SSH | | √ | Supports deployment to SSH servers |
| Webhook | | √ | Supports callback to Webhook |
## Screenshots
@ -116,23 +120,27 @@ Certimate will automatically add a TXT record for you; you only need to fill in
For example, if you purchased the domain from Alibaba Cloud, the authorization information would be as follows:
```bash
accessKeyId: xxx
accessKeySecret: TOKEN
accessKeyId: your-access-key-id
accessKeySecret: your-access-key-secret
```
If you purchased the domain from Tencent Cloud, the authorization information would be as follows:
```bash
secretId: xxx
secretKey: TOKEN
secretId: your-secret-id
secretKey: your-secret-key
```
Notes: This authorization information requires relevant administration permissions for accessing the DNS services. Please refer to the documentations of each service provider for the specific permissions list.
### 3. Authorization Information for the Deployment Service Provider
After Certimate applies for the certificate, it will automatically deploy the certificate to your specified target, such as Alibaba Cloud CDN. At this point, you need to fill in the authorization information for Alibaba Cloud. Certimate will use the authorization information and domain name you provided to locate the corresponding CDN service and deploy the certificate to that service.
The authorization information for the deployment service provider is the same as that for the DNS provider, with the distinction that the DNS provider's authorization information is used to prove that the domain belongs to you, while the deployment service provider's authorization information is used to provide authorization for the certificate deployment.
Notes: This authorization information requires relevant administration permissions to access the target deployment services. Please refer to the documentations of each service provider for the specific permissions list.
## FAQ
Q: Do you provide SaaS services?
@ -145,7 +153,7 @@ Q: Data Security?
Q: Automatic Certificate Renewal?
> A: Certificates that have already been issued will be automatically renewed 10 days before expiration. The system checks once a day to see if any certificates are nearing expiration, and if so, it will automatically reapply for the certificate and deploy it to the target service.
> A: Certificates that have already been issued will be automatically renewed **10 days before expiration**. The system checks once a day to see if any certificates are nearing expiration, and if so, it will automatically reapply for the certificate and deploy it to the target service.
## Contributing
@ -164,3 +172,7 @@ Support for more service providers, UI enhancements, bug fixes, and documentatio
- Wechat Group
<img src="https://i.imgur.com/zSHEoIm.png" width="400"/>
## Star History
[![Stargazers over time](https://starchart.cc/usual2970/certimate.svg?variant=adaptive)](https://starchart.cc/usual2970/certimate)

View File

@ -60,11 +60,12 @@ type Certificate struct {
}
type ApplyOption struct {
Email string `json:"email"`
Domain string `json:"domain"`
Access string `json:"access"`
Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"`
Email string `json:"email"`
Domain string `json:"domain"`
Access string `json:"access"`
KeyAlgorithm string `json:"keyAlgorithm"`
Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"`
}
type ApplyUser struct {
@ -91,11 +92,10 @@ type Applicant interface {
func Get(record *models.Record) (Applicant, error) {
if record.GetString("applyConfig") == "" {
return nil, errors.New("apply config is empty")
return nil, errors.New("applyConfig is empty")
}
applyConfig := &domain.ApplyConfig{}
record.UnmarshalJSONField("applyConfig", applyConfig)
access, err := app.GetApp().Dao().FindRecordById("access", applyConfig.Access)
@ -103,17 +103,23 @@ func Get(record *models.Record) (Applicant, error) {
return nil, fmt.Errorf("access record not found: %w", err)
}
email := applyConfig.Email
if email == "" {
email = defaultEmail
if applyConfig.Email == "" {
applyConfig.Email = defaultEmail
}
if applyConfig.Timeout == 0 {
applyConfig.Timeout = defaultTimeout
}
option := &ApplyOption{
Email: email,
Domain: record.GetString("domain"),
Access: access.GetString("config"),
Nameservers: applyConfig.Nameservers,
Timeout: applyConfig.Timeout,
Email: applyConfig.Email,
Domain: record.GetString("domain"),
Access: access.GetString("config"),
KeyAlgorithm: applyConfig.KeyAlgorithm,
Nameservers: applyConfig.Nameservers,
Timeout: applyConfig.Timeout,
}
switch access.GetString("configType") {
case configTypeAliyun:
return NewAliyun(option), nil
@ -171,7 +177,7 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM.
config.CADirURL = sslProviderUrls[sslProvider.Provider]
config.Certificate.KeyType = certcrypto.RSA2048
config.Certificate.KeyType = parseKeyAlgorithm(option.KeyAlgorithm)
// A client facilitates communication with the CA server.
client, err := lego.NewClient(config)
@ -180,7 +186,7 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
}
challengeOptions := make([]dns01.ChallengeOption, 0)
nameservers := ParseNameservers(option.Nameservers)
nameservers := parseNameservers(option.Nameservers)
if len(nameservers) > 0 {
challengeOptions = append(challengeOptions, dns01.AddRecursiveNameservers(nameservers))
}
@ -195,7 +201,6 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
myUser.Registration = reg
domains := strings.Split(option.Domain, ";")
request := certificate.ObtainRequest{
Domains: domains,
Bundle: true,
@ -231,7 +236,6 @@ func getReg(client *lego.Client, sslProvider *SSLProviderConfig) (*registration.
default:
err = errors.New("unknown ssl provider")
}
if err != nil {
@ -241,15 +245,13 @@ func getReg(client *lego.Client, sslProvider *SSLProviderConfig) (*registration.
return reg, nil
}
func ParseNameservers(ns string) []string {
func parseNameservers(ns string) []string {
nameservers := make([]string, 0)
lines := strings.Split(ns, ";")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
@ -259,3 +261,22 @@ func ParseNameservers(ns string) []string {
return nameservers
}
func parseKeyAlgorithm(algo string) certcrypto.KeyType {
switch algo {
case "RSA2048":
return certcrypto.RSA2048
case "RSA3072":
return certcrypto.RSA3072
case "RSA4096":
return certcrypto.RSA4096
case "RSA8192":
return certcrypto.RSA8192
case "EC256":
return certcrypto.EC256
case "EC384":
return certcrypto.EC384
default:
return certcrypto.RSA2048
}
}

View File

@ -1,10 +1,11 @@
package domain
type ApplyConfig struct {
Email string `json:"email"`
Access string `json:"access"`
Timeout int64 `json:"timeout"`
Nameservers string `json:"nameservers"`
Email string `json:"email"`
Access string `json:"access"`
KeyAlgorithm string `json:"keyAlgorithm"`
Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"`
}
type DeployConfig struct {

329
ui/dist/assets/index-C20g8xcX.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
ui/dist/assets/index-YqBWA4KK.css vendored Normal file

File diff suppressed because one or more lines are too long

4
ui/dist/index.html vendored
View File

@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Certimate - Your Trusted SSL Automation Partner</title>
<script type="module" crossorigin src="/assets/index-DIhd7QG6.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CV_7sKTK.css">
<script type="module" crossorigin src="/assets/index-C20g8xcX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-YqBWA4KK.css">
</head>
<body class="bg-background">
<div id="root"></div>

77
ui/package-lock.json generated
View File

@ -11,6 +11,7 @@
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
@ -1184,6 +1185,35 @@
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collapsible": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz",
"integrity": "sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-presence": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.1.tgz",
@ -1234,15 +1264,15 @@
}
},
"node_modules/@radix-ui/react-collapsible": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz",
"integrity": "sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==",
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.1.tgz",
"integrity": "sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-presence": "1.1.0",
"@radix-ui/react-presence": "1.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
@ -1262,6 +1292,43 @@
}
}
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.1.tgz",
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
"integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",

View File

@ -13,6 +13,7 @@
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",

View File

@ -0,0 +1,22 @@
import * as React from "react";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
import { cn } from "@/lib/utils";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = React.forwardRef<
React.ElementRef<typeof CollapsiblePrimitive.CollapsibleContent>,
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleContent>
>(({ className, ...props }, ref) => (
<CollapsiblePrimitive.CollapsibleContent
ref={ref}
className={cn("overflow-y-hidden transition-all data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down", className)}
{...props}
/>
));
CollapsibleContent.displayName = CollapsiblePrimitive.CollapsibleContent.displayName;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@ -50,8 +50,9 @@ export type DeployConfig = {
export type ApplyConfig = {
access: string;
email: string;
timeout?: number;
keyAlgorithm?: string;
nameservers?: string;
timeout?: number;
};
export type Statistic = {

View File

@ -30,15 +30,17 @@
"domain.application.form.domain.changed.message": "Domain updated successfully",
"domain.application.form.email.label": "Email",
"domain.application.form.email.tips": "(A email is required to apply for a certificate)",
"domain.application.form.email.placeholder": "Please select email",
"domain.application.form.email.add": "Add Email",
"domain.application.form.email.list": "Email List",
"domain.application.form.email.errmsg.empty": "Please select email",
"domain.application.form.access.label": "DNS Provider Authorization Configuration",
"domain.application.form.access.placeholder": "Please select DNS provider authorization configuration",
"domain.application.form.access.errmsg.empty": "Please select DNS provider authorization configuration",
"domain.application.form.access.list": "Provider Authorization Configurations",
"domain.application.form.timeout.label": "Timeout",
"domain.application.form.timeoue.placeholder": "Timeout (seconds)",
"domain.application.form.advanced_settings.label": "Advanced Settings",
"domain.application.form.key_algorithm.label": "Certificate Key Algorithm",
"domain.application.form.key_algorithm.placeholder": "Please select certificate key algorithm",
"domain.application.form.timeout.label": "DNS Propagation Timeout (seconds)",
"domain.application.form.timeoue.placeholder": "Please enter maximum waiting time for DNS propagation",
"domain.application.unsaved.message": "Please save applyment configuration first",
"domain.deployment.tab": "Deploy Settings",

View File

@ -30,15 +30,17 @@
"domain.application.form.domain.changed.message": "域名编辑成功",
"domain.application.form.email.label": "邮箱",
"domain.application.form.email.tips": "(申请证书需要提供邮箱)",
"domain.application.form.email.placeholder": "请选择邮箱",
"domain.application.form.email.add": "添加邮箱",
"domain.application.form.email.list": "邮箱列表",
"domain.application.form.email.errmsg.empty": "请选择邮箱",
"domain.application.form.access.label": "DNS 服务商授权配置",
"domain.application.form.access.placeholder": "请选择 DNS 服务商授权配置",
"domain.application.form.access.errmsg.empty": "请选择 DNS 服务商授权配置",
"domain.application.form.access.list": "已有的 DNS 服务商授权配置",
"domain.application.form.timeout.label": "超时时间",
"domain.application.form.timeoue.placeholder": "超时时间(单位:秒)",
"domain.application.form.advanced_settings.label": "高级设置",
"domain.application.form.key_algorithm.label": "数字证书算法",
"domain.application.form.key_algorithm.placeholder": "请选择数字证书算法",
"domain.application.form.timeout.label": "DNS 传播检查超时时间(单位:秒)",
"domain.application.form.timeoue.placeholder": "请输入 DNS 传播检查超时时间",
"domain.application.unsaved.message": "请先保存申请配置",
"domain.deployment.tab": "部署配置",

View File

@ -4,11 +4,12 @@ import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Plus } from "lucide-react";
import { ChevronsUpDown, Plus } from "lucide-react";
import { ClientResponseError } from "pocketbase";
import { Button } from "@/components/ui/button";
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
@ -58,8 +59,9 @@ const Edit = () => {
}),
email: z.string().email("common.errmsg.email_invalid").optional(),
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
message: "domain.application.form.access.errmsg.empty",
message: "domain.application.form.access.placeholder",
}),
keyAlgorithm: z.string().optional(),
nameservers: z.string().optional(),
timeout: z.number().optional(),
});
@ -71,6 +73,7 @@ const Edit = () => {
domain: "",
email: "",
access: "",
keyAlgorithm: "RSA2048",
nameservers: "",
timeout: 60,
},
@ -83,6 +86,7 @@ const Edit = () => {
domain: domain.domain,
email: domain.applyConfig?.email,
access: domain.applyConfig?.access,
keyAlgorithm: domain.applyConfig?.keyAlgorithm,
nameservers: domain.applyConfig?.nameservers,
timeout: domain.applyConfig?.timeout,
});
@ -101,6 +105,7 @@ const Edit = () => {
applyConfig: {
email: data.email ?? "",
access: data.access,
keyAlgorithm: data.keyAlgorithm,
nameservers: data.nameservers,
timeout: data.timeout,
},
@ -235,6 +240,7 @@ const Edit = () => {
</FormItem>
)}
/>
{/* 邮箱 */}
<FormField
control={form.control}
@ -261,7 +267,7 @@ const Edit = () => {
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.application.form.email.errmsg.empty")} />
<SelectValue placeholder={t("domain.application.form.email.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
@ -280,7 +286,8 @@ const Edit = () => {
</FormItem>
)}
/>
{/* 授权 */}
{/* DNS 服务商授权 */}
<FormField
control={form.control}
name="access"
@ -332,48 +339,95 @@ const Edit = () => {
)}
/>
{/* 超时时间 */}
<FormField
control={form.control}
name="timeout"
render={({ field }) => (
<FormItem>
<FormLabel>{t("domain.application.form.timeout.label")}</FormLabel>
<FormControl>
<Input
type="number"
placeholder={t("ddomain.application.form.timeout.placeholder")}
{...field}
value={field.value}
onChange={(e) => {
form.setValue("timeout", parseInt(e.target.value));
}}
<div>
<hr />
<Collapsible>
<CollapsibleTrigger className="w-full my-4">
<div className="flex items-center justify-between space-x-4">
<span className="flex-1 text-sm text-gray-600 text-left">{t("domain.application.form.advanced_settings.label")}</span>
<ChevronsUpDown className="h-4 w-4" />
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col space-y-8">
{/* 证书算法 */}
<FormField
control={form.control}
name="keyAlgorithm"
render={({ field }) => (
<FormItem>
<FormLabel>{t("domain.application.form.key_algorithm.label")}</FormLabel>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("keyAlgorithm", value);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("domain.application.form.key_algorithm.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="RSA2048">RSA2048</SelectItem>
<SelectItem value="RSA3072">RSA3072</SelectItem>
<SelectItem value="RSA4096">RSA4096</SelectItem>
<SelectItem value="RSA8192">RSA8192</SelectItem>
<SelectItem value="EC256">EC256</SelectItem>
<SelectItem value="EC384">EC384</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormItem>
)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* DNS */}
<FormField
control={form.control}
name="nameservers"
render={({ field }) => (
<FormItem>
<StringList
value={field.value ?? ""}
onValueChange={(val: string) => {
form.setValue("nameservers", val);
}}
valueType="dns"
></StringList>
{/* nameservers */}
<FormField
control={form.control}
name="nameservers"
render={({ field }) => (
<FormItem>
<StringList
value={field.value ?? ""}
onValueChange={(val: string) => {
form.setValue("nameservers", val);
}}
valueType="dns"
></StringList>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
{/* DNS 超时时间 */}
<FormField
control={form.control}
name="timeout"
render={({ field }) => (
<FormItem>
<FormLabel>{t("domain.application.form.timeout.label")}</FormLabel>
<FormControl>
<Input
type="number"
placeholder={t("domain.application.form.timeout.placeholder")}
{...field}
value={field.value}
onChange={(e) => {
form.setValue("timeout", parseInt(e.target.value));
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CollapsibleContent>
</Collapsible>
</div>
<div className="flex justify-end">
<Button type="submit">{domain?.id ? t("common.save") : t("common.next")}</Button>

View File

@ -70,10 +70,20 @@ module.exports = {
height: "0",
},
},
"collapsible-down": {
from: { height: 0 },
to: { height: "var(--radix-collapsible-content-height)" },
},
"collapsible-up": {
from: { height: "var(--radix-collapsible-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"collapsible-down": "collapsible-down 0.2s ease-out",
"collapsible-up": "collapsible-up 0.2s ease-out",
},
},
},