Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac14924a73 | ||
|
|
7550aec904 | ||
|
|
139a6980ac | ||
|
|
d5a6411e26 | ||
|
|
1fcd9897be | ||
|
|
99d4bf2624 | ||
|
|
844347acf9 | ||
|
|
eeae9b4405 | ||
|
|
feef94f873 |
19
README.md
19
README.md
@@ -4,12 +4,12 @@
|
|||||||
|
|
||||||
做个人产品或在小企业负责运维的同学,需要管理多个域名,要给域名申请证书。但手动申请证书有以下缺点:
|
做个人产品或在小企业负责运维的同学,需要管理多个域名,要给域名申请证书。但手动申请证书有以下缺点:
|
||||||
|
|
||||||
1. 😱麻烦:申请、部署证书虽不复杂,但也挺麻烦的,尤其是你维护多个域名的时候。
|
1. 😱麻烦:申请、部署证书虽不困难,但也挺麻烦的,尤其是维护多个域名的时候。
|
||||||
2. 😭易忘:当前免费证书有效期仅90天,这就要求定期操作,增加工作量的同时,也很容易忘掉,导致网站无法访问。
|
2. 😭易忘:当前免费证书有效期仅90天,这就要求定期操作,增加工作量的同时,也很容易忘掉,导致网站无法访问。
|
||||||
|
|
||||||
Certimate 就是为了解决上述问题而产生的,它具有以下特点:
|
Certimate 就是为了解决上述问题而产生的,它具有以下特点:
|
||||||
|
|
||||||
1. 操作简单:自动申请、部署 SSL 证书,并在证书即将过期时自动续期,无需人工干预。
|
1. 操作简单:自动申请、部署、续期 SSL 证书,全程无需人工干预。
|
||||||
2. 支持私有部署:部署方法简单,只需下载二进制文件执行即可。二进制文件、docker 镜像全部用 github actions 生成,过程透明,可自行审计。
|
2. 支持私有部署:部署方法简单,只需下载二进制文件执行即可。二进制文件、docker 镜像全部用 github actions 生成,过程透明,可自行审计。
|
||||||
3. 数据安全:由于是私有部署,所有数据均存储在本地,不会保存在服务商的服务器,确保数据的安全性。
|
3. 数据安全:由于是私有部署,所有数据均存储在本地,不会保存在服务商的服务器,确保数据的安全性。
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
|
|||||||
- [2. dns 服务商授权信息](#2-dns-服务商授权信息)
|
- [2. dns 服务商授权信息](#2-dns-服务商授权信息)
|
||||||
- [3. 部署服务商授权信息](#3-部署服务商授权信息)
|
- [3. 部署服务商授权信息](#3-部署服务商授权信息)
|
||||||
- [六、常见问题](#六常见问题)
|
- [六、常见问题](#六常见问题)
|
||||||
- [七、许可证](#七许可证)
|
- [七、贡献](#七贡献)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -162,6 +162,15 @@ Q: 自动续期证书?
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 七、许可证
|
## 七、贡献
|
||||||
|
|
||||||
|
Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.md)。你可以使用它做任何你想做的事,甚至把它当作一个付费服务提供给用户。
|
||||||
|
|
||||||
|
你可以通过以下方式来支持 Certimate 的开发:
|
||||||
|
|
||||||
|
* 提交代码:如果你发现了 bug 或有新的功能需求,而你又有相关经验,可以提交代码给我们。
|
||||||
|
* 提交 issue:功能建议或者 bug 可以[提交 issue](https://github.com/usual2970/certimate/issues) 给我们。
|
||||||
|
|
||||||
|
支持更多服务商、UI 的优化改进、BUG 修复、文档完善等,欢迎大家提交 PR。
|
||||||
|
|
||||||
|
|
||||||
Certimate 采用 MIT 许可证,详情请查看 [LICENSE](LICENSE.md) 文件。
|
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ const (
|
|||||||
configTypeAliyun = "aliyun"
|
configTypeAliyun = "aliyun"
|
||||||
configTypeCloudflare = "cloudflare"
|
configTypeCloudflare = "cloudflare"
|
||||||
configTypeNamesilo = "namesilo"
|
configTypeNamesilo = "namesilo"
|
||||||
|
configTypeGodaddy = "godaddy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const defaultEmail = "536464346@qq.com"
|
||||||
|
|
||||||
type Certificate struct {
|
type Certificate struct {
|
||||||
CertUrl string `json:"certUrl"`
|
CertUrl string `json:"certUrl"`
|
||||||
CertStableUrl string `json:"certStableUrl"`
|
CertStableUrl string `json:"certStableUrl"`
|
||||||
@@ -59,8 +62,12 @@ type Applicant interface {
|
|||||||
|
|
||||||
func Get(record *models.Record) (Applicant, error) {
|
func Get(record *models.Record) (Applicant, error) {
|
||||||
access := record.ExpandedOne("access")
|
access := record.ExpandedOne("access")
|
||||||
|
email := record.GetString("email")
|
||||||
|
if email == "" {
|
||||||
|
email = defaultEmail
|
||||||
|
}
|
||||||
option := &ApplyOption{
|
option := &ApplyOption{
|
||||||
Email: "536464346@qq.com",
|
Email: email,
|
||||||
Domain: record.GetString("domain"),
|
Domain: record.GetString("domain"),
|
||||||
Access: access.GetString("config"),
|
Access: access.GetString("config"),
|
||||||
}
|
}
|
||||||
@@ -73,6 +80,8 @@ func Get(record *models.Record) (Applicant, error) {
|
|||||||
return NewCloudflare(option), nil
|
return NewCloudflare(option), nil
|
||||||
case configTypeNamesilo:
|
case configTypeNamesilo:
|
||||||
return NewNamesilo(option), nil
|
return NewNamesilo(option), nil
|
||||||
|
case configTypeGodaddy:
|
||||||
|
return NewGodaddy(option), nil
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("unknown config type")
|
return nil, errors.New("unknown config type")
|
||||||
}
|
}
|
||||||
|
|||||||
35
internal/applicant/godaddy.go
Normal file
35
internal/applicant/godaddy.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package applicant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"certimate/internal/domain"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
godaddyProvider "github.com/go-acme/lego/v4/providers/dns/godaddy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type godaddy struct {
|
||||||
|
option *ApplyOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGodaddy(option *ApplyOption) Applicant {
|
||||||
|
return &godaddy{
|
||||||
|
option: option,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *godaddy) Apply() (*Certificate, error) {
|
||||||
|
|
||||||
|
access := &domain.GodaddyAccess{}
|
||||||
|
json.Unmarshal([]byte(a.option.Access), access)
|
||||||
|
|
||||||
|
os.Setenv("GODADDY_API_KEY", access.ApiKey)
|
||||||
|
os.Setenv("GODADDY_API_SECRET", access.ApiKey)
|
||||||
|
|
||||||
|
dnsProvider, err := godaddyProvider.NewDNSProvider()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return apply(a.option, dnsProvider)
|
||||||
|
}
|
||||||
@@ -22,3 +22,8 @@ type QiniuAccess struct {
|
|||||||
type NameSiloAccess struct {
|
type NameSiloAccess struct {
|
||||||
ApiKey string `json:"apiKey"`
|
ApiKey string `json:"apiKey"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GodaddyAccess struct {
|
||||||
|
ApiKey string `json:"apiKey"`
|
||||||
|
ApiSecret string `json:"apiSecret"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,73 +12,10 @@ import (
|
|||||||
func init() {
|
func init() {
|
||||||
m.Register(func(db dbx.Builder) error {
|
m.Register(func(db dbx.Builder) error {
|
||||||
jsonData := `[
|
jsonData := `[
|
||||||
{
|
|
||||||
"id": "_pb_users_auth_",
|
|
||||||
"created": "2024-07-29 09:44:56.398Z",
|
|
||||||
"updated": "2024-09-07 07:42:28.389Z",
|
|
||||||
"name": "users",
|
|
||||||
"type": "auth",
|
|
||||||
"system": false,
|
|
||||||
"schema": [
|
|
||||||
{
|
|
||||||
"system": false,
|
|
||||||
"id": "users_name",
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"required": false,
|
|
||||||
"presentable": false,
|
|
||||||
"unique": false,
|
|
||||||
"options": {
|
|
||||||
"min": null,
|
|
||||||
"max": null,
|
|
||||||
"pattern": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"system": false,
|
|
||||||
"id": "users_avatar",
|
|
||||||
"name": "avatar",
|
|
||||||
"type": "file",
|
|
||||||
"required": false,
|
|
||||||
"presentable": false,
|
|
||||||
"unique": false,
|
|
||||||
"options": {
|
|
||||||
"mimeTypes": [
|
|
||||||
"image/jpeg",
|
|
||||||
"image/png",
|
|
||||||
"image/svg+xml",
|
|
||||||
"image/gif",
|
|
||||||
"image/webp"
|
|
||||||
],
|
|
||||||
"thumbs": null,
|
|
||||||
"maxSelect": 1,
|
|
||||||
"maxSize": 5242880,
|
|
||||||
"protected": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"indexes": [],
|
|
||||||
"listRule": "id = @request.auth.id",
|
|
||||||
"viewRule": "id = @request.auth.id",
|
|
||||||
"createRule": "",
|
|
||||||
"updateRule": "id = @request.auth.id",
|
|
||||||
"deleteRule": "id = @request.auth.id",
|
|
||||||
"options": {
|
|
||||||
"allowEmailAuth": true,
|
|
||||||
"allowOAuth2Auth": true,
|
|
||||||
"allowUsernameAuth": true,
|
|
||||||
"exceptEmailDomains": null,
|
|
||||||
"manageRule": null,
|
|
||||||
"minPasswordLength": 8,
|
|
||||||
"onlyEmailDomains": null,
|
|
||||||
"onlyVerified": false,
|
|
||||||
"requireEmail": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "z3p974ainxjqlvs",
|
"id": "z3p974ainxjqlvs",
|
||||||
"created": "2024-07-29 10:02:48.334Z",
|
"created": "2024-07-29 10:02:48.334Z",
|
||||||
"updated": "2024-09-07 07:42:28.389Z",
|
"updated": "2024-09-12 13:09:54.500Z",
|
||||||
"name": "domains",
|
"name": "domains",
|
||||||
"type": "base",
|
"type": "base",
|
||||||
"system": false,
|
"system": false,
|
||||||
@@ -333,7 +270,7 @@ func init() {
|
|||||||
{
|
{
|
||||||
"id": "4yzbv8urny5ja1e",
|
"id": "4yzbv8urny5ja1e",
|
||||||
"created": "2024-07-29 10:04:39.685Z",
|
"created": "2024-07-29 10:04:39.685Z",
|
||||||
"updated": "2024-09-10 12:51:41.405Z",
|
"updated": "2024-09-12 13:18:00.093Z",
|
||||||
"name": "access",
|
"name": "access",
|
||||||
"type": "base",
|
"type": "base",
|
||||||
"system": false,
|
"system": false,
|
||||||
@@ -381,7 +318,8 @@ func init() {
|
|||||||
"webhook",
|
"webhook",
|
||||||
"cloudflare",
|
"cloudflare",
|
||||||
"qiniu",
|
"qiniu",
|
||||||
"namesilo"
|
"namesilo",
|
||||||
|
"godaddy"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -397,6 +335,23 @@ func init() {
|
|||||||
"min": "",
|
"min": "",
|
||||||
"max": ""
|
"max": ""
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "hsxcnlvd",
|
||||||
|
"name": "usage",
|
||||||
|
"type": "select",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"values": [
|
||||||
|
"apply",
|
||||||
|
"deploy",
|
||||||
|
"all"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"indexes": [
|
"indexes": [
|
||||||
@@ -412,7 +367,7 @@ func init() {
|
|||||||
{
|
{
|
||||||
"id": "0a1o4e6sstp694f",
|
"id": "0a1o4e6sstp694f",
|
||||||
"created": "2024-07-30 06:30:27.801Z",
|
"created": "2024-07-30 06:30:27.801Z",
|
||||||
"updated": "2024-09-07 07:42:28.390Z",
|
"updated": "2024-09-12 13:09:54.500Z",
|
||||||
"name": "deployments",
|
"name": "deployments",
|
||||||
"type": "base",
|
"type": "base",
|
||||||
"system": false,
|
"system": false,
|
||||||
@@ -493,6 +448,69 @@ func init() {
|
|||||||
"updateRule": null,
|
"updateRule": null,
|
||||||
"deleteRule": null,
|
"deleteRule": null,
|
||||||
"options": {}
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "_pb_users_auth_",
|
||||||
|
"created": "2024-09-12 13:09:54.234Z",
|
||||||
|
"updated": "2024-09-12 13:09:54.500Z",
|
||||||
|
"name": "users",
|
||||||
|
"type": "auth",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "users_name",
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "users_avatar",
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "file",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"mimeTypes": [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/svg+xml",
|
||||||
|
"image/gif",
|
||||||
|
"image/webp"
|
||||||
|
],
|
||||||
|
"thumbs": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 5242880,
|
||||||
|
"protected": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": "id = @request.auth.id",
|
||||||
|
"viewRule": "id = @request.auth.id",
|
||||||
|
"createRule": "",
|
||||||
|
"updateRule": "id = @request.auth.id",
|
||||||
|
"deleteRule": "id = @request.auth.id",
|
||||||
|
"options": {
|
||||||
|
"allowEmailAuth": true,
|
||||||
|
"allowOAuth2Auth": true,
|
||||||
|
"allowUsernameAuth": true,
|
||||||
|
"exceptEmailDomains": null,
|
||||||
|
"manageRule": null,
|
||||||
|
"minPasswordLength": 8,
|
||||||
|
"onlyEmailDomains": null,
|
||||||
|
"onlyVerified": false,
|
||||||
|
"requireEmail": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]`
|
]`
|
||||||
|
|
||||||
20
migrations/1726147268_update_access_usage.go
Normal file
20
migrations/1726147268_update_access_usage.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(db dbx.Builder) error {
|
||||||
|
// add up queries...
|
||||||
|
db.NewQuery("update access set usage='all' where configType in ('aliyun', 'tencent')").Execute()
|
||||||
|
db.NewQuery("update access set usage='deploy' where configType in ('ssh', 'webhook','qiniu')").Execute()
|
||||||
|
db.NewQuery("update access set usage='apply' where configType in ('cloudflare','namesilo','godaddy')").Execute()
|
||||||
|
return nil
|
||||||
|
}, func(db dbx.Builder) error {
|
||||||
|
// add down queries...
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -12,73 +12,10 @@ import (
|
|||||||
func init() {
|
func init() {
|
||||||
m.Register(func(db dbx.Builder) error {
|
m.Register(func(db dbx.Builder) error {
|
||||||
jsonData := `[
|
jsonData := `[
|
||||||
{
|
|
||||||
"id": "_pb_users_auth_",
|
|
||||||
"created": "2024-07-29 09:44:56.398Z",
|
|
||||||
"updated": "2024-09-02 14:02:40.191Z",
|
|
||||||
"name": "users",
|
|
||||||
"type": "auth",
|
|
||||||
"system": false,
|
|
||||||
"schema": [
|
|
||||||
{
|
|
||||||
"system": false,
|
|
||||||
"id": "users_name",
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"required": false,
|
|
||||||
"presentable": false,
|
|
||||||
"unique": false,
|
|
||||||
"options": {
|
|
||||||
"min": null,
|
|
||||||
"max": null,
|
|
||||||
"pattern": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"system": false,
|
|
||||||
"id": "users_avatar",
|
|
||||||
"name": "avatar",
|
|
||||||
"type": "file",
|
|
||||||
"required": false,
|
|
||||||
"presentable": false,
|
|
||||||
"unique": false,
|
|
||||||
"options": {
|
|
||||||
"mimeTypes": [
|
|
||||||
"image/jpeg",
|
|
||||||
"image/png",
|
|
||||||
"image/svg+xml",
|
|
||||||
"image/gif",
|
|
||||||
"image/webp"
|
|
||||||
],
|
|
||||||
"thumbs": null,
|
|
||||||
"maxSelect": 1,
|
|
||||||
"maxSize": 5242880,
|
|
||||||
"protected": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"indexes": [],
|
|
||||||
"listRule": "id = @request.auth.id",
|
|
||||||
"viewRule": "id = @request.auth.id",
|
|
||||||
"createRule": "",
|
|
||||||
"updateRule": "id = @request.auth.id",
|
|
||||||
"deleteRule": "id = @request.auth.id",
|
|
||||||
"options": {
|
|
||||||
"allowEmailAuth": true,
|
|
||||||
"allowOAuth2Auth": true,
|
|
||||||
"allowUsernameAuth": true,
|
|
||||||
"exceptEmailDomains": null,
|
|
||||||
"manageRule": null,
|
|
||||||
"minPasswordLength": 8,
|
|
||||||
"onlyEmailDomains": null,
|
|
||||||
"onlyVerified": false,
|
|
||||||
"requireEmail": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "z3p974ainxjqlvs",
|
"id": "z3p974ainxjqlvs",
|
||||||
"created": "2024-07-29 10:02:48.334Z",
|
"created": "2024-07-29 10:02:48.334Z",
|
||||||
"updated": "2024-09-04 15:10:19.440Z",
|
"updated": "2024-09-12 23:13:12.119Z",
|
||||||
"name": "domains",
|
"name": "domains",
|
||||||
"type": "base",
|
"type": "base",
|
||||||
"system": false,
|
"system": false,
|
||||||
@@ -97,6 +34,19 @@ func init() {
|
|||||||
"pattern": ""
|
"pattern": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "ukkhuw85",
|
||||||
|
"name": "email",
|
||||||
|
"type": "email",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"exceptDomains": null,
|
||||||
|
"onlyDomains": null
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"system": false,
|
"system": false,
|
||||||
"id": "v98eebqq",
|
"id": "v98eebqq",
|
||||||
@@ -333,7 +283,7 @@ func init() {
|
|||||||
{
|
{
|
||||||
"id": "4yzbv8urny5ja1e",
|
"id": "4yzbv8urny5ja1e",
|
||||||
"created": "2024-07-29 10:04:39.685Z",
|
"created": "2024-07-29 10:04:39.685Z",
|
||||||
"updated": "2024-09-04 15:09:46.789Z",
|
"updated": "2024-09-12 23:08:52.810Z",
|
||||||
"name": "access",
|
"name": "access",
|
||||||
"type": "base",
|
"type": "base",
|
||||||
"system": false,
|
"system": false,
|
||||||
@@ -380,7 +330,9 @@ func init() {
|
|||||||
"ssh",
|
"ssh",
|
||||||
"webhook",
|
"webhook",
|
||||||
"cloudflare",
|
"cloudflare",
|
||||||
"qiniu"
|
"qiniu",
|
||||||
|
"namesilo",
|
||||||
|
"godaddy"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -396,6 +348,23 @@ func init() {
|
|||||||
"min": "",
|
"min": "",
|
||||||
"max": ""
|
"max": ""
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "hsxcnlvd",
|
||||||
|
"name": "usage",
|
||||||
|
"type": "select",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"values": [
|
||||||
|
"apply",
|
||||||
|
"deploy",
|
||||||
|
"all"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"indexes": [
|
"indexes": [
|
||||||
@@ -411,7 +380,7 @@ func init() {
|
|||||||
{
|
{
|
||||||
"id": "0a1o4e6sstp694f",
|
"id": "0a1o4e6sstp694f",
|
||||||
"created": "2024-07-30 06:30:27.801Z",
|
"created": "2024-07-30 06:30:27.801Z",
|
||||||
"updated": "2024-09-02 14:02:40.191Z",
|
"updated": "2024-09-12 23:08:52.810Z",
|
||||||
"name": "deployments",
|
"name": "deployments",
|
||||||
"type": "base",
|
"type": "base",
|
||||||
"system": false,
|
"system": false,
|
||||||
@@ -492,6 +461,114 @@ func init() {
|
|||||||
"updateRule": null,
|
"updateRule": null,
|
||||||
"deleteRule": null,
|
"deleteRule": null,
|
||||||
"options": {}
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "_pb_users_auth_",
|
||||||
|
"created": "2024-09-12 13:09:54.234Z",
|
||||||
|
"updated": "2024-09-12 23:08:52.811Z",
|
||||||
|
"name": "users",
|
||||||
|
"type": "auth",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "users_name",
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "users_avatar",
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "file",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"mimeTypes": [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/svg+xml",
|
||||||
|
"image/gif",
|
||||||
|
"image/webp"
|
||||||
|
],
|
||||||
|
"thumbs": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 5242880,
|
||||||
|
"protected": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": "id = @request.auth.id",
|
||||||
|
"viewRule": "id = @request.auth.id",
|
||||||
|
"createRule": "",
|
||||||
|
"updateRule": "id = @request.auth.id",
|
||||||
|
"deleteRule": "id = @request.auth.id",
|
||||||
|
"options": {
|
||||||
|
"allowEmailAuth": true,
|
||||||
|
"allowOAuth2Auth": true,
|
||||||
|
"allowUsernameAuth": true,
|
||||||
|
"exceptEmailDomains": null,
|
||||||
|
"manageRule": null,
|
||||||
|
"minPasswordLength": 8,
|
||||||
|
"onlyEmailDomains": null,
|
||||||
|
"onlyVerified": false,
|
||||||
|
"requireEmail": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dy6ccjb60spfy6p",
|
||||||
|
"created": "2024-09-12 23:12:21.677Z",
|
||||||
|
"updated": "2024-09-12 23:19:09.110Z",
|
||||||
|
"name": "settings",
|
||||||
|
"type": "base",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "1tcmdsdf",
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "f9wyhypi",
|
||||||
|
"name": "content",
|
||||||
|
"type": "json",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSize": 2000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
"CREATE UNIQUE INDEX ` + "`" + `idx_RO7X9Vw` + "`" + ` ON ` + "`" + `settings` + "`" + ` (` + "`" + `name` + "`" + `)"
|
||||||
|
],
|
||||||
|
"listRule": null,
|
||||||
|
"viewRule": null,
|
||||||
|
"createRule": null,
|
||||||
|
"updateRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"options": {}
|
||||||
}
|
}
|
||||||
]`
|
]`
|
||||||
|
|
||||||
284
ui/dist/assets/index-BKUIxIk5.js
vendored
284
ui/dist/assets/index-BKUIxIk5.js
vendored
File diff suppressed because one or more lines are too long
284
ui/dist/assets/index-BRoqOo1T.js
vendored
Normal file
284
ui/dist/assets/index-BRoqOo1T.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ui/dist/assets/index-Cg0yCJnh.css
vendored
Normal file
1
ui/dist/assets/index-Cg0yCJnh.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ui/dist/assets/index-Kh_0Jotc.css
vendored
1
ui/dist/assets/index-Kh_0Jotc.css
vendored
File diff suppressed because one or more lines are too long
5
ui/dist/imgs/providers/godaddy.svg
vendored
Normal file
5
ui/dist/imgs/providers/godaddy.svg
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="512" cy="512" r="512" style="fill:#1bdbdb"/>
|
||||||
|
<path d="M697.6 315.9c-53.2-33.2-123.3-25.3-185.6 13.9-62.4-39.3-132.4-47.2-185.6-13.9-84.1 52.5-94.3 187.8-22.8 302.2 52.7 84.3 135.1 133.7 208.4 132.8 73.3.9 155.7-48.5 208.4-132.8 71.5-114.4 61.3-249.7-22.8-302.2M342.2 594c-15-24.1-26.1-49.5-33-75.5-6.5-24.5-8.9-48.5-7.1-71.2 3.2-42.3 20.4-75.2 48.4-92.7s65.2-18.6 104.5-2.9c5.9 2.4 11.8 5.1 17.6 8.1-21 19-40.3 41.9-56.7 68.1-43.4 69.5-56.6 146.7-41.5 208.4-11.8-12.8-22.6-27-32.2-42.3m372.6-75.6c-6.9 26.1-17.9 51.5-33 75.5-9.6 15.4-20.4 29.5-32.3 42.3 13.5-55.2 4.4-122.9-28.9-186.3-2.3-4.5-7.7-5.9-12-3.3l-103.5 64.7c-4 2.5-5.2 7.7-2.7 11.7l15.2 24.3c2.5 4 7.7 5.2 11.7 2.7l67.1-41.9c2.2 6.4 4.3 12.9 6 19.5 6.5 24.5 8.9 48.5 7.1 71.2-3.2 42.3-20.4 75.2-48.4 92.7-14 8.8-30.3 13.4-48 13.9h-2.2c-17.7-.5-34-5.1-48-13.9-28-17.5-45.2-50.4-48.4-92.7-1.7-22.7.7-46.7 7.1-71.2 6.8-26.1 17.9-51.5 33-75.5 15-24.1 33-45.2 53.4-62.8 19.2-16.6 39.7-29.2 60.9-37.6 39.4-15.7 76.5-14.6 104.5 2.9s45.2 50.4 48.4 92.7c1.8 22.6-.6 46.6-7 71.1" style="fill:#fff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
4
ui/dist/index.html
vendored
4
ui/dist/index.html
vendored
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Certimate - Your Trusted SSL Automation Partner</title>
|
<title>Certimate - Your Trusted SSL Automation Partner</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BKUIxIk5.js"></script>
|
<script type="module" crossorigin src="/assets/index-BRoqOo1T.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Kh_0Jotc.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Cg0yCJnh.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-background">
|
<body class="bg-background">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
5
ui/public/imgs/providers/godaddy.svg
Normal file
5
ui/public/imgs/providers/godaddy.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="512" cy="512" r="512" style="fill:#1bdbdb"/>
|
||||||
|
<path d="M697.6 315.9c-53.2-33.2-123.3-25.3-185.6 13.9-62.4-39.3-132.4-47.2-185.6-13.9-84.1 52.5-94.3 187.8-22.8 302.2 52.7 84.3 135.1 133.7 208.4 132.8 73.3.9 155.7-48.5 208.4-132.8 71.5-114.4 61.3-249.7-22.8-302.2M342.2 594c-15-24.1-26.1-49.5-33-75.5-6.5-24.5-8.9-48.5-7.1-71.2 3.2-42.3 20.4-75.2 48.4-92.7s65.2-18.6 104.5-2.9c5.9 2.4 11.8 5.1 17.6 8.1-21 19-40.3 41.9-56.7 68.1-43.4 69.5-56.6 146.7-41.5 208.4-11.8-12.8-22.6-27-32.2-42.3m372.6-75.6c-6.9 26.1-17.9 51.5-33 75.5-9.6 15.4-20.4 29.5-32.3 42.3 13.5-55.2 4.4-122.9-28.9-186.3-2.3-4.5-7.7-5.9-12-3.3l-103.5 64.7c-4 2.5-5.2 7.7-2.7 11.7l15.2 24.3c2.5 4 7.7 5.2 11.7 2.7l67.1-41.9c2.2 6.4 4.3 12.9 6 19.5 6.5 24.5 8.9 48.5 7.1 71.2-3.2 42.3-20.4 75.2-48.4 92.7-14 8.8-30.3 13.4-48 13.9h-2.2c-17.7-.5-34-5.1-48-13.9-28-17.5-45.2-50.4-48.4-92.7-1.7-22.7.7-46.7 7.1-71.2 6.8-26.1 17.9-51.5 33-75.5 15-24.1 33-45.2 53.4-62.8 19.2-16.6 39.7-29.2 60.9-37.6 39.4-15.7 76.5-14.6 104.5 2.9s45.2 50.4 48.4 92.7c1.8 22.6-.6 46.6-7 71.1" style="fill:#fff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -14,7 +14,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
import { Access, accessFormType, AliyunConfig } from "@/domain/access";
|
import { Access, accessFormType, AliyunConfig, getUsageByConfigType } from "@/domain/access";
|
||||||
import { save } from "@/repository/access";
|
import { save } from "@/repository/access";
|
||||||
import { useConfig } from "@/providers/config";
|
import { useConfig } from "@/providers/config";
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ const AccessAliyunForm = ({
|
|||||||
id: data.id as string,
|
id: data.id as string,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
configType: data.configType,
|
configType: data.configType,
|
||||||
|
usage: getUsageByConfigType(data.configType),
|
||||||
config: {
|
config: {
|
||||||
accessKeyId: data.accessKeyId,
|
accessKeyId: data.accessKeyId,
|
||||||
accessKeySecret: data.accessSecretId,
|
accessKeySecret: data.accessSecretId,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
import { Access, accessFormType, CloudflareConfig } from "@/domain/access";
|
import { Access, accessFormType, CloudflareConfig, getUsageByConfigType } from "@/domain/access";
|
||||||
import { save } from "@/repository/access";
|
import { save } from "@/repository/access";
|
||||||
import { useConfig } from "@/providers/config";
|
import { useConfig } from "@/providers/config";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
@@ -56,6 +56,7 @@ const AccessCloudflareForm = ({
|
|||||||
id: data.id as string,
|
id: data.id as string,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
configType: data.configType,
|
configType: data.configType,
|
||||||
|
usage: getUsageByConfigType(data.configType),
|
||||||
config: {
|
config: {
|
||||||
dnsApiToken: data.dnsApiToken,
|
dnsApiToken: data.dnsApiToken,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ import {
|
|||||||
import AccessCloudflareForm from "./AccessCloudflareForm";
|
import AccessCloudflareForm from "./AccessCloudflareForm";
|
||||||
import AccessQiniuForm from "./AccessQiniuForm";
|
import AccessQiniuForm from "./AccessQiniuForm";
|
||||||
import AccessNamesiloForm from "./AccessNamesiloForm";
|
import AccessNamesiloForm from "./AccessNamesiloForm";
|
||||||
|
import AccessGodaddyFrom from "./AccessGodaddyForm";
|
||||||
|
|
||||||
type TargetConfigEditProps = {
|
type TargetConfigEditProps = {
|
||||||
op: "add" | "edit";
|
op: "add" | "edit";
|
||||||
@@ -121,6 +123,16 @@ export function AccessEdit({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case "godaddy":
|
||||||
|
form = (
|
||||||
|
<AccessGodaddyFrom
|
||||||
|
data={data}
|
||||||
|
onAfterReq={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOptionCls = (val: string) => {
|
const getOptionCls = (val: string) => {
|
||||||
@@ -136,44 +148,46 @@ export function AccessEdit({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{op == "add" ? "添加" : "编辑"}授权</DialogTitle>
|
<DialogTitle>{op == "add" ? "添加" : "编辑"}授权</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="container">
|
<ScrollArea className="max-h-[80vh]">
|
||||||
<Label>服务商</Label>
|
<div className="container py-3">
|
||||||
|
<Label>服务商</Label>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(val) => {
|
onValueChange={(val) => {
|
||||||
console.log(val);
|
console.log(val);
|
||||||
setConfigType(val);
|
setConfigType(val);
|
||||||
}}
|
}}
|
||||||
defaultValue={configType}
|
defaultValue={configType}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="mt-3">
|
<SelectTrigger className="mt-3">
|
||||||
<SelectValue placeholder="请选择服务商" />
|
<SelectValue placeholder="请选择服务商" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>服务商</SelectLabel>
|
<SelectLabel>服务商</SelectLabel>
|
||||||
{typeKeys.map((key) => (
|
{typeKeys.map((key) => (
|
||||||
<SelectItem value={key} key={key}>
|
<SelectItem value={key} key={key}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center space-x-2 rounded cursor-pointer",
|
"flex items-center space-x-2 rounded cursor-pointer",
|
||||||
getOptionCls(key)
|
getOptionCls(key)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={accessTypeMap.get(key)?.[1]}
|
src={accessTypeMap.get(key)?.[1]}
|
||||||
className="h-6 w-6"
|
className="h-6 w-6"
|
||||||
/>
|
/>
|
||||||
<div>{accessTypeMap.get(key)?.[0]}</div>
|
<div>{accessTypeMap.get(key)?.[0]}</div>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{form}
|
{form}
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
198
ui/src/components/certimate/AccessGodaddyForm.tsx
Normal file
198
ui/src/components/certimate/AccessGodaddyForm.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
|
import z from "zod";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Access,
|
||||||
|
accessFormType,
|
||||||
|
getUsageByConfigType,
|
||||||
|
GodaddyConfig,
|
||||||
|
} from "@/domain/access";
|
||||||
|
import { save } from "@/repository/access";
|
||||||
|
import { useConfig } from "@/providers/config";
|
||||||
|
import { ClientResponseError } from "pocketbase";
|
||||||
|
import { PbErrorData } from "@/domain/base";
|
||||||
|
|
||||||
|
const AccessGodaddyFrom = ({
|
||||||
|
data,
|
||||||
|
onAfterReq,
|
||||||
|
}: {
|
||||||
|
data?: Access;
|
||||||
|
onAfterReq: () => void;
|
||||||
|
}) => {
|
||||||
|
const { addAccess, updateAccess } = useConfig();
|
||||||
|
const formSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
name: z.string().min(1).max(64),
|
||||||
|
configType: accessFormType,
|
||||||
|
apiKey: z.string().min(1).max(64),
|
||||||
|
apiSecret: z.string().min(1).max(64),
|
||||||
|
});
|
||||||
|
|
||||||
|
let config: GodaddyConfig = {
|
||||||
|
apiKey: "",
|
||||||
|
apiSecret: "",
|
||||||
|
};
|
||||||
|
if (data) config = data.config as GodaddyConfig;
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
id: data?.id,
|
||||||
|
name: data?.name,
|
||||||
|
configType: "godaddy",
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
apiSecret: config.apiSecret,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||||
|
console.log(data);
|
||||||
|
const req: Access = {
|
||||||
|
id: data.id as string,
|
||||||
|
name: data.name,
|
||||||
|
configType: data.configType,
|
||||||
|
usage: getUsageByConfigType(data.configType),
|
||||||
|
config: {
|
||||||
|
apiKey: data.apiKey,
|
||||||
|
apiSecret: data.apiSecret,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rs = await save(req);
|
||||||
|
|
||||||
|
onAfterReq();
|
||||||
|
|
||||||
|
req.id = rs.id;
|
||||||
|
req.created = rs.created;
|
||||||
|
req.updated = rs.updated;
|
||||||
|
if (data.id) {
|
||||||
|
updateAccess(req);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addAccess(req);
|
||||||
|
} catch (e) {
|
||||||
|
const err = e as ClientResponseError;
|
||||||
|
|
||||||
|
Object.entries(err.response.data as PbErrorData).forEach(
|
||||||
|
([key, value]) => {
|
||||||
|
form.setError(key as keyof z.infer<typeof formSchema>, {
|
||||||
|
type: "manual",
|
||||||
|
message: value.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="max-w-[35em] mx-auto mt-10">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
console.log(e);
|
||||||
|
e.stopPropagation();
|
||||||
|
form.handleSubmit(onSubmit)(e);
|
||||||
|
}}
|
||||||
|
className="space-y-8"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>名称</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="请输入授权名称" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="id"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="hidden">
|
||||||
|
<FormLabel>配置类型</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="configType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="hidden">
|
||||||
|
<FormLabel>配置类型</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="apiKey"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>GODADDY_API_KEY</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="请输入GODADDY_API_KEY" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="apiSecret"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>GODADDY_API_SECRET</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="请输入GODADDY_API_SECRET" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button type="submit">保存</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccessGodaddyFrom;
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
import { Access, accessFormType, NamesiloConfig } from "@/domain/access";
|
import { Access, accessFormType, getUsageByConfigType, NamesiloConfig } from "@/domain/access";
|
||||||
import { save } from "@/repository/access";
|
import { save } from "@/repository/access";
|
||||||
import { useConfig } from "@/providers/config";
|
import { useConfig } from "@/providers/config";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
@@ -56,6 +56,7 @@ const AccessNamesiloForm = ({
|
|||||||
id: data.id as string,
|
id: data.id as string,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
configType: data.configType,
|
configType: data.configType,
|
||||||
|
usage: getUsageByConfigType(data.configType),
|
||||||
config: {
|
config: {
|
||||||
apiKey: data.apiKey,
|
apiKey: data.apiKey,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
import { Access, accessFormType, QiniuConfig } from "@/domain/access";
|
import { Access, accessFormType, getUsageByConfigType, QiniuConfig } from "@/domain/access";
|
||||||
import { save } from "@/repository/access";
|
import { save } from "@/repository/access";
|
||||||
import { useConfig } from "@/providers/config";
|
import { useConfig } from "@/providers/config";
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ const AccessQiniuForm = ({
|
|||||||
id: data.id as string,
|
id: data.id as string,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
configType: data.configType,
|
configType: data.configType,
|
||||||
|
usage: getUsageByConfigType(data.configType),
|
||||||
config: {
|
config: {
|
||||||
accessKey: data.accessKey,
|
accessKey: data.accessKey,
|
||||||
secretKey: data.secretKey,
|
secretKey: data.secretKey,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Access, accessFormType, SSHConfig } from "@/domain/access";
|
import { Access, accessFormType, getUsageByConfigType, SSHConfig } from "@/domain/access";
|
||||||
import { useConfig } from "@/providers/config";
|
import { useConfig } from "@/providers/config";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -18,6 +18,7 @@ import { save } from "@/repository/access";
|
|||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
import { PbErrorData } from "@/domain/base";
|
import { PbErrorData } from "@/domain/base";
|
||||||
import { readFileContent } from "@/lib/file";
|
import { readFileContent } from "@/lib/file";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
const AccessSSHForm = ({
|
const AccessSSHForm = ({
|
||||||
data,
|
data,
|
||||||
@@ -27,6 +28,11 @@ const AccessSSHForm = ({
|
|||||||
onAfterReq: () => void;
|
onAfterReq: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { addAccess, updateAccess } = useConfig();
|
const { addAccess, updateAccess } = useConfig();
|
||||||
|
|
||||||
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const [fileName, setFileName] = useState("");
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
name: z.string().min(1).max(64),
|
name: z.string().min(1).max(64),
|
||||||
@@ -38,7 +44,7 @@ const AccessSSHForm = ({
|
|||||||
username: z.string().min(1).max(64),
|
username: z.string().min(1).max(64),
|
||||||
password: z.string().min(0).max(64),
|
password: z.string().min(0).max(64),
|
||||||
key: z.string().min(0).max(20480),
|
key: z.string().min(0).max(20480),
|
||||||
keyFile: z.string().optional(),
|
keyFile: z.any().optional(),
|
||||||
command: z.string().min(1).max(2048),
|
command: z.string().min(1).max(2048),
|
||||||
certPath: z.string().min(0).max(2048),
|
certPath: z.string().min(0).max(2048),
|
||||||
keyPath: z.string().min(0).max(2048),
|
keyPath: z.string().min(0).max(2048),
|
||||||
@@ -81,6 +87,7 @@ const AccessSSHForm = ({
|
|||||||
id: data.id as string,
|
id: data.id as string,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
configType: data.configType,
|
configType: data.configType,
|
||||||
|
usage: getUsageByConfigType(data.configType),
|
||||||
config: {
|
config: {
|
||||||
host: data.host,
|
host: data.host,
|
||||||
port: data.port,
|
port: data.port,
|
||||||
@@ -127,9 +134,16 @@ const AccessSSHForm = ({
|
|||||||
) => {
|
) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const content = await readFileContent(file);
|
const savedFile = file;
|
||||||
|
setFileName(savedFile.name);
|
||||||
|
const content = await readFileContent(savedFile);
|
||||||
form.setValue("key", content);
|
form.setValue("key", content);
|
||||||
form.setValue("keyFile", "");
|
};
|
||||||
|
|
||||||
|
const handleSelectFileClick = () => {
|
||||||
|
console.log(fileInputRef.current);
|
||||||
|
|
||||||
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -279,12 +293,26 @@ const AccessSSHForm = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Key(使用证书登录)</FormLabel>
|
<FormLabel>Key(使用证书登录)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<div>
|
||||||
placeholder="请输入Key"
|
<Button
|
||||||
{...field}
|
type={"button"}
|
||||||
type="file"
|
variant={"secondary"}
|
||||||
onChange={handleFileChange}
|
size={"sm"}
|
||||||
/>
|
className="w-48"
|
||||||
|
onClick={handleSelectFileClick}
|
||||||
|
>
|
||||||
|
{fileName ? fileName : "请选择文件"}
|
||||||
|
</Button>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入Key"
|
||||||
|
{...field}
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
hidden
|
||||||
|
type="file"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
import { Access, accessFormType, TencentConfig } from "@/domain/access";
|
import { Access, accessFormType, getUsageByConfigType, TencentConfig } from "@/domain/access";
|
||||||
import { save } from "@/repository/access";
|
import { save } from "@/repository/access";
|
||||||
import { useConfig } from "@/providers/config";
|
import { useConfig } from "@/providers/config";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
@@ -58,6 +58,7 @@ const AccessTencentForm = ({
|
|||||||
id: data.id as string,
|
id: data.id as string,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
configType: data.configType,
|
configType: data.configType,
|
||||||
|
usage: getUsageByConfigType(data.configType),
|
||||||
config: {
|
config: {
|
||||||
secretId: data.secretId,
|
secretId: data.secretId,
|
||||||
secretKey: data.secretKey,
|
secretKey: data.secretKey,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
import { Access, accessFormType, WebhookConfig } from "@/domain/access";
|
import { Access, accessFormType, getUsageByConfigType, WebhookConfig } from "@/domain/access";
|
||||||
import { save } from "@/repository/access";
|
import { save } from "@/repository/access";
|
||||||
import { useConfig } from "@/providers/config";
|
import { useConfig } from "@/providers/config";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
@@ -56,6 +56,7 @@ const WebhookForm = ({
|
|||||||
id: data.id as string,
|
id: data.id as string,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
configType: data.configType,
|
configType: data.configType,
|
||||||
|
usage: getUsageByConfigType(data.configType),
|
||||||
config: {
|
config: {
|
||||||
url: data.url,
|
url: data.url,
|
||||||
},
|
},
|
||||||
|
|||||||
141
ui/src/components/certimate/EmailsEdit.tsx
Normal file
141
ui/src/components/certimate/EmailsEdit.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "../ui/dialog";
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "../ui/form";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { useConfig } from "@/providers/config";
|
||||||
|
import { update } from "@/repository/settings";
|
||||||
|
import { ClientResponseError } from "pocketbase";
|
||||||
|
import { PbErrorData } from "@/domain/base";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
type EmailsEditProps = {
|
||||||
|
className?: string;
|
||||||
|
trigger: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
|
||||||
|
const {
|
||||||
|
config: { emails },
|
||||||
|
setEmails,
|
||||||
|
} = useConfig();
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||||
|
if (emails.content.emails.includes(data.email)) {
|
||||||
|
form.setError("email", {
|
||||||
|
message: "邮箱已存在",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到 config
|
||||||
|
const newEmails = [...emails.content.emails, data.email];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await update({
|
||||||
|
...emails,
|
||||||
|
name: "emails",
|
||||||
|
content: {
|
||||||
|
emails: newEmails,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新本地状态
|
||||||
|
setEmails(resp);
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
form.reset();
|
||||||
|
form.clearErrors();
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
} catch (e) {
|
||||||
|
const err = e as ClientResponseError;
|
||||||
|
|
||||||
|
Object.entries(err.response.data as PbErrorData).forEach(
|
||||||
|
([key, value]) => {
|
||||||
|
form.setError(key as keyof z.infer<typeof formSchema>, {
|
||||||
|
type: "manual",
|
||||||
|
message: value.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog onOpenChange={setOpen} open={open}>
|
||||||
|
<DialogTrigger asChild className={cn(className)}>
|
||||||
|
{trigger}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[600px] w-full dark:text-stone-200">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>添加邮箱</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="container py-3">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
console.log(e);
|
||||||
|
e.stopPropagation();
|
||||||
|
form.handleSubmit(onSubmit)(e);
|
||||||
|
}}
|
||||||
|
className="space-y-8"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>邮箱</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="请输入邮箱" {...field} type="email" />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button type="submit">保存</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailsEdit;
|
||||||
@@ -5,6 +5,7 @@ export const accessTypeMap: Map<string, [string, string]> = new Map([
|
|||||||
["aliyun", ["阿里云", "/imgs/providers/aliyun.svg"]],
|
["aliyun", ["阿里云", "/imgs/providers/aliyun.svg"]],
|
||||||
["cloudflare", ["Cloudflare", "/imgs/providers/cloudflare.svg"]],
|
["cloudflare", ["Cloudflare", "/imgs/providers/cloudflare.svg"]],
|
||||||
["namesilo", ["Namesilo", "/imgs/providers/namesilo.svg"]],
|
["namesilo", ["Namesilo", "/imgs/providers/namesilo.svg"]],
|
||||||
|
["godaddy", ["GoDaddy", "/imgs/providers/godaddy.svg"]],
|
||||||
["qiniu", ["七牛云", "/imgs/providers/qiniu.svg"]],
|
["qiniu", ["七牛云", "/imgs/providers/qiniu.svg"]],
|
||||||
["ssh", ["SSH部署", "/imgs/providers/ssh.svg"]],
|
["ssh", ["SSH部署", "/imgs/providers/ssh.svg"]],
|
||||||
["webhook", ["Webhook", "/imgs/providers/webhook.svg"]],
|
["webhook", ["Webhook", "/imgs/providers/webhook.svg"]],
|
||||||
@@ -19,14 +20,18 @@ export const accessFormType = z.union(
|
|||||||
z.literal("cloudflare"),
|
z.literal("cloudflare"),
|
||||||
z.literal("qiniu"),
|
z.literal("qiniu"),
|
||||||
z.literal("namesilo"),
|
z.literal("namesilo"),
|
||||||
|
z.literal("godaddy"),
|
||||||
],
|
],
|
||||||
{ message: "请选择云服务商" }
|
{ message: "请选择云服务商" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type AccessUsage = "apply" | "deploy" | "all";
|
||||||
|
|
||||||
export type Access = {
|
export type Access = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
configType: string;
|
configType: string;
|
||||||
|
usage: AccessUsage;
|
||||||
config:
|
config:
|
||||||
| TencentConfig
|
| TencentConfig
|
||||||
| AliyunConfig
|
| AliyunConfig
|
||||||
@@ -34,7 +39,8 @@ export type Access = {
|
|||||||
| WebhookConfig
|
| WebhookConfig
|
||||||
| CloudflareConfig
|
| CloudflareConfig
|
||||||
| QiniuConfig
|
| QiniuConfig
|
||||||
| NamesiloConfig;
|
| NamesiloConfig
|
||||||
|
| GodaddyConfig;
|
||||||
|
|
||||||
deleted?: string;
|
deleted?: string;
|
||||||
created?: string;
|
created?: string;
|
||||||
@@ -67,6 +73,10 @@ export type AliyunConfig = {
|
|||||||
export type NamesiloConfig = {
|
export type NamesiloConfig = {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
};
|
};
|
||||||
|
export type GodaddyConfig = {
|
||||||
|
apiKey: string;
|
||||||
|
apiSecret: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type SSHConfig = {
|
export type SSHConfig = {
|
||||||
host: string;
|
host: string;
|
||||||
@@ -79,3 +89,22 @@ export type SSHConfig = {
|
|||||||
certPath: string;
|
certPath: string;
|
||||||
keyPath: string;
|
keyPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUsageByConfigType = (configType: string): AccessUsage => {
|
||||||
|
switch (configType) {
|
||||||
|
case "aliyun":
|
||||||
|
case "tencent":
|
||||||
|
return "all";
|
||||||
|
case "ssh":
|
||||||
|
case "webhook":
|
||||||
|
case "qiniu":
|
||||||
|
return "deploy";
|
||||||
|
|
||||||
|
case "cloudflare":
|
||||||
|
case "namesilo":
|
||||||
|
case "godaddy":
|
||||||
|
return "apply";
|
||||||
|
default:
|
||||||
|
return "all";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Deployment, Pahse } from "./deployment";
|
|||||||
export type Domain = {
|
export type Domain = {
|
||||||
id: string;
|
id: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
|
email?: string;
|
||||||
crontab: string;
|
crontab: string;
|
||||||
access: string;
|
access: string;
|
||||||
targetAccess: string;
|
targetAccess: string;
|
||||||
|
|||||||
9
ui/src/domain/settings.ts
Normal file
9
ui/src/domain/settings.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export type Setting = {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
content: EmailsSetting;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmailsSetting = {
|
||||||
|
emails: string[];
|
||||||
|
};
|
||||||
@@ -227,7 +227,7 @@ export default function Dashboard() {
|
|||||||
href="https://github.com/usual2970/certimate/releases"
|
href="https://github.com/usual2970/certimate/releases"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Certimate v0.1.2
|
Certimate v0.1.4
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ const Dashboard = () => {
|
|||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="text-muted-foreground">控制面板</div>
|
<div className="text-muted-foreground">控制面板</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex mt-10 gap-5 flex-col md:flex-row">
|
<div className="flex mt-10 gap-5 flex-col flex-wrap md:flex-row">
|
||||||
<div className="w-full md:w-[300px] flex items-center rounded-md p-3 shadow-lg border">
|
<div className="w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg border">
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<SquareSigma size={48} strokeWidth={1} className="text-blue-400" />
|
<SquareSigma size={48} strokeWidth={1} className="text-blue-400" />
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +80,7 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full md:w-[300px] flex items-center rounded-md p-3 shadow-lg border">
|
<div className="w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg border">
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<CalendarX2 size={48} strokeWidth={1} className="text-red-400" />
|
<CalendarX2 size={48} strokeWidth={1} className="text-red-400" />
|
||||||
</div>
|
</div>
|
||||||
@@ -101,7 +101,7 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border w-full md:w-[300px] flex items-center rounded-md p-3 shadow-lg">
|
<div className="border w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg">
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<LoaderPinwheel
|
<LoaderPinwheel
|
||||||
size={48}
|
size={48}
|
||||||
@@ -126,7 +126,7 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border w-full md:w-[300px] flex items-center rounded-md p-3 shadow-lg">
|
<div className="border w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg">
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<Ban size={48} strokeWidth={1} className="text-gray-400" />
|
<Ban size={48} strokeWidth={1} className="text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
@@ -171,7 +171,7 @@ const Dashboard = () => {
|
|||||||
<div className="mt-2 flex justify-end">
|
<div className="mt-2 flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("/");
|
navigate("/edit");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
添加域名
|
添加域名
|
||||||
|
|||||||
@@ -34,10 +34,11 @@ import { useLocation, useNavigate } from "react-router-dom";
|
|||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { AccessEdit } from "@/components/certimate/AccessEdit";
|
import { AccessEdit } from "@/components/certimate/AccessEdit";
|
||||||
import { accessTypeMap } from "@/domain/access";
|
import { accessTypeMap } from "@/domain/access";
|
||||||
|
import EmailsEdit from "@/components/certimate/EmailsEdit";
|
||||||
|
|
||||||
const Edit = () => {
|
const Edit = () => {
|
||||||
const {
|
const {
|
||||||
config: { accesses },
|
config: { accesses, emails },
|
||||||
} = useConfig();
|
} = useConfig();
|
||||||
|
|
||||||
const [domain, setDomain] = useState<Domain>();
|
const [domain, setDomain] = useState<Domain>();
|
||||||
@@ -62,6 +63,7 @@ const Edit = () => {
|
|||||||
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
|
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
|
||||||
message: "请输入正确的域名",
|
message: "请输入正确的域名",
|
||||||
}),
|
}),
|
||||||
|
email: z.string().email().optional(),
|
||||||
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
|
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
|
||||||
message: "请选择DNS服务商授权配置",
|
message: "请选择DNS服务商授权配置",
|
||||||
}),
|
}),
|
||||||
@@ -78,6 +80,7 @@ const Edit = () => {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: "",
|
id: "",
|
||||||
domain: "",
|
domain: "",
|
||||||
|
email: "",
|
||||||
access: "",
|
access: "",
|
||||||
targetAccess: "",
|
targetAccess: "",
|
||||||
targetType: "",
|
targetType: "",
|
||||||
@@ -89,6 +92,7 @@ const Edit = () => {
|
|||||||
form.reset({
|
form.reset({
|
||||||
id: domain.id,
|
id: domain.id,
|
||||||
domain: domain.domain,
|
domain: domain.domain,
|
||||||
|
email: domain.email,
|
||||||
access: domain.access,
|
access: domain.access,
|
||||||
targetAccess: domain.targetAccess,
|
targetAccess: domain.targetAccess,
|
||||||
targetType: domain.targetType,
|
targetType: domain.targetType,
|
||||||
@@ -99,6 +103,10 @@ const Edit = () => {
|
|||||||
const [targetType, setTargetType] = useState(domain ? domain.targetType : "");
|
const [targetType, setTargetType] = useState(domain ? domain.targetType : "");
|
||||||
|
|
||||||
const targetAccesses = accesses.filter((item) => {
|
const targetAccesses = accesses.filter((item) => {
|
||||||
|
if (item.usage == "apply") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (targetType == "") {
|
if (targetType == "") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -115,6 +123,7 @@ const Edit = () => {
|
|||||||
id: data.id as string,
|
id: data.id as string,
|
||||||
crontab: "0 0 * * *",
|
crontab: "0 0 * * *",
|
||||||
domain: data.domain,
|
domain: data.domain,
|
||||||
|
email: data.email,
|
||||||
access: data.access,
|
access: data.access,
|
||||||
targetAccess: data.targetAccess,
|
targetAccess: data.targetAccess,
|
||||||
targetType: data.targetType,
|
targetType: data.targetType,
|
||||||
@@ -176,6 +185,51 @@ const Edit = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex w-full justify-between">
|
||||||
|
<div>Email(申请证书需要提供邮箱)</div>
|
||||||
|
<EmailsEdit
|
||||||
|
trigger={
|
||||||
|
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
||||||
|
<Plus size={14} />
|
||||||
|
新增
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue("email", value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="请选择邮箱" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>邮箱列表</SelectLabel>
|
||||||
|
{emails.content.emails.map((item) => (
|
||||||
|
<SelectItem key={item} value={item}>
|
||||||
|
<div>{item}</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="access"
|
name="access"
|
||||||
@@ -207,19 +261,21 @@ const Edit = () => {
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>服务商授权配置</SelectLabel>
|
<SelectLabel>服务商授权配置</SelectLabel>
|
||||||
{accesses.map((item) => (
|
{accesses
|
||||||
<SelectItem key={item.id} value={item.id}>
|
.filter((item) => item.usage != "deploy")
|
||||||
<div className="flex items-center space-x-2">
|
.map((item) => (
|
||||||
<img
|
<SelectItem key={item.id} value={item.id}>
|
||||||
className="w-6"
|
<div className="flex items-center space-x-2">
|
||||||
src={
|
<img
|
||||||
accessTypeMap.get(item.configType)?.[1]
|
className="w-6"
|
||||||
}
|
src={
|
||||||
/>
|
accessTypeMap.get(item.configType)?.[1]
|
||||||
<div>{item.name}</div>
|
}
|
||||||
</div>
|
/>
|
||||||
</SelectItem>
|
<div>{item.name}</div>
|
||||||
))}
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -10,9 +10,12 @@ import {
|
|||||||
useReducer,
|
useReducer,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { configReducer } from "./reducer";
|
import { configReducer } from "./reducer";
|
||||||
|
import { getEmails } from "@/repository/settings";
|
||||||
|
import { Setting } from "@/domain/settings";
|
||||||
|
|
||||||
export type ConfigData = {
|
export type ConfigData = {
|
||||||
accesses: Access[];
|
accesses: Access[];
|
||||||
|
emails: Setting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigContext = {
|
export type ConfigContext = {
|
||||||
@@ -20,6 +23,7 @@ export type ConfigContext = {
|
|||||||
deleteAccess: (id: string) => void;
|
deleteAccess: (id: string) => void;
|
||||||
addAccess: (access: Access) => void;
|
addAccess: (access: Access) => void;
|
||||||
updateAccess: (access: Access) => void;
|
updateAccess: (access: Access) => void;
|
||||||
|
setEmails: (email: Setting) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Context = createContext({} as ConfigContext);
|
const Context = createContext({} as ConfigContext);
|
||||||
@@ -31,7 +35,10 @@ interface ContainerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ConfigProvider = ({ children }: ContainerProps) => {
|
export const ConfigProvider = ({ children }: ContainerProps) => {
|
||||||
const [config, dispatchConfig] = useReducer(configReducer, { accesses: [] });
|
const [config, dispatchConfig] = useReducer(configReducer, {
|
||||||
|
accesses: [],
|
||||||
|
emails: { content: { emails: [] } },
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const featchData = async () => {
|
const featchData = async () => {
|
||||||
@@ -41,6 +48,18 @@ export const ConfigProvider = ({ children }: ContainerProps) => {
|
|||||||
featchData();
|
featchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const featchEmails = async () => {
|
||||||
|
const emails = await getEmails();
|
||||||
|
dispatchConfig({ type: "SET_EMAILS", payload: emails });
|
||||||
|
};
|
||||||
|
featchEmails();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setEmails = useCallback((emails: Setting) => {
|
||||||
|
dispatchConfig({ type: "SET_EMAILS", payload: emails });
|
||||||
|
}, []);
|
||||||
|
|
||||||
const deleteAccess = useCallback((id: string) => {
|
const deleteAccess = useCallback((id: string) => {
|
||||||
dispatchConfig({ type: "DELETE_ACCESS", payload: id });
|
dispatchConfig({ type: "DELETE_ACCESS", payload: id });
|
||||||
}, []);
|
}, []);
|
||||||
@@ -58,9 +77,11 @@ export const ConfigProvider = ({ children }: ContainerProps) => {
|
|||||||
value={{
|
value={{
|
||||||
config: {
|
config: {
|
||||||
accesses: config.accesses,
|
accesses: config.accesses,
|
||||||
|
emails: config.emails,
|
||||||
},
|
},
|
||||||
deleteAccess,
|
deleteAccess,
|
||||||
addAccess,
|
addAccess,
|
||||||
|
setEmails,
|
||||||
updateAccess,
|
updateAccess,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { Access } from "@/domain/access";
|
import { Access } from "@/domain/access";
|
||||||
import { ConfigData } from ".";
|
import { ConfigData } from ".";
|
||||||
|
import { Setting } from "@/domain/settings";
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: "ADD_ACCESS"; payload: Access }
|
| { type: "ADD_ACCESS"; payload: Access }
|
||||||
| { type: "DELETE_ACCESS"; payload: string }
|
| { type: "DELETE_ACCESS"; payload: string }
|
||||||
| { type: "UPDATE_ACCESS"; payload: Access }
|
| { type: "UPDATE_ACCESS"; payload: Access }
|
||||||
| { type: "SET_ACCESSES"; payload: Access[] };
|
| { type: "SET_ACCESSES"; payload: Access[] }
|
||||||
|
| { type: "SET_EMAILS"; payload: Setting }
|
||||||
|
| { type: "ADD_EMAIL"; payload: string };
|
||||||
|
|
||||||
export const configReducer = (
|
export const configReducer = (
|
||||||
state: ConfigData,
|
state: ConfigData,
|
||||||
@@ -40,6 +43,23 @@ export const configReducer = (
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "SET_EMAILS": {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
emails: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "ADD_EMAIL": {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
emails: {
|
||||||
|
...state.emails,
|
||||||
|
content: {
|
||||||
|
emails: [...state.emails.content.emails, action.payload],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
26
ui/src/repository/settings.ts
Normal file
26
ui/src/repository/settings.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Setting } from "@/domain/settings";
|
||||||
|
import { getPb } from "./api";
|
||||||
|
|
||||||
|
export const getEmails = async () => {
|
||||||
|
try {
|
||||||
|
const resp = await getPb()
|
||||||
|
.collection("settings")
|
||||||
|
.getFirstListItem<Setting>("name='emails'");
|
||||||
|
return resp;
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
content: { emails: [] },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const update = async (setting: Setting) => {
|
||||||
|
const pb = getPb();
|
||||||
|
let resp: Setting;
|
||||||
|
if (setting.id) {
|
||||||
|
resp = await pb.collection("settings").update(setting.id, setting);
|
||||||
|
} else {
|
||||||
|
resp = await pb.collection("settings").create(setting);
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ module.exports = {
|
|||||||
padding: "2rem",
|
padding: "2rem",
|
||||||
screens: {
|
screens: {
|
||||||
"2xl": "1400px",
|
"2xl": "1400px",
|
||||||
|
'3xl': '1920px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
|
|||||||
Reference in New Issue
Block a user