Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39bffe3389 | ||
|
|
3f2767b28b | ||
|
|
312c6e685a | ||
|
|
d2b6ab75b7 | ||
|
|
dc16294b3d | ||
|
|
77dfcef168 | ||
|
|
30ef5841d6 | ||
|
|
217ba85ff8 | ||
|
|
71e2555391 | ||
|
|
f036eb1cf2 | ||
|
|
1347066549 | ||
|
|
7fc149f67d | ||
|
|
dfba5ee638 | ||
|
|
9ba79f996f | ||
|
|
cd85000908 | ||
|
|
995349ab3e | ||
|
|
4fa8031318 | ||
|
|
3f45bb1629 | ||
|
|
0e139e6284 | ||
|
|
82dbfc6de3 | ||
|
|
9b2937d601 |
27
README.md
27
README.md
@@ -18,13 +18,8 @@ Certimate 就是为了解决上述问题而产生的,它具有以下特点:
|
|||||||
* [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
* [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
||||||
* [域名变量及部署授权组介绍](https://docs.certimate.me/blog/multi-deployer)
|
* [域名变量及部署授权组介绍](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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 一、安装
|
## 一、安装
|
||||||
|
|
||||||
安装 Certimate 非常简单,你可以选择以下方式之一进行安装:
|
安装 Certimate 非常简单,你可以选择以下方式之一进行安装:
|
||||||
@@ -37,10 +32,14 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
|
|||||||
./certimate serve
|
./certimate serve
|
||||||
```
|
```
|
||||||
|
|
||||||
|
或运行以下命令自动给 Certimate 自身添加证书
|
||||||
|
```bash
|
||||||
|
./certimate serve 你的域名
|
||||||
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> MacOS 在执行二进制文件时会提示:无法打开“certimate”,因为Apple无法检查其是否包含恶意软件。可在系统设置> 隐私与安全性> 安全性 中点击 "仍然允许",然后再次尝试执行二进制文件。
|
> MacOS 在执行二进制文件时会提示:无法打开“certimate”,因为Apple无法检查其是否包含恶意软件。可在系统设置> 隐私与安全性> 安全性 中点击 "仍然允许",然后再次尝试执行二进制文件。
|
||||||
|
|
||||||
|
|
||||||
### 2. Docker 安装
|
### 2. Docker 安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -58,7 +57,6 @@ go mod vendor
|
|||||||
go run main.go serve
|
go run main.go serve
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## 二、使用
|
## 二、使用
|
||||||
|
|
||||||
执行完上述安装操作后,在浏览器中访问 `http://127.0.0.1:8090` 即可访问 Certimate 管理页面。
|
执行完上述安装操作后,在浏览器中访问 `http://127.0.0.1:8090` 即可访问 Certimate 管理页面。
|
||||||
@@ -73,7 +71,7 @@ go run main.go serve
|
|||||||
## 三、支持的服务商列表
|
## 三、支持的服务商列表
|
||||||
|
|
||||||
| 服务商 | 是否域名服务商 | 是否部署服务 | 备注 |
|
| 服务商 | 是否域名服务商 | 是否部署服务 | 备注 |
|
||||||
|------|------|-----|------|
|
| ---------- | -------------- | ------------ | ------------------------------------------------------ |
|
||||||
| 阿里云 | 是 | 是 | 支持阿里云注册的域名,支持部署到阿里云 CDN,OSS |
|
| 阿里云 | 是 | 是 | 支持阿里云注册的域名,支持部署到阿里云 CDN,OSS |
|
||||||
| 腾讯云 | 是 | 是 | 支持腾讯云注册的域名,支持部署到腾讯云 CDN |
|
| 腾讯云 | 是 | 是 | 支持腾讯云注册的域名,支持部署到腾讯云 CDN |
|
||||||
| 七牛云 | 否 | 是 | 七牛云没有注册域名服务,支持部署到七牛云 CDN |
|
| 七牛云 | 否 | 是 | 七牛云没有注册域名服务,支持部署到七牛云 CDN |
|
||||||
@@ -81,9 +79,6 @@ go run main.go serve
|
|||||||
| SSH | 否 | 是 | 支持部署到 SSH 服务器 |
|
| SSH | 否 | 是 | 支持部署到 SSH 服务器 |
|
||||||
| WEBHOOK | 否 | 是 | 支持回调到 WEBHOOK |
|
| WEBHOOK | 否 | 是 | 支持回调到 WEBHOOK |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 四、系统截图
|
## 四、系统截图
|
||||||
|
|
||||||

|

|
||||||
@@ -96,7 +91,6 @@ go run main.go serve
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## 五、概念
|
## 五、概念
|
||||||
|
|
||||||
Certimate 的工作流程如下:
|
Certimate 的工作流程如下:
|
||||||
@@ -140,7 +134,6 @@ Certimate 申请证书后,会自动将证书部署到你指定的目标上,
|
|||||||
|
|
||||||
## 六、常见问题
|
## 六、常见问题
|
||||||
|
|
||||||
|
|
||||||
Q: 提供saas服务吗?
|
Q: 提供saas服务吗?
|
||||||
|
|
||||||
> A: 不提供,目前仅支持self-hosted(私有部署)。
|
> A: 不提供,目前仅支持self-hosted(私有部署)。
|
||||||
@@ -153,8 +146,6 @@ Q: 自动续期证书?
|
|||||||
|
|
||||||
> A: 已经申请的证书会在过期前10天自动续期。每天会检查一次证书是否快要过期,快要过期时会自动重新申请证书并部署到目标服务上。
|
> A: 已经申请的证书会在过期前10天自动续期。每天会检查一次证书是否快要过期,快要过期时会自动重新申请证书并部署到目标服务上。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 七、贡献
|
## 七、贡献
|
||||||
|
|
||||||
Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.md)。你可以使用它做任何你想做的事,甚至把它当作一个付费服务提供给用户。
|
Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.md)。你可以使用它做任何你想做的事,甚至把它当作一个付费服务提供给用户。
|
||||||
@@ -169,7 +160,9 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.
|
|||||||
## 八、加入社区
|
## 八、加入社区
|
||||||
|
|
||||||
* [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
|
* [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
|
||||||
|
* 微信群聊(超200人需邀请入群,可先加作者好友)
|
||||||
|
|
||||||
* 微信群聊
|
<img src="https://i.imgur.com/8xwsLTA.png" width="400"/>
|
||||||
|
|
||||||
<img src="https://i.imgur.com/zSHEoIm.png" width="400"/>
|
## 九、Star History
|
||||||
|
[](https://starchart.cc/usual2970/certimate)
|
||||||
|
|||||||
25
README_EN.md
25
README_EN.md
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
For individuals managing personal projects or those responsible for IT operations in small businesses who need to manage multiple domain names, applying for certificates manually comes with several drawbacks:
|
For individuals managing personal projects or those responsible for IT operations in small businesses who need to manage multiple domain names, applying for certificates manually comes with several drawbacks:
|
||||||
|
|
||||||
|
|
||||||
1. 😱Troublesome: Applying for and deploying certificates isn’t difficult, but it can be quite a hassle, especially when managing multiple domains.
|
1. 😱Troublesome: Applying for and deploying certificates isn’t difficult, but it can be quite a hassle, especially when managing multiple domains.
|
||||||
2. 😭Easily forgotten: The current free certificate has a validity period of only 90 days, requiring regular renewal operations. This increases the workload and makes it easy to forget, which can result in the website becoming inaccessible.
|
2. 😭Easily forgotten: The current free certificate has a validity period of only 90 days, requiring regular renewal operations. This increases the workload and makes it easy to forget, which can result in the website becoming inaccessible.
|
||||||
|
|
||||||
@@ -19,30 +18,29 @@ Related articles:
|
|||||||
* [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
* [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
||||||
* [Introduction to Domain Variables and Deployment Authorization Groups](https://docs.certimate.me/blog/multi-deployer)
|
* [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
|
## Installation
|
||||||
|
|
||||||
Installing Certimate is very simple, you can choose one of the following methods for installation:
|
Installing Certimate is very simple, you can choose one of the following methods for installation:
|
||||||
|
|
||||||
### 1. Binary File
|
### 1. Binary File
|
||||||
|
|
||||||
|
|
||||||
You can download the precompiled binary files directly from the [Releases page](https://github.com/usual2970/certimate/releases), and after extracting them, execute:
|
You can download the precompiled binary files directly from the [Releases page](https://github.com/usual2970/certimate/releases), and after extracting them, execute:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./certimate serve
|
./certimate serve
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or run the following command to automatically add a certificate to Certimate itself.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./certimate serve yourDomain
|
||||||
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> When executing the binary file on macOS, you may see a prompt saying: “Cannot open ‘certimate’ because Apple cannot check it for malicious software.” You can go to System Preferences > Security & Privacy > General, then click “Allow Anyway,” and try executing the binary file again.
|
> When executing the binary file on macOS, you may see a prompt saying: “Cannot open ‘certimate’ because Apple cannot check it for malicious software.” You can go to System Preferences > Security & Privacy > General, then click “Allow Anyway,” and try executing the binary file again.
|
||||||
|
|
||||||
|
|
||||||
### 2. Docker Installation
|
### 2. Docker Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -60,7 +58,6 @@ go mod vendor
|
|||||||
go run main.go serve
|
go run main.go serve
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
After completing the installation steps above, you can access the Certimate management page by visiting http://127.0.0.1:8090 in your browser.
|
After completing the installation steps above, you can access the Certimate management page by visiting http://127.0.0.1:8090 in your browser.
|
||||||
@@ -74,9 +71,8 @@ password:1234567890
|
|||||||
|
|
||||||
## List of Supported Providers
|
## List of Supported Providers
|
||||||
|
|
||||||
|
|
||||||
| Provider | Domain Registrar | Deployment Service | Remarks |
|
| Provider | Domain Registrar | Deployment Service | Remarks |
|
||||||
|--------------|------------------|--------------------|------------------------------------------------------|
|
| ------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------- |
|
||||||
| Alibaba Cloud | Yes | Yes | Supports domains registered with Alibaba Cloud; supports deployment to Alibaba Cloud CDN and OSS. |
|
| 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. |
|
| 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. |
|
| Qiniu Cloud | No | Yes | Qiniu Cloud does not offer domain registration services; supports deployment to Qiniu Cloud CDN. |
|
||||||
@@ -84,8 +80,6 @@ password:1234567890
|
|||||||
| SSH | No | Yes | Supports deployment to SSH servers. |
|
| SSH | No | Yes | Supports deployment to SSH servers. |
|
||||||
| WEBHOOK | No | Yes | Supports callbacks to WEBHOOK. |
|
| WEBHOOK | No | Yes | Supports callbacks to WEBHOOK. |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|

|
||||||
@@ -98,7 +92,6 @@ password:1234567890
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## Concepts
|
## Concepts
|
||||||
|
|
||||||
The workflow of Certimate is as follows:
|
The workflow of Certimate is as follows:
|
||||||
@@ -142,7 +135,6 @@ The authorization information for the deployment service provider is the same as
|
|||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
|
|
||||||
Q: Do you provide SaaS services?
|
Q: Do you provide SaaS services?
|
||||||
|
|
||||||
> A: No, we do not provide that. Currently, we only support self-hosted.
|
> A: No, we do not provide that. Currently, we only support self-hosted.
|
||||||
@@ -155,8 +147,6 @@ 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
|
## Contributing
|
||||||
|
|
||||||
Certimate is a free and open-source project, licensed under the [MIT License](LICENSE.md). You can use it for anything you want, even offering it as a paid service to users.
|
Certimate is a free and open-source project, licensed under the [MIT License](LICENSE.md). You can use it for anything you want, even offering it as a paid service to users.
|
||||||
@@ -171,7 +161,6 @@ Support for more service providers, UI enhancements, bug fixes, and documentatio
|
|||||||
## Join the Community
|
## Join the Community
|
||||||
|
|
||||||
* [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
|
* [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
|
||||||
|
|
||||||
* Wechat Group
|
* Wechat Group
|
||||||
|
|
||||||
<img src="https://i.imgur.com/zSHEoIm.png" width="400"/>
|
<img src="https://i.imgur.com/zSHEoIm.png" width="400"/>
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -18,7 +18,8 @@ require (
|
|||||||
github.com/pocketbase/dbx v1.10.1
|
github.com/pocketbase/dbx v1.10.1
|
||||||
github.com/pocketbase/pocketbase v0.22.18
|
github.com/pocketbase/pocketbase v0.22.18
|
||||||
github.com/qiniu/go-sdk/v7 v7.22.0
|
github.com/qiniu/go-sdk/v7 v7.22.0
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
|
||||||
golang.org/x/crypto v0.26.0
|
golang.org/x/crypto v0.26.0
|
||||||
)
|
)
|
||||||
|
|||||||
5
go.sum
5
go.sum
@@ -381,9 +381,12 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
|||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
||||||
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017 h1:OymmfmyFkvHirY3WHsoRT3cdTEsqygLbMn8jM41erK4=
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017/go.mod h1:gnLxGXlLmF+jDqWR1/RVoF/UUwxQxomQhkc0oN7KeuI=
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992 h1:266lOve+E8vzhnrb/Mr05Ee+oxXD9C82JiusY/AZqXw=
|
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017 h1:SXrldOXwgomYuATVAuz5ofpTjB+99qVELgdy5R5kMgI=
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 h1:LoYv5u+gUoFpU/AmIuTRG/2KiEkdm9gCC0dTvk8WITQ=
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 h1:LoYv5u+gUoFpU/AmIuTRG/2KiEkdm9gCC0dTvk8WITQ=
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898/go.mod h1:c1j6YQ+vCbeA8kJ59Im4UnMd1GxovlpPBDhGZoewfn8=
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898/go.mod h1:c1j6YQ+vCbeA8kJ59Im4UnMd1GxovlpPBDhGZoewfn8=
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992 h1:A6O89OlCJQUpNxGqC/E5By04UNKBryIt5olQIGOx8mg=
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992 h1:A6O89OlCJQUpNxGqC/E5By04UNKBryIt5olQIGOx8mg=
|
||||||
|
|||||||
@@ -172,13 +172,7 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
|
|||||||
}
|
}
|
||||||
myUser.Registration = reg
|
myUser.Registration = reg
|
||||||
|
|
||||||
domains := []string{option.Domain}
|
domains := strings.Split(option.Domain, ";")
|
||||||
|
|
||||||
// 如果是通配置符域名,把根域名也加入
|
|
||||||
if strings.HasPrefix(option.Domain, "*.") && len(strings.Split(option.Domain, ".")) == 3 {
|
|
||||||
rootDomain := strings.TrimPrefix(option.Domain, "*.")
|
|
||||||
domains = append(domains, rootDomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
request := certificate.ObtainRequest{
|
request := certificate.ObtainRequest{
|
||||||
Domains: domains,
|
Domains: domains,
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import (
|
|||||||
"certimate/internal/utils/rand"
|
"certimate/internal/utils/rand"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
||||||
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
||||||
ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
|
ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
|
||||||
|
cdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tencentCdn struct {
|
type tencentCdn struct {
|
||||||
@@ -89,14 +92,29 @@ func (t *tencentCdn) deploy(certId string) error {
|
|||||||
// 实例化要请求产品的client对象,clientProfile是可选的
|
// 实例化要请求产品的client对象,clientProfile是可选的
|
||||||
client, _ := ssl.NewClient(t.credential, "", cpf)
|
client, _ := ssl.NewClient(t.credential, "", cpf)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 实例化一个请求对象,每个接口都会对应一个request对象
|
// 实例化一个请求对象,每个接口都会对应一个request对象
|
||||||
request := ssl.NewDeployCertificateInstanceRequest()
|
request := ssl.NewDeployCertificateInstanceRequest()
|
||||||
|
|
||||||
request.CertificateId = common.StringPtr(certId)
|
request.CertificateId = common.StringPtr(certId)
|
||||||
request.InstanceIdList = common.StringPtrs([]string{t.option.Domain})
|
|
||||||
request.ResourceType = common.StringPtr("cdn")
|
request.ResourceType = common.StringPtr("cdn")
|
||||||
request.Status = common.Int64Ptr(1)
|
request.Status = common.Int64Ptr(1)
|
||||||
|
|
||||||
|
// 如果是泛域名就从cdn列表下获取SSL证书中的可用域名
|
||||||
|
if(strings.Contains(t.option.Domain, "*")){
|
||||||
|
list, errGetList := t.getDomainList()
|
||||||
|
if errGetList != nil {
|
||||||
|
return fmt.Errorf("failed to get certificate domain list: %w", errGetList)
|
||||||
|
}
|
||||||
|
if list == nil || len(list) == 0 {
|
||||||
|
return fmt.Errorf("failed to get certificate domain list: empty list.")
|
||||||
|
}
|
||||||
|
request.InstanceIdList = common.StringPtrs(list)
|
||||||
|
}else{ // 否则直接使用传入的域名
|
||||||
|
request.InstanceIdList = common.StringPtrs([]string{t.option.Domain})
|
||||||
|
}
|
||||||
|
|
||||||
// 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
|
// 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
|
||||||
resp, err := client.DeployCertificateInstance(request)
|
resp, err := client.DeployCertificateInstance(request)
|
||||||
|
|
||||||
@@ -106,3 +124,27 @@ func (t *tencentCdn) deploy(certId string) error {
|
|||||||
t.infos = append(t.infos, toStr("部署证书", resp.Response))
|
t.infos = append(t.infos, toStr("部署证书", resp.Response))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *tencentCdn) getDomainList() ([]string, error) {
|
||||||
|
cpf := profile.NewClientProfile()
|
||||||
|
cpf.HttpProfile.Endpoint = "cdn.tencentcloudapi.com"
|
||||||
|
client, _ := cdn.NewClient(t.credential, "", cpf)
|
||||||
|
|
||||||
|
request := cdn.NewDescribeCertDomainsRequest()
|
||||||
|
|
||||||
|
cert := base64.StdEncoding.EncodeToString([]byte(t.option.Certificate.Certificate))
|
||||||
|
request.Cert = &cert
|
||||||
|
|
||||||
|
|
||||||
|
response, err := client.DescribeCertDomains(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get domain list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
domains := make([]string, 0)
|
||||||
|
for _, domain := range response.Response.Domains {
|
||||||
|
domains = append(domains, *domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return domains, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,11 +31,12 @@ func Send(title, content string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n := notifyPackage.New()
|
||||||
// 添加推送渠道
|
// 添加推送渠道
|
||||||
notifyPackage.UseServices(notifiers...)
|
n.UseServices(notifiers...)
|
||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
return notifyPackage.Send(context.Background(), title, content)
|
return n.Send(context.Background(), title, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNotifiers() ([]notifyPackage.Notifier, error) {
|
func getNotifiers() ([]notifyPackage.Notifier, error) {
|
||||||
|
|||||||
322
ui/dist/assets/index--un-5Tw_.js
vendored
322
ui/dist/assets/index--un-5Tw_.js
vendored
File diff suppressed because one or more lines are too long
332
ui/dist/assets/index-DOR0Uh6g.js
vendored
Normal file
332
ui/dist/assets/index-DOR0Uh6g.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ui/dist/assets/index-DOft-CKV.css
vendored
Normal file
1
ui/dist/assets/index-DOft-CKV.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ui/dist/assets/index-I--T0qY3.css
vendored
1
ui/dist/assets/index-I--T0qY3.css
vendored
File diff suppressed because one or more lines are too long
4
ui/dist/index.html
vendored
4
ui/dist/index.html
vendored
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Certimate - Your Trusted SSL Automation Partner</title>
|
<title>Certimate - Your Trusted SSL Automation Partner</title>
|
||||||
<script type="module" crossorigin src="/assets/index--un-5Tw_.js"></script>
|
<script type="module" crossorigin src="/assets/index-DOR0Uh6g.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-I--T0qY3.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DOft-CKV.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-background">
|
<body class="bg-background">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ import { PbErrorData } from "@/domain/base";
|
|||||||
|
|
||||||
const AccessAliyunForm = ({
|
const AccessAliyunForm = ({
|
||||||
data,
|
data,
|
||||||
|
op,
|
||||||
onAfterReq,
|
onAfterReq,
|
||||||
}: {
|
}: {
|
||||||
data?: Access;
|
data?: Access;
|
||||||
|
op: "add" | "edit" | "copy";
|
||||||
onAfterReq: () => void;
|
onAfterReq: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { addAccess, updateAccess } = useConfig();
|
const { addAccess, updateAccess } = useConfig();
|
||||||
@@ -69,6 +71,7 @@ const AccessAliyunForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
req.id = op == "copy" ? "" : req.id;
|
||||||
const rs = await save(req);
|
const rs = await save(req);
|
||||||
|
|
||||||
onAfterReq();
|
onAfterReq();
|
||||||
@@ -76,10 +79,11 @@ const AccessAliyunForm = ({
|
|||||||
req.id = rs.id;
|
req.id = rs.id;
|
||||||
req.created = rs.created;
|
req.created = rs.created;
|
||||||
req.updated = rs.updated;
|
req.updated = rs.updated;
|
||||||
if (data.id) {
|
if (data.id && op == "edit") {
|
||||||
updateAccess(req);
|
updateAccess(req);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log(req);
|
||||||
addAccess(req);
|
addAccess(req);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const err = e as ClientResponseError;
|
const err = e as ClientResponseError;
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ import { PbErrorData } from "@/domain/base";
|
|||||||
|
|
||||||
const AccessCloudflareForm = ({
|
const AccessCloudflareForm = ({
|
||||||
data,
|
data,
|
||||||
|
op,
|
||||||
onAfterReq,
|
onAfterReq,
|
||||||
}: {
|
}: {
|
||||||
data?: Access;
|
data?: Access;
|
||||||
|
op: "add" | "edit" | "copy";
|
||||||
onAfterReq: () => void;
|
onAfterReq: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { addAccess, updateAccess } = useConfig();
|
const { addAccess, updateAccess } = useConfig();
|
||||||
@@ -65,6 +67,7 @@ const AccessCloudflareForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
req.id = op == "copy" ? "" : req.id;
|
||||||
const rs = await save(req);
|
const rs = await save(req);
|
||||||
|
|
||||||
onAfterReq();
|
onAfterReq();
|
||||||
@@ -72,7 +75,7 @@ const AccessCloudflareForm = ({
|
|||||||
req.id = rs.id;
|
req.id = rs.id;
|
||||||
req.created = rs.created;
|
req.created = rs.created;
|
||||||
req.updated = rs.updated;
|
req.updated = rs.updated;
|
||||||
if (data.id) {
|
if (data.id && op == "edit") {
|
||||||
updateAccess(req);
|
updateAccess(req);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import AccessGodaddyFrom from "./AccessGodaddyForm";
|
|||||||
import AccessLocalForm from "./AccessLocalForm";
|
import AccessLocalForm from "./AccessLocalForm";
|
||||||
|
|
||||||
type TargetConfigEditProps = {
|
type TargetConfigEditProps = {
|
||||||
op: "add" | "edit";
|
op: "add" | "edit" | "copy";
|
||||||
className?: string;
|
className?: string;
|
||||||
trigger: React.ReactNode;
|
trigger: React.ReactNode;
|
||||||
data?: Access;
|
data?: Access;
|
||||||
@@ -60,6 +60,7 @@ export function AccessEdit({
|
|||||||
form = (
|
form = (
|
||||||
<AccessTencentForm
|
<AccessTencentForm
|
||||||
data={data}
|
data={data}
|
||||||
|
op={op}
|
||||||
onAfterReq={() => {
|
onAfterReq={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
@@ -70,6 +71,7 @@ export function AccessEdit({
|
|||||||
form = (
|
form = (
|
||||||
<AccessAliyunForm
|
<AccessAliyunForm
|
||||||
data={data}
|
data={data}
|
||||||
|
op={op}
|
||||||
onAfterReq={() => {
|
onAfterReq={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
@@ -80,6 +82,7 @@ export function AccessEdit({
|
|||||||
form = (
|
form = (
|
||||||
<AccessSSHForm
|
<AccessSSHForm
|
||||||
data={data}
|
data={data}
|
||||||
|
op={op}
|
||||||
onAfterReq={() => {
|
onAfterReq={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
@@ -90,6 +93,7 @@ export function AccessEdit({
|
|||||||
form = (
|
form = (
|
||||||
<WebhookForm
|
<WebhookForm
|
||||||
data={data}
|
data={data}
|
||||||
|
op={op}
|
||||||
onAfterReq={() => {
|
onAfterReq={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
@@ -100,6 +104,7 @@ export function AccessEdit({
|
|||||||
form = (
|
form = (
|
||||||
<AccessCloudflareForm
|
<AccessCloudflareForm
|
||||||
data={data}
|
data={data}
|
||||||
|
op={op}
|
||||||
onAfterReq={() => {
|
onAfterReq={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
@@ -110,6 +115,7 @@ export function AccessEdit({
|
|||||||
form = (
|
form = (
|
||||||
<AccessQiniuForm
|
<AccessQiniuForm
|
||||||
data={data}
|
data={data}
|
||||||
|
op={op}
|
||||||
onAfterReq={() => {
|
onAfterReq={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
@@ -120,6 +126,7 @@ export function AccessEdit({
|
|||||||
form = (
|
form = (
|
||||||
<AccessNamesiloForm
|
<AccessNamesiloForm
|
||||||
data={data}
|
data={data}
|
||||||
|
op={op}
|
||||||
onAfterReq={() => {
|
onAfterReq={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
@@ -130,6 +137,7 @@ export function AccessEdit({
|
|||||||
form = (
|
form = (
|
||||||
<AccessGodaddyFrom
|
<AccessGodaddyFrom
|
||||||
data={data}
|
data={data}
|
||||||
|
op={op}
|
||||||
onAfterReq={() => {
|
onAfterReq={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
@@ -140,6 +148,7 @@ export function AccessEdit({
|
|||||||
form = (
|
form = (
|
||||||
<AccessLocalForm
|
<AccessLocalForm
|
||||||
data={data}
|
data={data}
|
||||||
|
op={op}
|
||||||
onAfterReq={() => {
|
onAfterReq={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
@@ -159,7 +168,7 @@ export function AccessEdit({
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-[600px] w-full dark:text-stone-200">
|
<DialogContent className="sm:max-w-[600px] w-full dark:text-stone-200">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{op == "add" ? t('access.add') : t('access.edit')}</DialogTitle>
|
<DialogTitle>{op == "add" ? t('access.add') : op == "edit" ? t('access.edit') : t('access.copy')}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<ScrollArea className="max-h-[80vh]">
|
<ScrollArea className="max-h-[80vh]">
|
||||||
<div className="container py-3">
|
<div className="container py-3">
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ import { PbErrorData } from "@/domain/base";
|
|||||||
|
|
||||||
const AccessGodaddyFrom = ({
|
const AccessGodaddyFrom = ({
|
||||||
data,
|
data,
|
||||||
|
op,
|
||||||
onAfterReq,
|
onAfterReq,
|
||||||
}: {
|
}: {
|
||||||
data?: Access;
|
data?: Access;
|
||||||
|
op: "add" | "edit" | "copy";
|
||||||
onAfterReq: () => void;
|
onAfterReq: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { addAccess, updateAccess } = useConfig();
|
const { addAccess, updateAccess } = useConfig();
|
||||||
@@ -74,6 +76,7 @@ const AccessGodaddyFrom = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
req.id = op == "copy" ? "" : req.id;
|
||||||
const rs = await save(req);
|
const rs = await save(req);
|
||||||
|
|
||||||
onAfterReq();
|
onAfterReq();
|
||||||
@@ -81,7 +84,7 @@ const AccessGodaddyFrom = ({
|
|||||||
req.id = rs.id;
|
req.id = rs.id;
|
||||||
req.created = rs.created;
|
req.created = rs.created;
|
||||||
req.updated = rs.updated;
|
req.updated = rs.updated;
|
||||||
if (data.id) {
|
if (data.id && op == "edit") {
|
||||||
updateAccess(req);
|
updateAccess(req);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ import { PbErrorData } from "@/domain/base";
|
|||||||
|
|
||||||
const AccessLocalForm = ({
|
const AccessLocalForm = ({
|
||||||
data,
|
data,
|
||||||
|
op,
|
||||||
onAfterReq,
|
onAfterReq,
|
||||||
}: {
|
}: {
|
||||||
data?: Access;
|
data?: Access;
|
||||||
|
op: "add" | "edit" | "copy";
|
||||||
onAfterReq: () => void;
|
onAfterReq: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { addAccess, updateAccess, reloadAccessGroups } = useConfig();
|
const { addAccess, updateAccess, reloadAccessGroups } = useConfig();
|
||||||
@@ -79,6 +81,7 @@ const AccessLocalForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
req.id = op == "copy" ? "" : req.id;
|
||||||
const rs = await save(req);
|
const rs = await save(req);
|
||||||
|
|
||||||
onAfterReq();
|
onAfterReq();
|
||||||
@@ -86,7 +89,7 @@ const AccessLocalForm = ({
|
|||||||
req.id = rs.id;
|
req.id = rs.id;
|
||||||
req.created = rs.created;
|
req.created = rs.created;
|
||||||
req.updated = rs.updated;
|
req.updated = rs.updated;
|
||||||
if (data.id) {
|
if (data.id && op == "edit") {
|
||||||
updateAccess(req);
|
updateAccess(req);
|
||||||
} else {
|
} else {
|
||||||
addAccess(req);
|
addAccess(req);
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ import { PbErrorData } from "@/domain/base";
|
|||||||
|
|
||||||
const AccessNamesiloForm = ({
|
const AccessNamesiloForm = ({
|
||||||
data,
|
data,
|
||||||
|
op,
|
||||||
onAfterReq,
|
onAfterReq,
|
||||||
}: {
|
}: {
|
||||||
data?: Access;
|
data?: Access;
|
||||||
|
op: "add" | "edit" | "copy";
|
||||||
onAfterReq: () => void;
|
onAfterReq: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { addAccess, updateAccess } = useConfig();
|
const { addAccess, updateAccess } = useConfig();
|
||||||
@@ -64,6 +66,7 @@ const AccessNamesiloForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
req.id = op == "copy" ? "" : req.id;
|
||||||
const rs = await save(req);
|
const rs = await save(req);
|
||||||
|
|
||||||
onAfterReq();
|
onAfterReq();
|
||||||
@@ -71,7 +74,7 @@ const AccessNamesiloForm = ({
|
|||||||
req.id = rs.id;
|
req.id = rs.id;
|
||||||
req.created = rs.created;
|
req.created = rs.created;
|
||||||
req.updated = rs.updated;
|
req.updated = rs.updated;
|
||||||
if (data.id) {
|
if (data.id && op == "edit") {
|
||||||
updateAccess(req);
|
updateAccess(req);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ import { PbErrorData } from "@/domain/base";
|
|||||||
|
|
||||||
const AccessQiniuForm = ({
|
const AccessQiniuForm = ({
|
||||||
data,
|
data,
|
||||||
|
op,
|
||||||
onAfterReq,
|
onAfterReq,
|
||||||
}: {
|
}: {
|
||||||
data?: Access;
|
data?: Access;
|
||||||
|
op: "add" | "edit" | "copy";
|
||||||
onAfterReq: () => void;
|
onAfterReq: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { addAccess, updateAccess } = useConfig();
|
const { addAccess, updateAccess } = useConfig();
|
||||||
@@ -69,6 +71,7 @@ const AccessQiniuForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
req.id = op == "copy" ? "" : req.id;
|
||||||
const rs = await save(req);
|
const rs = await save(req);
|
||||||
|
|
||||||
onAfterReq();
|
onAfterReq();
|
||||||
@@ -76,7 +79,7 @@ const AccessQiniuForm = ({
|
|||||||
req.id = rs.id;
|
req.id = rs.id;
|
||||||
req.created = rs.created;
|
req.created = rs.created;
|
||||||
req.updated = rs.updated;
|
req.updated = rs.updated;
|
||||||
if (data.id) {
|
if (data.id && op == "edit") {
|
||||||
updateAccess(req);
|
updateAccess(req);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,9 +39,11 @@ import { updateById } from "@/repository/access_group";
|
|||||||
|
|
||||||
const AccessSSHForm = ({
|
const AccessSSHForm = ({
|
||||||
data,
|
data,
|
||||||
|
op,
|
||||||
onAfterReq,
|
onAfterReq,
|
||||||
}: {
|
}: {
|
||||||
data?: Access;
|
data?: Access;
|
||||||
|
op: "add" | "edit" | "copy";
|
||||||
onAfterReq: () => void;
|
onAfterReq: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@@ -145,6 +147,7 @@ const AccessSSHForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
req.id = op == "copy" ? "" : req.id;
|
||||||
const rs = await save(req);
|
const rs = await save(req);
|
||||||
|
|
||||||
onAfterReq();
|
onAfterReq();
|
||||||
@@ -152,7 +155,7 @@ const AccessSSHForm = ({
|
|||||||
req.id = rs.id;
|
req.id = rs.id;
|
||||||
req.created = rs.created;
|
req.created = rs.created;
|
||||||
req.updated = rs.updated;
|
req.updated = rs.updated;
|
||||||
if (data.id) {
|
if (data.id && op == "edit") {
|
||||||
updateAccess(req);
|
updateAccess(req);
|
||||||
} else {
|
} else {
|
||||||
addAccess(req);
|
addAccess(req);
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ import { PbErrorData } from "@/domain/base";
|
|||||||
|
|
||||||
const AccessTencentForm = ({
|
const AccessTencentForm = ({
|
||||||
data,
|
data,
|
||||||
|
op,
|
||||||
onAfterReq,
|
onAfterReq,
|
||||||
}: {
|
}: {
|
||||||
data?: Access;
|
data?: Access;
|
||||||
|
op: "add" | "edit" | "copy";
|
||||||
onAfterReq: () => void;
|
onAfterReq: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { addAccess, updateAccess } = useConfig();
|
const { addAccess, updateAccess } = useConfig();
|
||||||
@@ -68,6 +70,7 @@ const AccessTencentForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
req.id = op == "copy" ? "" : req.id;
|
||||||
const rs = await save(req);
|
const rs = await save(req);
|
||||||
|
|
||||||
onAfterReq();
|
onAfterReq();
|
||||||
@@ -75,7 +78,7 @@ const AccessTencentForm = ({
|
|||||||
req.id = rs.id;
|
req.id = rs.id;
|
||||||
req.created = rs.created;
|
req.created = rs.created;
|
||||||
req.updated = rs.updated;
|
req.updated = rs.updated;
|
||||||
if (data.id) {
|
if (data.id && op == "edit") {
|
||||||
updateAccess(req);
|
updateAccess(req);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ import { PbErrorData } from "@/domain/base";
|
|||||||
|
|
||||||
const WebhookForm = ({
|
const WebhookForm = ({
|
||||||
data,
|
data,
|
||||||
|
op,
|
||||||
onAfterReq,
|
onAfterReq,
|
||||||
}: {
|
}: {
|
||||||
data?: Access;
|
data?: Access;
|
||||||
|
op: "add" | "edit" | "copy";
|
||||||
onAfterReq: () => void;
|
onAfterReq: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { addAccess, updateAccess } = useConfig();
|
const { addAccess, updateAccess } = useConfig();
|
||||||
@@ -64,6 +66,7 @@ const WebhookForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
req.id = op == "copy" ? "" : req.id;
|
||||||
const rs = await save(req);
|
const rs = await save(req);
|
||||||
|
|
||||||
onAfterReq();
|
onAfterReq();
|
||||||
@@ -71,7 +74,7 @@ const WebhookForm = ({
|
|||||||
req.id = rs.id;
|
req.id = rs.id;
|
||||||
req.created = rs.created;
|
req.created = rs.created;
|
||||||
req.updated = rs.updated;
|
req.updated = rs.updated;
|
||||||
if (data.id) {
|
if (data.id && op == "edit") {
|
||||||
updateAccess(req);
|
updateAccess(req);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
245
ui/src/components/certimate/StringList.tsx
Normal file
245
ui/src/components/certimate/StringList.tsx
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import Show from "../Show";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { FormControl, FormLabel } from "../ui/form";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "../ui/dialog";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Edit, Plus, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
type StringListProps = {
|
||||||
|
className?: string;
|
||||||
|
value: string;
|
||||||
|
valueType?: "domain" | "ip";
|
||||||
|
onValueChange: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const titles: Record<string, string> = {
|
||||||
|
domain: "domain",
|
||||||
|
ip: "IP",
|
||||||
|
};
|
||||||
|
|
||||||
|
const StringList = ({
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
onValueChange,
|
||||||
|
valueType = "domain",
|
||||||
|
}: StringListProps) => {
|
||||||
|
const [list, setList] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useMemo(() => {
|
||||||
|
if (value) {
|
||||||
|
setList(value.split(";"));
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const changeList = () => {
|
||||||
|
onValueChange(list.join(";"));
|
||||||
|
};
|
||||||
|
changeList();
|
||||||
|
}, [list]);
|
||||||
|
|
||||||
|
const addVal = (val: string) => {
|
||||||
|
if (list.includes(val)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setList([...list, val]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editVal = (index: number, val: string) => {
|
||||||
|
const newList = [...list];
|
||||||
|
newList[index] = val;
|
||||||
|
setList(newList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveClick = (index: number) => {
|
||||||
|
const newList = [...list];
|
||||||
|
newList.splice(index, 1);
|
||||||
|
setList(newList);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={cn(className)}>
|
||||||
|
<FormLabel className="flex justify-between items-center">
|
||||||
|
<div>{t(titles[valueType])}</div>
|
||||||
|
|
||||||
|
<Show when={list.length > 0}>
|
||||||
|
<StringEdit
|
||||||
|
op="add"
|
||||||
|
onValueChange={(val: string) => {
|
||||||
|
addVal(val);
|
||||||
|
}}
|
||||||
|
valueType={valueType}
|
||||||
|
value={""}
|
||||||
|
trigger={
|
||||||
|
<div className="flex items-center text-primary">
|
||||||
|
<Plus size={16} className="cursor-pointer " />
|
||||||
|
|
||||||
|
<div className="text-sm ">{t("add")}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Show
|
||||||
|
when={list.length > 0}
|
||||||
|
fallback={
|
||||||
|
<div className="border rounded-md p-3 text-sm mt-2 flex flex-col items-center">
|
||||||
|
<div className="text-muted-foreground">暂未添加域名</div>
|
||||||
|
|
||||||
|
<StringEdit
|
||||||
|
value={""}
|
||||||
|
trigger={t("add")}
|
||||||
|
onValueChange={addVal}
|
||||||
|
valueType={valueType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="border rounded-md p-3 text-sm mt-2 text-gray-700 space-y-2 dark:text-white dark:border-stone-700 dark:bg-stone-950">
|
||||||
|
{list.map((item, index) => (
|
||||||
|
<div key={index} className="flex justify-between items-center">
|
||||||
|
<div>{item}</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<StringEdit
|
||||||
|
op="edit"
|
||||||
|
valueType={valueType}
|
||||||
|
trigger={
|
||||||
|
<Edit
|
||||||
|
size={16}
|
||||||
|
className="cursor-pointer text-gray-600 dark:text-white"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={item}
|
||||||
|
onValueChange={(val: string) => {
|
||||||
|
editVal(index, val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Trash2
|
||||||
|
size={16}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
onRemoveClick(index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StringList;
|
||||||
|
|
||||||
|
type ValueType = "domain" | "ip";
|
||||||
|
|
||||||
|
type StringEditProps = {
|
||||||
|
value: string;
|
||||||
|
trigger: React.ReactNode;
|
||||||
|
onValueChange: (value: string) => void;
|
||||||
|
valueType: ValueType;
|
||||||
|
op?: "add" | "edit";
|
||||||
|
};
|
||||||
|
|
||||||
|
const StringEdit = ({
|
||||||
|
trigger,
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
op = "add",
|
||||||
|
valueType,
|
||||||
|
}: StringEditProps) => {
|
||||||
|
const [currentValue, setCurrentValue] = useState<string>("");
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const domainSchema = z
|
||||||
|
.string()
|
||||||
|
.regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
|
||||||
|
message: t("domain.not.empty.verify.message"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ipSchema = z.string().ip({ message: t("ip.not.empty.verify.message") });
|
||||||
|
|
||||||
|
const schedules: Record<ValueType, z.ZodString> = {
|
||||||
|
domain: domainSchema,
|
||||||
|
ip: ipSchema,
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSaveClick = useCallback(() => {
|
||||||
|
const schema = schedules[valueType];
|
||||||
|
|
||||||
|
const resp = schema.safeParse(currentValue);
|
||||||
|
if (!resp.success) {
|
||||||
|
setError(JSON.parse(resp.error.message)[0].message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentValue("");
|
||||||
|
setOpen(false);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
onValueChange(currentValue);
|
||||||
|
}, [currentValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setOpen(open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTrigger className="text-primary">{trigger}</DialogTrigger>
|
||||||
|
<DialogContent className="dark:text-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="dark:text-white">
|
||||||
|
{t(titles[valueType])}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<Input
|
||||||
|
value={currentValue}
|
||||||
|
className="dark:text-white"
|
||||||
|
onChange={(e) => {
|
||||||
|
setCurrentValue(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Show when={error.length > 0}>
|
||||||
|
<div className="text-red-500 text-sm">{error}</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
onSaveClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{op === "add" ? t("add") : t("confirm")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -26,6 +26,22 @@ export type Domain = {
|
|||||||
expand?: {
|
expand?: {
|
||||||
lastDeployment?: Deployment;
|
lastDeployment?: Deployment;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
applyConfig?: ApplyConfig;
|
||||||
|
deployConfig?: DeployConfig[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeployConfig = {
|
||||||
|
access: string;
|
||||||
|
type: string;
|
||||||
|
config?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApplyConfig = {
|
||||||
|
access: string;
|
||||||
|
email: string;
|
||||||
|
timeout?: number;
|
||||||
|
nameservers?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Statistic = {
|
export type Statistic = {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const version = "Certimate v0.1.14";
|
export const version = "Certimate v0.1.18";
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
|
"copy": "Copy",
|
||||||
"succeed": "Successful",
|
"succeed": "Successful",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"document": "Document",
|
"document": "Document",
|
||||||
@@ -82,6 +83,7 @@
|
|||||||
"pagination.prev": "Previous",
|
"pagination.prev": "Previous",
|
||||||
"domain": "Domain",
|
"domain": "Domain",
|
||||||
"domain.add": "Add Domain",
|
"domain.add": "Add Domain",
|
||||||
|
"domain.edit":"Edit Domain",
|
||||||
"domain.delete": "Delete Domain",
|
"domain.delete": "Delete Domain",
|
||||||
"domain.not.empty.verify.message": "Please enter domain",
|
"domain.not.empty.verify.message": "Please enter domain",
|
||||||
"domain.management.name": "Domain List",
|
"domain.management.name": "Domain List",
|
||||||
@@ -154,6 +156,8 @@
|
|||||||
"access.management": "Authorization Management",
|
"access.management": "Authorization Management",
|
||||||
"access.add": "Add Authorization",
|
"access.add": "Add Authorization",
|
||||||
"access.edit": "Edit Authorization",
|
"access.edit": "Edit Authorization",
|
||||||
|
"access.copy": "Copy Authorization",
|
||||||
|
"access.delete.confirm": "Are you sure you want to delete the deployment authorization?",
|
||||||
"access.all": "All Authorizations",
|
"access.all": "All Authorizations",
|
||||||
"access.list": "Authorization List",
|
"access.list": "Authorization List",
|
||||||
"access.type": "Provider",
|
"access.type": "Provider",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"confirm": "确认",
|
"confirm": "确认",
|
||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
|
"copy": "复制",
|
||||||
"succeed": "成功",
|
"succeed": "成功",
|
||||||
"add": "新增",
|
"add": "新增",
|
||||||
"document": "文档",
|
"document": "文档",
|
||||||
@@ -82,6 +83,7 @@
|
|||||||
"pagination.prev": "上一页",
|
"pagination.prev": "上一页",
|
||||||
"domain": "域名",
|
"domain": "域名",
|
||||||
"domain.add": "新增域名",
|
"domain.add": "新增域名",
|
||||||
|
"domain.edit": "编辑域名",
|
||||||
"domain.delete": "删除域名",
|
"domain.delete": "删除域名",
|
||||||
"domain.not.empty.verify.message": "请输入域名",
|
"domain.not.empty.verify.message": "请输入域名",
|
||||||
"domain.management.name": "域名列表",
|
"domain.management.name": "域名列表",
|
||||||
@@ -154,6 +156,8 @@
|
|||||||
"access.management": "授权管理",
|
"access.management": "授权管理",
|
||||||
"access.add": "添加授权",
|
"access.add": "添加授权",
|
||||||
"access.edit": "编辑授权",
|
"access.edit": "编辑授权",
|
||||||
|
"access.copy": "复制授权",
|
||||||
|
"access.delete.confirm": "确定要删除授权吗?",
|
||||||
"access.all": "所有授权",
|
"access.all": "所有授权",
|
||||||
"access.list": "授权列表",
|
"access.list": "授权列表",
|
||||||
"access.type": "服务商",
|
"access.type": "服务商",
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ import { remove } from "@/repository/access";
|
|||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { Key } from "lucide-react";
|
import { Key } from "lucide-react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent, AlertDialogDescription, AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger
|
||||||
|
} from "@/components/ui/alert-dialog.tsx";
|
||||||
|
|
||||||
const Access = () => {
|
const Access = () => {
|
||||||
const { config, deleteAccess } = useConfig();
|
const { config, deleteAccess } = useConfig();
|
||||||
@@ -149,15 +158,45 @@ const Access = () => {
|
|||||||
data={access}
|
data={access}
|
||||||
/>
|
/>
|
||||||
<Separator orientation="vertical" className="h-4 mx-2" />
|
<Separator orientation="vertical" className="h-4 mx-2" />
|
||||||
<Button
|
<AccessEdit
|
||||||
variant={"link"}
|
trigger={
|
||||||
className="p-0"
|
<Button variant={"link"} className="p-0">
|
||||||
|
{t("copy")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
op="copy"
|
||||||
|
data={access}
|
||||||
|
/>
|
||||||
|
<Separator orientation="vertical" className="h-4 mx-2" />
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant={"link"} size={"sm"}>
|
||||||
|
{t('delete')}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="dark:text-gray-200">
|
||||||
|
{t('access.group.delete')}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{t('access.delete.confirm')}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel className="dark:text-gray-200">
|
||||||
|
{t('cancel')}
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDelete(access);
|
handleDelete(access);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("delete")}
|
{t('confirm')}
|
||||||
</Button>
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const Dashboard = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="text-muted-foreground">{t('dashboard')}</div>
|
<div className="text-muted-foreground">{t("dashboard")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex mt-10 gap-5 flex-col flex-wrap md:flex-row">
|
<div className="flex mt-10 gap-5 flex-col flex-wrap md:flex-row">
|
||||||
<div className="w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg border">
|
<div className="w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg border">
|
||||||
@@ -66,7 +66,7 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground font-semibold">
|
<div className="text-muted-foreground font-semibold">
|
||||||
{t('dashboard.all')}
|
{t("dashboard.all")}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline">
|
<div className="flex items-baseline">
|
||||||
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
||||||
@@ -91,7 +91,7 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground font-semibold">
|
<div className="text-muted-foreground font-semibold">
|
||||||
{t('dashboard.near.expired')}
|
{t("dashboard.near.expired")}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline">
|
<div className="flex items-baseline">
|
||||||
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
||||||
@@ -120,7 +120,7 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground font-semibold">
|
<div className="text-muted-foreground font-semibold">
|
||||||
{t('dashboard.enabled')}
|
{t("dashboard.enabled")}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline">
|
<div className="flex items-baseline">
|
||||||
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
||||||
@@ -144,7 +144,9 @@ const Dashboard = () => {
|
|||||||
<Ban size={48} strokeWidth={1} className="text-gray-400" />
|
<Ban size={48} strokeWidth={1} className="text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground font-semibold">{t('dashboard.not.enabled')}</div>
|
<div className="text-muted-foreground font-semibold">
|
||||||
|
{t("dashboard.not.enabled")}
|
||||||
|
</div>
|
||||||
<div className="flex items-baseline">
|
<div className="flex items-baseline">
|
||||||
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
||||||
{statistic?.disabled ? (
|
{statistic?.disabled ? (
|
||||||
@@ -168,22 +170,19 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground mt-5 text-sm">
|
<div className="text-muted-foreground mt-5 text-sm">
|
||||||
{t('deployment.log.name')}
|
{t("deployment.log.name")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{deployments?.length == 0 ? (
|
{deployments?.length == 0 ? (
|
||||||
<>
|
<>
|
||||||
<Alert className="max-w-[40em] mt-10">
|
<Alert className="max-w-[40em] mt-10">
|
||||||
<AlertTitle>{t('no.data')}</AlertTitle>
|
<AlertTitle>{t("no.data")}</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<div className="flex items-center mt-5">
|
<div className="flex items-center mt-5">
|
||||||
<div>
|
<div>
|
||||||
<Smile className="text-yellow-400" size={36} />
|
<Smile className="text-yellow-400" size={36} />
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-2">
|
<div className="ml-2"> {t("deployment.log.empty")}</div>
|
||||||
{" "}
|
|
||||||
{t('deployment.log.empty')}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex justify-end">
|
<div className="mt-2 flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
@@ -191,7 +190,7 @@ const Dashboard = () => {
|
|||||||
navigate("/edit");
|
navigate("/edit");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('domain.add')}
|
{t("domain.add")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
@@ -200,16 +199,18 @@ const Dashboard = () => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
|
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
|
||||||
<div className="w-48">{t('domain')}</div>
|
<div className="w-48">{t("domain")}</div>
|
||||||
|
|
||||||
<div className="w-24">{t('deployment.log.status')}</div>
|
<div className="w-24">{t("deployment.log.status")}</div>
|
||||||
<div className="w-56">{t('deployment.log.stage')}</div>
|
<div className="w-56">{t("deployment.log.stage")}</div>
|
||||||
<div className="w-56 sm:ml-2 text-center">{t('deployment.log.last.execution.time')}</div>
|
<div className="w-56 sm:ml-2 text-center">
|
||||||
|
{t("deployment.log.last.execution.time")}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grow">{t('operation')}</div>
|
<div className="grow">{t("operation")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:hidden flex text-sm text-muted-foreground">
|
<div className="sm:hidden flex text-sm text-muted-foreground">
|
||||||
{t('deployment.log.name')}
|
{t("deployment.log.name")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{deployments?.map((deployment) => (
|
{deployments?.map((deployment) => (
|
||||||
@@ -218,7 +219,14 @@ const Dashboard = () => {
|
|||||||
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
|
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
|
||||||
>
|
>
|
||||||
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center">
|
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center">
|
||||||
{deployment.expand.domain?.domain}
|
{deployment.expand.domain?.domain
|
||||||
|
.split(";")
|
||||||
|
.map((domain: string) => (
|
||||||
|
<>
|
||||||
|
{domain}
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
|
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
|
||||||
<DeployState deployment={deployment} />
|
<DeployState deployment={deployment} />
|
||||||
@@ -236,14 +244,14 @@ const Dashboard = () => {
|
|||||||
<Sheet>
|
<Sheet>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button variant={"link"} className="p-0">
|
<Button variant={"link"} className="p-0">
|
||||||
{t('deployment.log.detail.button.text')}
|
{t("deployment.log.detail.button.text")}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent className="sm:max-w-5xl">
|
<SheetContent className="sm:max-w-5xl">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>
|
<SheetTitle>
|
||||||
{deployment.expand.domain?.domain}-{deployment.id}
|
{deployment.expand.domain?.domain}-{deployment.id}
|
||||||
{t('deployment.log.detail')}
|
{t("deployment.log.detail")}
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">
|
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -39,6 +38,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { EmailsSetting } from "@/domain/settings";
|
import { EmailsSetting } from "@/domain/settings";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import StringList from "@/components/certimate/StringList";
|
||||||
|
|
||||||
const Edit = () => {
|
const Edit = () => {
|
||||||
const {
|
const {
|
||||||
@@ -70,16 +70,16 @@ const Edit = () => {
|
|||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
|
domain: z.string().min(1, {
|
||||||
message: 'domain.not.empty.verify.message',
|
message: "domain.not.empty.verify.message",
|
||||||
}),
|
}),
|
||||||
email: z.string().email('email.valid.message').optional(),
|
email: z.string().email("email.valid.message").optional(),
|
||||||
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
|
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
|
||||||
message: 'domain.management.edit.dns.access.not.empty.message',
|
message: "domain.management.edit.dns.access.not.empty.message",
|
||||||
}),
|
}),
|
||||||
targetAccess: z.string().optional(),
|
targetAccess: z.string().optional(),
|
||||||
targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, {
|
targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, {
|
||||||
message: 'domain.management.edit.target.type.not.empty.message',
|
message: "domain.management.edit.target.type.not.empty.message",
|
||||||
}),
|
}),
|
||||||
variables: z.string().optional(),
|
variables: z.string().optional(),
|
||||||
group: z.string().optional(),
|
group: z.string().optional(),
|
||||||
@@ -140,11 +140,11 @@ const Edit = () => {
|
|||||||
if (group == "" && targetAccess == "") {
|
if (group == "" && targetAccess == "") {
|
||||||
form.setError("group", {
|
form.setError("group", {
|
||||||
type: "manual",
|
type: "manual",
|
||||||
message: 'domain.management.edit.target.access.verify.msg',
|
message: "domain.management.edit.target.access.verify.msg",
|
||||||
});
|
});
|
||||||
form.setError("targetAccess", {
|
form.setError("targetAccess", {
|
||||||
type: "manual",
|
type: "manual",
|
||||||
message: 'domain.management.edit.target.access.verify.msg',
|
message: "domain.management.edit.target.access.verify.msg",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -164,13 +164,13 @@ const Edit = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await save(req);
|
await save(req);
|
||||||
let description = t('domain.management.edit.succeed.tips');
|
let description = t("domain.management.edit.succeed.tips");
|
||||||
if (req.id == "") {
|
if (req.id == "") {
|
||||||
description = t('domain.management.add.succeed.tips');
|
description = t("domain.management.add.succeed.tips");
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t('succeed'),
|
title: t("succeed"),
|
||||||
description,
|
description,
|
||||||
});
|
});
|
||||||
navigate("/domains");
|
navigate("/domains");
|
||||||
@@ -195,7 +195,7 @@ const Edit = () => {
|
|||||||
<div className="">
|
<div className="">
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<div className=" h-5 text-muted-foreground">
|
<div className=" h-5 text-muted-foreground">
|
||||||
{domain?.id ? t('domain.edit') : t('domain.add')}
|
{domain?.id ? t("domain.edit") : t("domain.add")}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex w-full justify-center md:space-x-10 flex-col md:flex-row">
|
<div className="mt-5 flex w-full justify-center md:space-x-10 flex-col md:flex-row">
|
||||||
<div className="w-full md:w-[200px] text-muted-foreground space-x-3 md:space-y-3 flex-row md:flex-col flex">
|
<div className="w-full md:w-[200px] text-muted-foreground space-x-3 md:space-y-3 flex-row md:flex-col flex">
|
||||||
@@ -208,7 +208,7 @@ const Edit = () => {
|
|||||||
setTab("base");
|
setTab("base");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('basic.setting')}
|
{t("basic.setting")}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -219,7 +219,7 @@ const Edit = () => {
|
|||||||
setTab("advance");
|
setTab("advance");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('advanced.setting')}
|
{t("advanced.setting")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -234,10 +234,15 @@ const Edit = () => {
|
|||||||
name="domain"
|
name="domain"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem hidden={tab != "base"}>
|
<FormItem hidden={tab != "base"}>
|
||||||
<FormLabel>{t('domain')}</FormLabel>
|
<>
|
||||||
<FormControl>
|
<StringList
|
||||||
<Input placeholder={t('domain.not.empty.verify.message')} {...field} />
|
value={field.value}
|
||||||
</FormControl>
|
valueType="domain"
|
||||||
|
onValueChange={(domain: string) => {
|
||||||
|
form.setValue("domain", domain);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -249,12 +254,15 @@ const Edit = () => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem hidden={tab != "base"}>
|
<FormItem hidden={tab != "base"}>
|
||||||
<FormLabel className="flex w-full justify-between">
|
<FormLabel className="flex w-full justify-between">
|
||||||
<div>{t('email') + t('domain.management.edit.email.description')}</div>
|
<div>
|
||||||
|
{t("email") +
|
||||||
|
t("domain.management.edit.email.description")}
|
||||||
|
</div>
|
||||||
<EmailsEdit
|
<EmailsEdit
|
||||||
trigger={
|
trigger={
|
||||||
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
||||||
<Plus size={14} />
|
<Plus size={14} />
|
||||||
{t('add')}
|
{t("add")}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -268,11 +276,15 @@ const Edit = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t('domain.management.edit.email.not.empty.message')} />
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"domain.management.edit.email.not.empty.message"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>{t('email.list')}</SelectLabel>
|
<SelectLabel>{t("email.list")}</SelectLabel>
|
||||||
{(emails.content as EmailsSetting).emails.map(
|
{(emails.content as EmailsSetting).emails.map(
|
||||||
(item) => (
|
(item) => (
|
||||||
<SelectItem key={item} value={item}>
|
<SelectItem key={item} value={item}>
|
||||||
@@ -295,12 +307,14 @@ const Edit = () => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem hidden={tab != "base"}>
|
<FormItem hidden={tab != "base"}>
|
||||||
<FormLabel className="flex w-full justify-between">
|
<FormLabel className="flex w-full justify-between">
|
||||||
<div>{t('domain.management.edit.dns.access.label')}</div>
|
<div>
|
||||||
|
{t("domain.management.edit.dns.access.label")}
|
||||||
|
</div>
|
||||||
<AccessEdit
|
<AccessEdit
|
||||||
trigger={
|
trigger={
|
||||||
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
||||||
<Plus size={14} />
|
<Plus size={14} />
|
||||||
{t('add')}
|
{t("add")}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
op="add"
|
op="add"
|
||||||
@@ -315,11 +329,17 @@ const Edit = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t('domain.management.edit.access.not.empty.message')} />
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"domain.management.edit.access.not.empty.message"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>{t('domain.management.edit.access.label')}</SelectLabel>
|
<SelectLabel>
|
||||||
|
{t("domain.management.edit.access.label")}
|
||||||
|
</SelectLabel>
|
||||||
{accesses
|
{accesses
|
||||||
.filter((item) => item.usage != "deploy")
|
.filter((item) => item.usage != "deploy")
|
||||||
.map((item) => (
|
.map((item) => (
|
||||||
@@ -351,7 +371,9 @@ const Edit = () => {
|
|||||||
name="targetType"
|
name="targetType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem hidden={tab != "base"}>
|
<FormItem hidden={tab != "base"}>
|
||||||
<FormLabel>{t('domain.management.edit.target.type')}</FormLabel>
|
<FormLabel>
|
||||||
|
{t("domain.management.edit.target.type")}
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
{...field}
|
{...field}
|
||||||
@@ -361,11 +383,17 @@ const Edit = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t('domain.management.edit.target.type.not.empty.message')} />
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"domain.management.edit.target.type.not.empty.message"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>{t('domain.management.edit.target.type')}</SelectLabel>
|
<SelectLabel>
|
||||||
|
{t("domain.management.edit.target.type")}
|
||||||
|
</SelectLabel>
|
||||||
{targetTypeKeys.map((key) => (
|
{targetTypeKeys.map((key) => (
|
||||||
<SelectItem key={key} value={key}>
|
<SelectItem key={key} value={key}>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@@ -373,7 +401,9 @@ const Edit = () => {
|
|||||||
className="w-6"
|
className="w-6"
|
||||||
src={targetTypeMap.get(key)?.[1]}
|
src={targetTypeMap.get(key)?.[1]}
|
||||||
/>
|
/>
|
||||||
<div>{t(targetTypeMap.get(key)?.[0] || '')}</div>
|
<div>
|
||||||
|
{t(targetTypeMap.get(key)?.[0] || "")}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -392,12 +422,12 @@ const Edit = () => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem hidden={tab != "base"}>
|
<FormItem hidden={tab != "base"}>
|
||||||
<FormLabel className="w-full flex justify-between">
|
<FormLabel className="w-full flex justify-between">
|
||||||
<div>{t('domain.management.edit.target.access')}</div>
|
<div>{t("domain.management.edit.target.access")}</div>
|
||||||
<AccessEdit
|
<AccessEdit
|
||||||
trigger={
|
trigger={
|
||||||
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
||||||
<Plus size={14} />
|
<Plus size={14} />
|
||||||
{t('add')}
|
{t("add")}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
op="add"
|
op="add"
|
||||||
@@ -411,12 +441,19 @@ const Edit = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t('domain.management.edit.target.access.not.empty.message')} />
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"domain.management.edit.target.access.not.empty.message"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>
|
<SelectLabel>
|
||||||
{t('domain.management.edit.target.access.content.label')} {form.getValues().targetAccess}
|
{t(
|
||||||
|
"domain.management.edit.target.access.content.label"
|
||||||
|
)}{" "}
|
||||||
|
{form.getValues().targetAccess}
|
||||||
</SelectLabel>
|
</SelectLabel>
|
||||||
<SelectItem value="emptyId">
|
<SelectItem value="emptyId">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@@ -452,9 +489,7 @@ const Edit = () => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem hidden={tab != "advance" || targetType != "ssh"}>
|
<FormItem hidden={tab != "advance" || targetType != "ssh"}>
|
||||||
<FormLabel className="w-full flex justify-between">
|
<FormLabel className="w-full flex justify-between">
|
||||||
<div>
|
<div>{t("domain.management.edit.group.label")}</div>
|
||||||
{t('domain.management.edit.group.label')}
|
|
||||||
</div>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
@@ -466,7 +501,11 @@ const Edit = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t('domain.management.edit.group.not.empty.message')} />
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"domain.management.edit.group.not.empty.message"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="emptyId">
|
<SelectItem value="emptyId">
|
||||||
@@ -511,10 +550,12 @@ const Edit = () => {
|
|||||||
name="variables"
|
name="variables"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem hidden={tab != "advance"}>
|
<FormItem hidden={tab != "advance"}>
|
||||||
<FormLabel>{t('variables')}</FormLabel>
|
<FormLabel>{t("variables")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder={t('domain.management.edit.variables.placeholder')}
|
placeholder={t(
|
||||||
|
"domain.management.edit.variables.placeholder"
|
||||||
|
)}
|
||||||
{...field}
|
{...field}
|
||||||
className="placeholder:whitespace-pre-wrap"
|
className="placeholder:whitespace-pre-wrap"
|
||||||
/>
|
/>
|
||||||
@@ -530,10 +571,12 @@ const Edit = () => {
|
|||||||
name="nameservers"
|
name="nameservers"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem hidden={tab != "advance"}>
|
<FormItem hidden={tab != "advance"}>
|
||||||
<FormLabel>{t('dns')}</FormLabel>
|
<FormLabel>{t("dns")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder={t('domain.management.edit.dns.placeholder')}
|
placeholder={t(
|
||||||
|
"domain.management.edit.dns.placeholder"
|
||||||
|
)}
|
||||||
{...field}
|
{...field}
|
||||||
className="placeholder:whitespace-pre-wrap"
|
className="placeholder:whitespace-pre-wrap"
|
||||||
/>
|
/>
|
||||||
@@ -545,7 +588,7 @@ const Edit = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit">{t('save')}</Button>
|
<Button type="submit">{t("save")}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const Home = () => {
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const query = new URLSearchParams(location.search);
|
const query = new URLSearchParams(location.search);
|
||||||
@@ -129,12 +129,12 @@ const Home = () => {
|
|||||||
await save(domain);
|
await save(domain);
|
||||||
|
|
||||||
toast.toast({
|
toast.toast({
|
||||||
title: t('operation.succeed'),
|
title: t("operation.succeed"),
|
||||||
description: t('domain.management.start.deploy.succeed.tips'),
|
description: t("domain.management.start.deploy.succeed.tips"),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.toast({
|
toast.toast({
|
||||||
title: t('domain.management.execution.failed'),
|
title: t("domain.management.execution.failed"),
|
||||||
description: (
|
description: (
|
||||||
// 这里的 text 只是占位作用,实际文案在 src/i18n/locales/[lang].json
|
// 这里的 text 只是占位作用,实际文案在 src/i18n/locales/[lang].json
|
||||||
<Trans i18nKey="domain.management.execution.failed.tips">
|
<Trans i18nKey="domain.management.execution.failed.tips">
|
||||||
@@ -142,7 +142,9 @@ const Home = () => {
|
|||||||
<Link
|
<Link
|
||||||
to={`/history?domain=${domain.id}`}
|
to={`/history?domain=${domain.id}`}
|
||||||
className="underline text-blue-500"
|
className="underline text-blue-500"
|
||||||
>text2</Link>
|
>
|
||||||
|
text2
|
||||||
|
</Link>
|
||||||
text3
|
text3
|
||||||
</Trans>
|
</Trans>
|
||||||
),
|
),
|
||||||
@@ -176,10 +178,10 @@ const Home = () => {
|
|||||||
<div className="">
|
<div className="">
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="text-muted-foreground">{t('domain.management.name')}</div>
|
<div className="text-muted-foreground">
|
||||||
<Button onClick={handleCreateClick}>
|
{t("domain.management.name")}
|
||||||
{t('domain.add')}
|
</div>
|
||||||
</Button>
|
<Button onClick={handleCreateClick}>{t("domain.add")}</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!domains.length ? (
|
{!domains.length ? (
|
||||||
@@ -190,26 +192,32 @@ const Home = () => {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="text-center text-sm text-muted-foreground mt-3">
|
<div className="text-center text-sm text-muted-foreground mt-3">
|
||||||
{t('domain.management.empty')}
|
{t("domain.management.empty")}
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleCreateClick} className="mt-3">
|
<Button onClick={handleCreateClick} className="mt-3">
|
||||||
{t('domain.add')}
|
{t("domain.add")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
|
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
|
||||||
<div className="w-36">{t('domain')}</div>
|
<div className="w-36">{t("domain")}</div>
|
||||||
<div className="w-40">{t('domain.management.expiry.date')}</div>
|
<div className="w-40">{t("domain.management.expiry.date")}</div>
|
||||||
<div className="w-32">{t('domain.management.last.execution.status')}</div>
|
<div className="w-32">
|
||||||
<div className="w-64">{t('domain.management.last.execution.stage')}</div>
|
{t("domain.management.last.execution.status")}
|
||||||
<div className="w-40 sm:ml-2">{t('domain.management.last.execution.time')}</div>
|
</div>
|
||||||
<div className="w-24">{t('domain.management.enable')}</div>
|
<div className="w-64">
|
||||||
<div className="grow">{t('operation')}</div>
|
{t("domain.management.last.execution.stage")}
|
||||||
|
</div>
|
||||||
|
<div className="w-40 sm:ml-2">
|
||||||
|
{t("domain.management.last.execution.time")}
|
||||||
|
</div>
|
||||||
|
<div className="w-24">{t("domain.management.enable")}</div>
|
||||||
|
<div className="grow">{t("operation")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:hidden flex text-sm text-muted-foreground">
|
<div className="sm:hidden flex text-sm text-muted-foreground">
|
||||||
{t('domain')}
|
{t("domain")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{domains.map((domain) => (
|
{domains.map((domain) => (
|
||||||
@@ -217,15 +225,26 @@ const Home = () => {
|
|||||||
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
|
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
|
||||||
key={domain.id}
|
key={domain.id}
|
||||||
>
|
>
|
||||||
<div className="sm:w-36 w-full pt-1 sm:pt-0 flex items-center">
|
<div className="sm:w-36 w-full pt-1 sm:pt-0 flex items-center truncate">
|
||||||
{domain.domain}
|
{domain.domain.split(";").map((item) => (
|
||||||
|
<>
|
||||||
|
{item}
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:w-40 w-full pt-1 sm:pt-0 flex items-center">
|
<div className="sm:w-40 w-full pt-1 sm:pt-0 flex items-center">
|
||||||
<div>
|
<div>
|
||||||
{domain.expiredAt ? (
|
{domain.expiredAt ? (
|
||||||
<>
|
<>
|
||||||
<div>{t('domain.management.expiry.date1', { date: 90 })}</div>
|
<div>
|
||||||
<div>{t('domain.management.expiry.date2', { date: getDate(domain.expiredAt) })}</div>
|
{t("domain.management.expiry.date1", { date: 90 })}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{t("domain.management.expiry.date2", {
|
||||||
|
date: getDate(domain.expiredAt),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"---"
|
"---"
|
||||||
@@ -269,7 +288,7 @@ const Home = () => {
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<div className="border rounded-sm px-3 bg-background text-muted-foreground text-xs">
|
<div className="border rounded-sm px-3 bg-background text-muted-foreground text-xs">
|
||||||
{domain.enabled ? t('disable') : t('enable')}
|
{domain.enabled ? t("disable") : t("enable")}
|
||||||
</div>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -281,7 +300,7 @@ const Home = () => {
|
|||||||
className="p-0"
|
className="p-0"
|
||||||
onClick={() => handleHistoryClick(domain.id)}
|
onClick={() => handleHistoryClick(domain.id)}
|
||||||
>
|
>
|
||||||
{t('deployment.log.name')}
|
{t("deployment.log.name")}
|
||||||
</Button>
|
</Button>
|
||||||
<Show when={domain.enabled ? true : false}>
|
<Show when={domain.enabled ? true : false}>
|
||||||
<Separator orientation="vertical" className="h-4 mx-2" />
|
<Separator orientation="vertical" className="h-4 mx-2" />
|
||||||
@@ -290,7 +309,7 @@ const Home = () => {
|
|||||||
className="p-0"
|
className="p-0"
|
||||||
onClick={() => handleRightNowClick(domain)}
|
onClick={() => handleRightNowClick(domain)}
|
||||||
>
|
>
|
||||||
{t('domain.management.start.deploying')}
|
{t("domain.management.start.deploying")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
@@ -307,7 +326,7 @@ const Home = () => {
|
|||||||
className="p-0"
|
className="p-0"
|
||||||
onClick={() => handleForceClick(domain)}
|
onClick={() => handleForceClick(domain)}
|
||||||
>
|
>
|
||||||
{t('domain.management.forced.deployment')}
|
{t("domain.management.forced.deployment")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
@@ -318,7 +337,7 @@ const Home = () => {
|
|||||||
className="p-0"
|
className="p-0"
|
||||||
onClick={() => handleDownloadClick(domain)}
|
onClick={() => handleDownloadClick(domain)}
|
||||||
>
|
>
|
||||||
{t('download')}
|
{t("download")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
@@ -328,24 +347,26 @@ const Home = () => {
|
|||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant={"link"} className="p-0">
|
<Button variant={"link"} className="p-0">
|
||||||
{t('delete')}
|
{t("delete")}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>{t('domain.delete')}</AlertDialogTitle>
|
<AlertDialogTitle>
|
||||||
|
{t("domain.delete")}
|
||||||
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
{t('domain.management.delete.confirm')}
|
{t("domain.management.delete.confirm")}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
|
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDeleteClick(domain.id);
|
handleDeleteClick(domain.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('confirm')}
|
{t("confirm")}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
@@ -357,7 +378,7 @@ const Home = () => {
|
|||||||
className="p-0"
|
className="p-0"
|
||||||
onClick={() => handleEditClick(domain.id)}
|
onClick={() => handleEditClick(domain.id)}
|
||||||
>
|
>
|
||||||
{t('edit')}
|
{t("edit")}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -40,20 +40,17 @@ const History = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="h-[80vh] overflow-hidden">
|
<ScrollArea className="h-[80vh] overflow-hidden">
|
||||||
<div className="text-muted-foreground">{t('deployment.log.name')}</div>
|
<div className="text-muted-foreground">{t("deployment.log.name")}</div>
|
||||||
{!deployments?.length ? (
|
{!deployments?.length ? (
|
||||||
<>
|
<>
|
||||||
<Alert className="max-w-[40em] mx-auto mt-20">
|
<Alert className="max-w-[40em] mx-auto mt-20">
|
||||||
<AlertTitle>{t('no.data')}</AlertTitle>
|
<AlertTitle>{t("no.data")}</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<div className="flex items-center mt-5">
|
<div className="flex items-center mt-5">
|
||||||
<div>
|
<div>
|
||||||
<Smile className="text-yellow-400" size={36} />
|
<Smile className="text-yellow-400" size={36} />
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-2">
|
<div className="ml-2"> {t("deployment.log.empty")}</div>
|
||||||
{" "}
|
|
||||||
{t('deployment.log.empty')}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex justify-end">
|
<div className="mt-2 flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
@@ -61,7 +58,7 @@ const History = () => {
|
|||||||
navigate("/");
|
navigate("/");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('domain.add')}
|
{t("domain.add")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
@@ -70,16 +67,18 @@ const History = () => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
|
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
|
||||||
<div className="w-48">{t('domain')}</div>
|
<div className="w-48">{t("domain")}</div>
|
||||||
|
|
||||||
<div className="w-24">{t('deployment.log.status')}</div>
|
<div className="w-24">{t("deployment.log.status")}</div>
|
||||||
<div className="w-56">{t('deployment.log.stage')}</div>
|
<div className="w-56">{t("deployment.log.stage")}</div>
|
||||||
<div className="w-56 sm:ml-2 text-center">{t('deployment.log.last.execution.time')}</div>
|
<div className="w-56 sm:ml-2 text-center">
|
||||||
|
{t("deployment.log.last.execution.time")}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grow">{t('operation')}</div>
|
<div className="grow">{t("operation")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:hidden flex text-sm text-muted-foreground">
|
<div className="sm:hidden flex text-sm text-muted-foreground">
|
||||||
{t('deployment.log.name')}
|
{t("deployment.log.name")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{deployments?.map((deployment) => (
|
{deployments?.map((deployment) => (
|
||||||
@@ -88,7 +87,14 @@ const History = () => {
|
|||||||
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
|
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
|
||||||
>
|
>
|
||||||
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center">
|
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center">
|
||||||
{deployment.expand.domain?.domain}
|
{deployment.expand.domain?.domain
|
||||||
|
.split(";")
|
||||||
|
.map((domain: string) => (
|
||||||
|
<>
|
||||||
|
{domain}
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
|
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
|
||||||
<DeployState deployment={deployment} />
|
<DeployState deployment={deployment} />
|
||||||
@@ -106,14 +112,14 @@ const History = () => {
|
|||||||
<Sheet>
|
<Sheet>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button variant={"link"} className="p-0">
|
<Button variant={"link"} className="p-0">
|
||||||
{t('deployment.log.detail.button.text')}
|
{t("deployment.log.detail.button.text")}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent className="sm:max-w-5xl">
|
<SheetContent className="sm:max-w-5xl">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>
|
<SheetTitle>
|
||||||
{deployment.expand.domain?.domain}-{deployment.id}
|
{deployment.expand.domain?.domain}-{deployment.id}
|
||||||
{t('deployment.log.detail')}
|
{t("deployment.log.detail")}
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">
|
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">
|
||||||
|
|||||||
Reference in New Issue
Block a user