mirror of https://github.com/usual2970/certimate
v0.2
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") != "" {
|
|
||||||
singleDeployer, err := Get(record, cert)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rs = append(rs, singleDeployer)
|
|
||||||
}
|
|
||||||
|
|
||||||
if record.GetString("group") != "" {
|
|
||||||
group := record.ExpandedOne("group")
|
|
||||||
|
|
||||||
if errs := app.GetApp().Dao().ExpandRecord(group, []string{"access"}, nil); len(errs) > 0 {
|
|
||||||
|
|
||||||
errList := make([]error, 0)
|
|
||||||
for name, err := range errs {
|
|
||||||
errList = append(errList, fmt.Errorf("展开记录失败,%s: %w", name, err))
|
|
||||||
}
|
|
||||||
err := errors.Join(errList...)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
records := group.ExpandedAll("access")
|
|
||||||
|
|
||||||
deployers, err := getByGroup(record, cert, records...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rs = append(rs, deployers...)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return rs, nil
|
return rs, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getByGroup(record *models.Record, cert *applicant.Certificate, accesses ...*models.Record) ([]Deployer, error) {
|
deployConfigs := make([]domain.DeployConfig, 0)
|
||||||
|
|
||||||
rs := make([]Deployer, 0)
|
err := record.UnmarshalJSONField("deployConfig", &deployConfigs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("解析部署配置失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(deployConfigs) == 0 {
|
||||||
|
return rs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, deployConfig := range deployConfigs {
|
||||||
|
|
||||||
|
deployer, err := getWithDeployConfig(record, cert, deployConfig)
|
||||||
|
|
||||||
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"
|
||||||
|
|
@ -24,10 +23,6 @@ type sshAccess struct {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,11 +56,7 @@ 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 {
|
||||||
|
|
@ -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,10 +140,6 @@ 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,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink href="#/domains">
|
||||||
|
{t("domain.management.name")}
|
||||||
|
</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator />
|
||||||
|
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage>
|
||||||
{domain?.id ? t("domain.edit") : t("domain.add")}
|
{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…
Reference in New Issue