mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 09:21:56 +08:00
v0.2
This commit is contained in:
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)
|
||||
|
20
internal/domain/domains.go
Normal file
20
internal/domain/domains.go
Normal file
@ -0,0 +1,20 @@
|
||||
package domain
|
||||
|
||||
type ApplyConfig struct {
|
||||
Email string `json:"email"`
|
||||
Access string `json:"access"`
|
||||
Timeout int64 `json:"timeout"`
|
||||
Nameservers string `json:"nameservers"`
|
||||
}
|
||||
|
||||
type DeployConfig struct {
|
||||
Id string `json:"id"`
|
||||
Access string `json:"access"`
|
||||
Type string `json:"type"`
|
||||
Config map[string]any `json:"config"`
|
||||
}
|
||||
|
||||
type KV struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
@ -5,7 +5,6 @@ import (
|
||||
"certimate/internal/deployer"
|
||||
"certimate/internal/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 {
|
||||
|
||||
|
731
migrations/1728778480_collections_snapshot.go
Normal file
731
migrations/1728778480_collections_snapshot.go
Normal file
@ -0,0 +1,731 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
jsonData := `[
|
||||
{
|
||||
"id": "z3p974ainxjqlvs",
|
||||
"created": "2024-07-29 10:02:48.334Z",
|
||||
"updated": "2024-10-08 06:50:56.637Z",
|
||||
"name": "domains",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "iuaerpl2",
|
||||
"name": "domain",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "ukkhuw85",
|
||||
"name": "email",
|
||||
"type": "email",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"exceptDomains": null,
|
||||
"onlyDomains": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "v98eebqq",
|
||||
"name": "crontab",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "alc8e9ow",
|
||||
"name": "access",
|
||||
"type": "relation",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "4yzbv8urny5ja1e",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "topsc9bj",
|
||||
"name": "certUrl",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "vixgq072",
|
||||
"name": "certStableUrl",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "g3a3sza5",
|
||||
"name": "privateKey",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "gr6iouny",
|
||||
"name": "certificate",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "tk6vnrmn",
|
||||
"name": "issuerCertificate",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "sjo6ibse",
|
||||
"name": "csr",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "x03n1bkj",
|
||||
"name": "expiredAt",
|
||||
"type": "date",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "srybpixz",
|
||||
"name": "targetType",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"aliyun-oss",
|
||||
"aliyun-cdn",
|
||||
"aliyun-dcdn",
|
||||
"ssh",
|
||||
"webhook",
|
||||
"tencent-cdn",
|
||||
"qiniu-cdn",
|
||||
"local"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "xy7yk0mb",
|
||||
"name": "targetAccess",
|
||||
"type": "relation",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "4yzbv8urny5ja1e",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "6jqeyggw",
|
||||
"name": "enabled",
|
||||
"type": "bool",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "hdsjcchf",
|
||||
"name": "deployed",
|
||||
"type": "bool",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "aiya3rev",
|
||||
"name": "rightnow",
|
||||
"type": "bool",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "ixznmhzc",
|
||||
"name": "lastDeployedAt",
|
||||
"type": "date",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "ghtlkn5j",
|
||||
"name": "lastDeployment",
|
||||
"type": "relation",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "0a1o4e6sstp694f",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "zfnyj9he",
|
||||
"name": "variables",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "1bspzuku",
|
||||
"name": "group",
|
||||
"type": "relation",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "teolp9pl72dxlxq",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "g65gfh7a",
|
||||
"name": "nameservers",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "wwrzc3jo",
|
||||
"name": "applyConfig",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 2000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "474iwy8r",
|
||||
"name": "deployConfig",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 2000000
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX ` + "`" + `idx_4ABO6EQ` + "`" + ` ON ` + "`" + `domains` + "`" + ` (` + "`" + `domain` + "`" + `)"
|
||||
],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "4yzbv8urny5ja1e",
|
||||
"created": "2024-07-29 10:04:39.685Z",
|
||||
"updated": "2024-10-11 13:55:13.777Z",
|
||||
"name": "access",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "geeur58v",
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "iql7jpwx",
|
||||
"name": "config",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 2000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "hwy7m03o",
|
||||
"name": "configType",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"aliyun",
|
||||
"tencent",
|
||||
"huaweicloud",
|
||||
"qiniu",
|
||||
"cloudflare",
|
||||
"namesilo",
|
||||
"godaddy",
|
||||
"local",
|
||||
"ssh",
|
||||
"webhook"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "lr33hiwg",
|
||||
"name": "deleted",
|
||||
"type": "date",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "hsxcnlvd",
|
||||
"name": "usage",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"apply",
|
||||
"deploy",
|
||||
"all"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "c8egzzwj",
|
||||
"name": "group",
|
||||
"type": "relation",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "teolp9pl72dxlxq",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX ` + "`" + `idx_wkoST0j` + "`" + ` ON ` + "`" + `access` + "`" + ` (` + "`" + `name` + "`" + `)"
|
||||
],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "0a1o4e6sstp694f",
|
||||
"created": "2024-07-30 06:30:27.801Z",
|
||||
"updated": "2024-09-26 12:29:38.334Z",
|
||||
"name": "deployments",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "farvlzk7",
|
||||
"name": "domain",
|
||||
"type": "relation",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "z3p974ainxjqlvs",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "jx5f69i3",
|
||||
"name": "log",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 2000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "qbxdtg9q",
|
||||
"name": "phase",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"check",
|
||||
"apply",
|
||||
"deploy"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "rglrp1hz",
|
||||
"name": "phaseSuccess",
|
||||
"type": "bool",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "lt1g1blu",
|
||||
"name": "deployedAt",
|
||||
"type": "date",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "wledpzgb",
|
||||
"name": "wholeSuccess",
|
||||
"type": "bool",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "_pb_users_auth_",
|
||||
"created": "2024-09-12 13:09:54.234Z",
|
||||
"updated": "2024-09-26 12:29:38.334Z",
|
||||
"name": "users",
|
||||
"type": "auth",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "users_name",
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "users_avatar",
|
||||
"name": "avatar",
|
||||
"type": "file",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"mimeTypes": [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/svg+xml",
|
||||
"image/gif",
|
||||
"image/webp"
|
||||
],
|
||||
"thumbs": null,
|
||||
"maxSelect": 1,
|
||||
"maxSize": 5242880,
|
||||
"protected": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": "id = @request.auth.id",
|
||||
"viewRule": "id = @request.auth.id",
|
||||
"createRule": "",
|
||||
"updateRule": "id = @request.auth.id",
|
||||
"deleteRule": "id = @request.auth.id",
|
||||
"options": {
|
||||
"allowEmailAuth": true,
|
||||
"allowOAuth2Auth": true,
|
||||
"allowUsernameAuth": true,
|
||||
"exceptEmailDomains": null,
|
||||
"manageRule": null,
|
||||
"minPasswordLength": 8,
|
||||
"onlyEmailDomains": null,
|
||||
"onlyVerified": false,
|
||||
"requireEmail": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "dy6ccjb60spfy6p",
|
||||
"created": "2024-09-12 23:12:21.677Z",
|
||||
"updated": "2024-09-26 12:29:38.334Z",
|
||||
"name": "settings",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "1tcmdsdf",
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "f9wyhypi",
|
||||
"name": "content",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 2000000
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX ` + "`" + `idx_RO7X9Vw` + "`" + ` ON ` + "`" + `settings` + "`" + ` (` + "`" + `name` + "`" + `)"
|
||||
],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "teolp9pl72dxlxq",
|
||||
"created": "2024-09-13 12:51:05.611Z",
|
||||
"updated": "2024-09-26 12:29:38.334Z",
|
||||
"name": "access_groups",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "7sajiv6i",
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "xp8admif",
|
||||
"name": "access",
|
||||
"type": "relation",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "4yzbv8urny5ja1e",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": null,
|
||||
"displayFields": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX ` + "`" + `idx_RgRXp0R` + "`" + ` ON ` + "`" + `access_groups` + "`" + ` (` + "`" + `name` + "`" + `)"
|
||||
],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
}
|
||||
]`
|
||||
|
||||
collections := []*models.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return daos.New(db).ImportCollections(collections, true, nil)
|
||||
}, func(db dbx.Builder) error {
|
||||
return nil
|
||||
})
|
||||
}
|
1
ui/dist/assets/index-CWUb5Xuf.css
vendored
Normal file
1
ui/dist/assets/index-CWUb5Xuf.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ui/dist/assets/index-DOft-CKV.css
vendored
1
ui/dist/assets/index-DOft-CKV.css
vendored
File diff suppressed because one or more lines are too long
332
ui/dist/assets/index-DbwFzZm1.js
vendored
Normal file
332
ui/dist/assets/index-DbwFzZm1.js
vendored
Normal file
File diff suppressed because one or more lines are too long
332
ui/dist/assets/index-DpHAV802.js
vendored
332
ui/dist/assets/index-DpHAV802.js
vendored
File diff suppressed because one or more lines are too long
4
ui/dist/index.html
vendored
4
ui/dist/index.html
vendored
@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<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>
|
||||
|
28
ui/package-lock.json
generated
28
ui/package-lock.json
generated
@ -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 });
|
||||
|
115
ui/src/components/ui/breadcrumb.tsx
Normal file
115
ui/src/components/ui/breadcrumb.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Breadcrumb = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<"nav"> & {
|
||||
separator?: React.ReactNode
|
||||
}
|
||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
||||
Breadcrumb.displayName = "Breadcrumb"
|
||||
|
||||
const BreadcrumbList = React.forwardRef<
|
||||
HTMLOListElement,
|
||||
React.ComponentPropsWithoutRef<"ol">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbList.displayName = "BreadcrumbList"
|
||||
|
||||
const BreadcrumbItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentPropsWithoutRef<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbItem.displayName = "BreadcrumbItem"
|
||||
|
||||
const BreadcrumbLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<"a"> & {
|
||||
asChild?: boolean
|
||||
}
|
||||
>(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn("transition-colors hover:text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
BreadcrumbLink.displayName = "BreadcrumbLink"
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.ComponentPropsWithoutRef<"span">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("font-normal text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage"
|
||||
|
||||
const BreadcrumbSeparator = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) => (
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
||||
|
||||
const BreadcrumbEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
@ -91,23 +91,15 @@ export type GodaddyConfig = {
|
||||
apiSecret: string;
|
||||
};
|
||||
|
||||
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>
|
||||
|
@ -97,7 +97,6 @@ const Home = () => {
|
||||
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…
x
Reference in New Issue
Block a user