mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 17:31:55 +08:00
Support deploying one certificate to multiple SSH hosts, and support deploying multiple certificates to one SSH host.
This commit is contained in:
parent
505cfc5c1e
commit
6c1b1fb72b
@ -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 {
|
func (a *aliyun) GetInfo() []string {
|
||||||
return a.infos
|
return a.infos
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,10 @@ func NewAliyunCdn(option *DeployerOption) (*AliyunCdn, error) {
|
|||||||
}, nil
|
}, 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 {
|
func (a *AliyunCdn) GetInfo() []string {
|
||||||
return a.infos
|
return a.infos
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,12 @@ package deployer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"certimate/internal/applicant"
|
"certimate/internal/applicant"
|
||||||
|
"certimate/internal/utils/app"
|
||||||
|
"certimate/internal/utils/variables"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
@ -24,21 +27,81 @@ type DeployerOption struct {
|
|||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
Product string `json:"product"`
|
Product string `json:"product"`
|
||||||
Access string `json:"access"`
|
Access string `json:"access"`
|
||||||
|
AceessRecord *models.Record `json:"-"`
|
||||||
Certificate applicant.Certificate `json:"certificate"`
|
Certificate applicant.Certificate `json:"certificate"`
|
||||||
|
Variables map[string]string `json:"variables"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Deployer interface {
|
type Deployer interface {
|
||||||
Deploy(ctx context.Context) error
|
Deploy(ctx context.Context) error
|
||||||
GetInfo() []string
|
GetInfo() []string
|
||||||
|
GetID() string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
|
func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error) {
|
||||||
access := record.ExpandedOne("targetAccess")
|
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{
|
option := &DeployerOption{
|
||||||
DomainId: record.Id,
|
DomainId: record.Id,
|
||||||
Domain: record.GetString("domain"),
|
Domain: record.GetString("domain"),
|
||||||
Product: getProduct(record),
|
Product: getProduct(record),
|
||||||
Access: access.GetString("config"),
|
Access: access.GetString("config"),
|
||||||
|
AceessRecord: access,
|
||||||
|
Variables: variables.Parse2Map(record.GetString("variables")),
|
||||||
}
|
}
|
||||||
if cert != nil {
|
if cert != nil {
|
||||||
option.Certificate = *cert
|
option.Certificate = *cert
|
||||||
@ -66,6 +129,13 @@ func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
|
|||||||
return nil, errors.New("not implemented")
|
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 {
|
func getProduct(record *models.Record) string {
|
||||||
targetType := record.GetString("targetType")
|
targetType := record.GetString("targetType")
|
||||||
rs := strings.Split(targetType, "-")
|
rs := strings.Split(targetType, "-")
|
||||||
|
@ -33,6 +33,10 @@ func NewQiNiu(option *DeployerOption) (*qiuniu, error) {
|
|||||||
}, nil
|
}, 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 {
|
func (q *qiuniu) GetInfo() []string {
|
||||||
return q.info
|
return q.info
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
xpath "path"
|
xpath "path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
sshPkg "golang.org/x/crypto/ssh"
|
sshPkg "golang.org/x/crypto/ssh"
|
||||||
@ -35,6 +36,10 @@ func NewSSH(option *DeployerOption) (Deployer, error) {
|
|||||||
}, nil
|
}, 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 {
|
func (s *ssh) GetInfo() []string {
|
||||||
return s.infos
|
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 {
|
if err := json.Unmarshal([]byte(s.option.Access), access); err != nil {
|
||||||
return err
|
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)
|
client, err := s.getClient(access)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -39,6 +39,10 @@ func NewTencentCdn(option *DeployerOption) (Deployer, error) {
|
|||||||
}, nil
|
}, 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 {
|
func (t *tencentCdn) GetInfo() []string {
|
||||||
return t.infos
|
return t.infos
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,10 @@ func NewWebhook(option *DeployerOption) (Deployer, error) {
|
|||||||
}, nil
|
}, 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 {
|
func (w *webhook) GetInfo() []string {
|
||||||
return w.infos
|
return w.infos
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ func deploy(ctx context.Context, record *models.Record) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
history.record(checkPhase, "获取记录成功", nil)
|
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)
|
errList := make([]error, 0)
|
||||||
for name, err := range errs {
|
for name, err := range errs {
|
||||||
@ -96,24 +96,28 @@ func deploy(ctx context.Context, record *models.Record) error {
|
|||||||
|
|
||||||
// ############3.部署证书
|
// ############3.部署证书
|
||||||
history.record(deployPhase, "开始部署", nil, false)
|
history.record(deployPhase, "开始部署", nil, false)
|
||||||
deployer, err := deployer.Get(currRecord, certificate)
|
deployers, err := deployer.Gets(currRecord, certificate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
history.record(deployPhase, "获取deployer失败", &RecordInfo{Err: err})
|
history.record(deployPhase, "获取deployer失败", &RecordInfo{Err: err})
|
||||||
app.GetApp().Logger().Error("获取deployer失败", "err", err)
|
app.GetApp().Logger().Error("获取deployer失败", "err", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, deployer := range deployers {
|
||||||
if err = deployer.Deploy(ctx); err != nil {
|
if err = deployer.Deploy(ctx); err != nil {
|
||||||
|
|
||||||
app.GetApp().Logger().Error("部署失败", "err", err)
|
app.GetApp().Logger().Error("部署失败", "err", err)
|
||||||
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfo()})
|
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfo()})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
history.record(deployPhase, fmt.Sprintf("[%s]-部署成功", deployer.GetID()), &RecordInfo{
|
||||||
|
Info: deployer.GetInfo(),
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
app.GetApp().Logger().Info("部署成功")
|
app.GetApp().Logger().Info("部署成功")
|
||||||
history.record(deployPhase, "部署成功", &RecordInfo{
|
history.record(deployPhase, "部署成功", nil, true)
|
||||||
Info: deployer.GetInfo(),
|
|
||||||
}, true)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
30
internal/utils/variables/variables.go
Normal file
30
internal/utils/variables/variables.go
Normal 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
|
||||||
|
}
|
56
internal/utils/variables/variables_test.go
Normal file
56
internal/utils/variables/variables_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
679
migrations/1726299230_collections_snapshot.go
Normal file
679
migrations/1726299230_collections_snapshot.go
Normal 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
|
||||||
|
})
|
||||||
|
}
|
284
ui/dist/assets/index-B_74ZyCB.js
vendored
284
ui/dist/assets/index-B_74ZyCB.js
vendored
File diff suppressed because one or more lines are too long
1
ui/dist/assets/index-Cg0yCJnh.css
vendored
1
ui/dist/assets/index-Cg0yCJnh.css
vendored
File diff suppressed because one or more lines are too long
1
ui/dist/assets/index-DXJTf3ck.css
vendored
Normal file
1
ui/dist/assets/index-DXJTf3ck.css
vendored
Normal file
File diff suppressed because one or more lines are too long
291
ui/dist/assets/index-pPAQ4idS.js
vendored
Normal file
291
ui/dist/assets/index-pPAQ4idS.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
ui/dist/index.html
vendored
4
ui/dist/index.html
vendored
@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Certimate - Your Trusted SSL Automation Partner</title>
|
<title>Certimate - Your Trusted SSL Automation Partner</title>
|
||||||
<script type="module" crossorigin src="/assets/index-B_74ZyCB.js"></script>
|
<script type="module" crossorigin src="/assets/index-pPAQ4idS.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Cg0yCJnh.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DXJTf3ck.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-background">
|
<body class="bg-background">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
120
ui/src/components/certimate/AccessGroupEdit.tsx
Normal file
120
ui/src/components/certimate/AccessGroupEdit.tsx
Normal 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;
|
@ -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 { 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";
|
||||||
@ -19,6 +24,17 @@ 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";
|
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 = ({
|
const AccessSSHForm = ({
|
||||||
data,
|
data,
|
||||||
@ -27,12 +43,19 @@ const AccessSSHForm = ({
|
|||||||
data?: Access;
|
data?: Access;
|
||||||
onAfterReq: () => void;
|
onAfterReq: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { addAccess, updateAccess } = useConfig();
|
const {
|
||||||
|
addAccess,
|
||||||
|
updateAccess,
|
||||||
|
reloadAccessGroups,
|
||||||
|
config: { accessGroups },
|
||||||
|
} = useConfig();
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const [fileName, setFileName] = useState("");
|
const [fileName, setFileName] = useState("");
|
||||||
|
|
||||||
|
const originGroup = data ? (data.group ? data.group : "") : "";
|
||||||
|
|
||||||
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),
|
||||||
@ -40,6 +63,7 @@ const AccessSSHForm = ({
|
|||||||
host: z.string().ip({
|
host: z.string().ip({
|
||||||
message: "请输入合法的IP地址",
|
message: "请输入合法的IP地址",
|
||||||
}),
|
}),
|
||||||
|
group: z.string().optional(),
|
||||||
port: z.string().min(1).max(5),
|
port: z.string().min(1).max(5),
|
||||||
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),
|
||||||
@ -69,6 +93,7 @@ const AccessSSHForm = ({
|
|||||||
id: data?.id,
|
id: data?.id,
|
||||||
name: data?.name,
|
name: data?.name,
|
||||||
configType: "ssh",
|
configType: "ssh",
|
||||||
|
group: data?.group,
|
||||||
host: config.host,
|
host: config.host,
|
||||||
port: config.port,
|
port: config.port,
|
||||||
username: config.username,
|
username: config.username,
|
||||||
@ -83,11 +108,15 @@ const AccessSSHForm = ({
|
|||||||
|
|
||||||
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
let group = data.group;
|
||||||
|
if (group == "emptyId") group = "";
|
||||||
|
|
||||||
const req: Access = {
|
const req: Access = {
|
||||||
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),
|
usage: getUsageByConfigType(data.configType),
|
||||||
|
group: group,
|
||||||
config: {
|
config: {
|
||||||
host: data.host,
|
host: data.host,
|
||||||
port: data.port,
|
port: data.port,
|
||||||
@ -110,9 +139,28 @@ const AccessSSHForm = ({
|
|||||||
req.updated = rs.updated;
|
req.updated = rs.updated;
|
||||||
if (data.id) {
|
if (data.id) {
|
||||||
updateAccess(req);
|
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) {
|
} catch (e) {
|
||||||
const err = e as ClientResponseError;
|
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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="id"
|
name="id"
|
||||||
|
@ -11,6 +11,10 @@ export const accessTypeMap: Map<string, [string, string]> = new Map([
|
|||||||
["webhook", ["Webhook", "/imgs/providers/webhook.svg"]],
|
["webhook", ["Webhook", "/imgs/providers/webhook.svg"]],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const getProviderInfo = (t: string) => {
|
||||||
|
return accessTypeMap.get(t);
|
||||||
|
};
|
||||||
|
|
||||||
export const accessFormType = z.union(
|
export const accessFormType = z.union(
|
||||||
[
|
[
|
||||||
z.literal("aliyun"),
|
z.literal("aliyun"),
|
||||||
@ -32,6 +36,7 @@ export type Access = {
|
|||||||
name: string;
|
name: string;
|
||||||
configType: string;
|
configType: string;
|
||||||
usage: AccessUsage;
|
usage: AccessUsage;
|
||||||
|
group?: string;
|
||||||
config:
|
config:
|
||||||
| TencentConfig
|
| TencentConfig
|
||||||
| AliyunConfig
|
| AliyunConfig
|
||||||
|
10
ui/src/domain/access_groups.ts
Normal file
10
ui/src/domain/access_groups.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Access } from "./access";
|
||||||
|
|
||||||
|
export type AccessGroup = {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
access?: string[];
|
||||||
|
expand?: {
|
||||||
|
access: Access[];
|
||||||
|
};
|
||||||
|
};
|
@ -6,12 +6,14 @@ export type Domain = {
|
|||||||
email?: string;
|
email?: string;
|
||||||
crontab: string;
|
crontab: string;
|
||||||
access: string;
|
access: string;
|
||||||
targetAccess: string;
|
targetAccess?: string;
|
||||||
targetType: string;
|
targetType: string;
|
||||||
expiredAt?: string;
|
expiredAt?: string;
|
||||||
phase?: Pahse;
|
phase?: Pahse;
|
||||||
phaseSuccess?: boolean;
|
phaseSuccess?: boolean;
|
||||||
lastDeployedAt?: string;
|
lastDeployedAt?: string;
|
||||||
|
variables?: string;
|
||||||
|
group?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
created?: string;
|
created?: string;
|
||||||
updated?: string;
|
updated?: string;
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
BookOpen,
|
BookOpen,
|
||||||
CircleUser,
|
CircleUser,
|
||||||
Earth,
|
Earth,
|
||||||
|
Group,
|
||||||
History,
|
History,
|
||||||
Home,
|
Home,
|
||||||
Menu,
|
Menu,
|
||||||
@ -100,6 +101,17 @@ export default function Dashboard() {
|
|||||||
授权管理
|
授权管理
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/access_groups"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary",
|
||||||
|
getClass("/access_groups")
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Group className="h-4 w-4" />
|
||||||
|
部署授权组
|
||||||
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="/history"
|
to="/history"
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -161,13 +173,24 @@ export default function Dashboard() {
|
|||||||
to="/access"
|
to="/access"
|
||||||
className={cn(
|
className={cn(
|
||||||
"mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground",
|
"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" />
|
<Server className="h-5 w-5" />
|
||||||
授权管理
|
授权管理
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/access_groups"
|
||||||
|
className={cn(
|
||||||
|
"mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground",
|
||||||
|
getClass("/access_groups")
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Group className="h-5 w-5" />
|
||||||
|
部署授权组
|
||||||
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="/history"
|
to="/history"
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -227,7 +250,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.5
|
Certimate v0.1.6
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,6 +23,8 @@ const Access = () => {
|
|||||||
const page = query.get("page");
|
const page = query.get("page");
|
||||||
const pageNumber = page ? Number(page) : 1;
|
const pageNumber = page ? Number(page) : 1;
|
||||||
|
|
||||||
|
const accessGroupId = query.get("accessGroupId");
|
||||||
|
|
||||||
const startIndex = (pageNumber - 1) * perPage;
|
const startIndex = (pageNumber - 1) * perPage;
|
||||||
const endIndex = startIndex + perPage;
|
const endIndex = startIndex + perPage;
|
||||||
|
|
||||||
@ -65,7 +67,12 @@ const Access = () => {
|
|||||||
<div className="sm:hidden flex text-sm text-muted-foreground">
|
<div className="sm:hidden flex text-sm text-muted-foreground">
|
||||||
授权列表
|
授权列表
|
||||||
</div>
|
</div>
|
||||||
{accesses.slice(startIndex, endIndex).map((access) => (
|
{accesses
|
||||||
|
.filter((item) => {
|
||||||
|
return accessGroupId ? item.group == accessGroupId : true;
|
||||||
|
})
|
||||||
|
.slice(startIndex, endIndex)
|
||||||
|
.map((access) => (
|
||||||
<div
|
<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"
|
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}
|
key={access.id}
|
||||||
|
219
ui/src/pages/access_groups/AccessGroups.tsx
Normal file
219
ui/src/pages/access_groups/AccessGroups.tsx
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
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 { Toaster } from "@/components/ui/toaster";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
const AccessGroups = () => {
|
||||||
|
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>
|
||||||
|
<Toaster />
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="text-muted-foreground">部署授权组</div>
|
||||||
|
|
||||||
|
<AccessGroupEdit trigger={<Button>新增授权组</Button>} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
所有授权
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccessGroups;
|
@ -35,16 +35,22 @@ 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";
|
import EmailsEdit from "@/components/certimate/EmailsEdit";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Edit = () => {
|
const Edit = () => {
|
||||||
const {
|
const {
|
||||||
config: { accesses, emails },
|
config: { accesses, emails, accessGroups },
|
||||||
} = useConfig();
|
} = useConfig();
|
||||||
|
|
||||||
const [domain, setDomain] = useState<Domain>();
|
const [domain, setDomain] = useState<Domain>();
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
const [tab, setTab] = useState<"base" | "advance">("base");
|
||||||
|
|
||||||
|
const [targetType, setTargetType] = useState(domain ? domain.targetType : "");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Parsing query parameters
|
// Parsing query parameters
|
||||||
const queryParams = new URLSearchParams(location.search);
|
const queryParams = new URLSearchParams(location.search);
|
||||||
@ -53,6 +59,7 @@ const Edit = () => {
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const data = await get(id);
|
const data = await get(id);
|
||||||
setDomain(data);
|
setDomain(data);
|
||||||
|
setTargetType(data.targetType);
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}
|
}
|
||||||
@ -67,12 +74,12 @@ const Edit = () => {
|
|||||||
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
|
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
|
||||||
message: "请选择DNS服务商授权配置",
|
message: "请选择DNS服务商授权配置",
|
||||||
}),
|
}),
|
||||||
targetAccess: z.string().regex(/^[a-zA-Z0-9]+$/, {
|
targetAccess: z.string().optional(),
|
||||||
message: "请选择部署服务商配置",
|
|
||||||
}),
|
|
||||||
targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, {
|
targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, {
|
||||||
message: "请选择部署服务类型",
|
message: "请选择部署服务类型",
|
||||||
}),
|
}),
|
||||||
|
variables: z.string().optional(),
|
||||||
|
group: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
@ -84,6 +91,8 @@ const Edit = () => {
|
|||||||
access: "",
|
access: "",
|
||||||
targetAccess: "",
|
targetAccess: "",
|
||||||
targetType: "",
|
targetType: "",
|
||||||
|
variables: "",
|
||||||
|
group: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -96,12 +105,12 @@ const Edit = () => {
|
|||||||
access: domain.access,
|
access: domain.access,
|
||||||
targetAccess: domain.targetAccess,
|
targetAccess: domain.targetAccess,
|
||||||
targetType: domain.targetType,
|
targetType: domain.targetType,
|
||||||
|
variables: domain.variables,
|
||||||
|
group: domain.group,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [domain, form]);
|
}, [domain, form]);
|
||||||
|
|
||||||
const [targetType, setTargetType] = useState(domain ? domain.targetType : "");
|
|
||||||
|
|
||||||
const targetAccesses = accesses.filter((item) => {
|
const targetAccesses = accesses.filter((item) => {
|
||||||
if (item.usage == "apply") {
|
if (item.usage == "apply") {
|
||||||
return false;
|
return false;
|
||||||
@ -110,7 +119,7 @@ const Edit = () => {
|
|||||||
if (targetType == "") {
|
if (targetType == "") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const types = form.getValues().targetType.split("-");
|
const types = targetType.split("-");
|
||||||
return item.configType === types[0];
|
return item.configType === types[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -119,14 +128,31 @@ const Edit = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
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 = {
|
const req: Domain = {
|
||||||
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,
|
email: data.email,
|
||||||
access: data.access,
|
access: data.access,
|
||||||
targetAccess: data.targetAccess,
|
group: group,
|
||||||
|
targetAccess: targetAccess,
|
||||||
targetType: data.targetType,
|
targetType: data.targetType,
|
||||||
|
variables: data.variables,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -161,10 +187,36 @@ const Edit = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="">
|
<div className="">
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<div className="border-b dark:border-stone-500 h-10 text-muted-foreground">
|
<div className=" h-5 text-muted-foreground">
|
||||||
{domain?.id ? "编辑" : "新增"}域名
|
{domain?.id ? "编辑" : "新增"}域名
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-[35em] mx-auto mt-10">
|
<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");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
基础设置
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"cursor-pointer text-right",
|
||||||
|
tab === "advance" ? "text-primary" : ""
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setTab("advance");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
高级设置
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full md:w-[35em] bg-gray-100 dark:bg-gray-900 p-5 rounded mt-3 md:mt-0">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
@ -174,7 +226,7 @@ const Edit = () => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="domain"
|
name="domain"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem hidden={tab != "base"}>
|
||||||
<FormLabel>域名</FormLabel>
|
<FormLabel>域名</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="请输入域名" {...field} />
|
<Input placeholder="请输入域名" {...field} />
|
||||||
@ -184,12 +236,11 @@ const Edit = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="email"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem hidden={tab != "base"}>
|
||||||
<FormLabel className="flex w-full justify-between">
|
<FormLabel className="flex w-full justify-between">
|
||||||
<div>Email(申请证书需要提供邮箱)</div>
|
<div>Email(申请证书需要提供邮箱)</div>
|
||||||
<EmailsEdit
|
<EmailsEdit
|
||||||
@ -229,12 +280,11 @@ const Edit = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="access"
|
name="access"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem hidden={tab != "base"}>
|
||||||
<FormLabel className="flex w-full justify-between">
|
<FormLabel className="flex w-full justify-between">
|
||||||
<div>DNS 服务商授权配置</div>
|
<div>DNS 服务商授权配置</div>
|
||||||
<AccessEdit
|
<AccessEdit
|
||||||
@ -269,7 +319,9 @@ const Edit = () => {
|
|||||||
<img
|
<img
|
||||||
className="w-6"
|
className="w-6"
|
||||||
src={
|
src={
|
||||||
accessTypeMap.get(item.configType)?.[1]
|
accessTypeMap.get(
|
||||||
|
item.configType
|
||||||
|
)?.[1]
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div>{item.name}</div>
|
<div>{item.name}</div>
|
||||||
@ -285,12 +337,11 @@ const Edit = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="targetType"
|
name="targetType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem hidden={tab != "base"}>
|
||||||
<FormLabel>部署服务类型</FormLabel>
|
<FormLabel>部署服务类型</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
@ -326,12 +377,11 @@ const Edit = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="targetAccess"
|
name="targetAccess"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem hidden={tab != "base"}>
|
||||||
<FormLabel className="w-full flex justify-between">
|
<FormLabel className="w-full flex justify-between">
|
||||||
<div>部署服务商授权配置</div>
|
<div>部署服务商授权配置</div>
|
||||||
<AccessEdit
|
<AccessEdit
|
||||||
@ -356,7 +406,14 @@ const Edit = () => {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>服务商授权配置</SelectLabel>
|
<SelectLabel>
|
||||||
|
服务商授权配置{form.getValues().targetAccess}
|
||||||
|
</SelectLabel>
|
||||||
|
<SelectItem value="emptyId">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
--
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
{targetAccesses.map((item) => (
|
{targetAccesses.map((item) => (
|
||||||
<SelectItem key={item.id} value={item.id}>
|
<SelectItem key={item.id} value={item.id}>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@ -380,6 +437,84 @@ const Edit = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="variables"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem hidden={tab != "advance"}>
|
||||||
|
<FormLabel>变量</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder={`可在SSH部署中使用,形如:\nkey=val;\nkey2=val2;`}
|
||||||
|
{...field}
|
||||||
|
className="placeholder:whitespace-pre-wrap"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit">保存</Button>
|
<Button type="submit">保存</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -387,6 +522,7 @@ const Edit = () => {
|
|||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Access } from "@/domain/access";
|
import { Access } from "@/domain/access";
|
||||||
import { list } from "@/repository/access";
|
import { list } from "@/repository/access";
|
||||||
|
import { list as getAccessGroups } from "@/repository/access_group";
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
@ -12,10 +12,12 @@ import {
|
|||||||
import { configReducer } from "./reducer";
|
import { configReducer } from "./reducer";
|
||||||
import { getEmails } from "@/repository/settings";
|
import { getEmails } from "@/repository/settings";
|
||||||
import { Setting } from "@/domain/settings";
|
import { Setting } from "@/domain/settings";
|
||||||
|
import { AccessGroup } from "@/domain/access_groups";
|
||||||
|
|
||||||
export type ConfigData = {
|
export type ConfigData = {
|
||||||
accesses: Access[];
|
accesses: Access[];
|
||||||
emails: Setting;
|
emails: Setting;
|
||||||
|
accessGroups: AccessGroup[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigContext = {
|
export type ConfigContext = {
|
||||||
@ -24,6 +26,8 @@ export type ConfigContext = {
|
|||||||
addAccess: (access: Access) => void;
|
addAccess: (access: Access) => void;
|
||||||
updateAccess: (access: Access) => void;
|
updateAccess: (access: Access) => void;
|
||||||
setEmails: (email: Setting) => void;
|
setEmails: (email: Setting) => void;
|
||||||
|
setAccessGroups: (accessGroups: AccessGroup[]) => void;
|
||||||
|
reloadAccessGroups: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Context = createContext({} as ConfigContext);
|
const Context = createContext({} as ConfigContext);
|
||||||
@ -38,6 +42,7 @@ export const ConfigProvider = ({ children }: ContainerProps) => {
|
|||||||
const [config, dispatchConfig] = useReducer(configReducer, {
|
const [config, dispatchConfig] = useReducer(configReducer, {
|
||||||
accesses: [],
|
accesses: [],
|
||||||
emails: { content: { emails: [] } },
|
emails: { content: { emails: [] } },
|
||||||
|
accessGroups: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -56,6 +61,19 @@ export const ConfigProvider = ({ children }: ContainerProps) => {
|
|||||||
featchEmails();
|
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) => {
|
const setEmails = useCallback((emails: Setting) => {
|
||||||
dispatchConfig({ type: "SET_EMAILS", payload: emails });
|
dispatchConfig({ type: "SET_EMAILS", payload: emails });
|
||||||
}, []);
|
}, []);
|
||||||
@ -72,17 +90,24 @@ export const ConfigProvider = ({ children }: ContainerProps) => {
|
|||||||
dispatchConfig({ type: "UPDATE_ACCESS", payload: access });
|
dispatchConfig({ type: "UPDATE_ACCESS", payload: access });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const setAccessGroups = useCallback((accessGroups: AccessGroup[]) => {
|
||||||
|
dispatchConfig({ type: "SET_ACCESS_GROUPS", payload: accessGroups });
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Context.Provider
|
<Context.Provider
|
||||||
value={{
|
value={{
|
||||||
config: {
|
config: {
|
||||||
accesses: config.accesses,
|
accesses: config.accesses,
|
||||||
emails: config.emails,
|
emails: config.emails,
|
||||||
|
accessGroups: config.accessGroups,
|
||||||
},
|
},
|
||||||
deleteAccess,
|
deleteAccess,
|
||||||
addAccess,
|
addAccess,
|
||||||
setEmails,
|
setEmails,
|
||||||
updateAccess,
|
updateAccess,
|
||||||
|
setAccessGroups,
|
||||||
|
reloadAccessGroups,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children && children}
|
{children && children}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Access } from "@/domain/access";
|
import { Access } from "@/domain/access";
|
||||||
import { ConfigData } from ".";
|
import { ConfigData } from ".";
|
||||||
import { Setting } from "@/domain/settings";
|
import { Setting } from "@/domain/settings";
|
||||||
|
import { AccessGroup } from "@/domain/access_groups";
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: "ADD_ACCESS"; payload: Access }
|
| { type: "ADD_ACCESS"; payload: Access }
|
||||||
@ -8,7 +9,8 @@ type Action =
|
|||||||
| { 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: "SET_EMAILS"; payload: Setting }
|
||||||
| { type: "ADD_EMAIL"; payload: string };
|
| { type: "ADD_EMAIL"; payload: string }
|
||||||
|
| { type: "SET_ACCESS_GROUPS"; payload: AccessGroup[] };
|
||||||
|
|
||||||
export const configReducer = (
|
export const configReducer = (
|
||||||
state: ConfigData,
|
state: ConfigData,
|
||||||
@ -60,6 +62,12 @@ export const configReducer = (
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "SET_ACCESS_GROUPS": {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
accessGroups: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
49
ui/src/repository/access_group.ts
Normal file
49
ui/src/repository/access_group.ts
Normal 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);
|
||||||
|
};
|
@ -10,6 +10,7 @@ import LoginLayout from "./pages/LoginLayout";
|
|||||||
import Password from "./pages/setting/Password";
|
import Password from "./pages/setting/Password";
|
||||||
import SettingLayout from "./pages/SettingLayout";
|
import SettingLayout from "./pages/SettingLayout";
|
||||||
import Dashboard from "./pages/dashboard/Dashboard";
|
import Dashboard from "./pages/dashboard/Dashboard";
|
||||||
|
import AccessGroups from "./pages/access_groups/AccessGroups";
|
||||||
|
|
||||||
export const router = createHashRouter([
|
export const router = createHashRouter([
|
||||||
{
|
{
|
||||||
@ -32,6 +33,10 @@ export const router = createHashRouter([
|
|||||||
path: "/access",
|
path: "/access",
|
||||||
element: <Access />,
|
element: <Access />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/access_groups",
|
||||||
|
element: <AccessGroups />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/history",
|
path: "/history",
|
||||||
element: <History />,
|
element: <History />,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user