Compare commits

...

27 Commits

Author SHA1 Message Date
usual2970
2cca82eb95 Merge pull request #61 from usual2970/feat/settings
Feat/settings
2024-09-21 06:38:04 +08:00
yoan
30beee6027 support email modification 2024-09-21 06:37:11 +08:00
yoan
b649348162 Merge branch 'main' into feat/settings 2024-09-21 06:35:16 +08:00
yoan
b912c5e688 support email update 2024-09-21 06:34:32 +08:00
usual2970
5981200df2 Merge pull request #59 from minibear2021/patch-1
Update README.md
2024-09-21 06:25:23 +08:00
Chen Gang
f9e7bfd606 Update README.md 2024-09-20 17:37:28 +08:00
yoan
7f6549bdf3 add wechat group 2024-09-20 10:26:33 +08:00
yoan
2af26dbfe0 add community 2024-09-19 18:06:18 +08:00
yoan
b7f382e16f add telegram group 2024-09-19 15:57:04 +08:00
yoan
7762955989 add telegram group 2024-09-19 15:52:10 +08:00
yoan
1ab603b506 add telegram group 2024-09-19 15:47:09 +08:00
yoan
b432cbfd3f push image to Dockerhub 2024-09-19 09:55:37 +08:00
yoan
e4d76113f8 push image to Dockerhub 2024-09-19 09:51:19 +08:00
usual2970
12a3adc559 Merge pull request #46 from usual2970/feat/force_deploy
force deploy and custom nameservers
2024-09-19 08:41:08 +08:00
yoan
e50f1a74d6 general domain include root domain 2024-09-19 08:39:59 +08:00
yoan
ba6a504588 force deploy and custom nameservers 2024-09-18 22:42:18 +08:00
usual2970
2d37c42584 Merge pull request #41 from PBK-B/feat-ali-dcdn
feat: 支持阿里云 DCDN (全站加速) 部署
2024-09-18 07:47:32 +08:00
PBK-B
f4b3a8cf81 feat: domains add aliyun-dcdn item #40 2024-09-17 19:29:30 +08:00
PBK-B
0390ac3eda feat: support aliyun dcdn(esa) 2024-09-17 19:10:25 +08:00
yoan
1977201051 add issue templates 2024-09-17 10:47:49 +08:00
yoan
2efe0de0cf UI optimization 2024-09-17 08:59:39 +08:00
yoan
34e40e5e54 UI Optimization 2024-09-16 08:44:36 +08:00
usual2970
e7e269dfb0 Merge pull request #37 from usual2970/feat/access_ui
Merge authorization group into authorization management
2024-09-15 21:59:03 +08:00
yoan
fa85580e35 Merge authorization group into authorization management 2024-09-15 21:58:08 +08:00
usual2970
500fce6180 Update README.md 2024-09-14 22:02:40 +08:00
usual2970
f501df2804 Merge pull request #30 from usual2970/feat/deploy_group
添加部署变量、部署授权组的支持
2024-09-14 15:39:26 +08:00
yoan
6c1b1fb72b Support deploying one certificate to multiple SSH hosts, and support deploying multiple certificates to one SSH host. 2024-09-14 15:36:15 +08:00
57 changed files with 3768 additions and 751 deletions

32
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,32 @@
---
name: Bug report
about: 创建一个报告来帮助我们改进
title: "[Bug] 标题简要描述问题"
labels: bug
assignees: ''
---
**描述问题**
简要描述问题是什么
**复现步骤**
复现该问题的步骤:
1. 去到 '...'
2. 点击 '...'
3. 滚动到 '...'
4. 发现问题
**期望的结果**
简要描述你期望发生的事情。
**截图**
如有可能,请添加截图以帮助解释问题。
**环境**
- 操作系统: [e.g. Windows, macOS]
- 浏览器: [e.g. Chrome, Safari]
- 仓库版本: [e.g. v1.0.0]
**其他信息**
在此处添加关于该问题的任何其他信息。

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 加入频道讨论
url: https://t.me/+ZXphsppxUg41YmVl
about: 加入到电报频道寻求更多帮助

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: 提出一个新功能请求
title: "[Feature] 简要描述你希望实现的功能"
labels: enhancement
assignees: ''
---
**功能描述**
简要描述你希望添加的功能和相关问题。
**动机**
为什么这个功能对项目有帮助?
**替代方案**
描述你已经考虑过的替代方案。
**其他信息**
在这里添加任何相关的附加信息或截图。

View File

@@ -4,6 +4,12 @@ on:
push:
tags:
- "*"
workflow_dispatch:
inputs:
tag:
description: "Tag version to be used for Docker image"
required: true
default: "v0.1.9"
jobs:
build-and-push:
@@ -19,7 +25,22 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
usual2970/certimate
registry.cn-shanghai.aliyuncs.com/usual2970/certimate
- name: Log in to DOCKERHUB
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Log in to ALIYUNCS
uses: docker/login-action@v3
with:
@@ -27,10 +48,6 @@ jobs:
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:
@@ -38,6 +55,4 @@ jobs:
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
tags: ${{ steps.meta.outputs.tags }}

View File

@@ -13,6 +13,11 @@ Certimate 就是为了解决上述问题而产生的,它具有以下特点:
2. 支持私有部署部署方法简单只需下载二进制文件执行即可。二进制文件、docker 镜像全部用 github actions 生成,过程透明,可自行审计。
3. 数据安全:由于是私有部署,所有数据均存储在本地,不会保存在服务商的服务器,确保数据的安全性。
相关文章:
* [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
* [域名变量及部署授权组介绍](https://docs.certimate.me/blog/multi-deployer)
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。使用文档请访问[https://docs.certimate.me](https://docs.certimate.me)
@@ -30,6 +35,7 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
- [3. 部署服务商授权信息](#3-部署服务商授权信息)
- [六、常见问题](#六常见问题)
- [七、贡献](#七贡献)
- [八、加入社区](#八加入社区)
@@ -109,7 +115,7 @@ go run main.go serve
Certimate 的工作流程如下:
* 用户通过 Certimate 管理页面填写申请证书的信息包括域名、dns 服务商的授权信息、以及要部署到的服务商的授权信息。
* Certimate 向证书商的 API 发起申请请求,获取 SSL 证书。
* Certimate 向证书商的 API 发起申请请求,获取 SSL 证书。
* Certimate 存储证书信息,包括证书内容、私钥、证书有效期等,并在证书即将过期时自动续期。
* Certimate 向服务商的 API 发起部署请求,将证书部署到服务商的服务器上。
@@ -173,4 +179,10 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.
支持更多服务商、UI 的优化改进、BUG 修复、文档完善等,欢迎大家提交 PR。
## 八、加入社区
* [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
* 微信群聊
<img src="https://i.imgur.com/lJUfTeD.png" width="400"/>

BIN
certimate
View File

Binary file not shown.

22
go.mod
View File

@@ -7,9 +7,9 @@ toolchain go1.22.5
require (
github.com/alibabacloud-go/cas-20200407/v2 v2.3.0
github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.8
github.com/alibabacloud-go/tea v1.2.1
github.com/alibabacloud-go/tea-utils/v2 v2.0.5
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9
github.com/alibabacloud-go/tea v1.2.2
github.com/alibabacloud-go/tea-utils/v2 v2.0.6
github.com/go-acme/lego/v4 v4.17.4
github.com/gojek/heimdall/v7 v7.0.3
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
@@ -23,16 +23,24 @@ require (
golang.org/x/crypto v0.26.0
)
require github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
require (
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
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/darabonba-openapi v0.1.18 // indirect
github.com/alibabacloud-go/dcdn-20180115/v3 v3.4.2
github.com/alibabacloud-go/debug v1.0.0 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
github.com/alibabacloud-go/tea-utils v1.4.3 // indirect
github.com/alibabacloud-go/tea-utils v1.4.5 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 // indirect
github.com/aliyun/credentials-go v1.3.1 // indirect
@@ -57,7 +65,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
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/clbanning/mxj/v2 v2.5.6 // indirect
github.com/cloudflare/cloudflare-go v0.97.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/disintegration/imaging v1.6.2 // indirect

43
go.sum
View File

@@ -35,26 +35,61 @@ github.com/alibabacloud-go/cas-20200407/v2 v2.3.0 h1:nOrp0n2nFZiYN0wIG7S26YVVaMM
github.com/alibabacloud-go/cas-20200407/v2 v2.3.0/go.mod h1:yzkgdLANANu/v56k0ptslGl++JJL4Op1V09HTavfoCo=
github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0 h1:yTKngw4rBR3hdpoo/uCyBffYXfPfjNjlaDL8nTYUIds=
github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0/go.mod h1:HxQrwVKBx3/6bIwmdDcpqBpSQt2tpi/j4LfEhl+QFPk=
github.com/alibabacloud-go/darabonba-openapi v0.1.18 h1:3eUVmAr7WCJp7fgIvmCd9ZUyuwtJYbtUqJIed5eXCmk=
github.com/alibabacloud-go/darabonba-openapi v0.1.18/go.mod h1:PB4HffMhJVmAgNKNq3wYbTUlFvPgxJpTzd1F5pTuUsc=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.0/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.6/go.mod h1:CzQnh+94WDnJOnKZH5YRyouL+OOcdBnXY5VWAf0McgI=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.8 h1:benoD0QHDrylMzEQVpX/6uKtrN8LohT66ZlKXVJh7pM=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.8/go.mod h1:CzQnh+94WDnJOnKZH5YRyouL+OOcdBnXY5VWAf0McgI=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9 h1:fxMCrZatZfXq5nLcgkmWBXmU3FLC1OR+m/SqVtMqflk=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9/go.mod h1:bb+Io8Sn2RuM3/Rpme6ll86jMyFSrD1bxeV/+v61KeU=
github.com/alibabacloud-go/darabonba-string v1.0.0/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
github.com/alibabacloud-go/dcdn-20180115 v1.0.20 h1:Vp6K2GxtSL1DzZ2dyumbEPuujzxGFN0Hau+mwcqBrVo=
github.com/alibabacloud-go/dcdn-20180115 v1.0.20/go.mod h1:FYEDKSB19NzejWQWGzq5QMi+w01xtDnAMd/I+Qz0nKw=
github.com/alibabacloud-go/dcdn-20180115/v3 v3.4.2 h1:WKMtPfhEmf8jX4FvdG7MFBJeCknPQ+FEHQppDcaCoU0=
github.com/alibabacloud-go/dcdn-20180115/v3 v3.4.2/go.mod h1:dGuR8qQqofJKl99rVaWvObnP3bMkru3cdOtqJJ95048=
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 h1:NqugFkGxx1TXSh/pBcU00Y6bljgDPaFdh5MUSeJ7e50=
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
github.com/alibabacloud-go/debug v1.0.0 h1:3eIEQWfay1fB24PQIEzXAswlVJtdQok8f3EVN5VrBnA=
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
github.com/alibabacloud-go/openapi-util v0.0.11/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 h1:L0TIjr9Qh/SLVc1yPhFkcB9+9SbCNK/jPq4ZKB5zmnc=
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1/go.mod h1:EKxBRDLcMzwl4VLF/1WJwlByZZECJawPXUvinKMsTTs=
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.10/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.12/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.19/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.1 h1:rFF1LnrAdhaiPmKwH5xwYOKlMh66CqRwPUTzIK74ask=
github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA=
github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea-fileform v1.1.1 h1:1YG6erAP3joQ0XdCXYIotuD7zyOM6qCR49xkp5FZDeU=
github.com/alibabacloud-go/tea-fileform v1.1.1/go.mod h1:ZeCV91o4ISmxidd686f0ebdS5EDHWU+vW+TkjLhrsFE=
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 h1:EhAHI6edMeqgkZEqP7r4nc9iMWAUBKGxJHoBsOSKTtU=
github.com/alibabacloud-go/tea-oss-sdk v1.1.3/go.mod h1:yUnodpR3Bf2rudLE7V/Gft5txjJF30Pk+hH77K/Eab0=
github.com/alibabacloud-go/tea-oss-utils v1.1.0 h1:y65crjjcZ2Pbb6UZtC2deuIZHDVTS3IaDWE7M9nVLRc=
github.com/alibabacloud-go/tea-oss-utils v1.1.0/go.mod h1:PFCF12e9yEKyBUIn7X1IrF/pNjvxgkHy0CgxX4+xRuY=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils v1.3.6/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils v1.4.3 h1:8SzwmmRrOnQ09Hf5a9GyfJc0d7Sjv6fmsZoF4UDbFjo=
github.com/alibabacloud-go/tea-utils v1.4.3/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
github.com/alibabacloud-go/tea-utils v1.4.5 h1:h0/6Xd2f3bPE4XHTvkpjwxowIwRCJAJOqY6Eq8f3zfA=
github.com/alibabacloud-go/tea-utils v1.4.5/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
github.com/alibabacloud-go/tea-utils/v2 v2.0.0/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5 h1:EUakYEUAwr6L3wLT0vejIw2rc0IA1RSXDwLnIb3f2vU=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.6 h1:ZkmUlhlQbaDC+Eba/GARMPy6hKdCLiSke5RsN5LcyQ0=
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-xml v1.1.1/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 h1:lM7JnA9dEdDFH9XOgRNQMDTQnOjlLkDTNA7c0aWTQ30=
@@ -111,6 +146,8 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
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/clbanning/mxj/v2 v2.5.6 h1:Jm4VaCI/+Ug5Q57IzEoZbwx4iQFA6wkXv72juUSeK+g=
github.com/clbanning/mxj/v2 v2.5.6/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.97.0 h1:feZRGiRF1EbljnNIYdt8014FnOLtC3CCvgkLXu915ks=
github.com/cloudflare/cloudflare-go v0.97.0/go.mod h1:JXRwuTfHpe5xFg8xytc2w0XC6LcrFsBVMS4WlVaiGg8=
@@ -395,6 +432,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -438,6 +476,7 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -473,6 +512,7 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -482,6 +522,7 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -494,8 +535,10 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@@ -6,10 +6,12 @@ import (
"crypto/elliptic"
"crypto/rand"
"errors"
"strings"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/pocketbase/pocketbase/models"
@@ -35,9 +37,10 @@ type Certificate struct {
}
type ApplyOption struct {
Email string `json:"email"`
Domain string `json:"domain"`
Access string `json:"access"`
Email string `json:"email"`
Domain string `json:"domain"`
Access string `json:"access"`
Nameservers string `json:"nameservers"`
}
type MyUser struct {
@@ -67,9 +70,10 @@ func Get(record *models.Record) (Applicant, error) {
email = defaultEmail
}
option := &ApplyOption{
Email: email,
Domain: record.GetString("domain"),
Access: access.GetString("config"),
Email: email,
Domain: record.GetString("domain"),
Access: access.GetString("config"),
Nameservers: record.GetString("nameservers"),
}
switch access.GetString("configType") {
case configTypeTencent:
@@ -111,7 +115,13 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
return nil, err
}
client.Challenge.SetDNS01Provider(provider)
challengeOptions := make([]dns01.ChallengeOption, 0)
nameservers := ParseNameservers(option.Nameservers)
if len(nameservers) > 0 {
challengeOptions = append(challengeOptions, dns01.AddRecursiveNameservers(nameservers))
}
client.Challenge.SetDNS01Provider(provider, challengeOptions...)
// New users will need to register
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
@@ -120,8 +130,16 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
}
myUser.Registration = reg
domains := []string{option.Domain}
// 如果是通配置符域名,把根域名也加入
if strings.HasPrefix(option.Domain, "*.") && len(strings.Split(option.Domain, ".")) == 3 {
rootDomain := strings.TrimPrefix(option.Domain, "*.")
domains = append(domains, rootDomain)
}
request := certificate.ObtainRequest{
Domains: []string{option.Domain},
Domains: domains,
Bundle: true,
}
certificates, err := client.Certificate.Obtain(request)
@@ -138,3 +156,22 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
Csr: string(certificates.CSR),
}, nil
}
func ParseNameservers(ns string) []string {
nameservers := make([]string, 0)
lines := strings.Split(ns, ";")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
nameservers = append(nameservers, line)
}
return nameservers
}

View File

@@ -38,6 +38,10 @@ func NewAliyun(option *DeployerOption) (Deployer, error) {
}
func (a *aliyun) GetID() string {
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
}
func (a *aliyun) GetInfo() []string {
return a.infos
}

View File

@@ -36,6 +36,10 @@ func NewAliyunCdn(option *DeployerOption) (*AliyunCdn, error) {
}, nil
}
func (a *AliyunCdn) GetID() string {
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
}
func (a *AliyunCdn) GetInfo() []string {
return a.infos
}

View File

@@ -0,0 +1,86 @@
/*
* @Author: Bin
* @Date: 2024-09-17
* @FilePath: /certimate/internal/deployer/aliyun_esa.go
*/
package deployer
import (
"certimate/internal/domain"
"context"
"encoding/json"
"fmt"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
dcdn20180115 "github.com/alibabacloud-go/dcdn-20180115/v3/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
)
type AliyunEsa struct {
client *dcdn20180115.Client
option *DeployerOption
infos []string
}
func NewAliyunEsa(option *DeployerOption) (*AliyunEsa, error) {
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
a := &AliyunEsa{
option: option,
}
client, err := a.createClient(access.AccessKeyId, access.AccessKeySecret)
if err != nil {
return nil, err
}
return &AliyunEsa{
client: client,
option: option,
infos: make([]string, 0),
}, nil
}
func (a *AliyunEsa) GetID() string {
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
}
func (a *AliyunEsa) GetInfo() []string {
return a.infos
}
func (a *AliyunEsa) Deploy(ctx context.Context) error {
certName := fmt.Sprintf("%s-%s", a.option.Domain, a.option.DomainId)
setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{
DomainName: tea.String(a.option.Domain),
CertName: tea.String(certName),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),
SSLPub: tea.String(a.option.Certificate.Certificate),
SSLPri: tea.String(a.option.Certificate.PrivateKey),
CertRegion: tea.String("cn-hangzhou"),
}
runtime := &util.RuntimeOptions{}
resp, err := a.client.SetDcdnDomainSSLCertificateWithOptions(setDcdnDomainSSLCertificateRequest, runtime)
if err != nil {
return err
}
a.infos = append(a.infos, toStr("dcdn设置证书", resp))
return nil
}
func (a *AliyunEsa) createClient(accessKeyId, accessKeySecret string) (_result *dcdn20180115.Client, _err error) {
config := &openapi.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
config.Endpoint = tea.String("dcdn.aliyuncs.com")
_result = &dcdn20180115.Client{}
_result, _err = dcdn20180115.NewClient(config)
return _result, _err
}

View File

@@ -2,9 +2,12 @@ package deployer
import (
"certimate/internal/applicant"
"certimate/internal/utils/app"
"certimate/internal/utils/variables"
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/pocketbase/pocketbase/models"
@@ -13,6 +16,7 @@ import (
const (
targetAliyunOss = "aliyun-oss"
targetAliyunCdn = "aliyun-cdn"
targetAliyunEsa = "aliyun-dcdn"
targetSSH = "ssh"
targetWebhook = "webhook"
targetTencentCdn = "tencent-cdn"
@@ -20,25 +24,85 @@ const (
)
type DeployerOption struct {
DomainId string `json:"domainId"`
Domain string `json:"domain"`
Product string `json:"product"`
Access string `json:"access"`
Certificate applicant.Certificate `json:"certificate"`
DomainId string `json:"domainId"`
Domain string `json:"domain"`
Product string `json:"product"`
Access string `json:"access"`
AceessRecord *models.Record `json:"-"`
Certificate applicant.Certificate `json:"certificate"`
Variables map[string]string `json:"variables"`
}
type Deployer interface {
Deploy(ctx context.Context) error
GetInfo() []string
GetID() string
}
func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
access := record.ExpandedOne("targetAccess")
func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error) {
rs := make([]Deployer, 0)
if record.GetString("targetAccess") != "" {
singleDeployer, err := Get(record, cert)
if err != nil {
return nil, err
}
rs = append(rs, singleDeployer)
}
if record.GetString("group") != "" {
group := record.ExpandedOne("group")
if errs := app.GetApp().Dao().ExpandRecord(group, []string{"access"}, nil); len(errs) > 0 {
errList := make([]error, 0)
for name, err := range errs {
errList = append(errList, fmt.Errorf("展开记录失败,%s: %w", name, err))
}
err := errors.Join(errList...)
return nil, err
}
records := group.ExpandedAll("access")
deployers, err := getByGroup(record, cert, records...)
if err != nil {
return nil, err
}
rs = append(rs, deployers...)
}
return rs, nil
}
func getByGroup(record *models.Record, cert *applicant.Certificate, accesses ...*models.Record) ([]Deployer, error) {
rs := make([]Deployer, 0)
for _, access := range accesses {
deployer, err := getWithAccess(record, cert, access)
if err != nil {
return nil, err
}
rs = append(rs, deployer)
}
return rs, nil
}
func getWithAccess(record *models.Record, cert *applicant.Certificate, access *models.Record) (Deployer, error) {
option := &DeployerOption{
DomainId: record.Id,
Domain: record.GetString("domain"),
Product: getProduct(record),
Access: access.GetString("config"),
DomainId: record.Id,
Domain: record.GetString("domain"),
Product: getProduct(record),
Access: access.GetString("config"),
AceessRecord: access,
Variables: variables.Parse2Map(record.GetString("variables")),
}
if cert != nil {
option.Certificate = *cert
@@ -54,6 +118,8 @@ func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
return NewAliyun(option)
case targetAliyunCdn:
return NewAliyunCdn(option)
case targetAliyunEsa:
return NewAliyunEsa(option)
case targetSSH:
return NewSSH(option)
case targetWebhook:
@@ -66,6 +132,13 @@ func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
return nil, errors.New("not implemented")
}
func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
access := record.ExpandedOne("targetAccess")
return getWithAccess(record, cert, access)
}
func getProduct(record *models.Record) string {
targetType := record.GetString("targetType")
rs := strings.Split(targetType, "-")

View File

@@ -33,6 +33,10 @@ func NewQiNiu(option *DeployerOption) (*qiuniu, error) {
}, nil
}
func (a *qiuniu) GetID() string {
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
}
func (q *qiuniu) GetInfo() []string {
return q.info
}

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"os"
xpath "path"
"strings"
"github.com/pkg/sftp"
sshPkg "golang.org/x/crypto/ssh"
@@ -35,6 +36,10 @@ func NewSSH(option *DeployerOption) (Deployer, error) {
}, nil
}
func (a *ssh) GetID() string {
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
}
func (s *ssh) GetInfo() []string {
return s.infos
}
@@ -44,6 +49,15 @@ func (s *ssh) Deploy(ctx context.Context) error {
if err := json.Unmarshal([]byte(s.option.Access), access); err != nil {
return err
}
// 将证书路径和命令中的变量替换为实际值
for k, v := range s.option.Variables {
key := fmt.Sprintf("${%s}", k)
access.CertPath = strings.ReplaceAll(access.CertPath, key, v)
access.KeyPath = strings.ReplaceAll(access.KeyPath, key, v)
access.Command = strings.ReplaceAll(access.Command, key, v)
}
// 连接
client, err := s.getClient(access)
if err != nil {

View File

@@ -39,6 +39,10 @@ func NewTencentCdn(option *DeployerOption) (Deployer, error) {
}, nil
}
func (a *tencentCdn) GetID() string {
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
}
func (t *tencentCdn) GetInfo() []string {
return t.infos
}

View File

@@ -32,6 +32,10 @@ func NewWebhook(option *DeployerOption) (Deployer, error) {
}, nil
}
func (a *webhook) GetID() string {
return fmt.Sprintf("%s-%s", a.option.AceessRecord.GetString("name"), a.option.AceessRecord.Id)
}
func (w *webhook) GetInfo() []string {
return w.infos
}

View File

@@ -41,7 +41,7 @@ func deploy(ctx context.Context, record *models.Record) error {
return err
}
history.record(checkPhase, "获取记录成功", nil)
if errs := app.GetApp().Dao().ExpandRecord(currRecord, []string{"access", "targetAccess"}, nil); len(errs) > 0 {
if errs := app.GetApp().Dao().ExpandRecord(currRecord, []string{"access", "targetAccess", "group"}, nil); len(errs) > 0 {
errList := make([]error, 0)
for name, err := range errs {
@@ -96,24 +96,28 @@ func deploy(ctx context.Context, record *models.Record) error {
// ############3.部署证书
history.record(deployPhase, "开始部署", nil, false)
deployer, err := deployer.Get(currRecord, certificate)
deployers, err := deployer.Gets(currRecord, certificate)
if err != nil {
history.record(deployPhase, "获取deployer失败", &RecordInfo{Err: err})
app.GetApp().Logger().Error("获取deployer失败", "err", err)
return err
}
if err = deployer.Deploy(ctx); err != nil {
for _, deployer := range deployers {
if err = deployer.Deploy(ctx); err != nil {
app.GetApp().Logger().Error("部署失败", "err", err)
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfo()})
return err
}
history.record(deployPhase, fmt.Sprintf("[%s]-部署成功", deployer.GetID()), &RecordInfo{
Info: deployer.GetInfo(),
}, false)
app.GetApp().Logger().Error("部署失败", "err", err)
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfo()})
return err
}
app.GetApp().Logger().Info("部署成功")
history.record(deployPhase, "部署成功", &RecordInfo{
Info: deployer.GetInfo(),
}, true)
history.record(deployPhase, "部署成功", nil, true)
return nil
}

View File

@@ -0,0 +1,30 @@
package variables
import "strings"
// Parse2Map 将变量赋值字符串解析为map
func Parse2Map(str string) map[string]string {
m := make(map[string]string)
lines := strings.Split(str, ";")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
kv := strings.Split(line, "=")
if len(kv) != 2 {
continue
}
m[kv[0]] = kv[1]
}
return m
}

View File

@@ -0,0 +1,56 @@
package variables
import (
"reflect"
"testing"
)
func TestParse2Map(t *testing.T) {
type args struct {
str string
}
tests := []struct {
name string
args args
want map[string]string
}{
{
name: "test1",
args: args{
str: "a=1;b=2;c=3",
},
want: map[string]string{
"a": "1",
"b": "2",
"c": "3",
},
},
{
name: "test2",
args: args{
str: `a=1;
b=2;
c=`,
},
want: map[string]string{
"a": "1",
"b": "2",
"c": "",
},
},
{
name: "test3",
args: args{
str: "1",
},
want: map[string]string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Parse2Map(tt.args.str); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Parse2Map() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,679 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
)
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `[
{
"id": "z3p974ainxjqlvs",
"created": "2024-07-29 10:02:48.334Z",
"updated": "2024-09-14 02:53:22.520Z",
"name": "domains",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "iuaerpl2",
"name": "domain",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "ukkhuw85",
"name": "email",
"type": "email",
"required": false,
"presentable": false,
"unique": false,
"options": {
"exceptDomains": null,
"onlyDomains": null
}
},
{
"system": false,
"id": "v98eebqq",
"name": "crontab",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "alc8e9ow",
"name": "access",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "4yzbv8urny5ja1e",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "topsc9bj",
"name": "certUrl",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "vixgq072",
"name": "certStableUrl",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "g3a3sza5",
"name": "privateKey",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "gr6iouny",
"name": "certificate",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "tk6vnrmn",
"name": "issuerCertificate",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "sjo6ibse",
"name": "csr",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "x03n1bkj",
"name": "expiredAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "srybpixz",
"name": "targetType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun-oss",
"aliyun-cdn",
"ssh",
"webhook",
"tencent-cdn",
"qiniu-cdn"
]
}
},
{
"system": false,
"id": "xy7yk0mb",
"name": "targetAccess",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "4yzbv8urny5ja1e",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "6jqeyggw",
"name": "enabled",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "hdsjcchf",
"name": "deployed",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "aiya3rev",
"name": "rightnow",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "ixznmhzc",
"name": "lastDeployedAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "ghtlkn5j",
"name": "lastDeployment",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "0a1o4e6sstp694f",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "zfnyj9he",
"name": "variables",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "1bspzuku",
"name": "group",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "teolp9pl72dxlxq",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_4ABO6EQ` + "`" + ` ON ` + "`" + `domains` + "`" + ` (` + "`" + `domain` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "4yzbv8urny5ja1e",
"created": "2024-07-29 10:04:39.685Z",
"updated": "2024-09-13 23:47:27.173Z",
"name": "access",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "geeur58v",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "iql7jpwx",
"name": "config",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
},
{
"system": false,
"id": "hwy7m03o",
"name": "configType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun",
"tencent",
"ssh",
"webhook",
"cloudflare",
"qiniu",
"namesilo",
"godaddy"
]
}
},
{
"system": false,
"id": "lr33hiwg",
"name": "deleted",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "hsxcnlvd",
"name": "usage",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"apply",
"deploy",
"all"
]
}
},
{
"system": false,
"id": "c8egzzwj",
"name": "group",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "teolp9pl72dxlxq",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_wkoST0j` + "`" + ` ON ` + "`" + `access` + "`" + ` (` + "`" + `name` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "0a1o4e6sstp694f",
"created": "2024-07-30 06:30:27.801Z",
"updated": "2024-09-13 12:52:50.804Z",
"name": "deployments",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "farvlzk7",
"name": "domain",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "z3p974ainxjqlvs",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "jx5f69i3",
"name": "log",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
},
{
"system": false,
"id": "qbxdtg9q",
"name": "phase",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"check",
"apply",
"deploy"
]
}
},
{
"system": false,
"id": "rglrp1hz",
"name": "phaseSuccess",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "lt1g1blu",
"name": "deployedAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "_pb_users_auth_",
"created": "2024-09-12 13:09:54.234Z",
"updated": "2024-09-12 23:34:40.687Z",
"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:34:40.687Z",
"name": "settings",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "1tcmdsdf",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "f9wyhypi",
"name": "content",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_RO7X9Vw` + "`" + ` ON ` + "`" + `settings` + "`" + ` (` + "`" + `name` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "teolp9pl72dxlxq",
"created": "2024-09-13 12:51:05.611Z",
"updated": "2024-09-14 00:01:58.239Z",
"name": "access_groups",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "7sajiv6i",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "xp8admif",
"name": "access",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "4yzbv8urny5ja1e",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": null,
"displayFields": null
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_RgRXp0R` + "`" + ` ON ` + "`" + `access_groups` + "`" + ` (` + "`" + `name` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
}
]`
collections := []*models.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
return err
}
return daos.New(db).ImportCollections(collections, true, nil)
}, func(db dbx.Builder) error {
return nil
})
}

View File

@@ -0,0 +1,85 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models/schema"
)
func init() {
m.Register(func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("z3p974ainxjqlvs")
if err != nil {
return err
}
// update
edit_targetType := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "srybpixz",
"name": "targetType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun-oss",
"aliyun-cdn",
"aliyun-dcdn",
"ssh",
"webhook",
"tencent-cdn",
"qiniu-cdn"
]
}
}`), edit_targetType); err != nil {
return err
}
collection.Schema.AddField(edit_targetType)
return dao.SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("z3p974ainxjqlvs")
if err != nil {
return err
}
// update
edit_targetType := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "srybpixz",
"name": "targetType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun-oss",
"aliyun-cdn",
"ssh",
"webhook",
"tencent-cdn",
"qiniu-cdn"
]
}
}`), edit_targetType); err != nil {
return err
}
collection.Schema.AddField(edit_targetType)
return dao.SaveCollection(collection)
})
}

View File

@@ -0,0 +1,694 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
)
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `[
{
"id": "z3p974ainxjqlvs",
"created": "2024-07-29 10:02:48.334Z",
"updated": "2024-09-18 14:23:22.359Z",
"name": "domains",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "iuaerpl2",
"name": "domain",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "ukkhuw85",
"name": "email",
"type": "email",
"required": false,
"presentable": false,
"unique": false,
"options": {
"exceptDomains": null,
"onlyDomains": null
}
},
{
"system": false,
"id": "v98eebqq",
"name": "crontab",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "alc8e9ow",
"name": "access",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "4yzbv8urny5ja1e",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "topsc9bj",
"name": "certUrl",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "vixgq072",
"name": "certStableUrl",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "g3a3sza5",
"name": "privateKey",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "gr6iouny",
"name": "certificate",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "tk6vnrmn",
"name": "issuerCertificate",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "sjo6ibse",
"name": "csr",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "x03n1bkj",
"name": "expiredAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "srybpixz",
"name": "targetType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun-oss",
"aliyun-cdn",
"aliyun-dcdn",
"ssh",
"webhook",
"tencent-cdn",
"qiniu-cdn"
]
}
},
{
"system": false,
"id": "xy7yk0mb",
"name": "targetAccess",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "4yzbv8urny5ja1e",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "6jqeyggw",
"name": "enabled",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "hdsjcchf",
"name": "deployed",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "aiya3rev",
"name": "rightnow",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "ixznmhzc",
"name": "lastDeployedAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "ghtlkn5j",
"name": "lastDeployment",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "0a1o4e6sstp694f",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "zfnyj9he",
"name": "variables",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "1bspzuku",
"name": "group",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "teolp9pl72dxlxq",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "g65gfh7a",
"name": "nameservers",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_4ABO6EQ` + "`" + ` ON ` + "`" + `domains` + "`" + ` (` + "`" + `domain` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "4yzbv8urny5ja1e",
"created": "2024-07-29 10:04:39.685Z",
"updated": "2024-09-17 00:53:25.859Z",
"name": "access",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "geeur58v",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "iql7jpwx",
"name": "config",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
},
{
"system": false,
"id": "hwy7m03o",
"name": "configType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun",
"tencent",
"ssh",
"webhook",
"cloudflare",
"qiniu",
"namesilo",
"godaddy"
]
}
},
{
"system": false,
"id": "lr33hiwg",
"name": "deleted",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "hsxcnlvd",
"name": "usage",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"apply",
"deploy",
"all"
]
}
},
{
"system": false,
"id": "c8egzzwj",
"name": "group",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "teolp9pl72dxlxq",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_wkoST0j` + "`" + ` ON ` + "`" + `access` + "`" + ` (` + "`" + `name` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "0a1o4e6sstp694f",
"created": "2024-07-30 06:30:27.801Z",
"updated": "2024-09-17 00:53:25.859Z",
"name": "deployments",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "farvlzk7",
"name": "domain",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "z3p974ainxjqlvs",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "jx5f69i3",
"name": "log",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
},
{
"system": false,
"id": "qbxdtg9q",
"name": "phase",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"check",
"apply",
"deploy"
]
}
},
{
"system": false,
"id": "rglrp1hz",
"name": "phaseSuccess",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "lt1g1blu",
"name": "deployedAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "_pb_users_auth_",
"created": "2024-09-12 13:09:54.234Z",
"updated": "2024-09-17 00:53:25.859Z",
"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-17 00:53:25.860Z",
"name": "settings",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "1tcmdsdf",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "f9wyhypi",
"name": "content",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_RO7X9Vw` + "`" + ` ON ` + "`" + `settings` + "`" + ` (` + "`" + `name` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "teolp9pl72dxlxq",
"created": "2024-09-13 12:51:05.611Z",
"updated": "2024-09-17 00:53:25.860Z",
"name": "access_groups",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "7sajiv6i",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "xp8admif",
"name": "access",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "4yzbv8urny5ja1e",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": null,
"displayFields": null
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_RgRXp0R` + "`" + ` ON ` + "`" + `access_groups` + "`" + ` (` + "`" + `name` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
}
]`
collections := []*models.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
return err
}
return daos.New(db).ImportCollections(collections, true, nil)
}, func(db dbx.Builder) error {
return nil
})
}

303
ui/dist/assets/index-BLKGMHXS.js vendored Normal file
View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<path d="M0 0 C8.00019718 6.13887963 14.34445574 12.99622218 18.3125 22.3125 C15.06464319 20.86413142 12.72723872 19.02613433 10.0625 16.6875 C-1.60459795 7.2541616 -16.18206674 0.63880608 -31.375 1.4375 C-43.09356312 2.78801868 -53.73683191 6.67588577 -62 15.5 C-71.51641841 27.69982709 -72.77906448 40.43807508 -71.6875 55.3125 C-69.75496452 69.19218976 -63.07550701 81.31999767 -54.6875 92.3125 C-53.96433594 93.26769531 -53.96433594 93.26769531 -53.2265625 94.2421875 C-49.80310028 98.49787258 -45.86843357 101.64242084 -41.4453125 104.83203125 C-40.86523438 105.32058594 -40.28515625 105.80914062 -39.6875 106.3125 C-39.6875 106.9725 -39.6875 107.6325 -39.6875 108.3125 C-45.21884376 108.61097445 -50.74543637 108.82766089 -56.28295898 108.97167969 C-58.1623705 109.03173819 -60.04122791 109.11342379 -61.91870117 109.21777344 C-77.40966514 110.05667884 -77.40966514 110.05667884 -82.80957031 105.921875 C-85.19168462 102.82857874 -86.97025266 99.81402723 -88.6875 96.3125 C-89.16856201 95.46268555 -89.64962402 94.61287109 -90.14526367 93.73730469 C-91.39695086 91.46566255 -92.49925956 89.17751393 -93.5625 86.8125 C-93.93689209 85.9877417 -94.31128418 85.1629834 -94.69702148 84.31323242 C-101.84695145 68.18991103 -104.37476618 48.01876725 -97.94140625 31.1171875 C-97.52761719 30.19164062 -97.11382812 29.26609375 -96.6875 28.3125 C-96.26339844 27.30445313 -95.83929688 26.29640625 -95.40234375 25.2578125 C-88.42098361 9.9727176 -77.15139172 -1.27183748 -61.6875 -7.6875 C-40.67179933 -14.71095263 -18.47073623 -12.74198216 0 0 Z " fill="#70A2EA" transform="translate(151.6875,52.6875)"/>
<path d="M0 0 C-0.19335938 0.56847656 -0.38671875 1.13695313 -0.5859375 1.72265625 C-1.13131792 3.33989819 -1.66856703 4.95989684 -2.19921875 6.58203125 C-2.82868314 8.48270666 -3.50036013 10.36940667 -4.1875 12.25 C-7.48441131 27.46651373 -2.93859997 42.92249193 5 56 C10.27285559 63.85713427 17.42266677 71.71133339 26 76 C28.93108041 76.12365056 31.83742448 76.18844664 34.76953125 76.203125 C35.64268478 76.20882507 36.51583832 76.21452515 37.41545105 76.22039795 C39.26564317 76.22985345 41.11585218 76.23638411 42.96606445 76.24023438 C45.79082844 76.24992565 48.61486044 76.28093755 51.43945312 76.3125 C53.23697485 76.31903152 55.03450177 76.32428141 56.83203125 76.328125 C58.0963372 76.3466452 58.0963372 76.3466452 59.38618469 76.36553955 C64.08745075 76.34777001 67.19523771 76.06753063 71 73 C76.14974577 66.13367231 76.85747523 60.40719967 76 52 C72.94347249 40.85669565 66.57825718 33.02825174 57.07421875 26.66796875 C41.98336589 18.48017438 25.32550226 16.19330935 8.4375 15.4375 C7.52419922 15.39431641 6.61089844 15.35113281 5.66992188 15.30664062 C3.44672009 15.20188243 1.22343293 15.09970983 -1 15 C-1 14.34 -1 13.68 -1 13 C24.14499626 7.59108915 50.41887378 8.13666192 72.734375 22.42578125 C84.76471086 30.78479048 94.05571707 43.44065596 97 58 C97.83992741 68.1617447 96.68764977 76.53638183 90.375 84.75 C82.2520582 93.4708233 71.29646331 96.75137499 59.70800781 97.22460938 C55.46822317 97.34944357 51.22762628 97.41269989 46.98632812 97.45996094 C45.16900252 97.48655278 43.35182742 97.52746211 41.53515625 97.58300781 C30.19480451 97.9245351 21.92945424 97.88338566 13 90 C12.08879395 89.26837646 12.08879395 89.26837646 11.15917969 88.52197266 C-1.6887866 77.91909315 -10.95597826 65.95656876 -16 50 C-16.4125 48.7625 -16.825 47.525 -17.25 46.25 C-19.92772011 34.34631695 -18.39209844 21.70178399 -12.3515625 11.109375 C-4.91282895 0 -4.91282895 0 0 0 Z " fill="#79AFEB" transform="translate(103,64)"/>
<path d="M0 0 C-0.42954381 1.3983022 -0.86907034 2.79353907 -1.3125 4.1875 C-1.55613281 4.96480469 -1.79976562 5.74210937 -2.05078125 6.54296875 C-3.87971163 11.27711365 -6.67096194 14.91291499 -9.9375 18.75 C-16.72958324 27.14022047 -20.11560508 36.06308588 -19 47 C-17.21558238 54.72810276 -14.24239154 60.64225254 -7.625 65.25 C-1.74362035 68.9878388 -1.74362035 68.9878388 5.015625 69.8046875 C7.86441607 69.65527538 9.37848936 69.78974988 12 71 C16.09718736 75.3958468 19.01252254 80.82170573 22 86 C22.99935816 87.6670516 23.99905049 89.3339034 25 91 C5.62249954 91.36484834 -12.69092394 90.61101563 -28 77 C-36.44050971 67.52265225 -39.76608235 57.3077398 -39.3828125 44.7109375 C-38.14502828 32.71200883 -32.24783597 21.70604907 -24 13 C-23.34 13 -22.68 13 -22 13 C-22 12.34 -22 11.68 -22 11 C-16.32431209 6.09717172 -7.83025539 0 0 0 Z " fill="#73A7EA" transform="translate(39,70)"/>
<path d="M0 0 C-2.12910166 2.33471584 -4.07517335 3.68146181 -6.9375 5 C-9.90732447 6.41679699 -12.38725285 7.9901945 -15 10 C-15.66 10 -16.32 10 -17 10 C-17 10.66 -17 11.32 -17 12 C-18.12525155 13.02649277 -19.30282645 13.99587696 -20.5 14.9375 C-33.49161609 26.0469385 -41.59305065 43.11799329 -43.1953125 60.06591797 C-43.714182 69.16559176 -42.71323311 78.06671309 -41 87 C-46.81627942 82.15310048 -49.61443677 75.38967056 -51 68 C-51.69814606 57.29890869 -51.08055936 48.06628054 -47 38 C-46.67257813 37.15308594 -46.34515625 36.30617187 -46.0078125 35.43359375 C-41.86810307 26.02104276 -34.29231204 18.10356984 -27 11 C-27.433125 10.51789062 -27.86625 10.03578125 -28.3125 9.5390625 C-28.869375 8.90742187 -29.42625 8.27578125 -30 7.625 C-30.556875 6.99851562 -31.11375 6.37203125 -31.6875 5.7265625 C-33 4 -33 4 -33 2 C-27.87373026 1.19944082 -22.8041505 0.72131302 -17.625 0.4375 C-16.8812915 0.3950415 -16.13758301 0.35258301 -15.37133789 0.30883789 C-10.24014323 0.03021845 -5.13944387 -0.09344443 0 0 Z " fill="#71A4EA" transform="translate(88,39)"/>
</svg>

After

Width:  |  Height:  |  Size: 5.5 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-B_74ZyCB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cg0yCJnh.css">
<script type="module" crossorigin src="/assets/index-BLKGMHXS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BmYeXvQX.css">
</head>
<body class="bg-background">
<div id="root"></div>

30
ui/package-lock.json generated
View File

@@ -20,6 +20,7 @@
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2",
"class-variance-authority": "^0.7.0",
@@ -1762,6 +1763,35 @@
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.0.tgz",
"integrity": "sha512-bZgOKB/LtZIij75FSuPzyEti/XBhJH52ExgtdVqjCIh+Nx/FW+LhnbXtbCzIi34ccyMsyOja8T0thCzoHFXNKA==",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-presence": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-roving-focus": "1.1.0",
"@radix-ui/react-use-controllable-state": "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-toast": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-toast/-/react-toast-1.2.1.tgz",

View File

@@ -22,6 +22,7 @@
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2",
"class-variance-authority": "^0.7.0",

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<path d="M0 0 C8.00019718 6.13887963 14.34445574 12.99622218 18.3125 22.3125 C15.06464319 20.86413142 12.72723872 19.02613433 10.0625 16.6875 C-1.60459795 7.2541616 -16.18206674 0.63880608 -31.375 1.4375 C-43.09356312 2.78801868 -53.73683191 6.67588577 -62 15.5 C-71.51641841 27.69982709 -72.77906448 40.43807508 -71.6875 55.3125 C-69.75496452 69.19218976 -63.07550701 81.31999767 -54.6875 92.3125 C-53.96433594 93.26769531 -53.96433594 93.26769531 -53.2265625 94.2421875 C-49.80310028 98.49787258 -45.86843357 101.64242084 -41.4453125 104.83203125 C-40.86523438 105.32058594 -40.28515625 105.80914062 -39.6875 106.3125 C-39.6875 106.9725 -39.6875 107.6325 -39.6875 108.3125 C-45.21884376 108.61097445 -50.74543637 108.82766089 -56.28295898 108.97167969 C-58.1623705 109.03173819 -60.04122791 109.11342379 -61.91870117 109.21777344 C-77.40966514 110.05667884 -77.40966514 110.05667884 -82.80957031 105.921875 C-85.19168462 102.82857874 -86.97025266 99.81402723 -88.6875 96.3125 C-89.16856201 95.46268555 -89.64962402 94.61287109 -90.14526367 93.73730469 C-91.39695086 91.46566255 -92.49925956 89.17751393 -93.5625 86.8125 C-93.93689209 85.9877417 -94.31128418 85.1629834 -94.69702148 84.31323242 C-101.84695145 68.18991103 -104.37476618 48.01876725 -97.94140625 31.1171875 C-97.52761719 30.19164062 -97.11382812 29.26609375 -96.6875 28.3125 C-96.26339844 27.30445313 -95.83929688 26.29640625 -95.40234375 25.2578125 C-88.42098361 9.9727176 -77.15139172 -1.27183748 -61.6875 -7.6875 C-40.67179933 -14.71095263 -18.47073623 -12.74198216 0 0 Z " fill="#70A2EA" transform="translate(151.6875,52.6875)"/>
<path d="M0 0 C-0.19335938 0.56847656 -0.38671875 1.13695313 -0.5859375 1.72265625 C-1.13131792 3.33989819 -1.66856703 4.95989684 -2.19921875 6.58203125 C-2.82868314 8.48270666 -3.50036013 10.36940667 -4.1875 12.25 C-7.48441131 27.46651373 -2.93859997 42.92249193 5 56 C10.27285559 63.85713427 17.42266677 71.71133339 26 76 C28.93108041 76.12365056 31.83742448 76.18844664 34.76953125 76.203125 C35.64268478 76.20882507 36.51583832 76.21452515 37.41545105 76.22039795 C39.26564317 76.22985345 41.11585218 76.23638411 42.96606445 76.24023438 C45.79082844 76.24992565 48.61486044 76.28093755 51.43945312 76.3125 C53.23697485 76.31903152 55.03450177 76.32428141 56.83203125 76.328125 C58.0963372 76.3466452 58.0963372 76.3466452 59.38618469 76.36553955 C64.08745075 76.34777001 67.19523771 76.06753063 71 73 C76.14974577 66.13367231 76.85747523 60.40719967 76 52 C72.94347249 40.85669565 66.57825718 33.02825174 57.07421875 26.66796875 C41.98336589 18.48017438 25.32550226 16.19330935 8.4375 15.4375 C7.52419922 15.39431641 6.61089844 15.35113281 5.66992188 15.30664062 C3.44672009 15.20188243 1.22343293 15.09970983 -1 15 C-1 14.34 -1 13.68 -1 13 C24.14499626 7.59108915 50.41887378 8.13666192 72.734375 22.42578125 C84.76471086 30.78479048 94.05571707 43.44065596 97 58 C97.83992741 68.1617447 96.68764977 76.53638183 90.375 84.75 C82.2520582 93.4708233 71.29646331 96.75137499 59.70800781 97.22460938 C55.46822317 97.34944357 51.22762628 97.41269989 46.98632812 97.45996094 C45.16900252 97.48655278 43.35182742 97.52746211 41.53515625 97.58300781 C30.19480451 97.9245351 21.92945424 97.88338566 13 90 C12.08879395 89.26837646 12.08879395 89.26837646 11.15917969 88.52197266 C-1.6887866 77.91909315 -10.95597826 65.95656876 -16 50 C-16.4125 48.7625 -16.825 47.525 -17.25 46.25 C-19.92772011 34.34631695 -18.39209844 21.70178399 -12.3515625 11.109375 C-4.91282895 0 -4.91282895 0 0 0 Z " fill="#79AFEB" transform="translate(103,64)"/>
<path d="M0 0 C-0.42954381 1.3983022 -0.86907034 2.79353907 -1.3125 4.1875 C-1.55613281 4.96480469 -1.79976562 5.74210937 -2.05078125 6.54296875 C-3.87971163 11.27711365 -6.67096194 14.91291499 -9.9375 18.75 C-16.72958324 27.14022047 -20.11560508 36.06308588 -19 47 C-17.21558238 54.72810276 -14.24239154 60.64225254 -7.625 65.25 C-1.74362035 68.9878388 -1.74362035 68.9878388 5.015625 69.8046875 C7.86441607 69.65527538 9.37848936 69.78974988 12 71 C16.09718736 75.3958468 19.01252254 80.82170573 22 86 C22.99935816 87.6670516 23.99905049 89.3339034 25 91 C5.62249954 91.36484834 -12.69092394 90.61101563 -28 77 C-36.44050971 67.52265225 -39.76608235 57.3077398 -39.3828125 44.7109375 C-38.14502828 32.71200883 -32.24783597 21.70604907 -24 13 C-23.34 13 -22.68 13 -22 13 C-22 12.34 -22 11.68 -22 11 C-16.32431209 6.09717172 -7.83025539 0 0 0 Z " fill="#73A7EA" transform="translate(39,70)"/>
<path d="M0 0 C-2.12910166 2.33471584 -4.07517335 3.68146181 -6.9375 5 C-9.90732447 6.41679699 -12.38725285 7.9901945 -15 10 C-15.66 10 -16.32 10 -17 10 C-17 10.66 -17 11.32 -17 12 C-18.12525155 13.02649277 -19.30282645 13.99587696 -20.5 14.9375 C-33.49161609 26.0469385 -41.59305065 43.11799329 -43.1953125 60.06591797 C-43.714182 69.16559176 -42.71323311 78.06671309 -41 87 C-46.81627942 82.15310048 -49.61443677 75.38967056 -51 68 C-51.69814606 57.29890869 -51.08055936 48.06628054 -47 38 C-46.67257813 37.15308594 -46.34515625 36.30617187 -46.0078125 35.43359375 C-41.86810307 26.02104276 -34.29231204 18.10356984 -27 11 C-27.433125 10.51789062 -27.86625 10.03578125 -28.3125 9.5390625 C-28.869375 8.90742187 -29.42625 8.27578125 -30 7.625 C-30.556875 6.99851562 -31.11375 6.37203125 -31.6875 5.7265625 C-33 4 -33 4 -33 2 C-27.87373026 1.19944082 -22.8041505 0.72131302 -17.625 0.4375 C-16.8812915 0.3950415 -16.13758301 0.35258301 -15.37133789 0.30883789 C-10.24014323 0.03021845 -5.13944387 -0.09344443 0 0 Z " fill="#71A4EA" transform="translate(88,39)"/>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1,120 @@
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/access_group";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
import { useState } from "react";
type AccessGroupEditProps = {
className?: string;
trigger: React.ReactNode;
};
const AccessGroupEdit = ({ className, trigger }: AccessGroupEditProps) => {
const { reloadAccessGroups } = useConfig();
const [open, setOpen] = useState(false);
const formSchema = z.object({
name: z.string().min(1).max(64),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
try {
await update({
name: data.name,
});
// 更新本地状态
reloadAccessGroups();
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="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入组名" {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
</div>
</DialogContent>
</Dialog>
);
};
export default AccessGroupEdit;

View File

@@ -0,0 +1,217 @@
import AccessGroupEdit from "@/components/certimate/AccessGroupEdit";
import Show from "@/components/Show";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
import { getProviderInfo } from "@/domain/access";
import { getErrMessage } from "@/lib/error";
import { useConfig } from "@/providers/config";
import { remove } from "@/repository/access_group";
import { Group } from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
import { useNavigate } from "react-router-dom";
const AccessGroupList = () => {
const {
config: { accessGroups },
reloadAccessGroups,
} = useConfig();
const { toast } = useToast();
const navigate = useNavigate();
const handleRemoveClick = async (id: string) => {
try {
await remove(id);
reloadAccessGroups();
} catch (e) {
toast({
title: "删除失败",
description: getErrMessage(e),
variant: "destructive",
});
return;
}
};
const handleAddAccess = () => {
navigate("/access");
};
return (
<div className="mt-10">
<Show when={accessGroups.length == 0}>
<>
<div className="flex flex-col items-center mt-10">
<span className="bg-orange-100 p-5 rounded-full">
<Group size={40} className="text-primary" />
</span>
<div className="text-center text-sm text-muted-foreground mt-3">
</div>
<AccessGroupEdit
trigger={<Button></Button>}
className="mt-3"
/>
</div>
</>
</Show>
<ScrollArea className="h-[75vh] overflow-hidden">
<div className="flex gap-5 flex-wrap">
{accessGroups.map((accessGroup) => (
<Card className="w-full md:w-[350px]">
<CardHeader>
<CardTitle>{accessGroup.name}</CardTitle>
<CardDescription>
{accessGroup.expand ? accessGroup.expand.access.length : 0}
</CardDescription>
</CardHeader>
<CardContent className="min-h-[180px]">
{accessGroup.expand ? (
<>
{accessGroup.expand.access.slice(0, 3).map((access) => (
<div key={access.id} className="flex flex-col mb-3">
<div className="flex items-center">
<div className="">
<img
src={getProviderInfo(access.configType)![1]}
alt="provider"
className="w-8 h-8"
></img>
</div>
<div className="ml-3">
<div className="text-sm font-semibold text-gray-700 dark:text-gray-200">
{access.name}
</div>
<div className="text-xs text-muted-foreground">
{getProviderInfo(access.configType)![0]}
</div>
</div>
</div>
</div>
))}
</>
) : (
<>
<div className="flex text-gray-700 dark:text-gray-200 items-center">
<div>
<Group size={40} />
</div>
<div className="ml-2">
使
</div>
</div>
</>
)}
</CardContent>
<CardFooter>
<div className="flex justify-end w-full">
<Show
when={
accessGroup.expand && accessGroup.expand.access.length > 0
? true
: false
}
>
<div>
<Button
size="sm"
variant={"link"}
onClick={() => {
navigate(
`/access?accessGroupId=${accessGroup.id}&tab=access`,
{
replace: true,
}
);
}}
>
</Button>
</div>
</Show>
<Show
when={
!accessGroup.expand ||
accessGroup.expand.access.length == 0
? true
: false
}
>
<div>
<Button size="sm" onClick={handleAddAccess}>
</Button>
</div>
</Show>
<div className="ml-3">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"destructive"} size={"sm"}>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="dark:text-gray-200">
</AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="dark:text-gray-200">
</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleRemoveClick(
accessGroup.id ? accessGroup.id : ""
);
}}
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</CardFooter>
</Card>
))}
</div>
</ScrollArea>
</div>
);
};
export default AccessGroupList;

View File

@@ -1,4 +1,9 @@
import { Access, accessFormType, getUsageByConfigType, 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";
@@ -19,6 +24,17 @@ import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
import { readFileContent } from "@/lib/file";
import { useRef, useState } from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../ui/select";
import { cn } from "@/lib/utils";
import AccessGroupEdit from "./AccessGroupEdit";
import { Plus } from "lucide-react";
import { updateById } from "@/repository/access_group";
const AccessSSHForm = ({
data,
@@ -27,12 +43,19 @@ const AccessSSHForm = ({
data?: Access;
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const {
addAccess,
updateAccess,
reloadAccessGroups,
config: { accessGroups },
} = useConfig();
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [fileName, setFileName] = useState("");
const originGroup = data ? (data.group ? data.group : "") : "";
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
@@ -40,6 +63,7 @@ const AccessSSHForm = ({
host: z.string().ip({
message: "请输入合法的IP地址",
}),
group: z.string().optional(),
port: z.string().min(1).max(5),
username: z.string().min(1).max(64),
password: z.string().min(0).max(64),
@@ -69,6 +93,7 @@ const AccessSSHForm = ({
id: data?.id,
name: data?.name,
configType: "ssh",
group: data?.group,
host: config.host,
port: config.port,
username: config.username,
@@ -83,11 +108,15 @@ const AccessSSHForm = ({
const onSubmit = async (data: z.infer<typeof formSchema>) => {
console.log(data);
let group = data.group;
if (group == "emptyId") group = "";
const req: Access = {
id: data.id as string,
name: data.name,
configType: data.configType,
usage: getUsageByConfigType(data.configType),
group: group,
config: {
host: data.host,
port: data.port,
@@ -110,9 +139,28 @@ const AccessSSHForm = ({
req.updated = rs.updated;
if (data.id) {
updateAccess(req);
return;
} else {
addAccess(req);
}
addAccess(req);
// 同步更新授权组
if (group != originGroup) {
if (originGroup) {
await updateById({
id: originGroup,
"access-": req.id,
});
}
if (group) {
await updateById({
id: group,
"access+": req.id,
});
}
}
reloadAccessGroups();
} catch (e) {
const err = e as ClientResponseError;
@@ -172,6 +220,67 @@ const AccessSSHForm = ({
)}
/>
<FormField
control={form.control}
name="group"
render={({ field }) => (
<FormItem>
<FormLabel className="w-full flex justify-between">
<div>( ssh )</div>
<AccessGroupEdit
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}
defaultValue="emptyId"
onValueChange={(value) => {
form.setValue("group", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择分组" />
</SelectTrigger>
<SelectContent>
<SelectItem value="emptyId">
<div
className={cn(
"flex items-center space-x-2 rounded cursor-pointer"
)}
>
--
</div>
</SelectItem>
{accessGroups.map((item) => (
<SelectItem
value={item.id ? item.id : ""}
key={item.id}
>
<div
className={cn(
"flex items-center space-x-2 rounded cursor-pointer"
)}
>
{item.name}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="id"

View File

@@ -0,0 +1,49 @@
import { Deployment } from "@/domain/deployment";
import { CircleCheck, CircleX } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
type DeployStateProps = {
deployment: Deployment;
};
const DeployState = ({ deployment }: DeployStateProps) => {
// 获取指定阶段的错误信息
const error = (state: "check" | "apply" | "deploy") => {
if (!deployment.log[state]) {
return "";
}
return deployment.log[state][deployment.log[state].length - 1].error;
};
return (
<>
{deployment.phase === "deploy" && deployment.phaseSuccess ? (
<CircleCheck size={16} className="text-green-700" />
) : (
<>
{error(deployment.phase).length ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild className="cursor-pointer">
<CircleX size={16} className="text-red-700" />
</TooltipTrigger>
<TooltipContent className="max-w-[35em]">
{error(deployment.phase)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<CircleX size={16} className="text-red-700" />
)}
</>
)}
</>
);
};
export default DeployState;

View File

@@ -0,0 +1,31 @@
import { BookOpen } from "lucide-react";
import { Separator } from "../ui/separator";
import { version } from "@/domain/version";
const Version = () => {
return (
<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"
>
{version}
</a>
</div>
</div>
);
};
export default Version;

View File

@@ -0,0 +1,53 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -11,6 +11,10 @@ export const accessTypeMap: Map<string, [string, string]> = new Map([
["webhook", ["Webhook", "/imgs/providers/webhook.svg"]],
]);
export const getProviderInfo = (t: string) => {
return accessTypeMap.get(t);
};
export const accessFormType = z.union(
[
z.literal("aliyun"),
@@ -32,6 +36,7 @@ export type Access = {
name: string;
configType: string;
usage: AccessUsage;
group?: string;
config:
| TencentConfig
| AliyunConfig

View File

@@ -0,0 +1,10 @@
import { Access } from "./access";
export type AccessGroup = {
id?: string;
name?: string;
access?: string[];
expand?: {
access: Access[];
};
};

View File

@@ -6,13 +6,17 @@ export type Domain = {
email?: string;
crontab: string;
access: string;
targetAccess: string;
targetAccess?: string;
targetType: string;
expiredAt?: string;
phase?: Pahse;
phaseSuccess?: boolean;
lastDeployedAt?: string;
variables?: string;
nameservers?: string;
group?: string;
enabled?: boolean;
deployed?: boolean;
created?: string;
updated?: string;
deleted?: string;
@@ -38,6 +42,7 @@ export const getLastDeployment = (domain: Domain): Deployment | undefined => {
export const targetTypeMap: Map<string, [string, string]> = new Map([
["aliyun-cdn", ["阿里云-CDN", "/imgs/providers/aliyun.svg"]],
["aliyun-oss", ["阿里云-OSS", "/imgs/providers/aliyun.svg"]],
["aliyun-dcdn", ["阿里云-DCDN", "/imgs/providers/aliyun.svg"]],
["tencent-cdn", ["腾讯云-CDN", "/imgs/providers/tencent.svg"]],
["ssh", ["SSH部署", "/imgs/providers/ssh.svg"]],
["qiniu-cdn", ["七牛云-CDN", "/imgs/providers/qiniu.svg"]],

1
ui/src/domain/version.ts Normal file
View File

@@ -0,0 +1 @@
export const version = "Certimate v0.1.9";

View File

@@ -7,7 +7,7 @@ import { ThemeProvider } from "./components/ThemeProvider.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<RouterProvider router={router} />
</ThemeProvider>
</React.StrictMode>

View File

@@ -5,15 +5,7 @@ import {
useLocation,
useNavigate,
} from "react-router-dom";
import {
BookOpen,
CircleUser,
Earth,
History,
Home,
Menu,
Server,
} from "lucide-react";
import { CircleUser, Earth, History, Home, Menu, Server } from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -30,7 +22,8 @@ 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";
import Version from "@/components/certimate/Version";
export default function Dashboard() {
const navigate = useNavigate();
@@ -53,12 +46,12 @@ export default function Dashboard() {
};
const handleSettingClick = () => {
navigate("/setting/password");
navigate("/setting/account");
};
return (
<>
<ConfigProvider>
<div className="grid min-h-screen w-full md:grid-cols-[220px_1fr] lg:grid-cols-[280px_1fr]">
<div className="grid min-h-screen w-full md:grid-cols-[180px_1fr] lg:grid-cols-[200px_1fr] 2xl:md:grid-cols-[280px_1fr] ">
<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 dark:border-stone-500 px-4 lg:h-[60px] lg:px-6">
@@ -161,7 +154,7 @@ export default function Dashboard() {
to="/access"
className={cn(
"mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground",
getClass("/dns_provider")
getClass("/access")
)}
>
<Server className="h-5 w-5" />
@@ -199,7 +192,7 @@ export default function Dashboard() {
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleSettingClick}>
</DropdownMenuItem>
<DropdownMenuItem onClick={handleLogoutClick}>
@@ -211,26 +204,7 @@ export default function Dashboard() {
<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.5
</a>
</div>
</div>
<Version />
</main>
</div>
</div>

View File

@@ -1,4 +1,7 @@
import Version from "@/components/certimate/Version";
import { getPb } from "@/repository/api";
import { Navigate, Outlet } from "react-router-dom";
const LoginLayout = () => {
@@ -8,6 +11,8 @@ const LoginLayout = () => {
return (
<div className="container">
<Outlet />
<Version />
</div>
);
};

View File

@@ -1,20 +1,57 @@
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Toaster } from "@/components/ui/toaster";
import { Outlet } from "react-router-dom";
import { KeyRound, UserRound } from "lucide-react";
import { useEffect, useState } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
const SettingLayout = () => {
const location = useLocation();
const [tabValue, setTabValue] = useState("account");
const navigate = useNavigate();
useEffect(() => {
const pathname = location.pathname;
const tabValue = pathname.split("/")[2];
setTabValue(tabValue);
}, [location]);
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 className="w-full mt-5 p-3 flex justify-center">
<Tabs defaultValue="account" className="" value={tabValue}>
<TabsList>
<TabsTrigger
value="account"
onClick={() => {
navigate("/setting/account");
}}
className="px-5"
>
<UserRound size={14} />
<div className="ml-1"></div>
</TabsTrigger>
<TabsTrigger
value="password"
onClick={() => {
navigate("/setting/password");
}}
className="px-5"
>
<KeyRound size={14} />
<div className="ml-1"></div>
</TabsTrigger>
</TabsList>
<TabsContent value={tabValue}>
<div className="mt-5 w-full md:w-[45em]">
<Outlet />
</div>
</TabsContent>
</Tabs>
</div>
</div>
);

View File

@@ -1,7 +1,10 @@
import { AccessEdit } from "@/components/certimate/AccessEdit";
import AccessGroupEdit from "@/components/certimate/AccessGroupEdit";
import AccessGroupList from "@/components/certimate/AccessGroupList";
import XPagination from "@/components/certimate/XPagination";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Access as AccessType, accessTypeMap } from "@/domain/access";
import { convertZulu2Beijing } from "@/lib/time";
import { useConfig } from "@/providers/config";
@@ -23,6 +26,10 @@ const Access = () => {
const page = query.get("page");
const pageNumber = page ? Number(page) : 1;
const tab = query.get("tab");
const accessGroupId = query.get("accessGroupId");
const startIndex = (pageNumber - 1) * perPage;
const endIndex = startIndex + perPage;
@@ -31,95 +38,141 @@ const Access = () => {
deleteAccess(rs.id);
};
const handleTabItemClick = (tab: string) => {
query.set("tab", tab);
navigate({ search: query.toString() });
};
return (
<div className="">
<div className="flex justify-between items-center">
<div className="text-muted-foreground"></div>
<AccessEdit trigger={<Button></Button>} op="add" />
{tab != "access_group" ? (
<AccessEdit trigger={<Button></Button>} op="add" />
) : (
<AccessGroupEdit trigger={<Button></Button>} />
)}
</div>
{accesses.length === 0 ? (
<div className="flex flex-col items-center mt-10">
<span className="bg-orange-100 p-5 rounded-full">
<Key size={40} className="text-primary" />
</span>
<div className="text-center text-sm text-muted-foreground mt-3">
</div>
<AccessEdit
trigger={<Button></Button>}
op="add"
className="mt-3"
/>
</div>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48"></div>
<div className="w-48"></div>
<div className="w-52"></div>
<div className="w-52"></div>
<div className="grow"></div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
</div>
{accesses.slice(startIndex, endIndex).map((access) => (
<div
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">
{access.name}
</div>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center space-x-2">
<img
src={accessTypeMap.get(access.configType)?.[1]}
className="w-6"
/>
<div>{accessTypeMap.get(access.configType)?.[0]}</div>
</div>
<div className="sm:w-52 w-full pt-1 sm:pt-0 flex items-center">
{access.created && convertZulu2Beijing(access.created)}
</div>
<div className="sm:w-52 w-full pt-1 sm:pt-0 flex items-center">
{access.updated && convertZulu2Beijing(access.updated)}
</div>
<div className="flex items-center grow justify-start pt-1 sm:pt-0">
<AccessEdit
trigger={
<Button variant={"link"} className="p-0">
</Button>
}
op="edit"
data={access}
/>
<Separator orientation="vertical" className="h-4 mx-2" />
<Button
variant={"link"}
className="p-0"
onClick={() => {
handleDelete(access);
}}
>
</Button>
</div>
</div>
))}
<XPagination
totalPages={totalPages}
currentPage={pageNumber}
onPageChange={(page) => {
query.set("page", page.toString());
navigate({ search: query.toString() });
<Tabs
defaultValue={tab ? tab : "access"}
value={tab ? tab : "access"}
className="w-full mt-5"
>
<TabsList className="space-x-5 px-3">
<TabsTrigger
value="access"
onClick={() => {
handleTabItemClick("access");
}}
/>
</>
)}
>
</TabsTrigger>
<TabsTrigger
value="access_group"
onClick={() => {
handleTabItemClick("access_group");
}}
>
</TabsTrigger>
</TabsList>
<TabsContent value="access">
{accesses.length === 0 ? (
<div className="flex flex-col items-center mt-10">
<span className="bg-orange-100 p-5 rounded-full">
<Key size={40} className="text-primary" />
</span>
<div className="text-center text-sm text-muted-foreground mt-3">
</div>
<AccessEdit
trigger={<Button></Button>}
op="add"
className="mt-3"
/>
</div>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48"></div>
<div className="w-48"></div>
<div className="w-52"></div>
<div className="w-52"></div>
<div className="grow"></div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
</div>
{accesses
.filter((item) => {
return accessGroupId ? item.group == accessGroupId : true;
})
.slice(startIndex, endIndex)
.map((access) => (
<div
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">
{access.name}
</div>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center space-x-2">
<img
src={accessTypeMap.get(access.configType)?.[1]}
className="w-6"
/>
<div>{accessTypeMap.get(access.configType)?.[0]}</div>
</div>
<div className="sm:w-52 w-full pt-1 sm:pt-0 flex items-center">
{" "}
{access.created && convertZulu2Beijing(access.created)}
</div>
<div className="sm:w-52 w-full pt-1 sm:pt-0 flex items-center">
{" "}
{access.updated && convertZulu2Beijing(access.updated)}
</div>
<div className="flex items-center grow justify-start pt-1 sm:pt-0">
<AccessEdit
trigger={
<Button variant={"link"} className="p-0">
</Button>
}
op="edit"
data={access}
/>
<Separator orientation="vertical" className="h-4 mx-2" />
<Button
variant={"link"}
className="p-0"
onClick={() => {
handleDelete(access);
}}
>
</Button>
</div>
</div>
))}
<XPagination
totalPages={totalPages}
currentPage={pageNumber}
onPageChange={(page) => {
query.set("page", page.toString());
navigate({ search: query.toString() });
}}
/>
</>
)}
</TabsContent>
<TabsContent value="access_group">
<AccessGroupList />
</TabsContent>
</Tabs>
</div>
);
};

View File

@@ -1,4 +1,5 @@
import DeployProgress from "@/components/certimate/DeployProgress";
import DeployState from "@/components/certimate/DeployState";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
@@ -17,8 +18,6 @@ import { statistics } from "@/repository/domains";
import {
Ban,
CalendarX2,
CircleCheck,
CircleX,
LoaderPinwheel,
Smile,
SquareSigma,
@@ -204,11 +203,7 @@ const Dashboard = () => {
{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" />
)}
<DeployState deployment={deployment} />
</div>
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center">
<DeployProgress

View File

@@ -35,16 +35,22 @@ import { Plus } from "lucide-react";
import { AccessEdit } from "@/components/certimate/AccessEdit";
import { accessTypeMap } from "@/domain/access";
import EmailsEdit from "@/components/certimate/EmailsEdit";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
const Edit = () => {
const {
config: { accesses, emails },
config: { accesses, emails, accessGroups },
} = useConfig();
const [domain, setDomain] = useState<Domain>();
const location = useLocation();
const [tab, setTab] = useState<"base" | "advance">("base");
const [targetType, setTargetType] = useState(domain ? domain.targetType : "");
useEffect(() => {
// Parsing query parameters
const queryParams = new URLSearchParams(location.search);
@@ -53,6 +59,7 @@ const Edit = () => {
const fetchData = async () => {
const data = await get(id);
setDomain(data);
setTargetType(data.targetType);
};
fetchData();
}
@@ -67,12 +74,13 @@ const Edit = () => {
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
message: "请选择DNS服务商授权配置",
}),
targetAccess: z.string().regex(/^[a-zA-Z0-9]+$/, {
message: "请选择部署服务商配置",
}),
targetAccess: z.string().optional(),
targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, {
message: "请选择部署服务类型",
}),
variables: z.string().optional(),
group: z.string().optional(),
nameservers: z.string().optional(),
});
const form = useForm<z.infer<typeof formSchema>>({
@@ -84,6 +92,9 @@ const Edit = () => {
access: "",
targetAccess: "",
targetType: "",
variables: "",
group: "",
nameservers: "",
},
});
@@ -96,12 +107,13 @@ const Edit = () => {
access: domain.access,
targetAccess: domain.targetAccess,
targetType: domain.targetType,
variables: domain.variables,
group: domain.group,
nameservers: domain.nameservers,
});
}
}, [domain, form]);
const [targetType, setTargetType] = useState(domain ? domain.targetType : "");
const targetAccesses = accesses.filter((item) => {
if (item.usage == "apply") {
return false;
@@ -110,7 +122,7 @@ const Edit = () => {
if (targetType == "") {
return true;
}
const types = form.getValues().targetType.split("-");
const types = targetType.split("-");
return item.configType === types[0];
});
@@ -119,14 +131,32 @@ const Edit = () => {
const navigate = useNavigate();
const onSubmit = async (data: z.infer<typeof formSchema>) => {
const group = data.group == "emptyId" ? "" : data.group;
const targetAccess =
data.targetAccess === "emptyId" ? "" : data.targetAccess;
if (group == "" && targetAccess == "") {
form.setError("group", {
type: "manual",
message: "部署授权和部署授权组至少选一个",
});
form.setError("targetAccess", {
type: "manual",
message: "部署授权和部署授权组至少选一个",
});
return;
}
const req: Domain = {
id: data.id as string,
crontab: "0 0 * * *",
domain: data.domain,
email: data.email,
access: data.access,
targetAccess: data.targetAccess,
group: group,
targetAccess: targetAccess,
targetType: data.targetType,
variables: data.variables,
nameservers: data.nameservers,
};
try {
@@ -161,109 +191,234 @@ const Edit = () => {
<>
<div className="">
<Toaster />
<div className="border-b dark:border-stone-500 h-10 text-muted-foreground">
<div className=" h-5 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 dark:text-stone-200"
<div className="mt-5 flex w-full justify-center md:space-x-10 flex-col md:flex-row">
<div className="w-full md:w-[200px] text-muted-foreground space-x-3 md:space-y-3 flex-row md:flex-col flex">
<div
className={cn(
"cursor-pointer text-right",
tab === "base" ? "text-primary" : ""
)}
onClick={() => {
setTab("base");
}}
>
<FormField
control={form.control}
name="domain"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入域名" {...field} />
</FormControl>
</div>
<div
className={cn(
"cursor-pointer text-right",
tab === "advance" ? "text-primary" : ""
)}
onClick={() => {
setTab("advance");
}}
>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
<div className="w-full md:w-[35em] bg-gray-100 dark:bg-gray-900 p-5 rounded mt-3 md:mt-0">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 dark:text-stone-200"
>
<FormField
control={form.control}
name="domain"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="请输入域名" {...field} />
</FormControl>
<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>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel className="flex w-full justify-between">
<div>Email</div>
<EmailsEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
</div>
}
/>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("email", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择邮箱" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
{emails.content.emails.map((item) => (
<SelectItem key={item} value={item}>
<div>{item}</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="access"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel className="flex w-full justify-between">
<div>DNS </div>
<AccessEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
</div>
}
op="add"
/>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("access", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择授权配置" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
{accesses
.filter((item) => item.usage != "deploy")
.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img
className="w-6"
src={
accessTypeMap.get(
item.configType
)?.[1]
}
/>
<div>{item.name}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="targetType"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel></FormLabel>
<FormControl>
<Select
{...field}
onValueChange={(value) => {
setTargetType(value);
form.setValue("targetType", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择部署服务类型" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
{targetTypeKeys.map((key) => (
<SelectItem key={key} value={key}>
<div className="flex items-center space-x-2">
<img
className="w-6"
src={targetTypeMap.get(key)?.[1]}
/>
<div>{targetTypeMap.get(key)?.[0]}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="targetAccess"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel className="w-full flex justify-between">
<div></div>
<AccessEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
</div>
}
op="add"
/>
</FormLabel>
<FormControl>
<Select
{...field}
onValueChange={(value) => {
form.setValue("targetAccess", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择授权配置" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>
{form.getValues().targetAccess}
</SelectLabel>
<SelectItem value="emptyId">
<div className="flex items-center space-x-2">
--
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="access"
render={({ field }) => (
<FormItem>
<FormLabel className="flex w-full justify-between">
<div>DNS </div>
<AccessEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
</div>
}
op="add"
/>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
onValueChange={(value) => {
form.setValue("access", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择授权配置" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
{accesses
.filter((item) => item.usage != "deploy")
.map((item) => (
{targetAccesses.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img
@@ -276,115 +431,120 @@ const Edit = () => {
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="targetType"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Select
{...field}
onValueChange={(value) => {
setTargetType(value);
form.setValue("targetType", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择部署服务类型" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
{targetTypeKeys.map((key) => (
<SelectItem key={key} value={key}>
<div className="flex items-center space-x-2">
<img
className="w-6"
src={targetTypeMap.get(key)?.[1]}
/>
<div>{targetTypeMap.get(key)?.[0]}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormField
control={form.control}
name="group"
render={({ field }) => (
<FormItem hidden={tab != "advance" || targetType != "ssh"}>
<FormLabel className="w-full flex justify-between">
<div>
( ssh )
</div>
</FormLabel>
<FormControl>
<Select
{...field}
value={field.value}
defaultValue="emptyId"
onValueChange={(value) => {
form.setValue("group", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择分组" />
</SelectTrigger>
<SelectContent>
<SelectItem value="emptyId">
<div
className={cn(
"flex items-center space-x-2 rounded cursor-pointer"
)}
>
--
</div>
</SelectItem>
{accessGroups
.filter((item) => {
return (
item.expand && item.expand?.access.length > 0
);
})
.map((item) => (
<SelectItem
value={item.id ? item.id : ""}
key={item.id}
>
<div
className={cn(
"flex items-center space-x-2 rounded cursor-pointer"
)}
>
{item.name}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="variables"
render={({ field }) => (
<FormItem hidden={tab != "advance"}>
<FormLabel></FormLabel>
<FormControl>
<Textarea
placeholder={`可在SSH部署中使用,形如:\nkey=val;\nkey2=val2;`}
{...field}
className="placeholder:whitespace-pre-wrap"
/>
</FormControl>
<FormField
control={form.control}
name="targetAccess"
render={({ field }) => (
<FormItem>
<FormLabel className="w-full flex justify-between">
<div></div>
<AccessEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
</div>
}
op="add"
/>
</FormLabel>
<FormControl>
<Select
{...field}
onValueChange={(value) => {
form.setValue("targetAccess", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择授权配置" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
{targetAccesses.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img
className="w-6"
src={
accessTypeMap.get(item.configType)?.[1]
}
/>
<div>{item.name}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="nameservers"
render={({ field }) => (
<FormItem hidden={tab != "advance"}>
<FormLabel></FormLabel>
<FormControl>
<Textarea
placeholder={`自定义域名服务器,多个用分号隔开,如:\n8.8.8.8;\n8.8.4.4;`}
{...field}
className="placeholder:whitespace-pre-wrap"
/>
</FormControl>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
</div>
</div>
</div>
</>

View File

@@ -1,4 +1,5 @@
import DeployProgress from "@/components/certimate/DeployProgress";
import DeployState from "@/components/certimate/DeployState";
import XPagination from "@/components/certimate/XPagination";
import Show from "@/components/Show";
import {
@@ -31,7 +32,7 @@ import {
} from "@/repository/domains";
import { TooltipContent, TooltipProvider } from "@radix-ui/react-tooltip";
import { CircleCheck, CircleX, Earth } from "lucide-react";
import { Earth } from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
@@ -149,6 +150,10 @@ const Home = () => {
}
};
const handleForceClick = async (domain: Domain) => {
await handleRightNowClick({ ...domain, deployed: false });
};
const handleDownloadClick = async (domain: Domain) => {
const zipName = `${domain.id}-${domain.domain}.zip`;
const files: CustomFile[] = [
@@ -192,12 +197,12 @@ const Home = () => {
) : (
<>
<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-36"></div>
<div className="w-40"></div>
<div className="w-32"></div>
<div className="w-64"></div>
<div className="w-40 sm:ml-2"></div>
<div className="w-32"></div>
<div className="w-24"></div>
<div className="grow"></div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
@@ -209,10 +214,10 @@ const Home = () => {
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
key={domain.id}
>
<div className="sm:w-40 w-full pt-1 sm:pt-0 flex items-center">
<div className="sm:w-36 w-full pt-1 sm:pt-0 flex items-center">
{domain.domain}
</div>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center">
<div className="sm:w-40 w-full pt-1 sm:pt-0 flex items-center">
<div>
{domain.expiredAt ? (
<>
@@ -227,12 +232,7 @@ const Home = () => {
<div className="sm:w-32 w-full pt-1 sm:pt-0 flex items-center">
{domain.lastDeployedAt && domain.expand?.lastDeployment ? (
<>
{domain.expand.lastDeployment?.phase === "deploy" &&
domain.expand.lastDeployment?.phaseSuccess ? (
<CircleCheck size={16} className="text-green-700" />
) : (
<CircleX size={16} className="text-red-700" />
)}
<DeployState deployment={domain.expand.lastDeployment} />
</>
) : (
"---"
@@ -253,7 +253,7 @@ const Home = () => {
? convertZulu2Beijing(domain.lastDeployedAt)
: "---"}
</div>
<div className="sm:w-32 flex items-center">
<div className="sm:w-24 flex items-center">
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
@@ -291,6 +291,23 @@ const Home = () => {
</Button>
</Show>
<Show
when={
(domain.enabled ? true : false) && domain.deployed
? true
: false
}
>
<Separator orientation="vertical" className="h-4 mx-2" />
<Button
variant={"link"}
className="p-0"
onClick={() => handleForceClick(domain)}
>
</Button>
</Show>
<Show when={domain.expiredAt ? true : false}>
<Separator orientation="vertical" className="h-4 mx-2" />
<Button

View File

@@ -1,4 +1,5 @@
import DeployProgress from "@/components/certimate/DeployProgress";
import DeployState from "@/components/certimate/DeployState";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
@@ -13,7 +14,7 @@ import {
import { Deployment, DeploymentListReq, Log } from "@/domain/deployment";
import { convertZulu2Beijing } from "@/lib/time";
import { list } from "@/repository/deployment";
import { CircleCheck, CircleX, Smile } from "lucide-react";
import { Smile } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
@@ -88,11 +89,7 @@ const History = () => {
{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" />
)}
<DeployState deployment={deployment} />
</div>
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center">
<DeployProgress

View File

@@ -0,0 +1,109 @@
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 { useState } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { z } from "zod";
const formSchema = z.object({
email: z.string().email("请输入正确的邮箱"),
});
const Account = () => {
const { toast } = useToast();
const navigate = useNavigate();
const [changed, setChanged] = useState(false);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: getPb().authStore.model?.email,
},
});
const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
await getPb().admins.update(getPb().authStore.model?.id, {
email: values.email,
});
getPb().authStore.clear();
toast({
title: "修改账户邮箱功",
description: "请重新登录",
});
setTimeout(() => {
navigate("/login");
}, 500);
} catch (e) {
const message = getErrMessage(e);
toast({
title: "修改账户邮箱失败",
description: message,
variant: "destructive",
});
}
};
return (
<>
<div className="w-full md:max-w-[35em]">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 dark:text-stone-200"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
placeholder="请输入邮箱"
{...field}
type="email"
onChange={(e) => {
setChanged(true);
form.setValue("email", e.target.value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
{changed ? (
<Button type="submit"></Button>
) : (
<Button type="submit" disabled variant={"secondary"}>
</Button>
)}
</div>
</form>
</Form>
</div>
</>
);
};
export default Account;

View File

@@ -84,64 +84,70 @@ const Password = () => {
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>
<div className="w-full md:max-w-[35em]">
<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>
)}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="newPassword" {...field} type="password" />
</FormControl>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
placeholder="newPassword"
{...field}
type="password"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
placeholder="confirmPassword"
{...field}
type="password"
/>
</FormControl>
<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>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
</div>
</>
);
};

View File

@@ -1,6 +1,6 @@
import { Access } from "@/domain/access";
import { list } from "@/repository/access";
import { list as getAccessGroups } from "@/repository/access_group";
import {
createContext,
ReactNode,
@@ -12,10 +12,12 @@ import {
import { configReducer } from "./reducer";
import { getEmails } from "@/repository/settings";
import { Setting } from "@/domain/settings";
import { AccessGroup } from "@/domain/access_groups";
export type ConfigData = {
accesses: Access[];
emails: Setting;
accessGroups: AccessGroup[];
};
export type ConfigContext = {
@@ -24,6 +26,8 @@ export type ConfigContext = {
addAccess: (access: Access) => void;
updateAccess: (access: Access) => void;
setEmails: (email: Setting) => void;
setAccessGroups: (accessGroups: AccessGroup[]) => void;
reloadAccessGroups: () => void;
};
const Context = createContext({} as ConfigContext);
@@ -38,6 +42,7 @@ export const ConfigProvider = ({ children }: ContainerProps) => {
const [config, dispatchConfig] = useReducer(configReducer, {
accesses: [],
emails: { content: { emails: [] } },
accessGroups: [],
});
useEffect(() => {
@@ -56,6 +61,19 @@ export const ConfigProvider = ({ children }: ContainerProps) => {
featchEmails();
}, []);
useEffect(() => {
const featchAccessGroups = async () => {
const accessGroups = await getAccessGroups();
dispatchConfig({ type: "SET_ACCESS_GROUPS", payload: accessGroups });
};
featchAccessGroups();
}, []);
const reloadAccessGroups = useCallback(async () => {
const accessGroups = await getAccessGroups();
dispatchConfig({ type: "SET_ACCESS_GROUPS", payload: accessGroups });
}, []);
const setEmails = useCallback((emails: Setting) => {
dispatchConfig({ type: "SET_EMAILS", payload: emails });
}, []);
@@ -72,17 +90,24 @@ export const ConfigProvider = ({ children }: ContainerProps) => {
dispatchConfig({ type: "UPDATE_ACCESS", payload: access });
}, []);
const setAccessGroups = useCallback((accessGroups: AccessGroup[]) => {
dispatchConfig({ type: "SET_ACCESS_GROUPS", payload: accessGroups });
}, []);
return (
<Context.Provider
value={{
config: {
accesses: config.accesses,
emails: config.emails,
accessGroups: config.accessGroups,
},
deleteAccess,
addAccess,
setEmails,
updateAccess,
setAccessGroups,
reloadAccessGroups,
}}
>
{children && children}

View File

@@ -1,6 +1,7 @@
import { Access } from "@/domain/access";
import { ConfigData } from ".";
import { Setting } from "@/domain/settings";
import { AccessGroup } from "@/domain/access_groups";
type Action =
| { type: "ADD_ACCESS"; payload: Access }
@@ -8,7 +9,8 @@ type Action =
| { type: "UPDATE_ACCESS"; payload: Access }
| { type: "SET_ACCESSES"; payload: Access[] }
| { type: "SET_EMAILS"; payload: Setting }
| { type: "ADD_EMAIL"; payload: string };
| { type: "ADD_EMAIL"; payload: string }
| { type: "SET_ACCESS_GROUPS"; payload: AccessGroup[] };
export const configReducer = (
state: ConfigData,
@@ -60,6 +62,12 @@ export const configReducer = (
},
};
}
case "SET_ACCESS_GROUPS": {
return {
...state,
accessGroups: action.payload,
};
}
default:
return state;
}

View File

@@ -0,0 +1,49 @@
import { AccessGroup } from "@/domain/access_groups";
import { getPb } from "./api";
import { Access } from "@/domain/access";
export const list = async () => {
const resp = await getPb()
.collection("access_groups")
.getFullList<AccessGroup>({
sort: "-created",
expand: "access",
});
return resp;
};
export const remove = async (id: string) => {
const pb = getPb();
// 查询有没有关联的access
const accessGroup = await pb.collection("access").getList<Access>(1, 1, {
filter: `group='${id}' && deleted=null`,
});
if (accessGroup.items.length > 0) {
throw new Error("该分组下有授权配置,无法删除");
}
await pb.collection("access_groups").delete(id);
};
export const update = async (accessGroup: AccessGroup) => {
const pb = getPb();
if (accessGroup.id) {
return await pb
.collection("access_groups")
.update(accessGroup.id, accessGroup);
}
return await pb.collection("access_groups").create(accessGroup);
};
type UpdateByIdReq = {
id: string;
[key: string]: string | string[];
};
export const updateById = async (req: UpdateByIdReq) => {
const pb = getPb();
return await pb.collection("access_groups").update(req.id, req);
};

View File

@@ -10,6 +10,7 @@ import LoginLayout from "./pages/LoginLayout";
import Password from "./pages/setting/Password";
import SettingLayout from "./pages/SettingLayout";
import Dashboard from "./pages/dashboard/Dashboard";
import Account from "./pages/setting/Account";
export const router = createHashRouter([
{
@@ -44,6 +45,10 @@ export const router = createHashRouter([
path: "/setting/password",
element: <Password />,
},
{
path: "/setting/account",
element: <Account />,
},
],
},
],