mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 17:31:55 +08:00
v0.2
This commit is contained in:
parent
19f5348802
commit
1928a47961
@ -3,6 +3,7 @@ package applicant
|
|||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
||||||
@ -25,6 +26,7 @@ func (a *aliyun) Apply() (*Certificate, error) {
|
|||||||
|
|
||||||
os.Setenv("ALICLOUD_ACCESS_KEY", access.AccessKeyId)
|
os.Setenv("ALICLOUD_ACCESS_KEY", access.AccessKeyId)
|
||||||
os.Setenv("ALICLOUD_SECRET_KEY", access.AccessKeySecret)
|
os.Setenv("ALICLOUD_SECRET_KEY", access.AccessKeySecret)
|
||||||
|
os.Setenv("ALICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||||
dnsProvider, err := alidns.NewDNSProvider()
|
dnsProvider, err := alidns.NewDNSProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package applicant
|
package applicant
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"certimate/internal/domain"
|
||||||
"certimate/internal/utils/app"
|
"certimate/internal/utils/app"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
@ -46,6 +47,8 @@ var sslProviderUrls = map[string]string{
|
|||||||
|
|
||||||
const defaultEmail = "536464346@qq.com"
|
const defaultEmail = "536464346@qq.com"
|
||||||
|
|
||||||
|
const defaultTimeout = 60
|
||||||
|
|
||||||
type Certificate struct {
|
type Certificate struct {
|
||||||
CertUrl string `json:"certUrl"`
|
CertUrl string `json:"certUrl"`
|
||||||
CertStableUrl string `json:"certStableUrl"`
|
CertStableUrl string `json:"certStableUrl"`
|
||||||
@ -60,6 +63,7 @@ type ApplyOption struct {
|
|||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
Access string `json:"access"`
|
Access string `json:"access"`
|
||||||
Nameservers string `json:"nameservers"`
|
Nameservers string `json:"nameservers"`
|
||||||
|
Timeout int64 `json:"timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MyUser struct {
|
type MyUser struct {
|
||||||
@ -83,8 +87,22 @@ type Applicant interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Get(record *models.Record) (Applicant, error) {
|
func Get(record *models.Record) (Applicant, error) {
|
||||||
access := record.ExpandedOne("access")
|
|
||||||
email := record.GetString("email")
|
if record.GetString("applyConfig") == "" {
|
||||||
|
return nil, errors.New("apply config is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
applyConfig := &domain.ApplyConfig{}
|
||||||
|
|
||||||
|
record.UnmarshalJSONField("applyConfig", applyConfig)
|
||||||
|
|
||||||
|
access, err := app.GetApp().Dao().FindRecordById("access", applyConfig.Access)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("access record not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
email := applyConfig.Email
|
||||||
if email == "" {
|
if email == "" {
|
||||||
email = defaultEmail
|
email = defaultEmail
|
||||||
}
|
}
|
||||||
@ -92,7 +110,8 @@ func Get(record *models.Record) (Applicant, error) {
|
|||||||
Email: email,
|
Email: email,
|
||||||
Domain: record.GetString("domain"),
|
Domain: record.GetString("domain"),
|
||||||
Access: access.GetString("config"),
|
Access: access.GetString("config"),
|
||||||
Nameservers: record.GetString("nameservers"),
|
Nameservers: applyConfig.Nameservers,
|
||||||
|
Timeout: applyConfig.Timeout,
|
||||||
}
|
}
|
||||||
switch access.GetString("configType") {
|
switch access.GetString("configType") {
|
||||||
case configTypeAliyun:
|
case configTypeAliyun:
|
||||||
|
@ -3,6 +3,7 @@ package applicant
|
|||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
cf "github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
cf "github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||||
@ -23,6 +24,7 @@ func (c *cloudflare) Apply() (*Certificate, error) {
|
|||||||
json.Unmarshal([]byte(c.option.Access), access)
|
json.Unmarshal([]byte(c.option.Access), access)
|
||||||
|
|
||||||
os.Setenv("CLOUDFLARE_DNS_API_TOKEN", access.DnsApiToken)
|
os.Setenv("CLOUDFLARE_DNS_API_TOKEN", access.DnsApiToken)
|
||||||
|
os.Setenv("CLOUDFLARE_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", c.option.Timeout))
|
||||||
|
|
||||||
provider, err := cf.NewDNSProvider()
|
provider, err := cf.NewDNSProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -3,6 +3,7 @@ package applicant
|
|||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
godaddyProvider "github.com/go-acme/lego/v4/providers/dns/godaddy"
|
godaddyProvider "github.com/go-acme/lego/v4/providers/dns/godaddy"
|
||||||
@ -25,6 +26,7 @@ func (a *godaddy) Apply() (*Certificate, error) {
|
|||||||
|
|
||||||
os.Setenv("GODADDY_API_KEY", access.ApiKey)
|
os.Setenv("GODADDY_API_KEY", access.ApiKey)
|
||||||
os.Setenv("GODADDY_API_SECRET", access.ApiSecret)
|
os.Setenv("GODADDY_API_SECRET", access.ApiSecret)
|
||||||
|
os.Setenv("GODADDY_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||||
|
|
||||||
dnsProvider, err := godaddyProvider.NewDNSProvider()
|
dnsProvider, err := godaddyProvider.NewDNSProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -3,6 +3,7 @@ package applicant
|
|||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
huaweicloudProvider "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
|
huaweicloudProvider "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
|
||||||
@ -26,6 +27,8 @@ func (t *huaweicloud) Apply() (*Certificate, error) {
|
|||||||
os.Setenv("HUAWEICLOUD_REGION", access.Region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
|
os.Setenv("HUAWEICLOUD_REGION", access.Region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
|
||||||
os.Setenv("HUAWEICLOUD_ACCESS_KEY_ID", access.AccessKeyId)
|
os.Setenv("HUAWEICLOUD_ACCESS_KEY_ID", access.AccessKeyId)
|
||||||
os.Setenv("HUAWEICLOUD_SECRET_ACCESS_KEY", access.SecretAccessKey)
|
os.Setenv("HUAWEICLOUD_SECRET_ACCESS_KEY", access.SecretAccessKey)
|
||||||
|
os.Setenv("HUAWEICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
|
||||||
|
|
||||||
dnsProvider, err := huaweicloudProvider.NewDNSProvider()
|
dnsProvider, err := huaweicloudProvider.NewDNSProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -3,6 +3,7 @@ package applicant
|
|||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
namesiloProvider "github.com/go-acme/lego/v4/providers/dns/namesilo"
|
namesiloProvider "github.com/go-acme/lego/v4/providers/dns/namesilo"
|
||||||
@ -24,6 +25,7 @@ func (a *namesilo) Apply() (*Certificate, error) {
|
|||||||
json.Unmarshal([]byte(a.option.Access), access)
|
json.Unmarshal([]byte(a.option.Access), access)
|
||||||
|
|
||||||
os.Setenv("NAMESILO_API_KEY", access.ApiKey)
|
os.Setenv("NAMESILO_API_KEY", access.ApiKey)
|
||||||
|
os.Setenv("NAMESILO_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||||
|
|
||||||
dnsProvider, err := namesiloProvider.NewDNSProvider()
|
dnsProvider, err := namesiloProvider.NewDNSProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -3,6 +3,7 @@ package applicant
|
|||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
||||||
@ -25,6 +26,8 @@ func (t *tencent) Apply() (*Certificate, error) {
|
|||||||
|
|
||||||
os.Setenv("TENCENTCLOUD_SECRET_ID", access.SecretId)
|
os.Setenv("TENCENTCLOUD_SECRET_ID", access.SecretId)
|
||||||
os.Setenv("TENCENTCLOUD_SECRET_KEY", access.SecretKey)
|
os.Setenv("TENCENTCLOUD_SECRET_KEY", access.SecretKey)
|
||||||
|
os.Setenv("TENCENTCLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
|
||||||
|
|
||||||
dnsProvider, err := tencentcloud.NewDNSProvider()
|
dnsProvider, err := tencentcloud.NewDNSProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -190,7 +190,7 @@ func (a *aliyun) resource() (*cas20200407.ListCloudResourcesResponseBodyData, er
|
|||||||
|
|
||||||
listCloudResourcesRequest := &cas20200407.ListCloudResourcesRequest{
|
listCloudResourcesRequest := &cas20200407.ListCloudResourcesRequest{
|
||||||
CloudProduct: tea.String(a.option.Product),
|
CloudProduct: tea.String(a.option.Product),
|
||||||
Keyword: tea.String(a.option.Domain),
|
Keyword: tea.String(getDeployString(a.option.DeployConfig, "domain")),
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := a.client.ListCloudResources(listCloudResourcesRequest)
|
resp, err := a.client.ListCloudResources(listCloudResourcesRequest)
|
||||||
|
@ -2,6 +2,7 @@ package deployer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
|
"certimate/internal/utils/rand"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -46,9 +47,9 @@ func (a *AliyunCdn) GetInfo() []string {
|
|||||||
|
|
||||||
func (a *AliyunCdn) Deploy(ctx context.Context) error {
|
func (a *AliyunCdn) Deploy(ctx context.Context) error {
|
||||||
|
|
||||||
certName := fmt.Sprintf("%s-%s", a.option.Domain, a.option.DomainId)
|
certName := fmt.Sprintf("%s-%s-%s", a.option.Domain, a.option.DomainId, rand.RandStr(6))
|
||||||
setCdnDomainSSLCertificateRequest := &cdn20180510.SetCdnDomainSSLCertificateRequest{
|
setCdnDomainSSLCertificateRequest := &cdn20180510.SetCdnDomainSSLCertificateRequest{
|
||||||
DomainName: tea.String(a.option.Domain),
|
DomainName: tea.String(getDeployString(a.option.DeployConfig, "domain")),
|
||||||
CertName: tea.String(certName),
|
CertName: tea.String(certName),
|
||||||
CertType: tea.String("upload"),
|
CertType: tea.String("upload"),
|
||||||
SSLProtocol: tea.String("on"),
|
SSLProtocol: tea.String("on"),
|
||||||
|
@ -7,6 +7,7 @@ package deployer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"certimate/internal/domain"
|
"certimate/internal/domain"
|
||||||
|
"certimate/internal/utils/rand"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -51,9 +52,9 @@ func (a *AliyunEsa) GetInfo() []string {
|
|||||||
|
|
||||||
func (a *AliyunEsa) Deploy(ctx context.Context) error {
|
func (a *AliyunEsa) Deploy(ctx context.Context) error {
|
||||||
|
|
||||||
certName := fmt.Sprintf("%s-%s", a.option.Domain, a.option.DomainId)
|
certName := fmt.Sprintf("%s-%s-%s", a.option.Domain, a.option.DomainId, rand.RandStr(6))
|
||||||
setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{
|
setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{
|
||||||
DomainName: tea.String(a.option.Domain),
|
DomainName: tea.String(getDeployString(a.option.DeployConfig, "domain")),
|
||||||
CertName: tea.String(certName),
|
CertName: tea.String(certName),
|
||||||
CertType: tea.String("upload"),
|
CertType: tea.String("upload"),
|
||||||
SSLProtocol: tea.String("on"),
|
SSLProtocol: tea.String("on"),
|
||||||
|
@ -2,8 +2,8 @@ package deployer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"certimate/internal/applicant"
|
"certimate/internal/applicant"
|
||||||
|
"certimate/internal/domain"
|
||||||
"certimate/internal/utils/app"
|
"certimate/internal/utils/app"
|
||||||
"certimate/internal/utils/variables"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@ -30,6 +30,7 @@ type DeployerOption struct {
|
|||||||
Product string `json:"product"`
|
Product string `json:"product"`
|
||||||
Access string `json:"access"`
|
Access string `json:"access"`
|
||||||
AceessRecord *models.Record `json:"-"`
|
AceessRecord *models.Record `json:"-"`
|
||||||
|
DeployConfig domain.DeployConfig `json:"deployConfig"`
|
||||||
Certificate applicant.Certificate `json:"certificate"`
|
Certificate applicant.Certificate `json:"certificate"`
|
||||||
Variables map[string]string `json:"variables"`
|
Variables map[string]string `json:"variables"`
|
||||||
}
|
}
|
||||||
@ -42,52 +43,29 @@ type Deployer interface {
|
|||||||
|
|
||||||
func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error) {
|
func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error) {
|
||||||
rs := make([]Deployer, 0)
|
rs := make([]Deployer, 0)
|
||||||
|
if record.GetString("deployConfig") == "" {
|
||||||
if record.GetString("targetAccess") != "" {
|
return rs, nil
|
||||||
singleDeployer, err := Get(record, cert)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rs = append(rs, singleDeployer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if record.GetString("group") != "" {
|
deployConfigs := make([]domain.DeployConfig, 0)
|
||||||
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...)
|
|
||||||
|
|
||||||
|
err := record.UnmarshalJSONField("deployConfig", &deployConfigs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("解析部署配置失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return rs, nil
|
if len(deployConfigs) == 0 {
|
||||||
|
return rs, nil
|
||||||
|
}
|
||||||
|
|
||||||
}
|
for _, deployConfig := range deployConfigs {
|
||||||
|
|
||||||
func getByGroup(record *models.Record, cert *applicant.Certificate, accesses ...*models.Record) ([]Deployer, error) {
|
deployer, err := getWithDeployConfig(record, cert, deployConfig)
|
||||||
|
|
||||||
rs := make([]Deployer, 0)
|
|
||||||
|
|
||||||
for _, access := range accesses {
|
|
||||||
deployer, err := getWithAccess(record, cert, access)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rs = append(rs, deployer)
|
rs = append(rs, deployer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,15 +73,21 @@ func getByGroup(record *models.Record, cert *applicant.Certificate, accesses ...
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWithAccess(record *models.Record, cert *applicant.Certificate, access *models.Record) (Deployer, error) {
|
func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, deployConfig domain.DeployConfig) (Deployer, error) {
|
||||||
|
|
||||||
|
access, err := app.GetApp().Dao().FindRecordById("access", deployConfig.Access)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("access record not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
option := &DeployerOption{
|
option := &DeployerOption{
|
||||||
DomainId: record.Id,
|
DomainId: record.Id,
|
||||||
Domain: record.GetString("domain"),
|
Domain: record.GetString("domain"),
|
||||||
Product: getProduct(record),
|
Product: getProduct(deployConfig.Type),
|
||||||
Access: access.GetString("config"),
|
Access: access.GetString("config"),
|
||||||
AceessRecord: access,
|
AceessRecord: access,
|
||||||
Variables: variables.Parse2Map(record.GetString("variables")),
|
DeployConfig: deployConfig,
|
||||||
}
|
}
|
||||||
if cert != nil {
|
if cert != nil {
|
||||||
option.Certificate = *cert
|
option.Certificate = *cert
|
||||||
@ -114,7 +98,7 @@ func getWithAccess(record *models.Record, cert *applicant.Certificate, access *m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch record.GetString("targetType") {
|
switch deployConfig.Type {
|
||||||
case targetAliyunOss:
|
case targetAliyunOss:
|
||||||
return NewAliyun(option)
|
return NewAliyun(option)
|
||||||
case targetAliyunCdn:
|
case targetAliyunCdn:
|
||||||
@ -136,16 +120,8 @@ func getWithAccess(record *models.Record, cert *applicant.Certificate, access *m
|
|||||||
return nil, errors.New("not implemented")
|
return nil, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
|
func getProduct(t string) string {
|
||||||
|
rs := strings.Split(t, "-")
|
||||||
access := record.ExpandedOne("targetAccess")
|
|
||||||
|
|
||||||
return getWithAccess(record, cert, access)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getProduct(record *models.Record) string {
|
|
||||||
targetType := record.GetString("targetType")
|
|
||||||
rs := strings.Split(targetType, "-")
|
|
||||||
if len(rs) < 2 {
|
if len(rs) < 2 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -159,3 +135,39 @@ func toStr(tag string, data any) string {
|
|||||||
byts, _ := json.Marshal(data)
|
byts, _ := json.Marshal(data)
|
||||||
return tag + ":" + string(byts)
|
return tag + ":" + string(byts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDeployString(conf domain.DeployConfig, key string) string {
|
||||||
|
if _, ok := conf.Config[key]; !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val, ok := conf.Config[key].(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDeployVariables(conf domain.DeployConfig) map[string]string {
|
||||||
|
rs := make(map[string]string)
|
||||||
|
data, ok := conf.Config["variables"]
|
||||||
|
if !ok {
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
|
||||||
|
bts, _ := json.Marshal(data)
|
||||||
|
|
||||||
|
kvData := make([]domain.KV, 0)
|
||||||
|
|
||||||
|
if err := json.Unmarshal(bts, &kvData); err != nil {
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kv := range kvData {
|
||||||
|
rs[kv.Key] = kv.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return rs
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -11,9 +11,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type localAccess struct {
|
type localAccess struct {
|
||||||
Command string `json:"command"`
|
|
||||||
CertPath string `json:"certPath"`
|
|
||||||
KeyPath string `json:"keyPath"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type local struct {
|
type local struct {
|
||||||
@ -41,18 +38,27 @@ func (l *local) Deploy(ctx context.Context) error {
|
|||||||
if err := json.Unmarshal([]byte(l.option.Access), access); err != nil {
|
if err := json.Unmarshal([]byte(l.option.Access), access); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preCommand := getDeployString(l.option.DeployConfig, "preCommand")
|
||||||
|
|
||||||
|
if preCommand != "" {
|
||||||
|
if err := execCmd(preCommand); err != nil {
|
||||||
|
return fmt.Errorf("执行前置命令失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 复制文件
|
// 复制文件
|
||||||
if err := copyFile(l.option.Certificate.Certificate, access.CertPath); err != nil {
|
if err := copyFile(l.option.Certificate.Certificate, getDeployString(l.option.DeployConfig, "certPath")); err != nil {
|
||||||
return fmt.Errorf("复制证书失败: %w", err)
|
return fmt.Errorf("复制证书失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := copyFile(l.option.Certificate.PrivateKey, access.KeyPath); err != nil {
|
if err := copyFile(l.option.Certificate.PrivateKey, getDeployString(l.option.DeployConfig, "keyPath")); err != nil {
|
||||||
return fmt.Errorf("复制私钥失败: %w", err)
|
return fmt.Errorf("复制私钥失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行命令
|
// 执行命令
|
||||||
|
|
||||||
if err := execCmd(access.Command); err != nil {
|
if err := execCmd(getDeployString(l.option.DeployConfig, "command")); err != nil {
|
||||||
return fmt.Errorf("执行命令失败: %w", err)
|
return fmt.Errorf("执行命令失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ func (q *qiuniu) Deploy(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *qiuniu) enableHttps(certId string) error {
|
func (q *qiuniu) enableHttps(certId string) error {
|
||||||
path := fmt.Sprintf("/domain/%s/sslize", q.option.Domain)
|
path := fmt.Sprintf("/domain/%s/sslize", getDeployString(q.option.DeployConfig, "domain"))
|
||||||
|
|
||||||
body := &modifyDomainCertReq{
|
body := &modifyDomainCertReq{
|
||||||
CertID: certId,
|
CertID: certId,
|
||||||
@ -104,7 +104,7 @@ type domainInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *qiuniu) getDomainInfo() (*domainInfo, error) {
|
func (q *qiuniu) getDomainInfo() (*domainInfo, error) {
|
||||||
path := fmt.Sprintf("/domain/%s", q.option.Domain)
|
path := fmt.Sprintf("/domain/%s", getDeployString(q.option.DeployConfig, "domain"))
|
||||||
|
|
||||||
res, err := q.req(qiniuGateway+path, http.MethodGet, nil)
|
res, err := q.req(qiniuGateway+path, http.MethodGet, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -135,8 +135,8 @@ func (q *qiuniu) uploadCert() (string, error) {
|
|||||||
path := "/sslcert"
|
path := "/sslcert"
|
||||||
|
|
||||||
body := &uploadCertReq{
|
body := &uploadCertReq{
|
||||||
Name: q.option.Domain,
|
Name: getDeployString(q.option.DeployConfig, "domain"),
|
||||||
CommonName: q.option.Domain,
|
CommonName: getDeployString(q.option.DeployConfig, "domain"),
|
||||||
Pri: q.option.Certificate.PrivateKey,
|
Pri: q.option.Certificate.PrivateKey,
|
||||||
Ca: q.option.Certificate.Certificate,
|
Ca: q.option.Certificate.Certificate,
|
||||||
}
|
}
|
||||||
@ -166,7 +166,7 @@ type modifyDomainCertReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *qiuniu) modifyDomainCert(certId string) error {
|
func (q *qiuniu) modifyDomainCert(certId string) error {
|
||||||
path := fmt.Sprintf("/domain/%s/httpsconf", q.option.Domain)
|
path := fmt.Sprintf("/domain/%s/httpsconf", getDeployString(q.option.DeployConfig, "domain"))
|
||||||
|
|
||||||
body := &modifyDomainCertReq{
|
body := &modifyDomainCertReq{
|
||||||
CertID: certId,
|
CertID: certId,
|
||||||
|
@ -7,7 +7,6 @@ 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"
|
||||||
@ -19,15 +18,11 @@ type ssh struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type sshAccess struct {
|
type sshAccess struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Port string `json:"port"`
|
Port string `json:"port"`
|
||||||
PreCommand string `json:"preCommand"`
|
|
||||||
Command string `json:"command"`
|
|
||||||
CertPath string `json:"certPath"`
|
|
||||||
KeyPath string `json:"keyPath"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSSH(option *DeployerOption) (Deployer, error) {
|
func NewSSH(option *DeployerOption) (Deployer, error) {
|
||||||
@ -50,16 +45,6 @@ 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)
|
|
||||||
access.PreCommand = strings.ReplaceAll(access.PreCommand, key, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 连接
|
// 连接
|
||||||
client, err := s.getClient(access)
|
client, err := s.getClient(access)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -70,29 +55,30 @@ func (s *ssh) Deploy(ctx context.Context) error {
|
|||||||
s.infos = append(s.infos, toStr("ssh连接成功", nil))
|
s.infos = append(s.infos, toStr("ssh连接成功", nil))
|
||||||
|
|
||||||
// 执行前置命令
|
// 执行前置命令
|
||||||
if access.PreCommand != "" {
|
preCommand := getDeployString(s.option.DeployConfig, "preCommand")
|
||||||
err, stdout, stderr := s.sshExecCommand(client, access.PreCommand)
|
if preCommand != "" {
|
||||||
|
err, stdout, stderr := s.sshExecCommand(client, preCommand)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传证书
|
// 上传证书
|
||||||
if err := s.upload(client, s.option.Certificate.Certificate, access.CertPath); err != nil {
|
if err := s.upload(client, s.option.Certificate.Certificate, getDeployString(s.option.DeployConfig, "certPath")); err != nil {
|
||||||
return fmt.Errorf("failed to upload certificate: %w", err)
|
return fmt.Errorf("failed to upload certificate: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.infos = append(s.infos, toStr("ssh上传证书成功", nil))
|
s.infos = append(s.infos, toStr("ssh上传证书成功", nil))
|
||||||
|
|
||||||
// 上传私钥
|
// 上传私钥
|
||||||
if err := s.upload(client, s.option.Certificate.PrivateKey, access.KeyPath); err != nil {
|
if err := s.upload(client, s.option.Certificate.PrivateKey, getDeployString(s.option.DeployConfig, "keyPath")); err != nil {
|
||||||
return fmt.Errorf("failed to upload private key: %w", err)
|
return fmt.Errorf("failed to upload private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.infos = append(s.infos, toStr("ssh上传私钥成功", nil))
|
s.infos = append(s.infos, toStr("ssh上传私钥成功", nil))
|
||||||
|
|
||||||
// 执行命令
|
// 执行命令
|
||||||
err, stdout, stderr := s.sshExecCommand(client, access.Command)
|
err, stdout, stderr := s.sshExecCommand(client, getDeployString(s.option.DeployConfig, "command"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,10 @@ type webhookAccess struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type hookData struct {
|
type hookData struct {
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
Certificate string `json:"certificate"`
|
Certificate string `json:"certificate"`
|
||||||
PrivateKey string `json:"privateKey"`
|
PrivateKey string `json:"privateKey"`
|
||||||
|
Variables map[string]string `json:"variables"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type webhook struct {
|
type webhook struct {
|
||||||
@ -50,6 +51,7 @@ func (w *webhook) Deploy(ctx context.Context) error {
|
|||||||
Domain: w.option.Domain,
|
Domain: w.option.Domain,
|
||||||
Certificate: w.option.Certificate.Certificate,
|
Certificate: w.option.Certificate.Certificate,
|
||||||
PrivateKey: w.option.Certificate.PrivateKey,
|
PrivateKey: w.option.Certificate.PrivateKey,
|
||||||
|
Variables: getDeployVariables(w.option.DeployConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
body, _ := json.Marshal(data)
|
body, _ := json.Marshal(data)
|
||||||
|
20
internal/domain/domains.go
Normal file
20
internal/domain/domains.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
type ApplyConfig struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Access string `json:"access"`
|
||||||
|
Timeout int64 `json:"timeout"`
|
||||||
|
Nameservers string `json:"nameservers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeployConfig struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Access string `json:"access"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Config map[string]any `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
@ -5,7 +5,6 @@ import (
|
|||||||
"certimate/internal/deployer"
|
"certimate/internal/deployer"
|
||||||
"certimate/internal/utils/app"
|
"certimate/internal/utils/app"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -41,18 +40,6 @@ 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", "group"}, nil); len(errs) > 0 {
|
|
||||||
|
|
||||||
errList := make([]error, 0)
|
|
||||||
for name, err := range errs {
|
|
||||||
errList = append(errList, fmt.Errorf("展开记录失败,%s: %w", name, err))
|
|
||||||
}
|
|
||||||
err = errors.Join(errList...)
|
|
||||||
app.GetApp().Logger().Error("展开记录失败", "err", err)
|
|
||||||
history.record(checkPhase, "获取授权信息失败", &RecordInfo{Err: err})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
history.record(checkPhase, "获取授权信息成功", nil)
|
|
||||||
|
|
||||||
cert := currRecord.GetString("certificate")
|
cert := currRecord.GetString("certificate")
|
||||||
expiredAt := currRecord.GetDateTime("expiredAt").Time()
|
expiredAt := currRecord.GetDateTime("expiredAt").Time()
|
||||||
@ -106,6 +93,13 @@ func deploy(ctx context.Context, record *models.Record) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 没有部署配置,也算成功
|
||||||
|
if len(deployers) == 0 {
|
||||||
|
history.record(deployPhase, "没有部署配置", &RecordInfo{Info: []string{"没有部署配置"}})
|
||||||
|
history.setWholeSuccess(true)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
for _, deployer := range deployers {
|
for _, deployer := range deployers {
|
||||||
if err = deployer.Deploy(ctx); err != nil {
|
if err = deployer.Deploy(ctx); err != nil {
|
||||||
|
|
||||||
|
731
migrations/1728778480_collections_snapshot.go
Normal file
731
migrations/1728778480_collections_snapshot.go
Normal file
@ -0,0 +1,731 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
"github.com/pocketbase/pocketbase/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(db dbx.Builder) error {
|
||||||
|
jsonData := `[
|
||||||
|
{
|
||||||
|
"id": "z3p974ainxjqlvs",
|
||||||
|
"created": "2024-07-29 10:02:48.334Z",
|
||||||
|
"updated": "2024-10-08 06:50:56.637Z",
|
||||||
|
"name": "domains",
|
||||||
|
"type": "base",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "iuaerpl2",
|
||||||
|
"name": "domain",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "ukkhuw85",
|
||||||
|
"name": "email",
|
||||||
|
"type": "email",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"exceptDomains": null,
|
||||||
|
"onlyDomains": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "v98eebqq",
|
||||||
|
"name": "crontab",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "alc8e9ow",
|
||||||
|
"name": "access",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "4yzbv8urny5ja1e",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "topsc9bj",
|
||||||
|
"name": "certUrl",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "vixgq072",
|
||||||
|
"name": "certStableUrl",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "g3a3sza5",
|
||||||
|
"name": "privateKey",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "gr6iouny",
|
||||||
|
"name": "certificate",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "tk6vnrmn",
|
||||||
|
"name": "issuerCertificate",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "sjo6ibse",
|
||||||
|
"name": "csr",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "x03n1bkj",
|
||||||
|
"name": "expiredAt",
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": "",
|
||||||
|
"max": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "srybpixz",
|
||||||
|
"name": "targetType",
|
||||||
|
"type": "select",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"values": [
|
||||||
|
"aliyun-oss",
|
||||||
|
"aliyun-cdn",
|
||||||
|
"aliyun-dcdn",
|
||||||
|
"ssh",
|
||||||
|
"webhook",
|
||||||
|
"tencent-cdn",
|
||||||
|
"qiniu-cdn",
|
||||||
|
"local"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "xy7yk0mb",
|
||||||
|
"name": "targetAccess",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "4yzbv8urny5ja1e",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "6jqeyggw",
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "bool",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "hdsjcchf",
|
||||||
|
"name": "deployed",
|
||||||
|
"type": "bool",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "aiya3rev",
|
||||||
|
"name": "rightnow",
|
||||||
|
"type": "bool",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "ixznmhzc",
|
||||||
|
"name": "lastDeployedAt",
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": "",
|
||||||
|
"max": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "ghtlkn5j",
|
||||||
|
"name": "lastDeployment",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "0a1o4e6sstp694f",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "zfnyj9he",
|
||||||
|
"name": "variables",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "1bspzuku",
|
||||||
|
"name": "group",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "teolp9pl72dxlxq",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "g65gfh7a",
|
||||||
|
"name": "nameservers",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "wwrzc3jo",
|
||||||
|
"name": "applyConfig",
|
||||||
|
"type": "json",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSize": 2000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "474iwy8r",
|
||||||
|
"name": "deployConfig",
|
||||||
|
"type": "json",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSize": 2000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
"CREATE UNIQUE INDEX ` + "`" + `idx_4ABO6EQ` + "`" + ` ON ` + "`" + `domains` + "`" + ` (` + "`" + `domain` + "`" + `)"
|
||||||
|
],
|
||||||
|
"listRule": null,
|
||||||
|
"viewRule": null,
|
||||||
|
"createRule": null,
|
||||||
|
"updateRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4yzbv8urny5ja1e",
|
||||||
|
"created": "2024-07-29 10:04:39.685Z",
|
||||||
|
"updated": "2024-10-11 13:55:13.777Z",
|
||||||
|
"name": "access",
|
||||||
|
"type": "base",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "geeur58v",
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "iql7jpwx",
|
||||||
|
"name": "config",
|
||||||
|
"type": "json",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSize": 2000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "hwy7m03o",
|
||||||
|
"name": "configType",
|
||||||
|
"type": "select",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"values": [
|
||||||
|
"aliyun",
|
||||||
|
"tencent",
|
||||||
|
"huaweicloud",
|
||||||
|
"qiniu",
|
||||||
|
"cloudflare",
|
||||||
|
"namesilo",
|
||||||
|
"godaddy",
|
||||||
|
"local",
|
||||||
|
"ssh",
|
||||||
|
"webhook"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "lr33hiwg",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": "",
|
||||||
|
"max": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "hsxcnlvd",
|
||||||
|
"name": "usage",
|
||||||
|
"type": "select",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"values": [
|
||||||
|
"apply",
|
||||||
|
"deploy",
|
||||||
|
"all"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "c8egzzwj",
|
||||||
|
"name": "group",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "teolp9pl72dxlxq",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
"CREATE UNIQUE INDEX ` + "`" + `idx_wkoST0j` + "`" + ` ON ` + "`" + `access` + "`" + ` (` + "`" + `name` + "`" + `)"
|
||||||
|
],
|
||||||
|
"listRule": null,
|
||||||
|
"viewRule": null,
|
||||||
|
"createRule": null,
|
||||||
|
"updateRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0a1o4e6sstp694f",
|
||||||
|
"created": "2024-07-30 06:30:27.801Z",
|
||||||
|
"updated": "2024-09-26 12:29:38.334Z",
|
||||||
|
"name": "deployments",
|
||||||
|
"type": "base",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "farvlzk7",
|
||||||
|
"name": "domain",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "z3p974ainxjqlvs",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "jx5f69i3",
|
||||||
|
"name": "log",
|
||||||
|
"type": "json",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSize": 2000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "qbxdtg9q",
|
||||||
|
"name": "phase",
|
||||||
|
"type": "select",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"values": [
|
||||||
|
"check",
|
||||||
|
"apply",
|
||||||
|
"deploy"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "rglrp1hz",
|
||||||
|
"name": "phaseSuccess",
|
||||||
|
"type": "bool",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "lt1g1blu",
|
||||||
|
"name": "deployedAt",
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": "",
|
||||||
|
"max": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "wledpzgb",
|
||||||
|
"name": "wholeSuccess",
|
||||||
|
"type": "bool",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": null,
|
||||||
|
"viewRule": null,
|
||||||
|
"createRule": null,
|
||||||
|
"updateRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "_pb_users_auth_",
|
||||||
|
"created": "2024-09-12 13:09:54.234Z",
|
||||||
|
"updated": "2024-09-26 12:29:38.334Z",
|
||||||
|
"name": "users",
|
||||||
|
"type": "auth",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "users_name",
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "users_avatar",
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "file",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"mimeTypes": [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/svg+xml",
|
||||||
|
"image/gif",
|
||||||
|
"image/webp"
|
||||||
|
],
|
||||||
|
"thumbs": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 5242880,
|
||||||
|
"protected": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": "id = @request.auth.id",
|
||||||
|
"viewRule": "id = @request.auth.id",
|
||||||
|
"createRule": "",
|
||||||
|
"updateRule": "id = @request.auth.id",
|
||||||
|
"deleteRule": "id = @request.auth.id",
|
||||||
|
"options": {
|
||||||
|
"allowEmailAuth": true,
|
||||||
|
"allowOAuth2Auth": true,
|
||||||
|
"allowUsernameAuth": true,
|
||||||
|
"exceptEmailDomains": null,
|
||||||
|
"manageRule": null,
|
||||||
|
"minPasswordLength": 8,
|
||||||
|
"onlyEmailDomains": null,
|
||||||
|
"onlyVerified": false,
|
||||||
|
"requireEmail": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dy6ccjb60spfy6p",
|
||||||
|
"created": "2024-09-12 23:12:21.677Z",
|
||||||
|
"updated": "2024-09-26 12:29:38.334Z",
|
||||||
|
"name": "settings",
|
||||||
|
"type": "base",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "1tcmdsdf",
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "f9wyhypi",
|
||||||
|
"name": "content",
|
||||||
|
"type": "json",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSize": 2000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
"CREATE UNIQUE INDEX ` + "`" + `idx_RO7X9Vw` + "`" + ` ON ` + "`" + `settings` + "`" + ` (` + "`" + `name` + "`" + `)"
|
||||||
|
],
|
||||||
|
"listRule": null,
|
||||||
|
"viewRule": null,
|
||||||
|
"createRule": null,
|
||||||
|
"updateRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "teolp9pl72dxlxq",
|
||||||
|
"created": "2024-09-13 12:51:05.611Z",
|
||||||
|
"updated": "2024-09-26 12:29:38.334Z",
|
||||||
|
"name": "access_groups",
|
||||||
|
"type": "base",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "7sajiv6i",
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "xp8admif",
|
||||||
|
"name": "access",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "4yzbv8urny5ja1e",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": null,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
"CREATE UNIQUE INDEX ` + "`" + `idx_RgRXp0R` + "`" + ` ON ` + "`" + `access_groups` + "`" + ` (` + "`" + `name` + "`" + `)"
|
||||||
|
],
|
||||||
|
"listRule": null,
|
||||||
|
"viewRule": null,
|
||||||
|
"createRule": null,
|
||||||
|
"updateRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
]`
|
||||||
|
|
||||||
|
collections := []*models.Collection{}
|
||||||
|
if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return daos.New(db).ImportCollections(collections, true, nil)
|
||||||
|
}, func(db dbx.Builder) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
1
ui/dist/assets/index-CWUb5Xuf.css
vendored
Normal file
1
ui/dist/assets/index-CWUb5Xuf.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ui/dist/assets/index-DOft-CKV.css
vendored
1
ui/dist/assets/index-DOft-CKV.css
vendored
File diff suppressed because one or more lines are too long
332
ui/dist/assets/index-DbwFzZm1.js
vendored
Normal file
332
ui/dist/assets/index-DbwFzZm1.js
vendored
Normal file
File diff suppressed because one or more lines are too long
332
ui/dist/assets/index-DpHAV802.js
vendored
332
ui/dist/assets/index-DpHAV802.js
vendored
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-DpHAV802.js"></script>
|
<script type="module" crossorigin src="/assets/index-DbwFzZm1.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DOft-CKV.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CWUb5Xuf.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-background">
|
<body class="bg-background">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
28
ui/package-lock.json
generated
28
ui/package-lock.json
generated
@ -33,6 +33,7 @@
|
|||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.417.0",
|
"lucide-react": "^0.417.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
"nanoid": "^5.0.7",
|
||||||
"pocketbase": "^0.21.4",
|
"pocketbase": "^0.21.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@ -4159,9 +4160,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "5.0.7",
|
||||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-5.0.7.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
"integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@ -4169,10 +4170,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^18 || >=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/natural-compare": {
|
"node_modules/natural-compare": {
|
||||||
@ -4561,6 +4562,23 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss/node_modules/nanoid": {
|
||||||
|
"version": "3.3.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz",
|
||||||
|
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
|
@ -29,22 +29,23 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"i18next": "^23.15.1",
|
||||||
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
|
"i18next-http-backend": "^2.6.1",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.417.0",
|
"lucide-react": "^0.417.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
"nanoid": "^5.0.7",
|
||||||
"pocketbase": "^0.21.4",
|
"pocketbase": "^0.21.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.52.1",
|
"react-hook-form": "^7.52.1",
|
||||||
|
"react-i18next": "^15.0.2",
|
||||||
"react-router-dom": "^6.25.1",
|
"react-router-dom": "^6.25.1",
|
||||||
"tailwind-merge": "^2.4.0",
|
"tailwind-merge": "^2.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^0.9.1",
|
"vaul": "^0.9.1",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8"
|
||||||
"i18next": "^23.15.1",
|
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
|
||||||
"i18next-http-backend": "^2.6.1",
|
|
||||||
"react-i18next": "^15.0.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { Access, accessFormType, getUsageByConfigType } from "@/domain/access";
|
||||||
Access,
|
|
||||||
accessFormType,
|
|
||||||
getUsageByConfigType,
|
|
||||||
LocalConfig,
|
|
||||||
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";
|
||||||
@ -20,7 +14,7 @@ import {
|
|||||||
} from "../ui/form";
|
} from "../ui/form";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Textarea } from "../ui/textarea";
|
|
||||||
import { save } from "@/repository/access";
|
import { save } from "@/repository/access";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
import { PbErrorData } from "@/domain/base";
|
import { PbErrorData } from "@/domain/base";
|
||||||
@ -39,30 +33,19 @@ const AccessLocalForm = ({
|
|||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1, "access.form.name.not.empty")
|
||||||
|
.max(64, t("zod.rule.string.max", { max: 64 })),
|
||||||
configType: accessFormType,
|
configType: accessFormType,
|
||||||
|
|
||||||
command: z.string().min(1, 'access.form.ssh.command.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
|
||||||
certPath: z.string().min(0, 'access.form.ssh.cert.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
|
||||||
keyPath: z.string().min(0, 'access.form.ssh.key.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let config: LocalConfig = {
|
|
||||||
command: "sudo service nginx restart",
|
|
||||||
certPath: "/etc/nginx/ssl/certificate.crt",
|
|
||||||
keyPath: "/etc/nginx/ssl/private.key",
|
|
||||||
};
|
|
||||||
if (data) config = data.config as SSHConfig;
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: data?.id,
|
id: data?.id,
|
||||||
name: data?.name || '',
|
name: data?.name || "",
|
||||||
configType: "local",
|
configType: "local",
|
||||||
certPath: config.certPath,
|
|
||||||
keyPath: config.keyPath,
|
|
||||||
command: config.command,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -73,15 +56,11 @@ const AccessLocalForm = ({
|
|||||||
configType: data.configType,
|
configType: data.configType,
|
||||||
usage: getUsageByConfigType(data.configType),
|
usage: getUsageByConfigType(data.configType),
|
||||||
|
|
||||||
config: {
|
config: {},
|
||||||
command: data.command,
|
|
||||||
certPath: data.certPath,
|
|
||||||
keyPath: data.keyPath,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
req.id = op == "copy" ? "" : req.id;
|
req.id = op == "copy" ? "" : req.id;
|
||||||
const rs = await save(req);
|
const rs = await save(req);
|
||||||
|
|
||||||
onAfterReq();
|
onAfterReq();
|
||||||
@ -128,9 +107,12 @@ const AccessLocalForm = ({
|
|||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('name')}</FormLabel>
|
<FormLabel>{t("name")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t('access.form.name.not.empty')} {...field} />
|
<Input
|
||||||
|
placeholder={t("access.form.name.not.empty")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -143,7 +125,7 @@ const AccessLocalForm = ({
|
|||||||
name="id"
|
name="id"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="hidden">
|
<FormItem className="hidden">
|
||||||
<FormLabel>{t('access.form.config.field')}</FormLabel>
|
<FormLabel>{t("access.form.config.field")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -158,7 +140,7 @@ const AccessLocalForm = ({
|
|||||||
name="configType"
|
name="configType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="hidden">
|
<FormItem className="hidden">
|
||||||
<FormLabel>{t('access.form.config.field')}</FormLabel>
|
<FormLabel>{t("access.form.config.field")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -168,55 +150,10 @@ const AccessLocalForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="certPath"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('access.form.ssh.cert.path')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={t('access.form.ssh.cert.path.not.empty')} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="keyPath"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('access.form.ssh.key.path')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={t('access.form.ssh.key.path.not.empty')} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="command"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('access.form.ssh.command')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea placeholder={t('access.form.ssh.command.not.empty')} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit">{t('save')}</Button>
|
<Button type="submit">{t("save")}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -18,7 +18,6 @@ import {
|
|||||||
} from "../ui/form";
|
} from "../ui/form";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Textarea } from "../ui/textarea";
|
|
||||||
import { save } from "@/repository/access";
|
import { save } from "@/repository/access";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
import { PbErrorData } from "@/domain/base";
|
import { PbErrorData } from "@/domain/base";
|
||||||
@ -66,7 +65,10 @@ const AccessSSHForm = ({
|
|||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1, "access.form.name.not.empty")
|
||||||
|
.max(64, t("zod.rule.string.max", { max: 64 })),
|
||||||
configType: accessFormType,
|
configType: accessFormType,
|
||||||
host: z.string().refine(
|
host: z.string().refine(
|
||||||
(str) => {
|
(str) => {
|
||||||
@ -77,16 +79,23 @@ const AccessSSHForm = ({
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
group: z.string().optional(),
|
group: z.string().optional(),
|
||||||
port: z.string().min(1, 'access.form.ssh.port.not.empty').max(5, t('zod.rule.string.max', { max: 5 })),
|
port: z
|
||||||
username: z.string().min(1, 'username.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
|
.string()
|
||||||
password: z.string().min(0, 'password.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
|
.min(1, "access.form.ssh.port.not.empty")
|
||||||
key: z.string().min(0, 'access.form.ssh.key.not.empty').max(20480, t('zod.rule.string.max', { max: 20480 })),
|
.max(5, t("zod.rule.string.max", { max: 5 })),
|
||||||
|
username: z
|
||||||
|
.string()
|
||||||
|
.min(1, "username.not.empty")
|
||||||
|
.max(64, t("zod.rule.string.max", { max: 64 })),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(0, "password.not.empty")
|
||||||
|
.max(64, t("zod.rule.string.max", { max: 64 })),
|
||||||
|
key: z
|
||||||
|
.string()
|
||||||
|
.min(0, "access.form.ssh.key.not.empty")
|
||||||
|
.max(20480, t("zod.rule.string.max", { max: 20480 })),
|
||||||
keyFile: z.any().optional(),
|
keyFile: z.any().optional(),
|
||||||
|
|
||||||
preCommand: z.string().min(0).max(2048, t('zod.rule.string.max', { max: 2048 })).optional(),
|
|
||||||
command: z.string().min(1, 'access.form.ssh.command.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
|
||||||
certPath: z.string().min(0, 'access.form.ssh.cert.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
|
||||||
keyPath: z.string().min(0, 'access.form.ssh.key.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let config: SSHConfig = {
|
let config: SSHConfig = {
|
||||||
@ -96,10 +105,6 @@ const AccessSSHForm = ({
|
|||||||
password: "",
|
password: "",
|
||||||
key: "",
|
key: "",
|
||||||
keyFile: "",
|
keyFile: "",
|
||||||
preCommand: "",
|
|
||||||
command: "sudo service nginx restart",
|
|
||||||
certPath: "/etc/nginx/ssl/certificate.crt",
|
|
||||||
keyPath: "/etc/nginx/ssl/private.key",
|
|
||||||
};
|
};
|
||||||
if (data) config = data.config as SSHConfig;
|
if (data) config = data.config as SSHConfig;
|
||||||
|
|
||||||
@ -107,7 +112,7 @@ const AccessSSHForm = ({
|
|||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: data?.id,
|
id: data?.id,
|
||||||
name: data?.name || '',
|
name: data?.name || "",
|
||||||
configType: "ssh",
|
configType: "ssh",
|
||||||
group: data?.group,
|
group: data?.group,
|
||||||
host: config.host,
|
host: config.host,
|
||||||
@ -116,10 +121,6 @@ const AccessSSHForm = ({
|
|||||||
password: config.password,
|
password: config.password,
|
||||||
key: config.key,
|
key: config.key,
|
||||||
keyFile: config.keyFile,
|
keyFile: config.keyFile,
|
||||||
certPath: config.certPath,
|
|
||||||
keyPath: config.keyPath,
|
|
||||||
command: config.command,
|
|
||||||
preCommand: config.preCommand,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -139,15 +140,11 @@ const AccessSSHForm = ({
|
|||||||
username: data.username,
|
username: data.username,
|
||||||
password: data.password,
|
password: data.password,
|
||||||
key: data.key,
|
key: data.key,
|
||||||
command: data.command,
|
|
||||||
preCommand: data.preCommand,
|
|
||||||
certPath: data.certPath,
|
|
||||||
keyPath: data.keyPath,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
req.id = op == "copy" ? "" : req.id;
|
req.id = op == "copy" ? "" : req.id;
|
||||||
const rs = await save(req);
|
const rs = await save(req);
|
||||||
|
|
||||||
onAfterReq();
|
onAfterReq();
|
||||||
@ -228,9 +225,12 @@ const AccessSSHForm = ({
|
|||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('name')}</FormLabel>
|
<FormLabel>{t("name")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t('access.form.name.not.empty')} {...field} />
|
<Input
|
||||||
|
placeholder={t("access.form.name.not.empty")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -244,12 +244,12 @@ const AccessSSHForm = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="w-full flex justify-between">
|
<FormLabel className="w-full flex justify-between">
|
||||||
<div>{t('access.form.ssh.group.label')}</div>
|
<div>{t("access.form.ssh.group.label")}</div>
|
||||||
<AccessGroupEdit
|
<AccessGroupEdit
|
||||||
trigger={
|
trigger={
|
||||||
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
||||||
<Plus size={14} />
|
<Plus size={14} />
|
||||||
{t('add')}
|
{t("add")}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -264,7 +264,9 @@ const AccessSSHForm = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t('access.group.not.empty')} />
|
<SelectValue
|
||||||
|
placeholder={t("access.group.not.empty")}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="emptyId">
|
<SelectItem value="emptyId">
|
||||||
@ -304,7 +306,7 @@ const AccessSSHForm = ({
|
|||||||
name="id"
|
name="id"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="hidden">
|
<FormItem className="hidden">
|
||||||
<FormLabel>{t('access.form.config.field')}</FormLabel>
|
<FormLabel>{t("access.form.config.field")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -319,7 +321,7 @@ const AccessSSHForm = ({
|
|||||||
name="configType"
|
name="configType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="hidden">
|
<FormItem className="hidden">
|
||||||
<FormLabel>{t('access.form.config.field')}</FormLabel>
|
<FormLabel>{t("access.form.config.field")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -334,9 +336,12 @@ const AccessSSHForm = ({
|
|||||||
name="host"
|
name="host"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="grow">
|
<FormItem className="grow">
|
||||||
<FormLabel>{t('access.form.ssh.host')}</FormLabel>
|
<FormLabel>{t("access.form.ssh.host")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t('access.form.ssh.host.not.empty')} {...field} />
|
<Input
|
||||||
|
placeholder={t("access.form.ssh.host.not.empty")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -349,10 +354,10 @@ const AccessSSHForm = ({
|
|||||||
name="port"
|
name="port"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('access.form.ssh.port')}</FormLabel>
|
<FormLabel>{t("access.form.ssh.port")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('access.form.ssh.port.not.empty')}
|
placeholder={t("access.form.ssh.port.not.empty")}
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
@ -369,9 +374,9 @@ const AccessSSHForm = ({
|
|||||||
name="username"
|
name="username"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('username')}</FormLabel>
|
<FormLabel>{t("username")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t('username.not.empty')} {...field} />
|
<Input placeholder={t("username.not.empty")} {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -384,10 +389,10 @@ const AccessSSHForm = ({
|
|||||||
name="password"
|
name="password"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('password')}</FormLabel>
|
<FormLabel>{t("password")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('password.not.empty')}
|
placeholder={t("password.not.empty")}
|
||||||
{...field}
|
{...field}
|
||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
@ -403,9 +408,12 @@ const AccessSSHForm = ({
|
|||||||
name="key"
|
name="key"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem hidden>
|
<FormItem hidden>
|
||||||
<FormLabel>{t('access.form.ssh.key')}</FormLabel>
|
<FormLabel>{t("access.form.ssh.key")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t('access.form.ssh.key.not.empty')} {...field} />
|
<Input
|
||||||
|
placeholder={t("access.form.ssh.key.not.empty")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -418,7 +426,7 @@ const AccessSSHForm = ({
|
|||||||
name="keyFile"
|
name="keyFile"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('access.form.ssh.key')}</FormLabel>
|
<FormLabel>{t("access.form.ssh.key")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
@ -428,10 +436,12 @@ const AccessSSHForm = ({
|
|||||||
className="w-48"
|
className="w-48"
|
||||||
onClick={handleSelectFileClick}
|
onClick={handleSelectFileClick}
|
||||||
>
|
>
|
||||||
{fileName ? fileName : t('access.form.ssh.key.file.not.empty')}
|
{fileName
|
||||||
|
? fileName
|
||||||
|
: t("access.form.ssh.key.file.not.empty")}
|
||||||
</Button>
|
</Button>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('access.form.ssh.key.not.empty')}
|
placeholder={t("access.form.ssh.key.not.empty")}
|
||||||
{...field}
|
{...field}
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
@ -447,70 +457,10 @@ const AccessSSHForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="certPath"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('access.form.ssh.cert.path')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={t('access.form.ssh.cert.path.not.empty')} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="keyPath"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('access.form.ssh.key.path')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={t('access.form.ssh.key.path.not.empty')} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="preCommand"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('access.form.ssh.pre.command')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea placeholder={t('access.form.ssh.pre.command.not.empty')} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="command"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('access.form.ssh.command')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea placeholder={t('access.form.ssh.command.not.empty')} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit">{t('save')}</Button>
|
<Button type="submit">{t("save")}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -43,10 +43,14 @@ import { Input } from "../ui/input";
|
|||||||
import { Textarea } from "../ui/textarea";
|
import { Textarea } from "../ui/textarea";
|
||||||
import KVList from "./KVList";
|
import KVList from "./KVList";
|
||||||
import { produce } from "immer";
|
import { produce } from "immer";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
type DeployEditContextProps = {
|
type DeployEditContextProps = {
|
||||||
deploy: DeployConfig;
|
deploy: DeployConfig;
|
||||||
|
error: Record<string, string>;
|
||||||
setDeploy: (deploy: DeployConfig) => void;
|
setDeploy: (deploy: DeployConfig) => void;
|
||||||
|
setError: (error: Record<string, string>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeployEditContext = createContext<DeployEditContextProps>(
|
const DeployEditContext = createContext<DeployEditContextProps>(
|
||||||
@ -59,53 +63,92 @@ export const useDeployEditContext = () => {
|
|||||||
|
|
||||||
type DeployListProps = {
|
type DeployListProps = {
|
||||||
deploys: DeployConfig[];
|
deploys: DeployConfig[];
|
||||||
|
onChange: (deploys: DeployConfig[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeployList = ({ deploys }: DeployListProps) => {
|
const DeployList = ({ deploys, onChange }: DeployListProps) => {
|
||||||
const [list, setList] = useState<DeployConfig[]>([]);
|
const [list, setList] = useState<DeployConfig[]>([]);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setList(deploys);
|
setList(deploys);
|
||||||
}, [deploys]);
|
}, [deploys]);
|
||||||
|
|
||||||
|
const handleAdd = (deploy: DeployConfig) => {
|
||||||
|
deploy.id = nanoid();
|
||||||
|
|
||||||
|
const newList = [...list, deploy];
|
||||||
|
|
||||||
|
setList(newList);
|
||||||
|
|
||||||
|
onChange(newList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
const newList = list.filter((item) => item.id !== id);
|
||||||
|
|
||||||
|
setList(newList);
|
||||||
|
|
||||||
|
onChange(newList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = (deploy: DeployConfig) => {
|
||||||
|
const newList = list.map((item) => {
|
||||||
|
if (item.id === deploy.id) {
|
||||||
|
return { ...deploy };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
setList(newList);
|
||||||
|
|
||||||
|
onChange(newList);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show
|
<Show
|
||||||
when={list.length > 0}
|
when={list.length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<Alert className="w-full">
|
<Alert className="w-full border dark:border-stone-400">
|
||||||
<AlertDescription className="flex flex-col items-center">
|
<AlertDescription className="flex flex-col items-center">
|
||||||
<div>暂无部署配置,请添加后开始部署证书吧</div>
|
<div>{t("deployment.not.added")}</div>
|
||||||
<div className="flex justify-end mt-2">
|
<div className="flex justify-end mt-2">
|
||||||
<DeployEditDialog
|
<DeployEditDialog
|
||||||
trigger={<Button size={"sm"}>添加部署</Button>}
|
onSave={(config: DeployConfig) => {
|
||||||
|
handleAdd(config);
|
||||||
|
}}
|
||||||
|
trigger={<Button size={"sm"}>{t("add")}</Button>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex justify-end py-2 border-b">
|
<div className="flex justify-end py-2 border-b dark:border-stone-400">
|
||||||
<DeployEditDialog trigger={<Button size={"sm"}>添加部署</Button>} />
|
<DeployEditDialog
|
||||||
|
trigger={<Button size={"sm"}>{t("add")}</Button>}
|
||||||
|
onSave={(config: DeployConfig) => {
|
||||||
|
handleAdd(config);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full md:w-[35em] rounded mt-5 border">
|
<div className="w-full md:w-[35em] rounded mt-5 border dark:border-stone-400">
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="flex justify-between text-sm p-3 items-center text-stone-700">
|
{list.map((item) => (
|
||||||
<div className="flex space-x-2 items-center">
|
<DeployItem
|
||||||
<div>
|
key={item.id}
|
||||||
<img src="/imgs/providers/ssh.svg" className="w-9"></img>
|
item={item}
|
||||||
</div>
|
onDelete={() => {
|
||||||
<div className="text-stone-600 flex-col flex space-y-0">
|
handleDelete(item.id ?? "");
|
||||||
<div>ssh部署</div>
|
}}
|
||||||
<div>业务服务器</div>
|
onSave={(deploy: DeployConfig) => {
|
||||||
</div>
|
handleSave(deploy);
|
||||||
</div>
|
}}
|
||||||
<div className="flex space-x-2">
|
/>
|
||||||
<EditIcon size={16} className="cursor-pointer" />
|
))}
|
||||||
<Trash2 size={16} className="cursor-pointer" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
@ -113,11 +156,87 @@ const DeployList = ({ deploys }: DeployListProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DeployItemProps = {
|
||||||
|
item: DeployConfig;
|
||||||
|
onDelete: () => void;
|
||||||
|
onSave: (deploy: DeployConfig) => void;
|
||||||
|
};
|
||||||
|
const DeployItem = ({ item, onDelete, onSave }: DeployItemProps) => {
|
||||||
|
const {
|
||||||
|
config: { accesses },
|
||||||
|
} = useConfig();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const access = accesses.find((access) => access.id === item.access);
|
||||||
|
const getImg = () => {
|
||||||
|
if (!access) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessType = accessTypeMap.get(access.configType);
|
||||||
|
|
||||||
|
if (accessType) {
|
||||||
|
return accessType[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeName = () => {
|
||||||
|
if (!access) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessType = targetTypeMap.get(item.type);
|
||||||
|
|
||||||
|
if (accessType) {
|
||||||
|
return t(accessType[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between text-sm p-3 items-center text-stone-700">
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
|
<div>
|
||||||
|
<img src={getImg()} className="w-9"></img>
|
||||||
|
</div>
|
||||||
|
<div className="text-stone-600 flex-col flex space-y-0">
|
||||||
|
<div>{getTypeName()}</div>
|
||||||
|
<div>{access?.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<DeployEditDialog
|
||||||
|
trigger={<EditIcon size={16} className="cursor-pointer" />}
|
||||||
|
deployConfig={item}
|
||||||
|
onSave={(deploy: DeployConfig) => {
|
||||||
|
onSave(deploy);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Trash2
|
||||||
|
size={16}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
onDelete();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
type DeployEditDialogProps = {
|
type DeployEditDialogProps = {
|
||||||
trigger: React.ReactNode;
|
trigger: React.ReactNode;
|
||||||
deployConfig?: DeployConfig;
|
deployConfig?: DeployConfig;
|
||||||
|
onSave: (deploy: DeployConfig) => void;
|
||||||
};
|
};
|
||||||
const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
|
const DeployEditDialog = ({
|
||||||
|
trigger,
|
||||||
|
deployConfig,
|
||||||
|
onSave,
|
||||||
|
}: DeployEditDialogProps) => {
|
||||||
const {
|
const {
|
||||||
config: { accesses },
|
config: { accesses },
|
||||||
} = useConfig();
|
} = useConfig();
|
||||||
@ -129,6 +248,10 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
|
|||||||
type: "",
|
type: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [error, setError] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (deployConfig) {
|
if (deployConfig) {
|
||||||
setLocDeployConfig({ ...deployConfig });
|
setLocDeployConfig({ ...deployConfig });
|
||||||
@ -150,6 +273,7 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
|
|||||||
t = locDeployConfig.type;
|
t = locDeployConfig.type;
|
||||||
}
|
}
|
||||||
setDeployType(t as TargetType);
|
setDeployType(t as TargetType);
|
||||||
|
setError({});
|
||||||
}, [locDeployConfig.type]);
|
}, [locDeployConfig.type]);
|
||||||
|
|
||||||
const setDeploy = useCallback(
|
const setDeploy = useCallback(
|
||||||
@ -177,23 +301,62 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
|
|||||||
return item.configType === types[0];
|
return item.configType === types[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleSaveClick = () => {
|
||||||
|
// 验证数据
|
||||||
|
// 保存数据
|
||||||
|
// 清理数据
|
||||||
|
// 关闭弹框
|
||||||
|
const newError = { ...error };
|
||||||
|
if (locDeployConfig.type === "") {
|
||||||
|
newError.type = t("domain.management.edit.access.not.empty.message");
|
||||||
|
} else {
|
||||||
|
newError.type = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locDeployConfig.access === "") {
|
||||||
|
newError.access = t("domain.management.edit.access.not.empty.message");
|
||||||
|
} else {
|
||||||
|
newError.access = "";
|
||||||
|
}
|
||||||
|
setError(newError);
|
||||||
|
|
||||||
|
for (const key in newError) {
|
||||||
|
if (newError[key] !== "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave(locDeployConfig);
|
||||||
|
|
||||||
|
setLocDeployConfig({
|
||||||
|
access: "",
|
||||||
|
type: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
setError({});
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DeployEditContext.Provider
|
<DeployEditContext.Provider
|
||||||
value={{
|
value={{
|
||||||
deploy: locDeployConfig,
|
deploy: locDeployConfig,
|
||||||
setDeploy: setDeploy,
|
setDeploy: setDeploy,
|
||||||
|
error: error,
|
||||||
|
setError: setError,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dialog>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger>{trigger}</DialogTrigger>
|
<DialogTrigger>{trigger}</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent className="dark:text-stone-200">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>部署</DialogTitle>
|
<DialogTitle>{t("deployment")}</DialogTitle>
|
||||||
<DialogDescription></DialogDescription>
|
<DialogDescription></DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{/* 授权类型 */}
|
{/* 授权类型 */}
|
||||||
<div>
|
<div>
|
||||||
<Label>授权类型</Label>
|
<Label>{t("deployment.access.type")}</Label>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={locDeployConfig.type}
|
value={locDeployConfig.type}
|
||||||
@ -227,11 +390,13 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
|
|||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
<div className="text-red-500 text-sm mt-1">{error.type}</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 授权 */}
|
{/* 授权 */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="flex justify-between">
|
<Label className="flex justify-between">
|
||||||
<div>授权配置</div>
|
<div>{t("deployment.access.config")}</div>
|
||||||
<AccessEdit
|
<AccessEdit
|
||||||
trigger={
|
trigger={
|
||||||
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
||||||
@ -275,12 +440,21 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
|
|||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
<div className="text-red-500 text-sm mt-1">{error.access}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DeployEdit type={deployType!} />
|
<DeployEdit type={deployType!} />
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button>保存</Button>
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSaveClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("save")}
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@ -317,8 +491,27 @@ const DeployEdit = ({ type }: DeployEditProps) => {
|
|||||||
|
|
||||||
const DeploySSH = () => {
|
const DeploySSH = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { setError } = useDeployEditContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setError({});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { deploy: data, setDeploy } = useDeployEditContext();
|
const { deploy: data, setDeploy } = useDeployEditContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data.id) {
|
||||||
|
setDeploy({
|
||||||
|
...data,
|
||||||
|
config: {
|
||||||
|
certPath: "/etc/nginx/ssl/nginx.crt",
|
||||||
|
keyPath: "/etc/nginx/ssl/nginx.key",
|
||||||
|
preCommand: "",
|
||||||
|
command: "sudo service nginx reload",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
@ -358,10 +551,11 @@ const DeploySSH = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>前置命令</Label>
|
<Label>{t("access.form.ssh.pre.command")}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
value={data?.config?.preCommand}
|
value={data?.config?.preCommand}
|
||||||
|
placeholder={t("access.form.ssh.pre.command.not.empty")}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newData = produce(data, (draft) => {
|
const newData = produce(data, (draft) => {
|
||||||
if (!draft.config) {
|
if (!draft.config) {
|
||||||
@ -375,10 +569,11 @@ const DeploySSH = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>命令</Label>
|
<Label>{t("access.form.ssh.command")}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
value={data?.config?.command}
|
value={data?.config?.command}
|
||||||
|
placeholder={t("access.form.ssh.command.not.empty")}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newData = produce(data, (draft) => {
|
const newData = produce(data, (draft) => {
|
||||||
if (!draft.config) {
|
if (!draft.config) {
|
||||||
@ -396,25 +591,69 @@ const DeploySSH = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DeployCDN = () => {
|
const DeployCDN = () => {
|
||||||
const { deploy: data, setDeploy } = useDeployEditContext();
|
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setError({});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const resp = domainSchema.safeParse(data.config?.domain);
|
||||||
|
if (!resp.success) {
|
||||||
|
setError({
|
||||||
|
...error,
|
||||||
|
domain: JSON.parse(resp.error.message)[0].message,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setError({
|
||||||
|
...error,
|
||||||
|
domain: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const domainSchema = z
|
||||||
|
.string()
|
||||||
|
.regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
|
||||||
|
message: t("domain.not.empty.verify.message"),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<Label>部署至域名</Label>
|
<Label>{t("deployment.access.cdn.deploy.to.domain")}</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="部署至域名"
|
placeholder={t("deployment.access.cdn.deploy.to.domain")}
|
||||||
className="w-full mt-1"
|
className="w-full mt-1"
|
||||||
value={data?.config?.domain}
|
value={data?.config?.domain}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
const temp = e.target.value;
|
||||||
|
|
||||||
|
const resp = domainSchema.safeParse(temp);
|
||||||
|
if (!resp.success) {
|
||||||
|
setError({
|
||||||
|
...error,
|
||||||
|
domain: JSON.parse(resp.error.message)[0].message,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setError({
|
||||||
|
...error,
|
||||||
|
domain: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const newData = produce(data, (draft) => {
|
const newData = produce(data, (draft) => {
|
||||||
if (!draft.config) {
|
if (!draft.config) {
|
||||||
draft.config = {};
|
draft.config = {};
|
||||||
}
|
}
|
||||||
draft.config.domain = e.target.value;
|
draft.config.domain = temp;
|
||||||
});
|
});
|
||||||
setDeploy(newData);
|
setDeploy(newData);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -423,6 +662,12 @@ const DeployCDN = () => {
|
|||||||
const DeployWebhook = () => {
|
const DeployWebhook = () => {
|
||||||
const { deploy: data, setDeploy } = useDeployEditContext();
|
const { deploy: data, setDeploy } = useDeployEditContext();
|
||||||
|
|
||||||
|
const { setError } = useDeployEditContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setError({});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<KVList
|
<KVList
|
||||||
|
@ -70,8 +70,8 @@ const KVList = ({ variables, onValueChange }: KVListProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between dark:text-stone-200">
|
||||||
<Label>变量</Label>
|
<Label>{t("variable")}</Label>
|
||||||
<Show when={!!locVariables?.length}>
|
<Show when={!!locVariables?.length}>
|
||||||
<KVEdit
|
<KVEdit
|
||||||
variable={{
|
variable={{
|
||||||
@ -97,7 +97,7 @@ const KVList = ({ variables, onValueChange }: KVListProps) => {
|
|||||||
fallback={
|
fallback={
|
||||||
<div className="border rounded-md p-3 text-sm mt-2 flex flex-col items-center">
|
<div className="border rounded-md p-3 text-sm mt-2 flex flex-col items-center">
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
{t("not.added.yet.variable")}
|
{t("variable.not.added")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<KVEdit
|
<KVEdit
|
||||||
@ -119,7 +119,7 @@ const KVList = ({ variables, onValueChange }: KVListProps) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="border p-3 rounded-md text-stone-700 text-sm">
|
<div className="border p-3 rounded-md text-stone-700 text-sm dark:text-stone-200">
|
||||||
{locVariables?.map((item, index) => (
|
{locVariables?.map((item, index) => (
|
||||||
<div key={index} className="flex justify-between items-center">
|
<div key={index} className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
@ -175,14 +175,14 @@ const KVEdit = ({ variable, trigger, onSave }: KVEditProps) => {
|
|||||||
const handleSaveClick = () => {
|
const handleSaveClick = () => {
|
||||||
if (!locVariable.key) {
|
if (!locVariable.key) {
|
||||||
setErr({
|
setErr({
|
||||||
key: t("name.required"),
|
key: t("variable.name.required"),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!locVariable.value) {
|
if (!locVariable.value) {
|
||||||
setErr({
|
setErr({
|
||||||
value: t("value.required"),
|
value: t("variable.value.required"),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -202,14 +202,14 @@ const KVEdit = ({ variable, trigger, onSave }: KVEditProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTrigger>{trigger}</DialogTrigger>
|
<DialogTrigger>{trigger}</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent className="dark:text-stone-200">
|
||||||
<DialogHeader className="flex flex-col">
|
<DialogHeader className="flex flex-col">
|
||||||
<DialogTitle>变量</DialogTitle>
|
<DialogTitle>{t("variable")}</DialogTitle>
|
||||||
|
|
||||||
<div className="pt-5 flex flex-col items-start">
|
<div className="pt-5 flex flex-col items-start">
|
||||||
<Label>名称</Label>
|
<Label>{t("variable.name")}</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="请输入变量名"
|
placeholder={t("variable.name.placeholder")}
|
||||||
value={locVariable?.key}
|
value={locVariable?.key}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setLocVariable({ ...locVariable, key: e.target.value });
|
setLocVariable({ ...locVariable, key: e.target.value });
|
||||||
@ -220,9 +220,9 @@ const KVEdit = ({ variable, trigger, onSave }: KVEditProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-2 flex flex-col items-start">
|
<div className="pt-2 flex flex-col items-start">
|
||||||
<Label>值</Label>
|
<Label>{t("variable.value")}</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="请输入变量值"
|
placeholder={t("variable.value.placeholder")}
|
||||||
value={locVariable?.value}
|
value={locVariable?.value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setLocVariable({ ...locVariable, value: e.target.value });
|
setLocVariable({ ...locVariable, value: e.target.value });
|
||||||
|
115
ui/src/components/ui/breadcrumb.tsx
Normal file
115
ui/src/components/ui/breadcrumb.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Breadcrumb = React.forwardRef<
|
||||||
|
HTMLElement,
|
||||||
|
React.ComponentPropsWithoutRef<"nav"> & {
|
||||||
|
separator?: React.ReactNode
|
||||||
|
}
|
||||||
|
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
||||||
|
Breadcrumb.displayName = "Breadcrumb"
|
||||||
|
|
||||||
|
const BreadcrumbList = React.forwardRef<
|
||||||
|
HTMLOListElement,
|
||||||
|
React.ComponentPropsWithoutRef<"ol">
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ol
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
BreadcrumbList.displayName = "BreadcrumbList"
|
||||||
|
|
||||||
|
const BreadcrumbItem = React.forwardRef<
|
||||||
|
HTMLLIElement,
|
||||||
|
React.ComponentPropsWithoutRef<"li">
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<li
|
||||||
|
ref={ref}
|
||||||
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
BreadcrumbItem.displayName = "BreadcrumbItem"
|
||||||
|
|
||||||
|
const BreadcrumbLink = React.forwardRef<
|
||||||
|
HTMLAnchorElement,
|
||||||
|
React.ComponentPropsWithoutRef<"a"> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
>(({ asChild, className, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "a"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
className={cn("transition-colors hover:text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
BreadcrumbLink.displayName = "BreadcrumbLink"
|
||||||
|
|
||||||
|
const BreadcrumbPage = React.forwardRef<
|
||||||
|
HTMLSpanElement,
|
||||||
|
React.ComponentPropsWithoutRef<"span">
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<span
|
||||||
|
ref={ref}
|
||||||
|
role="link"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-current="page"
|
||||||
|
className={cn("font-normal text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
BreadcrumbPage.displayName = "BreadcrumbPage"
|
||||||
|
|
||||||
|
const BreadcrumbSeparator = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"li">) => (
|
||||||
|
<li
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <ChevronRight />}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
||||||
|
|
||||||
|
const BreadcrumbEllipsis = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) => (
|
||||||
|
<span
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
<span className="sr-only">More</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
BreadcrumbEllipsis,
|
||||||
|
}
|
@ -91,23 +91,15 @@ export type GodaddyConfig = {
|
|||||||
apiSecret: string;
|
apiSecret: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LocalConfig = {
|
export type LocalConfig = Record<string, string>;
|
||||||
command: string;
|
|
||||||
certPath: string;
|
|
||||||
keyPath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSHConfig = {
|
export type SSHConfig = {
|
||||||
host: string;
|
host: string;
|
||||||
port: string;
|
port: string;
|
||||||
preCommand?: string;
|
|
||||||
command: string;
|
|
||||||
username: string;
|
username: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
keyFile?: string;
|
keyFile?: string;
|
||||||
certPath: string;
|
|
||||||
keyPath: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebhookConfig = {
|
export type WebhookConfig = {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Deployment, Pahse } from "./deployment";
|
import { Deployment, Pahse } from "./deployment";
|
||||||
|
|
||||||
export type Domain = {
|
export type Domain = {
|
||||||
id: string;
|
id?: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
crontab: string;
|
crontab: string;
|
||||||
|
@ -1 +1 @@
|
|||||||
export const version = "Certimate v0.1.19";
|
export const version = "Certimate v0.2.0";
|
||||||
|
@ -224,5 +224,20 @@
|
|||||||
"access.form.ssh.pre.command.not.empty": "Command to be executed before deploying the certificate",
|
"access.form.ssh.pre.command.not.empty": "Command to be executed before deploying the certificate",
|
||||||
"access.form.ssh.command": "Command",
|
"access.form.ssh.command": "Command",
|
||||||
"access.form.ssh.command.not.empty": "Please enter command",
|
"access.form.ssh.command.not.empty": "Please enter command",
|
||||||
"access.form.ding.access.token.placeholder": "Signature for signed addition"
|
"access.form.ding.access.token.placeholder": "Signature for signed addition",
|
||||||
|
|
||||||
|
"variable": "Variable",
|
||||||
|
"variable.name": "Name",
|
||||||
|
"variable.value": "Value",
|
||||||
|
"variable.not.added": "Variable not added yet",
|
||||||
|
"variable.name.required": "Variable name cannot be empty",
|
||||||
|
"variable.value.required": "Variable value cannot be empty",
|
||||||
|
"variable.name.placeholder": "Variable name",
|
||||||
|
"variable.value.placeholder": "Variable value",
|
||||||
|
|
||||||
|
"deployment": "Deployment",
|
||||||
|
"deployment.not.added": "Deployment not added yet",
|
||||||
|
"deployment.access.type": "Access Type",
|
||||||
|
"deployment.access.config": "Access Configuration",
|
||||||
|
"deployment.access.cdn.deploy.to.domain": "Deploy to domain"
|
||||||
}
|
}
|
||||||
|
@ -224,5 +224,20 @@
|
|||||||
"access.form.ssh.pre.command.not.empty": "在部署证书前执行的前置命令",
|
"access.form.ssh.pre.command.not.empty": "在部署证书前执行的前置命令",
|
||||||
"access.form.ssh.command": "Command",
|
"access.form.ssh.command": "Command",
|
||||||
"access.form.ssh.command.not.empty": "请输入要执行的命令",
|
"access.form.ssh.command.not.empty": "请输入要执行的命令",
|
||||||
"access.form.ding.access.token.placeholder": "加签的签名"
|
"access.form.ding.access.token.placeholder": "加签的签名",
|
||||||
|
|
||||||
|
"variable": "变量",
|
||||||
|
"variable.name": "变量名",
|
||||||
|
"variable.value": "值",
|
||||||
|
"variable.not.added": "尚未添加变量",
|
||||||
|
"variable.name.required": "变量名不能为空",
|
||||||
|
"variable.value.required": "变量值不能为空",
|
||||||
|
"variable.name.placeholder": "请输入变量名",
|
||||||
|
"variable.value.placeholder": "请输入变量值",
|
||||||
|
|
||||||
|
"deployment": "部署",
|
||||||
|
"deployment.not.added": "暂无部署配置,请添加后开始部署证书吧",
|
||||||
|
"deployment.access.type": "授权类型",
|
||||||
|
"deployment.access.config": "授权配置",
|
||||||
|
"deployment.access.cdn.deploy.to.domain": "部署到域名"
|
||||||
}
|
}
|
||||||
|
@ -23,39 +23,44 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { useConfig } from "@/providers/config";
|
import { useConfig } from "@/providers/config";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Domain, targetTypeKeys, targetTypeMap } from "@/domain/domain";
|
import { DeployConfig, Domain } from "@/domain/domain";
|
||||||
import { save, get } from "@/repository/domains";
|
import { save, get } from "@/repository/domains";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
import { PbErrorData } from "@/domain/base";
|
import { PbErrorData } from "@/domain/base";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { Plus, Trash2, Edit as EditIcon } 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";
|
import EmailsEdit from "@/components/certimate/EmailsEdit";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { EmailsSetting } from "@/domain/settings";
|
import { EmailsSetting } from "@/domain/settings";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import StringList from "@/components/certimate/StringList";
|
import StringList from "@/components/certimate/StringList";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import DeployList from "@/components/certimate/DeployList";
|
import DeployList from "@/components/certimate/DeployList";
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from "@/components/ui/breadcrumb";
|
||||||
|
|
||||||
const Edit = () => {
|
const Edit = () => {
|
||||||
const {
|
const {
|
||||||
config: { accesses, emails, accessGroups },
|
config: { accesses, emails },
|
||||||
} = useConfig();
|
} = useConfig();
|
||||||
|
|
||||||
const [domain, setDomain] = useState<Domain>();
|
const [domain, setDomain] = useState<Domain>({} as Domain);
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [tab, setTab] = useState<"apply" | "deploy">("apply");
|
const [tab, setTab] = useState<"apply" | "deploy">("apply");
|
||||||
|
|
||||||
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);
|
||||||
@ -64,7 +69,6 @@ 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();
|
||||||
}
|
}
|
||||||
@ -109,22 +113,8 @@ const Edit = () => {
|
|||||||
}
|
}
|
||||||
}, [domain, form]);
|
}, [domain, form]);
|
||||||
|
|
||||||
const targetAccesses = accesses.filter((item) => {
|
|
||||||
if (item.usage == "apply") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType == "") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const types = targetType.split("-");
|
|
||||||
return item.configType === types[0];
|
|
||||||
});
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
const req: Domain = {
|
const req: Domain = {
|
||||||
@ -142,7 +132,7 @@ const Edit = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await save(req);
|
const resp = await save(req);
|
||||||
let description = t("domain.management.edit.succeed.tips");
|
let description = t("domain.management.edit.succeed.tips");
|
||||||
if (req.id == "") {
|
if (req.id == "") {
|
||||||
description = t("domain.management.add.succeed.tips");
|
description = t("domain.management.add.succeed.tips");
|
||||||
@ -152,7 +142,44 @@ const Edit = () => {
|
|||||||
title: t("succeed"),
|
title: t("succeed"),
|
||||||
description,
|
description,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!domain?.id) setTab("deploy");
|
if (!domain?.id) setTab("deploy");
|
||||||
|
setDomain({ ...resp });
|
||||||
|
} catch (e) {
|
||||||
|
const err = e as ClientResponseError;
|
||||||
|
|
||||||
|
Object.entries(err.response.data as PbErrorData).forEach(
|
||||||
|
([key, value]) => {
|
||||||
|
form.setError(key as keyof z.infer<typeof formSchema>, {
|
||||||
|
type: "manual",
|
||||||
|
message: value.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handelOnDeployListChange = async (list: DeployConfig[]) => {
|
||||||
|
const req = {
|
||||||
|
...domain,
|
||||||
|
deployConfig: list,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const resp = await save(req);
|
||||||
|
let description = t("domain.management.edit.succeed.tips");
|
||||||
|
if (req.id == "") {
|
||||||
|
description = t("domain.management.add.succeed.tips");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t("succeed"),
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!domain?.id) setTab("deploy");
|
||||||
|
setDomain({ ...resp });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const err = e as ClientResponseError;
|
const err = e as ClientResponseError;
|
||||||
|
|
||||||
@ -174,7 +201,22 @@ const Edit = () => {
|
|||||||
<div className="">
|
<div className="">
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<div className=" h-5 text-muted-foreground">
|
<div className=" h-5 text-muted-foreground">
|
||||||
{domain?.id ? t("domain.edit") : t("domain.add")}
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink href="#/domains">
|
||||||
|
{t("domain.management.name")}
|
||||||
|
</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator />
|
||||||
|
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage>
|
||||||
|
{domain?.id ? t("domain.edit") : t("domain.add")}
|
||||||
|
</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex w-full justify-center md:space-x-10 flex-col md:flex-row">
|
<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 md:mt-5">
|
<div className="w-full md:w-[200px] text-muted-foreground space-x-3 md:space-y-3 flex-row md:flex-col flex md:mt-5">
|
||||||
@ -425,7 +467,12 @@ const Edit = () => {
|
|||||||
tab == "apply" && "hidden"
|
tab == "apply" && "hidden"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DeployList deploys={domain?.deployConfig ?? []} />
|
<DeployList
|
||||||
|
deploys={domain?.deployConfig ?? []}
|
||||||
|
onChange={(list: DeployConfig[]) => {
|
||||||
|
handelOnDeployListChange(list);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -97,7 +97,6 @@ const Home = () => {
|
|||||||
const checkedDomains = domains.filter((domain) => domain.id === id);
|
const checkedDomains = domains.filter((domain) => domain.id === id);
|
||||||
const isChecked = checkedDomains[0].enabled;
|
const isChecked = checkedDomains[0].enabled;
|
||||||
|
|
||||||
|
|
||||||
const data = checkedDomains[0];
|
const data = checkedDomains[0];
|
||||||
data.enabled = !isChecked;
|
data.enabled = !isChecked;
|
||||||
|
|
||||||
@ -114,8 +113,8 @@ const Home = () => {
|
|||||||
|
|
||||||
const handleRightNowClick = async (domain: Domain) => {
|
const handleRightNowClick = async (domain: Domain) => {
|
||||||
try {
|
try {
|
||||||
unsubscribeId(domain.id);
|
unsubscribeId(domain.id ?? "");
|
||||||
subscribeId(domain.id, (resp) => {
|
subscribeId(domain.id ?? "", (resp) => {
|
||||||
console.log(resp);
|
console.log(resp);
|
||||||
const updatedDomains = domains.map((domain) => {
|
const updatedDomains = domains.map((domain) => {
|
||||||
if (domain.id === resp.id) {
|
if (domain.id === resp.id) {
|
||||||
@ -283,7 +282,7 @@ const Home = () => {
|
|||||||
<Switch
|
<Switch
|
||||||
checked={domain.enabled}
|
checked={domain.enabled}
|
||||||
onCheckedChange={() => {
|
onCheckedChange={() => {
|
||||||
handelCheckedChange(domain.id);
|
handelCheckedChange(domain.id ?? "");
|
||||||
}}
|
}}
|
||||||
></Switch>
|
></Switch>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@ -299,7 +298,7 @@ const Home = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant={"link"}
|
variant={"link"}
|
||||||
className="p-0"
|
className="p-0"
|
||||||
onClick={() => handleHistoryClick(domain.id)}
|
onClick={() => handleHistoryClick(domain.id ?? "")}
|
||||||
>
|
>
|
||||||
{t("deployment.log.name")}
|
{t("deployment.log.name")}
|
||||||
</Button>
|
</Button>
|
||||||
@ -364,7 +363,7 @@ const Home = () => {
|
|||||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDeleteClick(domain.id);
|
handleDeleteClick(domain.id ?? "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("confirm")}
|
{t("confirm")}
|
||||||
@ -377,7 +376,7 @@ const Home = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant={"link"}
|
variant={"link"}
|
||||||
className="p-0"
|
className="p-0"
|
||||||
onClick={() => handleEditClick(domain.id)}
|
onClick={() => handleEditClick(domain.id ?? "")}
|
||||||
>
|
>
|
||||||
{t("edit")}
|
{t("edit")}
|
||||||
</Button>
|
</Button>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user