Compare commits

...

108 Commits

Author SHA1 Message Date
yoan
ac14924a73 update ui 2024-09-13 07:55:25 +08:00
yoan
7550aec904 When adding a domain, you can also add a custom email address. 2024-09-13 07:36:26 +08:00
yoan
139a6980ac Add godaddy provider 2024-09-12 21:59:50 +08:00
yoan
d5a6411e26 Merge branch 'main' of github.com:usual2970/certimate 2024-09-12 06:11:34 +08:00
yoan
1fcd9897be update README 2024-09-12 06:11:15 +08:00
usual2970
99d4bf2624 Update README.md 2024-09-11 08:03:18 +08:00
yoan
844347acf9 fix file select 2024-09-11 07:35:19 +08:00
usual2970
eeae9b4405 Merge pull request #16 from gexin1/main
fix: 首页展示问题和授权管理弹窗无法滚动
2024-09-11 06:37:38 +08:00
river
feef94f873 fix: 1. 修复首页控制面板在md和2xl的时候会超出屏幕
2.修复授权管理-添加授权选择ssh 弹窗过长超出屏幕无法滚动
2024-09-10 22:56:54 +08:00
yoan
7ad8cdfe47 add namesilo provider 2024-09-10 21:02:31 +08:00
yoan
b8cff68167 fix missing tzdata 2024-09-10 18:01:34 +08:00
usual2970
719b5f359d Update README.md 2024-09-10 11:27:39 +08:00
usual2970
00afb3bbbf Update README.md 2024-09-09 22:36:02 +08:00
usual2970
f92799cd92 Update README.md 2024-09-09 22:20:58 +08:00
usual2970
a441357858 Update README.md 2024-09-09 22:15:15 +08:00
yoan
51771385f2 Update readme 2024-09-09 10:42:45 +08:00
yoan
45fa3babf4 Update readme 2024-09-08 09:25:23 +08:00
yoan
e722aa9cb6 Update README 2024-09-08 09:13:44 +08:00
yoan
825eadfca1 update tag 2024-09-07 21:29:14 +08:00
yoan
c0e6f67899 update api 2024-09-07 21:26:19 +08:00
yoan
34534d9e26 Update readme 2024-09-07 21:05:25 +08:00
yoan
7ce7bc5111 Merge branch 'main' of github.com:usual2970/certimate 2024-09-07 20:56:29 +08:00
yoan
c2d3ed9ff1 Add dashboard 2024-09-07 20:55:36 +08:00
usual2970
91b90a2169 Update README.md 2024-09-05 22:19:43 +08:00
yoan
0d5d356a0d Merge branch 'feat/qiniu' 2024-09-05 10:34:07 +08:00
yoan
e722bc4245 v0.0.15 2024-09-05 10:32:52 +08:00
usual2970
7a231fd2e6 Merge pull request #12 from usual2970/feat/qiniu
Add qiniu deployer
2024-09-05 10:31:46 +08:00
yoan
abc6ea1ee4 Add qiniu deployer 2024-09-05 10:30:57 +08:00
yoan
e244d6e921 Add qiniu deployer 2024-09-05 10:30:19 +08:00
yoan
a297b1cc1f update Readme 2024-09-04 14:26:23 +08:00
yoan
45366a9436 update readme 2024-09-03 11:23:08 +08:00
usual2970
69671075fa Merge pull request #6 from usual2970/feat/pagination
Feat/pagination
2024-09-02 22:56:04 +08:00
yoan
10a9efe9bd add pagination 2024-09-02 22:55:17 +08:00
usual2970
d826e64e29 Merge pull request #10 from usual2970/feat/cloudflare
Add cloudflare provider
2024-09-02 14:51:26 +08:00
yoan
9c19465a85 update action trigger method 2024-09-02 14:50:47 +08:00
yoan
7a6571bb2e fixconflict 2024-09-02 14:42:30 +08:00
yoan
3f2b743b1e fix conflict 2024-09-02 14:41:17 +08:00
yoan
52cca47ec8 add cloudflare provider 2024-09-02 14:36:46 +08:00
usual2970
bc46ac958c Merge pull request #9 from usual2970/feat/doc_domain
update doc
2024-09-02 13:26:38 +08:00
yoan
400af5943f update doc 2024-09-02 13:23:09 +08:00
yoan
2b8fb9314f add cloudflare 2024-09-02 13:21:37 +08:00
usual2970
1de39da699 Merge pull request #7 from usual2970/feat/docs
Feat/docs
2024-09-01 19:40:09 +08:00
yoan
04f6d21e4a update document 2024-09-01 19:37:57 +08:00
yoan
d5fc218958 update document 2024-09-01 19:36:47 +08:00
yoan
a0da15ea7e update github action 2024-08-31 22:20:43 +08:00
yoan
19d19486ae Add pagination 2024-08-31 22:18:13 +08:00
yoan
a10d7dc2e9 update readme 2024-08-31 16:32:10 +08:00
yoan
227d44926f Optimize ui 2024-08-31 16:18:34 +08:00
yoan
7d3e94247e Add dark mod 2024-08-31 13:16:03 +08:00
yoan
7479c25e7a To optimize Docker image size. 2024-08-30 23:50:11 +08:00
yoan
6466d1fb0d To optimize Docker image size. 2024-08-30 23:47:41 +08:00
yoan
78877d0643 To optimize Docker image size. 2024-08-30 23:30:16 +08:00
yoan
7ad10c3e34 To optimize Docker image size. 2024-08-30 22:55:06 +08:00
yoan
c8fff39534 To optimize Docker image size. 2024-08-30 22:33:56 +08:00
yoan
0272054f1b build ui 2024-08-30 22:27:50 +08:00
yoan
48bcd32268 To optimize Docker image size. 2024-08-30 22:25:01 +08:00
yoan
51d5c24ef3 build ui 2024-08-30 11:35:51 +08:00
yoan
993b34eca6 Fix the issue with SSH deployment failing to upload the certificate. 2024-08-30 11:29:06 +08:00
yoan
2549f61028 update migrations 2024-08-29 23:04:04 +08:00
yoan
8ae30c7405 update migrations 2024-08-29 22:46:13 +08:00
yoan
fb40caa25c add deploy info 2024-08-29 21:41:55 +08:00
yoan
bae35536b8 update readme 2024-08-29 17:45:55 +08:00
yoan
a311e52a1c update readme 2024-08-29 17:32:08 +08:00
yoan
98d1611cca hide banner 2024-08-29 12:56:09 +08:00
yoan
1e11b23fdc add tencent cdn deployer 2024-08-29 11:34:08 +08:00
yoan
1861e73531 add webhook deployer 2024-08-28 18:53:33 +08:00
yoan
4b8fa3502c password modification page stylesheet 2024-08-28 16:45:26 +08:00
yoan
71d4c410a0 password modification page stylesheet 2024-08-28 16:44:54 +08:00
yoan
da666bcb9b update ignore 2024-08-28 15:05:23 +08:00
yoan
28b4fed1a5 Add password modification. 2024-08-28 14:41:49 +08:00
yoan
e31b162c57 Add password modification. 2024-08-28 14:37:12 +08:00
yoan
c3665cb1bb Add password modification. 2024-08-28 14:36:11 +08:00
yoan
cf0f82793a update readme 2024-08-28 01:42:55 +08:00
yoan
89f5c9618d update readme 2024-08-28 01:41:29 +08:00
yoan
5a365e9e86 Add docker support 2024-08-28 01:29:40 +08:00
yoan
812050159f Add docker support 2024-08-28 01:24:03 +08:00
yoan
852d4fb358 Add docker support 2024-08-28 01:06:20 +08:00
yoan
ec25d092d7 add docker support 2024-08-28 00:37:08 +08:00
yoan
382b41bae8 Add docker support 2024-08-27 23:38:12 +08:00
yoan
8c684e3233 Add docker support 2024-08-27 23:32:10 +08:00
yoan
76e6b9f20c Add docker support 2024-08-27 23:25:20 +08:00
yoan
0fac606d67 Add docker support 2024-08-27 23:01:38 +08:00
yoan
4f15191e48 Add docker support 2024-08-27 22:58:01 +08:00
yoan
b15cfcff2c Add docker support 2024-08-27 22:57:11 +08:00
yoan
bfcf294428 Add docker support 2024-08-27 22:46:44 +08:00
yoan
d3ad4e42b0 Add docker support 2024-08-27 22:25:17 +08:00
yoan
18709dade3 Add docker support 2024-08-27 22:16:01 +08:00
yoan
f3c84489e9 Add docker support 2024-08-27 22:09:55 +08:00
yoan
9bc83b2370 update version 2024-08-26 22:04:46 +08:00
yoan
282a96e3a4 update version 2024-08-26 21:52:51 +08:00
yoan
c820fee906 update default admin 2024-08-26 21:51:51 +08:00
yoan
4a03f930a1 Supports certificate downloads, wildcard domains, and more. 2024-08-26 21:51:30 +08:00
yoan
5b05253e21 readme 2024-08-25 23:11:50 +08:00
yoan
73e22f3527 fix readme 2024-08-23 20:58:34 +08:00
yoan
133e94966b add readme file 2024-08-23 17:25:11 +08:00
yoan
f13ba57851 fix some ui problems 2024-08-23 14:56:42 +08:00
yoan
76c445a51c fix nixpacks 2024-08-23 14:24:10 +08:00
yoan
10bf69181f fix nixpacks 2024-08-23 14:09:27 +08:00
yoan
36196b50b2 ignore env 2024-08-23 13:58:42 +08:00
yoan
ad91b05801 ignore env 2024-08-23 13:57:56 +08:00
yoan
1277753845 fix nixpacks 2024-08-23 13:48:07 +08:00
yoan
c7a37ab723 fix 2024-08-23 13:46:16 +08:00
yoan
8ddba6d29f fix nixpacks 2024-08-23 13:39:06 +08:00
yoan
0ed5370f17 fix nixpacks 2024-08-23 13:37:01 +08:00
yoan
35ecb955e2 nixpacks 2024-08-23 13:31:13 +08:00
yoan
eac571934c nixpacks 2024-08-23 13:22:44 +08:00
yoan
f394e34861 added some optimizations 2024-08-23 11:33:43 +08:00
usual2970
a5beb26d9d Update release.yml 2024-08-23 01:16:42 +08:00
100 changed files with 4867 additions and 643 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
vendor
ui/node_modules
pb_data
build
.vscode

43
.github/workflows/push_image.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Docker Image CI
on:
push:
tags:
- "*"
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
registry: registry.cn-shanghai.aliyuncs.com
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract Git tag
id: get_tag
run: echo "tag=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile_build
platforms: linux/amd64,linux/arm64
push: true
tags: |
registry.cn-shanghai.aliyuncs.com/usual2970/certimate:${{ env.tag }}
registry.cn-shanghai.aliyuncs.com/usual2970/certimate:latest

View File

@@ -1,8 +1,9 @@
name: basebuild
on:
pull_request:
push:
tags:
- "*"
jobs:
goreleaser:
@@ -33,4 +34,4 @@ jobs:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}

3
.gitignore vendored
View File

@@ -4,6 +4,7 @@ pb_data
main
./certimate
build
/docker/data
# Editor directories and files
.vscode/*
@@ -15,3 +16,5 @@ build
*.njsproj
*.sln
*.sw?
./dist

View File

@@ -39,6 +39,8 @@ archives:
format: zip
files:
- CHANGELOG.md
- LICENSE.md
- README.md
checksum:
name_template: "checksums.txt"

View File

@@ -1,3 +1,8 @@
## v0.0.3
- 解决一些 bug
- 添加 README.md
## v0.0.1
- Initial release

16
Dockerfile_build Normal file
View File

@@ -0,0 +1,16 @@
FROM golang:1.22-alpine as builder
WORKDIR /app
COPY ../. /app/
RUN go build -o certimate
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/certimate .
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]

21
LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Yoan.Liu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

176
README.md Normal file
View File

@@ -0,0 +1,176 @@
# 🔒Certimate
做个人产品或在小企业负责运维的同学,需要管理多个域名,要给域名申请证书。但手动申请证书有以下缺点:
1. 😱麻烦:申请、部署证书虽不困难,但也挺麻烦的,尤其是维护多个域名的时候。
2. 😭易忘当前免费证书有效期仅90天这就要求定期操作增加工作量的同时也很容易忘掉导致网站无法访问。
Certimate 就是为了解决上述问题而产生的,它具有以下特点:
1. 操作简单:自动申请、部署、续期 SSL 证书,全程无需人工干预。
2. 支持私有部署部署方法简单只需下载二进制文件执行即可。二进制文件、docker 镜像全部用 github actions 生成,过程透明,可自行审计。
3. 数据安全:由于是私有部署,所有数据均存储在本地,不会保存在服务商的服务器,确保数据的安全性。
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。使用文档请访问[https://docs.certimate.me](https://docs.certimate.me)
- [🔒Certimate](#certimate)
- [一、安装](#一安装)
- [1. 二进制文件](#1-二进制文件)
- [2. Docker 安装](#2-docker-安装)
- [3. 源代码安装](#3-源代码安装)
- [二、使用](#二使用)
- [三、支持的服务商列表](#三支持的服务商列表)
- [四、系统截图](#四系统截图)
- [五、概念](#五概念)
- [1. 域名](#1-域名)
- [2. dns 服务商授权信息](#2-dns-服务商授权信息)
- [3. 部署服务商授权信息](#3-部署服务商授权信息)
- [六、常见问题](#六常见问题)
- [七、贡献](#七贡献)
## 一、安装
安装 Certimate 非常简单,你可以选择以下方式之一进行安装:
### 1. 二进制文件
你可以直接从[Releases 页](https://github.com/usual2970/certimate/releases)下载预先编译好的二进制文件,解压后执行:
```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
```
### 3. 源代码安装
```bash
git clone EMAIL:usual2970/certimate.git
cd certimate
go run main.go serve
```
## 二、使用
执行完上述安装操作后,在浏览器中访问 `http://127.0.0.1:8090` 即可访问 Certimate 管理页面。
```bash
用户名admin@certimate.fun
密码1234567890
```
![usage.gif](https://i.imgur.com/zpCoLVM.gif)
## 三、支持的服务商列表
| 服务商 | 是否域名服务商 | 是否部署服务 | 备注 |
|------|------|-----|------|
| 阿里云| 是 | 是 | 支持阿里云注册的域名,支持部署到阿里云 CDN,OSS |
| 腾讯云| 是 | 是 | 支持腾讯云注册的域名,支持部署到腾讯云 CDN |
| 七牛云| 否 | 是 | 七牛云没有注册域名服务,支持部署到七牛云 CDN |
|CloudFlare| 是 | 否 | 支持 CloudFlare 注册的域名CloudFlare 服务自带SSL证书 |
|SSH| 否 | 是 | 支持部署到 SSH 服务器 |
|WEBHOOK| 否 | 是 | 支持回调到 WEBHOOK |
## 四、系统截图
![login](https://i.imgur.com/SYjjbql.jpeg)
![dashboard](https://i.imgur.com/WMVbBId.jpeg)
![domains](https://i.imgur.com/8wit3ZA.jpeg)
![accesses](https://i.imgur.com/EWtOoJ0.jpeg)
![history](https://i.imgur.com/aaPtSW7.jpeg)
## 五、概念
Certimate 的工作流程如下:
* 用户通过 Certimate 管理页面填写申请证书的信息包括域名、dns 服务商的授权信息、以及要部署到的服务商的授权信息。
* Certimate 向证书场商的 API 发起申请请求,获取 SSL 证书。
* Certimate 存储证书信息,包括证书内容、私钥、证书有效期等,并在证书即将过期时自动续期。
* Certimate 向服务商的 API 发起部署请求,将证书部署到服务商的服务器上。
这就涉及域名、dns 服务商的授权信息、部署服务商的授权信息等。
### 1. 域名
就是要申请证书的域名。
### 2. dns 服务商授权信息
给域名申请证书需要证明域名是你的,所以我们手动申请证书的时候一般需要在域名服务商的控制台解析记录中添加一个 TXT 记录。
Certimate 会自动添加一个 TXT 记录,你只需要在 Certimate 后台中填写你的域名服务商的授权信息即可。
比如你在阿里云购买的域名,授权信息如下:
```bash
accessKeyId: xxx
accessKeySecret: TOKEN
```
在腾讯云购买的域名,授权信息如下:
```bash
secretId: xxx
secretKey: TOKEN
```
### 3. 部署服务商授权信息
Certimate 申请证书后,会自动将证书部署到你指定的目标上,比如阿里云 CDN 这时你需要填写阿里云的授权信息。Certimate 会根据你填写的授权信息及域名找到对应的 CDN 服务,并将证书部署到对应的 CDN 服务上。
部署服务商授权信息和 dns 服务商授权信息一致,区别在于 dns 服务商授权信息用于证明域名是你的,部署服务商授权信息用于提供证书部署的授权信息。
## 六、常见问题
Q: 提供saas服务吗
> A: 不提供目前仅支持self-hosted私有部署
Q: 数据安全?
> A: 由于仅支持私有部署各种数据都保存在用户的服务器上。另外Certimate源码也开源二进制包及Docker镜像打包过程全部使用Github actions进行过程透明可见可自行审计。
Q: 自动续期证书?
> A: 已经申请的证书会在过期前10天自动续期。每天会检查一次证书是否快要过期快要过期时会自动重新申请证书并部署到目标服务上。
## 七、贡献
Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.md)。你可以使用它做任何你想做的事,甚至把它当作一个付费服务提供给用户。
你可以通过以下方式来支持 Certimate 的开发:
* 提交代码:如果你发现了 bug 或有新的功能需求,而你又有相关经验,可以提交代码给我们。
* 提交 issue功能建议或者 bug 可以[提交 issue](https://github.com/usual2970/certimate/issues) 给我们。
支持更多服务商、UI 的优化改进、BUG 修复、文档完善等,欢迎大家提交 PR。

BIN
certimate Executable file
View File

Binary file not shown.

10
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,10 @@
version: '3.0'
services:
certimate:
image: registry.cn-shanghai.aliyuncs.com/usual2970/certimate:latest
container_name: certimate_server
ports:
- 8090:8090
volumes:
- ./data:/app/pb_data
restart: unless-stopped

22
go.mod
View File

@@ -11,15 +11,23 @@ require (
github.com/alibabacloud-go/tea v1.2.1
github.com/alibabacloud-go/tea-utils/v2 v2.0.5
github.com/go-acme/lego/v4 v4.17.4
github.com/gojek/heimdall/v7 v7.0.3
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
github.com/pkg/sftp v1.13.6
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/ssl v1.0.992
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.992
golang.org/x/crypto v0.26.0
)
require github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
@@ -50,6 +58,8 @@ require (
github.com/aws/smithy-go v1.20.3 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/clbanning/mxj/v2 v2.5.5 // indirect
github.com/cloudflare/cloudflare-go v0.97.0 // indirect
github.com/davecgh/go-spew v1.1.1 // 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
@@ -59,10 +69,14 @@ require (
github.com/go-jose/go-jose/v4 v4.0.2 // 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
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
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
@@ -77,12 +91,15 @@ require (
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // 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/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898 // 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/valyala/bytebufferpool v1.0.0 // indirect
@@ -105,6 +122,7 @@ require (
google.golang.org/grpc v1.65.0 // 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
modernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect

77
go.sum
View File

@@ -18,11 +18,17 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.7.1+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
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=
@@ -99,17 +105,21 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudr
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/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
github.com/clbanning/mxj/v2 v2.5.5/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/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=
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=
@@ -119,6 +129,8 @@ github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCO
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elastic/go-sysinfo v1.0.2/go.mod h1:O/D5m1VpYLwGjCYzEt63g3Z1uO3jXfwyzzjiW90t8cY=
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -132,6 +144,7 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
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=
@@ -145,11 +158,22 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gojek/heimdall/v7 v7.0.3 h1:+5sAhl8S0m+qRRL8IVeHCJudFh/XkG3wyO++nvOg+gc=
github.com/gojek/heimdall/v7 v7.0.3/go.mod h1:Z43HtMid7ysSjmsedPTXAki6jcdcNVnjn5pmsTyiMic=
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf h1:5xRGbUdOmZKoDXkGx5evVLehuCMpuO1hl701bEQqXOM=
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf/go.mod h1:QzhUKaYKJmcbTnCYCAVQrroCOY7vOOI8cSQ4NbuhYf0=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
@@ -176,10 +200,14 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
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=
@@ -196,16 +224,26 @@ github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDP
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/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=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@@ -215,6 +253,9 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
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=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -223,6 +264,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 h1:FwuzbVh87iLiUQj1+uQUsuw9x5t9m5n5g7rG7o4svW4=
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61/go.mod h1:paQfF1YtHe+GrGg5fOgjsjoCX/UKDr9bc1DoWpZfns8=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f/go.mod h1:aEt7p9Rvh67BYApmZwNDPpgircTO2kgdmDUoF/1QmwA=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -247,8 +291,12 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
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=
github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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=
@@ -260,8 +308,16 @@ github.com/pocketbase/dbx v1.10.1/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWK
github.com/pocketbase/pocketbase v0.22.18 h1:yVckUhi5GDORqCb0BbtlvRB1CVxHY9HO9btEaeZHVJU=
github.com/pocketbase/pocketbase v0.22.18/go.mod h1:0QFvDOOW7ANId78ChZSagyHbmP6CgMxDQrQFXzeaDpA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
github.com/qiniu/go-sdk/v7 v7.22.0 h1:NiRj6+beSkKsPBr4XN9OdjPJQKhERtOwOwu3HJtzcWQ=
github.com/qiniu/go-sdk/v7 v7.22.0/go.mod h1:44lnyCs6gflCxMUV1yTBlZhPEB4ZO6LIDHkMV8Rofms=
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -276,8 +332,11 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
@@ -288,10 +347,15 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898 h1:ERwcXqhc94L9cFxtiI0pvt7IJtlHl/p/Jayl3mLw+ms=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992 h1:266lOve+E8vzhnrb/Mr05Ee+oxXD9C82JiusY/AZqXw=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/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/ssl v1.0.992 h1:A6O89OlCJQUpNxGqC/E5By04UNKBryIt5olQIGOx8mg=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992/go.mod h1:BcvC7ZPdSlhRggVq4J1ToJlgv8bmODIAuSo0naFZOLo=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.992 h1:ttCM2rrkGipHMFTavrPExKCWcfNjT7AMQ5ERrPExdI4=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.992/go.mod h1:WtzarrflM+eoyD8vcRuIPd8fT5UXD4IhUry6iSAUnxc=
github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
@@ -351,6 +415,7 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
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=
@@ -380,6 +445,7 @@ 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/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=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -392,6 +458,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190425145619-16072639606e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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=
@@ -420,6 +487,7 @@ golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
@@ -488,20 +556,27 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=

View File

@@ -1,17 +1,13 @@
package applicant
import (
"certimate/internal/domain"
"encoding/json"
"os"
"github.com/go-acme/lego/v4/providers/dns/alidns"
)
type aliyunAccess struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type aliyun struct {
option *ApplyOption
}
@@ -24,7 +20,7 @@ func NewAliyun(option *ApplyOption) Applicant {
func (a *aliyun) Apply() (*Certificate, error) {
access := &aliyunAccess{}
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("ALICLOUD_ACCESS_KEY", access.AccessKeyId)

View File

@@ -16,10 +16,15 @@ import (
)
const (
configTypeTencent = "tencent"
configTypeAliyun = "aliyun"
configTypeTencent = "tencent"
configTypeAliyun = "aliyun"
configTypeCloudflare = "cloudflare"
configTypeNamesilo = "namesilo"
configTypeGodaddy = "godaddy"
)
const defaultEmail = "536464346@qq.com"
type Certificate struct {
CertUrl string `json:"certUrl"`
CertStableUrl string `json:"certStableUrl"`
@@ -57,8 +62,12 @@ type Applicant interface {
func Get(record *models.Record) (Applicant, error) {
access := record.ExpandedOne("access")
email := record.GetString("email")
if email == "" {
email = defaultEmail
}
option := &ApplyOption{
Email: "536464346@qq.com",
Email: email,
Domain: record.GetString("domain"),
Access: access.GetString("config"),
}
@@ -67,6 +76,12 @@ func Get(record *models.Record) (Applicant, error) {
return NewTencent(option), nil
case configTypeAliyun:
return NewAliyun(option), nil
case configTypeCloudflare:
return NewCloudflare(option), nil
case configTypeNamesilo:
return NewNamesilo(option), nil
case configTypeGodaddy:
return NewGodaddy(option), nil
default:
return nil, errors.New("unknown config type")
}

View File

@@ -0,0 +1,33 @@
package applicant
import (
"certimate/internal/domain"
"encoding/json"
"os"
cf "github.com/go-acme/lego/v4/providers/dns/cloudflare"
)
type cloudflare struct {
option *ApplyOption
}
func NewCloudflare(option *ApplyOption) Applicant {
return &cloudflare{
option: option,
}
}
func (c *cloudflare) Apply() (*Certificate, error) {
access := &domain.CloudflareAccess{}
json.Unmarshal([]byte(c.option.Access), access)
os.Setenv("CLOUDFLARE_DNS_API_TOKEN", access.DnsApiToken)
provider, err := cf.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(c.option, provider)
}

View File

@@ -0,0 +1,35 @@
package applicant
import (
"certimate/internal/domain"
"encoding/json"
"os"
godaddyProvider "github.com/go-acme/lego/v4/providers/dns/godaddy"
)
type godaddy struct {
option *ApplyOption
}
func NewGodaddy(option *ApplyOption) Applicant {
return &godaddy{
option: option,
}
}
func (a *godaddy) Apply() (*Certificate, error) {
access := &domain.GodaddyAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("GODADDY_API_KEY", access.ApiKey)
os.Setenv("GODADDY_API_SECRET", access.ApiKey)
dnsProvider, err := godaddyProvider.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
}

View File

@@ -0,0 +1,34 @@
package applicant
import (
"certimate/internal/domain"
"encoding/json"
"os"
namesiloProvider "github.com/go-acme/lego/v4/providers/dns/namesilo"
)
type namesilo struct {
option *ApplyOption
}
func NewNamesilo(option *ApplyOption) Applicant {
return &namesilo{
option: option,
}
}
func (a *namesilo) Apply() (*Certificate, error) {
access := &domain.NameSiloAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("NAMESILO_API_KEY", access.ApiKey)
dnsProvider, err := namesiloProvider.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
}

View File

@@ -1,17 +1,13 @@
package applicant
import (
"certimate/internal/domain"
"encoding/json"
"os"
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
)
type tencentAccess struct {
SecretId string `json:"secretId"`
SecretKey string `json:"secretKey"`
}
type tencent struct {
option *ApplyOption
}
@@ -24,7 +20,7 @@ func NewTencent(option *ApplyOption) Applicant {
func (t *tencent) Apply() (*Certificate, error) {
access := &tencentAccess{}
access := &domain.TencentAccess{}
json.Unmarshal([]byte(t.option.Access), access)
os.Setenv("TENCENTCLOUD_SECRET_ID", access.SecretId)

View File

@@ -2,6 +2,7 @@ package deployer
import (
"certimate/internal/applicant"
"certimate/internal/domain"
"certimate/internal/utils/rand"
"context"
"encoding/json"
@@ -15,21 +16,18 @@ import (
"github.com/alibabacloud-go/tea/tea"
)
type aliyunAccess struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type aliyun struct {
client *cas20200407.Client
option *DeployerOption
infos []string
}
func NewAliyun(option *DeployerOption) (Deployer, error) {
access := &aliyunAccess{}
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
a := &aliyun{
option: option,
infos: make([]string, 0),
}
client, err := a.createClient(access.AccessKeyId, access.AccessKeySecret)
if err != nil {
@@ -40,6 +38,10 @@ func NewAliyun(option *DeployerOption) (Deployer, error) {
}
func (a *aliyun) GetInfo() []string {
return a.infos
}
func (a *aliyun) Deploy(ctx context.Context) error {
// 查询有没有对应的资源
@@ -48,24 +50,32 @@ func (a *aliyun) Deploy(ctx context.Context) error {
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 {
@@ -84,10 +94,11 @@ func (a *aliyun) updateDeployStatus(jobId int64) error {
JobId: tea.Int64(jobId),
}
_, err := a.client.UpdateDeploymentJobStatus(req)
resp, err := a.client.UpdateDeploymentJobStatus(req)
if err != nil {
return err
}
a.infos = append(a.infos, toStr("查询对应的资源", resp))
return nil
}

View File

@@ -1,6 +1,7 @@
package deployer
import (
"certimate/internal/domain"
"context"
"encoding/json"
"fmt"
@@ -14,10 +15,11 @@ import (
type AliyunCdn struct {
client *cdn20180510.Client
option *DeployerOption
infos []string
}
func NewAliyunCdn(option *DeployerOption) (*AliyunCdn, error) {
access := &aliyunAccess{}
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
a := &AliyunCdn{
option: option,
@@ -30,9 +32,14 @@ func NewAliyunCdn(option *DeployerOption) (*AliyunCdn, error) {
return &AliyunCdn{
client: client,
option: option,
infos: make([]string, 0),
}, nil
}
func (a *AliyunCdn) GetInfo() []string {
return a.infos
}
func (a *AliyunCdn) Deploy(ctx context.Context) error {
certName := fmt.Sprintf("%s-%s", a.option.Domain, a.option.DomainId)
@@ -48,11 +55,13 @@ func (a *AliyunCdn) Deploy(ctx context.Context) error {
runtime := &util.RuntimeOptions{}
_, err := a.client.SetCdnDomainSSLCertificateWithOptions(setCdnDomainSSLCertificateRequest, runtime)
resp, err := a.client.SetCdnDomainSSLCertificateWithOptions(setCdnDomainSSLCertificateRequest, runtime)
if err != nil {
return err
}
a.infos = append(a.infos, toStr("cdn设置证书", resp))
return nil
}

View File

@@ -3,6 +3,7 @@ package deployer
import (
"certimate/internal/applicant"
"context"
"encoding/json"
"errors"
"strings"
@@ -10,13 +11,12 @@ import (
)
const (
configTypeAliyun = "aliyun"
)
const (
targetAliyunOss = "aliyun-oss"
targetAliyunCdn = "aliyun-cdn"
targetSSH = "ssh"
targetAliyunOss = "aliyun-oss"
targetAliyunCdn = "aliyun-cdn"
targetSSH = "ssh"
targetWebhook = "webhook"
targetTencentCdn = "tencent-cdn"
targetQiniuCdn = "qiniu-cdn"
)
type DeployerOption struct {
@@ -29,20 +29,26 @@ type DeployerOption struct {
type Deployer interface {
Deploy(ctx context.Context) error
GetInfo() []string
}
func Get(record *models.Record) (Deployer, error) {
func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
access := record.ExpandedOne("targetAccess")
option := &DeployerOption{
DomainId: record.Id,
Domain: record.GetString("domain"),
Product: getProduct(record),
Access: access.GetString("config"),
Certificate: applicant.Certificate{
}
if cert != nil {
option.Certificate = *cert
} else {
option.Certificate = applicant.Certificate{
Certificate: record.GetString("certificate"),
PrivateKey: record.GetString("privateKey"),
},
}
}
switch record.GetString("targetType") {
case targetAliyunOss:
return NewAliyun(option)
@@ -50,6 +56,12 @@ func Get(record *models.Record) (Deployer, error) {
return NewAliyunCdn(option)
case targetSSH:
return NewSSH(option)
case targetWebhook:
return NewWebhook(option)
case targetTencentCdn:
return NewTencentCdn(option)
case targetQiniuCdn:
return NewQiNiu(option)
}
return nil, errors.New("not implemented")
}
@@ -62,3 +74,11 @@ func getProduct(record *models.Record) string {
}
return rs[1]
}
func toStr(tag string, data any) string {
if data == nil {
return tag
}
byts, _ := json.Marshal(data)
return tag + "" + string(byts)
}

208
internal/deployer/qiniu.go Normal file
View File

@@ -0,0 +1,208 @@
package deployer
import (
"bytes"
"certimate/internal/domain"
xhttp "certimate/internal/utils/http"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/qiniu/go-sdk/v7/auth"
)
const qiniuGateway = "http://api.qiniu.com"
type qiuniu struct {
option *DeployerOption
info []string
credentials *auth.Credentials
}
func NewQiNiu(option *DeployerOption) (*qiuniu, error) {
access := &domain.QiniuAccess{}
json.Unmarshal([]byte(option.Access), access)
return &qiuniu{
option: option,
info: make([]string, 0),
credentials: auth.New(access.AccessKey, access.SecretKey),
}, nil
}
func (q *qiuniu) GetInfo() []string {
return q.info
}
func (q *qiuniu) Deploy(ctx context.Context) error {
// 上传证书
certId, err := q.uploadCert()
if err != nil {
return fmt.Errorf("uploadCert failed: %w", err)
}
// 获取域名信息
domainInfo, err := q.getDomainInfo()
if err != nil {
return fmt.Errorf("getDomainInfo failed: %w", err)
}
// 判断域名是否启用 https
if domainInfo.Https != nil && domainInfo.Https.CertID != "" {
// 启用了 https
// 修改域名证书
err = q.modifyDomainCert(certId)
if err != nil {
return fmt.Errorf("modifyDomainCert failed: %w", err)
}
} else {
// 没启用 https
// 启用 https
err = q.enableHttps(certId)
if err != nil {
return fmt.Errorf("enableHttps failed: %w", err)
}
}
return nil
}
func (q *qiuniu) enableHttps(certId string) error {
path := fmt.Sprintf("/domain/%s/sslize", q.option.Domain)
body := &modifyDomainCertReq{
CertID: certId,
ForceHttps: true,
Http2Enable: true,
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("enable https failed: %w", err)
}
_, err = q.req(qiniuGateway+path, http.MethodPut, bytes.NewReader(bodyBytes))
if err != nil {
return fmt.Errorf("enable https failed: %w", err)
}
return nil
}
type domainInfo struct {
Https *modifyDomainCertReq `json:"https"`
}
func (q *qiuniu) getDomainInfo() (*domainInfo, error) {
path := fmt.Sprintf("/domain/%s", q.option.Domain)
res, err := q.req(qiniuGateway+path, http.MethodGet, nil)
if err != nil {
return nil, fmt.Errorf("req failed: %w", err)
}
resp := &domainInfo{}
err = json.Unmarshal(res, resp)
if err != nil {
return nil, fmt.Errorf("json.Unmarshal failed: %w", err)
}
return resp, nil
}
type uploadCertReq struct {
Name string `json:"name"`
CommonName string `json:"common_name"`
Pri string `json:"pri"`
Ca string `json:"ca"`
}
type uploadCertResp struct {
CertID string `json:"certID"`
}
func (q *qiuniu) uploadCert() (string, error) {
path := "/sslcert"
body := &uploadCertReq{
Name: q.option.Domain,
CommonName: q.option.Domain,
Pri: q.option.Certificate.PrivateKey,
Ca: q.option.Certificate.Certificate,
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return "", fmt.Errorf("json.Marshal failed: %w", err)
}
res, err := q.req(qiniuGateway+path, http.MethodPost, bytes.NewReader(bodyBytes))
if err != nil {
return "", fmt.Errorf("req failed: %w", err)
}
resp := &uploadCertResp{}
err = json.Unmarshal(res, resp)
if err != nil {
return "", fmt.Errorf("json.Unmarshal failed: %w", err)
}
return resp.CertID, nil
}
type modifyDomainCertReq struct {
CertID string `json:"certId"`
ForceHttps bool `json:"forceHttps"`
Http2Enable bool `json:"http2Enable"`
}
func (q *qiuniu) modifyDomainCert(certId string) error {
path := fmt.Sprintf("/domain/%s/httpsconf", q.option.Domain)
body := &modifyDomainCertReq{
CertID: certId,
ForceHttps: true,
Http2Enable: true,
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("json.Marshal failed: %w", err)
}
_, err = q.req(qiniuGateway+path, http.MethodPut, bytes.NewReader(bodyBytes))
if err != nil {
return fmt.Errorf("req failed: %w", err)
}
return nil
}
func (q *qiuniu) req(url, method string, body io.Reader) ([]byte, error) {
req := xhttp.BuildReq(url, method, body, map[string]string{
"Content-Type": "application/json",
})
if err := q.credentials.AddToken(auth.TokenQBox, req); err != nil {
return nil, fmt.Errorf("credentials.AddToken failed: %w", err)
}
respBody, err := xhttp.ToRequest(req)
if err != nil {
return nil, fmt.Errorf("ToRequest failed: %w", err)
}
defer respBody.Close()
res, err := io.ReadAll(respBody)
if err != nil {
return nil, fmt.Errorf("io.ReadAll failed: %w", err)
}
return res, nil
}

View File

@@ -0,0 +1,86 @@
package deployer
import (
"certimate/internal/applicant"
"testing"
"github.com/qiniu/go-sdk/v7/auth"
)
func Test_qiuniu_uploadCert(t *testing.T) {
type fields struct {
option *DeployerOption
}
tests := []struct {
name string
fields fields
want string
wantErr bool
}{
{
name: "test",
fields: fields{
option: &DeployerOption{
DomainId: "1",
Domain: "example.com",
Product: "test",
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
Certificate: applicant.Certificate{
Certificate: "",
PrivateKey: "",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q, _ := NewQiNiu(tt.fields.option)
got, err := q.uploadCert()
if (err != nil) != tt.wantErr {
t.Errorf("qiuniu.uploadCert() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("qiuniu.uploadCert() = %v, want %v", got, tt.want)
}
})
}
}
func Test_qiuniu_modifyDomainCert(t *testing.T) {
type fields struct {
option *DeployerOption
info []string
credentials *auth.Credentials
}
type args struct {
certId string
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "test",
fields: fields{
option: &DeployerOption{
DomainId: "1",
Domain: "jt1.ikit.fun",
Product: "test",
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q, _ := NewQiNiu(tt.fields.option)
if err := q.modifyDomainCert(tt.args.certId); (err != nil) != tt.wantErr {
t.Errorf("qiuniu.modifyDomainCert() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@@ -14,6 +14,7 @@ import (
type ssh struct {
option *DeployerOption
infos []string
}
type sshAccess struct {
@@ -30,9 +31,14 @@ type sshAccess struct {
func NewSSH(option *DeployerOption) (Deployer, error) {
return &ssh{
option: option,
infos: make([]string, 0),
}, nil
}
func (s *ssh) GetInfo() []string {
return s.infos
}
func (s *ssh) Deploy(ctx context.Context) error {
access := &sshAccess{}
if err := json.Unmarshal([]byte(s.option.Access), access); err != nil {
@@ -45,6 +51,8 @@ func (s *ssh) Deploy(ctx context.Context) error {
}
defer client.Close()
s.infos = append(s.infos, toStr("ssh连接成功", nil))
// 上传
session, err := client.NewSession()
if err != nil {
@@ -52,16 +60,22 @@ func (s *ssh) Deploy(ctx context.Context) error {
}
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 {
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 {
return fmt.Errorf("failed to upload private key: %w", err)
}
s.infos = append(s.infos, toStr("ssh上传私钥成功", nil))
// 执行命令
var stdoutBuf bytes.Buffer
session.Stdout = &stdoutBuf
@@ -72,6 +86,8 @@ func (s *ssh) Deploy(ctx context.Context) error {
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
}
@@ -83,7 +99,7 @@ func (s *ssh) upload(client *sshPkg.Client, content, path string) error {
}
defer sftpCli.Close()
if err := sftpCli.MkdirAll(xpath.Base(path)); err != nil {
if err := sftpCli.MkdirAll(xpath.Dir(path)); err != nil {
return fmt.Errorf("failed to create remote directory: %w", err)
}

View File

@@ -0,0 +1,12 @@
package deployer
import (
"os"
"path"
"testing"
)
func TestPath(t *testing.T) {
dir := path.Dir("./a/b/c")
os.MkdirAll(dir, 0755)
}

View File

@@ -0,0 +1,171 @@
package deployer
import (
"certimate/internal/domain"
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"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"
tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813"
)
type tencentCdn struct {
option *DeployerOption
credential *common.Credential
infos []string
}
func NewTencentCdn(option *DeployerOption) (Deployer, error) {
access := &domain.TencentAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err)
}
credential := common.NewCredential(
access.SecretId,
access.SecretKey,
)
return &tencentCdn{
option: option,
credential: credential,
infos: make([]string, 0),
}, nil
}
func (t *tencentCdn) GetInfo() []string {
return t.infos
}
func (t *tencentCdn) Deploy(ctx context.Context) error {
// 查询有没有对应的资源
resource, err := t.resource()
if err != nil {
return fmt.Errorf("failed to get resource: %w", err)
}
t.infos = append(t.infos, toStr("查询对应的资源", resource))
// 上传证书
certId, err := t.uploadCert()
if err != nil {
return fmt.Errorf("failed to upload certificate: %w", err)
}
t.infos = append(t.infos, toStr("上传证书", certId))
if err := t.deploy(resource, certId); err != nil {
return fmt.Errorf("failed to deploy: %w", err)
}
return nil
}
func (t *tencentCdn) uploadCert() (string, error) {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
client, _ := ssl.NewClient(t.credential, "", cpf)
request := ssl.NewUploadCertificateRequest()
request.CertificatePublicKey = common.StringPtr(t.option.Certificate.Certificate)
request.CertificatePrivateKey = common.StringPtr(t.option.Certificate.PrivateKey)
request.Alias = common.StringPtr(t.option.Domain)
request.Repeatable = common.BoolPtr(true)
response, err := client.UploadCertificate(request)
if err != nil {
return "", fmt.Errorf("failed to upload certificate: %w", err)
}
return *response.Response.CertificateId, nil
}
func (t *tencentCdn) deploy(resource *tag.ResourceTagMapping, certId string) error {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
// 实例化要请求产品的client对象,clientProfile是可选的
client, _ := ssl.NewClient(t.credential, "", cpf)
resourceId, err := getResourceId(resource)
if err != nil {
return fmt.Errorf("failed to get resource id: %w", err)
}
// 实例化一个请求对象,每个接口都会对应一个request对象
request := ssl.NewDeployCertificateInstanceRequest()
request.CertificateId = common.StringPtr(certId)
request.InstanceIdList = common.StringPtrs([]string{resourceId})
request.ResourceType = common.StringPtr("cdn")
request.Status = common.Int64Ptr(1)
// 返回的resp是一个DeployCertificateInstanceResponse的实例与请求对象对应
resp, err := client.DeployCertificateInstance(request)
if err != nil {
return fmt.Errorf("failed to deploy certificate: %w", err)
}
t.infos = append(t.infos, toStr("部署证书", resp.Response))
return nil
}
func (t *tencentCdn) resource() (*tag.ResourceTagMapping, error) {
request := tag.NewGetResourcesRequest()
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "tag.tencentcloudapi.com"
client, err := tag.NewClient(t.credential, "", cpf)
if err != nil {
return nil, fmt.Errorf("failed to create client: %w", err)
}
response, err := client.GetResources(request)
if err != nil {
return nil, fmt.Errorf("failed to get resources: %w", err)
}
for _, resource := range response.Response.ResourceTagMappingList {
if t.compare(resource) {
return resource, nil
}
}
return nil, errors.New("no resource found")
}
func (t *tencentCdn) compare(resource *tag.ResourceTagMapping) bool {
slices := strings.Split(*resource.Resource, "/")
if len(slices) != 3 {
return false
}
typeSlices := strings.Split(slices[0], "::")
if len(typeSlices) != 3 {
return false
}
if typeSlices[1] != "cdn" || slices[2] != t.option.Domain {
return false
}
return true
}
func getResourceId(resource *tag.ResourceTagMapping) (string, error) {
slices := strings.Split(*resource.Resource, "/")
if len(slices) != 3 {
return "", errors.New("invalid resource")
}
return slices[2], nil
}

View File

@@ -0,0 +1,63 @@
package deployer
import (
"bytes"
xhttp "certimate/internal/utils/http"
"context"
"encoding/json"
"fmt"
"net/http"
)
type webhookAccess struct {
Url string `json:"url"`
}
type hookData struct {
Domain string `json:"domain"`
Certificate string `json:"certificate"`
PrivateKey string `json:"privateKey"`
}
type webhook struct {
option *DeployerOption
infos []string
}
func NewWebhook(option *DeployerOption) (Deployer, error) {
return &webhook{
option: option,
infos: make([]string, 0),
}, nil
}
func (w *webhook) GetInfo() []string {
return w.infos
}
func (w *webhook) Deploy(ctx context.Context) error {
access := &webhookAccess{}
if err := json.Unmarshal([]byte(w.option.Access), access); err != nil {
return fmt.Errorf("failed to parse hook access config: %w", err)
}
data := &hookData{
Domain: w.option.Domain,
Certificate: w.option.Certificate.Certificate,
PrivateKey: w.option.Certificate.PrivateKey,
}
body, _ := json.Marshal(data)
resp, err := xhttp.Req(access.Url, http.MethodPost, bytes.NewReader(body), map[string]string{
"Content-Type": "application/json",
})
if err != nil {
return fmt.Errorf("failed to send hook request: %w", err)
}
w.infos = append(w.infos, toStr("webhook response", string(resp)))
return nil
}

29
internal/domain/access.go Normal file
View File

@@ -0,0 +1,29 @@
package domain
type AliyunAccess struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type TencentAccess struct {
SecretId string `json:"secretId"`
SecretKey string `json:"secretKey"`
}
type CloudflareAccess struct {
DnsApiToken string `json:"dnsApiToken"`
}
type QiniuAccess struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
}
type NameSiloAccess struct {
ApiKey string `json:"apiKey"`
}
type GodaddyAccess struct {
ApiKey string `json:"apiKey"`
ApiSecret string `json:"apiSecret"`
}

View File

@@ -26,15 +26,18 @@ func deploy(ctx context.Context, record *models.Record) error {
app.GetApp().Logger().Error("部署失败", "err", r)
}
}()
currRecord, err := app.GetApp().Dao().FindRecordById("domains", record.Id)
var certificate *applicant.Certificate
history := NewHistory(record)
defer history.commit()
// ############1.检查域名配置
history.record(checkPhase, "开始检查", nil)
currRecord, err := app.GetApp().Dao().FindRecordById("domains", record.Id)
if err != nil {
app.GetApp().Logger().Error("获取记录失败", "err", err)
history.record(checkPhase, "获取域名配置失败", err)
history.record(checkPhase, "获取域名配置失败", &RecordInfo{Err: err})
return err
}
history.record(checkPhase, "获取记录成功", nil)
@@ -46,7 +49,7 @@ func deploy(ctx context.Context, record *models.Record) error {
}
err = errors.Join(errList...)
app.GetApp().Logger().Error("展开记录失败", "err", err)
history.record(checkPhase, "获取授权信息失败", err)
history.record(checkPhase, "获取授权信息失败", &RecordInfo{Err: err})
return err
}
history.record(checkPhase, "获取授权信息成功", nil)
@@ -54,9 +57,11 @@ func deploy(ctx context.Context, record *models.Record) error {
cert := currRecord.GetString("certificate")
expiredAt := currRecord.GetDateTime("expiredAt").Time()
if cert != "" && time.Until(expiredAt) > time.Hour*24 && currRecord.GetBool("deployed") {
if cert != "" && time.Until(expiredAt) > time.Hour*24*10 && currRecord.GetBool("deployed") {
app.GetApp().Logger().Info("证书在有效期内")
history.record(checkPhase, "证书在有效期内且已部署,跳过", nil, true)
history.record(checkPhase, "证书在有效期内且已部署,跳过", &RecordInfo{
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
}, true)
return err
}
history.record(checkPhase, "检查通过", nil, true)
@@ -65,72 +70,50 @@ func deploy(ctx context.Context, record *models.Record) error {
history.record(applyPhase, "开始申请", nil)
if cert != "" && time.Until(expiredAt) > time.Hour*24 {
history.record(applyPhase, "证书在有效期内,跳过", nil)
history.record(applyPhase, "证书在有效期内,跳过", &RecordInfo{
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
})
} else {
applicant, err := applicant.Get(currRecord)
if err != nil {
history.record(applyPhase, "获取applicant失败", err)
history.record(applyPhase, "获取applicant失败", &RecordInfo{Err: err})
app.GetApp().Logger().Error("获取applicant失败", "err", err)
return err
}
certificate, err := applicant.Apply()
certificate, err = applicant.Apply()
if err != nil {
history.record(applyPhase, "申请证书失败", err)
history.record(applyPhase, "申请证书失败", &RecordInfo{Err: err})
app.GetApp().Logger().Error("申请证书失败", "err", err)
return err
}
history.record(applyPhase, "申请证书成功", nil)
if err = saveCert(ctx, record, certificate); err != nil {
history.record(applyPhase, "保存证书失败", err)
app.GetApp().Logger().Error("保存证书失败", "err", err)
return err
}
history.record(applyPhase, "申请证书成功", &RecordInfo{
Info: []string{fmt.Sprintf("证书地址: %s", certificate.CertUrl)},
})
history.setCert(certificate)
}
history.record(applyPhase, "保存证书成功", nil, true)
// ############3.部署证书
history.record(deployPhase, "开始部署", nil, false)
deployer, err := deployer.Get(currRecord)
deployer, err := deployer.Get(currRecord, certificate)
if err != nil {
history.record(deployPhase, "获取deployer失败", err)
history.record(deployPhase, "获取deployer失败", &RecordInfo{Err: err})
app.GetApp().Logger().Error("获取deployer失败", "err", err)
return err
}
if err = deployer.Deploy(ctx); err != nil {
setDeployed(ctx, record, false)
app.GetApp().Logger().Error("部署失败", "err", err)
history.record(deployPhase, "部署失败", err)
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfo()})
return err
}
setDeployed(ctx, record, true)
app.GetApp().Logger().Info("部署成功")
history.record(deployPhase, "部署成功", nil, true)
history.record(deployPhase, "部署成功", &RecordInfo{
Info: deployer.GetInfo(),
}, true)
return nil
}
func setDeployed(ctx context.Context, record *models.Record, deployed bool) error {
record.Set("deployed", deployed)
if err := app.GetApp().Dao().SaveRecord(record); err != nil {
return err
}
return nil
}
func saveCert(ctx context.Context, record *models.Record, cert *applicant.Certificate) error {
record.Set("certUrl", cert.CertUrl)
record.Set("certStableUrl", cert.CertStableUrl)
record.Set("privateKey", cert.PrivateKey)
record.Set("certificate", cert.Certificate)
record.Set("issuerCertificate", cert.IssuerCertificate)
record.Set("csr", cert.Csr)
record.Set("expiredAt", time.Now().Add(time.Hour*24*90))
if err := app.GetApp().Dao().SaveRecord(record); err != nil {
return err
}
return nil
}

View File

@@ -14,10 +14,12 @@ func create(ctx context.Context, record *models.Record) error {
}
if record.GetBool("rightnow") {
go func() {
if err := deploy(ctx, record); err != nil {
app.GetApp().Logger().Error("deploy failed", "err", err)
}
}()
if err := deploy(ctx, record); err != nil {
return err
}
}
scheduler := app.GetScheduler()
@@ -45,9 +47,11 @@ func update(ctx context.Context, record *models.Record) error {
if record.GetBool("rightnow") {
if err := deploy(ctx, record); err != nil {
return err
}
go func() {
if err := deploy(ctx, record); err != nil {
app.GetApp().Logger().Error("deploy failed", "err", err)
}
}()
}
err := scheduler.Add(record.Id, record.GetString("crontab"), func() {

View File

@@ -1,6 +1,7 @@
package domains
import (
"certimate/internal/applicant"
"certimate/internal/utils/app"
"certimate/internal/utils/xtime"
"time"
@@ -9,9 +10,15 @@ import (
)
type historyItem struct {
Time string `json:"time"`
Message string `json:"message"`
Error string `json:"error"`
Time string `json:"time"`
Message string `json:"message"`
Error string `json:"error"`
Info []string `json:"info"`
}
type RecordInfo struct {
Err error `json:"err"`
Info []string `json:"info"`
}
type history struct {
@@ -20,6 +27,7 @@ type history struct {
Phase Phase `json:"phase"`
PhaseSuccess bool `json:"phaseSuccess"`
DeployedAt string `json:"deployedAt"`
Cert *applicant.Certificate `json:"cert"`
}
func NewHistory(record *models.Record) *history {
@@ -32,26 +40,34 @@ func NewHistory(record *models.Record) *history {
}
}
func (a *history) record(phase Phase, msg string, err error, pass ...bool) {
func (a *history) record(phase Phase, msg string, info *RecordInfo, pass ...bool) {
if info == nil {
info = &RecordInfo{}
}
a.Phase = phase
if len(pass) > 0 {
a.PhaseSuccess = pass[0]
}
errMsg := ""
if err != nil {
errMsg = err.Error()
if info.Err != nil {
errMsg = info.Err.Error()
a.PhaseSuccess = false
}
a.Log[phase] = append(a.Log[phase], historyItem{
Message: msg,
Error: errMsg,
Info: info.Info,
Time: xtime.BeijingTimeStr(),
})
}
func (a *history) setCert(cert *applicant.Certificate) {
a.Cert = cert
}
func (a *history) commit() error {
collection, err := app.GetApp().Dao().FindCollectionByNameOrId("deployments")
if err != nil {
@@ -78,6 +94,19 @@ func (a *history) commit() error {
domainRecord.Set("lastDeployedAt", a.DeployedAt)
domainRecord.Set("lastDeployment", record.Id)
domainRecord.Set("rightnow", false)
if a.Phase == deployPhase && a.PhaseSuccess {
domainRecord.Set("deployed", true)
}
cert := a.Cert
if cert != nil {
domainRecord.Set("certUrl", cert.CertUrl)
domainRecord.Set("certStableUrl", cert.CertStableUrl)
domainRecord.Set("privateKey", cert.PrivateKey)
domainRecord.Set("certificate", cert.Certificate)
domainRecord.Set("issuerCertificate", cert.IssuerCertificate)
domainRecord.Set("csr", cert.Csr)
domainRecord.Set("expiredAt", time.Now().Add(time.Hour*24*90))
}
if err := app.GetApp().Dao().SaveRecord(domainRecord); err != nil {
return err

View File

@@ -12,7 +12,9 @@ var intanceOnce sync.Once
func GetApp() *pocketbase.PocketBase {
intanceOnce.Do(func() {
instance = pocketbase.New()
instance = pocketbase.NewWithConfig(pocketbase.Config{
HideStartBanner: true,
})
})
return instance

View File

@@ -0,0 +1,73 @@
package http
import (
"fmt"
"io"
"net/http"
"time"
"github.com/gojek/heimdall/v7/httpclient"
)
type Options struct {
Timeout time.Duration
}
type Option func(o *Options)
func WithTimeout(timeout time.Duration) Option {
return func(o *Options) {
o.Timeout = timeout
}
}
func Req(url string, method string, body io.Reader, head map[string]string, opts ...Option) ([]byte, error) {
reader, err := Req2GetReader(url, method, body, head, opts...)
if err != nil {
return nil, err
}
defer reader.Close()
return io.ReadAll(reader)
}
func Req2GetReader(url string, method string, body io.Reader, head map[string]string, opts ...Option) (io.ReadCloser, error) {
req := BuildReq(url, method, body, head)
return ToRequest(req, opts...)
}
func BuildReq(url string, method string, body io.Reader, head map[string]string) *http.Request {
// Create an http.Request instance
req, _ := http.NewRequest(method, url, body)
for k, v := range head {
req.Header.Set(k, v)
}
return req
}
func ToRequest(req *http.Request, opts ...Option) (io.ReadCloser, error) {
options := &Options{
Timeout: 30000 * time.Millisecond,
}
for _, opt := range opts {
opt(options)
}
client := httpclient.NewClient(httpclient.WithHTTPTimeout(options.Timeout))
// Call the `Do` method, which has a similar interface to the `http.Do` method
res, err := client.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status code is not 200: %d", res.StatusCode)
}
return res.Body, nil
}

View File

@@ -14,6 +14,8 @@ import (
"certimate/internal/utils/app"
_ "certimate/migrations"
"certimate/ui"
_ "time/tzdata"
)
func main() {

View File

@@ -13,14 +13,14 @@ func init() {
dao := daos.New(db)
admin := &models.Admin{}
admin.Email = "admin@essay.com"
admin.Email = "admin@certimate.fun"
admin.SetPassword("1234567890")
return dao.SaveAdmin(admin)
}, func(db dbx.Builder) error {
// add down queries...
dao := daos.New(db)
admin, _ := dao.FindAdminByEmail("admin@essay.com")
admin, _ := dao.FindAdminByEmail("admin@certimate.fun")
if admin != nil {
return dao.DeleteAdmin(admin)
}

View File

@@ -12,73 +12,10 @@ import (
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `[
{
"id": "_pb_users_auth_",
"created": "2024-07-29 09:44:56.398Z",
"updated": "2024-08-21 04:13:40.056Z",
"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": "z3p974ainxjqlvs",
"created": "2024-07-29 10:02:48.334Z",
"updated": "2024-08-22 08:05:10.026Z",
"updated": "2024-09-12 13:09:54.500Z",
"name": "domains",
"type": "base",
"system": false,
@@ -237,7 +174,10 @@ func init() {
"values": [
"aliyun-oss",
"aliyun-cdn",
"ssh"
"ssh",
"webhook",
"tencent-cdn",
"qiniu-cdn"
]
}
},
@@ -330,7 +270,7 @@ func init() {
{
"id": "4yzbv8urny5ja1e",
"created": "2024-07-29 10:04:39.685Z",
"updated": "2024-08-22 08:00:20.090Z",
"updated": "2024-09-12 13:18:00.093Z",
"name": "access",
"type": "base",
"system": false,
@@ -374,7 +314,12 @@ func init() {
"values": [
"aliyun",
"tencent",
"ssh"
"ssh",
"webhook",
"cloudflare",
"qiniu",
"namesilo",
"godaddy"
]
}
},
@@ -390,6 +335,23 @@ func init() {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "hsxcnlvd",
"name": "usage",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"apply",
"deploy",
"all"
]
}
}
],
"indexes": [
@@ -405,7 +367,7 @@ func init() {
{
"id": "0a1o4e6sstp694f",
"created": "2024-07-30 06:30:27.801Z",
"updated": "2024-08-21 04:13:40.056Z",
"updated": "2024-09-12 13:09:54.500Z",
"name": "deployments",
"type": "base",
"system": false,
@@ -486,6 +448,69 @@ func init() {
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "_pb_users_auth_",
"created": "2024-09-12 13:09:54.234Z",
"updated": "2024-09-12 13:09:54.500Z",
"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
}
}
]`

View File

@@ -0,0 +1,20 @@
package migrations
import (
"github.com/pocketbase/dbx"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(db dbx.Builder) error {
// add up queries...
db.NewQuery("update access set usage='all' where configType in ('aliyun', 'tencent')").Execute()
db.NewQuery("update access set usage='deploy' where configType in ('ssh', 'webhook','qiniu')").Execute()
db.NewQuery("update access set usage='apply' where configType in ('cloudflare','namesilo','godaddy')").Execute()
return nil
}, func(db dbx.Builder) error {
// add down queries...
return nil
})
}

View File

@@ -12,73 +12,10 @@ import (
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `[
{
"id": "_pb_users_auth_",
"created": "2024-07-29 09:44:56.398Z",
"updated": "2024-07-29 09:44:56.400Z",
"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": "z3p974ainxjqlvs",
"created": "2024-07-29 10:02:48.334Z",
"updated": "2024-08-21 03:48:45.452Z",
"updated": "2024-09-12 23:13:12.119Z",
"name": "domains",
"type": "base",
"system": false,
@@ -97,6 +34,19 @@ func init() {
"pattern": ""
}
},
{
"system": false,
"id": "ukkhuw85",
"name": "email",
"type": "email",
"required": false,
"presentable": false,
"unique": false,
"options": {
"exceptDomains": null,
"onlyDomains": null
}
},
{
"system": false,
"id": "v98eebqq",
@@ -235,7 +185,12 @@ func init() {
"options": {
"maxSelect": 1,
"values": [
"aliyun-oss"
"aliyun-oss",
"aliyun-cdn",
"ssh",
"webhook",
"tencent-cdn",
"qiniu-cdn"
]
}
},
@@ -328,7 +283,7 @@ func init() {
{
"id": "4yzbv8urny5ja1e",
"created": "2024-07-29 10:04:39.685Z",
"updated": "2024-08-21 03:48:20.637Z",
"updated": "2024-09-12 23:08:52.810Z",
"name": "access",
"type": "base",
"system": false,
@@ -371,7 +326,13 @@ func init() {
"maxSelect": 1,
"values": [
"aliyun",
"tencent"
"tencent",
"ssh",
"webhook",
"cloudflare",
"qiniu",
"namesilo",
"godaddy"
]
}
},
@@ -387,6 +348,23 @@ func init() {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "hsxcnlvd",
"name": "usage",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"apply",
"deploy",
"all"
]
}
}
],
"indexes": [
@@ -402,7 +380,7 @@ func init() {
{
"id": "0a1o4e6sstp694f",
"created": "2024-07-30 06:30:27.801Z",
"updated": "2024-08-21 03:48:34.762Z",
"updated": "2024-09-12 23:08:52.810Z",
"name": "deployments",
"type": "base",
"system": false,
@@ -483,6 +461,114 @@ func init() {
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "_pb_users_auth_",
"created": "2024-09-12 13:09:54.234Z",
"updated": "2024-09-12 23:08:52.811Z",
"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-12 23:19:09.110Z",
"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": {}
}
]`

30
nixpacks.toml Normal file
View File

@@ -0,0 +1,30 @@
providers = []
buildImage = 'ghcr.io/railwayapp/nixpacks:ubuntu-1707782610'
[variables]
CGO_ENABLED = '0'
NIXPACKS_METADATA = 'go'
PORT = '8090'
[phases.build]
paths = ['/bin','/usr/local/bin']
dependsOn = [
'install',
'setup',
]
cmds = ['go build -o out']
cacheDirectories = ['/root/.cache/go-build']
[phases.install]
dependsOn = ['setup']
cmds = ['go mod download']
cacheDirectories = ['/root/.cache/go-build']
[phases.setup]
nixPkgs = ['go']
nixOverlays = []
nixpkgsArchive = '1f13eabcd6f5b00fe9de9575ac52c66a0e887ce6'
[start]
cmd = './out serve --http=0.0.0.0:8090 --dir=/data/pb_data '

View File

@@ -1 +0,0 @@
VITE_API_DOMAIN=http://127.0.0.1:8090

4
ui/.gitignore vendored
View File

@@ -6,7 +6,7 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
./ui/.env
node_modules
dist-ssr
*.local
@@ -21,3 +21,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.env

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

284
ui/dist/assets/index-BRoqOo1T.js vendored Normal file
View File

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

24
ui/dist/imgs/providers/cloudflare.svg vendored Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -70 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<g transform="translate(0.000000, -1.000000)">
<path
d="M202.3569,50.394 L197.0459,48.27 C172.0849,104.434 72.7859,70.289 66.8109,86.997 C65.8149,98.283 121.0379,89.143 160.5169,91.056 C172.5559,91.639 178.5929,100.727 173.4809,115.54 L183.5499,115.571 C195.1649,79.362 232.2329,97.841 233.7819,85.891 C231.2369,78.034 191.1809,85.891 202.3569,50.394 Z"
fill="#FFFFFF">
</path>
<path
d="M176.332,109.3483 C177.925,104.0373 177.394,98.7263 174.739,95.5393 C172.083,92.3523 168.365,90.2283 163.585,89.6973 L71.17,88.6343 C70.639,88.6343 70.108,88.1033 69.577,88.1033 C69.046,87.5723 69.046,87.0413 69.577,86.5103 C70.108,85.4483 70.639,84.9163 71.701,84.9163 L164.647,83.8543 C175.801,83.3233 187.486,74.2943 191.734,63.6723 L197.046,49.8633 C197.046,49.3313 197.577,48.8003 197.046,48.2693 C191.203,21.1823 166.772,0.9993 138.091,0.9993 C111.535,0.9993 88.697,17.9953 80.73,41.8963 C75.419,38.1783 69.046,36.0533 61.61,36.5853 C48.863,37.6473 38.772,48.2693 37.178,61.0163 C36.647,64.2033 37.178,67.3903 37.71,70.5763 C16.996,71.1073 0,88.1033 0,109.3483 C0,111.4723 0,113.0663 0.531,115.1903 C0.531,116.2533 1.593,116.7843 2.125,116.7843 L172.614,116.7843 C173.676,116.7843 174.739,116.2533 174.739,115.1903 L176.332,109.3483 Z"
fill="#F4811F">
</path>
<path
d="M205.5436,49.8628 L202.8876,49.8628 C202.3566,49.8628 201.8256,50.3938 201.2946,50.9248 L197.5766,63.6718 C195.9836,68.9828 196.5146,74.2948 199.1706,77.4808 C201.8256,80.6678 205.5436,82.7918 210.3236,83.3238 L229.9756,84.3858 C230.5066,84.3858 231.0376,84.9168 231.5686,84.9168 C232.0996,85.4478 232.0996,85.9788 231.5686,86.5098 C231.0376,87.5728 230.5066,88.1038 229.4436,88.1038 L209.2616,89.1658 C198.1076,89.6968 186.4236,98.7258 182.1746,109.3478 L181.1116,114.1288 C180.5806,114.6598 181.1116,115.7218 182.1746,115.7218 L252.2826,115.7218 C253.3446,115.7218 253.8756,115.1908 253.8756,114.1288 C254.9376,109.8798 255.9996,105.0998 255.9996,100.3188 C255.9996,72.7008 233.1616,49.8628 205.5436,49.8628"
fill="#FAAD3F">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

5
ui/dist/imgs/providers/godaddy.svg vendored Normal file
View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<circle cx="512" cy="512" r="512" style="fill:#1bdbdb"/>
<path d="M697.6 315.9c-53.2-33.2-123.3-25.3-185.6 13.9-62.4-39.3-132.4-47.2-185.6-13.9-84.1 52.5-94.3 187.8-22.8 302.2 52.7 84.3 135.1 133.7 208.4 132.8 73.3.9 155.7-48.5 208.4-132.8 71.5-114.4 61.3-249.7-22.8-302.2M342.2 594c-15-24.1-26.1-49.5-33-75.5-6.5-24.5-8.9-48.5-7.1-71.2 3.2-42.3 20.4-75.2 48.4-92.7s65.2-18.6 104.5-2.9c5.9 2.4 11.8 5.1 17.6 8.1-21 19-40.3 41.9-56.7 68.1-43.4 69.5-56.6 146.7-41.5 208.4-11.8-12.8-22.6-27-32.2-42.3m372.6-75.6c-6.9 26.1-17.9 51.5-33 75.5-9.6 15.4-20.4 29.5-32.3 42.3 13.5-55.2 4.4-122.9-28.9-186.3-2.3-4.5-7.7-5.9-12-3.3l-103.5 64.7c-4 2.5-5.2 7.7-2.7 11.7l15.2 24.3c2.5 4 7.7 5.2 11.7 2.7l67.1-41.9c2.2 6.4 4.3 12.9 6 19.5 6.5 24.5 8.9 48.5 7.1 71.2-3.2 42.3-20.4 75.2-48.4 92.7-14 8.8-30.3 13.4-48 13.9h-2.2c-17.7-.5-34-5.1-48-13.9-28-17.5-45.2-50.4-48.4-92.7-1.7-22.7.7-46.7 7.1-71.2 6.8-26.1 17.9-51.5 33-75.5 15-24.1 33-45.2 53.4-62.8 19.2-16.6 39.7-29.2 60.9-37.6 39.4-15.7 76.5-14.6 104.5 2.9s45.2 50.4 48.4 92.7c1.8 22.6-.6 46.6-7 71.1" style="fill:#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

8
ui/dist/imgs/providers/namesilo.svg vendored Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<circle cx="512" cy="512" r="512" style="fill:#031b4e" />
<path
d="M668 415.5c0 16.6-69.9 30.2-156 30.2s-156-13.6-156-30.2v85c0 17.1 69.9 30.7 156 30.7s156-13.6 156-30.7v-85zM512 556.4c86.1 0 156-13.6 156-30.2v85.5c0 16.6-69.9 30.2-156 30.2s-156-13.6-156-30.2v-85.5c0 16.6 69.9 30.2 156 30.2zm156 80.5c0 16.6-69.9 30.2-156 30.2s-156-13.6-156-30.2v85.5c0 16.6 69.9 30.2 156 30.2s156-13.6 156-30.2v-85.5zm0-296.9L512 239.4 356 340v55.4c0 14.1 69.9 25.2 156 25.2s156-11.1 156-25.2V340z"
style="fill:#fff" />
</svg>

After

Width:  |  Height:  |  Size: 743 B

26
ui/dist/imgs/providers/ssh.svg vendored Normal file
View File

@@ -0,0 +1,26 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 73 73" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" fill="#2562eb">
<g id="SVGRepo_bgCarrier" stroke-width="0" />
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" />
<g id="SVGRepo_iconCarrier">
<title>databases-and-servers/servers/ssh</title>
<desc>Created with Sketch.</desc>
<defs> </defs>
<g id="databases-and-servers/servers/ssh" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="container" transform="translate(2.000000, 2.000000)" fill="#FFFFFF" fill-rule="nonzero"
stroke="#2562eb" stroke-width="2">
<rect id="mask" x="-1" y="-1" width="71" height="71" rx="14"> </rect>
</g>
<g id="terminal" transform="translate(11.000000, 17.000000)" fill="#2562eb" fill-rule="nonzero">
<path
d="M49.2407485,0.0215730994 L1.65983626,0.0215730994 C0.743128655,0.0215730994 0,0.76480117 0,1.68140936 L0,9.94480117 L50.9005848,9.94480117 L50.9005848,1.68140936 C50.9005848,0.76480117 50.1574561,0.0215730994 49.2407485,0.0215730994 Z M32.0399298,7.19626901 C30.8196082,7.19626901 29.826848,6.20350877 29.826848,4.98318713 C29.826848,3.7628655 30.8196082,2.77010526 32.0399298,2.77010526 C33.2602515,2.77010526 34.2530117,3.7628655 34.2530117,4.98318713 C34.2530117,6.20350877 33.2602515,7.19626901 32.0399298,7.19626901 Z M38.0686667,7.19626901 C36.848345,7.19626901 35.8555848,6.20350877 35.8555848,4.98318713 C35.8555848,3.7628655 36.848345,2.77010526 38.0686667,2.77010526 C39.2889883,2.77010526 40.2817485,3.7628655 40.2817485,4.98318713 C40.2817485,6.20350877 39.2889883,7.19626901 38.0686667,7.19626901 Z M44.0974035,7.19626901 C42.8770819,7.19626901 41.8843216,6.20350877 41.8843216,4.98318713 C41.8843216,3.7628655 42.8770819,2.77010526 44.0974035,2.77010526 C45.3177251,2.77010526 46.3104854,3.7628655 46.3104854,4.98318713 C46.3104854,6.20350877 45.3177251,7.19626901 44.0974035,7.19626901 Z"
id="Shape"> </path>
<path
d="M0,13.2643743 L0,37.0905205 C0,38.0071287 0.743128655,38.7503567 1.65983626,38.7503567 L49.240848,38.7503567 C50.1574561,38.7503567 50.9006842,38.0072281 50.9006842,37.0905205 L50.9006842,13.2643743 L0,13.2643743 Z M23.4073099,27.2714795 L17.9739708,31.8946842 C17.2758772,32.4887895 16.2283392,32.404386 15.6342339,31.706193 C15.0401287,31.0080994 15.1245322,29.9605614 15.8227251,29.3664561 L19.7704035,26.0073158 L15.8227251,22.6481754 C15.1245322,22.0540702 15.0402281,21.0066316 15.6342339,20.3084386 C16.2282398,19.6101462 17.2758772,19.5259415 17.9739708,20.1199474 L23.4073099,24.743152 C24.185731,25.4055556 24.185731,26.6091754 23.4073099,27.2714795 Z M34.0021871,32.2903567 L27.4453567,32.2903567 C26.5287485,32.2903567 25.7855205,31.5472281 25.7855205,30.6305205 C25.7855205,29.7138129 26.5286491,28.9706842 27.4453567,28.9706842 L34.0021871,28.9706842 C34.9187953,28.9706842 35.6620234,29.7138129 35.6620234,30.6305205 C35.6620234,31.5472281 34.9187953,32.2903567 34.0021871,32.2903567 Z"
id="Shape"> </path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

1
ui/dist/imgs/providers/webhook.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24"><path fill="#059669" d="M7 20.5q-1.864 0-3.182-1.318T2.5 16q0-1.246.608-2.268t1.617-1.615q.275-.165.525-.026q.25.14.25.459q0 .121-.064.24q-.065.12-.169.181q-.776.452-1.272 1.258T3.5 16q0 1.442 1.029 2.471T7 19.5q1.404 0 2.414-.961q1.009-.962 1.086-2.347q.02-.31.199-.5q.18-.192.47-.192h4.725q.142-.321.44-.526q.297-.205.666-.205q.51 0 .87.36q.36.361.36.871t-.36.87t-.87.36q-.388 0-.676-.194q-.288-.196-.43-.536h-4.417q-.235 1.84-1.54 2.92T7 20.5m0-3.27q-.51 0-.87-.36T5.77 16q0-.512.378-.892q.379-.381.994-.314l2.562-4.265q-.84-.656-1.272-1.574T8 7q0-1.864 1.318-3.182T12.5 2.5q1.73 0 2.991 1.14t1.455 2.802q.048.225-.108.392q-.155.166-.386.166q-.171 0-.315-.132t-.185-.309q-.16-1.296-1.136-2.177Q13.838 3.5 12.5 3.5q-1.442 0-2.471 1.029T9 7q0 .883.393 1.618t1.101 1.251q.235.162.303.423q.069.262-.068.485l-2.727 4.542q.108.144.168.321t.06.36q0 .51-.36.87t-.87.36m10 3.27q-.708 0-1.34-.201t-1.195-.591q-.329-.221-.268-.58q.06-.359.474-.359q.106 0 .208.05t.188.106q.402.271.903.423T17 19.5q1.442 0 2.471-1.029T20.5 16t-1.029-2.471T17 12.5q-.25 0-.475.038t-.45.093q-.285.086-.558-.017t-.41-.32l-2.459-4.088q-.525.073-.952-.279T11.27 7q0-.51.36-.87q.362-.36.871-.36t.87.36t.36.87q0 .183-.057.357t-.165.299l2.367 3.977q.258-.05.53-.092q.274-.041.595-.041q1.864 0 3.182 1.318T21.5 16t-1.318 3.182T17 20.5"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

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-3QHTbODC.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-127rBbJX.css">
<script type="module" crossorigin src="/assets/index-BRoqOo1T.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cg0yCJnh.css">
</head>
<body class="bg-background">
<div id="root"></div>

111
ui/package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
@@ -23,6 +24,7 @@
"@radix-ui/react-tooltip": "^1.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"jszip": "^3.10.1",
"lucide-react": "^0.417.0",
"moment": "^2.30.1",
"pocketbase": "^0.21.4",
@@ -1621,6 +1623,36 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz",
"integrity": "sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==",
"dependencies": {
"@radix-ui/number": "1.1.0",
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-presence": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-2.1.1.tgz",
@@ -2796,6 +2828,11 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -3543,6 +3580,11 @@
"node": ">= 4"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -3582,8 +3624,7 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/invariant": {
"version": "2.2.4",
@@ -3662,6 +3703,11 @@
"node": ">=8"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
@@ -3748,6 +3794,17 @@
"node": ">=6"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
@@ -3770,6 +3827,14 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz",
@@ -4021,6 +4086,11 @@
"resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw=="
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
@@ -4286,6 +4356,11 @@
"node": ">= 0.8.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
@@ -4466,6 +4541,20 @@
"pify": "^2.3.0"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
@@ -4584,6 +4673,11 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz",
@@ -4604,6 +4698,11 @@
"node": ">=10"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -4651,6 +4750,14 @@
"node": ">=0.10.0"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",

View File

@@ -17,6 +17,7 @@
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
@@ -25,6 +26,7 @@
"@radix-ui/react-tooltip": "^1.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"jszip": "^3.10.1",
"lucide-react": "^0.417.0",
"moment": "^2.30.1",
"pocketbase": "^0.21.4",

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -70 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<g transform="translate(0.000000, -1.000000)">
<path
d="M202.3569,50.394 L197.0459,48.27 C172.0849,104.434 72.7859,70.289 66.8109,86.997 C65.8149,98.283 121.0379,89.143 160.5169,91.056 C172.5559,91.639 178.5929,100.727 173.4809,115.54 L183.5499,115.571 C195.1649,79.362 232.2329,97.841 233.7819,85.891 C231.2369,78.034 191.1809,85.891 202.3569,50.394 Z"
fill="#FFFFFF">
</path>
<path
d="M176.332,109.3483 C177.925,104.0373 177.394,98.7263 174.739,95.5393 C172.083,92.3523 168.365,90.2283 163.585,89.6973 L71.17,88.6343 C70.639,88.6343 70.108,88.1033 69.577,88.1033 C69.046,87.5723 69.046,87.0413 69.577,86.5103 C70.108,85.4483 70.639,84.9163 71.701,84.9163 L164.647,83.8543 C175.801,83.3233 187.486,74.2943 191.734,63.6723 L197.046,49.8633 C197.046,49.3313 197.577,48.8003 197.046,48.2693 C191.203,21.1823 166.772,0.9993 138.091,0.9993 C111.535,0.9993 88.697,17.9953 80.73,41.8963 C75.419,38.1783 69.046,36.0533 61.61,36.5853 C48.863,37.6473 38.772,48.2693 37.178,61.0163 C36.647,64.2033 37.178,67.3903 37.71,70.5763 C16.996,71.1073 0,88.1033 0,109.3483 C0,111.4723 0,113.0663 0.531,115.1903 C0.531,116.2533 1.593,116.7843 2.125,116.7843 L172.614,116.7843 C173.676,116.7843 174.739,116.2533 174.739,115.1903 L176.332,109.3483 Z"
fill="#F4811F">
</path>
<path
d="M205.5436,49.8628 L202.8876,49.8628 C202.3566,49.8628 201.8256,50.3938 201.2946,50.9248 L197.5766,63.6718 C195.9836,68.9828 196.5146,74.2948 199.1706,77.4808 C201.8256,80.6678 205.5436,82.7918 210.3236,83.3238 L229.9756,84.3858 C230.5066,84.3858 231.0376,84.9168 231.5686,84.9168 C232.0996,85.4478 232.0996,85.9788 231.5686,86.5098 C231.0376,87.5728 230.5066,88.1038 229.4436,88.1038 L209.2616,89.1658 C198.1076,89.6968 186.4236,98.7258 182.1746,109.3478 L181.1116,114.1288 C180.5806,114.6598 181.1116,115.7218 182.1746,115.7218 L252.2826,115.7218 C253.3446,115.7218 253.8756,115.1908 253.8756,114.1288 C254.9376,109.8798 255.9996,105.0998 255.9996,100.3188 C255.9996,72.7008 233.1616,49.8628 205.5436,49.8628"
fill="#FAAD3F">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<circle cx="512" cy="512" r="512" style="fill:#1bdbdb"/>
<path d="M697.6 315.9c-53.2-33.2-123.3-25.3-185.6 13.9-62.4-39.3-132.4-47.2-185.6-13.9-84.1 52.5-94.3 187.8-22.8 302.2 52.7 84.3 135.1 133.7 208.4 132.8 73.3.9 155.7-48.5 208.4-132.8 71.5-114.4 61.3-249.7-22.8-302.2M342.2 594c-15-24.1-26.1-49.5-33-75.5-6.5-24.5-8.9-48.5-7.1-71.2 3.2-42.3 20.4-75.2 48.4-92.7s65.2-18.6 104.5-2.9c5.9 2.4 11.8 5.1 17.6 8.1-21 19-40.3 41.9-56.7 68.1-43.4 69.5-56.6 146.7-41.5 208.4-11.8-12.8-22.6-27-32.2-42.3m372.6-75.6c-6.9 26.1-17.9 51.5-33 75.5-9.6 15.4-20.4 29.5-32.3 42.3 13.5-55.2 4.4-122.9-28.9-186.3-2.3-4.5-7.7-5.9-12-3.3l-103.5 64.7c-4 2.5-5.2 7.7-2.7 11.7l15.2 24.3c2.5 4 7.7 5.2 11.7 2.7l67.1-41.9c2.2 6.4 4.3 12.9 6 19.5 6.5 24.5 8.9 48.5 7.1 71.2-3.2 42.3-20.4 75.2-48.4 92.7-14 8.8-30.3 13.4-48 13.9h-2.2c-17.7-.5-34-5.1-48-13.9-28-17.5-45.2-50.4-48.4-92.7-1.7-22.7.7-46.7 7.1-71.2 6.8-26.1 17.9-51.5 33-75.5 15-24.1 33-45.2 53.4-62.8 19.2-16.6 39.7-29.2 60.9-37.6 39.4-15.7 76.5-14.6 104.5 2.9s45.2 50.4 48.4 92.7c1.8 22.6-.6 46.6-7 71.1" style="fill:#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<circle cx="512" cy="512" r="512" style="fill:#031b4e" />
<path
d="M668 415.5c0 16.6-69.9 30.2-156 30.2s-156-13.6-156-30.2v85c0 17.1 69.9 30.7 156 30.7s156-13.6 156-30.7v-85zM512 556.4c86.1 0 156-13.6 156-30.2v85.5c0 16.6-69.9 30.2-156 30.2s-156-13.6-156-30.2v-85.5c0 16.6 69.9 30.2 156 30.2zm156 80.5c0 16.6-69.9 30.2-156 30.2s-156-13.6-156-30.2v85.5c0 16.6 69.9 30.2 156 30.2s156-13.6 156-30.2v-85.5zm0-296.9L512 239.4 356 340v55.4c0 14.1 69.9 25.2 156 25.2s156-11.1 156-25.2V340z"
style="fill:#fff" />
</svg>

After

Width:  |  Height:  |  Size: 743 B

View File

@@ -0,0 +1,26 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 73 73" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" fill="#2562eb">
<g id="SVGRepo_bgCarrier" stroke-width="0" />
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" />
<g id="SVGRepo_iconCarrier">
<title>databases-and-servers/servers/ssh</title>
<desc>Created with Sketch.</desc>
<defs> </defs>
<g id="databases-and-servers/servers/ssh" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="container" transform="translate(2.000000, 2.000000)" fill="#FFFFFF" fill-rule="nonzero"
stroke="#2562eb" stroke-width="2">
<rect id="mask" x="-1" y="-1" width="71" height="71" rx="14"> </rect>
</g>
<g id="terminal" transform="translate(11.000000, 17.000000)" fill="#2562eb" fill-rule="nonzero">
<path
d="M49.2407485,0.0215730994 L1.65983626,0.0215730994 C0.743128655,0.0215730994 0,0.76480117 0,1.68140936 L0,9.94480117 L50.9005848,9.94480117 L50.9005848,1.68140936 C50.9005848,0.76480117 50.1574561,0.0215730994 49.2407485,0.0215730994 Z M32.0399298,7.19626901 C30.8196082,7.19626901 29.826848,6.20350877 29.826848,4.98318713 C29.826848,3.7628655 30.8196082,2.77010526 32.0399298,2.77010526 C33.2602515,2.77010526 34.2530117,3.7628655 34.2530117,4.98318713 C34.2530117,6.20350877 33.2602515,7.19626901 32.0399298,7.19626901 Z M38.0686667,7.19626901 C36.848345,7.19626901 35.8555848,6.20350877 35.8555848,4.98318713 C35.8555848,3.7628655 36.848345,2.77010526 38.0686667,2.77010526 C39.2889883,2.77010526 40.2817485,3.7628655 40.2817485,4.98318713 C40.2817485,6.20350877 39.2889883,7.19626901 38.0686667,7.19626901 Z M44.0974035,7.19626901 C42.8770819,7.19626901 41.8843216,6.20350877 41.8843216,4.98318713 C41.8843216,3.7628655 42.8770819,2.77010526 44.0974035,2.77010526 C45.3177251,2.77010526 46.3104854,3.7628655 46.3104854,4.98318713 C46.3104854,6.20350877 45.3177251,7.19626901 44.0974035,7.19626901 Z"
id="Shape"> </path>
<path
d="M0,13.2643743 L0,37.0905205 C0,38.0071287 0.743128655,38.7503567 1.65983626,38.7503567 L49.240848,38.7503567 C50.1574561,38.7503567 50.9006842,38.0072281 50.9006842,37.0905205 L50.9006842,13.2643743 L0,13.2643743 Z M23.4073099,27.2714795 L17.9739708,31.8946842 C17.2758772,32.4887895 16.2283392,32.404386 15.6342339,31.706193 C15.0401287,31.0080994 15.1245322,29.9605614 15.8227251,29.3664561 L19.7704035,26.0073158 L15.8227251,22.6481754 C15.1245322,22.0540702 15.0402281,21.0066316 15.6342339,20.3084386 C16.2282398,19.6101462 17.2758772,19.5259415 17.9739708,20.1199474 L23.4073099,24.743152 C24.185731,25.4055556 24.185731,26.6091754 23.4073099,27.2714795 Z M34.0021871,32.2903567 L27.4453567,32.2903567 C26.5287485,32.2903567 25.7855205,31.5472281 25.7855205,30.6305205 C25.7855205,29.7138129 26.5286491,28.9706842 27.4453567,28.9706842 L34.0021871,28.9706842 C34.9187953,28.9706842 35.6620234,29.7138129 35.6620234,30.6305205 C35.6620234,31.5472281 34.9187953,32.2903567 34.0021871,32.2903567 Z"
id="Shape"> </path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24"><path fill="#059669" d="M7 20.5q-1.864 0-3.182-1.318T2.5 16q0-1.246.608-2.268t1.617-1.615q.275-.165.525-.026q.25.14.25.459q0 .121-.064.24q-.065.12-.169.181q-.776.452-1.272 1.258T3.5 16q0 1.442 1.029 2.471T7 19.5q1.404 0 2.414-.961q1.009-.962 1.086-2.347q.02-.31.199-.5q.18-.192.47-.192h4.725q.142-.321.44-.526q.297-.205.666-.205q.51 0 .87.36q.36.361.36.871t-.36.87t-.87.36q-.388 0-.676-.194q-.288-.196-.43-.536h-4.417q-.235 1.84-1.54 2.92T7 20.5m0-3.27q-.51 0-.87-.36T5.77 16q0-.512.378-.892q.379-.381.994-.314l2.562-4.265q-.84-.656-1.272-1.574T8 7q0-1.864 1.318-3.182T12.5 2.5q1.73 0 2.991 1.14t1.455 2.802q.048.225-.108.392q-.155.166-.386.166q-.171 0-.315-.132t-.185-.309q-.16-1.296-1.136-2.177Q13.838 3.5 12.5 3.5q-1.442 0-2.471 1.029T9 7q0 .883.393 1.618t1.101 1.251q.235.162.303.423q.069.262-.068.485l-2.727 4.542q.108.144.168.321t.06.36q0 .51-.36.87t-.87.36m10 3.27q-.708 0-1.34-.201t-1.195-.591q-.329-.221-.268-.58q.06-.359.474-.359q.106 0 .208.05t.188.106q.402.271.903.423T17 19.5q1.442 0 2.471-1.029T20.5 16t-1.029-2.471T17 12.5q-.25 0-.475.038t-.45.093q-.285.086-.558-.017t-.41-.32l-2.459-4.088q-.525.073-.952-.279T11.27 7q0-.51.36-.87q.362-.36.871-.36t.87.36t.36.87q0 .183-.057.357t-.165.299l2.367 3.977q.258-.05.53-.092q.274-.041.595-.041q1.864 0 3.182 1.318T21.5 16t-1.318 3.182T17 20.5"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,73 @@
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
return;
}
root.classList.add(theme);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};

View File

@@ -0,0 +1,37 @@
import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTheme } from "./ThemeProvider";
export function ThemeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100 dark:text-white" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -14,7 +14,7 @@ import {
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { Access, accessFormType, AliyunConfig } from "@/domain/access";
import { Access, accessFormType, AliyunConfig, getUsageByConfigType } from "@/domain/access";
import { save } from "@/repository/access";
import { useConfig } from "@/providers/config";
@@ -59,6 +59,7 @@ const AccessAliyunForm = ({
id: data.id as string,
name: data.name,
configType: data.configType,
usage: getUsageByConfigType(data.configType),
config: {
accessKeyId: data.accessKeyId,
accessKeySecret: data.accessSecretId,

View File

@@ -0,0 +1,174 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
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, CloudflareConfig, getUsageByConfigType } from "@/domain/access";
import { save } from "@/repository/access";
import { useConfig } from "@/providers/config";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
const AccessCloudflareForm = ({
data,
onAfterReq,
}: {
data?: Access;
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
configType: accessFormType,
dnsApiToken: z.string().min(1).max(64),
});
let config: CloudflareConfig = {
dnsApiToken: "",
};
if (data) config = data.config as CloudflareConfig;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
configType: "cloudflare",
dnsApiToken: config.dnsApiToken,
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
console.log(data);
const req: Access = {
id: data.id as string,
name: data.name,
configType: data.configType,
usage: getUsageByConfigType(data.configType),
config: {
dnsApiToken: data.dnsApiToken,
},
};
try {
const rs = await save(req);
onAfterReq();
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id) {
updateAccess(req);
return;
}
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 (
<>
<div className="max-w-[35em] mx-auto mt-10">
<Form {...form}>
<form
onSubmit={(e) => {
console.log(e);
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dnsApiToken"
render={({ field }) => (
<FormItem>
<FormLabel>CLOUD_DNS_API_TOKEN</FormLabel>
<FormControl>
<Input placeholder="请输入CLOUD_DNS_API_TOKEN" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
</div>
</>
);
};
export default AccessCloudflareForm;

View File

@@ -5,6 +5,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useState } from "react";
@@ -15,9 +16,22 @@ import { Label } from "../ui/label";
import { Access, accessTypeMap } from "@/domain/access";
import AccessAliyunForm from "./AccessAliyunForm";
import { cn } from "@/lib/utils";
import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
import AccessSSHForm from "./AccessSSHForm";
import WebhookForm from "./AccessWebhookFrom";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "../ui/select";
import AccessCloudflareForm from "./AccessCloudflareForm";
import AccessQiniuForm from "./AccessQiniuForm";
import AccessNamesiloForm from "./AccessNamesiloForm";
import AccessGodaddyFrom from "./AccessGodaddyForm";
type TargetConfigEditProps = {
op: "add" | "edit";
@@ -69,6 +83,56 @@ export function AccessEdit({
/>
);
break;
case "webhook":
form = (
<WebhookForm
data={data}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
case "cloudflare":
form = (
<AccessCloudflareForm
data={data}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
case "qiniu":
form = (
<AccessQiniuForm
data={data}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
case "namesilo":
form = (
<AccessNamesiloForm
data={data}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
case "godaddy":
form = (
<AccessGodaddyFrom
data={data}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
}
const getOptionCls = (val: string) => {
@@ -80,37 +144,50 @@ export function AccessEdit({
<DialogTrigger asChild className={cn(className)}>
{trigger}
</DialogTrigger>
<DialogContent className="sm:max-w-[600px] w-full">
<DialogContent className="sm:max-w-[600px] w-full dark:text-stone-200">
<DialogHeader>
<DialogTitle>{op == "add" ? "添加" : "编辑"}</DialogTitle>
</DialogHeader>
<div className="container">
<Label></Label>
<RadioGroup
value={configType}
className="flex mt-3 space-x-2"
onValueChange={setConfigType}
>
{typeKeys.map((key) => (
<div className="flex items-center space-x-2" key={key}>
<RadioGroupItem value={key} id={key} hidden />
<Label htmlFor={key}>
<div
className={cn(
"flex items-center space-x-2 border p-2 rounded cursor-pointer",
getOptionCls(key)
)}
>
<img src={accessTypeMap.get(key)?.[1]} className="h-6" />
<div>{accessTypeMap.get(key)?.[0]}</div>
</div>
</Label>
</div>
))}
</RadioGroup>
<ScrollArea className="max-h-[80vh]">
<div className="container py-3">
<Label></Label>
{form}
</div>
<Select
onValueChange={(val) => {
console.log(val);
setConfigType(val);
}}
defaultValue={configType}
>
<SelectTrigger className="mt-3">
<SelectValue placeholder="请选择服务商" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
{typeKeys.map((key) => (
<SelectItem value={key} key={key}>
<div
className={cn(
"flex items-center space-x-2 rounded cursor-pointer",
getOptionCls(key)
)}
>
<img
src={accessTypeMap.get(key)?.[1]}
className="h-6 w-6"
/>
<div>{accessTypeMap.get(key)?.[0]}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
{form}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
);

View File

@@ -0,0 +1,198 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
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,
getUsageByConfigType,
GodaddyConfig,
} from "@/domain/access";
import { save } from "@/repository/access";
import { useConfig } from "@/providers/config";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
const AccessGodaddyFrom = ({
data,
onAfterReq,
}: {
data?: Access;
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
configType: accessFormType,
apiKey: z.string().min(1).max(64),
apiSecret: z.string().min(1).max(64),
});
let config: GodaddyConfig = {
apiKey: "",
apiSecret: "",
};
if (data) config = data.config as GodaddyConfig;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
configType: "godaddy",
apiKey: config.apiKey,
apiSecret: config.apiSecret,
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
console.log(data);
const req: Access = {
id: data.id as string,
name: data.name,
configType: data.configType,
usage: getUsageByConfigType(data.configType),
config: {
apiKey: data.apiKey,
apiSecret: data.apiSecret,
},
};
try {
const rs = await save(req);
onAfterReq();
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id) {
updateAccess(req);
return;
}
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 (
<>
<div className="max-w-[35em] mx-auto mt-10">
<Form {...form}>
<form
onSubmit={(e) => {
console.log(e);
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="apiKey"
render={({ field }) => (
<FormItem>
<FormLabel>GODADDY_API_KEY</FormLabel>
<FormControl>
<Input placeholder="请输入GODADDY_API_KEY" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="apiSecret"
render={({ field }) => (
<FormItem>
<FormLabel>GODADDY_API_SECRET</FormLabel>
<FormControl>
<Input placeholder="请输入GODADDY_API_SECRET" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
</div>
</>
);
};
export default AccessGodaddyFrom;

View File

@@ -0,0 +1,174 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
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, getUsageByConfigType, NamesiloConfig } from "@/domain/access";
import { save } from "@/repository/access";
import { useConfig } from "@/providers/config";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
const AccessNamesiloForm = ({
data,
onAfterReq,
}: {
data?: Access;
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
configType: accessFormType,
apiKey: z.string().min(1).max(64),
});
let config: NamesiloConfig = {
apiKey: "",
};
if (data) config = data.config as NamesiloConfig;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
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,
configType: data.configType,
usage: getUsageByConfigType(data.configType),
config: {
apiKey: data.apiKey,
},
};
try {
const rs = await save(req);
onAfterReq();
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id) {
updateAccess(req);
return;
}
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 (
<>
<div className="max-w-[35em] mx-auto mt-10">
<Form {...form}>
<form
onSubmit={(e) => {
console.log(e);
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="apiKey"
render={({ field }) => (
<FormItem>
<FormLabel>NAMESILO_API_KEY</FormLabel>
<FormControl>
<Input placeholder="请输入NAMESILO_API_KEY" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
</div>
</>
);
};
export default AccessNamesiloForm;

View File

@@ -0,0 +1,196 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
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, getUsageByConfigType, QiniuConfig } from "@/domain/access";
import { save } from "@/repository/access";
import { useConfig } from "@/providers/config";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
const AccessQiniuForm = ({
data,
onAfterReq,
}: {
data?: Access;
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
configType: accessFormType,
accessKey: z.string().min(1).max(64),
secretKey: z.string().min(1).max(64),
});
let config: QiniuConfig = {
accessKey: "",
secretKey: "",
};
if (data) config = data.config as QiniuConfig;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
configType: "qiniu",
accessKey: config.accessKey,
secretKey: config.secretKey,
},
});
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: {
accessKey: data.accessKey,
secretKey: data.secretKey,
},
};
try {
const rs = await save(req);
onAfterReq();
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id) {
updateAccess(req);
return;
}
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></FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accessKey"
render={({ field }) => (
<FormItem>
<FormLabel>AccessKey</FormLabel>
<FormControl>
<Input placeholder="请输入AccessKey" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="secretKey"
render={({ field }) => (
<FormItem>
<FormLabel>SecretKey</FormLabel>
<FormControl>
<Input placeholder="请输入SecretKey" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
</div>
</>
);
};
export default AccessQiniuForm;

View File

@@ -1,4 +1,4 @@
import { Access, accessFormType, SSHConfig } from "@/domain/access";
import { Access, accessFormType, getUsageByConfigType, SSHConfig } from "@/domain/access";
import { useConfig } from "@/providers/config";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
@@ -18,6 +18,7 @@ import { save } from "@/repository/access";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
import { readFileContent } from "@/lib/file";
import { useRef, useState } from "react";
const AccessSSHForm = ({
data,
@@ -27,6 +28,11 @@ const AccessSSHForm = ({
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [fileName, setFileName] = useState("");
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
@@ -38,7 +44,7 @@ const AccessSSHForm = ({
username: z.string().min(1).max(64),
password: z.string().min(0).max(64),
key: z.string().min(0).max(20480),
keyFile: z.string().optional(),
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),
@@ -81,6 +87,7 @@ const AccessSSHForm = ({
id: data.id as string,
name: data.name,
configType: data.configType,
usage: getUsageByConfigType(data.configType),
config: {
host: data.host,
port: data.port,
@@ -127,9 +134,16 @@ const AccessSSHForm = ({
) => {
const file = event.target.files?.[0];
if (!file) return;
const content = await readFileContent(file);
const savedFile = file;
setFileName(savedFile.name);
const content = await readFileContent(savedFile);
form.setValue("key", content);
form.setValue("keyFile", "");
};
const handleSelectFileClick = () => {
console.log(fileInputRef.current);
fileInputRef.current?.click();
};
return (
@@ -245,7 +259,11 @@ const AccessSSHForm = ({
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入密码" {...field} />
<Input
placeholder="请输入密码"
{...field}
type="password"
/>
</FormControl>
<FormMessage />
@@ -275,12 +293,26 @@ const AccessSSHForm = ({
<FormItem>
<FormLabel>Key使</FormLabel>
<FormControl>
<Input
placeholder="请输入Key"
{...field}
type="file"
onChange={handleFileChange}
/>
<div>
<Button
type={"button"}
variant={"secondary"}
size={"sm"}
className="w-48"
onClick={handleSelectFileClick}
>
{fileName ? fileName : "请选择文件"}
</Button>
<Input
placeholder="请输入Key"
{...field}
ref={fileInputRef}
className="hidden"
hidden
type="file"
onChange={handleFileChange}
/>
</div>
</FormControl>
<FormMessage />

View File

@@ -14,7 +14,7 @@ import {
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { Access, accessFormType, 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";
@@ -58,6 +58,7 @@ const AccessTencentForm = ({
id: data.id as string,
name: data.name,
configType: data.configType,
usage: getUsageByConfigType(data.configType),
config: {
secretId: data.secretId,
secretKey: data.secretKey,

View File

@@ -0,0 +1,174 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
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, getUsageByConfigType, WebhookConfig } from "@/domain/access";
import { save } from "@/repository/access";
import { useConfig } from "@/providers/config";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
const WebhookForm = ({
data,
onAfterReq,
}: {
data?: Access;
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
configType: accessFormType,
url: z.string().url(),
});
let config: WebhookConfig = {
url: "",
};
if (data) config = data.config as WebhookConfig;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
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,
configType: data.configType,
usage: getUsageByConfigType(data.configType),
config: {
url: data.url,
},
};
try {
const rs = await save(req);
onAfterReq();
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id) {
updateAccess(req);
return;
}
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 (
<>
<div className="max-w-[35em] mx-auto mt-10">
<Form {...form}>
<form
onSubmit={(e) => {
console.log(e);
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook Url</FormLabel>
<FormControl>
<Input placeholder="请输入Webhook Url" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
</div>
</>
);
};
export default WebhookForm;

View File

@@ -0,0 +1,141 @@
import { cn } from "@/lib/utils";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "../ui/form";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { useConfig } from "@/providers/config";
import { update } from "@/repository/settings";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
import { useState } from "react";
type EmailsEditProps = {
className?: string;
trigger: React.ReactNode;
};
const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
const {
config: { emails },
setEmails,
} = useConfig();
const [open, setOpen] = useState(false);
const formSchema = z.object({
email: z.string().email(),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
if (emails.content.emails.includes(data.email)) {
form.setError("email", {
message: "邮箱已存在",
});
return;
}
// 保存到 config
const newEmails = [...emails.content.emails, data.email];
try {
const resp = await update({
...emails,
name: "emails",
content: {
emails: newEmails,
},
});
// 更新本地状态
setEmails(resp);
// 关闭弹窗
form.reset();
form.clearErrors();
setOpen(false);
} 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 (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild className={cn(className)}>
{trigger}
</DialogTrigger>
<DialogContent className="sm:max-w-[600px] w-full dark:text-stone-200">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="container py-3">
<Form {...form}>
<form
onSubmit={(e) => {
console.log(e);
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入邮箱" {...field} type="email" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
</div>
</DialogContent>
</Dialog>
);
};
export default EmailsEdit;

View File

@@ -0,0 +1,102 @@
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
} from "../ui/pagination";
type PaginationProps = {
totalPages: number;
currentPage: number;
onPageChange: (page: number) => void;
};
type PageNumber = number | string;
const XPagination = ({
totalPages,
currentPage,
onPageChange,
}: PaginationProps) => {
const pageNeighbours = 1; // Number of page numbers to show on either side of the current page
const getPageNumbers = () => {
const totalNumbers = pageNeighbours * 2 + 3; // total pages to display (left + right neighbours + current + 2 for start and end)
const totalBlocks = totalNumbers + 2; // adding 2 for the start and end page numbers
if (totalPages > totalBlocks) {
let pages: PageNumber[] = [];
const leftBound = Math.max(2, currentPage - pageNeighbours);
const rightBound = Math.min(totalPages - 1, currentPage + pageNeighbours);
const beforeLastPage = totalPages - 1;
pages = range(leftBound, rightBound);
if (currentPage > pageNeighbours + 2) {
pages.unshift("...");
}
if (currentPage < beforeLastPage - pageNeighbours) {
pages.push("...");
}
pages.unshift(1);
pages.push(totalPages);
return pages;
}
return range(1, totalPages);
};
const range = (from: number, to: number, step = 1) => {
let i = from;
const range = [];
while (i <= to) {
range.push(i);
i += step;
}
return range;
};
const pages = getPageNumbers();
return (
<>
<Pagination className="dark:text-stone-200 justify-end mt-3">
<PaginationContent>
{pages.map((page, index) => {
if (page === "...") {
return (
<PaginationItem key={index}>
<PaginationEllipsis />
</PaginationItem>
);
}
return (
<PaginationItem key={index}>
<PaginationLink
href="#"
isActive={currentPage == page}
onClick={(e) => {
e.preventDefault();
onPageChange(page as number);
}}
>
{page}
</PaginationLink>
</PaginationItem>
);
})}
</PaginationContent>
</Pagination>
</>
);
};
export default XPagination;

View File

@@ -0,0 +1,117 @@
import * as React from "react";
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
import { ButtonProps, buttonVariants } from "@/components/ui/button";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
Pagination.displayName = "Pagination";
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
));
PaginationContent.displayName = "PaginationContent";
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
));
PaginationItem.displayName = "PaginationItem";
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">;
const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
);
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>
);
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>
);
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>
);
PaginationEllipsis.displayName = "PaginationEllipsis";
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};

View File

@@ -0,0 +1,46 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@@ -1,17 +1,17 @@
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
@@ -25,8 +25,8 @@ const SheetOverlay = React.forwardRef<
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
@@ -45,7 +45,7 @@ const sheetVariants = cva(
side: "right",
},
}
)
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
@@ -64,13 +64,13 @@ const SheetContent = React.forwardRef<
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<X className="h-4 w-4 dark:text-stone-200" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
@@ -83,8 +83,8 @@ const SheetHeader = ({
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({
className,
@@ -97,8 +97,8 @@ const SheetFooter = ({
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
@@ -109,8 +109,8 @@ const SheetTitle = React.forwardRef<
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
@@ -121,8 +121,8 @@ const SheetDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
@@ -135,4 +135,4 @@ export {
SheetFooter,
SheetTitle,
SheetDescription,
}
};

View File

@@ -3,24 +3,63 @@ import { z } from "zod";
export const accessTypeMap: Map<string, [string, string]> = new Map([
["tencent", ["腾讯云", "/imgs/providers/tencent.svg"]],
["aliyun", ["阿里云", "/imgs/providers/aliyun.svg"]],
["ssh", ["SSH部署", "/imgs/providers/ssh.png"]],
["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"]],
]);
export const accessFormType = z.union(
[z.literal("aliyun"), z.literal("tencent"), z.literal("ssh")],
[
z.literal("aliyun"),
z.literal("tencent"),
z.literal("ssh"),
z.literal("webhook"),
z.literal("cloudflare"),
z.literal("qiniu"),
z.literal("namesilo"),
z.literal("godaddy"),
],
{ message: "请选择云服务商" }
);
type AccessUsage = "apply" | "deploy" | "all";
export type Access = {
id: string;
name: string;
configType: string;
config: TencentConfig | AliyunConfig | SSHConfig;
usage: AccessUsage;
config:
| TencentConfig
| AliyunConfig
| SSHConfig
| WebhookConfig
| CloudflareConfig
| QiniuConfig
| NamesiloConfig
| GodaddyConfig;
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;
@@ -31,6 +70,14 @@ export type AliyunConfig = {
accessKeySecret: string;
};
export type NamesiloConfig = {
apiKey: string;
};
export type GodaddyConfig = {
apiKey: string;
apiSecret: string;
};
export type SSHConfig = {
host: string;
port: string;
@@ -42,3 +89,22 @@ export type SSHConfig = {
certPath: string;
keyPath: string;
};
export const getUsageByConfigType = (configType: string): AccessUsage => {
switch (configType) {
case "aliyun":
case "tencent":
return "all";
case "ssh":
case "webhook":
case "qiniu":
return "deploy";
case "cloudflare":
case "namesilo":
case "godaddy":
return "apply";
default:
return "all";
}
};

View File

@@ -24,6 +24,7 @@ export type Log = {
time: string;
message: string;
error: string;
info?: string[];
};
export type DeploymentListReq = {

View File

@@ -3,6 +3,7 @@ import { Deployment, Pahse } from "./deployment";
export type Domain = {
id: string;
domain: string;
email?: string;
crontab: string;
access: string;
targetAccess: string;
@@ -16,11 +17,20 @@ export type Domain = {
updated?: string;
deleted?: string;
rightnow?: boolean;
certificate?: string;
privateKey?: string;
expand?: {
lastDeployment?: Deployment;
};
};
export type Statistic = {
total: number;
expired: number;
enabled: number;
disabled: number;
};
export const getLastDeployment = (domain: Domain): Deployment | undefined => {
return domain.expand?.lastDeployment;
};
@@ -28,7 +38,10 @@ 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"]],
["ssh", ["SSH部署", "/imgs/providers/ssh.png"]],
["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"]],
]);
export const targetTypeKeys = Array.from(targetTypeMap.keys());

View File

@@ -0,0 +1,9 @@
export type Setting = {
id?: string;
name?: string;
content: EmailsSetting;
};
type EmailsSetting = {
emails: string[];
};

View File

@@ -1,3 +1,5 @@
import JSZip from "jszip";
export function readFileContent(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
@@ -15,3 +17,24 @@ export function readFileContent(file: File): Promise<string> {
reader.readAsText(file);
});
}
export type CustomFile = {
name: string;
content: string;
};
export const saveFiles2ZIP = async (zipName: string, files: CustomFile[]) => {
const zip = new JSZip();
files.forEach((file) => {
zip.file(file.name, file.content);
});
const content = await zip.generateAsync({ type: "blob" });
// Save the zip file to the local system
const link = document.createElement("a");
link.href = URL.createObjectURL(content);
link.download = zipName;
link.click();
};

View File

@@ -20,3 +20,49 @@ export const getDate = (zuluTime: string) => {
const time = convertZulu2Beijing(zuluTime);
return time.split(" ")[0];
};
export function getTimeBefore(days: number): string {
// 获取当前时间
const currentDate = new Date();
// 减去指定的天数
currentDate.setUTCDate(currentDate.getUTCDate() - days);
// 格式化日期为 yyyy-mm-dd
const year = currentDate.getUTCFullYear();
const month = String(currentDate.getUTCMonth() + 1).padStart(2, "0"); // 月份从 0 开始
const day = String(currentDate.getUTCDate()).padStart(2, "0");
// 格式化时间为 hh:ii:ss
const hours = String(currentDate.getUTCHours()).padStart(2, "0");
const minutes = String(currentDate.getUTCMinutes()).padStart(2, "0");
const seconds = String(currentDate.getUTCSeconds()).padStart(2, "0");
// 组合成 yyyy-mm-dd hh:ii:ss 格式
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
return formattedDate;
}
export function getTimeAfter(days: number): string {
// 获取当前时间
const currentDate = new Date();
// 加上指定的天数
currentDate.setUTCDate(currentDate.getUTCDate() + days);
// 格式化日期为 yyyy-mm-dd
const year = currentDate.getUTCFullYear();
const month = String(currentDate.getUTCMonth() + 1).padStart(2, "0"); // 月份从 0 开始
const day = String(currentDate.getUTCDate()).padStart(2, "0");
// 格式化时间为 hh:ii:ss
const hours = String(currentDate.getUTCHours()).padStart(2, "0");
const minutes = String(currentDate.getUTCMinutes()).padStart(2, "0");
const seconds = String(currentDate.getUTCSeconds()).padStart(2, "0");
// 组合成 yyyy-mm-dd hh:ii:ss 格式
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
return formattedDate;
}

View File

@@ -3,9 +3,12 @@ import ReactDOM from "react-dom/client";
import "./global.css";
import { RouterProvider } from "react-router-dom";
import { router } from "./router.tsx";
import { ThemeProvider } from "./components/ThemeProvider.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<RouterProvider router={router} />
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<RouterProvider router={router} />
</ThemeProvider>
</React.StrictMode>
);

View File

@@ -5,7 +5,15 @@ import {
useLocation,
useNavigate,
} from "react-router-dom";
import { CircleUser, Earth, History, Menu, Server } from "lucide-react";
import {
BookOpen,
CircleUser,
Earth,
History,
Home,
Menu,
Server,
} from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -21,6 +29,8 @@ import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
import { ConfigProvider } from "@/providers/config";
import { getPb } from "@/repository/api";
import { ThemeToggle } from "@/components/ThemeToggle";
import { Separator } from "@/components/ui/separator";
export default function Dashboard() {
const navigate = useNavigate();
@@ -41,16 +51,20 @@ export default function Dashboard() {
getPb().authStore.clear();
navigate("/login");
};
const handleSettingClick = () => {
navigate("/setting/password");
};
return (
<>
<ConfigProvider>
<div className="grid min-h-screen w-full md:grid-cols-[220px_1fr] lg:grid-cols-[280px_1fr]">
<div className="hidden border-r bg-muted/40 md:block">
<div className="hidden border-r dark:border-stone-500 bg-muted/40 md:block">
<div className="flex h-full max-h-screen flex-col gap-2">
<div className="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6">
<div className="flex h-14 items-center border-b dark:border-stone-500 px-4 lg:h-[60px] lg:px-6">
<Link to="/" className="flex items-center gap-2 font-semibold">
<img src="/vite.svg" className="w-[36px] h-[36px]" />
<span className="">Certimate</span>
<span className="dark:text-white">Certimate</span>
</Link>
</div>
<div className="flex-1">
@@ -61,6 +75,16 @@ export default function Dashboard() {
"flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary",
getClass("/")
)}
>
<Home className="h-4 w-4" />
</Link>
<Link
to="/domains"
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary",
getClass("/domains")
)}
>
<Earth className="h-4 w-4" />
@@ -91,7 +115,7 @@ export default function Dashboard() {
</div>
</div>
<div className="flex flex-col">
<header className="flex h-14 items-center gap-4 border-b bg-muted/40 px-4 lg:h-[60px] lg:px-6">
<header className="flex h-14 items-center gap-4 border-b dark:border-stone-500 bg-muted/40 px-4 lg:h-[60px] lg:px-6">
<Sheet>
<SheetTrigger asChild>
<Button
@@ -99,7 +123,7 @@ export default function Dashboard() {
size="icon"
className="shrink-0 md:hidden"
>
<Menu className="h-5 w-5" />
<Menu className="h-5 w-5 dark:text-white" />
<span className="sr-only">Toggle navigation menu</span>
</Button>
</SheetTrigger>
@@ -110,7 +134,7 @@ export default function Dashboard() {
className="flex items-center gap-2 text-lg font-semibold"
>
<img src="/vite.svg" className="w-[36px] h-[36px]" />
<span className="">Certimate</span>
<span className="dark:text-white">Certimate</span>
<span className="sr-only">Certimate</span>
</Link>
<Link
@@ -119,6 +143,16 @@ export default function Dashboard() {
"mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground",
getClass("/")
)}
>
<Home className="h-5 w-5" />
</Link>
<Link
to="/domains"
className={cn(
"mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground",
getClass("/domains")
)}
>
<Earth className="h-5 w-5" />
@@ -148,6 +182,7 @@ export default function Dashboard() {
</SheetContent>
</Sheet>
<div className="w-full flex-1"></div>
<ThemeToggle />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@@ -160,17 +195,42 @@ export default function Dashboard() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuLabel></DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleSettingClick}>
</DropdownMenuItem>
<DropdownMenuItem onClick={handleLogoutClick}>
Logout
退
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</header>
<main className="flex flex-1 flex-col gap-4 p-4 lg:gap-6 lg:p-6">
<main className="flex flex-1 flex-col gap-4 p-4 lg:gap-6 lg:p-6 relative">
<Outlet />
<div className="fixed right-0 bottom-0 w-full flex justify-between p-5">
<div className=""></div>
<div className="text-muted-foreground text-sm hover:text-stone-900 dark:hover:text-stone-200 flex">
<a
href="https://docs.certimate.me"
target="_blank"
className="flex items-center"
>
<BookOpen size={16} />
<div className="ml-1"></div>
</a>
<Separator orientation="vertical" className="mx-2" />
<a
href="https://github.com/usual2970/certimate/releases"
target="_blank"
>
Certimate v0.1.4
</a>
</div>
</div>
</main>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { Toaster } from "@/components/ui/toaster";
import { Outlet } from "react-router-dom";
const SettingLayout = () => {
return (
<div>
<Toaster />
<div className="text-muted-foreground border-b dark:border-stone-500 py-5">
</div>
<div className="w-full sm:w-[35em] mt-10 flex flex-col p-3 mx-auto">
{/* <div className="text-muted-foreground">
<span className="transition-all text-sm bg-gray-400 px-3 py-1 rounded-sm text-white cursor-pointer">
密码
</span>
</div> */}
<Outlet />
</div>
</div>
);
};
export default SettingLayout;

View File

@@ -1,4 +1,5 @@
import { AccessEdit } from "@/components/certimate/AccessEdit";
import XPagination from "@/components/certimate/XPagination";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Access as AccessType, accessTypeMap } from "@/domain/access";
@@ -6,11 +7,25 @@ import { convertZulu2Beijing } from "@/lib/time";
import { useConfig } from "@/providers/config";
import { remove } from "@/repository/access";
import { Key } from "lucide-react";
import { useLocation, useNavigate } from "react-router-dom";
const Access = () => {
const { config, deleteAccess } = useConfig();
const { accesses } = config;
const perPage = 10;
const totalPages = Math.ceil(accesses.length / perPage);
const navigate = useNavigate();
const location = useLocation();
const query = new URLSearchParams(location.search);
const page = query.get("page");
const pageNumber = page ? Number(page) : 1;
const startIndex = (pageNumber - 1) * perPage;
const endIndex = startIndex + perPage;
const handleDelete = async (data: AccessType) => {
const rs = await remove(data);
deleteAccess(rs.id);
@@ -39,7 +54,7 @@ const Access = () => {
</div>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b sm:p-2 mt-5">
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48"></div>
<div className="w-48"></div>
@@ -50,9 +65,9 @@ const Access = () => {
<div className="sm:hidden flex text-sm text-muted-foreground">
</div>
{accesses.map((access) => (
{accesses.slice(startIndex, endIndex).map((access) => (
<div
className="flex flex-col sm:flex-row text-secondary-foreground border-b sm:p-2 hover:bg-muted/50 text-sm"
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
key={access.id}
>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center">
@@ -95,6 +110,14 @@ const Access = () => {
</div>
</div>
))}
<XPagination
totalPages={totalPages}
currentPage={pageNumber}
onPageChange={(page) => {
query.set("page", page.toString());
navigate({ search: query.toString() });
}}
/>
</>
)}
</div>

View File

@@ -0,0 +1,317 @@
import DeployProgress from "@/components/certimate/DeployProgress";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Deployment, DeploymentListReq, Log } from "@/domain/deployment";
import { Statistic } from "@/domain/domain";
import { convertZulu2Beijing } from "@/lib/time";
import { list } from "@/repository/deployment";
import { statistics } from "@/repository/domains";
import {
Ban,
CalendarX2,
CircleCheck,
CircleX,
LoaderPinwheel,
Smile,
SquareSigma,
} from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const Dashboard = () => {
const [statistic, setStatistic] = useState<Statistic>();
const [deployments, setDeployments] = useState<Deployment[]>();
const navigate = useNavigate();
useEffect(() => {
const fetchStatistic = async () => {
const data = await statistics();
setStatistic(data);
};
fetchStatistic();
}, []);
useEffect(() => {
const fetchData = async () => {
const param: DeploymentListReq = {
perPage: 8,
};
const data = await list(param);
setDeployments(data.items);
};
fetchData();
}, []);
return (
<div className="flex flex-col">
<div className="flex justify-between items-center">
<div className="text-muted-foreground"></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">
<div className="p-3">
<SquareSigma size={48} strokeWidth={1} className="text-blue-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold"></div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.total ? (
<Link to="/domains" className="hover:underline">
{statistic?.total}
</Link>
) : (
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div>
</div>
</div>
</div>
<div className="w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg border">
<div className="p-3">
<CalendarX2 size={48} strokeWidth={1} className="text-red-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold"></div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.expired ? (
<Link to="/domains?state=expired" className="hover:underline">
{statistic?.expired}
</Link>
) : (
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div>
</div>
</div>
</div>
<div className="border w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg">
<div className="p-3">
<LoaderPinwheel
size={48}
strokeWidth={1}
className="text-green-400"
/>
</div>
<div>
<div className="text-muted-foreground font-semibold"></div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.enabled ? (
<Link to="/domains?state=enabled" className="hover:underline">
{statistic?.enabled}
</Link>
) : (
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div>
</div>
</div>
</div>
<div className="border w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg">
<div className="p-3">
<Ban size={48} strokeWidth={1} className="text-gray-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold"></div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.disabled ? (
<Link
to="/domains?state=disabled"
className="hover:underline"
>
{statistic?.disabled}
</Link>
) : (
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div>
</div>
</div>
</div>
</div>
<div>
<div className="text-muted-foreground mt-5 text-sm"></div>
{deployments?.length == 0 ? (
<>
<Alert className="max-w-[40em] mt-10">
<AlertTitle></AlertTitle>
<AlertDescription>
<div className="flex items-center mt-5">
<div>
<Smile className="text-yellow-400" size={36} />
</div>
<div className="ml-2">
{" "}
</div>
</div>
<div className="mt-2 flex justify-end">
<Button
onClick={() => {
navigate("/edit");
}}
>
</Button>
</div>
</AlertDescription>
</Alert>
</>
) : (
<>
<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-24"></div>
<div className="w-56"></div>
<div className="w-56 sm:ml-2 text-center"></div>
<div className="grow"></div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
</div>
{deployments?.map((deployment) => (
<div
key={deployment.id}
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}
</div>
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
{deployment.phase === "deploy" && deployment.phaseSuccess ? (
<CircleCheck size={16} className="text-green-700" />
) : (
<CircleX size={16} className="text-red-700" />
)}
</div>
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center">
<DeployProgress
phase={deployment.phase}
phaseSuccess={deployment.phaseSuccess}
/>
</div>
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center sm:justify-center">
{convertZulu2Beijing(deployment.deployedAt)}
</div>
<div className="flex items-center grow justify-start pt-1 sm:pt-0 sm:ml-2">
<Sheet>
<SheetTrigger asChild>
<Button variant={"link"} className="p-0">
</Button>
</SheetTrigger>
<SheetContent className="sm:max-w-5xl">
<SheetHeader>
<SheetTitle>
{deployment.expand.domain?.domain}-{deployment.id}
</SheetTitle>
</SheetHeader>
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">
{deployment.log.check && (
<>
{deployment.log.check.map((item: Log) => {
return (
<div className="flex flex-col mt-2">
<div className="flex">
<div>[{item.time}]</div>
<div className="ml-2">{item.message}</div>
</div>
{item.error && (
<div className="mt-1 text-red-600">
{item.error}
</div>
)}
</div>
);
})}
</>
)}
{deployment.log.apply && (
<>
{deployment.log.apply.map((item: Log) => {
return (
<div className="flex flex-col mt-2">
<div className="flex">
<div>[{item.time}]</div>
<div className="ml-2">{item.message}</div>
</div>
{item.info &&
item.info.map((info: string) => {
return (
<div className="mt-1 text-green-600">
{info}
</div>
);
})}
{item.error && (
<div className="mt-1 text-red-600">
{item.error}
</div>
)}
</div>
);
})}
</>
)}
{deployment.log.deploy && (
<>
{deployment.log.deploy.map((item: Log) => {
return (
<div className="flex flex-col mt-2">
<div className="flex">
<div>[{item.time}]</div>
<div className="ml-2">{item.message}</div>
</div>
{item.error && (
<div className="mt-1 text-red-600">
{item.error}
</div>
)}
</div>
);
})}
</>
)}
</div>
</SheetContent>
</Sheet>
</div>
</div>
))}
</>
)}
</div>
</div>
);
};
export default Dashboard;

View File

@@ -34,13 +34,11 @@ import { useLocation, useNavigate } from "react-router-dom";
import { Plus } from "lucide-react";
import { AccessEdit } from "@/components/certimate/AccessEdit";
import { accessTypeMap } from "@/domain/access";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import EmailsEdit from "@/components/certimate/EmailsEdit";
const Edit = () => {
const {
config: { accesses },
config: { accesses, emails },
} = useConfig();
const [domain, setDomain] = useState<Domain>();
@@ -62,11 +60,10 @@ const Edit = () => {
const formSchema = z.object({
id: z.string().optional(),
domain: z
.string()
.regex(/^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z]{2,6})+$/, {
message: "请输入正确的域名",
}),
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: "请输入正确的域名",
}),
email: z.string().email().optional(),
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
message: "请选择DNS服务商授权配置",
}),
@@ -83,6 +80,7 @@ const Edit = () => {
defaultValues: {
id: "",
domain: "",
email: "",
access: "",
targetAccess: "",
targetType: "",
@@ -94,6 +92,7 @@ const Edit = () => {
form.reset({
id: domain.id,
domain: domain.domain,
email: domain.email,
access: domain.access,
targetAccess: domain.targetAccess,
targetType: domain.targetType,
@@ -104,6 +103,10 @@ const Edit = () => {
const [targetType, setTargetType] = useState(domain ? domain.targetType : "");
const targetAccesses = accesses.filter((item) => {
if (item.usage == "apply") {
return false;
}
if (targetType == "") {
return true;
}
@@ -120,6 +123,7 @@ const Edit = () => {
id: data.id as string,
crontab: "0 0 * * *",
domain: data.domain,
email: data.email,
access: data.access,
targetAccess: data.targetAccess,
targetType: data.targetType,
@@ -136,7 +140,7 @@ const Edit = () => {
title: "成功",
description,
});
navigate("/");
navigate("/domains");
} catch (e) {
const err = e as ClientResponseError;
@@ -153,20 +157,19 @@ const Edit = () => {
}
};
const getOptionCls = (val: string) => {
return form.getValues().targetType == val ? "border-primary" : "";
};
return (
<>
<div className="">
<Toaster />
<div className="border-b h-10 text-muted-foreground">
<div className="border-b dark:border-stone-500 h-10 text-muted-foreground">
{domain?.id ? "编辑" : "新增"}
</div>
<div className="max-w-[35em] mx-auto mt-10">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 dark:text-stone-200"
>
<FormField
control={form.control}
name="domain"
@@ -182,6 +185,51 @@ const Edit = () => {
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<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.emails.map((item) => (
<SelectItem key={item} value={item}>
<div>{item}</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="access"
@@ -213,19 +261,21 @@ const Edit = () => {
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
{accesses.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>
))}
{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>
@@ -243,37 +293,33 @@ const Edit = () => {
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<RadioGroup
className="flex mt-3 space-x-2"
onValueChange={(val: string) => {
setTargetType(val);
form.setValue("targetType", val);
}}
<Select
{...field}
onValueChange={(value) => {
setTargetType(value);
form.setValue("targetType", value);
}}
>
{targetTypeKeys.map((key) => (
<div
className="flex items-center space-x-2"
key={key}
>
<Label>
<RadioGroupItem value={key} id={key} hidden />
<div
className={cn(
"flex items-center space-x-2 border p-2 rounded cursor-pointer",
getOptionCls(key)
)}
>
<img
src={targetTypeMap.get(key)?.[1]}
className="h-6"
/>
<div>{targetTypeMap.get(key)?.[0]}</div>
</div>
</Label>
</div>
))}
</RadioGroup>
<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 />

View File

@@ -1,4 +1,5 @@
import DeployProgress from "@/components/certimate/DeployProgress";
import XPagination from "@/components/certimate/XPagination";
import Show from "@/components/Show";
import {
AlertDialogAction,
@@ -19,22 +20,43 @@ import { Toaster } from "@/components/ui/toaster";
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
import { useToast } from "@/components/ui/use-toast";
import { Domain } from "@/domain/domain";
import { CustomFile, saveFiles2ZIP } from "@/lib/file";
import { convertZulu2Beijing, getDate } from "@/lib/time";
import { list, remove, save } from "@/repository/domains";
import {
list,
remove,
save,
subscribeId,
unsubscribeId,
} from "@/repository/domains";
import { TooltipContent, TooltipProvider } from "@radix-ui/react-tooltip";
import { CircleCheck, CircleX, Earth } from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Link, useLocation, useNavigate } from "react-router-dom";
const Home = () => {
const toast = useToast();
const navigate = useNavigate();
const location = useLocation();
const query = new URLSearchParams(location.search);
const page = query.get("page");
const state = query.get("state");
const [totalPage, setTotalPage] = useState(0);
const handleCreateClick = () => {
navigate("/edit");
};
const setPage = (newPage: number) => {
query.set("page", newPage.toString());
navigate(`?${query.toString()}`);
};
const handleEditClick = (id: string) => {
navigate(`/edit?id=${id}`);
};
@@ -56,11 +78,17 @@ const Home = () => {
useEffect(() => {
const fetchData = async () => {
const data = await list();
setDomains(data);
const data = await list({
page: page ? Number(page) : 1,
perPage: 10,
state: state ? state : "",
});
setDomains(data.items);
setTotalPage(data.totalPages);
};
fetchData();
}, []);
}, [page, state]);
const handelCheckedChange = async (id: string) => {
const checkedDomains = domains.filter((domain) => domain.id === id);
@@ -82,22 +110,25 @@ const Home = () => {
const handleRightNowClick = async (domain: Domain) => {
try {
unsubscribeId(domain.id);
subscribeId(domain.id, (resp) => {
console.log(resp);
const updatedDomains = domains.map((domain) => {
if (domain.id === resp.id) {
return { ...resp };
}
return domain;
});
setDomains(updatedDomains);
});
domain.rightnow = true;
const resp = await save(domain);
const updatedDomains = domains.map((domain) => {
if (domain.id === resp.id) {
return { ...resp };
}
return domain;
});
setDomains(updatedDomains);
await save(domain);
toast.toast({
title: "执行成功",
description: "执行成功",
title: "操作成功",
description: "已发起部署,请稍后查看部署日志。",
});
setTimeout(() => {
navigate(0);
}, 1000);
} catch (e) {
toast.toast({
title: "执行失败",
@@ -118,6 +149,22 @@ const Home = () => {
}
};
const handleDownloadClick = async (domain: Domain) => {
const zipName = `${domain.id}-${domain.domain}.zip`;
const files: CustomFile[] = [
{
name: `${domain.domain}.pem`,
content: domain.certificate ? domain.certificate : "",
},
{
name: `${domain.domain}.key`,
content: domain.privateKey ? domain.privateKey : "",
},
];
await saveFiles2ZIP(zipName, files);
};
return (
<>
<div className="">
@@ -144,7 +191,7 @@ const Home = () => {
</>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b sm:p-2 mt-5">
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-40"></div>
<div className="w-48"></div>
<div className="w-32"></div>
@@ -159,7 +206,7 @@ const Home = () => {
{domains.map((domain) => (
<div
className="flex flex-col sm:flex-row text-secondary-foreground border-b sm:p-2 hover:bg-muted/50 text-sm"
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
key={domain.id}
>
<div className="sm:w-40 w-full pt-1 sm:pt-0 flex items-center">
@@ -244,6 +291,17 @@ const Home = () => {
</Button>
</Show>
<Show when={domain.expiredAt ? true : false}>
<Separator orientation="vertical" className="h-4 mx-2" />
<Button
variant={"link"}
className="p-0"
onClick={() => handleDownloadClick(domain)}
>
</Button>
</Show>
{!domain.enabled && (
<>
<Separator orientation="vertical" className="h-4 mx-2" />
@@ -286,6 +344,14 @@ const Home = () => {
</div>
</div>
))}
<XPagination
totalPages={totalPage}
currentPage={page ? Number(page) : 1}
onPageChange={(page) => {
setPage(page);
}}
/>
</>
)}
</div>

View File

@@ -1,6 +1,7 @@
import DeployProgress from "@/components/certimate/DeployProgress";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Sheet,
@@ -35,7 +36,7 @@ const History = () => {
}, [domain]);
return (
<div>
<ScrollArea className="h-[80vh] overflow-hidden">
<div className="text-muted-foreground"></div>
{!deployments?.length ? (
<>
@@ -65,7 +66,7 @@ const History = () => {
</>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b sm:p-2 mt-5">
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48"></div>
<div className="w-24"></div>
@@ -81,7 +82,7 @@ const History = () => {
{deployments?.map((deployment) => (
<div
key={deployment.id}
className="flex flex-col sm:flex-row text-secondary-foreground border-b sm:p-2 hover:bg-muted/50 text-sm"
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center">
{deployment.expand.domain?.domain}
@@ -146,6 +147,14 @@ const History = () => {
<div>[{item.time}]</div>
<div className="ml-2">{item.message}</div>
</div>
{item.info &&
item.info.map((info: string) => {
return (
<div className="mt-1 text-green-600">
{info}
</div>
);
})}
{item.error && (
<div className="mt-1 text-red-600">
{item.error}
@@ -184,7 +193,7 @@ const History = () => {
))}
</>
)}
</div>
</ScrollArea>
);
};

View File

@@ -47,12 +47,15 @@ const Login = () => {
const navigage = useNavigate();
return (
<div className="max-w-[35em] border mx-auto mt-32 p-10 rounded-md shadow-md">
<div className="max-w-[35em] border dark:border-stone-500 mx-auto mt-32 p-10 rounded-md shadow-md">
<div className="flex justify-center mb-10">
<img src="/vite.svg" className="w-16" />
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 dark:text-stone-200"
>
<FormField
control={form.control}
name="username"

View File

@@ -0,0 +1,149 @@
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useToast } from "@/components/ui/use-toast";
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({
oldPassword: z.string().min(10, {
message: "密码至少10个字符",
}),
newPassword: z.string().min(10, {
message: "密码至少10个字符",
}),
confirmPassword: z.string().min(10, {
message: "密码至少10个字符",
}),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: "两次密码不一致",
path: ["confirmPassword"],
});
const Password = () => {
const { toast } = useToast();
const navigate = useNavigate();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
oldPassword: "",
newPassword: "",
confirmPassword: "",
},
});
const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
await getPb().admins.authWithPassword(
getPb().authStore.model?.email,
values.oldPassword
);
} catch (e) {
const message = getErrMessage(e);
form.setError("oldPassword", { message });
}
try {
await getPb().admins.update(getPb().authStore.model?.id, {
password: values.newPassword,
passwordConfirm: values.confirmPassword,
});
getPb().authStore.clear();
toast({
title: "修改密码成功",
description: "请重新登录",
});
setTimeout(() => {
navigate("/login");
}, 500);
} catch (e) {
const message = getErrMessage(e);
toast({
title: "修改密码失败",
description: message,
variant: "destructive",
});
}
};
return (
<>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 dark:text-stone-200"
>
<FormField
control={form.control}
name="oldPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="当前密码" {...field} type="password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="newPassword" {...field} type="password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
placeholder="confirmPassword"
{...field}
type="password"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
</>
);
};
export default Password;

View File

@@ -10,9 +10,12 @@ import {
useReducer,
} from "react";
import { configReducer } from "./reducer";
import { getEmails } from "@/repository/settings";
import { Setting } from "@/domain/settings";
export type ConfigData = {
accesses: Access[];
emails: Setting;
};
export type ConfigContext = {
@@ -20,6 +23,7 @@ export type ConfigContext = {
deleteAccess: (id: string) => void;
addAccess: (access: Access) => void;
updateAccess: (access: Access) => void;
setEmails: (email: Setting) => void;
};
const Context = createContext({} as ConfigContext);
@@ -31,7 +35,10 @@ interface ContainerProps {
}
export const ConfigProvider = ({ children }: ContainerProps) => {
const [config, dispatchConfig] = useReducer(configReducer, { accesses: [] });
const [config, dispatchConfig] = useReducer(configReducer, {
accesses: [],
emails: { content: { emails: [] } },
});
useEffect(() => {
const featchData = async () => {
@@ -41,6 +48,18 @@ export const ConfigProvider = ({ children }: ContainerProps) => {
featchData();
}, []);
useEffect(() => {
const featchEmails = async () => {
const emails = await getEmails();
dispatchConfig({ type: "SET_EMAILS", payload: emails });
};
featchEmails();
}, []);
const setEmails = useCallback((emails: Setting) => {
dispatchConfig({ type: "SET_EMAILS", payload: emails });
}, []);
const deleteAccess = useCallback((id: string) => {
dispatchConfig({ type: "DELETE_ACCESS", payload: id });
}, []);
@@ -58,9 +77,11 @@ export const ConfigProvider = ({ children }: ContainerProps) => {
value={{
config: {
accesses: config.accesses,
emails: config.emails,
},
deleteAccess,
addAccess,
setEmails,
updateAccess,
}}
>

View File

@@ -1,11 +1,14 @@
import { Access } from "@/domain/access";
import { ConfigData } from ".";
import { Setting } from "@/domain/settings";
type Action =
| { type: "ADD_ACCESS"; payload: Access }
| { type: "DELETE_ACCESS"; payload: string }
| { type: "UPDATE_ACCESS"; payload: Access }
| { type: "SET_ACCESSES"; payload: Access[] };
| { type: "SET_ACCESSES"; payload: Access[] }
| { type: "SET_EMAILS"; payload: Setting }
| { type: "ADD_EMAIL"; payload: string };
export const configReducer = (
state: ConfigData,
@@ -40,6 +43,23 @@ export const configReducer = (
),
};
}
case "SET_EMAILS": {
return {
...state,
emails: action.payload,
};
}
case "ADD_EMAIL": {
return {
...state,
emails: {
...state.emails,
content: {
emails: [...state.emails.content.emails, action.payload],
},
},
};
}
default:
return state;
}

View File

@@ -1,7 +1,9 @@
import PocketBase from "pocketbase";
const apiDomain = import.meta.env.VITE_API_DOMAIN;
console.log(apiDomain);
let pb: PocketBase;
export const getPb = () => {
if (pb) return pb;
pb = new PocketBase("http://127.0.0.1:8090");
pb = new PocketBase("/");
return pb;
};

View File

@@ -1,15 +1,69 @@
import { Domain } from "@/domain/domain";
import { Domain, Statistic } from "@/domain/domain";
import { getPb } from "./api";
import { getTimeAfter } from "@/lib/time";
export const list = async () => {
const response = getPb().collection("domains").getFullList<Domain>({
type DomainListReq = {
domain?: string;
page?: number;
perPage?: number;
state?: string;
};
export const list = async (req: DomainListReq) => {
let page = 1;
if (req.page) {
page = req.page;
}
let perPage = 2;
if (req.perPage) {
perPage = req.perPage;
}
const pb = getPb();
let filter = "";
if (req.state === "enabled") {
filter = "enabled=true";
} else if (req.state === "disabled") {
filter = "enabled=false";
} else if (req.state === "expired") {
filter = pb.filter("expiredAt<{:expiredAt}", {
expiredAt: getTimeAfter(15),
});
}
const response = pb.collection("domains").getList<Domain>(page, perPage, {
sort: "-created",
expand: "lastDeployment",
filter: filter,
});
return response;
};
export const statistics = async (): Promise<Statistic> => {
const pb = getPb();
const total = await pb.collection("domains").getList(1, 1, {});
const expired = await pb.collection("domains").getList(1, 1, {
filter: pb.filter("expiredAt<{:expiredAt}", {
expiredAt: getTimeAfter(15),
}),
});
const enabled = await pb.collection("domains").getList(1, 1, {
filter: "enabled=true",
});
const disabled = await pb.collection("domains").getList(1, 1, {
filter: "enabled=false",
});
return {
total: total.totalItems,
expired: expired.totalItems,
enabled: enabled.totalItems,
disabled: disabled.totalItems,
};
};
export const get = async (id: string) => {
const response = await getPb().collection("domains").getOne<Domain>(id);
return response;
@@ -25,3 +79,24 @@ export const save = async (data: Domain) => {
export const remove = async (id: string) => {
return await getPb().collection("domains").delete(id);
};
type Callback = (data: Domain) => void;
export const subscribeId = (id: string, callback: Callback) => {
return getPb()
.collection("domains")
.subscribe<Domain>(
id,
(e) => {
if (e.action === "update") {
callback(e.record);
}
},
{
expand: "lastDeployment",
}
);
};
export const unsubscribeId = (id: string) => {
getPb().collection("domains").unsubscribe(id);
};

View File

@@ -0,0 +1,26 @@
import { Setting } from "@/domain/settings";
import { getPb } from "./api";
export const getEmails = async () => {
try {
const resp = await getPb()
.collection("settings")
.getFirstListItem<Setting>("name='emails'");
return resp;
} catch (e) {
return {
content: { emails: [] },
};
}
};
export const update = async (setting: Setting) => {
const pb = getPb();
let resp: Setting;
if (setting.id) {
resp = await pb.collection("settings").update(setting.id, setting);
} else {
resp = await pb.collection("settings").create(setting);
}
return resp;
};

View File

@@ -7,6 +7,9 @@ import Access from "./pages/access/Access";
import History from "./pages/history/History";
import Login from "./pages/login/Login";
import LoginLayout from "./pages/LoginLayout";
import Password from "./pages/setting/Password";
import SettingLayout from "./pages/SettingLayout";
import Dashboard from "./pages/dashboard/Dashboard";
export const router = createHashRouter([
{
@@ -15,6 +18,10 @@ export const router = createHashRouter([
children: [
{
path: "/",
element: <Dashboard />,
},
{
path: "/domains",
element: <Home />,
},
{
@@ -29,6 +36,16 @@ export const router = createHashRouter([
path: "/history",
element: <History />,
},
{
path: "/setting",
element: <SettingLayout />,
children: [
{
path: "/setting/password",
element: <Password />,
},
],
},
],
},
{

View File

@@ -14,6 +14,7 @@ module.exports = {
padding: "2rem",
screens: {
"2xl": "1400px",
'3xl': '1920px',
},
},
extend: {

View File

@@ -9,4 +9,9 @@ export default defineConfig({
"@": path.resolve(__dirname, "./src"),
},
},
server: {
proxy: {
"/api": "http://127.0.0.1:8090",
},
},
});

BIN
usage.gif Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB