mirror of https://github.com/usual2970/certimate
v0.2
parent
19f5348802
commit
1928a47961
|
|
@ -3,6 +3,7 @@ package applicant
|
|||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"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_SECRET_KEY", access.AccessKeySecret)
|
||||
os.Setenv("ALICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
dnsProvider, err := alidns.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package applicant
|
||||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"certimate/internal/utils/app"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
|
|
@ -46,6 +47,8 @@ var sslProviderUrls = map[string]string{
|
|||
|
||||
const defaultEmail = "536464346@qq.com"
|
||||
|
||||
const defaultTimeout = 60
|
||||
|
||||
type Certificate struct {
|
||||
CertUrl string `json:"certUrl"`
|
||||
CertStableUrl string `json:"certStableUrl"`
|
||||
|
|
@ -60,6 +63,7 @@ type ApplyOption struct {
|
|||
Domain string `json:"domain"`
|
||||
Access string `json:"access"`
|
||||
Nameservers string `json:"nameservers"`
|
||||
Timeout int64 `json:"timeout"`
|
||||
}
|
||||
|
||||
type MyUser struct {
|
||||
|
|
@ -83,8 +87,22 @@ type Applicant interface {
|
|||
}
|
||||
|
||||
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 == "" {
|
||||
email = defaultEmail
|
||||
}
|
||||
|
|
@ -92,7 +110,8 @@ func Get(record *models.Record) (Applicant, error) {
|
|||
Email: email,
|
||||
Domain: record.GetString("domain"),
|
||||
Access: access.GetString("config"),
|
||||
Nameservers: record.GetString("nameservers"),
|
||||
Nameservers: applyConfig.Nameservers,
|
||||
Timeout: applyConfig.Timeout,
|
||||
}
|
||||
switch access.GetString("configType") {
|
||||
case configTypeAliyun:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package applicant
|
|||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
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)
|
||||
|
||||
os.Setenv("CLOUDFLARE_DNS_API_TOKEN", access.DnsApiToken)
|
||||
os.Setenv("CLOUDFLARE_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", c.option.Timeout))
|
||||
|
||||
provider, err := cf.NewDNSProvider()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package applicant
|
|||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
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_SECRET", access.ApiSecret)
|
||||
os.Setenv("GODADDY_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
|
||||
dnsProvider, err := godaddyProvider.NewDNSProvider()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package applicant
|
|||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
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_ACCESS_KEY_ID", access.AccessKeyId)
|
||||
os.Setenv("HUAWEICLOUD_SECRET_ACCESS_KEY", access.SecretAccessKey)
|
||||
os.Setenv("HUAWEICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
|
||||
|
||||
dnsProvider, err := huaweicloudProvider.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package applicant
|
|||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
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)
|
||||
|
||||
os.Setenv("NAMESILO_API_KEY", access.ApiKey)
|
||||
os.Setenv("NAMESILO_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
|
||||
dnsProvider, err := namesiloProvider.NewDNSProvider()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package applicant
|
|||
import (
|
||||
"certimate/internal/domain"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"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_KEY", access.SecretKey)
|
||||
os.Setenv("TENCENTCLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
|
||||
|
||||
dnsProvider, err := tencentcloud.NewDNSProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ func (a *aliyun) resource() (*cas20200407.ListCloudResourcesResponseBodyData, er
|
|||
|
||||
listCloudResourcesRequest := &cas20200407.ListCloudResourcesRequest{
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package deployer
|
|||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"certimate/internal/utils/rand"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
|
@ -46,9 +47,9 @@ func (a *AliyunCdn) GetInfo() []string {
|
|||
|
||||
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{
|
||||
DomainName: tea.String(a.option.Domain),
|
||||
DomainName: tea.String(getDeployString(a.option.DeployConfig, "domain")),
|
||||
CertName: tea.String(certName),
|
||||
CertType: tea.String("upload"),
|
||||
SSLProtocol: tea.String("on"),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ package deployer
|
|||
|
||||
import (
|
||||
"certimate/internal/domain"
|
||||
"certimate/internal/utils/rand"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
|
@ -51,9 +52,9 @@ func (a *AliyunEsa) GetInfo() []string {
|
|||
|
||||
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{
|
||||
DomainName: tea.String(a.option.Domain),
|
||||
DomainName: tea.String(getDeployString(a.option.DeployConfig, "domain")),
|
||||
CertName: tea.String(certName),
|
||||
CertType: tea.String("upload"),
|
||||
SSLProtocol: tea.String("on"),
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package deployer
|
|||
|
||||
import (
|
||||
"certimate/internal/applicant"
|
||||
"certimate/internal/domain"
|
||||
"certimate/internal/utils/app"
|
||||
"certimate/internal/utils/variables"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
|
@ -30,6 +30,7 @@ type DeployerOption struct {
|
|||
Product string `json:"product"`
|
||||
Access string `json:"access"`
|
||||
AceessRecord *models.Record `json:"-"`
|
||||
DeployConfig domain.DeployConfig `json:"deployConfig"`
|
||||
Certificate applicant.Certificate `json:"certificate"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
|
|
@ -42,52 +43,29 @@ type Deployer interface {
|
|||
|
||||
func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error) {
|
||||
rs := make([]Deployer, 0)
|
||||
|
||||
if record.GetString("targetAccess") != "" {
|
||||
singleDeployer, err := Get(record, cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rs = append(rs, singleDeployer)
|
||||
if record.GetString("deployConfig") == "" {
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
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...)
|
||||
deployConfigs := make([]domain.DeployConfig, 0)
|
||||
|
||||
err := record.UnmarshalJSONField("deployConfig", &deployConfigs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析部署配置失败: %w", err)
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
if len(deployConfigs) == 0 {
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
}
|
||||
for _, deployConfig := range deployConfigs {
|
||||
|
||||
func getByGroup(record *models.Record, cert *applicant.Certificate, accesses ...*models.Record) ([]Deployer, error) {
|
||||
deployer, err := getWithDeployConfig(record, cert, deployConfig)
|
||||
|
||||
rs := make([]Deployer, 0)
|
||||
|
||||
for _, access := range accesses {
|
||||
deployer, err := getWithAccess(record, cert, access)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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{
|
||||
DomainId: record.Id,
|
||||
Domain: record.GetString("domain"),
|
||||
Product: getProduct(record),
|
||||
Product: getProduct(deployConfig.Type),
|
||||
Access: access.GetString("config"),
|
||||
AceessRecord: access,
|
||||
Variables: variables.Parse2Map(record.GetString("variables")),
|
||||
DeployConfig: deployConfig,
|
||||
}
|
||||
if cert != nil {
|
||||
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:
|
||||
return NewAliyun(option)
|
||||
case targetAliyunCdn:
|
||||
|
|
@ -136,16 +120,8 @@ func getWithAccess(record *models.Record, cert *applicant.Certificate, access *m
|
|||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
|
||||
|
||||
access := record.ExpandedOne("targetAccess")
|
||||
|
||||
return getWithAccess(record, cert, access)
|
||||
}
|
||||
|
||||
func getProduct(record *models.Record) string {
|
||||
targetType := record.GetString("targetType")
|
||||
rs := strings.Split(targetType, "-")
|
||||
func getProduct(t string) string {
|
||||
rs := strings.Split(t, "-")
|
||||
if len(rs) < 2 {
|
||||
return ""
|
||||
}
|
||||
|
|
@ -159,3 +135,39 @@ func toStr(tag string, data any) string {
|
|||
byts, _ := json.Marshal(data)
|
||||
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 {
|
||||
Command string `json:"command"`
|
||||
CertPath string `json:"certPath"`
|
||||
KeyPath string `json:"keyPath"`
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 执行命令
|
||||
|
||||
if err := execCmd(access.Command); err != nil {
|
||||
if err := execCmd(getDeployString(l.option.DeployConfig, "command")); err != nil {
|
||||
return fmt.Errorf("执行命令失败: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ func (q *qiuniu) Deploy(ctx context.Context) 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{
|
||||
CertID: certId,
|
||||
|
|
@ -104,7 +104,7 @@ type domainInfo struct {
|
|||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
|
|
@ -135,8 +135,8 @@ func (q *qiuniu) uploadCert() (string, error) {
|
|||
path := "/sslcert"
|
||||
|
||||
body := &uploadCertReq{
|
||||
Name: q.option.Domain,
|
||||
CommonName: q.option.Domain,
|
||||
Name: getDeployString(q.option.DeployConfig, "domain"),
|
||||
CommonName: getDeployString(q.option.DeployConfig, "domain"),
|
||||
Pri: q.option.Certificate.PrivateKey,
|
||||
Ca: q.option.Certificate.Certificate,
|
||||
}
|
||||
|
|
@ -166,7 +166,7 @@ type modifyDomainCertReq struct {
|
|||
}
|
||||
|
||||
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{
|
||||
CertID: certId,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
xpath "path"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/sftp"
|
||||
sshPkg "golang.org/x/crypto/ssh"
|
||||
|
|
@ -19,15 +18,11 @@ type ssh struct {
|
|||
}
|
||||
|
||||
type sshAccess struct {
|
||||
Host string `json:"host"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Key string `json:"key"`
|
||||
Port string `json:"port"`
|
||||
PreCommand string `json:"preCommand"`
|
||||
Command string `json:"command"`
|
||||
CertPath string `json:"certPath"`
|
||||
KeyPath string `json:"keyPath"`
|
||||
Host string `json:"host"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Key string `json:"key"`
|
||||
Port string `json:"port"`
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
|
|
@ -70,29 +55,30 @@ func (s *ssh) Deploy(ctx context.Context) error {
|
|||
s.infos = append(s.infos, toStr("ssh连接成功", nil))
|
||||
|
||||
// 执行前置命令
|
||||
if access.PreCommand != "" {
|
||||
err, stdout, stderr := s.sshExecCommand(client, access.PreCommand)
|
||||
preCommand := getDeployString(s.option.DeployConfig, "preCommand")
|
||||
if preCommand != "" {
|
||||
err, stdout, stderr := s.sshExecCommand(client, preCommand)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@ type webhookAccess struct {
|
|||
}
|
||||
|
||||
type hookData struct {
|
||||
Domain string `json:"domain"`
|
||||
Certificate string `json:"certificate"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Domain string `json:"domain"`
|
||||
Certificate string `json:"certificate"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
|
||||
type webhook struct {
|
||||
|
|
@ -50,6 +51,7 @@ func (w *webhook) Deploy(ctx context.Context) error {
|
|||
Domain: w.option.Domain,
|
||||
Certificate: w.option.Certificate.Certificate,
|
||||
PrivateKey: w.option.Certificate.PrivateKey,
|
||||
Variables: getDeployVariables(w.option.DeployConfig),
|
||||
}
|
||||
|
||||
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/utils/app"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
|
|
@ -41,18 +40,6 @@ func deploy(ctx context.Context, record *models.Record) error {
|
|||
return err
|
||||
}
|
||||
history.record(checkPhase, "获取记录成功", nil)
|
||||
if errs := app.GetApp().Dao().ExpandRecord(currRecord, []string{"access", "targetAccess", "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")
|
||||
expiredAt := currRecord.GetDateTime("expiredAt").Time()
|
||||
|
|
@ -106,6 +93,13 @@ func deploy(ctx context.Context, record *models.Record) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// 没有部署配置,也算成功
|
||||
if len(deployers) == 0 {
|
||||
history.record(deployPhase, "没有部署配置", &RecordInfo{Info: []string{"没有部署配置"}})
|
||||
history.setWholeSuccess(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, deployer := range deployers {
|
||||
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" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Certimate - Your Trusted SSL Automation Partner</title>
|
||||
<script type="module" crossorigin src="/assets/index-DpHAV802.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DOft-CKV.css">
|
||||
<script type="module" crossorigin src="/assets/index-DbwFzZm1.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CWUb5Xuf.css">
|
||||
</head>
|
||||
<body class="bg-background">
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.417.0",
|
||||
"moment": "^2.30.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"pocketbase": "^0.21.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
|
@ -4159,9 +4160,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-5.0.7.tgz",
|
||||
"integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -4169,10 +4170,10 @@
|
|||
}
|
||||
],
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
|
|
@ -4561,6 +4562,23 @@
|
|||
"resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
|
|
|||
|
|
@ -29,22 +29,23 @@
|
|||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^23.15.1",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-http-backend": "^2.6.1",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.417.0",
|
||||
"moment": "^2.30.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"pocketbase": "^0.21.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-i18next": "^15.0.2",
|
||||
"react-router-dom": "^6.25.1",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.9.1",
|
||||
"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"
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
import {
|
||||
Access,
|
||||
accessFormType,
|
||||
getUsageByConfigType,
|
||||
LocalConfig,
|
||||
SSHConfig,
|
||||
} from "@/domain/access";
|
||||
import { Access, accessFormType, getUsageByConfigType } from "@/domain/access";
|
||||
import { useConfig } from "@/providers/config";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
|
@ -20,7 +14,7 @@ import {
|
|||
} from "../ui/form";
|
||||
import { Input } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
|
||||
import { save } from "@/repository/access";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
import { PbErrorData } from "@/domain/base";
|
||||
|
|
@ -39,30 +33,19 @@ const AccessLocalForm = ({
|
|||
|
||||
const formSchema = z.object({
|
||||
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,
|
||||
|
||||
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>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
id: data?.id,
|
||||
name: data?.name || '',
|
||||
name: data?.name || "",
|
||||
configType: "local",
|
||||
certPath: config.certPath,
|
||||
keyPath: config.keyPath,
|
||||
command: config.command,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -73,15 +56,11 @@ const AccessLocalForm = ({
|
|||
configType: data.configType,
|
||||
usage: getUsageByConfigType(data.configType),
|
||||
|
||||
config: {
|
||||
command: data.command,
|
||||
certPath: data.certPath,
|
||||
keyPath: data.keyPath,
|
||||
},
|
||||
config: {},
|
||||
};
|
||||
|
||||
try {
|
||||
req.id = op == "copy" ? "" : req.id;
|
||||
req.id = op == "copy" ? "" : req.id;
|
||||
const rs = await save(req);
|
||||
|
||||
onAfterReq();
|
||||
|
|
@ -128,9 +107,12 @@ const AccessLocalForm = ({
|
|||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('name')}</FormLabel>
|
||||
<FormLabel>{t("name")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('access.form.name.not.empty')} {...field} />
|
||||
<Input
|
||||
placeholder={t("access.form.name.not.empty")}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
|
@ -143,7 +125,7 @@ const AccessLocalForm = ({
|
|||
name="id"
|
||||
render={({ field }) => (
|
||||
<FormItem className="hidden">
|
||||
<FormLabel>{t('access.form.config.field')}</FormLabel>
|
||||
<FormLabel>{t("access.form.config.field")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
|
@ -158,7 +140,7 @@ const AccessLocalForm = ({
|
|||
name="configType"
|
||||
render={({ field }) => (
|
||||
<FormItem className="hidden">
|
||||
<FormLabel>{t('access.form.config.field')}</FormLabel>
|
||||
<FormLabel>{t("access.form.config.field")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</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 />
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">{t('save')}</Button>
|
||||
<Button type="submit">{t("save")}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import {
|
|||
} from "../ui/form";
|
||||
import { Input } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
import { save } from "@/repository/access";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
import { PbErrorData } from "@/domain/base";
|
||||
|
|
@ -66,7 +65,10 @@ const AccessSSHForm = ({
|
|||
|
||||
const formSchema = z.object({
|
||||
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,
|
||||
host: z.string().refine(
|
||||
(str) => {
|
||||
|
|
@ -77,16 +79,23 @@ const AccessSSHForm = ({
|
|||
}
|
||||
),
|
||||
group: z.string().optional(),
|
||||
port: z.string().min(1, 'access.form.ssh.port.not.empty').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 })),
|
||||
port: z
|
||||
.string()
|
||||
.min(1, "access.form.ssh.port.not.empty")
|
||||
.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(),
|
||||
|
||||
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 = {
|
||||
|
|
@ -96,10 +105,6 @@ const AccessSSHForm = ({
|
|||
password: "",
|
||||
key: "",
|
||||
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;
|
||||
|
||||
|
|
@ -107,7 +112,7 @@ const AccessSSHForm = ({
|
|||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
id: data?.id,
|
||||
name: data?.name || '',
|
||||
name: data?.name || "",
|
||||
configType: "ssh",
|
||||
group: data?.group,
|
||||
host: config.host,
|
||||
|
|
@ -116,10 +121,6 @@ const AccessSSHForm = ({
|
|||
password: config.password,
|
||||
key: config.key,
|
||||
keyFile: config.keyFile,
|
||||
certPath: config.certPath,
|
||||
keyPath: config.keyPath,
|
||||
command: config.command,
|
||||
preCommand: config.preCommand,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -139,15 +140,11 @@ const AccessSSHForm = ({
|
|||
username: data.username,
|
||||
password: data.password,
|
||||
key: data.key,
|
||||
command: data.command,
|
||||
preCommand: data.preCommand,
|
||||
certPath: data.certPath,
|
||||
keyPath: data.keyPath,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
req.id = op == "copy" ? "" : req.id;
|
||||
req.id = op == "copy" ? "" : req.id;
|
||||
const rs = await save(req);
|
||||
|
||||
onAfterReq();
|
||||
|
|
@ -228,9 +225,12 @@ const AccessSSHForm = ({
|
|||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('name')}</FormLabel>
|
||||
<FormLabel>{t("name")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('access.form.name.not.empty')} {...field} />
|
||||
<Input
|
||||
placeholder={t("access.form.name.not.empty")}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
|
@ -244,12 +244,12 @@ const AccessSSHForm = ({
|
|||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="w-full flex justify-between">
|
||||
<div>{t('access.form.ssh.group.label')}</div>
|
||||
<div>{t("access.form.ssh.group.label")}</div>
|
||||
<AccessGroupEdit
|
||||
trigger={
|
||||
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
||||
<Plus size={14} />
|
||||
{t('add')}
|
||||
{t("add")}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
|
@ -264,7 +264,9 @@ const AccessSSHForm = ({
|
|||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('access.group.not.empty')} />
|
||||
<SelectValue
|
||||
placeholder={t("access.group.not.empty")}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="emptyId">
|
||||
|
|
@ -304,7 +306,7 @@ const AccessSSHForm = ({
|
|||
name="id"
|
||||
render={({ field }) => (
|
||||
<FormItem className="hidden">
|
||||
<FormLabel>{t('access.form.config.field')}</FormLabel>
|
||||
<FormLabel>{t("access.form.config.field")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
|
@ -319,7 +321,7 @@ const AccessSSHForm = ({
|
|||
name="configType"
|
||||
render={({ field }) => (
|
||||
<FormItem className="hidden">
|
||||
<FormLabel>{t('access.form.config.field')}</FormLabel>
|
||||
<FormLabel>{t("access.form.config.field")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
|
@ -334,9 +336,12 @@ const AccessSSHForm = ({
|
|||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem className="grow">
|
||||
<FormLabel>{t('access.form.ssh.host')}</FormLabel>
|
||||
<FormLabel>{t("access.form.ssh.host")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('access.form.ssh.host.not.empty')} {...field} />
|
||||
<Input
|
||||
placeholder={t("access.form.ssh.host.not.empty")}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
|
@ -349,10 +354,10 @@ const AccessSSHForm = ({
|
|||
name="port"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('access.form.ssh.port')}</FormLabel>
|
||||
<FormLabel>{t("access.form.ssh.port")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('access.form.ssh.port.not.empty')}
|
||||
placeholder={t("access.form.ssh.port.not.empty")}
|
||||
{...field}
|
||||
type="number"
|
||||
/>
|
||||
|
|
@ -369,9 +374,9 @@ const AccessSSHForm = ({
|
|||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('username')}</FormLabel>
|
||||
<FormLabel>{t("username")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('username.not.empty')} {...field} />
|
||||
<Input placeholder={t("username.not.empty")} {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
|
@ -384,10 +389,10 @@ const AccessSSHForm = ({
|
|||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('password')}</FormLabel>
|
||||
<FormLabel>{t("password")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('password.not.empty')}
|
||||
placeholder={t("password.not.empty")}
|
||||
{...field}
|
||||
type="password"
|
||||
/>
|
||||
|
|
@ -403,9 +408,12 @@ const AccessSSHForm = ({
|
|||
name="key"
|
||||
render={({ field }) => (
|
||||
<FormItem hidden>
|
||||
<FormLabel>{t('access.form.ssh.key')}</FormLabel>
|
||||
<FormLabel>{t("access.form.ssh.key")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('access.form.ssh.key.not.empty')} {...field} />
|
||||
<Input
|
||||
placeholder={t("access.form.ssh.key.not.empty")}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
|
@ -418,7 +426,7 @@ const AccessSSHForm = ({
|
|||
name="keyFile"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('access.form.ssh.key')}</FormLabel>
|
||||
<FormLabel>{t("access.form.ssh.key")}</FormLabel>
|
||||
<FormControl>
|
||||
<div>
|
||||
<Button
|
||||
|
|
@ -428,10 +436,12 @@ const AccessSSHForm = ({
|
|||
className="w-48"
|
||||
onClick={handleSelectFileClick}
|
||||
>
|
||||
{fileName ? fileName : t('access.form.ssh.key.file.not.empty')}
|
||||
{fileName
|
||||
? fileName
|
||||
: t("access.form.ssh.key.file.not.empty")}
|
||||
</Button>
|
||||
<Input
|
||||
placeholder={t('access.form.ssh.key.not.empty')}
|
||||
placeholder={t("access.form.ssh.key.not.empty")}
|
||||
{...field}
|
||||
ref={fileInputRef}
|
||||
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 />
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">{t('save')}</Button>
|
||||
<Button type="submit">{t("save")}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -43,10 +43,14 @@ import { Input } from "../ui/input";
|
|||
import { Textarea } from "../ui/textarea";
|
||||
import KVList from "./KVList";
|
||||
import { produce } from "immer";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
type DeployEditContextProps = {
|
||||
deploy: DeployConfig;
|
||||
error: Record<string, string>;
|
||||
setDeploy: (deploy: DeployConfig) => void;
|
||||
setError: (error: Record<string, string>) => void;
|
||||
};
|
||||
|
||||
const DeployEditContext = createContext<DeployEditContextProps>(
|
||||
|
|
@ -59,53 +63,92 @@ export const useDeployEditContext = () => {
|
|||
|
||||
type DeployListProps = {
|
||||
deploys: DeployConfig[];
|
||||
onChange: (deploys: DeployConfig[]) => void;
|
||||
};
|
||||
|
||||
const DeployList = ({ deploys }: DeployListProps) => {
|
||||
const DeployList = ({ deploys, onChange }: DeployListProps) => {
|
||||
const [list, setList] = useState<DeployConfig[]>([]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
setList(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 (
|
||||
<>
|
||||
<Show
|
||||
when={list.length > 0}
|
||||
fallback={
|
||||
<Alert className="w-full">
|
||||
<Alert className="w-full border dark:border-stone-400">
|
||||
<AlertDescription className="flex flex-col items-center">
|
||||
<div>暂无部署配置,请添加后开始部署证书吧</div>
|
||||
<div>{t("deployment.not.added")}</div>
|
||||
<div className="flex justify-end mt-2">
|
||||
<DeployEditDialog
|
||||
trigger={<Button size={"sm"}>添加部署</Button>}
|
||||
onSave={(config: DeployConfig) => {
|
||||
handleAdd(config);
|
||||
}}
|
||||
trigger={<Button size={"sm"}>{t("add")}</Button>}
|
||||
/>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
}
|
||||
>
|
||||
<div className="flex justify-end py-2 border-b">
|
||||
<DeployEditDialog trigger={<Button size={"sm"}>添加部署</Button>} />
|
||||
<div className="flex justify-end py-2 border-b dark:border-stone-400">
|
||||
<DeployEditDialog
|
||||
trigger={<Button size={"sm"}>{t("add")}</Button>}
|
||||
onSave={(config: DeployConfig) => {
|
||||
handleAdd(config);
|
||||
}}
|
||||
/>
|
||||
</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="flex justify-between text-sm p-3 items-center text-stone-700">
|
||||
<div className="flex space-x-2 items-center">
|
||||
<div>
|
||||
<img src="/imgs/providers/ssh.svg" className="w-9"></img>
|
||||
</div>
|
||||
<div className="text-stone-600 flex-col flex space-y-0">
|
||||
<div>ssh部署</div>
|
||||
<div>业务服务器</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<EditIcon size={16} className="cursor-pointer" />
|
||||
<Trash2 size={16} className="cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
{list.map((item) => (
|
||||
<DeployItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
onDelete={() => {
|
||||
handleDelete(item.id ?? "");
|
||||
}}
|
||||
onSave={(deploy: DeployConfig) => {
|
||||
handleSave(deploy);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</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 = {
|
||||
trigger: React.ReactNode;
|
||||
deployConfig?: DeployConfig;
|
||||
onSave: (deploy: DeployConfig) => void;
|
||||
};
|
||||
const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
|
||||
const DeployEditDialog = ({
|
||||
trigger,
|
||||
deployConfig,
|
||||
onSave,
|
||||
}: DeployEditDialogProps) => {
|
||||
const {
|
||||
config: { accesses },
|
||||
} = useConfig();
|
||||
|
|
@ -129,6 +248,10 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
|
|||
type: "",
|
||||
});
|
||||
|
||||
const [error, setError] = useState<Record<string, string>>({});
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (deployConfig) {
|
||||
setLocDeployConfig({ ...deployConfig });
|
||||
|
|
@ -150,6 +273,7 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
|
|||
t = locDeployConfig.type;
|
||||
}
|
||||
setDeployType(t as TargetType);
|
||||
setError({});
|
||||
}, [locDeployConfig.type]);
|
||||
|
||||
const setDeploy = useCallback(
|
||||
|
|
@ -177,23 +301,62 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
|
|||
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 (
|
||||
<DeployEditContext.Provider
|
||||
value={{
|
||||
deploy: locDeployConfig,
|
||||
setDeploy: setDeploy,
|
||||
error: error,
|
||||
setError: setError,
|
||||
}}
|
||||
>
|
||||
<Dialog>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger>{trigger}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogContent className="dark:text-stone-200">
|
||||
<DialogHeader>
|
||||
<DialogTitle>部署</DialogTitle>
|
||||
<DialogTitle>{t("deployment")}</DialogTitle>
|
||||
<DialogDescription></DialogDescription>
|
||||
</DialogHeader>
|
||||
{/* 授权类型 */}
|
||||
<div>
|
||||
<Label>授权类型</Label>
|
||||
<Label>{t("deployment.access.type")}</Label>
|
||||
|
||||
<Select
|
||||
value={locDeployConfig.type}
|
||||
|
|
@ -227,11 +390,13 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
|
|||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="text-red-500 text-sm mt-1">{error.type}</div>
|
||||
</div>
|
||||
{/* 授权 */}
|
||||
<div>
|
||||
<Label className="flex justify-between">
|
||||
<div>授权配置</div>
|
||||
<div>{t("deployment.access.config")}</div>
|
||||
<AccessEdit
|
||||
trigger={
|
||||
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
||||
|
|
@ -275,12 +440,21 @@ const DeployEditDialog = ({ trigger, deployConfig }: DeployEditDialogProps) => {
|
|||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="text-red-500 text-sm mt-1">{error.access}</div>
|
||||
</div>
|
||||
|
||||
<DeployEdit type={deployType!} />
|
||||
|
||||
<DialogFooter>
|
||||
<Button>保存</Button>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
@ -317,8 +491,27 @@ const DeployEdit = ({ type }: DeployEditProps) => {
|
|||
|
||||
const DeploySSH = () => {
|
||||
const { t } = useTranslation();
|
||||
const { setError } = useDeployEditContext();
|
||||
|
||||
useEffect(() => {
|
||||
setError({});
|
||||
}, []);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="flex flex-col space-y-2">
|
||||
|
|
@ -358,10 +551,11 @@ const DeploySSH = () => {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<Label>前置命令</Label>
|
||||
<Label>{t("access.form.ssh.pre.command")}</Label>
|
||||
<Textarea
|
||||
className="mt-1"
|
||||
value={data?.config?.preCommand}
|
||||
placeholder={t("access.form.ssh.pre.command.not.empty")}
|
||||
onChange={(e) => {
|
||||
const newData = produce(data, (draft) => {
|
||||
if (!draft.config) {
|
||||
|
|
@ -375,10 +569,11 @@ const DeploySSH = () => {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<Label>命令</Label>
|
||||
<Label>{t("access.form.ssh.command")}</Label>
|
||||
<Textarea
|
||||
className="mt-1"
|
||||
value={data?.config?.command}
|
||||
placeholder={t("access.form.ssh.command.not.empty")}
|
||||
onChange={(e) => {
|
||||
const newData = produce(data, (draft) => {
|
||||
if (!draft.config) {
|
||||
|
|
@ -396,25 +591,69 @@ const DeploySSH = () => {
|
|||
};
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div>
|
||||
<Label>部署至域名</Label>
|
||||
<Label>{t("deployment.access.cdn.deploy.to.domain")}</Label>
|
||||
<Input
|
||||
placeholder="部署至域名"
|
||||
placeholder={t("deployment.access.cdn.deploy.to.domain")}
|
||||
className="w-full mt-1"
|
||||
value={data?.config?.domain}
|
||||
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) => {
|
||||
if (!draft.config) {
|
||||
draft.config = {};
|
||||
}
|
||||
draft.config.domain = e.target.value;
|
||||
draft.config.domain = temp;
|
||||
});
|
||||
setDeploy(newData);
|
||||
}}
|
||||
/>
|
||||
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -423,6 +662,12 @@ const DeployCDN = () => {
|
|||
const DeployWebhook = () => {
|
||||
const { deploy: data, setDeploy } = useDeployEditContext();
|
||||
|
||||
const { setError } = useDeployEditContext();
|
||||
|
||||
useEffect(() => {
|
||||
setError({});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<KVList
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@ const KVList = ({ variables, onValueChange }: KVListProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<Label>变量</Label>
|
||||
<div className="flex justify-between dark:text-stone-200">
|
||||
<Label>{t("variable")}</Label>
|
||||
<Show when={!!locVariables?.length}>
|
||||
<KVEdit
|
||||
variable={{
|
||||
|
|
@ -97,7 +97,7 @@ const KVList = ({ variables, onValueChange }: KVListProps) => {
|
|||
fallback={
|
||||
<div className="border rounded-md p-3 text-sm mt-2 flex flex-col items-center">
|
||||
<div className="text-muted-foreground">
|
||||
{t("not.added.yet.variable")}
|
||||
{t("variable.not.added")}
|
||||
</div>
|
||||
|
||||
<KVEdit
|
||||
|
|
@ -119,7 +119,7 @@ const KVList = ({ variables, onValueChange }: KVListProps) => {
|
|||
</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) => (
|
||||
<div key={index} className="flex justify-between items-center">
|
||||
<div>
|
||||
|
|
@ -175,14 +175,14 @@ const KVEdit = ({ variable, trigger, onSave }: KVEditProps) => {
|
|||
const handleSaveClick = () => {
|
||||
if (!locVariable.key) {
|
||||
setErr({
|
||||
key: t("name.required"),
|
||||
key: t("variable.name.required"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!locVariable.value) {
|
||||
setErr({
|
||||
value: t("value.required"),
|
||||
value: t("variable.value.required"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -202,14 +202,14 @@ const KVEdit = ({ variable, trigger, onSave }: KVEditProps) => {
|
|||
}}
|
||||
>
|
||||
<DialogTrigger>{trigger}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogContent className="dark:text-stone-200">
|
||||
<DialogHeader className="flex flex-col">
|
||||
<DialogTitle>变量</DialogTitle>
|
||||
<DialogTitle>{t("variable")}</DialogTitle>
|
||||
|
||||
<div className="pt-5 flex flex-col items-start">
|
||||
<Label>名称</Label>
|
||||
<Label>{t("variable.name")}</Label>
|
||||
<Input
|
||||
placeholder="请输入变量名"
|
||||
placeholder={t("variable.name.placeholder")}
|
||||
value={locVariable?.key}
|
||||
onChange={(e) => {
|
||||
setLocVariable({ ...locVariable, key: e.target.value });
|
||||
|
|
@ -220,9 +220,9 @@ const KVEdit = ({ variable, trigger, onSave }: KVEditProps) => {
|
|||
</div>
|
||||
|
||||
<div className="pt-2 flex flex-col items-start">
|
||||
<Label>值</Label>
|
||||
<Label>{t("variable.value")}</Label>
|
||||
<Input
|
||||
placeholder="请输入变量值"
|
||||
placeholder={t("variable.value.placeholder")}
|
||||
value={locVariable?.value}
|
||||
onChange={(e) => {
|
||||
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;
|
||||
};
|
||||
|
||||
export type LocalConfig = {
|
||||
command: string;
|
||||
certPath: string;
|
||||
keyPath: string;
|
||||
};
|
||||
export type LocalConfig = Record<string, string>;
|
||||
|
||||
export type SSHConfig = {
|
||||
host: string;
|
||||
port: string;
|
||||
preCommand?: string;
|
||||
command: string;
|
||||
username: string;
|
||||
password?: string;
|
||||
key?: string;
|
||||
keyFile?: string;
|
||||
certPath: string;
|
||||
keyPath: string;
|
||||
};
|
||||
|
||||
export type WebhookConfig = {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Deployment, Pahse } from "./deployment";
|
||||
|
||||
export type Domain = {
|
||||
id: string;
|
||||
id?: string;
|
||||
domain: string;
|
||||
email?: 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.command": "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.command": "Command",
|
||||
"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";
|
||||
import { useConfig } from "@/providers/config";
|
||||
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 { ClientResponseError } from "pocketbase";
|
||||
import { PbErrorData } from "@/domain/base";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { Plus, Trash2, Edit as EditIcon } from "lucide-react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { Plus } from "lucide-react";
|
||||
import { AccessEdit } from "@/components/certimate/AccessEdit";
|
||||
import { accessTypeMap } from "@/domain/access";
|
||||
import EmailsEdit from "@/components/certimate/EmailsEdit";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { EmailsSetting } from "@/domain/settings";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import StringList from "@/components/certimate/StringList";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import DeployList from "@/components/certimate/DeployList";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
|
||||
const Edit = () => {
|
||||
const {
|
||||
config: { accesses, emails, accessGroups },
|
||||
config: { accesses, emails },
|
||||
} = useConfig();
|
||||
|
||||
const [domain, setDomain] = useState<Domain>();
|
||||
const [domain, setDomain] = useState<Domain>({} as Domain);
|
||||
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [tab, setTab] = useState<"apply" | "deploy">("apply");
|
||||
|
||||
const [targetType, setTargetType] = useState(domain ? domain.targetType : "");
|
||||
|
||||
useEffect(() => {
|
||||
// Parsing query parameters
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
|
|
@ -64,7 +69,6 @@ const Edit = () => {
|
|||
const fetchData = async () => {
|
||||
const data = await get(id);
|
||||
setDomain(data);
|
||||
setTargetType(data.targetType);
|
||||
};
|
||||
fetchData();
|
||||
}
|
||||
|
|
@ -109,22 +113,8 @@ const Edit = () => {
|
|||
}
|
||||
}, [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 navigate = useNavigate();
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||
console.log(data);
|
||||
const req: Domain = {
|
||||
|
|
@ -142,7 +132,7 @@ const Edit = () => {
|
|||
};
|
||||
|
||||
try {
|
||||
await save(req);
|
||||
const resp = await save(req);
|
||||
let description = t("domain.management.edit.succeed.tips");
|
||||
if (req.id == "") {
|
||||
description = t("domain.management.add.succeed.tips");
|
||||
|
|
@ -152,7 +142,44 @@ const Edit = () => {
|
|||
title: t("succeed"),
|
||||
description,
|
||||
});
|
||||
|
||||
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) {
|
||||
const err = e as ClientResponseError;
|
||||
|
||||
|
|
@ -174,7 +201,22 @@ const Edit = () => {
|
|||
<div className="">
|
||||
<Toaster />
|
||||
<div className=" h-5 text-muted-foreground">
|
||||
{domain?.id ? t("domain.edit") : t("domain.add")}
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#/domains">
|
||||
{t("domain.management.name")}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>
|
||||
{domain?.id ? t("domain.edit") : t("domain.add")}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div 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">
|
||||
|
|
@ -425,7 +467,12 @@ const Edit = () => {
|
|||
tab == "apply" && "hidden"
|
||||
)}
|
||||
>
|
||||
<DeployList deploys={domain?.deployConfig ?? []} />
|
||||
<DeployList
|
||||
deploys={domain?.deployConfig ?? []}
|
||||
onChange={(list: DeployConfig[]) => {
|
||||
handelOnDeployListChange(list);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@ const Home = () => {
|
|||
const handelCheckedChange = async (id: string) => {
|
||||
const checkedDomains = domains.filter((domain) => domain.id === id);
|
||||
const isChecked = checkedDomains[0].enabled;
|
||||
|
||||
|
||||
const data = checkedDomains[0];
|
||||
data.enabled = !isChecked;
|
||||
|
|
@ -114,8 +113,8 @@ const Home = () => {
|
|||
|
||||
const handleRightNowClick = async (domain: Domain) => {
|
||||
try {
|
||||
unsubscribeId(domain.id);
|
||||
subscribeId(domain.id, (resp) => {
|
||||
unsubscribeId(domain.id ?? "");
|
||||
subscribeId(domain.id ?? "", (resp) => {
|
||||
console.log(resp);
|
||||
const updatedDomains = domains.map((domain) => {
|
||||
if (domain.id === resp.id) {
|
||||
|
|
@ -283,7 +282,7 @@ const Home = () => {
|
|||
<Switch
|
||||
checked={domain.enabled}
|
||||
onCheckedChange={() => {
|
||||
handelCheckedChange(domain.id);
|
||||
handelCheckedChange(domain.id ?? "");
|
||||
}}
|
||||
></Switch>
|
||||
</TooltipTrigger>
|
||||
|
|
@ -299,7 +298,7 @@ const Home = () => {
|
|||
<Button
|
||||
variant={"link"}
|
||||
className="p-0"
|
||||
onClick={() => handleHistoryClick(domain.id)}
|
||||
onClick={() => handleHistoryClick(domain.id ?? "")}
|
||||
>
|
||||
{t("deployment.log.name")}
|
||||
</Button>
|
||||
|
|
@ -364,7 +363,7 @@ const Home = () => {
|
|||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
handleDeleteClick(domain.id);
|
||||
handleDeleteClick(domain.id ?? "");
|
||||
}}
|
||||
>
|
||||
{t("confirm")}
|
||||
|
|
@ -377,7 +376,7 @@ const Home = () => {
|
|||
<Button
|
||||
variant={"link"}
|
||||
className="p-0"
|
||||
onClick={() => handleEditClick(domain.id)}
|
||||
onClick={() => handleEditClick(domain.id ?? "")}
|
||||
>
|
||||
{t("edit")}
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue