Compare commits

...

69 Commits

Author SHA1 Message Date
yoan
704d2eed32 v0.2.1 2024-10-16 08:16:43 +08:00
usual2970
3348301493 Merge pull request #204 from LeoChen98/change-tencent-ssl-upload-repeatable
change: tencent ssl upload repeatable to false
2024-10-16 08:13:38 +08:00
Leo Chen
afeae4269c change: tencent ssl upload repeatable to false
腾讯云ssl证书上传接口可重复选项设置为`false`,以避免重复上传导致的列表污染。
2024-10-16 00:19:57 +08:00
yoan
be15f2b6a6 Delete the mistakenly added files 2024-10-15 18:28:56 +08:00
yoan
0c1d3341f4 update tencent cdn deploy 2024-10-15 17:53:38 +08:00
usual2970
1a9cad355c Merge pull request #198 from fudiwei/main
chore: improve i18n
2024-10-15 16:58:36 +08:00
Fu Diwei
2c97fa929a chore: improve i18n 2024-10-14 21:43:05 +08:00
Fu Diwei
e397793153 chore: improve i18n 2024-10-14 21:00:50 +08:00
usual2970
9bd279a8a0 Update README.md 2024-10-13 19:24:52 +08:00
yoan
dd7897feff Update readme 2024-10-13 18:37:52 +08:00
yoan
d851a86a9b fix dark mod style 2024-10-13 18:36:02 +08:00
usual2970
62133a91a6 Merge pull request #190 from usual2970/feat/oss
Replace the OSS deployment api
2024-10-13 11:16:18 +08:00
yoan
5b30fc8aba Replace the OSS deployment api 2024-10-13 11:15:35 +08:00
usual2970
2ed94bf509 Merge pull request #188 from usual2970/feat/v0.2
Feat/v0.2
2024-10-13 08:16:34 +08:00
yoan
1928a47961 v0.2 2024-10-13 08:15:21 +08:00
yoan
19f5348802 Merge branch 'main' into feat/v0.2 2024-10-12 08:43:32 +08:00
yoan
f148240bcf v0.1.19 2024-10-12 08:04:34 +08:00
yoan
f914931bc9 go mod tidy 2024-10-11 22:28:26 +08:00
yoan
8c1033634d update Dockerfile name 2024-10-11 22:23:41 +08:00
usual2970
781b79f529 Merge pull request #184 from fudiwei/feat/huaweicloud
feat: add huaweicloud provider
2024-10-11 22:08:18 +08:00
yoan
7d74e1d41e init 2024-10-11 21:53:54 +08:00
RHQYZ
ad91703492 Merge branch 'main' into feat/huaweicloud 2024-10-11 09:43:01 +08:00
Fu Diwei
a007c81e9a feat: add huaweicloud provider 2024-10-11 09:30:14 +08:00
yoan
39bffe3389 v0.1.18 2024-10-11 07:53:32 +08:00
yoan
3b06c7b0a6 init 2024-10-11 07:52:16 +08:00
usual2970
3f2767b28b Merge pull request #183 from LeoChen98/feat-tencent-cdn-extensive-support
add feat: support for tencent cdn extensive domain
2024-10-11 07:02:24 +08:00
Leo Chen
312c6e685a change var name style 2024-10-10 22:45:19 +08:00
Leo Chen
d2b6ab75b7 add feat: support for tencent cdn extensive domain 2024-10-10 19:01:32 +08:00
yoan
9f1b00f04c init 2024-10-10 11:13:45 +08:00
usual2970
dc16294b3d Update README_EN.md 2024-10-09 12:33:42 +08:00
usual2970
77dfcef168 Update README.md 2024-10-09 12:31:53 +08:00
yoan
30ef5841d6 dark mode style fix 2024-10-09 09:26:39 +08:00
yoan
217ba85ff8 v0.1.16 2024-10-09 09:04:23 +08:00
yoan
71e2555391 multiple domain support 2024-10-08 22:02:00 +08:00
yoan
f036eb1cf2 Add the functionality to authorize copying 2024-10-04 08:19:46 +08:00
usual2970
1347066549 Merge pull request #131 from liburdi/hotfix/access_copy_word
fix: update en.json
2024-10-03 07:55:09 +08:00
liburdi
7fc149f67d fix: update en.json 2024-10-02 12:55:24 +08:00
yoan
dfba5ee638 Merge branch 'liburdi-feature/copy_access' 2024-10-01 07:04:56 +08:00
yoan
9ba79f996f fix conflict 2024-10-01 07:04:40 +08:00
yoan
cd85000908 Merge branch 'JonathanSimon123-main' 2024-10-01 06:59:18 +08:00
liburdi
995349ab3e feat: add issues 124 2024-09-30 18:22:16 +08:00
simon
4fa8031318 feat:Add star sequence diagram 2024-09-30 14:08:11 +08:00
simon
3f45bb1629 Merge branch 'main' of https://github.com/JonathanSimon123/certimate 2024-09-30 11:46:08 +08:00
蒋驰磊
0e139e6284 feat:Add star sequence diagram 2024-09-30 11:44:37 +08:00
JonathanSimon123
82dbfc6de3 Update README.md
feature:Add start sequence diagram
2024-09-30 11:04:41 +08:00
JonathanSimon123
9b2937d601 Update README_EN.md
feature:Add start sequence diagram
2024-09-30 11:02:06 +08:00
yoan
3375839a40 Merge branch 'l123wx-local_deployment_translate' 2024-09-30 07:35:26 +08:00
yoan
7f5ff6fab5 build ui 2024-09-30 07:34:44 +08:00
usual2970
5160b4c3d9 Merge pull request #120 from yanlc39/dev/fix_docker_deploy_way
簡化 Docker 部署的方式
2024-09-30 07:33:27 +08:00
Elvis Liao
85234b21c7 chore: translate local deployment form 2024-09-29 20:43:42 +08:00
yanlc39
223af9e09d feat: let docker deploy way simpley 2024-09-29 17:42:05 +08:00
usual2970
49fdf8213a Merge pull request #117 from sgpublic-forks/feat/pre_command
fix: wrong pre command reference
2024-09-29 08:30:04 +08:00
Madray Haven
7a48101015 fix: wrong pre command reference 2024-09-28 17:28:55 +08:00
yoan
6b85b4a0c9 Merge branch 'l123wx-i18n' 2024-09-28 07:47:11 +08:00
yoan
3c56a53e91 translate new element to English 2024-09-28 07:46:55 +08:00
yoan
9797a0835d Merge branch 'sgpublic-forks-feat/pre_command' 2024-09-28 07:05:02 +08:00
yoan
b6dc57f3e4 ui build 2024-09-28 07:04:17 +08:00
yoan
78ac21c767 Merge branch 'feat/pre_command' of github.com:sgpublic-forks/certimate into sgpublic-forks-feat/pre_command 2024-09-28 07:01:46 +08:00
elvis liao
1e2d8fa027 chore: abstract resources configure 2024-09-27 16:31:24 +08:00
Madray Haven
e7e2e4786d chore: create a func for running ssh command 2024-09-27 16:14:35 +08:00
Madray Haven
9acdd15c1e feat: pre command for ssh deploy 2024-09-27 15:12:04 +08:00
elvis liao
fcc0dd93fd refactor(DeployProgress): 重构逻辑,修复样式问题 2024-09-27 13:42:58 +08:00
elvis liao
5eba437732 feat: english translation 2024-09-27 13:41:40 +08:00
elvis liao
0d0fcfccf3 feat: i18n transformation completed 2024-09-27 13:01:37 +08:00
yoan
993ef7bf57 Add contributing guide 2024-09-27 07:26:11 +08:00
Elvis Liao
e820e5599b wip: i18n
- Change locales json to one-dimensional format
2024-09-27 00:42:40 +08:00
Elvis Liao
b3f1e1e444 chore: import i18n 2024-09-26 19:47:50 +08:00
elvis liao
0abb030889 wip: i18n chinese 2024-09-26 17:57:30 +08:00
elvis liao
85df8eb09d feat: Add i18n 2024-09-26 17:53:44 +08:00
103 changed files with 5886 additions and 2035 deletions

72
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,72 @@
# 向 Certimate 贡献代码
感谢你抽出时间来改进 Certimate以下是向 Certimate 主仓库提交 PRPull Request时的操作指南。
- [向 Certimate 贡献代码](#向-certimate-贡献代码)
- [前提条件](#前提条件)
- [修改 Go 代码](#修改-go-代码)
- [修改管理页面 (Admin UI)](#修改管理页面-admin-ui)
## 前提条件
- Go 1.22+ (用于修改 Go 代码)
- Node 20+ (用于修改 UI)
如果还没有这样做,你可以 fork Certimate 的主仓库,并克隆到本地以便进行修改:
```bash
git clone https://github.com/your_username/certimate.git
```
> **重要提示:**
> 建议为每个 bug 修复或新功能创建一个从 `main` 分支派生的新分支。如果你计划提交多个 PR请保持不同的改动在独立分支中以便更容易进行代码审查并最终合并。
> 保持一个 pr 只实现一个功能。
## 修改 Go 代码
假设你已经对 Certimate 的 Go 代码做了一些修改,现在你想要运行它:
1. 进入根目录
2. 运行以下命令启动服务:
```bash
go run main.go serve
```
这将启动一个 Web 服务器,默认运行在 `http://localhost:8090`,并使用来自 `ui/dist` 的预构建管理页面。
**在向主仓库提交 PR 之前,建议你:**
- 为你的改动添加单元测试或集成测试Certimate 使用 Go 的标准 `testing` 包)。你可以通过以下命令运行测试(在项目根目录下):
```bash
go test ./...
```
## 修改管理页面 (Admin UI)
Certimate 的管理页面是一个基于 React 和 Vite 构建的单页应用SPA
要启动 Admin UI
1. 进入 `ui` 项目目录
2. 运行 `npm install` 安装依赖
3. 启动 Vite 开发服务器:
```bash
npm run dev
```
你可以通过浏览器访问 `http://localhost:5173` 来查看运行中的管理页面。
由于 Admin UI 只是一个客户端应用,运行时需要 Certimate 的后端服务作为支撑。你可以手动运行Certimate或者使用预构建的可执行文件。
所有对 Admin UI 的修改将会自动反映在浏览器中,无需手动刷新页面。
完成修改后,运行以下命令来构建 Admin UI以便它能被嵌入到 Go 包中:
```bash
npm run build
```
完成所有步骤后,你可以将改动提交 PR 到 Certimate 主仓库。

73
CONTRIBUTING_EN.md Normal file
View File

@@ -0,0 +1,73 @@
# Contributing to Certimate
Thank you for taking the time to improve Certimate! Below is a guide for submitting a PR (Pull Request) to the main Certimate repository.
- [Contributing to Certimate](#contributing-to-certimate)
- [Prerequisites](#prerequisites)
- [Making Changes in the Go Code](#making-changes-in-the-go-code)
- [Making Changes in the Admin UI](#making-changes-in-the-admin-ui)
## Prerequisites
- Go 1.22+ (for Go code changes)
- Node 20+ (for Admin UI changes)
If you haven't done so already, you can fork the Certimate repository and clone your fork to work locally:
```bash
git clone https://github.com/your_username/certimate.git
```
> **Important:**
> It is recommended to create a new branch from `main` for each bug fix or feature. If you plan to submit multiple PRs, ensure the changes are in separate branches for easier review and eventual merge.
> Keep each PR focused on a single feature or fix.
## Making Changes in the Go Code
Once you have made changes to the Go code in Certimate, follow these steps to run the project:
1. Navigate to the root directory.
2. Start the service by running:
```bash
go run main.go serve
```
This will start a web server at `http://localhost:8090` using the prebuilt Admin UI located in `ui/dist`.
**Before submitting a PR to the main repository, consider:**
- Adding unit or integration tests for your changes. Certimate uses Gos standard `testing` package. You can run tests using the following command (while in the root project directory):
```bash
go test ./...
```
## Making Changes in the Admin UI
Certimates Admin UI is a single-page application (SPA) built using React and Vite.
To start the Admin UI:
1. Navigate to the `ui` project directory.
2. Install the necessary dependencies by running:
```bash
npm install
```
3. Start the Vite development server:
```bash
npm run dev
```
You can now access the running Admin UI at `http://localhost:5173` in your browser.
Since the Admin UI is a client-side application, you will also need to have the Certimate backend running. You can either manually run Certimate or use a prebuilt executable.
Any changes you make in the Admin UI will be automatically reflected in the browser without requiring a page reload.
After completing your changes, build the Admin UI so it can be embedded into the Go package:
```bash
npm run build
```
Once all steps are completed, you are ready to submit a PR to the main Certimate repository.

View File

@@ -1,4 +1,4 @@
[中文](README.md) | [English](README_EN.md)
[中文](README.md) | [English](README_EN.md)
# 🔒Certimate
@@ -15,16 +15,12 @@ Certimate 就是为了解决上述问题而产生的,它具有以下特点:
相关文章:
* [V0.2.0-第一个不向后兼容的版本](https://docs.certimate.me/blog/v0.2.0)
* [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 非常简单,你可以选择以下方式之一进行安装:
@@ -37,15 +33,19 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
./certimate serve
```
或运行以下命令自动给 Certimate 自身添加证书
```bash
./certimate serve 你的域名
```
> [!NOTE]
> MacOS 在执行二进制文件时会提示无法打开“certimate”因为Apple无法检查其是否包含恶意软件。可在系统设置> 隐私与安全性> 安全性 中点击 "仍然允许",然后再次尝试执行二进制文件。
### 2. Docker 安装
```bash
git clone git@github.com:usual2970/certimate.git && cd certimate/docker && docker compose up -d
mkdir -p ~/.certimate && cd ~/.certimate && curl -O https://raw.githubusercontent.com/usual2970/certimate/refs/heads/main/docker/docker-compose.yml && docker compose up -d
```
@@ -58,7 +58,6 @@ go mod vendor
go run main.go serve
```
## 二、使用
执行完上述安装操作后,在浏览器中访问 `http://127.0.0.1:8090` 即可访问 Certimate 管理页面。
@@ -72,17 +71,14 @@ go run main.go serve
## 三、支持的服务商列表
| 服务商 | 是否域名服务商 | 是否部署服务 | 备注 |
|------|------|-----|------|
| 阿里云| 是 | 是 | 支持阿里云注册的域名,支持部署到阿里云 CDN,OSS |
| 腾讯云| 是 | 是 | 支持腾讯云注册的域名,支持部署到腾讯云 CDN |
| 七牛云| 否 | 是 | 七牛云没有注册域名服务,支持部署到七牛云 CDN |
|CloudFlare| 是 | 否 | 支持 CloudFlare 注册的域名CloudFlare 服务自带SSL证书 |
|SSH| 否 | 是 | 支持部署到 SSH 服务器 |
|WEBHOOK| 否 | 是 | 支持回调到 WEBHOOK |
| 服务商 | 是否域名服务商 | 是否部署服务 | 备注 |
| ---------- | -------------- | ------------ | ------------------------------------------------------ |
| 阿里云 | 是 | 是 | 支持阿里云注册的域名,支持部署到阿里云 CDN,OSS |
| 腾讯云 | 是 | 是 | 支持腾讯云注册的域名,支持部署到腾讯云 CDN |
| 七牛云 | 否 | 是 | 七牛云没有注册域名服务,支持部署到七牛云 CDN |
| CloudFlare | 是 | 否 | 支持 CloudFlare 注册的域名CloudFlare 服务自带SSL证书 |
| SSH | 否 | 是 | 支持部署到 SSH 服务器 |
| WEBHOOK | 否 | 是 | 支持回调到 WEBHOOK |
## 四、系统截图
@@ -96,7 +92,6 @@ go run main.go serve
![history](https://i.imgur.com/aaPtSW7.jpeg)
## 五、概念
Certimate 的工作流程如下:
@@ -140,7 +135,6 @@ Certimate 申请证书后,会自动将证书部署到你指定的目标上,
## 六、常见问题
Q: 提供saas服务吗
> A: 不提供目前仅支持self-hosted私有部署
@@ -153,23 +147,23 @@ Q: 自动续期证书?
> A: 已经申请的证书会在过期前10天自动续期。每天会检查一次证书是否快要过期快要过期时会自动重新申请证书并部署到目标服务上。
## 七、贡献
Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.md)。你可以使用它做任何你想做的事,甚至把它当作一个付费服务提供给用户。
你可以通过以下方式来支持 Certimate 的开发:
* 提交代码:如果你发现了 bug 或有新的功能需求,而你又有相关经验,可以提交代码给我们。
* [提交代码:如果你发现了 bug 或有新的功能需求,而你又有相关经验,可以提交代码给我们](CONTRIBUTING.md)
* 提交 issue功能建议或者 bug 可以[提交 issue](https://github.com/usual2970/certimate/issues) 给我们。
支持更多服务商、UI 的优化改进、BUG 修复、文档完善等,欢迎大家提交 PR。
## 八、加入社区
* [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
[![Stargazers over time](https://starchart.cc/usual2970/certimate.svg?variant=adaptive)](https://starchart.cc/usual2970/certimate)

View File

@@ -1,10 +1,9 @@
[中文](README.md) | [English](README_EN.md)
[中文](README.md) | [English](README_EN.md)
# 🔒Certimate
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 isnt 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.
@@ -19,35 +18,34 @@ 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)
## Installation
Installing Certimate is very simple, you can choose one of the following methods for installation:
### 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:
```bash
./certimate serve
```
Or run the following command to automatically add a certificate to Certimate itself.
```bash
./certimate serve yourDomain
```
> [!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.
### 2. Docker Installation
```bash
git clone git@github.com:usual2970/certimate.git && cd certimate/docker && docker compose up -d
mkdir -p ~/.certimate && cd ~/.certimate && curl -O https://raw.githubusercontent.com/usual2970/certimate/refs/heads/main/docker/docker-compose.yml && docker compose up -d
```
@@ -60,7 +58,6 @@ go mod vendor
go run main.go serve
```
## 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.
@@ -74,19 +71,16 @@ 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 | 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. |
## Screenshots
## Screenshots
![login](https://i.imgur.com/SYjjbql.jpeg)
@@ -98,7 +92,6 @@ password1234567890
![history](https://i.imgur.com/aaPtSW7.jpeg)
## Concepts
The workflow of Certimate is as follows:
@@ -140,8 +133,7 @@ After Certimate applies for the certificate, it will automatically deploy the ce
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.
## FAQ
## FAQ
Q: Do you provide SaaS services?
@@ -155,23 +147,20 @@ 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.
## 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.
You can support the development of Certimate in the following ways:
* **Submit Code**: If you find a bug or have new feature requests, and you have relevant experience, you can submit code to us.
* **Submit Code**: If you find a bug or have new feature requests, and you have relevant experience, [you can submit code to us](CONTRIBUTING_EN.md).
* **Submit an Issue**: For feature suggestions or bugs, you can [submit an issue](https://github.com/usual2970/certimate/issues) to us.
Support for more service providers, UI enhancements, bug fixes, and documentation improvements are all welcome. We encourage everyone to submit pull requests (PRs).
## 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/lJUfTeD.png" width="400"/>
<img src="https://i.imgur.com/zSHEoIm.png" width="400"/>

91
go.mod
View File

@@ -1,16 +1,16 @@
module certimate
go 1.22
go 1.22.0
toolchain go1.22.5
toolchain go1.23.2
require (
github.com/alibabacloud-go/cas-20200407/v2 v2.3.0
github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9
github.com/alibabacloud-go/tea v1.2.2
github.com/alibabacloud-go/tea-utils/v2 v2.0.6
github.com/go-acme/lego/v4 v4.17.4
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/go-acme/lego/v4 v4.19.2
github.com/gojek/heimdall/v7 v7.0.3
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
github.com/nikoksr/notify v1.0.0
@@ -18,9 +18,10 @@ require (
github.com/pocketbase/dbx v1.10.1
github.com/pocketbase/pocketbase v0.22.18
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
golang.org/x/crypto v0.26.0
golang.org/x/crypto v0.27.0
)
require (
@@ -30,8 +31,10 @@ require (
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
github.com/blinkbean/dingtalk v1.1.3 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
go.mongodb.org/mongo-driver v1.12.0 // indirect
)
require (
@@ -44,39 +47,39 @@ require (
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
github.com/alibabacloud-go/tea-utils v1.4.5 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 // indirect
github.com/aliyun/credentials-go v1.3.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.33 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
github.com/aws/smithy-go v1.20.4 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/clbanning/mxj/v2 v2.5.6 // indirect
github.com/cloudflare/cloudflare-go v0.97.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/cloudflare/cloudflare-go v0.104.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
@@ -85,8 +88,6 @@ require (
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
@@ -97,39 +98,39 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/dns v1.1.59 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 // indirect
github.com/tjfoc/gmsm v1.3.2 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
go.opencensus.io v0.24.0 // indirect
gocloud.dev v0.37.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.22.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/term v0.24.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/time v0.6.0 // indirect
golang.org/x/tools v0.25.0 // indirect
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
google.golang.org/api v0.189.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/api v0.197.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.66.1 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

252
go.sum
View File

@@ -1,13 +1,13 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE=
cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U=
cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk=
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
cloud.google.com/go/compute v1.25.0 h1:H1/4SqSUhjPFE7L5ddzHOfY2bCAvjwNRZPNl6Ni5oYU=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/compute/metadata v0.5.1 h1:NM6oZeZNlYjiwYje+sYFjEpP0Q0zCan1bmQW/KmIrGs=
cloud.google.com/go/compute/metadata v0.5.1/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
cloud.google.com/go/iam v1.1.11 h1:0mQ8UKSfdHLut6pH9FM3bI55KWR46ketn0PuXleDyxw=
cloud.google.com/go/iam v1.1.11/go.mod h1:biXoiLWYIKntto2joP+62sd9uW5EpkZmKIvfNcTWlnQ=
cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
@@ -31,12 +31,9 @@ github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 h1:iC9YFYKDGEy3n/FtqJnOkZsene9olVspKmkX5A2YBEo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
github.com/alibabacloud-go/cas-20200407/v2 v2.3.0 h1:nOrp0n2nFZiYN0wIG7S26YVVaMMzOBkX9GJqUvYnGeE=
github.com/alibabacloud-go/cas-20200407/v2 v2.3.0/go.mod h1:yzkgdLANANu/v56k0ptslGl++JJL4Op1V09HTavfoCo=
github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0 h1:yTKngw4rBR3hdpoo/uCyBffYXfPfjNjlaDL8nTYUIds=
github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0/go.mod h1:HxQrwVKBx3/6bIwmdDcpqBpSQt2tpi/j4LfEhl+QFPk=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.0/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.6/go.mod h1:CzQnh+94WDnJOnKZH5YRyouL+OOcdBnXY5VWAf0McgI=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.8/go.mod h1:CzQnh+94WDnJOnKZH5YRyouL+OOcdBnXY5VWAf0McgI=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9 h1:fxMCrZatZfXq5nLcgkmWBXmU3FLC1OR+m/SqVtMqflk=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9/go.mod h1:bb+Io8Sn2RuM3/Rpme6ll86jMyFSrD1bxeV/+v61KeU=
@@ -80,8 +77,10 @@ github.com/alibabacloud-go/tea-xml v1.1.1/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCE
github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 h1:lM7JnA9dEdDFH9XOgRNQMDTQnOjlLkDTNA7c0aWTQ30=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.712/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 h1:r2uwBUQhLhcPzaWz9tRJqc8MjYwHb+oF2+Q6467BF14=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.15/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
github.com/aliyun/credentials-go v1.3.1 h1:uq/0v7kWrxmoLGpqjx7vtQ/s03f0zR//0br/xWDTE28=
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
@@ -90,44 +89,44 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.51.11 h1:El5VypsMIz7sFwAAj/j06JX9UGs4KAbAIEaZ57bNY4s=
github.com/aws/aws-sdk-go v1.51.11/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g=
github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4/go.mod h1:/MQxMqci8tlqDH+pjmoLu1i0tbWCUP1hhyMRuFxpQCw=
github.com/aws/aws-sdk-go-v2/config v1.27.33 h1:Nof9o/MsmH4oa0s2q9a0k7tMz5x/Yj5k06lDODWz3BU=
github.com/aws/aws-sdk-go-v2/config v1.27.33/go.mod h1:kEqdYzRb8dd8Sy2pOdEbExTTF5v7ozEXX0McgPE7xks=
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 h1:7Cxhp/BnT2RcGy4VisJ9miUPecY+lyE9I8JvcZofn9I=
github.com/aws/aws-sdk-go-v2/credentials v1.17.32/go.mod h1:P5/QMF3/DCHbXGEGkdbilXHsyTBX5D3HSwcrSc9p20I=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8 h1:u1KOU1S15ufyZqmH/rA3POkiRH6EcDANHj2xHRzq+zc=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8/go.mod h1:WPv2FRnkIOoDv/8j2gSUsI4qDc7392w5anFB/I89GZ8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 h1:sZXIzO38GZOU+O0C+INqbH7C2yALwfMWpd64tONS/NE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 h1:Roo69qTpfu8OlJ2Tb7pAYVuF0CpuUMB0IYWwYP/4DZM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17/go.mod h1:NcWPxQzGM1USQggaTVwz6VpqMZPX1CvDJLDh6jnOCa4=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 h1:FLMkfEiRjhgeDTCjjLoc3URo/TBkgeQbocA78lfkzSI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19/go.mod h1:Vx+GucNSsdhaxs3aZIKfSUjKVGsxN25nX2SRcdhuw08=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 h1:u+EfGmksnJc/x5tq3A+OD7LrMbSSR/5TrKLvkdy/fhY=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17/go.mod h1:VaMx6302JHax2vHJWgRo+5n9zvbacs3bLU/23DNQrTY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2 h1:Kp6PWAlXwP1UvIflkIP6MFZYBNDCa4mFCGtxrpICVOg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2/go.mod h1:5FmD/Dqq57gP+XwaUnd5WFPipAuzrf0HmupX27Gvjvc=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 h1:pIaGg+08llrP7Q5aiz9ICWbY8cqhTkyy+0SHvfzQpTc=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 h1:/Cfdu0XV3mONYKaOt1Gr0k1KvQzkzPyiKUdlWJqy+J4=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8KNk8sDkNPFaOKDE=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o=
github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/blinkbean/dingtalk v1.1.3 h1:MbidFZYom7DTFHD/YIs+eaI7kRy52kmWE/sy0xjo6E4=
github.com/blinkbean/dingtalk v1.1.3/go.mod h1:9BaLuGSBqY3vT5hstValh48DbsKO7vaHaJnG9pXwbto=
github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI=
@@ -138,8 +137,8 @@ github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn
github.com/clbanning/mxj/v2 v2.5.6 h1:Jm4VaCI/+Ug5Q57IzEoZbwx4iQFA6wkXv72juUSeK+g=
github.com/clbanning/mxj/v2 v2.5.6/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.97.0 h1:feZRGiRF1EbljnNIYdt8014FnOLtC3CCvgkLXu915ks=
github.com/cloudflare/cloudflare-go v0.97.0/go.mod h1:JXRwuTfHpe5xFg8xytc2w0XC6LcrFsBVMS4WlVaiGg8=
github.com/cloudflare/cloudflare-go v0.104.0 h1:R/lB0dZupaZbOgibAH/BRrkFbZ6Acn/WsKg2iX2xXuY=
github.com/cloudflare/cloudflare-go v0.104.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -147,8 +146,9 @@ github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
@@ -173,11 +173,11 @@ github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSe
github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw=
github.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=
github.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/go-acme/lego/v4 v4.17.4 h1:h0nePd3ObP6o7kAkndtpTzCw8shOZuWckNYeUQwo36Q=
github.com/go-acme/lego/v4 v4.17.4/go.mod h1:dU94SvPNqimEeb7EVilGGSnS0nU1O5Exir0pQ4QFL4U=
github.com/go-acme/lego/v4 v4.19.2 h1:Y8hrmMvWETdqzzkRly7m98xtPJJivWFsgWi8fcvZo+Y=
github.com/go-acme/lego/v4 v4.19.2/go.mod h1:wtDe3dDkmV4/oI2nydpNXSJpvV10J9RCyZ6MbYxNtlQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -214,15 +214,18 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -239,30 +242,26 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0 h1:e+8XbKB6IMn8A4OAyZccO4pYfB3s7bt6azNIPE7AnPg=
github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 h1:X3E16S6AUZsQKhJIQ5kNnylnp0GtSy2YhIbxfvDavtU=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
@@ -281,6 +280,7 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -309,8 +309,8 @@ github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxU
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -318,6 +318,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
@@ -333,8 +334,9 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.10.1 h1:cw+vsyfCJD8YObOVeqb93YErnlxwYMkNZ4rwN0G0AaA=
github.com/pocketbase/dbx v1.10.1/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/pocketbase v0.22.18 h1:yVckUhi5GDORqCb0BbtlvRB1CVxHY9HO9btEaeZHVJU=
@@ -377,19 +379,24 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
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/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
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/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.992/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/go.mod h1:c1j6YQ+vCbeA8kJ59Im4UnMd1GxovlpPBDhGZoewfn8=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/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.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002/go.mod h1:WdC0FYbqYhJwQ3kbqri6hVP5HAEp+rzX9FToItTAzUg=
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/go.mod h1:BcvC7ZPdSlhRggVq4J1ToJlgv8bmODIAuSo0naFZOLo=
github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
@@ -398,21 +405,27 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE=
go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
gocloud.dev v0.37.0 h1:XF1rN6R0qZI/9DYjN16Uy0durAmSlf58DHOcb28GPro=
@@ -423,13 +436,17 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -451,8 +468,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -462,8 +479,10 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@@ -472,11 +491,12 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -497,6 +517,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -508,8 +529,10 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -518,24 +541,28 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -549,8 +576,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -561,27 +588,28 @@ gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJ
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI=
google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=
google.golang.org/api v0.197.0 h1:x6CwqQLsFiA5JKAiGyGBjc2bNtHtLddhJCE2IKuhhcQ=
google.golang.org/api v0.197.0/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240722135656-d784300faade h1:lKFsS7wpngDgSCeFn7MoLy+wBDQZ1UQIJD4UNM1Qvkg=
google.golang.org/genproto v0.0.0-20240722135656-d784300faade/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a h1:hqK4+jJZXCU4pW7jsAdGOVFIfLHQeV7LaizZKnZ84HI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0=
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM=
google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View File

@@ -3,6 +3,7 @@ package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
"github.com/go-acme/lego/v4/providers/dns/alidns"
@@ -25,6 +26,7 @@ func (a *aliyun) Apply() (*Certificate, error) {
os.Setenv("ALICLOUD_ACCESS_KEY", access.AccessKeyId)
os.Setenv("ALICLOUD_SECRET_KEY", access.AccessKeySecret)
os.Setenv("ALICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := alidns.NewDNSProvider()
if err != nil {
return nil, err

View File

@@ -1,6 +1,7 @@
package applicant
import (
"certimate/internal/domain"
"certimate/internal/utils/app"
"crypto"
"crypto/ecdsa"
@@ -20,11 +21,12 @@ import (
)
const (
configTypeTencent = "tencent"
configTypeAliyun = "aliyun"
configTypeCloudflare = "cloudflare"
configTypeNamesilo = "namesilo"
configTypeGodaddy = "godaddy"
configTypeAliyun = "aliyun"
configTypeTencent = "tencent"
configTypeHuaweicloud = "huaweicloud"
configTypeCloudflare = "cloudflare"
configTypeNamesilo = "namesilo"
configTypeGodaddy = "godaddy"
)
const defaultSSLProvider = "letsencrypt"
@@ -45,6 +47,8 @@ var sslProviderUrls = map[string]string{
const defaultEmail = "536464346@qq.com"
const defaultTimeout = 60
type Certificate struct {
CertUrl string `json:"certUrl"`
CertStableUrl string `json:"certStableUrl"`
@@ -59,6 +63,7 @@ type ApplyOption struct {
Domain string `json:"domain"`
Access string `json:"access"`
Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"`
}
type MyUser struct {
@@ -82,8 +87,22 @@ type Applicant interface {
}
func Get(record *models.Record) (Applicant, error) {
access := record.ExpandedOne("access")
email := record.GetString("email")
if record.GetString("applyConfig") == "" {
return nil, errors.New("apply config is empty")
}
applyConfig := &domain.ApplyConfig{}
record.UnmarshalJSONField("applyConfig", applyConfig)
access, err := app.GetApp().Dao().FindRecordById("access", applyConfig.Access)
if err != nil {
return nil, fmt.Errorf("access record not found: %w", err)
}
email := applyConfig.Email
if email == "" {
email = defaultEmail
}
@@ -91,13 +110,16 @@ func Get(record *models.Record) (Applicant, error) {
Email: email,
Domain: record.GetString("domain"),
Access: access.GetString("config"),
Nameservers: record.GetString("nameservers"),
Nameservers: applyConfig.Nameservers,
Timeout: applyConfig.Timeout,
}
switch access.GetString("configType") {
case configTypeTencent:
return NewTencent(option), nil
case configTypeAliyun:
return NewAliyun(option), nil
case configTypeTencent:
return NewTencent(option), nil
case configTypeHuaweicloud:
return NewHuaweiCloud(option), nil
case configTypeCloudflare:
return NewCloudflare(option), nil
case configTypeNamesilo:
@@ -172,13 +194,7 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
}
myUser.Registration = reg
domains := []string{option.Domain}
// 如果是通配置符域名,把根域名也加入
if strings.HasPrefix(option.Domain, "*.") && len(strings.Split(option.Domain, ".")) == 3 {
rootDomain := strings.TrimPrefix(option.Domain, "*.")
domains = append(domains, rootDomain)
}
domains := strings.Split(option.Domain, ";")
request := certificate.ObtainRequest{
Domains: domains,

View File

@@ -3,6 +3,7 @@ package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
cf "github.com/go-acme/lego/v4/providers/dns/cloudflare"
@@ -23,6 +24,7 @@ func (c *cloudflare) Apply() (*Certificate, error) {
json.Unmarshal([]byte(c.option.Access), access)
os.Setenv("CLOUDFLARE_DNS_API_TOKEN", access.DnsApiToken)
os.Setenv("CLOUDFLARE_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", c.option.Timeout))
provider, err := cf.NewDNSProvider()
if err != nil {

View File

@@ -3,6 +3,7 @@ package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
godaddyProvider "github.com/go-acme/lego/v4/providers/dns/godaddy"
@@ -25,6 +26,7 @@ func (a *godaddy) Apply() (*Certificate, error) {
os.Setenv("GODADDY_API_KEY", access.ApiKey)
os.Setenv("GODADDY_API_SECRET", access.ApiSecret)
os.Setenv("GODADDY_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := godaddyProvider.NewDNSProvider()
if err != nil {

View File

@@ -0,0 +1,38 @@
package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
huaweicloudProvider "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
)
type huaweicloud struct {
option *ApplyOption
}
func NewHuaweiCloud(option *ApplyOption) Applicant {
return &huaweicloud{
option: option,
}
}
func (t *huaweicloud) Apply() (*Certificate, error) {
access := &domain.HuaweiCloudAccess{}
json.Unmarshal([]byte(t.option.Access), access)
os.Setenv("HUAWEICLOUD_REGION", access.Region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
os.Setenv("HUAWEICLOUD_ACCESS_KEY_ID", access.AccessKeyId)
os.Setenv("HUAWEICLOUD_SECRET_ACCESS_KEY", access.SecretAccessKey)
os.Setenv("HUAWEICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
dnsProvider, err := huaweicloudProvider.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(t.option, dnsProvider)
}

View File

@@ -3,6 +3,7 @@ package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
namesiloProvider "github.com/go-acme/lego/v4/providers/dns/namesilo"
@@ -24,6 +25,7 @@ func (a *namesilo) Apply() (*Certificate, error) {
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("NAMESILO_API_KEY", access.ApiKey)
os.Setenv("NAMESILO_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := namesiloProvider.NewDNSProvider()
if err != nil {

View File

@@ -3,6 +3,7 @@ package applicant
import (
"certimate/internal/domain"
"encoding/json"
"fmt"
"os"
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
@@ -25,6 +26,8 @@ func (t *tencent) Apply() (*Certificate, error) {
os.Setenv("TENCENTCLOUD_SECRET_ID", access.SecretId)
os.Setenv("TENCENTCLOUD_SECRET_KEY", access.SecretKey)
os.Setenv("TENCENTCLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
dnsProvider, err := tencentcloud.NewDNSProvider()
if err != nil {
return nil, err

View File

@@ -1,23 +1,16 @@
package deployer
import (
"certimate/internal/applicant"
"certimate/internal/domain"
"certimate/internal/utils/rand"
"context"
"encoding/json"
"errors"
"fmt"
"strings"
cas20200407 "github.com/alibabacloud-go/cas-20200407/v2/client"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)
type aliyun struct {
client *cas20200407.Client
client *oss.Client
option *DeployerOption
infos []string
}
@@ -47,160 +40,31 @@ func (a *aliyun) GetInfo() []string {
}
func (a *aliyun) Deploy(ctx context.Context) error {
// 查询有没有对应的资源
resource, err := a.resource()
if err != nil {
return err
}
a.infos = append(a.infos, toStr("查询对应的资源", resource))
// 查询有没有对应的联系人
contacts, err := a.contacts()
if err != nil {
return err
}
a.infos = append(a.infos, toStr("查询联系人", contacts))
// 上传证书
certId, err := a.uploadCert(&a.option.Certificate)
if err != nil {
return err
}
a.infos = append(a.infos, toStr("上传证书", certId))
// 部署证书
jobId, err := a.deploy(resource, certId, contacts)
if err != nil {
return err
}
a.infos = append(a.infos, toStr("创建部署证书任务", jobId))
// 等待部署成功
err = a.updateDeployStatus(*jobId)
if err != nil {
return err
}
// 部署成功后删除旧的证书
a.deleteCert(resource)
return nil
}
func (a *aliyun) updateDeployStatus(jobId int64) error {
// 查询部署状态
req := &cas20200407.UpdateDeploymentJobStatusRequest{
JobId: tea.Int64(jobId),
}
resp, err := a.client.UpdateDeploymentJobStatus(req)
if err != nil {
return err
}
a.infos = append(a.infos, toStr("查询对应的资源", resp))
return nil
}
func (a *aliyun) deleteCert(resource *cas20200407.ListCloudResourcesResponseBodyData) error {
// 查询有没有对应的资源
if resource.CertId == nil {
return nil
}
// 删除证书
_, err := a.client.DeleteUserCertificate(&cas20200407.DeleteUserCertificateRequest{
CertId: resource.CertId,
err := a.client.PutBucketCnameWithCertificate(getDeployString(a.option.DeployConfig, "bucket"), oss.PutBucketCname{
Cname: getDeployString(a.option.DeployConfig, "domain"),
CertificateConfiguration: &oss.CertificateConfiguration{
Certificate: a.option.Certificate.Certificate,
PrivateKey: a.option.Certificate.PrivateKey,
Force: true,
},
})
if err != nil {
return err
return fmt.Errorf("deploy aliyun oss error: %w", err)
}
return nil
}
func (a *aliyun) contacts() ([]*cas20200407.ListContactResponseBodyContactList, error) {
listContactRequest := &cas20200407.ListContactRequest{}
runtime := &util.RuntimeOptions{}
resp, err := a.client.ListContactWithOptions(listContactRequest, runtime)
func (a *aliyun) createClient(accessKeyId, accessKeySecret string) (*oss.Client, error) {
client, err := oss.New(
getDeployString(a.option.DeployConfig, "endpoint"),
accessKeyId,
accessKeySecret,
)
if err != nil {
return nil, err
return nil, fmt.Errorf("create aliyun client error: %w", err)
}
if resp.Body.TotalCount == nil {
return nil, errors.New("no contact found")
}
return resp.Body.ContactList, nil
}
func (a *aliyun) deploy(resource *cas20200407.ListCloudResourcesResponseBodyData, certId int64, contacts []*cas20200407.ListContactResponseBodyContactList) (*int64, error) {
contactIds := make([]string, 0, len(contacts))
for _, contact := range contacts {
contactIds = append(contactIds, fmt.Sprintf("%d", *contact.ContactId))
}
// 部署证书
createCloudResourceRequest := &cas20200407.CreateDeploymentJobRequest{
CertIds: tea.String(fmt.Sprintf("%d", certId)),
Name: tea.String(a.option.Domain + rand.RandStr(6)),
JobType: tea.String("user"),
ResourceIds: tea.String(fmt.Sprintf("%d", *resource.Id)),
ContactIds: tea.String(strings.Join(contactIds, ",")),
}
runtime := &util.RuntimeOptions{}
resp, err := a.client.CreateDeploymentJobWithOptions(createCloudResourceRequest, runtime)
if err != nil {
return nil, err
}
return resp.Body.JobId, nil
}
func (a *aliyun) uploadCert(cert *applicant.Certificate) (int64, error) {
uploadUserCertificateRequest := &cas20200407.UploadUserCertificateRequest{
Cert: &cert.Certificate,
Key: &cert.PrivateKey,
Name: tea.String(a.option.Domain + rand.RandStr(6)),
}
runtime := &util.RuntimeOptions{}
resp, err := a.client.UploadUserCertificateWithOptions(uploadUserCertificateRequest, runtime)
if err != nil {
return 0, err
}
return *resp.Body.CertId, nil
}
func (a *aliyun) createClient(accessKeyId, accessKeySecret string) (_result *cas20200407.Client, _err error) {
config := &openapi.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
config.Endpoint = tea.String("cas.aliyuncs.com")
_result = &cas20200407.Client{}
_result, _err = cas20200407.NewClient(config)
return _result, _err
}
func (a *aliyun) resource() (*cas20200407.ListCloudResourcesResponseBodyData, error) {
listCloudResourcesRequest := &cas20200407.ListCloudResourcesRequest{
CloudProduct: tea.String(a.option.Product),
Keyword: tea.String(a.option.Domain),
}
resp, err := a.client.ListCloudResources(listCloudResourcesRequest)
if err != nil {
return nil, err
}
if *resp.Body.Total == 0 {
return nil, errors.New("no resource found")
}
return resp.Body.Data[0], nil
return client, nil
}

View File

@@ -2,6 +2,7 @@ package deployer
import (
"certimate/internal/domain"
"certimate/internal/utils/rand"
"context"
"encoding/json"
"fmt"
@@ -46,9 +47,9 @@ func (a *AliyunCdn) GetInfo() []string {
func (a *AliyunCdn) Deploy(ctx context.Context) error {
certName := fmt.Sprintf("%s-%s", a.option.Domain, a.option.DomainId)
certName := fmt.Sprintf("%s-%s-%s", a.option.Domain, a.option.DomainId, rand.RandStr(6))
setCdnDomainSSLCertificateRequest := &cdn20180510.SetCdnDomainSSLCertificateRequest{
DomainName: tea.String(a.option.Domain),
DomainName: tea.String(getDeployString(a.option.DeployConfig, "domain")),
CertName: tea.String(certName),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),

View File

@@ -7,6 +7,7 @@ package deployer
import (
"certimate/internal/domain"
"certimate/internal/utils/rand"
"context"
"encoding/json"
"fmt"
@@ -51,9 +52,9 @@ func (a *AliyunEsa) GetInfo() []string {
func (a *AliyunEsa) Deploy(ctx context.Context) error {
certName := fmt.Sprintf("%s-%s", a.option.Domain, a.option.DomainId)
certName := fmt.Sprintf("%s-%s-%s", a.option.Domain, a.option.DomainId, rand.RandStr(6))
setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{
DomainName: tea.String(a.option.Domain),
DomainName: tea.String(getDeployString(a.option.DeployConfig, "domain")),
CertName: tea.String(certName),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),

View File

@@ -2,8 +2,8 @@ package deployer
import (
"certimate/internal/applicant"
"certimate/internal/domain"
"certimate/internal/utils/app"
"certimate/internal/utils/variables"
"context"
"encoding/json"
"errors"
@@ -30,6 +30,7 @@ type DeployerOption struct {
Product string `json:"product"`
Access string `json:"access"`
AceessRecord *models.Record `json:"-"`
DeployConfig domain.DeployConfig `json:"deployConfig"`
Certificate applicant.Certificate `json:"certificate"`
Variables map[string]string `json:"variables"`
}
@@ -42,52 +43,29 @@ type Deployer interface {
func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error) {
rs := make([]Deployer, 0)
if record.GetString("targetAccess") != "" {
singleDeployer, err := Get(record, cert)
if err != nil {
return nil, err
}
rs = append(rs, singleDeployer)
if record.GetString("deployConfig") == "" {
return rs, nil
}
if record.GetString("group") != "" {
group := record.ExpandedOne("group")
if errs := app.GetApp().Dao().ExpandRecord(group, []string{"access"}, nil); len(errs) > 0 {
errList := make([]error, 0)
for name, err := range errs {
errList = append(errList, fmt.Errorf("展开记录失败,%s: %w", name, err))
}
err := errors.Join(errList...)
return nil, err
}
records := group.ExpandedAll("access")
deployers, err := getByGroup(record, cert, records...)
if err != nil {
return nil, err
}
rs = append(rs, deployers...)
deployConfigs := make([]domain.DeployConfig, 0)
err := record.UnmarshalJSONField("deployConfig", &deployConfigs)
if err != nil {
return nil, fmt.Errorf("解析部署配置失败: %w", err)
}
return rs, nil
if len(deployConfigs) == 0 {
return rs, nil
}
}
for _, deployConfig := range deployConfigs {
func getByGroup(record *models.Record, cert *applicant.Certificate, accesses ...*models.Record) ([]Deployer, error) {
deployer, err := getWithDeployConfig(record, cert, deployConfig)
rs := make([]Deployer, 0)
for _, access := range accesses {
deployer, err := getWithAccess(record, cert, access)
if err != nil {
return nil, err
}
rs = append(rs, deployer)
}
@@ -95,15 +73,21 @@ func getByGroup(record *models.Record, cert *applicant.Certificate, accesses ...
}
func getWithAccess(record *models.Record, cert *applicant.Certificate, access *models.Record) (Deployer, error) {
func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, deployConfig domain.DeployConfig) (Deployer, error) {
access, err := app.GetApp().Dao().FindRecordById("access", deployConfig.Access)
if err != nil {
return nil, fmt.Errorf("access record not found: %w", err)
}
option := &DeployerOption{
DomainId: record.Id,
Domain: record.GetString("domain"),
Product: getProduct(record),
Product: getProduct(deployConfig.Type),
Access: access.GetString("config"),
AceessRecord: access,
Variables: variables.Parse2Map(record.GetString("variables")),
DeployConfig: deployConfig,
}
if cert != nil {
option.Certificate = *cert
@@ -114,7 +98,7 @@ func getWithAccess(record *models.Record, cert *applicant.Certificate, access *m
}
}
switch record.GetString("targetType") {
switch deployConfig.Type {
case targetAliyunOss:
return NewAliyun(option)
case targetAliyunCdn:
@@ -136,16 +120,8 @@ func getWithAccess(record *models.Record, cert *applicant.Certificate, access *m
return nil, errors.New("not implemented")
}
func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
access := record.ExpandedOne("targetAccess")
return getWithAccess(record, cert, access)
}
func getProduct(record *models.Record) string {
targetType := record.GetString("targetType")
rs := strings.Split(targetType, "-")
func getProduct(t string) string {
rs := strings.Split(t, "-")
if len(rs) < 2 {
return ""
}
@@ -159,3 +135,39 @@ func toStr(tag string, data any) string {
byts, _ := json.Marshal(data)
return tag + "" + string(byts)
}
func getDeployString(conf domain.DeployConfig, key string) string {
if _, ok := conf.Config[key]; !ok {
return ""
}
val, ok := conf.Config[key].(string)
if !ok {
return ""
}
return val
}
func getDeployVariables(conf domain.DeployConfig) map[string]string {
rs := make(map[string]string)
data, ok := conf.Config["variables"]
if !ok {
return rs
}
bts, _ := json.Marshal(data)
kvData := make([]domain.KV, 0)
if err := json.Unmarshal(bts, &kvData); err != nil {
return rs
}
for _, kv := range kvData {
rs[kv.Key] = kv.Value
}
return rs
}

View File

@@ -11,9 +11,6 @@ import (
)
type localAccess struct {
Command string `json:"command"`
CertPath string `json:"certPath"`
KeyPath string `json:"keyPath"`
}
type local struct {
@@ -41,18 +38,27 @@ func (l *local) Deploy(ctx context.Context) error {
if err := json.Unmarshal([]byte(l.option.Access), access); err != nil {
return err
}
preCommand := getDeployString(l.option.DeployConfig, "preCommand")
if preCommand != "" {
if err := execCmd(preCommand); err != nil {
return fmt.Errorf("执行前置命令失败: %w", err)
}
}
// 复制文件
if err := copyFile(l.option.Certificate.Certificate, access.CertPath); err != nil {
if err := copyFile(l.option.Certificate.Certificate, getDeployString(l.option.DeployConfig, "certPath")); err != nil {
return fmt.Errorf("复制证书失败: %w", err)
}
if err := copyFile(l.option.Certificate.PrivateKey, access.KeyPath); err != nil {
if err := copyFile(l.option.Certificate.PrivateKey, getDeployString(l.option.DeployConfig, "keyPath")); err != nil {
return fmt.Errorf("复制私钥失败: %w", err)
}
// 执行命令
if err := execCmd(access.Command); err != nil {
if err := execCmd(getDeployString(l.option.DeployConfig, "command")); err != nil {
return fmt.Errorf("执行命令失败: %w", err)
}

View File

@@ -78,7 +78,7 @@ func (q *qiuniu) Deploy(ctx context.Context) error {
}
func (q *qiuniu) enableHttps(certId string) error {
path := fmt.Sprintf("/domain/%s/sslize", q.option.Domain)
path := fmt.Sprintf("/domain/%s/sslize", getDeployString(q.option.DeployConfig, "domain"))
body := &modifyDomainCertReq{
CertID: certId,
@@ -104,7 +104,7 @@ type domainInfo struct {
}
func (q *qiuniu) getDomainInfo() (*domainInfo, error) {
path := fmt.Sprintf("/domain/%s", q.option.Domain)
path := fmt.Sprintf("/domain/%s", getDeployString(q.option.DeployConfig, "domain"))
res, err := q.req(qiniuGateway+path, http.MethodGet, nil)
if err != nil {
@@ -135,8 +135,8 @@ func (q *qiuniu) uploadCert() (string, error) {
path := "/sslcert"
body := &uploadCertReq{
Name: q.option.Domain,
CommonName: q.option.Domain,
Name: getDeployString(q.option.DeployConfig, "domain"),
CommonName: getDeployString(q.option.DeployConfig, "domain"),
Pri: q.option.Certificate.PrivateKey,
Ca: q.option.Certificate.Certificate,
}
@@ -166,7 +166,7 @@ type modifyDomainCertReq struct {
}
func (q *qiuniu) modifyDomainCert(certId string) error {
path := fmt.Sprintf("/domain/%s/httpsconf", q.option.Domain)
path := fmt.Sprintf("/domain/%s/httpsconf", getDeployString(q.option.DeployConfig, "domain"))
body := &modifyDomainCertReq{
CertID: certId,

View File

@@ -7,7 +7,6 @@ import (
"fmt"
"os"
xpath "path"
"strings"
"github.com/pkg/sftp"
sshPkg "golang.org/x/crypto/ssh"
@@ -24,9 +23,6 @@ type sshAccess struct {
Password string `json:"password"`
Key string `json:"key"`
Port string `json:"port"`
Command string `json:"command"`
CertPath string `json:"certPath"`
KeyPath string `json:"keyPath"`
}
func NewSSH(option *DeployerOption) (Deployer, error) {
@@ -49,15 +45,6 @@ func (s *ssh) Deploy(ctx context.Context) error {
if err := json.Unmarshal([]byte(s.option.Access), access); err != nil {
return err
}
// 将证书路径和命令中的变量替换为实际值
for k, v := range s.option.Variables {
key := fmt.Sprintf("${%s}", k)
access.CertPath = strings.ReplaceAll(access.CertPath, key, v)
access.KeyPath = strings.ReplaceAll(access.KeyPath, key, v)
access.Command = strings.ReplaceAll(access.Command, key, v)
}
// 连接
client, err := s.getClient(access)
if err != nil {
@@ -67,42 +54,52 @@ func (s *ssh) Deploy(ctx context.Context) error {
s.infos = append(s.infos, toStr("ssh连接成功", nil))
// 上传
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("failed to create session: %w", err)
// 执行前置命令
preCommand := getDeployString(s.option.DeployConfig, "preCommand")
if preCommand != "" {
err, stdout, stderr := s.sshExecCommand(client, preCommand)
if err != nil {
return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
}
}
defer session.Close()
s.infos = append(s.infos, toStr("ssh创建session成功", nil))
// 上传证书
if err := s.upload(client, s.option.Certificate.Certificate, access.CertPath); err != nil {
if err := s.upload(client, s.option.Certificate.Certificate, getDeployString(s.option.DeployConfig, "certPath")); err != nil {
return fmt.Errorf("failed to upload certificate: %w", err)
}
s.infos = append(s.infos, toStr("ssh上传证书成功", nil))
// 上传私钥
if err := s.upload(client, s.option.Certificate.PrivateKey, access.KeyPath); err != nil {
if err := s.upload(client, s.option.Certificate.PrivateKey, getDeployString(s.option.DeployConfig, "keyPath")); err != nil {
return fmt.Errorf("failed to upload private key: %w", err)
}
s.infos = append(s.infos, toStr("ssh上传私钥成功", nil))
// 执行命令
err, stdout, stderr := s.sshExecCommand(client, getDeployString(s.option.DeployConfig, "command"))
if err != nil {
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
}
s.infos = append(s.infos, toStr("ssh执行命令成功", stdout))
return nil
}
func (s *ssh) sshExecCommand(client *sshPkg.Client, command string) (error, string, string) {
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("failed to create ssh session: %w", err), "", ""
}
defer session.Close()
var stdoutBuf bytes.Buffer
session.Stdout = &stdoutBuf
var stderrBuf bytes.Buffer
session.Stderr = &stderrBuf
if err := session.Run(access.Command); err != nil {
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdoutBuf.String(), stderrBuf.String())
}
s.infos = append(s.infos, toStr("ssh执行命令成功", []string{stdoutBuf.String()}))
return nil
err = session.Run(command)
return err, stdoutBuf.String(), stderrBuf.String()
}
func (s *ssh) upload(client *sshPkg.Client, content, path string) error {

View File

@@ -4,9 +4,12 @@ import (
"certimate/internal/domain"
"certimate/internal/utils/rand"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
cdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
@@ -73,7 +76,7 @@ func (t *tencentCdn) uploadCert() (string, error) {
request.CertificatePublicKey = common.StringPtr(t.option.Certificate.Certificate)
request.CertificatePrivateKey = common.StringPtr(t.option.Certificate.PrivateKey)
request.Alias = common.StringPtr(t.option.Domain + "_" + rand.RandStr(6))
request.Repeatable = common.BoolPtr(true)
request.Repeatable = common.BoolPtr(false)
response, err := client.UploadCertificate(request)
if err != nil {
@@ -93,10 +96,24 @@ func (t *tencentCdn) deploy(certId string) error {
request := ssl.NewDeployCertificateInstanceRequest()
request.CertificateId = common.StringPtr(certId)
request.InstanceIdList = common.StringPtrs([]string{t.option.Domain})
request.ResourceType = common.StringPtr("cdn")
request.Status = common.Int64Ptr(1)
// 如果是泛域名就从cdn列表下获取SSL证书中的可用域名
domain := getDeployString(t.option.DeployConfig, "domain")
if strings.Contains(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{domain})
}
// 返回的resp是一个DeployCertificateInstanceResponse的实例与请求对象对应
resp, err := client.DeployCertificateInstance(request)
@@ -106,3 +123,26 @@ func (t *tencentCdn) deploy(certId string) error {
t.infos = append(t.infos, toStr("部署证书", resp.Response))
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
}

View File

@@ -14,9 +14,10 @@ type webhookAccess struct {
}
type hookData struct {
Domain string `json:"domain"`
Certificate string `json:"certificate"`
PrivateKey string `json:"privateKey"`
Domain string `json:"domain"`
Certificate string `json:"certificate"`
PrivateKey string `json:"privateKey"`
Variables map[string]string `json:"variables"`
}
type webhook struct {
@@ -50,6 +51,7 @@ func (w *webhook) Deploy(ctx context.Context) error {
Domain: w.option.Domain,
Certificate: w.option.Certificate.Certificate,
PrivateKey: w.option.Certificate.PrivateKey,
Variables: getDeployVariables(w.option.DeployConfig),
}
body, _ := json.Marshal(data)

View File

@@ -10,6 +10,12 @@ type TencentAccess struct {
SecretKey string `json:"secretKey"`
}
type HuaweiCloudAccess struct {
Region string `json:"region"`
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
}
type CloudflareAccess struct {
DnsApiToken string `json:"dnsApiToken"`
}

View File

@@ -0,0 +1,20 @@
package domain
type ApplyConfig struct {
Email string `json:"email"`
Access string `json:"access"`
Timeout int64 `json:"timeout"`
Nameservers string `json:"nameservers"`
}
type DeployConfig struct {
Id string `json:"id"`
Access string `json:"access"`
Type string `json:"type"`
Config map[string]any `json:"config"`
}
type KV struct {
Key string `json:"key"`
Value string `json:"value"`
}

View File

@@ -5,7 +5,6 @@ import (
"certimate/internal/deployer"
"certimate/internal/utils/app"
"context"
"errors"
"fmt"
"time"
@@ -41,18 +40,6 @@ func deploy(ctx context.Context, record *models.Record) error {
return err
}
history.record(checkPhase, "获取记录成功", nil)
if errs := app.GetApp().Dao().ExpandRecord(currRecord, []string{"access", "targetAccess", "group"}, nil); len(errs) > 0 {
errList := make([]error, 0)
for name, err := range errs {
errList = append(errList, fmt.Errorf("展开记录失败,%s: %w", name, err))
}
err = errors.Join(errList...)
app.GetApp().Logger().Error("展开记录失败", "err", err)
history.record(checkPhase, "获取授权信息失败", &RecordInfo{Err: err})
return err
}
history.record(checkPhase, "获取授权信息成功", nil)
cert := currRecord.GetString("certificate")
expiredAt := currRecord.GetDateTime("expiredAt").Time()
@@ -106,6 +93,13 @@ func deploy(ctx context.Context, record *models.Record) error {
return err
}
// 没有部署配置,也算成功
if len(deployers) == 0 {
history.record(deployPhase, "没有部署配置", &RecordInfo{Info: []string{"没有部署配置"}})
history.setWholeSuccess(true)
return nil
}
for _, deployer := range deployers {
if err = deployer.Deploy(ctx); err != nil {

View File

@@ -31,11 +31,12 @@ func Send(title, content string) error {
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) {

View File

@@ -0,0 +1,92 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models/schema"
)
func init() {
m.Register(func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e")
if err != nil {
return err
}
// update
edit_configType := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "hwy7m03o",
"name": "configType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun",
"tencent",
"huaweicloud",
"qiniu",
"cloudflare",
"namesilo",
"godaddy",
"local",
"ssh",
"webhook"
]
}
}`), edit_configType); err != nil {
return err
}
collection.Schema.AddField(edit_configType)
return dao.SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e")
if err != nil {
return err
}
// update
edit_configType := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "hwy7m03o",
"name": "configType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun",
"tencent",
"huaweicloud",
"ssh",
"webhook",
"cloudflare",
"qiniu",
"namesilo",
"godaddy",
"local"
]
}
}`), edit_configType); err != nil {
return err
}
collection.Schema.AddField(edit_configType)
return dao.SaveCollection(collection)
})
}

View File

@@ -0,0 +1,731 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
)
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `[
{
"id": "z3p974ainxjqlvs",
"created": "2024-07-29 10:02:48.334Z",
"updated": "2024-10-08 06:50:56.637Z",
"name": "domains",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "iuaerpl2",
"name": "domain",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "ukkhuw85",
"name": "email",
"type": "email",
"required": false,
"presentable": false,
"unique": false,
"options": {
"exceptDomains": null,
"onlyDomains": null
}
},
{
"system": false,
"id": "v98eebqq",
"name": "crontab",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "alc8e9ow",
"name": "access",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "4yzbv8urny5ja1e",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "topsc9bj",
"name": "certUrl",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "vixgq072",
"name": "certStableUrl",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "g3a3sza5",
"name": "privateKey",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "gr6iouny",
"name": "certificate",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "tk6vnrmn",
"name": "issuerCertificate",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "sjo6ibse",
"name": "csr",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "x03n1bkj",
"name": "expiredAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "srybpixz",
"name": "targetType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun-oss",
"aliyun-cdn",
"aliyun-dcdn",
"ssh",
"webhook",
"tencent-cdn",
"qiniu-cdn",
"local"
]
}
},
{
"system": false,
"id": "xy7yk0mb",
"name": "targetAccess",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "4yzbv8urny5ja1e",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "6jqeyggw",
"name": "enabled",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "hdsjcchf",
"name": "deployed",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "aiya3rev",
"name": "rightnow",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "ixznmhzc",
"name": "lastDeployedAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "ghtlkn5j",
"name": "lastDeployment",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "0a1o4e6sstp694f",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "zfnyj9he",
"name": "variables",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "1bspzuku",
"name": "group",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "teolp9pl72dxlxq",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "g65gfh7a",
"name": "nameservers",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "wwrzc3jo",
"name": "applyConfig",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
},
{
"system": false,
"id": "474iwy8r",
"name": "deployConfig",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_4ABO6EQ` + "`" + ` ON ` + "`" + `domains` + "`" + ` (` + "`" + `domain` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "4yzbv8urny5ja1e",
"created": "2024-07-29 10:04:39.685Z",
"updated": "2024-10-11 13:55:13.777Z",
"name": "access",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "geeur58v",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "iql7jpwx",
"name": "config",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
},
{
"system": false,
"id": "hwy7m03o",
"name": "configType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun",
"tencent",
"huaweicloud",
"qiniu",
"cloudflare",
"namesilo",
"godaddy",
"local",
"ssh",
"webhook"
]
}
},
{
"system": false,
"id": "lr33hiwg",
"name": "deleted",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "hsxcnlvd",
"name": "usage",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"apply",
"deploy",
"all"
]
}
},
{
"system": false,
"id": "c8egzzwj",
"name": "group",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "teolp9pl72dxlxq",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_wkoST0j` + "`" + ` ON ` + "`" + `access` + "`" + ` (` + "`" + `name` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "0a1o4e6sstp694f",
"created": "2024-07-30 06:30:27.801Z",
"updated": "2024-09-26 12:29:38.334Z",
"name": "deployments",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "farvlzk7",
"name": "domain",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "z3p974ainxjqlvs",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "jx5f69i3",
"name": "log",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
},
{
"system": false,
"id": "qbxdtg9q",
"name": "phase",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"check",
"apply",
"deploy"
]
}
},
{
"system": false,
"id": "rglrp1hz",
"name": "phaseSuccess",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "lt1g1blu",
"name": "deployedAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "wledpzgb",
"name": "wholeSuccess",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "_pb_users_auth_",
"created": "2024-09-12 13:09:54.234Z",
"updated": "2024-09-26 12:29:38.334Z",
"name": "users",
"type": "auth",
"system": false,
"schema": [
{
"system": false,
"id": "users_name",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "users_avatar",
"name": "avatar",
"type": "file",
"required": false,
"presentable": false,
"unique": false,
"options": {
"mimeTypes": [
"image/jpeg",
"image/png",
"image/svg+xml",
"image/gif",
"image/webp"
],
"thumbs": null,
"maxSelect": 1,
"maxSize": 5242880,
"protected": false
}
}
],
"indexes": [],
"listRule": "id = @request.auth.id",
"viewRule": "id = @request.auth.id",
"createRule": "",
"updateRule": "id = @request.auth.id",
"deleteRule": "id = @request.auth.id",
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"onlyVerified": false,
"requireEmail": false
}
},
{
"id": "dy6ccjb60spfy6p",
"created": "2024-09-12 23:12:21.677Z",
"updated": "2024-09-26 12:29:38.334Z",
"name": "settings",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "1tcmdsdf",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "f9wyhypi",
"name": "content",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_RO7X9Vw` + "`" + ` ON ` + "`" + `settings` + "`" + ` (` + "`" + `name` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "teolp9pl72dxlxq",
"created": "2024-09-13 12:51:05.611Z",
"updated": "2024-09-26 12:29:38.334Z",
"name": "access_groups",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "7sajiv6i",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "xp8admif",
"name": "access",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "4yzbv8urny5ja1e",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": null,
"displayFields": null
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_RgRXp0R` + "`" + ` ON ` + "`" + `access_groups` + "`" + ` (` + "`" + `name` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
}
]`
collections := []*models.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
return err
}
return daos.New(db).ImportCollections(collections, true, nil)
}, func(db dbx.Builder) error {
return nil
})
}

View File

File diff suppressed because one or more lines are too long

1
ui/dist/assets/index-CV_7sKTK.css vendored Normal file
View File

File diff suppressed because one or more lines are too long

324
ui/dist/assets/index-DipHpsma.js vendored Normal file
View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<svg t="1728551595312" class="icon" viewBox="0 0 1027 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4252" width="200" height="200"><path d="M378.88 143.36c20.48-6.826667 40.96-10.24 61.44-13.653333 23.893333 30.72 30.72 71.68 40.96 109.226666 6.826667 40.96 13.653333 81.92 13.653333 122.88 6.826667 27.306667 3.413333 58.026667 3.413334 85.333334s-3.413333 51.2 0 78.506666c0 37.546667-3.413333 71.68-3.413334 109.226667-6.826667 23.893333 0 47.786667-6.826666 71.68-10.24-3.413333-13.653333-13.653333-17.066667-20.48-40.96-61.44-78.506667-122.88-112.64-187.733333-34.133333-68.266667-68.266667-136.533333-71.68-215.04-6.826667-61.44 34.133333-119.466667 92.16-139.946667z m211.626667-10.24c6.826667-3.413333 10.24 0 17.066666 0 27.306667 6.826667 58.026667 10.24 81.92 27.306667s44.373333 40.96 51.2 68.266666c10.24 34.133333 6.826667 71.68 0 105.813334-10.24 44.373333-30.72 85.333333-51.2 126.293333-6.826667 13.653333-13.653333 23.893333-20.48 37.546667-10.24 23.893333-27.306667 47.786667-40.96 71.68-23.893333 40.96-47.786667 75.093333-71.68 116.053333-3.413333 3.413333-6.826667 13.653333-13.653333 6.826667-3.413333-40.96-6.826667-81.92-10.24-126.293334-6.826667-54.613333-3.413333-105.813333-3.413333-160.426666 3.413333-34.133333 3.413333-71.68 6.826666-105.813334 6.826667-40.96 13.653333-85.333333 27.306667-126.293333 13.653333-10.24 13.653333-30.72 27.306667-40.96zM160.426667 266.24c3.413333 0 6.826667 6.826667 10.24 10.24 98.986667 129.706667 187.733333 266.24 259.413333 413.013333 6.826667 10.24 13.653333 23.893333 13.653333 37.546667-13.653333-3.413333-23.893333-10.24-34.133333-17.066667-64.853333-34.133333-129.706667-71.68-194.56-109.226666-23.893333-17.066667-47.786667-34.133333-68.266667-51.2-40.96-27.306667-68.266667-78.506667-64.853333-129.706667 3.413333-61.44 37.546667-112.64 78.506667-153.6z m706.56 0h6.826666c17.066667 23.893333 40.96 47.786667 54.613334 75.093333 13.653333 23.893333 20.48 54.613333 23.893333 81.92 0 30.72-10.24 64.853333-34.133333 88.746667-13.653333 13.653333-23.893333 27.306667-40.96 37.546667-78.506667 61.44-163.84 109.226667-252.586667 153.6-13.653333 6.826667-23.893333 17.066667-40.96 17.066666 3.413333-17.066667 13.653333-34.133333 20.48-47.786666 58.026667-119.466667 129.706667-232.106667 208.213333-341.333334 17.066667-17.066667 37.546667-40.96 54.613334-64.853333z m-856.746667 273.066667c3.413333-3.413333 0-10.24 6.826667-13.653334 10.24 3.413333 20.48 10.24 27.306666 13.653334 122.88 68.266667 245.76 136.533333 365.226667 211.626666 3.413333 3.413333 6.826667 6.826667 6.826667 10.24H180.906667c-47.786667 0-92.16-20.48-126.293334-54.613333-27.306667-30.72-51.2-71.68-54.613333-112.64 6.826667-17.066667 3.413333-34.133333 10.24-54.613333z m983.04-3.413334c6.826667-3.413333 17.066667-10.24 23.893333-6.826666 0 17.066667 6.826667 37.546667 6.826667 54.613333-3.413333 23.893333-3.413333 44.373333-13.653333 64.853333-6.826667 17.066667-17.066667 37.546667-30.72 51.2-17.066667 13.653333-27.306667 30.72-47.786667 40.96-20.48 17.066667-51.2 20.48-75.093333 23.893334h-245.76c3.413333-3.413333 3.413333-6.826667 6.826666-10.24 122.88-78.506667 249.173333-150.186667 375.466667-218.453334zM184.32 798.72c44.373333-3.413333 88.746667 0 133.12-6.826667 30.72 0 64.853333-3.413333 95.573333 0-6.826667 13.653333-23.893333 20.48-34.133333 27.306667-34.133333 23.893333-68.266667 44.373333-105.813333 61.44s-81.92 10.24-112.64-13.653333c-23.893333-17.066667-44.373333-44.373333-58.026667-68.266667h81.92z m433.493333-6.826667c30.72-3.413333 61.44 0 95.573334 0 40.96 3.413333 85.333333 0 129.706666 6.826667 30.72 3.413333 61.44 0 88.746667 3.413333-10.24 20.48-27.306667 40.96-44.373333 58.026667-27.306667 27.306667-68.266667 40.96-105.813334 34.133333-34.133333-10.24-61.44-30.72-92.16-47.786666-17.066667-10.24-30.72-20.48-47.786666-30.72-10.24-10.24-17.066667-13.653333-23.893334-23.893334z" fill="#C71F1E" p-id="4253"></path></svg>

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-BnLdUh-0.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Djc_JtNf.css">
<script type="module" crossorigin src="/assets/index-DipHpsma.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CV_7sKTK.css">
</head>
<body class="bg-background">
<div id="root"></div>

179
ui/package-lock.json generated
View File

@@ -27,13 +27,19 @@
"@radix-ui/react-tooltip": "^1.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.6.1",
"immer": "^10.1.1",
"jszip": "^3.10.1",
"lucide-react": "^0.417.0",
"moment": "^2.30.1",
"nanoid": "^5.0.7",
"pocketbase": "^0.21.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.1",
"react-i18next": "^15.0.2",
"react-router-dom": "^6.25.1",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
@@ -382,6 +388,17 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.25.6",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.25.6.tgz",
"integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.24.7",
"resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.24.7.tgz",
@@ -2959,6 +2976,14 @@
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/cross-fetch": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/cross-fetch/-/cross-fetch-4.0.0.tgz",
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
"dependencies": {
"node-fetch": "^2.6.12"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -3697,6 +3722,52 @@
"node": ">= 0.4"
}
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/i18next": {
"version": "23.15.1",
"resolved": "https://registry.npmmirror.com/i18next/-/i18next-23.15.1.tgz",
"integrity": "sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz",
"integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-http-backend": {
"version": "2.6.1",
"resolved": "https://registry.npmmirror.com/i18next-http-backend/-/i18next-http-backend-2.6.1.tgz",
"integrity": "sha512-rCilMAnlEQNeKOZY1+x8wLM5IpYOj10guGvEpeC59tNjj6MMreLIjIW8D1RclhD3ifLwn6d/Y9HEM1RUE6DSog==",
"dependencies": {
"cross-fetch": "4.0.0"
}
},
"node_modules/ignore": {
"version": "5.3.1",
"resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.1.tgz",
@@ -3711,6 +3782,15 @@
"resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -4090,9 +4170,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "5.0.7",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-5.0.7.tgz",
"integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==",
"funding": [
{
"type": "github",
@@ -4100,10 +4180,10 @@
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
"node": "^18 || >=20"
}
},
"node_modules/natural-compare": {
@@ -4112,6 +4192,25 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.18.tgz",
@@ -4473,6 +4572,23 @@
"resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -4553,6 +4669,27 @@
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-i18next": {
"version": "15.0.2",
"resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-15.0.2.tgz",
"integrity": "sha512-z0W3/RES9Idv3MmJUcf0mDNeeMOUXe+xoL0kPfQPbDoZHmni/XsIoq5zgT2MCFUiau283GuBUK578uD/mkAbLQ==",
"dependencies": {
"@babel/runtime": "^7.25.0",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 23.2.3",
"react": ">= 16.8.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -4692,6 +4829,11 @@
"node": ">=8.10.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz",
@@ -5140,6 +5282,11 @@
"node": ">=8.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/ts-api-utils": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
@@ -5361,6 +5508,28 @@
}
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",

View File

@@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --host",
"build": "tsc -b && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
@@ -29,13 +29,19 @@
"@radix-ui/react-tooltip": "^1.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.6.1",
"immer": "^10.1.1",
"jszip": "^3.10.1",
"lucide-react": "^0.417.0",
"moment": "^2.30.1",
"nanoid": "^5.0.7",
"pocketbase": "^0.21.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.1",
"react-i18next": "^15.0.2",
"react-router-dom": "^6.25.1",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",

View File

@@ -0,0 +1 @@
<svg t="1728551595312" class="icon" viewBox="0 0 1027 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4252" width="200" height="200"><path d="M378.88 143.36c20.48-6.826667 40.96-10.24 61.44-13.653333 23.893333 30.72 30.72 71.68 40.96 109.226666 6.826667 40.96 13.653333 81.92 13.653333 122.88 6.826667 27.306667 3.413333 58.026667 3.413334 85.333334s-3.413333 51.2 0 78.506666c0 37.546667-3.413333 71.68-3.413334 109.226667-6.826667 23.893333 0 47.786667-6.826666 71.68-10.24-3.413333-13.653333-13.653333-17.066667-20.48-40.96-61.44-78.506667-122.88-112.64-187.733333-34.133333-68.266667-68.266667-136.533333-71.68-215.04-6.826667-61.44 34.133333-119.466667 92.16-139.946667z m211.626667-10.24c6.826667-3.413333 10.24 0 17.066666 0 27.306667 6.826667 58.026667 10.24 81.92 27.306667s44.373333 40.96 51.2 68.266666c10.24 34.133333 6.826667 71.68 0 105.813334-10.24 44.373333-30.72 85.333333-51.2 126.293333-6.826667 13.653333-13.653333 23.893333-20.48 37.546667-10.24 23.893333-27.306667 47.786667-40.96 71.68-23.893333 40.96-47.786667 75.093333-71.68 116.053333-3.413333 3.413333-6.826667 13.653333-13.653333 6.826667-3.413333-40.96-6.826667-81.92-10.24-126.293334-6.826667-54.613333-3.413333-105.813333-3.413333-160.426666 3.413333-34.133333 3.413333-71.68 6.826666-105.813334 6.826667-40.96 13.653333-85.333333 27.306667-126.293333 13.653333-10.24 13.653333-30.72 27.306667-40.96zM160.426667 266.24c3.413333 0 6.826667 6.826667 10.24 10.24 98.986667 129.706667 187.733333 266.24 259.413333 413.013333 6.826667 10.24 13.653333 23.893333 13.653333 37.546667-13.653333-3.413333-23.893333-10.24-34.133333-17.066667-64.853333-34.133333-129.706667-71.68-194.56-109.226666-23.893333-17.066667-47.786667-34.133333-68.266667-51.2-40.96-27.306667-68.266667-78.506667-64.853333-129.706667 3.413333-61.44 37.546667-112.64 78.506667-153.6z m706.56 0h6.826666c17.066667 23.893333 40.96 47.786667 54.613334 75.093333 13.653333 23.893333 20.48 54.613333 23.893333 81.92 0 30.72-10.24 64.853333-34.133333 88.746667-13.653333 13.653333-23.893333 27.306667-40.96 37.546667-78.506667 61.44-163.84 109.226667-252.586667 153.6-13.653333 6.826667-23.893333 17.066667-40.96 17.066666 3.413333-17.066667 13.653333-34.133333 20.48-47.786666 58.026667-119.466667 129.706667-232.106667 208.213333-341.333334 17.066667-17.066667 37.546667-40.96 54.613334-64.853333z m-856.746667 273.066667c3.413333-3.413333 0-10.24 6.826667-13.653334 10.24 3.413333 20.48 10.24 27.306666 13.653334 122.88 68.266667 245.76 136.533333 365.226667 211.626666 3.413333 3.413333 6.826667 6.826667 6.826667 10.24H180.906667c-47.786667 0-92.16-20.48-126.293334-54.613333-27.306667-30.72-51.2-71.68-54.613333-112.64 6.826667-17.066667 3.413333-34.133333 10.24-54.613333z m983.04-3.413334c6.826667-3.413333 17.066667-10.24 23.893333-6.826666 0 17.066667 6.826667 37.546667 6.826667 54.613333-3.413333 23.893333-3.413333 44.373333-13.653333 64.853333-6.826667 17.066667-17.066667 37.546667-30.72 51.2-17.066667 13.653333-27.306667 30.72-47.786667 40.96-20.48 17.066667-51.2 20.48-75.093333 23.893334h-245.76c3.413333-3.413333 3.413333-6.826667 6.826666-10.24 122.88-78.506667 249.173333-150.186667 375.466667-218.453334zM184.32 798.72c44.373333-3.413333 88.746667 0 133.12-6.826667 30.72 0 64.853333-3.413333 95.573333 0-6.826667 13.653333-23.893333 20.48-34.133333 27.306667-34.133333 23.893333-68.266667 44.373333-105.813333 61.44s-81.92 10.24-112.64-13.653333c-23.893333-17.066667-44.373333-44.373333-58.026667-68.266667h81.92z m433.493333-6.826667c30.72-3.413333 61.44 0 95.573334 0 40.96 3.413333 85.333333 0 129.706666 6.826667 30.72 3.413333 61.44 0 88.746667 3.413333-10.24 20.48-27.306667 40.96-44.373333 58.026667-27.306667 27.306667-68.266667 40.96-105.813334 34.133333-34.133333-10.24-61.44-30.72-92.16-47.786666-17.066667-10.24-30.72-20.48-47.786666-30.72-10.24-10.24-17.066667-13.653333-23.893334-23.893334z" fill="#C71F1E" p-id="4253"></path></svg>

View File

@@ -0,0 +1,33 @@
import { Languages } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export default function LocaleToggle() {
const { i18n } = useTranslation();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Languages className="h-[1.2rem] w-[1.2rem] dark:text-white" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{Object.keys(i18n.store.data).map((key) => (
<DropdownMenuItem onClick={() => i18n.changeLanguage(key)}>
{i18n.store.data[key].name as string}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,4 +1,5 @@
import { Moon, Sun } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
@@ -11,6 +12,7 @@ import { useTheme } from "./ThemeProvider";
export function ThemeToggle() {
const { setTheme } = useTheme();
const { t } = useTranslation();
return (
<DropdownMenu>
@@ -23,13 +25,13 @@ export function ThemeToggle() {
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
{t("common.theme.light")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
{t("common.theme.dark")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
{t("common.theme.system")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -14,7 +15,12 @@ import {
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { Access, accessFormType, AliyunConfig, getUsageByConfigType } from "@/domain/access";
import {
Access,
accessFormType,
AliyunConfig,
getUsageByConfigType,
} from "@/domain/access";
import { save } from "@/repository/access";
import { useConfig } from "@/providers/config";
@@ -23,18 +29,30 @@ import { PbErrorData } from "@/domain/base";
const AccessAliyunForm = ({
data,
op,
onAfterReq,
}: {
data?: Access;
op: "add" | "edit" | "copy";
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z
.string()
.min(1, "access.authorization.form.name.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
configType: accessFormType,
accessKeyId: z.string().min(1).max(64),
accessSecretId: z.string().min(1).max(64),
accessKeyId: z
.string()
.min(1, "access.authorization.form.access_key_id.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
accessSecretId: z
.string()
.min(1, "access.authorization.form.access_key_secret.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
});
let config: AliyunConfig = {
@@ -47,7 +65,7 @@ const AccessAliyunForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || "",
configType: "aliyun",
accessKeyId: config.accessKeyId,
accessSecretId: config.accessKeySecret,
@@ -67,6 +85,7 @@ const AccessAliyunForm = ({
};
try {
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
@@ -74,10 +93,11 @@ const AccessAliyunForm = ({
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id) {
if (data.id && op == "edit") {
updateAccess(req);
return;
}
console.log(req);
addAccess(req);
} catch (e) {
const err = e as ClientResponseError;
@@ -111,9 +131,12 @@ const AccessAliyunForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.name.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input
placeholder={t("access.authorization.form.name.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -126,7 +149,7 @@ const AccessAliyunForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -141,7 +164,7 @@ const AccessAliyunForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -156,9 +179,12 @@ const AccessAliyunForm = ({
name="accessKeyId"
render={({ field }) => (
<FormItem>
<FormLabel>AccessKeyId</FormLabel>
<FormLabel>{t("access.authorization.form.access_key_id.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入AccessKeyId" {...field} />
<Input
placeholder={t("access.authorization.form.access_key_id.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -171,9 +197,12 @@ const AccessAliyunForm = ({
name="accessSecretId"
render={({ field }) => (
<FormItem>
<FormLabel>AccessKeySecret</FormLabel>
<FormLabel>{t("access.authorization.form.access_key_secret.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入AccessKeySecret" {...field} />
<Input
placeholder={t("access.authorization.form.access_key_secret.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -184,7 +213,7 @@ const AccessAliyunForm = ({
<FormMessage />
<div className="flex justify-end">
<Button type="submit"></Button>
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>

View File

@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -14,7 +15,12 @@ import {
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { Access, accessFormType, CloudflareConfig, getUsageByConfigType } from "@/domain/access";
import {
Access,
accessFormType,
CloudflareConfig,
getUsageByConfigType,
} from "@/domain/access";
import { save } from "@/repository/access";
import { useConfig } from "@/providers/config";
import { ClientResponseError } from "pocketbase";
@@ -22,17 +28,26 @@ import { PbErrorData } from "@/domain/base";
const AccessCloudflareForm = ({
data,
op,
onAfterReq,
}: {
data?: Access;
op: "add" | "edit" | "copy";
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z
.string()
.min(1, "access.authorization.form.name.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
configType: accessFormType,
dnsApiToken: z.string().min(1).max(64),
dnsApiToken: z
.string()
.min(1, "access.authorization.form.cloud_dns_api_token.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
});
let config: CloudflareConfig = {
@@ -44,7 +59,7 @@ const AccessCloudflareForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || "",
configType: "cloudflare",
dnsApiToken: config.dnsApiToken,
},
@@ -63,6 +78,7 @@ const AccessCloudflareForm = ({
};
try {
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
@@ -70,7 +86,7 @@ const AccessCloudflareForm = ({
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id) {
if (data.id && op == "edit") {
updateAccess(req);
return;
}
@@ -106,9 +122,12 @@ const AccessCloudflareForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.name.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input
placeholder={t("access.authorization.form.name.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -121,7 +140,7 @@ const AccessCloudflareForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -136,7 +155,7 @@ const AccessCloudflareForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -151,9 +170,14 @@ const AccessCloudflareForm = ({
name="dnsApiToken"
render={({ field }) => (
<FormItem>
<FormLabel>CLOUD_DNS_API_TOKEN</FormLabel>
<FormLabel>{t("access.authorization.form.cloud_dns_api_token.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入CLOUD_DNS_API_TOKEN" {...field} />
<Input
placeholder={t(
"access.authorization.form.cloud_dns_api_token.placeholder"
)}
{...field}
/>
</FormControl>
<FormMessage />
@@ -162,7 +186,7 @@ const AccessCloudflareForm = ({
/>
<div className="flex justify-end">
<Button type="submit"></Button>
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>

View File

@@ -8,17 +8,12 @@ import {
import { ScrollArea } from "@/components/ui/scroll-area";
import { useState } from "react";
import AccessTencentForm from "./AccessTencentForm";
import { Label } from "../ui/label";
import { useTranslation } from "react-i18next";
import { Access, accessTypeMap } from "@/domain/access";
import AccessAliyunForm from "./AccessAliyunForm";
import { cn } from "@/lib/utils";
import AccessSSHForm from "./AccessSSHForm";
import WebhookForm from "./AccessWebhookFrom";
import { Label } from "../ui/label";
import {
Select,
SelectContent,
@@ -28,14 +23,19 @@ import {
SelectTrigger,
SelectValue,
} from "../ui/select";
import AccessCloudflareForm from "./AccessCloudflareForm";
import AccessAliyunForm from "./AccessAliyunForm";
import AccessTencentForm from "./AccessTencentForm";
import AccessHuaweicloudForm from "./AccessHuaweicloudForm";
import AccessQiniuForm from "./AccessQiniuForm";
import AccessCloudflareForm from "./AccessCloudflareForm";
import AccessNamesiloForm from "./AccessNamesiloForm";
import AccessGodaddyFrom from "./AccessGodaddyForm";
import AccessLocalForm from "./AccessLocalForm";
import AccessSSHForm from "./AccessSSHForm";
import AccessWebhookForm from "./AccessWebhookFrom";
type TargetConfigEditProps = {
op: "add" | "edit";
op: "add" | "edit" | "copy";
className?: string;
trigger: React.ReactNode;
data?: Access;
@@ -47,6 +47,7 @@ export function AccessEdit({
className,
}: TargetConfigEditProps) {
const [open, setOpen] = useState(false);
const { t } = useTranslation();
const typeKeys = Array.from(accessTypeMap.keys());
@@ -54,50 +55,33 @@ export function AccessEdit({
let form = <> </>;
switch (configType) {
case "tencent":
form = (
<AccessTencentForm
data={data}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
case "aliyun":
form = (
<AccessAliyunForm
data={data}
op={op}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
case "ssh":
case "tencent":
form = (
<AccessSSHForm
<AccessTencentForm
data={data}
op={op}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
case "webhook":
case "huaweicloud":
form = (
<WebhookForm
data={data}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
case "cloudflare":
form = (
<AccessCloudflareForm
<AccessHuaweicloudForm
data={data}
op={op}
onAfterReq={() => {
setOpen(false);
}}
@@ -108,6 +92,18 @@ export function AccessEdit({
form = (
<AccessQiniuForm
data={data}
op={op}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
case "cloudflare":
form = (
<AccessCloudflareForm
data={data}
op={op}
onAfterReq={() => {
setOpen(false);
}}
@@ -118,6 +114,7 @@ export function AccessEdit({
form = (
<AccessNamesiloForm
data={data}
op={op}
onAfterReq={() => {
setOpen(false);
}}
@@ -128,6 +125,7 @@ export function AccessEdit({
form = (
<AccessGodaddyFrom
data={data}
op={op}
onAfterReq={() => {
setOpen(false);
}}
@@ -138,6 +136,29 @@ export function AccessEdit({
form = (
<AccessLocalForm
data={data}
op={op}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
case "ssh":
form = (
<AccessSSHForm
data={data}
op={op}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
case "webhook":
form = (
<AccessWebhookForm
data={data}
op={op}
onAfterReq={() => {
setOpen(false);
}}
@@ -157,25 +178,30 @@ export function AccessEdit({
</DialogTrigger>
<DialogContent className="sm:max-w-[600px] w-full dark:text-stone-200">
<DialogHeader>
<DialogTitle>{op == "add" ? "添加" : "编辑"}</DialogTitle>
<DialogTitle>
{op == "add"
? t("access.authorization.add")
: op == "edit"
? t("access.authorization.edit")
: t("access.authorization.copy")}
</DialogTitle>
</DialogHeader>
<ScrollArea className="max-h-[80vh]">
<div className="container py-3">
<Label></Label>
<Label>{t("access.authorization.form.type.label")}</Label>
<Select
onValueChange={(val) => {
console.log(val);
setConfigType(val);
}}
defaultValue={configType}
>
<SelectTrigger className="mt-3">
<SelectValue placeholder="请选择服务商" />
<SelectValue placeholder={t("access.authorization.form.type.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
<SelectLabel>{t("access.authorization.form.type.list")}</SelectLabel>
{typeKeys.map((key) => (
<SelectItem value={key} key={key}>
<div
@@ -188,7 +214,7 @@ export function AccessEdit({
src={accessTypeMap.get(key)?.[1]}
className="h-6 w-6"
/>
<div>{accessTypeMap.get(key)?.[0]}</div>
<div>{t(accessTypeMap.get(key)?.[0] || "")}</div>
</div>
</SelectItem>
))}

View File

@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -27,18 +28,30 @@ import { PbErrorData } from "@/domain/base";
const AccessGodaddyFrom = ({
data,
op,
onAfterReq,
}: {
data?: Access;
op: "add" | "edit" | "copy";
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z
.string()
.min(1, "access.authorization.form.name.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
configType: accessFormType,
apiKey: z.string().min(1).max(64),
apiSecret: z.string().min(1).max(64),
apiKey: z
.string()
.min(1, "access.authorization.form.godaddy_api_key.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
apiSecret: z
.string()
.min(1, "access.authorization.form.godaddy_api_secret.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
});
let config: GodaddyConfig = {
@@ -51,7 +64,7 @@ const AccessGodaddyFrom = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || "",
configType: "godaddy",
apiKey: config.apiKey,
apiSecret: config.apiSecret,
@@ -72,6 +85,7 @@ const AccessGodaddyFrom = ({
};
try {
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
@@ -79,7 +93,7 @@ const AccessGodaddyFrom = ({
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id) {
if (data.id && op == "edit") {
updateAccess(req);
return;
}
@@ -115,9 +129,12 @@ const AccessGodaddyFrom = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.name.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input
placeholder={t("access.authorization.form.name.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -130,7 +147,7 @@ const AccessGodaddyFrom = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -145,7 +162,7 @@ const AccessGodaddyFrom = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -160,9 +177,12 @@ const AccessGodaddyFrom = ({
name="apiKey"
render={({ field }) => (
<FormItem>
<FormLabel>GODADDY_API_KEY</FormLabel>
<FormLabel>{t("access.authorization.form.godaddy_api_key.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入GODADDY_API_KEY" {...field} />
<Input
placeholder={t("access.authorization.form.godaddy_api_key.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -175,9 +195,14 @@ const AccessGodaddyFrom = ({
name="apiSecret"
render={({ field }) => (
<FormItem>
<FormLabel>GODADDY_API_SECRET</FormLabel>
<FormLabel>{t("access.authorization.form.godaddy_api_secret.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入GODADDY_API_SECRET" {...field} />
<Input
placeholder={t(
"access.authorization.form.godaddy_api_secret.placeholder"
)}
{...field}
/>
</FormControl>
<FormMessage />
@@ -186,7 +211,7 @@ const AccessGodaddyFrom = ({
/>
<div className="flex justify-end">
<Button type="submit"></Button>
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>

View File

@@ -25,6 +25,7 @@ import { update } from "@/repository/access_group";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
import { useState } from "react";
import { useTranslation } from "react-i18next";
type AccessGroupEditProps = {
className?: string;
@@ -35,9 +36,13 @@ const AccessGroupEdit = ({ className, trigger }: AccessGroupEditProps) => {
const { reloadAccessGroups } = useConfig();
const [open, setOpen] = useState(false);
const { t } = useTranslation();
const formSchema = z.object({
name: z.string().min(1).max(64),
name: z
.string()
.min(1, "access.group.form.name.errmsg.empty")
.max(64, t("common.errmsg.string_max", { max: 64 })),
});
const form = useForm<z.infer<typeof formSchema>>({
@@ -78,14 +83,13 @@ const AccessGroupEdit = ({ className, trigger }: AccessGroupEditProps) => {
</DialogTrigger>
<DialogContent className="sm:max-w-[600px] w-full dark:text-stone-200">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogTitle>{t("access.group.add")}</DialogTitle>
</DialogHeader>
<div className="container py-3">
<Form {...form}>
<form
onSubmit={(e) => {
console.log(e);
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
@@ -96,9 +100,13 @@ const AccessGroupEdit = ({ className, trigger }: AccessGroupEditProps) => {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t("access.group.form.name.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入组名" {...field} type="text" />
<Input
placeholder={t("access.group.form.name.errmsg.empty")}
{...field}
type="text"
/>
</FormControl>
<FormMessage />
@@ -107,7 +115,7 @@ const AccessGroupEdit = ({ className, trigger }: AccessGroupEditProps) => {
/>
<div className="flex justify-end">
<Button type="submit"></Button>
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>

View File

@@ -29,6 +29,7 @@ import { Group } from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const AccessGroupList = () => {
const {
@@ -39,8 +40,7 @@ const AccessGroupList = () => {
const { toast } = useToast();
const navigate = useNavigate();
const { t } = useTranslation();
const handleRemoveClick = async (id: string) => {
try {
@@ -48,7 +48,7 @@ const AccessGroupList = () => {
reloadAccessGroups();
} catch (e) {
toast({
title: "删除失败",
title: t("common.delete.failed.message"),
description: getErrMessage(e),
variant: "destructive",
});
@@ -69,10 +69,10 @@ const AccessGroupList = () => {
</span>
<div className="text-center text-sm text-muted-foreground mt-3">
{t("access.group.domains.nodata")}
</div>
<AccessGroupEdit
trigger={<Button></Button>}
trigger={<Button>{t("access.group.add")}</Button>}
className="mt-3"
/>
</div>
@@ -86,9 +86,11 @@ const AccessGroupList = () => {
<CardHeader>
<CardTitle>{accessGroup.name}</CardTitle>
<CardDescription>
{accessGroup.expand ? accessGroup.expand.access.length : 0}
{t("access.group.total", {
total: accessGroup.expand
? accessGroup.expand.access.length
: 0,
})}
</CardDescription>
</CardHeader>
<CardContent className="min-h-[180px]">
@@ -122,9 +124,7 @@ const AccessGroupList = () => {
<div>
<Group size={40} />
</div>
<div className="ml-2">
使
</div>
<div className="ml-2">{t("access.group.nodata")}</div>
</div>
</>
)}
@@ -151,7 +151,7 @@ const AccessGroupList = () => {
);
}}
>
{t("access.group.domains")}
</Button>
</div>
</Show>
@@ -166,7 +166,7 @@ const AccessGroupList = () => {
>
<div>
<Button size="sm" onClick={handleAddAccess}>
{t("access.authorization.add")}
</Button>
</div>
</Show>
@@ -175,21 +175,21 @@ const AccessGroupList = () => {
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"destructive"} size={"sm"}>
{t("common.delete")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="dark:text-gray-200">
{t("access.group.delete")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("access.group.delete.confirm")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="dark:text-gray-200">
{t("common.cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
@@ -198,7 +198,7 @@ const AccessGroupList = () => {
);
}}
>
{t("common.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View File

@@ -0,0 +1,250 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import {
Access,
accessFormType,
HuaweicloudConfig,
getUsageByConfigType,
} from "@/domain/access";
import { save } from "@/repository/access";
import { useConfig } from "@/providers/config";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
const AccessHuaweicloudForm = ({
data,
op,
onAfterReq,
}: {
data?: Access;
op: "add" | "edit" | "copy";
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z
.string()
.min(1, "access.authorization.form.name.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
configType: accessFormType,
region: z
.string()
.min(1, "access.authorization.form.region.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
accessKeyId: z
.string()
.min(1, "access.authorization.form.access_key_id.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
secretAccessKey: z
.string()
.min(1, "access.authorization.form.access_key_secret.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
});
let config: HuaweicloudConfig = {
region: "cn-north-1",
accessKeyId: "",
secretAccessKey: "",
};
if (data) config = data.config as HuaweicloudConfig;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name || "",
configType: "huaweicloud",
region: config.region,
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
const req: Access = {
id: data.id as string,
name: data.name,
configType: data.configType,
usage: getUsageByConfigType(data.configType),
config: {
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
},
};
try {
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id && op == "edit") {
updateAccess(req);
return;
}
console.log(req);
addAccess(req);
} catch (e) {
const err = e as ClientResponseError;
Object.entries(err.response.data as PbErrorData).forEach(
([key, value]) => {
form.setError(key as keyof z.infer<typeof formSchema>, {
type: "manual",
message: value.message,
});
}
);
return;
}
};
return (
<>
<div className="max-w-[35em] mx-auto mt-10">
<Form {...form}>
<form
onSubmit={(e) => {
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.name.label")}</FormLabel>
<FormControl>
<Input
placeholder={t("access.authorization.form.name.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="region"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.region.label")}</FormLabel>
<FormControl>
<Input
placeholder={t("access.authorization.form.region.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accessKeyId"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.access_key_id.label")}</FormLabel>
<FormControl>
<Input
placeholder={t("access.authorization.form.access_key_id.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="secretAccessKey"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.access_key_secret.label")}</FormLabel>
<FormControl>
<Input
placeholder={t("access.authorization.form.access_key_secret.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>
</div>
</>
);
};
export default AccessHuaweicloudForm;

View File

@@ -1,14 +1,9 @@
import {
Access,
accessFormType,
getUsageByConfigType,
LocalConfig,
SSHConfig,
} from "@/domain/access";
import { Access, accessFormType, getUsageByConfigType } from "@/domain/access";
import { useConfig } from "@/providers/config";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useTranslation } from "react-i18next";
import {
Form,
FormControl,
@@ -19,46 +14,38 @@ import {
} from "../ui/form";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { Textarea } from "../ui/textarea";
import { save } from "@/repository/access";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
const AccessLocalForm = ({
data,
op,
onAfterReq,
}: {
data?: Access;
op: "add" | "edit" | "copy";
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess, reloadAccessGroups } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z
.string()
.min(1, "access.authorization.form.name.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
configType: accessFormType,
command: z.string().min(1).max(2048),
certPath: z.string().min(0).max(2048),
keyPath: z.string().min(0).max(2048),
});
let config: LocalConfig = {
command: "sudo service nginx restart",
certPath: "/etc/nginx/ssl/certificate.crt",
keyPath: "/etc/nginx/ssl/private.key",
};
if (data) config = data.config as SSHConfig;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || "",
configType: "local",
certPath: config.certPath,
keyPath: config.keyPath,
command: config.command,
},
});
@@ -69,14 +56,11 @@ const AccessLocalForm = ({
configType: data.configType,
usage: getUsageByConfigType(data.configType),
config: {
command: data.command,
certPath: data.certPath,
keyPath: data.keyPath,
},
config: {},
};
try {
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
@@ -84,7 +68,7 @@ const AccessLocalForm = ({
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id) {
if (data.id && op == "edit") {
updateAccess(req);
} else {
addAccess(req);
@@ -123,9 +107,12 @@ const AccessLocalForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.name.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input
placeholder={t("access.authorization.form.name.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -138,7 +125,7 @@ const AccessLocalForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -153,7 +140,7 @@ const AccessLocalForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -163,55 +150,10 @@ const AccessLocalForm = ({
)}
/>
<FormField
control={form.control}
name="certPath"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入证书上传路径" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="keyPath"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入私钥上传路径" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="command"
render={({ field }) => (
<FormItem>
<FormLabel>Command</FormLabel>
<FormControl>
<Textarea placeholder="请输入要执行的命令" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit"></Button>
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>

View File

@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -14,7 +15,12 @@ import {
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { Access, accessFormType, getUsageByConfigType, NamesiloConfig } from "@/domain/access";
import {
Access,
accessFormType,
getUsageByConfigType,
NamesiloConfig,
} from "@/domain/access";
import { save } from "@/repository/access";
import { useConfig } from "@/providers/config";
import { ClientResponseError } from "pocketbase";
@@ -22,17 +28,26 @@ import { PbErrorData } from "@/domain/base";
const AccessNamesiloForm = ({
data,
op,
onAfterReq,
}: {
data?: Access;
op: "add" | "edit" | "copy";
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z
.string()
.min(1, "access.authorization.form.name.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
configType: accessFormType,
apiKey: z.string().min(1).max(64),
apiKey: z
.string()
.min(1, "access.authorization.form.namesilo_api_key.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
});
let config: NamesiloConfig = {
@@ -44,14 +59,13 @@ const AccessNamesiloForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || "",
configType: "namesilo",
apiKey: config.apiKey,
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
console.log(data);
const req: Access = {
id: data.id as string,
name: data.name,
@@ -63,6 +77,7 @@ const AccessNamesiloForm = ({
};
try {
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
@@ -70,7 +85,7 @@ const AccessNamesiloForm = ({
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id) {
if (data.id && op == "edit") {
updateAccess(req);
return;
}
@@ -106,9 +121,12 @@ const AccessNamesiloForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.name.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input
placeholder={t("access.authorization.form.name.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -121,7 +139,7 @@ const AccessNamesiloForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -136,7 +154,7 @@ const AccessNamesiloForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -151,9 +169,12 @@ const AccessNamesiloForm = ({
name="apiKey"
render={({ field }) => (
<FormItem>
<FormLabel>NAMESILO_API_KEY</FormLabel>
<FormLabel>{t("access.authorization.form.namesilo_api_key.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入NAMESILO_API_KEY" {...field} />
<Input
placeholder={t("access.authorization.form.namesilo_api_key.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -162,7 +183,7 @@ const AccessNamesiloForm = ({
/>
<div className="flex justify-end">
<Button type="submit"></Button>
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>

View File

@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -14,7 +15,12 @@ import {
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { Access, accessFormType, getUsageByConfigType, QiniuConfig } from "@/domain/access";
import {
Access,
accessFormType,
getUsageByConfigType,
QiniuConfig,
} from "@/domain/access";
import { save } from "@/repository/access";
import { useConfig } from "@/providers/config";
@@ -23,18 +29,24 @@ import { PbErrorData } from "@/domain/base";
const AccessQiniuForm = ({
data,
op,
onAfterReq,
}: {
data?: Access;
op: "add" | "edit" | "copy";
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z
.string()
.min(1, "access.authorization.form.name.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
configType: accessFormType,
accessKey: z.string().min(1).max(64),
secretKey: z.string().min(1).max(64),
accessKey: z.string().min(1, "access.authorization.form.access_key.placeholder").max(64),
secretKey: z.string().min(1, "access.authorization.form.secret_key.placeholder").max(64),
});
let config: QiniuConfig = {
@@ -47,7 +59,7 @@ const AccessQiniuForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || "",
configType: "qiniu",
accessKey: config.accessKey,
secretKey: config.secretKey,
@@ -67,6 +79,7 @@ const AccessQiniuForm = ({
};
try {
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
@@ -74,7 +87,7 @@ const AccessQiniuForm = ({
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id) {
if (data.id && op == "edit") {
updateAccess(req);
return;
}
@@ -111,9 +124,12 @@ const AccessQiniuForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.name.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input
placeholder={t("access.authorization.form.name.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -126,7 +142,7 @@ const AccessQiniuForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -141,7 +157,7 @@ const AccessQiniuForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -156,9 +172,12 @@ const AccessQiniuForm = ({
name="accessKey"
render={({ field }) => (
<FormItem>
<FormLabel>AccessKey</FormLabel>
<FormLabel>{t("access.authorization.form.access_key.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入AccessKey" {...field} />
<Input
placeholder={t("access.authorization.form.access_key.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -171,9 +190,12 @@ const AccessQiniuForm = ({
name="secretKey"
render={({ field }) => (
<FormItem>
<FormLabel>SecretKey</FormLabel>
<FormLabel>{t("access.authorization.form.secret_key.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入SecretKey" {...field} />
<Input
placeholder={t("access.authorization.form.secret_key.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -184,7 +206,7 @@ const AccessQiniuForm = ({
<FormMessage />
<div className="flex justify-end">
<Button type="submit"></Button>
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>

View File

@@ -18,12 +18,12 @@ import {
} from "../ui/form";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { Textarea } from "../ui/textarea";
import { save } from "@/repository/access";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
import { readFileContent } from "@/lib/file";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Select,
SelectContent,
@@ -38,9 +38,11 @@ import { updateById } from "@/repository/access_group";
const AccessSSHForm = ({
data,
op,
onAfterReq,
}: {
data?: Access;
op: "add" | "edit" | "copy";
onAfterReq: () => void;
}) => {
const {
@@ -53,6 +55,7 @@ const AccessSSHForm = ({
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [fileName, setFileName] = useState("");
const { t } = useTranslation();
const originGroup = data ? (data.group ? data.group : "") : "";
@@ -62,25 +65,37 @@ const AccessSSHForm = ({
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z
.string()
.min(1, "access.authorization.form.name.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
configType: accessFormType,
host: z.string().refine(
(str) => {
return ipReg.test(str) || domainReg.test(str);
},
{
message: "请输入正确的域名或IP",
message: "common.errmsg.host_invalid",
}
),
group: z.string().optional(),
port: z.string().min(1).max(5),
username: z.string().min(1).max(64),
password: z.string().min(0).max(64),
key: z.string().min(0).max(20480),
port: z
.string()
.min(1, "access.authorization.form.ssh_port.placeholder")
.max(5, t("common.errmsg.string_max", { max: 5 })),
username: z
.string()
.min(1, "username.not.empty")
.max(64, t("common.errmsg.string_max", { max: 64 })),
password: z
.string()
.min(0, "password.not.empty")
.max(64, t("common.errmsg.string_max", { max: 64 })),
key: z
.string()
.min(0, "access.authorization.form.ssh_key.placeholder")
.max(20480, t("common.errmsg.string_max", { max: 20480 })),
keyFile: z.any().optional(),
command: z.string().min(1).max(2048),
certPath: z.string().min(0).max(2048),
keyPath: z.string().min(0).max(2048),
});
let config: SSHConfig = {
@@ -90,9 +105,6 @@ const AccessSSHForm = ({
password: "",
key: "",
keyFile: "",
command: "sudo service nginx restart",
certPath: "/etc/nginx/ssl/certificate.crt",
keyPath: "/etc/nginx/ssl/private.key",
};
if (data) config = data.config as SSHConfig;
@@ -100,7 +112,7 @@ const AccessSSHForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || "",
configType: "ssh",
group: data?.group,
host: config.host,
@@ -109,14 +121,10 @@ const AccessSSHForm = ({
password: config.password,
key: config.key,
keyFile: config.keyFile,
certPath: config.certPath,
keyPath: config.keyPath,
command: config.command,
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
console.log(data);
let group = data.group;
if (group == "emptyId") group = "";
@@ -132,13 +140,11 @@ const AccessSSHForm = ({
username: data.username,
password: data.password,
key: data.key,
command: data.command,
certPath: data.certPath,
keyPath: data.keyPath,
},
};
try {
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
@@ -146,7 +152,7 @@ const AccessSSHForm = ({
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id) {
if (data.id && op == "edit") {
updateAccess(req);
} else {
addAccess(req);
@@ -219,9 +225,16 @@ const AccessSSHForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>
{t("access.authorization.form.name.label")}
</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input
placeholder={t(
"access.authorization.form.name.placeholder"
)}
{...field}
/>
</FormControl>
<FormMessage />
@@ -235,12 +248,12 @@ const AccessSSHForm = ({
render={({ field }) => (
<FormItem>
<FormLabel className="w-full flex justify-between">
<div>( ssh )</div>
<div>{t("access.authorization.form.ssh_group.label")}</div>
<AccessGroupEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t("common.add")}
</div>
}
/>
@@ -255,7 +268,11 @@ const AccessSSHForm = ({
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择分组" />
<SelectValue
placeholder={t(
"access.authorization.form.access_group.placeholder"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="emptyId">
@@ -295,7 +312,9 @@ const AccessSSHForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>
{t("access.authorization.form.config.label")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -310,7 +329,9 @@ const AccessSSHForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>
{t("access.authorization.form.config.label")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -325,9 +346,16 @@ const AccessSSHForm = ({
name="host"
render={({ field }) => (
<FormItem className="grow">
<FormLabel>HOST</FormLabel>
<FormLabel>
{t("access.authorization.form.ssh_host.label")}
</FormLabel>
<FormControl>
<Input placeholder="请输入Host" {...field} />
<Input
placeholder={t(
"access.authorization.form.ssh_host.placeholder"
)}
{...field}
/>
</FormControl>
<FormMessage />
@@ -340,10 +368,14 @@ const AccessSSHForm = ({
name="port"
render={({ field }) => (
<FormItem>
<FormLabel>SSH端口</FormLabel>
<FormLabel>
{t("access.authorization.form.ssh_port.label")}
</FormLabel>
<FormControl>
<Input
placeholder="请输入Port"
placeholder={t(
"access.authorization.form.ssh_port.placeholder"
)}
{...field}
type="number"
/>
@@ -360,9 +392,16 @@ const AccessSSHForm = ({
name="username"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>
{t("access.authorization.form.username.label")}
</FormLabel>
<FormControl>
<Input placeholder="请输入用户名" {...field} />
<Input
placeholder={t(
"access.authorization.form.username.placeholder"
)}
{...field}
/>
</FormControl>
<FormMessage />
@@ -375,10 +414,14 @@ const AccessSSHForm = ({
name="password"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>
{t("access.authorization.form.password.label")}
</FormLabel>
<FormControl>
<Input
placeholder="请输入密码"
placeholder={t(
"access.authorization.form.password.placeholder"
)}
{...field}
type="password"
/>
@@ -394,9 +437,16 @@ const AccessSSHForm = ({
name="key"
render={({ field }) => (
<FormItem hidden>
<FormLabel>Key使</FormLabel>
<FormLabel>
{t("access.authorization.form.ssh_key.label")}
</FormLabel>
<FormControl>
<Input placeholder="请输入Key" {...field} />
<Input
placeholder={t(
"access.authorization.form.ssh_key.placeholder"
)}
{...field}
/>
</FormControl>
<FormMessage />
@@ -409,7 +459,9 @@ const AccessSSHForm = ({
name="keyFile"
render={({ field }) => (
<FormItem>
<FormLabel>Key使</FormLabel>
<FormLabel>
{t("access.authorization.form.ssh_key.label")}
</FormLabel>
<FormControl>
<div>
<Button
@@ -419,10 +471,16 @@ const AccessSSHForm = ({
className="w-48"
onClick={handleSelectFileClick}
>
{fileName ? fileName : "请选择文件"}
{fileName
? fileName
: t(
"access.authorization.form.ssh_key_file.placeholder"
)}
</Button>
<Input
placeholder="请输入Key"
placeholder={t(
"access.authorization.form.ssh_key.placeholder"
)}
{...field}
ref={fileInputRef}
className="hidden"
@@ -438,55 +496,10 @@ const AccessSSHForm = ({
)}
/>
<FormField
control={form.control}
name="certPath"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入证书上传路径" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="keyPath"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入私钥上传路径" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="command"
render={({ field }) => (
<FormItem>
<FormLabel>Command</FormLabel>
<FormControl>
<Textarea placeholder="请输入要执行的命令" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit"></Button>
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>

View File

@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -14,7 +15,12 @@ import {
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { Access, accessFormType, getUsageByConfigType, TencentConfig } from "@/domain/access";
import {
Access,
accessFormType,
getUsageByConfigType,
TencentConfig,
} from "@/domain/access";
import { save } from "@/repository/access";
import { useConfig } from "@/providers/config";
import { ClientResponseError } from "pocketbase";
@@ -22,18 +28,30 @@ import { PbErrorData } from "@/domain/base";
const AccessTencentForm = ({
data,
op,
onAfterReq,
}: {
data?: Access;
op: "add" | "edit" | "copy";
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z
.string()
.min(1, "access.authorization.form.name.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
configType: accessFormType,
secretId: z.string().min(1).max(64),
secretKey: z.string().min(1).max(64),
secretId: z
.string()
.min(1, "access.authorization.form.secret_id.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
secretKey: z
.string()
.min(1, "access.authorization.form.secret_key.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
});
let config: TencentConfig = {
@@ -46,7 +64,7 @@ const AccessTencentForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || "",
configType: "tencent",
secretId: config.secretId,
secretKey: config.secretKey,
@@ -66,6 +84,7 @@ const AccessTencentForm = ({
};
try {
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
@@ -73,7 +92,7 @@ const AccessTencentForm = ({
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id) {
if (data.id && op == "edit") {
updateAccess(req);
return;
}
@@ -108,9 +127,12 @@ const AccessTencentForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.name.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input
placeholder={t("access.authorization.form.name.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -123,7 +145,7 @@ const AccessTencentForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -138,7 +160,7 @@ const AccessTencentForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -153,9 +175,12 @@ const AccessTencentForm = ({
name="secretId"
render={({ field }) => (
<FormItem>
<FormLabel>SecretId</FormLabel>
<FormLabel>{t("access.authorization.form.secret_id.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入SecretId" {...field} />
<Input
placeholder={t("access.authorization.form.secret_id.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -168,9 +193,12 @@ const AccessTencentForm = ({
name="secretKey"
render={({ field }) => (
<FormItem>
<FormLabel>SecretKey</FormLabel>
<FormLabel>{t("access.authorization.form.secret_key.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入SecretKey" {...field} />
<Input
placeholder={t("access.authorization.form.secret_key.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -179,7 +207,7 @@ const AccessTencentForm = ({
/>
<div className="flex justify-end">
<Button type="submit"></Button>
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>

View File

@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -14,7 +15,12 @@ import {
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { Access, accessFormType, getUsageByConfigType, WebhookConfig } from "@/domain/access";
import {
Access,
accessFormType,
getUsageByConfigType,
WebhookConfig,
} from "@/domain/access";
import { save } from "@/repository/access";
import { useConfig } from "@/providers/config";
import { ClientResponseError } from "pocketbase";
@@ -22,17 +28,23 @@ import { PbErrorData } from "@/domain/base";
const WebhookForm = ({
data,
op,
onAfterReq,
}: {
data?: Access;
op: "add" | "edit" | "copy";
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z
.string()
.min(1, "access.authorization.form.name.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
configType: accessFormType,
url: z.string().url(),
url: z.string().url("common.errmsg.url_invalid"),
});
let config: WebhookConfig = {
@@ -44,14 +56,13 @@ const WebhookForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || "",
configType: "webhook",
url: config.url,
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
console.log(data);
const req: Access = {
id: data.id as string,
name: data.name,
@@ -63,6 +74,7 @@ const WebhookForm = ({
};
try {
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
@@ -70,7 +82,7 @@ const WebhookForm = ({
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id) {
if (data.id && op == "edit") {
updateAccess(req);
return;
}
@@ -106,9 +118,12 @@ const WebhookForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.name.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input
placeholder={t("access.authorization.form.name.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -121,7 +136,7 @@ const WebhookForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -136,7 +151,7 @@ const WebhookForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -151,9 +166,12 @@ const WebhookForm = ({
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook Url</FormLabel>
<FormLabel>{t("access.authorization.form.webhook_url.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入Webhook Url" {...field} />
<Input
placeholder={t("access.authorization.form.webhook_url.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
@@ -162,7 +180,7 @@ const WebhookForm = ({
/>
<div className="flex justify-end">
<Button type="submit"></Button>
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>

View File

@@ -0,0 +1,850 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { Button } from "../ui/button";
import { EditIcon, Plus, Trash2 } from "lucide-react";
import {
DeployConfig,
KVType,
targetTypeKeys,
targetTypeMap,
} from "@/domain/domain";
import Show from "../Show";
import { Alert, AlertDescription } from "../ui/alert";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { Label } from "../ui/label";
import { useConfig } from "@/providers/config";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "../ui/select";
import { accessTypeMap } from "@/domain/access";
import { useTranslation } from "react-i18next";
import { AccessEdit } from "./AccessEdit";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import KVList from "./KVList";
import { produce } from "immer";
import { nanoid } from "nanoid";
import { z } from "zod";
type DeployEditContextProps = {
deploy: DeployConfig;
error: Record<string, string>;
setDeploy: (deploy: DeployConfig) => void;
setError: (error: Record<string, string>) => void;
};
const DeployEditContext = createContext<DeployEditContextProps>(
{} as DeployEditContextProps
);
export const useDeployEditContext = () => {
return useContext(DeployEditContext);
};
type DeployListProps = {
deploys: DeployConfig[];
onChange: (deploys: DeployConfig[]) => void;
};
const DeployList = ({ deploys, onChange }: DeployListProps) => {
const [list, setList] = useState<DeployConfig[]>([]);
const { t } = useTranslation();
useEffect(() => {
setList(deploys);
}, [deploys]);
const handleAdd = (deploy: DeployConfig) => {
deploy.id = nanoid();
const newList = [...list, deploy];
setList(newList);
onChange(newList);
};
const handleDelete = (id: string) => {
const newList = list.filter((item) => item.id !== id);
setList(newList);
onChange(newList);
};
const handleSave = (deploy: DeployConfig) => {
const newList = list.map((item) => {
if (item.id === deploy.id) {
return { ...deploy };
}
return item;
});
setList(newList);
onChange(newList);
};
return (
<>
<Show
when={list.length > 0}
fallback={
<Alert className="w-full border dark:border-stone-400">
<AlertDescription className="flex flex-col items-center">
<div>{t("domain.deployment.nodata")}</div>
<div className="flex justify-end mt-2">
<DeployEditDialog
onSave={(config: DeployConfig) => {
handleAdd(config);
}}
trigger={<Button size={"sm"}>{t("common.add")}</Button>}
/>
</div>
</AlertDescription>
</Alert>
}
>
<div className="flex justify-end py-2 border-b dark:border-stone-400">
<DeployEditDialog
trigger={<Button size={"sm"}>{t("common.add")}</Button>}
onSave={(config: DeployConfig) => {
handleAdd(config);
}}
/>
</div>
<div className="w-full md:w-[35em] rounded mt-5 border dark:border-stone-400 dark:text-stone-200">
<div className="">
{list.map((item) => (
<DeployItem
key={item.id}
item={item}
onDelete={() => {
handleDelete(item.id ?? "");
}}
onSave={(deploy: DeployConfig) => {
handleSave(deploy);
}}
/>
))}
</div>
</div>
</Show>
</>
);
};
type DeployItemProps = {
item: DeployConfig;
onDelete: () => void;
onSave: (deploy: DeployConfig) => void;
};
const DeployItem = ({ item, onDelete, onSave }: DeployItemProps) => {
const {
config: { accesses },
} = useConfig();
const { t } = useTranslation();
const access = accesses.find((access) => access.id === item.access);
const getImg = () => {
if (!access) {
return "";
}
const accessType = accessTypeMap.get(access.configType);
if (accessType) {
return accessType[1];
}
return "";
};
const getTypeName = () => {
if (!access) {
return "";
}
const accessType = targetTypeMap.get(item.type);
if (accessType) {
return t(accessType[0]);
}
return "";
};
return (
<div className="flex justify-between text-sm p-3 items-center text-stone-700 dark:text-stone-200">
<div className="flex space-x-2 items-center">
<div>
<img src={getImg()} className="w-9"></img>
</div>
<div className="text-stone-600 flex-col flex space-y-0 dark:text-stone-200">
<div>{getTypeName()}</div>
<div>{access?.name}</div>
</div>
</div>
<div className="flex space-x-2">
<DeployEditDialog
trigger={<EditIcon size={16} className="cursor-pointer" />}
deployConfig={item}
onSave={(deploy: DeployConfig) => {
onSave(deploy);
}}
/>
<Trash2
size={16}
className="cursor-pointer"
onClick={() => {
onDelete();
}}
/>
</div>
</div>
);
};
type DeployEditDialogProps = {
trigger: React.ReactNode;
deployConfig?: DeployConfig;
onSave: (deploy: DeployConfig) => void;
};
const DeployEditDialog = ({
trigger,
deployConfig,
onSave,
}: DeployEditDialogProps) => {
const {
config: { accesses },
} = useConfig();
const [deployType, setDeployType] = useState<TargetType>();
const [locDeployConfig, setLocDeployConfig] = useState<DeployConfig>({
access: "",
type: "",
});
const [error, setError] = useState<Record<string, string>>({});
const [open, setOpen] = useState(false);
useEffect(() => {
if (deployConfig) {
setLocDeployConfig({ ...deployConfig });
} else {
setLocDeployConfig({
access: "",
type: "",
});
}
}, [deployConfig]);
useEffect(() => {
const temp = locDeployConfig.type.split("-");
console.log(temp);
let t;
if (temp && temp.length > 1) {
t = temp[1];
} else {
t = locDeployConfig.type;
}
setDeployType(t as TargetType);
setError({});
}, [locDeployConfig.type]);
const setDeploy = useCallback(
(deploy: DeployConfig) => {
if (deploy.type !== locDeployConfig.type) {
setLocDeployConfig({ ...deploy, access: "", config: {} });
} else {
setLocDeployConfig({ ...deploy });
}
},
[locDeployConfig.type]
);
const { t } = useTranslation();
const targetAccesses = accesses.filter((item) => {
if (item.usage == "apply") {
return false;
}
if (locDeployConfig.type == "") {
return true;
}
const types = locDeployConfig.type.split("-");
return item.configType === types[0];
});
const handleSaveClick = () => {
// 验证数据
// 保存数据
// 清理数据
// 关闭弹框
const newError = { ...error };
if (locDeployConfig.type === "") {
newError.type = t("domain.deployment.form.access.placeholder");
} else {
newError.type = "";
}
if (locDeployConfig.access === "") {
newError.access = t("domain.deployment.form.access.placeholder");
} else {
newError.access = "";
}
setError(newError);
for (const key in newError) {
if (newError[key] !== "") {
return;
}
}
onSave(locDeployConfig);
setLocDeployConfig({
access: "",
type: "",
});
setError({});
setOpen(false);
};
return (
<DeployEditContext.Provider
value={{
deploy: locDeployConfig,
setDeploy: setDeploy,
error: error,
setError: setError,
}}
>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger>{trigger}</DialogTrigger>
<DialogContent className="dark:text-stone-200">
<DialogHeader>
<DialogTitle>{t("history.page.title")}</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{/* 部署方式 */}
<div>
<Label>{t("domain.deployment.form.type.label")}</Label>
<Select
value={locDeployConfig.type}
onValueChange={(val: string) => {
setDeploy({ ...locDeployConfig, type: val });
}}
>
<SelectTrigger className="mt-2">
<SelectValue
placeholder={t("domain.deployment.form.type.placeholder")}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>
{t("domain.deployment.form.type.list")}
</SelectLabel>
{targetTypeKeys.map((item) => (
<SelectItem key={item} value={item}>
<div className="flex items-center space-x-2">
<img
className="w-6"
src={targetTypeMap.get(item)?.[1]}
/>
<div>{t(targetTypeMap.get(item)?.[0] ?? "")}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{error.type}</div>
</div>
{/* 授权配置 */}
<div>
<Label className="flex justify-between">
<div>{t("domain.deployment.form.access.label")}</div>
<AccessEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t("common.add")}
</div>
}
op="add"
/>
</Label>
<Select
value={locDeployConfig.access}
onValueChange={(val: string) => {
setDeploy({ ...locDeployConfig, access: val });
}}
>
<SelectTrigger className="mt-2">
<SelectValue
placeholder={t(
"domain.deployment.form.access.placeholder"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>
{t("domain.deployment.form.access.list")}
</SelectLabel>
{targetAccesses.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img
className="w-6"
src={accessTypeMap.get(item.configType)?.[1]}
/>
<div>{item.name}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{error.access}</div>
</div>
{/* 其他参数 */}
<DeployEdit type={deployType!} />
<DialogFooter>
<Button
onClick={(e) => {
e.stopPropagation();
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DeployEditContext.Provider>
);
};
type TargetType = "ssh" | "cdn" | "webhook" | "local" | "oss" | "dcdn";
type DeployEditProps = {
type: TargetType;
};
const DeployEdit = ({ type }: DeployEditProps) => {
const getDeploy = () => {
switch (type) {
case "ssh":
return <DeploySSH />;
case "local":
return <DeploySSH />;
case "cdn":
return <DeployCDN />;
case "dcdn":
return <DeployCDN />;
case "oss":
return <DeployOSS />;
case "webhook":
return <DeployWebhook />;
default:
return <DeployCDN />;
}
};
return getDeploy();
};
const DeploySSH = () => {
const { t } = useTranslation();
const { setError } = useDeployEditContext();
useEffect(() => {
setError({});
}, []);
const { deploy: data, setDeploy } = useDeployEditContext();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
config: {
certPath: "/etc/nginx/ssl/nginx.crt",
keyPath: "/etc/nginx/ssl/nginx.key",
preCommand: "",
command: "sudo service nginx reload",
},
});
}
}, []);
return (
<>
<div className="flex flex-col space-y-2">
<div>
<Label>{t("access.authorization.form.ssh_cert_path.label")}</Label>
<Input
placeholder={t("access.authorization.form.ssh_cert_path.label")}
className="w-full mt-1"
value={data?.config?.certPath}
onChange={(e) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.certPath = e.target.value;
});
setDeploy(newData);
}}
/>
</div>
<div>
<Label>{t("access.authorization.form.ssh_key_path.label")}</Label>
<Input
placeholder={t(
"access.authorization.form.ssh_key_path.placeholder"
)}
className="w-full mt-1"
value={data?.config?.keyPath}
onChange={(e) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.keyPath = e.target.value;
});
setDeploy(newData);
}}
/>
</div>
<div>
<Label>{t("access.authorization.form.ssh_pre_command.label")}</Label>
<Textarea
className="mt-1"
value={data?.config?.preCommand}
placeholder={t(
"access.authorization.form.ssh_pre_command.placeholder"
)}
onChange={(e) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.preCommand = e.target.value;
});
setDeploy(newData);
}}
></Textarea>
</div>
<div>
<Label>{t("access.authorization.form.ssh_command.label")}</Label>
<Textarea
className="mt-1"
value={data?.config?.command}
placeholder={t("access.authorization.form.ssh_command.placeholder")}
onChange={(e) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.command = e.target.value;
});
setDeploy(newData);
}}
></Textarea>
</div>
</div>
</>
);
};
const DeployCDN = () => {
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { t } = useTranslation();
useEffect(() => {
setError({});
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
const domainSchema = z
.string()
.regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
});
return (
<div className="flex flex-col space-y-2">
<div>
<Label>{t("domain.deployment.form.cdn_domain.label")}</Label>
<Input
placeholder={t(
"domain.deployment.form.cdn_domain.placeholder"
)}
className="w-full mt-1"
value={data?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
</div>
</div>
);
};
const DeployOSS = () => {
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { t } = useTranslation();
useEffect(() => {
setError({});
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
useEffect(() => {
const bucketResp = bucketSchema.safeParse(data.config?.domain);
if (!bucketResp.success) {
setError({
...error,
bucket: JSON.parse(bucketResp.error.message)[0].message,
});
} else {
setError({
...error,
bucket: "",
});
}
}, []);
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
config: {
endpoint: "oss-cn-hangzhou.aliyuncs.com",
bucket: "",
domain: "",
},
});
}
}, []);
const domainSchema = z
.string()
.regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
});
const bucketSchema = z.string().min(1, {
message: t("domain.deployment.form.oss_bucket.placeholder"),
});
return (
<div className="flex flex-col space-y-2">
<div>
<Label>{t("domain.deployment.form.oss_endpoint.label")}</Label>
<Input
className="w-full mt-1"
value={data?.config?.endpoint}
onChange={(e) => {
const temp = e.target.value;
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.endpoint = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.endpoint}</div>
<Label>{t("domain.deployment.form.oss_bucket")}</Label>
<Input
placeholder={t(
"domain.deployment.form.oss_bucket.placeholder"
)}
className="w-full mt-1"
value={data?.config?.bucket}
onChange={(e) => {
const temp = e.target.value;
const resp = bucketSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
bucket: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
bucket: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.bucket = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.bucket}</div>
<Label>{t("domain.deployment.form.cdn_domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.cdn_domain.label")}
className="w-full mt-1"
value={data?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
</div>
</div>
);
};
const DeployWebhook = () => {
const { deploy: data, setDeploy } = useDeployEditContext();
const { setError } = useDeployEditContext();
useEffect(() => {
setError({});
}, []);
return (
<>
<KVList
variables={data?.config?.variables}
onValueChange={(variables: KVType[]) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.variables = variables;
});
setDeploy(newData);
}}
/>
</>
);
};
export default DeployList;

View File

@@ -1,3 +1,7 @@
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
import { Separator } from "../ui/separator";
type DeployProgressProps = {
@@ -6,80 +10,57 @@ type DeployProgressProps = {
};
const DeployProgress = ({ phase, phaseSuccess }: DeployProgressProps) => {
let rs = <> </>;
const { t } = useTranslation();
let step = 0;
if (phase === "check") {
if (phaseSuccess) {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div>
</div>
);
} else {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-red-600"> </div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div>
</div>
);
}
step = 1;
} else if (phase === "apply") {
step = 2;
} else if (phase === "deploy") {
step = 3;
}
if (phase === "apply") {
if (phaseSuccess) {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600"></div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div>
</div>
);
} else {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-red-600"></div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div>
</div>
);
}
}
if (phase === "deploy") {
if (phaseSuccess) {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600"></div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600"></div>
</div>
);
} else {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600"></div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-red-600"></div>
</div>
);
}
}
return rs;
return (
<div className="flex items-center">
<div
className={cn(
"text-xs text-nowrap",
step === 1 ? (phaseSuccess ? "text-green-600" : "text-red-600") : "",
step > 1 ? "text-green-600" : ""
)}
>
{t("history.props.stage.progress.check")}
</div>
<Separator
className={cn("h-1 grow max-w-[60px]", step > 1 ? "bg-green-600" : "")}
/>
<div
className={cn(
"text-xs text-nowrap",
step < 2 ? "text-muted-foreground" : "",
step === 2 ? (phaseSuccess ? "text-green-600" : "text-red-600") : "",
step > 2 ? "text-green-600" : ""
)}
>
{t("history.props.stage.progress.apply")}
</div>
<Separator
className={cn("h-1 grow max-w-[60px]", step > 2 ? "bg-green-600" : "")}
/>
<div
className={cn(
"text-xs text-nowrap",
step < 3 ? "text-muted-foreground" : "",
step === 3 ? (phaseSuccess ? "text-green-600" : "text-red-600") : "",
step > 3 ? "text-green-600" : ""
)}
>
{t("history.props.stage.progress.deploy")}
</div>
</div>
);
};
export default DeployProgress;

View File

@@ -10,6 +10,7 @@ import {
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
Form,
FormControl,
@@ -39,9 +40,10 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
} = useConfig();
const [open, setOpen] = useState(false);
const { t } = useTranslation();
const formSchema = z.object({
email: z.string().email(),
email: z.string().email("common.errmsg.email_invalid"),
});
const form = useForm<z.infer<typeof formSchema>>({
@@ -54,7 +56,7 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
const onSubmit = async (data: z.infer<typeof formSchema>) => {
if ((emails.content as EmailsSetting).emails.includes(data.email)) {
form.setError("email", {
message: "邮箱已存在",
message: "common.errmsg.email_duplicate",
});
return;
}
@@ -100,7 +102,7 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
</DialogTrigger>
<DialogContent className="sm:max-w-[600px] w-full dark:text-stone-200">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogTitle>{t("domain.application.form.email.add")}</DialogTitle>
</DialogHeader>
<div className="container py-3">
@@ -118,9 +120,13 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t("domain.application.form.email.label")}</FormLabel>
<FormControl>
<Input placeholder="请输入邮箱" {...field} type="email" />
<Input
placeholder={t("common.errmsg.email_empty")}
{...field}
type="email"
/>
</FormControl>
<FormMessage />
@@ -129,7 +135,7 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
/>
<div className="flex justify-end">
<Button type="submit"></Button>
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>

View File

@@ -0,0 +1,252 @@
import { KVType } from "@/domain/domain";
import { useEffect, useState } from "react";
import { Label } from "../ui/label";
import { Edit, Plus, Trash2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import Show from "../Show";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { produce } from "immer";
type KVListProps = {
variables?: KVType[];
onValueChange?: (variables: KVType[]) => void;
};
const KVList = ({ variables, onValueChange }: KVListProps) => {
const [locVariables, setLocVariables] = useState<KVType[]>([]);
const { t } = useTranslation();
useEffect(() => {
if (variables) {
setLocVariables(variables);
}
}, [variables]);
const handleAddClick = (variable: KVType) => {
// 查看是否存在key存在则更新不存在则添加
const index = locVariables.findIndex((item) => {
return item.key === variable.key;
});
const newList = produce(locVariables, (draft) => {
if (index === -1) {
draft.push(variable);
} else {
draft[index] = variable;
}
});
setLocVariables(newList);
onValueChange?.(newList);
};
const handleDeleteClick = (index: number) => {
const newList = [...locVariables];
newList.splice(index, 1);
setLocVariables(newList);
onValueChange?.(newList);
};
const handleEditClick = (index: number, variable: KVType) => {
const newList = [...locVariables];
newList[index] = variable;
setLocVariables(newList);
onValueChange?.(newList);
};
return (
<>
<div className="flex justify-between dark:text-stone-200">
<Label>{t("domain.deployment.form.variables.label")}</Label>
<Show when={!!locVariables?.length}>
<KVEdit
variable={{
key: "",
value: "",
}}
trigger={
<div className="flex items-center text-primary">
<Plus size={16} className="cursor-pointer " />
<div className="text-sm ">{t("common.add")}</div>
</div>
}
onSave={(variable) => {
handleAddClick(variable);
}}
/>
</Show>
</div>
<Show
when={!!locVariables?.length}
fallback={
<div className="border rounded-md p-3 text-sm mt-2 flex flex-col items-center">
<div className="text-muted-foreground">
{t("domain.deployment.form.variables.empty")}
</div>
<KVEdit
trigger={
<div className="flex items-center text-primary">
<Plus size={16} className="cursor-pointer " />
<div className="text-sm ">{t("common.add")}</div>
</div>
}
variable={{
key: "",
value: "",
}}
onSave={(variable) => {
handleAddClick(variable);
}}
/>
</div>
}
>
<div className="border p-3 rounded-md text-stone-700 text-sm dark:text-stone-200">
{locVariables?.map((item, index) => (
<div key={index} className="flex justify-between items-center">
<div>
{item.key}={item.value}
</div>
<div className="flex space-x-2">
<KVEdit
trigger={<Edit size={16} className="cursor-pointer" />}
variable={item}
onSave={(variable) => {
handleEditClick(index, variable);
}}
/>
<Trash2
size={16}
className="cursor-pointer"
onClick={() => {
handleDeleteClick(index);
}}
/>
</div>
</div>
))}
</div>
</Show>
</>
);
};
type KVEditProps = {
variable?: KVType;
trigger: React.ReactNode;
onSave: (variable: KVType) => void;
};
const KVEdit = ({ variable, trigger, onSave }: KVEditProps) => {
const [locVariable, setLocVariable] = useState<KVType>({
key: "",
value: "",
});
useEffect(() => {
if (variable) setLocVariable(variable!);
}, [variable]);
const { t } = useTranslation();
const [open, setOpen] = useState<boolean>(false);
const [err, setErr] = useState<Record<string, string>>({});
const handleSaveClick = () => {
if (!locVariable.key) {
setErr({
key: t("domain.deployment.form.variables.key.required"),
});
return;
}
if (!locVariable.value) {
setErr({
value: t("domain.deployment.form.variables.value.required"),
});
return;
}
onSave?.(locVariable);
setOpen(false);
setErr({});
};
return (
<Dialog
open={open}
onOpenChange={() => {
setOpen(!open);
}}
>
<DialogTrigger>{trigger}</DialogTrigger>
<DialogContent className="dark:text-stone-200">
<DialogHeader className="flex flex-col">
<DialogTitle>{t("domain.deployment.form.variables.label")}</DialogTitle>
<div className="pt-5 flex flex-col items-start">
<Label>{t("domain.deployment.form.variables.key")}</Label>
<Input
placeholder={t("domain.deployment.form.variables.key.placeholder")}
value={locVariable?.key}
onChange={(e) => {
setLocVariable({ ...locVariable, key: e.target.value });
}}
className="w-full mt-1"
/>
<div className="text-red-500 text-sm mt-1">{err?.key}</div>
</div>
<div className="pt-2 flex flex-col items-start">
<Label>{t("domain.deployment.form.variables.value")}</Label>
<Input
placeholder={t("domain.deployment.form.variables.value.placeholder")}
value={locVariable?.value}
onChange={(e) => {
setLocVariable({ ...locVariable, value: e.target.value });
}}
className="w-full mt-1"
/>
<div className="text-red-500 text-sm mt-1">{err?.value}</div>
</div>
</DialogHeader>
<DialogFooter>
<div className="flex justify-end">
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default KVList;

View File

@@ -0,0 +1,249 @@
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?: ValueType;
onValueChange: (value: string) => void;
};
const titles: Record<string, string> = {
domain: "common.text.domain",
ip: "common.text.ip",
dns: "common.text.dns",
};
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("common.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">
{t('common.text.' + valueType + '.empty')}
</div>
<StringEdit
value={""}
trigger={t("common.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" | "dns" | "host";
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("common.errmsg.domain_invalid"),
});
const ipSchema = z.string().ip({ message: t("common.errmsg.ip_invalid") });
const schedules: Record<ValueType, z.ZodString> = {
domain: domainSchema,
dns: ipSchema,
host: 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("common.add") : t("common.confirm")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,9 +1,12 @@
import { BookOpen } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Separator } from "../ui/separator";
import { version } from "@/domain/version";
const Version = () => {
const { t } = useTranslation();
return (
<div className="fixed right-0 bottom-0 w-full flex justify-between p-5">
<div className=""></div>
@@ -14,7 +17,7 @@ const Version = () => {
className="flex items-center"
>
<BookOpen size={16} />
<div className="ml-1"></div>
<div className="ml-1">{t("common.menu.document")}</div>
</a>
<Separator orientation="vertical" className="mx-2" />
<a

View File

@@ -8,6 +8,7 @@ import { useEffect, useState } from "react";
import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast";
import { useTranslation } from "react-i18next";
type DingTalkSetting = {
id: string;
@@ -17,6 +18,7 @@ type DingTalkSetting = {
const DingTalk = () => {
const { config, setChannels } = useNotify();
const { t } = useTranslation();
const [dingtalk, setDingtalk] = useState<DingTalkSetting>({
id: config.id ?? "",
@@ -70,15 +72,17 @@ const DingTalk = () => {
setChannels(resp);
toast({
title: "保存成功",
description: "配置保存成功",
title: t("common.save.succeeded.message"),
description: t("settings.notification.config.saved.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: "保存失败",
description: "配置保存失败:" + msg,
title: t("common.save.failed.message"),
description: `${t(
"settings.notification.config.failed.message"
)}: ${msg}`,
variant: "destructive",
});
}
@@ -100,7 +104,7 @@ const DingTalk = () => {
}}
/>
<Input
placeholder="加签的签名"
placeholder={t("settings.notification.dingtalk.secret.placeholder")}
className="mt-2"
value={dingtalk.data.secret}
onChange={(e) => {
@@ -127,7 +131,9 @@ const DingTalk = () => {
});
}}
/>
<Label htmlFor="airplane-mode"></Label>
<Label htmlFor="airplane-mode">
{t("settings.notification.config.enable")}
</Label>
</div>
<div className="flex justify-end mt-2">
@@ -136,7 +142,7 @@ const DingTalk = () => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</div>
</div>

View File

@@ -9,6 +9,7 @@ import {
} from "@/domain/settings";
import { getSetting, update } from "@/repository/settings";
import { useToast } from "../ui/use-toast";
import { useTranslation } from "react-i18next";
const NotifyTemplate = () => {
const [id, setId] = useState("");
@@ -17,6 +18,7 @@ const NotifyTemplate = () => {
]);
const { toast } = useToast();
const { t } = useTranslation();
useEffect(() => {
const featchData = async () => {
@@ -66,8 +68,8 @@ const NotifyTemplate = () => {
}
toast({
title: "保存成功",
description: "通知模板保存成功",
title: t("common.save.succeeded.message"),
description: t("settings.notification.template.saved.message"),
});
};
@@ -81,7 +83,7 @@ const NotifyTemplate = () => {
/>
<div className="text-muted-foreground text-sm mt-1">
, COUNT:即将过期张数
{t("settings.notification.template.variables.tips.title")}
</div>
<Textarea
@@ -92,10 +94,10 @@ const NotifyTemplate = () => {
}}
></Textarea>
<div className="text-muted-foreground text-sm mt-1">
, COUNT:即将过期张数DOMAINS:域名列表
{t("settings.notification.template.variables.tips.content")}
</div>
<div className="flex justify-end mt-2">
<Button onClick={handleSaveClick}></Button>
<Button onClick={handleSaveClick}>{t("common.save")}</Button>
</div>
</div>
);

View File

@@ -8,6 +8,7 @@ import { useEffect, useState } from "react";
import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast";
import { useTranslation } from "react-i18next";
type TelegramSetting = {
id: string;
@@ -17,6 +18,7 @@ type TelegramSetting = {
const Telegram = () => {
const { config, setChannels } = useNotify();
const { t } = useTranslation();
const [telegram, setTelegram] = useState<TelegramSetting>({
id: config.id ?? "",
@@ -48,7 +50,7 @@ const Telegram = () => {
const data = getDetailTelegram();
setTelegram({
id: config.id ?? "",
name: "telegram",
name: "common.provider.telegram",
data,
});
}, [config]);
@@ -70,15 +72,17 @@ const Telegram = () => {
setChannels(resp);
toast({
title: "保存成功",
description: "配置保存成功",
title: t("common.save.succeeded.message"),
description: t("settings.notification.config.saved.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: "保存失败",
description: "配置保存失败:" + msg,
title: t("common.save.failed.message"),
description: `${t(
"settings.notification.config.failed.message"
)}: ${msg}`,
variant: "destructive",
});
}
@@ -128,7 +132,9 @@ const Telegram = () => {
});
}}
/>
<Label htmlFor="airplane-mode"></Label>
<Label htmlFor="airplane-mode">
{t("settings.notification.config.enable")}
</Label>
</div>
<div className="flex justify-end mt-2">
@@ -137,7 +143,7 @@ const Telegram = () => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</div>
</div>

View File

@@ -9,6 +9,7 @@ import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast";
import { isValidURL } from "@/lib/url";
import { useTranslation } from "react-i18next";
type WebhookSetting = {
id: string;
@@ -18,6 +19,7 @@ type WebhookSetting = {
const Webhook = () => {
const { config, setChannels } = useNotify();
const { t } = useTranslation();
const [webhook, setWebhook] = useState<WebhookSetting>({
id: config.id ?? "",
@@ -59,8 +61,8 @@ const Webhook = () => {
webhook.data.url = webhook.data.url.trim();
if (!isValidURL(webhook.data.url)) {
toast({
title: "保存失败",
description: "Url格式不正确",
title: t("common.save.failed.message"),
description: t("settings.notification.url.errmsg.invalid"),
variant: "destructive",
});
return;
@@ -79,15 +81,17 @@ const Webhook = () => {
setChannels(resp);
toast({
title: "保存成功",
description: "配置保存成功",
title: t("common.save.succeeded.message"),
description: t("settings.notification.config.saved.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: "保存失败",
description: "配置保存失败:" + msg,
title: t("common.save.failed.message"),
description: `${t(
"settings.notification.config.failed.message"
)}: ${msg}`,
variant: "destructive",
});
}
@@ -123,7 +127,9 @@ const Webhook = () => {
});
}}
/>
<Label htmlFor="airplane-mode"></Label>
<Label htmlFor="airplane-mode">
{t("settings.notification.config.enable")}
</Label>
</div>
<div className="flex justify-end mt-2">
@@ -132,7 +138,7 @@ const Webhook = () => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</div>
</div>

View File

@@ -1,10 +1,10 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
@@ -15,8 +15,8 @@ const AccordionItem = React.forwardRef<
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
@@ -35,8 +35,8 @@ const AccordionTrigger = React.forwardRef<
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
@@ -49,8 +49,8 @@ const AccordionContent = React.forwardRef<
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
@@ -17,7 +17,7 @@ const alertVariants = cva(
variant: "default",
},
}
)
);
const Alert = React.forwardRef<
HTMLDivElement,
@@ -29,8 +29,8 @@ const Alert = React.forwardRef<
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
@@ -41,8 +41,8 @@ const AlertTitle = React.forwardRef<
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
@@ -53,7 +53,7 @@ const AlertDescription = React.forwardRef<
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription }
export { Alert, AlertTitle, AlertDescription };

View File

@@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@@ -12,6 +12,7 @@ import {
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { useTranslation } from "react-i18next"
const Form = FormProvider
@@ -145,7 +146,9 @@ const FormMessage = React.forwardRef<
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
const { t } = useTranslation()
const body = error ? t(String(error?.message)) : children
if (!body) {
return null

View File

@@ -3,6 +3,7 @@ import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
import { ButtonProps, buttonVariants } from "@/components/ui/button";
import { useTranslation } from "react-i18next";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
@@ -62,48 +63,60 @@ PaginationLink.displayName = "PaginationLink";
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span></span>
</PaginationLink>
);
}: React.ComponentProps<typeof PaginationLink>) => {
const { t } = useTranslation();
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>{t("common.pagination.prev")}</span>
</PaginationLink>
);
};
PaginationPrevious.displayName = "PaginationPrevious";
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span></span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
}: React.ComponentProps<typeof PaginationLink>) => {
const { t } = useTranslation();
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>{t("common.pagination.next")}</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
};
PaginationNext.displayName = "PaginationNext";
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
}: React.ComponentProps<"span">) => {
const { t } = useTranslation();
return (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">{t("common.pagination.more")}</span>
</span>
);
};
PaginationEllipsis.displayName = "PaginationEllipsis";
export {

View File

@@ -1,76 +1,73 @@
// Inspired by react-hot-toast library
import * as React from "react"
import * as React from "react";
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
} as const;
let count = 0
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
type ActionType = typeof actionTypes
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[]
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout)
}
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
@@ -78,7 +75,7 @@ export const reducer = (state: State, action: Action): State => {
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
};
case "UPDATE_TOAST":
return {
@@ -86,19 +83,19 @@ export const reducer = (state: State, action: Action): State => {
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
};
case "DISMISS_TOAST": {
const { toastId } = action
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
addToRemoveQueue(toast.id);
});
}
return {
@@ -111,44 +108,44 @@ export const reducer = (state: State, action: Action): State => {
}
: t
),
}
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
};
}
}
};
const listeners: Array<(state: State) => void> = []
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] }
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState)
})
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId()
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
@@ -157,36 +154,36 @@ function toast({ ...props }: Toast) {
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
if (!open) dismiss();
},
},
})
});
return {
id: id,
dismiss,
update,
}
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState)
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState)
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1)
listeners.splice(index, 1);
}
}
}, [state])
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
};
}
export { useToast, toast }
export { useToast, toast };

View File

@@ -1,15 +1,16 @@
import { z } from "zod";
export const accessTypeMap: Map<string, [string, string]> = new Map([
["tencent", ["腾讯云", "/imgs/providers/tencent.svg"]],
["aliyun", ["阿里云", "/imgs/providers/aliyun.svg"]],
["cloudflare", ["Cloudflare", "/imgs/providers/cloudflare.svg"]],
["namesilo", ["Namesilo", "/imgs/providers/namesilo.svg"]],
["godaddy", ["GoDaddy", "/imgs/providers/godaddy.svg"]],
["qiniu", ["七牛云", "/imgs/providers/qiniu.svg"]],
["ssh", ["SSH部署", "/imgs/providers/ssh.svg"]],
["webhook", ["Webhook", "/imgs/providers/webhook.svg"]],
["local", ["本地部署", "/imgs/providers/local.svg"]],
["aliyun", ["common.provider.aliyun", "/imgs/providers/aliyun.svg"]],
["tencent", ["common.provider.tencent", "/imgs/providers/tencent.svg"]],
["huaweicloud", ["common.provider.huaweicloud", "/imgs/providers/huaweicloud.svg"]],
["qiniu", ["common.provider.qiniu", "/imgs/providers/qiniu.svg"]],
["cloudflare", ["common.provider.cloudflare", "/imgs/providers/cloudflare.svg"]],
["namesilo", ["common.provider.namesilo", "/imgs/providers/namesilo.svg"]],
["godaddy", ["common.provider.godaddy", "/imgs/providers/godaddy.svg"]],
["local", ["common.provider.local", "/imgs/providers/local.svg"]],
["ssh", ["common.provider.ssh", "/imgs/providers/ssh.svg"]],
["webhook", ["common.provider.webhook", "/imgs/providers/webhook.svg"]],
]);
export const getProviderInfo = (t: string) => {
@@ -20,15 +21,16 @@ export const accessFormType = z.union(
[
z.literal("aliyun"),
z.literal("tencent"),
z.literal("ssh"),
z.literal("webhook"),
z.literal("cloudflare"),
z.literal("huaweicloud"),
z.literal("qiniu"),
z.literal("cloudflare"),
z.literal("namesilo"),
z.literal("godaddy"),
z.literal("local"),
z.literal("ssh"),
z.literal("webhook"),
],
{ message: "请选择云服务商" }
{ message: "access.authorization.form.type.placeholder" }
);
type AccessUsage = "apply" | "deploy" | "all";
@@ -40,85 +42,88 @@ export type Access = {
usage: AccessUsage;
group?: string;
config:
| TencentConfig
| AliyunConfig
| SSHConfig
| WebhookConfig
| CloudflareConfig
| TencentConfig
| HuaweicloudConfig
| QiniuConfig
| CloudflareConfig
| NamesiloConfig
| GodaddyConfig
| LocalConfig;
| LocalConfig
| SSHConfig
| WebhookConfig;
deleted?: string;
created?: string;
updated?: string;
};
export type QiniuConfig = {
accessKey: string;
secretKey: string;
};
export type WebhookConfig = {
url: string;
};
export type CloudflareConfig = {
dnsApiToken: string;
};
export type TencentConfig = {
secretId: string;
secretKey: string;
};
export type AliyunConfig = {
accessKeyId: string;
accessKeySecret: string;
};
export type TencentConfig = {
secretId: string;
secretKey: string;
};
export type HuaweicloudConfig = {
region: string;
accessKeyId: string;
secretAccessKey: string;
};
export type QiniuConfig = {
accessKey: string;
secretKey: string;
};
export type CloudflareConfig = {
dnsApiToken: string;
};
export type NamesiloConfig = {
apiKey: string;
};
export type GodaddyConfig = {
apiKey: string;
apiSecret: string;
};
export type LocalConfig = Record<string, string>;
export type SSHConfig = {
host: string;
port: string;
command: string;
username: string;
password?: string;
key?: string;
keyFile?: string;
certPath: string;
keyPath: string;
};
export type LocalConfig = {
command: string;
certPath: string;
keyPath: string;
export type WebhookConfig = {
url: string;
};
export const getUsageByConfigType = (configType: string): AccessUsage => {
switch (configType) {
case "aliyun":
case "tencent":
case "huaweicloud":
return "all";
case "ssh":
case "webhook":
case "qiniu":
case "local":
case "ssh":
case "webhook":
return "deploy";
case "cloudflare":
case "namesilo":
case "godaddy":
return "apply";
default:
return "all";
}

View File

@@ -1,13 +1,13 @@
import { Deployment, Pahse } from "./deployment";
export type Domain = {
id: string;
id?: string;
domain: string;
email?: string;
crontab: string;
access: string;
targetAccess?: string;
targetType: string;
targetType?: string;
expiredAt?: string;
phase?: Pahse;
phaseSuccess?: boolean;
@@ -26,6 +26,32 @@ export type Domain = {
expand?: {
lastDeployment?: Deployment;
};
applyConfig?: ApplyConfig;
deployConfig?: DeployConfig[];
};
export type KVType = {
key: string;
value: string;
};
export type DeployConfig = {
id?: string;
access: string;
type: string;
config?: {
[key: string]: string;
} & {
variables?: KVType[];
};
};
export type ApplyConfig = {
access: string;
email: string;
timeout?: number;
nameservers?: string;
};
export type Statistic = {
@@ -40,14 +66,14 @@ export const getLastDeployment = (domain: Domain): Deployment | undefined => {
};
export const targetTypeMap: Map<string, [string, string]> = new Map([
["aliyun-cdn", ["阿里云-CDN", "/imgs/providers/aliyun.svg"]],
["aliyun-oss", ["阿里云-OSS", "/imgs/providers/aliyun.svg"]],
["aliyun-dcdn", ["阿里云-DCDN", "/imgs/providers/aliyun.svg"]],
["tencent-cdn", ["腾讯云-CDN", "/imgs/providers/tencent.svg"]],
["ssh", ["SSH部署", "/imgs/providers/ssh.svg"]],
["qiniu-cdn", ["七牛云-CDN", "/imgs/providers/qiniu.svg"]],
["webhook", ["Webhook", "/imgs/providers/webhook.svg"]],
["local", ["本地部署", "/imgs/providers/local.svg"]],
["aliyun-oss", ["common.provider.aliyun.oss", "/imgs/providers/aliyun.svg"]],
["aliyun-cdn", ["common.provider.aliyun.cdn", "/imgs/providers/aliyun.svg"]],
["aliyun-dcdn", ["common.provider.aliyun.dcdn", "/imgs/providers/aliyun.svg"]],
["tencent-cdn", ["common.provider.tencent.cdn", "/imgs/providers/tencent.svg"]],
["qiniu-cdn", ["common.provider.qiniu.cdn", "/imgs/providers/qiniu.svg"]],
["local", ["common.provider.local", "/imgs/providers/local.svg"]],
["ssh", ["common.provider.ssh", "/imgs/providers/ssh.svg"]],
["webhook", ["common.provider.webhook", "/imgs/providers/webhook.svg"]],
]);
export const targetTypeKeys = Array.from(targetTypeMap.keys());

View File

@@ -50,8 +50,8 @@ export type NotifyChannelWebhook = {
};
export const defaultNotifyTemplate: NotifyTemplate = {
title: "您有{COUNT}张证书即将过期",
content: "有{COUNT}张证书即将过期,域名分别为{DOMAINS},请保持关注!",
title: "您有 {COUNT} 张证书即将过期",
content: "有 {COUNT} 张证书即将过期域名分别为 {DOMAINS}请保持关注!",
};
export type SSLProvider = "letsencrypt" | "zerossl";

View File

@@ -1 +1 @@
export const version = "Certimate v0.1.12";
export const version = "Certimate v0.2.1";

22
ui/src/i18n/index.ts Normal file
View File

@@ -0,0 +1,22 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import resources from "./locales";
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: "zh",
debug: true,
interpolation: {
escapeValue: false,
},
backend: {
loadPath: "/locales/{{lng}}.json",
},
});
export default i18n;

View File

@@ -0,0 +1,17 @@
import nlsCommon from "./nls.common.json";
import nlsLogin from "./nls.login.json";
import nlsDashboard from "./nls.dashboard.json";
import nlsSettings from "./nls.settings.json";
import nlsDomain from "./nls.domain.json";
import nlsAccess from "./nls.access.json";
import nlsHistory from "./nls.history.json";
export default Object.freeze({
...nlsCommon,
...nlsLogin,
...nlsDashboard,
...nlsSettings,
...nlsDomain,
...nlsAccess,
...nlsHistory,
});

View File

@@ -0,0 +1,77 @@
{
"access.page.title": "Authorization Management",
"access.authorization.tab": "Authorization",
"access.authorization.nodata": "Please add authorization to start deploying certificate.",
"access.authorization.add": "Add Authorization",
"access.authorization.edit": "Edit Authorization",
"access.authorization.copy": "Copy Authorization",
"access.authorization.delete": "Delete Authorization",
"access.authorization.delete.confirm": "Are you sure you want to delete the deployment authorization?",
"access.authorization.form.type.label": "Provider",
"access.authorization.form.type.placeholder": "Please select a provider",
"access.authorization.form.type.list": "Authorization List",
"access.authorization.form.name.label": "Name",
"access.authorization.form.name.placeholder": "Please enter authorization name",
"access.authorization.form.config.label": "Configuration Type",
"access.authorization.form.region.label": "Region",
"access.authorization.form.region.placeholder": "Please enter Region",
"access.authorization.form.access_key_id.label": "AccessKeyId",
"access.authorization.form.access_key_id.placeholder": "Please enter AccessKeyId",
"access.authorization.form.access_key_secret.label": "AccessKeySecret",
"access.authorization.form.access_key_secret.placeholder": "Please enter AccessKeySecret",
"access.authorization.form.access_key.label": "AccessKey",
"access.authorization.form.access_key.placeholder": "Please enter AccessKey",
"access.authorization.form.secret_id.label": "SecretId",
"access.authorization.form.secret_id.placeholder": "Please enter SecretId",
"access.authorization.form.secret_key.label": "SecretKey",
"access.authorization.form.secret_key.placeholder": "Please enter SecretKey",
"access.authorization.form.cloud_dns_api_token.label": "CLOUD_DNS_API_TOKEN",
"access.authorization.form.cloud_dns_api_token.placeholder": "Please enter CLOUD_DNS_API_TOKEN",
"access.authorization.form.godaddy_api_key.label": "GO_DADDY_API_KEY",
"access.authorization.form.godaddy_api_key.placeholder": "Please enter GO_DADDY_API_KEY",
"access.authorization.form.godaddy_api_secret.label": "GO_DADDY_API_SECRET",
"access.authorization.form.godaddy_api_secret.placeholder": "Please enter GO_DADDY_API_SECRET",
"access.authorization.form.namesilo_api_key.label": "NAMESILO_API_KEY",
"access.authorization.form.namesilo_api_key.placeholder": "Please enter NAMESILO_API_KEY",
"access.authorization.form.username.label": "Username",
"access.authorization.form.username.placeholder": "Please enter username",
"access.authorization.form.password.label": "Password",
"access.authorization.form.password.placeholder": "Please enter password",
"access.authorization.form.access_group.placeholder": "Please select a group",
"access.authorization.form.ssh_group.label": "Authorization Configuration Group (used to deploy a single domain certificate to multiple SSH hosts)",
"access.authorization.form.ssh_host.label": "Server Host",
"access.authorization.form.ssh_host.placeholder": "Please enter Host",
"access.authorization.form.ssh_port.label": "SSH Port",
"access.authorization.form.ssh_port.placeholder": "Please enter Port",
"access.authorization.form.ssh_key.label": "Key (Log in using private key)",
"access.authorization.form.ssh_key.placeholder": "Please enter Key",
"access.authorization.form.ssh_key_file.placeholder": "Please select file",
"access.authorization.form.ssh_key_path.label": "Private Key Save Path",
"access.authorization.form.ssh_key_path.placeholder": "Please enter private key save path",
"access.authorization.form.ssh_cert_path.label": "Certificate Save Path",
"access.authorization.form.ssh_cert_path.placeholder": "Please enter certificate save path",
"access.authorization.form.ssh_pre_command.label": "Pre-deployment Command",
"access.authorization.form.ssh_pre_command.placeholder": "Command to be executed before deploying the certificate",
"access.authorization.form.ssh_command.label": "Command",
"access.authorization.form.ssh_command.placeholder": "Please enter command",
"access.authorization.form.webhook_url.label": "Webhook URL",
"access.authorization.form.webhook_url.placeholder": "Please enter Webhook URL",
"access.group.tab": "Authorization Group",
"access.group.nodata": "No deployment authorization configuration yet, please add after starting use.",
"access.group.total": "Totally {{total}} deployment authorization configuration",
"access.group.add": "Add Group",
"access.group.delete": "Delete Group",
"access.group.delete.confirm": "Are you sure you want to delete the deployment authorization group?",
"access.group.form.name.label": "Group Name",
"access.group.form.name.errmsg.empty": "Please enter group name",
"access.group.domains": "All Authorizations",
"access.group.domains.nodata": "Please add a domain to start deploying the certificate."
}

View File

@@ -0,0 +1,72 @@
{
"common.save": "Save",
"common.save.succeeded.message": "Save Successful",
"common.save.failed.message": "Save Failed",
"common.add": "Add",
"common.edit": "Edit",
"common.copy": "Copy",
"common.download": "Download",
"common.delete": "Delete",
"common.delete.succeeded.message": "Delete Successful",
"common.delete.failed.message": "Delete Failed",
"common.next": "Next",
"common.confirm": "Confirm",
"common.cancel": "Cancel",
"common.submit": "Submit",
"common.update": "Update",
"common.update.succeeded.message": "Update Successful",
"common.update.failed.message": "Update Failed",
"common.text.domain": "Domain",
"common.text.domain.empty": "No Domain",
"common.text.ip": "IP Address",
"common.text.ip.empty": "No IP address",
"common.text.dns": "Domain Name Server",
"common.text.dns.empty": "No DNS",
"common.text.ca": "Certificate Authority",
"common.text.provider": "Provider",
"common.text.name": "Name",
"common.text.created_at": "Created At",
"common.text.updated_at": "Updated At",
"common.text.operations": "Operations",
"common.text.nodata": "No data available",
"common.menu.settings": "Settings",
"common.menu.logout": "Logout",
"common.menu.document": "Document",
"common.pagination.next": "Next",
"common.pagination.prev": "Previous",
"common.pagination.more": "More pages",
"common.theme.light": "Light",
"common.theme.dark": "Dark",
"common.theme.system": "System",
"common.errmsg.string_max": "Please enter no more than {{max}} characters",
"common.errmsg.email_invalid": "Please enter a valid email address",
"common.errmsg.email_empty": "Please enter email",
"common.errmsg.email_duplicate": "Email already exists",
"common.errmsg.domain_invalid": "Please enter domain",
"common.errmsg.host_invalid": "Please enter the correct domain name or IP",
"common.errmsg.ip_invalid": "Please enter IP",
"common.errmsg.url_invalid": "Please enter a valid URL",
"common.provider.aliyun": "Alibaba Cloud",
"common.provider.aliyun.cdn": "Alibaba Cloud-CDN",
"common.provider.aliyun.oss": "Alibaba Cloud-OSS",
"common.provider.aliyun.dcdn": "Alibaba Cloud-DCDN",
"common.provider.tencent": "Tencent",
"common.provider.tencent.cdn": "Tencent-CDN",
"common.provider.huaweicloud": "Huawei Cloud",
"common.provider.qiniu": "Qiniu",
"common.provider.qiniu.cdn": "Qiniu-CDN",
"common.provider.cloudflare": "Cloudflare",
"common.provider.namesilo": "Namesilo",
"common.provider.godaddy": "GoDaddy",
"common.provider.local": "Local Deployment",
"common.provider.ssh": "SSH Deployment",
"common.provider.webhook": "Webhook",
"common.provider.dingtalk": "DingTalk",
"common.provider.telegram": "Telegram"
}

View File

@@ -0,0 +1,11 @@
{
"dashboard.page.title": "Dashboard",
"dashboard.statistics.all": "All",
"dashboard.statistics.near_expired": "About to Expire",
"dashboard.statistics.enabled": "Enabled",
"dashboard.statistics.disabled": "Not Enabled",
"dashboard.statistics.unit": "",
"dashboard.history": "Deployment History"
}

View File

@@ -0,0 +1,65 @@
{
"domain.page.title": "Domain List",
"domain.nodata": "Please add a domain to start deploying the certificate.",
"domain.add": "Add Domain",
"domain.edit": "Edit Domain",
"domain.delete": "Delete Domain",
"domain.delete.confirm": "Are you sure you want to delete this domain?",
"domain.history": "Deployment History",
"domain.deploy": "Deploy Now",
"domain.deploy.started.message": "Deploy Started",
"domain.deploy.started.tips": "Deployment initiated, please check the deployment log later.",
"domain.deploy.failed.message": "Execution Failed",
"domain.deploy.failed.tips": "Execution failed, please check the details in <1>Deployment History</1>.",
"domain.deploy_forced": "Force Deployment",
"domain.props.expiry": "Validity Period",
"domain.props.expiry.date1": "Valid for {{date}} days",
"domain.props.expiry.date2": "Expiry on {{date}}",
"domain.props.last_execution_status": "Last Execution Status",
"domain.props.last_execution_stage": "Last Execution Stage",
"domain.props.last_execution_time": "Last Execution Time",
"domain.props.enable": "Enable",
"domain.props.enable.enabled": "Enable",
"domain.props.enable.disabled": "Disable",
"domain.application.tab": "Apply Settings",
"domain.application.form.domain.added.message": "Domain added successfully",
"domain.application.form.domain.changed.message": "Domain updated successfully",
"domain.application.form.email.label": "Email",
"domain.application.form.email.tips": "(A email is required to apply for a certificate)",
"domain.application.form.email.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.unsaved.message": "Please save applyment configuration first",
"domain.deployment.tab": "Deploy Settings",
"domain.deployment.nodata": "Deployment not added yet",
"domain.deployment.form.type.label": "Deploy Method",
"domain.deployment.form.type.placeholder": "Please select deploy method",
"domain.deployment.form.type.list": "Deploy Method List",
"domain.deployment.form.access.label": "Access Configuration",
"domain.deployment.form.access.placeholder": "Please select provider authorization configuration",
"domain.deployment.form.access.list": "Provider Authorization Configurations",
"domain.deployment.form.cdn_domain.label": "Deploy to domain",
"domain.deployment.form.cdn_domain.placeholder": "Please enter CDN domain",
"domain.deployment.form.oss_endpoint.label": "Endpoint",
"domain.deployment.form.oss_bucket": "Bucket",
"domain.deployment.form.oss_bucket.placeholder": "Please enter Bucket",
"domain.deployment.form.variables.label": "Variable",
"domain.deployment.form.variables.key": "Name",
"domain.deployment.form.variables.value": "Value",
"domain.deployment.form.variables.empty": "Variable not added yet",
"domain.deployment.form.variables.key.required": "Variable name cannot be empty",
"domain.deployment.form.variables.value.required": "Variable value cannot be empty",
"domain.deployment.form.variables.key.placeholder": "Variable name",
"domain.deployment.form.variables.value.placeholder": "Variable value"
}

View File

@@ -0,0 +1,15 @@
{
"history.page.title": "Deployment",
"history.nodata": "You have not created any deployments yet, please add a domain to start deployment!",
"history.props.domain": "Domain",
"history.props.status": "Status",
"history.props.stage": "Stage",
"history.props.stage.progress.check": "Check",
"history.props.stage.progress.apply": "Apply",
"history.props.stage.progress.deploy": "Deploy",
"history.props.last_execution_time": "Last Execution Time",
"history.log": "Log"
}

View File

@@ -0,0 +1,9 @@
{
"login.username.label": "Username",
"login.username.placeholder": "Username/Email",
"login.username.errmsg.invalid": "Please enter a valid email address",
"login.password.label": "Password",
"login.password.placeholder": "Password",
"login.password.errmsg.invalid": "Password should be at least 10 characters",
"login.submit": "Log In"
}

View File

@@ -0,0 +1,41 @@
{
"settings.page.title": "Settings",
"settings.account.relogin.message": "Please login again",
"settings.account.tab": "Account",
"settings.account.email.label": "Email",
"settings.account.email.placeholder": "Please enter email",
"settings.account.email.errmsg.invalid": "Please enter a valid email address",
"settings.account.email.changed.message": "Account email altered successfully",
"settings.account.email.failed.message": "Account email alteration failed",
"settings.password.tab": "Password",
"settings.password.current_password.label": "Current Password",
"settings.password.current_password.placeholder": "Please enter the current password",
"settings.password.new_password.label": "New Password",
"settings.password.new_password.placeholder": "Please enter the new password",
"settings.password.confirm_password.label": "Confirm Password",
"settings.password.confirm_password.placeholder": "Please enter the new password again",
"settings.password.password.errmsg.length": "Password should be at least 10 characters",
"settings.password.password.errmsg.not_matched": "Passwords do not match",
"settings.password.changed.message": "Password changed successfully",
"settings.password.failed.message": "Password change failed",
"settings.notification.tab": "Notification",
"settings.notification.template.label": "Template",
"settings.notification.template.saved.message": "Notification template saved successfully",
"settings.notification.template.variables.tips.title": "Optional variables ({COUNT}: number of expiring soon)",
"settings.notification.template.variables.tips.content": "Optional variables ({COUNT}: number of expiring soon. {DOMAINS}: Domain list)",
"settings.notification.config.enable": "Enable",
"settings.notification.config.saved.message": "Configuration saved successfully",
"settings.notification.config.failed.message": "Configuration save failed",
"settings.notification.dingtalk.secret.placeholder": "Signature for signed addition",
"settings.notification.url.errmsg.invalid": "Invalid Url format",
"settings.ca.tab": "Certificate Authority",
"settings.ca.provider.errmsg.empty": "Please select a Certificate Authority",
"settings.ca.eab_kid.errmsg.empty": "Please enter EAB_KID",
"settings.ca.eab_hmac_key.errmsg.empty": "Please enter EAB_HMAC_KEY.",
"settings.ca.eab_kid_hmac_key.errmsg.empty": "Please enter EAB_KID and EAB_HMAC_KEY"
}

View File

@@ -0,0 +1,17 @@
import { Resource } from "i18next";
import zh from "./zh";
import en from "./en";
const resources: Resource = {
zh: {
name: "简体中文",
translation: zh,
},
en: {
name: "English",
translation: en,
},
};
export default resources;

View File

@@ -0,0 +1,17 @@
import nlsCommon from "./nls.common.json";
import nlsLogin from "./nls.login.json";
import nlsDashboard from "./nls.dashboard.json";
import nlsSettings from "./nls.settings.json";
import nlsDomain from "./nls.domain.json";
import nlsAccess from "./nls.access.json";
import nlsHistory from "./nls.history.json";
export default Object.freeze({
...nlsCommon,
...nlsLogin,
...nlsDashboard,
...nlsSettings,
...nlsDomain,
...nlsAccess,
...nlsHistory,
});

View File

@@ -0,0 +1,78 @@
{
"access.page.title": "授权管理",
"access.authorization.tab": "授权",
"access.authorization.nodata": "请添加授权开始部署证书吧。",
"access.authorization.add": "新增授权",
"access.authorization.edit": "编辑授权",
"access.authorization.copy": "复制授权",
"access.authorization.delete": "删除授权",
"access.authorization.delete.confirm": "确定要删除授权吗?",
"access.authorization.form.type.label": "服务商",
"access.authorization.form.type.placeholder": "请选择服务商",
"access.authorization.form.type.list": "服务商列表",
"access.authorization.form.name.label": "名称",
"access.authorization.form.name.placeholder": "请输入授权名称",
"access.authorization.form.config.label": "配置类型",
"access.authorization.form.region.label": "Region",
"access.authorization.form.region.placeholder": "请输入区域",
"access.authorization.form.access_key_id.label": "AccessKeyId",
"access.authorization.form.access_key_id.placeholder": "请输入 AccessKeyId",
"access.authorization.form.access_key_secret.label": "AccessKeySecret",
"access.authorization.form.access_key_secret.placeholder": "请输入 AccessKeySecret",
"access.authorization.form.access_key.label": "AccessKey",
"access.authorization.form.access_key.placeholder": "请输入 AccessKey",
"access.authorization.form.secret_id.label": "SecretId",
"access.authorization.form.secret_id.placeholder": "请输入 SecretId",
"access.authorization.form.secret_key.label": "SecretKey",
"access.authorization.form.secret_key.placeholder": "请输入 SecretKey",
"access.authorization.form.cloud_dns_api_token.label": "CLOUD_DNS_API_TOKEN",
"access.authorization.form.cloud_dns_api_token.placeholder": "请输入 CLOUD_DNS_API_TOKEN",
"access.authorization.form.godaddy_api_key.label": "GO_DADDY_API_KEY",
"access.authorization.form.godaddy_api_key.placeholder": "请输入 GO_DADDY_API_KEY",
"access.authorization.form.godaddy_api_secret.label": "GO_DADDY_API_SECRET",
"access.authorization.form.godaddy_api_secret.placeholder": "请输入 GO_DADDY_API_SECRET",
"access.authorization.form.namesilo_api_key.label": "NAMESILO_API_KEY",
"access.authorization.form.namesilo_api_key.placeholder": "请输入 NAMESILO_API_KEY",
"access.authorization.form.username.label": "用户名",
"access.authorization.form.username.placeholder": "请输入用户名",
"access.authorization.form.password.label": "密码",
"access.authorization.form.password.placeholder": "请输入密码",
"access.authorization.form.access_group.placeholder": "请选择分组",
"access.authorization.form.ssh_group.label": "授权配置组(用于将一个域名证书部署到多个 SSH 主机)",
"access.authorization.form.ssh_host.label": "服务器 Host",
"access.authorization.form.ssh_host.placeholder": "请输入 Host",
"access.authorization.form.ssh_port.label": "SSH 端口",
"access.authorization.form.ssh_port.placeholder": "请输入 Port",
"access.authorization.form.ssh_key.label": "Key使用私钥登录",
"access.authorization.form.ssh_key.placeholder": "请输入 Key",
"access.authorization.form.ssh_key_file.placeholder": "请选择文件",
"access.authorization.form.ssh_key.label.passphrase": "私钥密码",
"access.authorization.form.ssh_key_path.label": "私钥保存路径",
"access.authorization.form.ssh_key_path.placeholder": "请输入私钥保存路径",
"access.authorization.form.ssh_cert_path.label": "证书保存路径",
"access.authorization.form.ssh_cert_path.placeholder": "请输入证书保存路径",
"access.authorization.form.ssh_pre_command.label": "前置 Command",
"access.authorization.form.ssh_pre_command.placeholder": "在部署证书前执行的前置命令",
"access.authorization.form.ssh_command.label": "Command",
"access.authorization.form.ssh_command.placeholder": "请输入要执行的命令",
"access.authorization.form.webhook_url.label": "Webhook URL",
"access.authorization.form.webhook_url.placeholder": "请输入 Webhook URL",
"access.group.tab": "授权组",
"access.group.nodata": "暂无部署授权配置,请添加后开始使用吧",
"access.group.total": "共有 {{total}} 个部署授权配置",
"access.group.add": "添加授权组",
"access.group.delete": "删除组",
"access.group.delete.confirm": "确定要删除部署授权组吗?",
"access.group.form.name.label": "组名",
"access.group.form.name.errmsg.empty": "请输入组名",
"access.group.domains": "所有授权",
"access.group.domains.nodata": "请添加域名开始部署证书吧。"
}

View File

@@ -0,0 +1,72 @@
{
"common.add": "新增",
"common.save": "保存",
"common.save.succeeded.message": "保存成功",
"common.save.failed.message": "保存失败",
"common.edit": "编辑",
"common.copy": "复制",
"common.download": "下载",
"common.delete": "刪除",
"common.delete.succeeded.message": "删除成功",
"common.delete.failed.message": "删除失败",
"common.next": "下一步",
"common.confirm": "确认",
"common.cancel": "取消",
"common.submit": "提交",
"common.update": "更新",
"common.update.succeeded.message": "修改成功",
"common.update.failed.message": "修改失败",
"common.text.domain": "域名",
"common.text.domain.empty": "无域名",
"common.text.ip": "IP 地址",
"common.text.ip.empty": "无 IP 地址",
"common.text.dns": "DNS域名服务器",
"common.text.dns.empty": "无 DNS 地址",
"common.text.ca": "CA证书颁发机构",
"common.text.name": "名称",
"common.text.provider": "服务商",
"common.text.created_at": "创建时间",
"common.text.updated_at": "更新时间",
"common.text.operations": "操作",
"common.text.nodata": "暂无数据",
"common.menu.settings": "系统设置",
"common.menu.logout": "退出登录",
"common.menu.document": "文档",
"common.pagination.next": "下一页",
"common.pagination.prev": "上一页",
"common.pagination.more": "更多",
"common.theme.light": "浅色",
"common.theme.dark": "暗黑",
"common.theme.system": "跟随系统",
"common.errmsg.string_max": "请输入不超过 {{max}} 个字符",
"common.errmsg.email_empty": "请输入邮箱",
"common.errmsg.email_invalid": "请输入正确的邮箱",
"common.errmsg.email_duplicate": "邮箱已存在",
"common.errmsg.domain_invalid": "请输入正确的域名",
"common.errmsg.host_invalid": "请输入正确的域名或 IP 地址",
"common.errmsg.ip_invalid": "请输入正确的 IP 地址",
"common.errmsg.url_invalid": "请输入正确的 URL",
"common.provider.tencent": "腾讯云",
"common.provider.tencent.cdn": "腾讯云-CDN",
"common.provider.aliyun": "阿里云",
"common.provider.aliyun.cdn": "阿里云-CDN",
"common.provider.aliyun.oss": "阿里云-OSS",
"common.provider.aliyun.dcdn": "阿里云-DCDN",
"common.provider.huaweicloud": "华为云",
"common.provider.qiniu": "七牛云",
"common.provider.qiniu.cdn": "七牛云-CDN",
"common.provider.cloudflare": "Cloudflare",
"common.provider.namesilo": "Namesilo",
"common.provider.godaddy": "GoDaddy",
"common.provider.local": "本地部署",
"common.provider.ssh": "SSH 部署",
"common.provider.webhook": "Webhook",
"common.provider.dingtalk": "钉钉",
"common.provider.telegram": "Telegram"
}

View File

@@ -0,0 +1,11 @@
{
"dashboard.page.title": "仪表盘",
"dashboard.statistics.all": "所有",
"dashboard.statistics.near_expired": "即将过期",
"dashboard.statistics.enabled": "启用中",
"dashboard.statistics.disabled": "未启用",
"dashboard.statistics.unit": "个",
"dashboard.history": "部署历史"
}

View File

@@ -0,0 +1,65 @@
{
"domain.page.title": "域名列表",
"domain.nodata": "请添加域名开始部署证书吧。",
"domain.add": "新增域名",
"domain.edit": "编辑域名",
"domain.delete": "删除域名",
"domain.delete.confirm": "确定要删除域名吗?",
"domain.history": "部署历史",
"domain.deploy": "立即部署",
"domain.deploy.started.message": "开始部署",
"domain.deploy.started.tips": "已发起部署,请稍后查看部署日志。",
"domain.deploy.failed.message": "执行失败",
"domain.deploy.failed.tips": "执行失败,请在 <1>部署历史</1> 查看详情。",
"domain.deploy_forced": "强行部署",
"domain.props.expiry": "有效期限",
"domain.props.expiry.date1": "有效期 {{date}} 天",
"domain.props.expiry.date2": "{{date}} 到期",
"domain.props.last_execution_status": "最近执行状态",
"domain.props.last_execution_stage": "最近执行阶段",
"domain.props.last_execution_time": "最近执行时间",
"domain.props.enable": "是否启用",
"domain.props.enable.enabled": "启用",
"domain.props.enable.disabled": "禁用",
"domain.application.tab": "申请配置",
"domain.application.form.domain.added.message": "域名添加成功",
"domain.application.form.domain.changed.message": "域名编辑成功",
"domain.application.form.email.label": "邮箱",
"domain.application.form.email.tips": "(申请证书需要提供邮箱)",
"domain.application.form.email.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.unsaved.message": "请先保存申请配置",
"domain.deployment.tab": "部署配置",
"domain.deployment.nodata": "暂无部署配置,请添加后开始部署证书吧",
"domain.deployment.form.type.label": "部署方式",
"domain.deployment.form.type.placeholder": "请选择部署方式",
"domain.deployment.form.type.list": "支持的部署方式",
"domain.deployment.form.access.label": "授权配置",
"domain.deployment.form.access.placeholder": "请选择授权配置",
"domain.deployment.form.access.list": "已有的服务商授权配置",
"domain.deployment.form.cdn_domain.label": "部署到域名",
"domain.deployment.form.cdn_domain.placeholder": "请输入 CDN 域名",
"domain.deployment.form.oss_endpoint.label": "Endpoint",
"domain.deployment.form.oss_bucket": "存储桶",
"domain.deployment.form.oss_bucket.placeholder": "请输入存储桶名",
"domain.deployment.form.variables.label": "变量",
"domain.deployment.form.variables.key": "变量名",
"domain.deployment.form.variables.value": "值",
"domain.deployment.form.variables.empty": "尚未添加变量",
"domain.deployment.form.variables.key.required": "变量名不能为空",
"domain.deployment.form.variables.value.required": "变量值不能为空",
"domain.deployment.form.variables.key.placeholder": "请输入变量名",
"domain.deployment.form.variables.value.placeholder": "请输入变量值"
}

View File

@@ -0,0 +1,15 @@
{
"history.page.title": "部署",
"history.nodata": "你暂未创建任何部署,请先添加域名进行部署吧!",
"history.props.domain": "域名",
"history.props.status": "状态",
"history.props.stage": "阶段",
"history.props.stage.progress.check": "检查",
"history.props.stage.progress.apply": "获取",
"history.props.stage.progress.deploy": "部署",
"history.props.last_execution_time": "最近执行时间",
"history.log": "日志"
}

View File

@@ -0,0 +1,9 @@
{
"login.username.label": "用户名",
"login.username.placeholder": "请输入用户名/邮箱",
"login.username.errmsg.invalid": "请输入正确的用户名/邮箱",
"login.password.label": "密码",
"login.password.placeholder": "请输入密码",
"login.password.errmsg.invalid": "密码至少 10 个字符",
"login.submit": "登录"
}

View File

@@ -0,0 +1,41 @@
{
"settings.page.title": "系统设置",
"settings.account.relogin.message": "请重新登录",
"settings.account.tab": "账号",
"settings.account.email.label": "登录邮箱",
"settings.account.email.errmsg.invalid": "请输入正确的邮箱地址",
"settings.account.email.placeholder": "请输入邮箱",
"settings.account.email.changed.message": "修改账户邮箱成功",
"settings.account.email.failed.message": "修改账户邮箱失败",
"settings.password.tab": "密码",
"settings.password.password.errmsg.length": "密码至少10个字符",
"settings.password.password.errmsg.not_matched": "两次密码不一致",
"settings.password.current_password.label": "当前密码",
"settings.password.current_password.placeholder": "请输入旧密码",
"settings.password.new_password.label": "新密码",
"settings.password.new_password.placeholder": "请输入新密码",
"settings.password.confirm_password.label": "确认密码",
"settings.password.confirm_password.placeholder": "请再次输入新密码",
"settings.password.changed.message": "修改密码成功",
"settings.password.failed.message": "修改密码失败",
"settings.notification.tab": "消息推送",
"settings.notification.template.label": "内容模板",
"settings.notification.template.saved.message": "通知模板保存成功",
"settings.notification.template.variables.tips.title": "可选的变量({COUNT}: 即将过期张数)",
"settings.notification.template.variables.tips.content": "可选的变量({COUNT}: 即将过期张数;{DOMAINS}: 域名列表)",
"settings.notification.config.enable": "是否启用",
"settings.notification.config.saved.message": "配置保存成功",
"settings.notification.config.failed.message": "配置保存失败",
"settings.notification.dingtalk.secret.placeholder": "加签的签名",
"settings.notification.url.errmsg.invalid": "URL 格式不正确",
"settings.ca.tab": "证书颁发机构CA",
"settings.ca.provider.errmsg.empty": "请选择证书分发机构",
"settings.ca.eab_kid.errmsg.empty": "请输入EAB_KID",
"settings.ca.eab_hmac_key.errmsg.empty": "请输入EAB_HMAC_KEY",
"settings.ca.eab_kid_hmac_key.errmsg.empty": "请输入EAB_KID和EAB_HMAC_KEY"
}

View File

@@ -4,6 +4,7 @@ import "./global.css";
import { RouterProvider } from "react-router-dom";
import { router } from "./router.tsx";
import { ThemeProvider } from "./components/ThemeProvider.tsx";
import "@/i18n";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>

View File

@@ -6,6 +6,7 @@ import {
useNavigate,
} from "react-router-dom";
import { CircleUser, Earth, History, Home, Menu, Server } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
@@ -13,8 +14,6 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
@@ -22,12 +21,14 @@ import { cn } from "@/lib/utils";
import { ConfigProvider } from "@/providers/config";
import { getPb } from "@/repository/api";
import { ThemeToggle } from "@/components/ThemeToggle";
import LocaleToggle from "@/components/LocaleToggle";
import Version from "@/components/certimate/Version";
export default function Dashboard() {
const navigate = useNavigate();
const location = useLocation();
const { t } = useTranslation();
if (!getPb().authStore.isValid || !getPb().authStore.isAdmin) {
return <Navigate to="/login" />;
@@ -70,7 +71,7 @@ export default function Dashboard() {
)}
>
<Home className="h-4 w-4" />
{t("dashboard.page.title")}
</Link>
<Link
to="/domains"
@@ -80,7 +81,7 @@ export default function Dashboard() {
)}
>
<Earth className="h-4 w-4" />
{t("domain.page.title")}
</Link>
<Link
to="/access"
@@ -90,7 +91,7 @@ export default function Dashboard() {
)}
>
<Server className="h-4 w-4" />
{t("access.page.title")}
</Link>
<Link
@@ -101,7 +102,7 @@ export default function Dashboard() {
)}
>
<History className="h-4 w-4" />
{t("history.page.title")}
</Link>
</nav>
</div>
@@ -138,7 +139,7 @@ export default function Dashboard() {
)}
>
<Home className="h-5 w-5" />
{t("dashboard.page.title")}
</Link>
<Link
to="/domains"
@@ -148,7 +149,7 @@ export default function Dashboard() {
)}
>
<Earth className="h-5 w-5" />
{t("domain.page.title")}
</Link>
<Link
to="/access"
@@ -158,7 +159,7 @@ export default function Dashboard() {
)}
>
<Server className="h-5 w-5" />
{t("access.page.title")}
</Link>
<Link
@@ -169,13 +170,14 @@ export default function Dashboard() {
)}
>
<History className="h-5 w-5" />
{t("history.page.title")}
</Link>
</nav>
</SheetContent>
</Sheet>
<div className="w-full flex-1"></div>
<ThemeToggle />
<LocaleToggle />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@@ -188,15 +190,11 @@ export default function Dashboard() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel></DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleSettingClick}>
{t("common.menu.settings")}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleLogoutClick}>
退
{t("common.menu.logout")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -4,11 +4,13 @@ import { KeyRound, Megaphone, ShieldCheck, UserRound } from "lucide-react";
import { useEffect, useState } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const SettingLayout = () => {
const location = useLocation();
const [tabValue, setTabValue] = useState("account");
const navigate = useNavigate();
const { t } = useTranslation();
useEffect(() => {
const pathname = location.pathname;
@@ -20,7 +22,7 @@ const SettingLayout = () => {
<div>
<Toaster />
<div className="text-muted-foreground border-b dark:border-stone-500 py-5">
{t("settings.page.title")}
</div>
<div className="w-full mt-5 p-0 md:p-3 flex justify-center">
<Tabs defaultValue="account" className="w-full" value={tabValue}>
@@ -33,8 +35,9 @@ const SettingLayout = () => {
className="px-5"
>
<UserRound size={14} />
<div className="ml-1"></div>
<div className="ml-1">{t("settings.account.tab")}</div>
</TabsTrigger>
<TabsTrigger
value="password"
onClick={() => {
@@ -43,7 +46,7 @@ const SettingLayout = () => {
className="px-5"
>
<KeyRound size={14} />
<div className="ml-1"></div>
<div className="ml-1">{t("settings.password.tab")}</div>
</TabsTrigger>
<TabsTrigger
@@ -54,8 +57,9 @@ const SettingLayout = () => {
className="px-5"
>
<Megaphone size={14} />
<div className="ml-1"></div>
<div className="ml-1">{t("settings.notification.tab")}</div>
</TabsTrigger>
<TabsTrigger
value="ssl-provider"
onClick={() => {
@@ -64,7 +68,7 @@ const SettingLayout = () => {
className="px-5"
>
<ShieldCheck size={14} />
<div className="ml-1"></div>
<div className="ml-1">{t("settings.ca.tab")}</div>
</TabsTrigger>
</TabsList>
<TabsContent value={tabValue}>

View File

@@ -9,8 +9,20 @@ import { Access as AccessType, accessTypeMap } from "@/domain/access";
import { convertZulu2Beijing } from "@/lib/time";
import { useConfig } from "@/providers/config";
import { remove } from "@/repository/access";
import { t } from "i18next";
import { Key } from "lucide-react";
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 { config, deleteAccess } = useConfig();
@@ -46,11 +58,11 @@ const Access = () => {
return (
<div className="">
<div className="flex justify-between items-center">
<div className="text-muted-foreground"></div>
<div className="text-muted-foreground">{t("access.page.title")}</div>
{tab != "access_group" ? (
<AccessEdit trigger={<Button></Button>} op="add" />
<AccessEdit trigger={<Button>{t("access.authorization.add")}</Button>} op="add" />
) : (
<AccessGroupEdit trigger={<Button></Button>} />
<AccessGroupEdit trigger={<Button>{t("access.group.add")}</Button>} />
)}
</div>
@@ -66,7 +78,7 @@ const Access = () => {
handleTabItemClick("access");
}}
>
{t("access.authorization.tab")}
</TabsTrigger>
<TabsTrigger
value="access_group"
@@ -74,7 +86,7 @@ const Access = () => {
handleTabItemClick("access_group");
}}
>
{t("access.group.tab")}
</TabsTrigger>
</TabsList>
<TabsContent value="access">
@@ -85,10 +97,10 @@ const Access = () => {
</span>
<div className="text-center text-sm text-muted-foreground mt-3">
{t("access.authorization.nodata")}
</div>
<AccessEdit
trigger={<Button></Button>}
trigger={<Button>{t("access.authorization.add")}</Button>}
op="add"
className="mt-3"
/>
@@ -96,15 +108,12 @@ const Access = () => {
) : (
<>
<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"></div>
<div className="w-48"></div>
<div className="w-48">{t("common.text.name")}</div>
<div className="w-48">{t("common.text.provider")}</div>
<div className="w-52"></div>
<div className="w-52"></div>
<div className="grow"></div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
<div className="w-60">{t("common.text.created_at")}</div>
<div className="w-60">{t("common.text.updated_at")}</div>
<div className="grow">{t("common.text.operations")}</div>
</div>
{accesses
.filter((item) => {
@@ -124,37 +133,67 @@ const Access = () => {
src={accessTypeMap.get(access.configType)?.[1]}
className="w-6"
/>
<div>{accessTypeMap.get(access.configType)?.[0]}</div>
<div>
{t(accessTypeMap.get(access.configType)?.[0] || "")}
</div>
</div>
<div className="sm:w-52 w-full pt-1 sm:pt-0 flex items-center">
{" "}
<div className="sm:w-60 w-full pt-1 sm:pt-0 flex items-center">
{access.created && convertZulu2Beijing(access.created)}
</div>
<div className="sm:w-52 w-full pt-1 sm:pt-0 flex items-center">
{" "}
<div className="sm:w-60 w-full pt-1 sm:pt-0 flex items-center">
{access.updated && convertZulu2Beijing(access.updated)}
</div>
<div className="flex items-center grow justify-start pt-1 sm:pt-0">
<AccessEdit
trigger={
<Button variant={"link"} className="p-0">
{t("common.edit")}
</Button>
}
op="edit"
data={access}
/>
<Separator orientation="vertical" className="h-4 mx-2" />
<Button
variant={"link"}
className="p-0"
onClick={() => {
handleDelete(access);
}}
>
</Button>
<AccessEdit
trigger={
<Button variant={"link"} className="p-0">
{t("common.copy")}
</Button>
}
op="copy"
data={access}
/>
<Separator orientation="vertical" className="h-4 mx-2" />
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"link"} className="p-0">
{t("common.delete")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="dark:text-gray-200">
{t("access.authorization.delete")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("access.authorization.delete.confirm")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="dark:text-gray-200">
{t("common.cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleDelete(access);
}}
>
{t("common.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
))}

View File

@@ -24,12 +24,14 @@ import {
} from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const Dashboard = () => {
const [statistic, setStatistic] = useState<Statistic>();
const [deployments, setDeployments] = useState<Deployment[]>();
const navigate = useNavigate();
const { t } = useTranslation();
useEffect(() => {
const fetchStatistic = async () => {
@@ -55,7 +57,7 @@ const Dashboard = () => {
return (
<div className="flex flex-col">
<div className="flex justify-between items-center">
<div className="text-muted-foreground"></div>
<div className="text-muted-foreground">{t("dashboard.page.title")}</div>
</div>
<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">
@@ -63,7 +65,9 @@ const Dashboard = () => {
<SquareSigma size={48} strokeWidth={1} className="text-blue-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold"></div>
<div className="text-muted-foreground font-semibold">
{t("dashboard.statistics.all")}
</div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.total ? (
@@ -74,7 +78,9 @@ const Dashboard = () => {
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div>
<div className="ml-1 text-stone-700 dark:text-stone-200">
{t("dashboard.statistics.unit")}
</div>
</div>
</div>
</div>
@@ -84,7 +90,9 @@ const Dashboard = () => {
<CalendarX2 size={48} strokeWidth={1} className="text-red-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold"></div>
<div className="text-muted-foreground font-semibold">
{t("dashboard.statistics.near_expired")}
</div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.expired ? (
@@ -95,7 +103,9 @@ const Dashboard = () => {
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div>
<div className="ml-1 text-stone-700 dark:text-stone-200">
{t("dashboard.statistics.unit")}
</div>
</div>
</div>
</div>
@@ -109,7 +119,9 @@ const Dashboard = () => {
/>
</div>
<div>
<div className="text-muted-foreground font-semibold"></div>
<div className="text-muted-foreground font-semibold">
{t("dashboard.statistics.enabled")}
</div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.enabled ? (
@@ -120,7 +132,9 @@ const Dashboard = () => {
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div>
<div className="ml-1 text-stone-700 dark:text-stone-200">
{t("dashboard.statistics.unit")}
</div>
</div>
</div>
</div>
@@ -130,7 +144,9 @@ const Dashboard = () => {
<Ban size={48} strokeWidth={1} className="text-gray-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold"></div>
<div className="text-muted-foreground font-semibold">
{t("dashboard.statistics.disabled")}
</div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.disabled ? (
@@ -144,28 +160,33 @@ const Dashboard = () => {
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div>
<div className="ml-1 text-stone-700 dark:text-stone-200">
{t("dashboard.statistics.unit")}
</div>
</div>
</div>
</div>
</div>
<div className="my-4">
<hr />
</div>
<div>
<div className="text-muted-foreground mt-5 text-sm"></div>
<div className="text-muted-foreground mt-5 text-sm">
{t("dashboard.history")}
</div>
{deployments?.length == 0 ? (
<>
<Alert className="max-w-[40em] mt-10">
<AlertTitle></AlertTitle>
<AlertTitle>{t("common.text.nodata")}</AlertTitle>
<AlertDescription>
<div className="flex items-center mt-5">
<div>
<Smile className="text-yellow-400" size={36} />
</div>
<div className="ml-2">
{" "}
</div>
<div className="ml-2"> {t("history.nodata")}</div>
</div>
<div className="mt-2 flex justify-end">
<Button
@@ -173,7 +194,7 @@ const Dashboard = () => {
navigate("/edit");
}}
>
{t("domain.add")}
</Button>
</div>
</AlertDescription>
@@ -182,16 +203,15 @@ 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="w-48"></div>
<div className="w-48">{t("history.props.domain")}</div>
<div className="w-24"></div>
<div className="w-56"></div>
<div className="w-56 sm:ml-2 text-center"></div>
<div className="w-24">{t("history.props.status")}</div>
<div className="w-56">{t("history.props.stage")}</div>
<div className="w-56 sm:ml-2 text-center">
{t("history.props.last_execution_time")}
</div>
<div className="grow"></div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
<div className="grow">{t("common.text.operations")}</div>
</div>
{deployments?.map((deployment) => (
@@ -200,7 +220,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"
>
<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 className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
<DeployState deployment={deployment} />
@@ -218,14 +245,14 @@ const Dashboard = () => {
<Sheet>
<SheetTrigger asChild>
<Button variant={"link"} className="p-0">
{t("history.log")}
</Button>
</SheetTrigger>
<SheetContent className="sm:max-w-5xl">
<SheetHeader>
<SheetTitle>
{deployment.expand.domain?.domain}-{deployment.id}
{t("history.log")}
</SheetTitle>
</SheetHeader>
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">

View File

@@ -1,4 +1,3 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -24,33 +23,43 @@ import {
} from "@/components/ui/select";
import { useConfig } from "@/providers/config";
import { useEffect, useState } from "react";
import { Domain, targetTypeKeys, targetTypeMap } from "@/domain/domain";
import { DeployConfig, Domain } from "@/domain/domain";
import { save, get } from "@/repository/domains";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
import { useToast } from "@/components/ui/use-toast";
import { Toaster } from "@/components/ui/toaster";
import { useLocation, useNavigate } from "react-router-dom";
import { useLocation } from "react-router-dom";
import { Plus } from "lucide-react";
import { AccessEdit } from "@/components/certimate/AccessEdit";
import { accessTypeMap } from "@/domain/access";
import EmailsEdit from "@/components/certimate/EmailsEdit";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { EmailsSetting } from "@/domain/settings";
import { useTranslation } from "react-i18next";
import StringList from "@/components/certimate/StringList";
import { Input } from "@/components/ui/input";
import DeployList from "@/components/certimate/DeployList";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
const Edit = () => {
const {
config: { accesses, emails, accessGroups },
config: { accesses, emails },
} = useConfig();
const [domain, setDomain] = useState<Domain>();
const [domain, setDomain] = useState<Domain>({} as Domain);
const location = useLocation();
const { t } = useTranslation();
const [tab, setTab] = useState<"base" | "advance">("base");
const [targetType, setTargetType] = useState(domain ? domain.targetType : "");
const [tab, setTab] = useState<"apply" | "deploy">("apply");
useEffect(() => {
// Parsing query parameters
@@ -60,7 +69,6 @@ const Edit = () => {
const fetchData = async () => {
const data = await get(id);
setDomain(data);
setTargetType(data.targetType);
};
fetchData();
}
@@ -68,20 +76,15 @@ const Edit = () => {
const formSchema = z.object({
id: z.string().optional(),
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: "请输入正确的域名",
domain: z.string().min(1, {
message: "common.errmsg.domain_invalid",
}),
email: z.string().email().optional(),
email: z.string().email("common.errmsg.email_invalid").optional(),
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
message: "请选择DNS服务商授权配置",
message: "domain.application.form.access.errmsg.empty",
}),
targetAccess: z.string().optional(),
targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, {
message: "请选择部署服务类型",
}),
variables: z.string().optional(),
group: z.string().optional(),
nameservers: z.string().optional(),
timeout: z.number().optional(),
});
const form = useForm<z.infer<typeof formSchema>>({
@@ -91,11 +94,8 @@ const Edit = () => {
domain: "",
email: "",
access: "",
targetAccess: "",
targetType: "",
variables: "",
group: "",
nameservers: "",
timeout: 60,
},
});
@@ -104,74 +104,81 @@ const Edit = () => {
form.reset({
id: domain.id,
domain: domain.domain,
email: domain.email,
access: domain.access,
targetAccess: domain.targetAccess,
targetType: domain.targetType,
variables: domain.variables,
group: domain.group,
nameservers: domain.nameservers,
email: domain.applyConfig?.email,
access: domain.applyConfig?.access,
nameservers: domain.applyConfig?.nameservers,
timeout: domain.applyConfig?.timeout,
});
}
}, [domain, form]);
const targetAccesses = accesses.filter((item) => {
if (item.usage == "apply") {
return false;
}
if (targetType == "") {
return true;
}
const types = targetType.split("-");
return item.configType === types[0];
});
const { toast } = useToast();
const navigate = useNavigate();
const onSubmit = async (data: z.infer<typeof formSchema>) => {
const group = data.group == "emptyId" ? "" : data.group;
const targetAccess =
data.targetAccess === "emptyId" ? "" : data.targetAccess;
if (group == "" && targetAccess == "") {
form.setError("group", {
type: "manual",
message: "部署授权和部署授权组至少选一个",
});
form.setError("targetAccess", {
type: "manual",
message: "部署授权和部署授权组至少选一个",
});
return;
}
console.log(data);
const req: Domain = {
id: data.id as string,
crontab: "0 0 * * *",
domain: data.domain,
email: data.email,
access: data.access,
group: group,
targetAccess: targetAccess,
targetType: data.targetType,
variables: data.variables,
nameservers: data.nameservers,
applyConfig: {
email: data.email ?? "",
access: data.access,
nameservers: data.nameservers,
timeout: data.timeout,
},
};
try {
await save(req);
let description = "域名编辑成功";
const resp = await save(req);
let description = t("domain.application.form.domain.changed.message");
if (req.id == "") {
description = "域名添加成功";
description = t("domain.application.form.domain.added.message");
}
toast({
title: "成功",
title: t("common.save.succeeded.message"),
description,
});
navigate("/domains");
if (!domain?.id) setTab("deploy");
setDomain({ ...resp });
} catch (e) {
const err = e as ClientResponseError;
Object.entries(err.response.data as PbErrorData).forEach(
([key, value]) => {
form.setError(key as keyof z.infer<typeof formSchema>, {
type: "manual",
message: value.message,
});
}
);
return;
}
};
const handelOnDeployListChange = async (list: DeployConfig[]) => {
const req = {
...domain,
deployConfig: list,
};
try {
const resp = await save(req);
let description = t("domain.application.form.domain.changed.message");
if (req.id == "") {
description = t("domain.application.form.domain.added.message");
}
toast({
title: t("common.save.succeeded.message"),
description,
});
if (!domain?.id) setTab("deploy");
setDomain({ ...resp });
} catch (e) {
const err = e as ClientResponseError;
@@ -193,360 +200,281 @@ const Edit = () => {
<div className="">
<Toaster />
<div className=" h-5 text-muted-foreground">
{domain?.id ? "编辑" : "新增"}
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#/domains">
{t("domain.page.title")}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
{domain?.id ? t("domain.edit") : t("domain.add")}
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<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 md:mt-5">
<div
className={cn(
"cursor-pointer text-right",
tab === "base" ? "text-primary" : ""
tab === "apply" ? "text-primary" : ""
)}
onClick={() => {
setTab("base");
setTab("apply");
}}
>
{t("domain.application.tab")}
</div>
<div
className={cn(
"cursor-pointer text-right",
tab === "advance" ? "text-primary" : ""
tab === "deploy" ? "text-primary" : ""
)}
onClick={() => {
setTab("advance");
if (!domain?.id) {
toast({
title: t("domain.application.unsaved.message"),
description: t("domain.application.unsaved.message"),
variant: "destructive",
});
return;
}
setTab("deploy");
}}
>
{t("domain.deployment.tab")}
</div>
</div>
<div className="w-full md:w-[35em] bg-gray-100 dark:bg-gray-900 p-5 rounded mt-3 md:mt-0">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 dark:text-stone-200"
>
<FormField
control={form.control}
name="domain"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入域名" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel className="flex w-full justify-between">
<div>Email</div>
<EmailsEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
</div>
}
/>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("email", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择邮箱" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
{(emails.content as EmailsSetting).emails.map(
(item) => (
<SelectItem key={item} value={item}>
<div>{item}</div>
</SelectItem>
)
)}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="access"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel className="flex w-full justify-between">
<div>DNS </div>
<AccessEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
</div>
}
op="add"
/>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("access", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择授权配置" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
{accesses
.filter((item) => item.usage != "deploy")
.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img
className="w-6"
src={
accessTypeMap.get(
item.configType
)?.[1]
}
/>
<div>{item.name}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="targetType"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel></FormLabel>
<FormControl>
<Select
{...field}
onValueChange={(value) => {
setTargetType(value);
form.setValue("targetType", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择部署服务类型" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
{targetTypeKeys.map((key) => (
<SelectItem key={key} value={key}>
<div className="flex items-center space-x-2">
<img
className="w-6"
src={targetTypeMap.get(key)?.[1]}
/>
<div>{targetTypeMap.get(key)?.[0]}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="targetAccess"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel className="w-full flex justify-between">
<div></div>
<AccessEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
</div>
}
op="add"
/>
</FormLabel>
<FormControl>
<Select
{...field}
onValueChange={(value) => {
form.setValue("targetAccess", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择授权配置" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>
{form.getValues().targetAccess}
</SelectLabel>
<SelectItem value="emptyId">
<div className="flex items-center space-x-2">
--
</div>
</SelectItem>
{targetAccesses.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img
className="w-6"
src={
accessTypeMap.get(item.configType)?.[1]
}
/>
<div>{item.name}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="group"
render={({ field }) => (
<FormItem hidden={tab != "advance" || targetType != "ssh"}>
<FormLabel className="w-full flex justify-between">
<div>
( ssh )
</div>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
defaultValue="emptyId"
onValueChange={(value) => {
form.setValue("group", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择分组" />
</SelectTrigger>
<SelectContent>
<SelectItem value="emptyId">
<div
className={cn(
"flex items-center space-x-2 rounded cursor-pointer"
)}
>
--
<div className="flex flex-col">
<div
className={cn(
"w-full md:w-[35em] p-5 rounded mt-3 md:mt-0",
tab == "deploy" && "hidden"
)}
>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 dark:text-stone-200"
>
{/* 域名 */}
<FormField
control={form.control}
name="domain"
render={({ field }) => (
<FormItem>
<>
<StringList
value={field.value}
valueType="domain"
onValueChange={(domain: string) => {
form.setValue("domain", domain);
}}
/>
</>
<FormMessage />
</FormItem>
)}
/>
{/* 邮箱 */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="flex w-full justify-between">
<div>
{t("domain.application.form.email.label") +
" " +
t("domain.application.form.email.tips")}
</div>
<EmailsEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t("common.add")}
</div>
</SelectItem>
{accessGroups
.filter((item) => {
return (
item.expand && item.expand?.access.length > 0
);
})
.map((item) => (
<SelectItem
value={item.id ? item.id : ""}
key={item.id}
>
<div
className={cn(
"flex items-center space-x-2 rounded cursor-pointer"
)}
>
{item.name}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
}
/>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("email", value);
}}
>
<SelectTrigger>
<SelectValue
placeholder={t(
"domain.application.form.email.errmsg.empty"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>
{t("domain.application.form.email.list")}
</SelectLabel>
{(emails.content as EmailsSetting).emails.map(
(item) => (
<SelectItem key={item} value={item}>
<div>{item}</div>
</SelectItem>
)
)}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="variables"
render={({ field }) => (
<FormItem hidden={tab != "advance"}>
<FormLabel></FormLabel>
<FormControl>
<Textarea
placeholder={`可在SSH部署中使用,形如:\nkey=val;\nkey2=val2;`}
{...field}
className="placeholder:whitespace-pre-wrap"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 授权 */}
<FormField
control={form.control}
name="access"
render={({ field }) => (
<FormItem>
<FormLabel className="flex w-full justify-between">
<div>{t("domain.application.form.access.label")}</div>
<AccessEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t("common.add")}
</div>
}
op="add"
/>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("access", value);
}}
>
<SelectTrigger>
<SelectValue
placeholder={t(
"domain.application.form.access.placeholder"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>
{t("domain.application.form.access.list")}
</SelectLabel>
{accesses
.filter((item) => item.usage != "deploy")
.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img
className="w-6"
src={
accessTypeMap.get(
item.configType
)?.[1]
}
/>
<div>{item.name}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="nameservers"
render={({ field }) => (
<FormItem hidden={tab != "advance"}>
<FormLabel></FormLabel>
<FormControl>
<Textarea
placeholder={`自定义域名服务器,多个用分号隔开,如:\n8.8.8.8;\n8.8.4.4;`}
{...field}
className="placeholder:whitespace-pre-wrap"
/>
</FormControl>
{/* 超时时间 */}
<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)
);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
{/* 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>
)}
/>
<div className="flex justify-end">
<Button type="submit">
{domain?.id ? t("common.save") : t("common.next")}
</Button>
</div>
</form>
</Form>
</div>
<div
className={cn(
"flex flex-col space-y-5 w-full md:w-[35em]",
tab == "apply" && "hidden"
)}
>
<DeployList
deploys={domain?.deployConfig ?? []}
onChange={(list: DeployConfig[]) => {
handelOnDeployListChange(list);
}}
/>
</div>
</div>
</div>
</div>

View File

@@ -35,11 +35,13 @@ import { TooltipContent, TooltipProvider } from "@radix-ui/react-tooltip";
import { Earth } from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useTranslation, Trans } from "react-i18next";
const Home = () => {
const toast = useToast();
const navigate = useNavigate();
const { t } = useTranslation();
const location = useLocation();
const query = new URLSearchParams(location.search);
@@ -111,8 +113,8 @@ const Home = () => {
const handleRightNowClick = async (domain: Domain) => {
try {
unsubscribeId(domain.id);
subscribeId(domain.id, (resp) => {
unsubscribeId(domain.id ?? "");
subscribeId(domain.id ?? "", (resp) => {
console.log(resp);
const updatedDomains = domains.map((domain) => {
if (domain.id === resp.id) {
@@ -127,23 +129,24 @@ const Home = () => {
await save(domain);
toast.toast({
title: "操作成功",
description: "已发起部署,请稍后查看部署日志。",
title: t("domain.deploy.started.message"),
description: t("domain.deploy.started.tips"),
});
} catch (e) {
toast.toast({
title: "执行失败",
title: t("domain.deploy.failed.message"),
description: (
<>
// 这里的 text 只是占位作用,实际文案在 src/i18n/locales/[lang].json
<Trans i18nKey="domain.deploy.failed.tips">
text1
<Link
to={`/history?domain=${domain.id}`}
className="underline text-blue-500"
>
text2
</Link>
</>
text3
</Trans>
),
variant: "destructive",
});
@@ -175,8 +178,8 @@ const Home = () => {
<div className="">
<Toaster />
<div className="flex justify-between items-center">
<div className="text-muted-foreground"></div>
<Button onClick={handleCreateClick}></Button>
<div className="text-muted-foreground">{t("domain.page.title")}</div>
<Button onClick={handleCreateClick}>{t("domain.add")}</Button>
</div>
{!domains.length ? (
@@ -187,26 +190,29 @@ const Home = () => {
</span>
<div className="text-center text-sm text-muted-foreground mt-3">
{t("domain.nodata")}
</div>
<Button onClick={handleCreateClick} className="mt-3">
{t("domain.add")}
</Button>
</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="w-36"></div>
<div className="w-40"></div>
<div className="w-32"></div>
<div className="w-64"></div>
<div className="w-40 sm:ml-2"></div>
<div className="w-24"></div>
<div className="grow"></div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
<div className="w-36">{t("common.text.domain")}</div>
<div className="w-40">{t("domain.props.expiry")}</div>
<div className="w-32">
{t("domain.props.last_execution_status")}
</div>
<div className="w-64">
{t("domain.props.last_execution_stage")}
</div>
<div className="w-40 sm:ml-2">
{t("domain.props.last_execution_time")}
</div>
<div className="w-24">{t("domain.props.enable")}</div>
<div className="grow">{t("common.text.operations")}</div>
</div>
{domains.map((domain) => (
@@ -214,15 +220,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"
key={domain.id}
>
<div className="sm:w-36 w-full pt-1 sm:pt-0 flex items-center">
{domain.domain}
<div className="sm:w-36 w-full pt-1 sm:pt-0 flex items-center truncate">
{domain.domain.split(";").map((item) => (
<>
{item}
<br />
</>
))}
</div>
<div className="sm:w-40 w-full pt-1 sm:pt-0 flex items-center">
<div>
{domain.expiredAt ? (
<>
<div>90</div>
<div>{getDate(domain.expiredAt)}</div>
<div>
{t("domain.props.expiry.date1", { date: 90 })}
</div>
<div>
{t("domain.props.expiry.date2", {
date: getDate(domain.expiredAt),
})}
</div>
</>
) : (
"---"
@@ -260,13 +277,15 @@ const Home = () => {
<Switch
checked={domain.enabled}
onCheckedChange={() => {
handelCheckedChange(domain.id);
handelCheckedChange(domain.id ?? "");
}}
></Switch>
</TooltipTrigger>
<TooltipContent>
<div className="border rounded-sm px-3 bg-background text-muted-foreground text-xs">
{domain.enabled ? "禁用" : "启用"}
{domain.enabled
? t("domain.props.enable.disabled")
: t("domain.props.enable.enabled")}
</div>
</TooltipContent>
</Tooltip>
@@ -276,9 +295,9 @@ const Home = () => {
<Button
variant={"link"}
className="p-0"
onClick={() => handleHistoryClick(domain.id)}
onClick={() => handleHistoryClick(domain.id ?? "")}
>
{t("domain.history")}
</Button>
<Show when={domain.enabled ? true : false}>
<Separator orientation="vertical" className="h-4 mx-2" />
@@ -287,7 +306,7 @@ const Home = () => {
className="p-0"
onClick={() => handleRightNowClick(domain)}
>
{t("domain.deploy")}
</Button>
</Show>
@@ -304,7 +323,7 @@ const Home = () => {
className="p-0"
onClick={() => handleForceClick(domain)}
>
{t("domain.deploy_forced")}
</Button>
</Show>
@@ -315,7 +334,7 @@ const Home = () => {
className="p-0"
onClick={() => handleDownloadClick(domain)}
>
{t("common.download")}
</Button>
</Show>
@@ -325,24 +344,28 @@ const Home = () => {
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"link"} className="p-0">
{t("common.delete")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogTitle>
{t("domain.delete")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("domain.delete.confirm")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogCancel>
{t("common.cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleDeleteClick(domain.id);
handleDeleteClick(domain.id ?? "");
}}
>
{t("common.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -352,9 +375,9 @@ const Home = () => {
<Button
variant={"link"}
className="p-0"
onClick={() => handleEditClick(domain.id)}
onClick={() => handleEditClick(domain.id ?? "")}
>
{t("common.edit")}
</Button>
</>
)}

View File

@@ -17,11 +17,13 @@ import { list } from "@/repository/deployment";
import { Smile } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
const History = () => {
const navigate = useNavigate();
const [deployments, setDeployments] = useState<Deployment[]>();
const [searchParams] = useSearchParams();
const { t } = useTranslation();
const domain = searchParams.get("domain");
useEffect(() => {
@@ -38,20 +40,17 @@ const History = () => {
return (
<ScrollArea className="h-[80vh] overflow-hidden">
<div className="text-muted-foreground"></div>
<div className="text-muted-foreground">{t("history.page.title")}</div>
{!deployments?.length ? (
<>
<Alert className="max-w-[40em] mx-auto mt-20">
<AlertTitle></AlertTitle>
<AlertTitle>{t("common.text.nodata")}</AlertTitle>
<AlertDescription>
<div className="flex items-center mt-5">
<div>
<Smile className="text-yellow-400" size={36} />
</div>
<div className="ml-2">
{" "}
</div>
<div className="ml-2"> {t("history.nodata")}</div>
</div>
<div className="mt-2 flex justify-end">
<Button
@@ -59,7 +58,7 @@ const History = () => {
navigate("/");
}}
>
{t("domain.add")}
</Button>
</div>
</AlertDescription>
@@ -68,16 +67,15 @@ 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="w-48"></div>
<div className="w-48">{t("history.props.domain")}</div>
<div className="w-24"></div>
<div className="w-56"></div>
<div className="w-56 sm:ml-2 text-center"></div>
<div className="w-24">{t("history.props.status")}</div>
<div className="w-56">{t("history.props.stage")}</div>
<div className="w-56 sm:ml-2 text-center">
{t("history.props.last_execution_time")}
</div>
<div className="grow"></div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
<div className="grow">{t("common.text.operations")}</div>
</div>
{deployments?.map((deployment) => (
@@ -86,7 +84,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"
>
<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 className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
<DeployState deployment={deployment} />
@@ -104,14 +109,14 @@ const History = () => {
<Sheet>
<SheetTrigger asChild>
<Button variant={"link"} className="p-0">
{t("history.log")}
</Button>
</SheetTrigger>
<SheetContent className="sm:max-w-5xl">
<SheetHeader>
<SheetTitle>
{deployment.expand.domain?.domain}-{deployment.id}
{t("history.log")}
</SheetTitle>
</SheetHeader>
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">

View File

@@ -1,3 +1,8 @@
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { z } from "zod";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
Form,
@@ -11,20 +16,19 @@ import { Input } from "@/components/ui/input";
import { getErrMessage } from "@/lib/error";
import { getPb } from "@/repository/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { z } from "zod";
const formSchema = z.object({
username: z.string().email({
message: "请输入正确的邮箱地址",
message: "login.username.errmsg.invalid",
}),
password: z.string().min(10, {
message: "密码至少10个字符",
message: "login.password.errmsg.invalid",
}),
});
const Login = () => {
const { t } = useTranslation();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
@@ -61,9 +65,9 @@ const Login = () => {
name="username"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t("login.username.label")}</FormLabel>
<FormControl>
<Input placeholder="email" {...field} />
<Input placeholder={t("login.username.placeholder")} {...field} />
</FormControl>
<FormMessage />
@@ -76,9 +80,9 @@ const Login = () => {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t("login.password.label")}</FormLabel>
<FormControl>
<Input placeholder="password" {...field} type="password" />
<Input placeholder={t("login.password.placeholder")} {...field} type="password" />
</FormControl>
<FormMessage />
@@ -86,7 +90,7 @@ const Login = () => {
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
<Button type="submit">{t("login.submit")}</Button>
</div>
</form>
</Form>

View File

@@ -15,16 +15,18 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { z } from "zod";
const formSchema = z.object({
email: z.string().email("请输入正确的邮箱"),
email: z.string().email("settings.account.email.errmsg.invalid"),
});
const Account = () => {
const { toast } = useToast();
const navigate = useNavigate();
const { t } = useTranslation();
const [changed, setChanged] = useState(false);
@@ -43,8 +45,8 @@ const Account = () => {
getPb().authStore.clear();
toast({
title: "修改账户邮箱功",
description: "请重新登录",
title: t("settings.account.email.changed.message"),
description: t("settings.account.relogin.message"),
});
setTimeout(() => {
navigate("/login");
@@ -52,7 +54,7 @@ const Account = () => {
} catch (e) {
const message = getErrMessage(e);
toast({
title: "修改账户邮箱失败",
title: t("settings.account.email.failed.message"),
description: message,
variant: "destructive",
});
@@ -72,10 +74,10 @@ const Account = () => {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t("settings.account.email.label")}</FormLabel>
<FormControl>
<Input
placeholder="请输入邮箱"
placeholder={t("settings.account.email.placeholder")}
{...field}
type="email"
onChange={(e) => {
@@ -92,10 +94,10 @@ const Account = () => {
<div className="flex justify-end">
{changed ? (
<Button type="submit"></Button>
<Button type="submit">{t("common.update")}</Button>
) : (
<Button type="submit" disabled variant={"secondary"}>
{t("common.update")}
</Button>
)}
</div>

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