mirror of
https://github.com/certimate-go/certimate.git
synced 2026-06-22 21:05:48 +08:00
feat: reusing certificates key
This commit is contained in:
parent
17f3c1b63f
commit
186a18db6d
@ -62,7 +62,7 @@ func internalCertApplyCommand(app core.App) *cobra.Command {
|
||||
|
||||
client, err := certapply.NewACMEClientWithAccount(params.Account, func(c *lego.Config) error {
|
||||
c.UserAgent = "certimate"
|
||||
c.Certificate.KeyType = params.Request.KeyType
|
||||
c.Certificate.KeyType = params.Request.PrivateKeyType
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@ -22,9 +23,10 @@ import (
|
||||
)
|
||||
|
||||
type ObtainCertificateRequest struct {
|
||||
Domains []string
|
||||
KeyType certcrypto.KeyType
|
||||
ValidityTo time.Time
|
||||
Domains []string
|
||||
PrivateKeyType certcrypto.KeyType
|
||||
PrivateKeyPEM string
|
||||
ValidityTo time.Time
|
||||
|
||||
// 提供商相关
|
||||
ChallengeType string
|
||||
@ -150,8 +152,19 @@ func (c *ACMEClient) sendObtainCertificateRequest(request *ObtainCertificateRequ
|
||||
return nil, fmt.Errorf("unsupported challenge type: '%s'", request.ChallengeType)
|
||||
}
|
||||
|
||||
var privkey crypto.PrivateKey
|
||||
if request.PrivateKeyPEM != "" {
|
||||
pk, err := certcrypto.ParsePEMPrivateKey([]byte(request.PrivateKeyPEM))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
||||
}
|
||||
|
||||
privkey = pk
|
||||
}
|
||||
|
||||
req := certificate.ObtainRequest{
|
||||
Domains: request.Domains,
|
||||
PrivateKey: privkey,
|
||||
Bundle: true,
|
||||
Profile: request.ACMEProfile,
|
||||
NotAfter: request.ValidityTo,
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
@ -16,6 +17,7 @@ import (
|
||||
"github.com/certimate-go/certimate/internal/domain"
|
||||
"github.com/certimate-go/certimate/internal/domain/dtos"
|
||||
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
|
||||
xcryptokey "github.com/certimate-go/certimate/pkg/utils/crypto/key"
|
||||
)
|
||||
|
||||
type CertificateService struct {
|
||||
@ -230,13 +232,25 @@ func (s *CertificateService) ValidateCertificate(ctx context.Context, req *dtos.
|
||||
}
|
||||
|
||||
func (s *CertificateService) ValidatePrivateKey(ctx context.Context, req *dtos.CertificateValidatePrivateKeyReq) (*dtos.CertificateValidatePrivateKeyResp, error) {
|
||||
_, err := xcert.ParsePrivateKeyFromPEM(req.PrivateKey)
|
||||
privkey, err := xcert.ParsePrivateKeyFromPEM(req.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var keyAlgorithmString string
|
||||
keyAlgorithm, keySize, _ := xcryptokey.GetPrivateKeyAlgorithm(privkey)
|
||||
switch keyAlgorithm {
|
||||
case x509.RSA:
|
||||
keyAlgorithmString = fmt.Sprintf("RSA%d", keySize)
|
||||
case x509.ECDSA:
|
||||
keyAlgorithmString = fmt.Sprintf("EC%d", keySize)
|
||||
case x509.Ed25519:
|
||||
keyAlgorithmString = "ED25519"
|
||||
}
|
||||
|
||||
return &dtos.CertificateValidatePrivateKeyResp{
|
||||
IsValid: true,
|
||||
IsValid: keyAlgorithmString != "",
|
||||
KeyAlgorithm: keyAlgorithmString,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"strings"
|
||||
@ -11,6 +9,7 @@ import (
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
|
||||
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
|
||||
xcryptokey "github.com/certimate-go/certimate/pkg/utils/crypto/key"
|
||||
)
|
||||
|
||||
const CollectionNameCertificate = "certificate"
|
||||
@ -45,58 +44,14 @@ func (c *Certificate) PopulateFromX509(certX509 *x509.Certificate) *Certificate
|
||||
c.ValidityNotBefore = certX509.NotBefore
|
||||
c.ValidityNotAfter = certX509.NotAfter
|
||||
|
||||
switch certX509.PublicKeyAlgorithm {
|
||||
keyAlgorithm, keySize, _ := xcryptokey.GetPublicKeyAlgorithm(certX509.PublicKey)
|
||||
switch keyAlgorithm {
|
||||
case x509.RSA:
|
||||
{
|
||||
len := 0
|
||||
if pubkey, ok := certX509.PublicKey.(*rsa.PublicKey); ok {
|
||||
len = pubkey.N.BitLen()
|
||||
}
|
||||
|
||||
switch len {
|
||||
case 0:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType("RSA")
|
||||
case 2048:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA2048
|
||||
case 3072:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA3072
|
||||
case 4096:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA4096
|
||||
case 8192:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA8192
|
||||
default:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf("RSA%d", len))
|
||||
}
|
||||
}
|
||||
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf("RSA%d", keySize))
|
||||
case x509.ECDSA:
|
||||
{
|
||||
len := 0
|
||||
if pubkey, ok := certX509.PublicKey.(*ecdsa.PublicKey); ok {
|
||||
if pubkey.Curve != nil && pubkey.Curve.Params() != nil {
|
||||
len = pubkey.Curve.Params().BitSize
|
||||
}
|
||||
}
|
||||
|
||||
switch len {
|
||||
case 0:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType("EC")
|
||||
case 256:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC256
|
||||
case 384:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC384
|
||||
case 521:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC512
|
||||
default:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf("EC%d", len))
|
||||
}
|
||||
}
|
||||
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf("EC%d", keySize))
|
||||
case x509.Ed25519:
|
||||
{
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType("ED25519")
|
||||
}
|
||||
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType("ED25519")
|
||||
default:
|
||||
c.KeyAlgorithm = CertificateKeyAlgorithmType("")
|
||||
}
|
||||
@ -146,7 +101,6 @@ func (t CertificateKeyAlgorithmType) KeyType() (certcrypto.KeyType, error) {
|
||||
CertificateKeyAlgorithmTypeRSA8192: certcrypto.RSA8192,
|
||||
CertificateKeyAlgorithmTypeEC256: certcrypto.EC256,
|
||||
CertificateKeyAlgorithmTypeEC384: certcrypto.EC384,
|
||||
CertificateKeyAlgorithmTypeEC512: certcrypto.KeyType("P512"),
|
||||
}
|
||||
|
||||
if keyType, ok := keyTypeMap[t]; ok {
|
||||
|
||||
@ -30,5 +30,6 @@ type CertificateValidatePrivateKeyReq struct {
|
||||
}
|
||||
|
||||
type CertificateValidatePrivateKeyResp struct {
|
||||
IsValid bool `json:"isValid"`
|
||||
IsValid bool `json:"isValid"`
|
||||
KeyAlgorithm string `json:"keyAlgorithm,omitempty"`
|
||||
}
|
||||
|
||||
@ -146,7 +146,9 @@ func (c WorkflowNodeConfig) AsBizApply() WorkflowNodeConfigForBizApply {
|
||||
Provider: xmaps.GetString(c, "provider"),
|
||||
ProviderAccessId: xmaps.GetString(c, "providerAccessId"),
|
||||
ProviderConfig: xmaps.GetKVMapAny(c, "providerConfig"),
|
||||
KeySource: xmaps.GetOrDefaultString(c, "keySource", "auto"),
|
||||
KeyAlgorithm: xmaps.GetOrDefaultString(c, "keyAlgorithm", string(CertificateKeyAlgorithmTypeRSA2048)),
|
||||
KeyContent: xmaps.GetString(c, "keyContent"),
|
||||
CAProvider: xmaps.GetString(c, "caProvider"),
|
||||
CAProviderAccessId: xmaps.GetString(c, "caProviderAccessId"),
|
||||
CAProviderConfig: xmaps.GetKVMapAny(c, "caProviderConfig"),
|
||||
@ -220,8 +222,10 @@ type WorkflowNodeConfigForBizApply struct {
|
||||
CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值时使用全局配置)
|
||||
CAProviderAccessId string `json:"caProviderAccessId,omitempty"` // CA 提供商授权记录 ID
|
||||
CAProviderConfig map[string]any `json:"caProviderConfig,omitempty"` // CA 提供商额外配置
|
||||
KeyAlgorithm string `json:"keyAlgorithm,omitempty"` // 证书算法
|
||||
ValidityLifetime string `json:"validityLifetime,omitempty"` // 证书有效期,形如 "30d"、"6h"
|
||||
KeySource string `json:"keySource"` // 私钥来源,可取值 "auto"、"reuse"、"custom"(零值时默认值 "auto")
|
||||
KeyAlgorithm string `json:"keyAlgorithm,omitempty"` // 私钥算法
|
||||
KeyContent string `json:"keyContent,omitempty"` // 私钥内容
|
||||
ValidityLifetime string `json:"validityLifetime,omitempty"` // 有效期,形如 "30d"、"6h"
|
||||
ACMEProfile string `json:"acmeProfile,omitempty"` // ACME Profiles Extension
|
||||
Nameservers []string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔
|
||||
DnsPropagationWait int32 `json:"dnsPropagationWait,omitempty"` // DNS 传播等待时间,等同于 lego 的 `--dns-propagation-wait` 参数
|
||||
@ -234,7 +238,7 @@ type WorkflowNodeConfigForBizApply struct {
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForBizUpload struct {
|
||||
Source string `json:"source"` // 证书来源(零值时默认值 "form")
|
||||
Source string `json:"source"` // 证书来源,可取值 "form"、"local"、"url"(零值时默认值 "form")
|
||||
Certificate string `json:"certificate"` // 证书,根据证书来源决定是 PEM 内容 / 文件路径 / URL
|
||||
PrivateKey string `json:"privateKey"` // 私钥,根据证书来源决定是 PEM 内容 / 文件路径 / URL
|
||||
}
|
||||
|
||||
@ -33,6 +33,12 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
BizApplyKeySourceAuto = "auto"
|
||||
BizApplyKeySourceReuse = "reuse"
|
||||
BizApplyKeySourceCustom = "custom"
|
||||
)
|
||||
|
||||
/**
|
||||
* Outputs:
|
||||
* - ref: "certificate": string
|
||||
@ -203,10 +209,15 @@ func (ne *bizApplyNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext, last
|
||||
}
|
||||
|
||||
func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nodeCfg *domain.WorkflowNodeConfigForBizApply, lastCertificate *domain.Certificate) (*certapply.ObtainCertificateResponse, error) {
|
||||
// 读取证书算法
|
||||
// 读取私钥算法
|
||||
// 如果复用私钥,则保持算法一致
|
||||
legoKeyType, err := domain.CertificateKeyAlgorithmType(nodeCfg.KeyAlgorithm).KeyType()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
if nodeCfg.KeySource == BizApplyKeySourceReuse && lastCertificate != nil {
|
||||
legoKeyType, _ = lastCertificate.KeyAlgorithm.KeyType()
|
||||
}
|
||||
}
|
||||
|
||||
// 读取质询提供商授权
|
||||
@ -256,8 +267,30 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod
|
||||
|
||||
// 构造证书申请请求
|
||||
obtainReq := &certapply.ObtainCertificateRequest{
|
||||
Domains: nodeCfg.Domains,
|
||||
KeyType: legoKeyType,
|
||||
Domains: nodeCfg.Domains,
|
||||
PrivateKeyType: legoKeyType,
|
||||
PrivateKeyPEM: lo.
|
||||
If(nodeCfg.KeySource == BizApplyKeySourceAuto, "").
|
||||
ElseF(func() string {
|
||||
switch nodeCfg.KeySource {
|
||||
case BizApplyKeySourceReuse:
|
||||
if lastCertificate != nil {
|
||||
return lastCertificate.PrivateKey
|
||||
}
|
||||
case BizApplyKeySourceCustom:
|
||||
return nodeCfg.KeyContent
|
||||
}
|
||||
return ""
|
||||
}),
|
||||
ValidityTo: lo.
|
||||
If(nodeCfg.ValidityLifetime == "", time.Time{}).
|
||||
ElseF(func() time.Time {
|
||||
duration, err := str2duration.ParseDuration(nodeCfg.ValidityLifetime)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Now().Add(duration)
|
||||
}),
|
||||
ChallengeType: nodeCfg.ChallengeType,
|
||||
Provider: nodeCfg.Provider,
|
||||
ProviderAccessConfig: providerAccessConfig,
|
||||
@ -268,24 +301,17 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod
|
||||
DnsPropagationTimeout: nodeCfg.DnsPropagationTimeout,
|
||||
DnsTTL: nodeCfg.DnsTTL,
|
||||
HttpDelayWait: nodeCfg.HttpDelayWait,
|
||||
ValidityTo: lo.If(nodeCfg.ValidityLifetime == "", time.Time{}).
|
||||
ElseF(func() time.Time {
|
||||
duration, err := str2duration.ParseDuration(nodeCfg.ValidityLifetime)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Now().Add(duration)
|
||||
}),
|
||||
ACMEProfile: nodeCfg.ACMEProfile,
|
||||
ARIReplacesAcctUrl: lo.If(lastCertificate == nil, "").
|
||||
ACMEProfile: nodeCfg.ACMEProfile,
|
||||
ARIReplacesAcctUrl: lo.
|
||||
If(lastCertificate == nil, "").
|
||||
ElseF(func() string {
|
||||
if lastCertificate.IsRenewed {
|
||||
return ""
|
||||
}
|
||||
|
||||
return lastCertificate.ACMEAcctUrl
|
||||
}),
|
||||
ARIReplacesCertId: lo.If(lastCertificate == nil, "").
|
||||
ARIReplacesCertId: lo.
|
||||
If(lastCertificate == nil, "").
|
||||
ElseF(func() string {
|
||||
if lastCertificate.IsRenewed {
|
||||
return ""
|
||||
|
||||
@ -11,19 +11,8 @@ func init() {
|
||||
tracer := NewTracer("v0.4.1")
|
||||
tracer.Printf("go ...")
|
||||
|
||||
// update collection `workflow`
|
||||
// - fix #982
|
||||
// adapt to new workflow data structure
|
||||
{
|
||||
collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
records, err := app.FindAllRecords(collection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type dWorkflowNode struct {
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
@ -71,48 +60,62 @@ func init() {
|
||||
return nodes, migrated
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
changed := false
|
||||
// update collection `workflow`
|
||||
// - fix #982
|
||||
{
|
||||
collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
graphDraft := make(map[string]any)
|
||||
if err := record.UnmarshalJSONField("graphDraft", &graphDraft); err == nil {
|
||||
if _, ok := graphDraft["nodes"]; ok {
|
||||
nodes := make([]*dWorkflowNode, 0)
|
||||
if err := mapstructure.Decode(graphDraft["nodes"], &nodes); err != nil {
|
||||
records, err := app.FindAllRecords(collection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
changed := false
|
||||
|
||||
graphDraft := make(map[string]any)
|
||||
if err := record.UnmarshalJSONField("graphDraft", &graphDraft); err == nil {
|
||||
if _, ok := graphDraft["nodes"]; ok {
|
||||
nodes := make([]*dWorkflowNode, 0)
|
||||
if err := mapstructure.Decode(graphDraft["nodes"], &nodes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newNodes, migrated := deepMigrateNodes(nodes); migrated {
|
||||
graphDraft["nodes"] = newNodes
|
||||
record.Set("graphDraft", graphDraft)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
graphContent := make(map[string]any)
|
||||
if err := record.UnmarshalJSONField("graphContent", &graphContent); err == nil {
|
||||
if _, ok := graphContent["nodes"]; ok {
|
||||
nodes := make([]*dWorkflowNode, 0)
|
||||
if err := mapstructure.Decode(graphContent["nodes"], &nodes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newNodes, migrated := deepMigrateNodes(nodes); migrated {
|
||||
graphContent["nodes"] = newNodes
|
||||
record.Set("graphContent", graphContent)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
if err := app.Save(record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newNodes, migrated := deepMigrateNodes(nodes); migrated {
|
||||
graphDraft["nodes"] = newNodes
|
||||
record.Set("graphDraft", graphDraft)
|
||||
changed = true
|
||||
}
|
||||
tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name)
|
||||
}
|
||||
}
|
||||
|
||||
graphContent := make(map[string]any)
|
||||
if err := record.UnmarshalJSONField("graphContent", &graphContent); err == nil {
|
||||
if _, ok := graphContent["nodes"]; ok {
|
||||
nodes := make([]*dWorkflowNode, 0)
|
||||
if err := mapstructure.Decode(graphContent["nodes"], &nodes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newNodes, migrated := deepMigrateNodes(nodes); migrated {
|
||||
graphContent["nodes"] = newNodes
|
||||
record.Set("graphContent", graphContent)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
if err := app.Save(record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
tracer := NewTracer("v0.4.3")
|
||||
tracer.Printf("go ...")
|
||||
|
||||
// update collection `certificate`
|
||||
// - rename field `acmeRenewed` to `isRenewed`
|
||||
// - add field `isRevoked`
|
||||
{
|
||||
collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := collection.Fields.AddMarshaledJSONAt(14, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "bool810050391",
|
||||
"name": "isRenewed",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "bool"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := collection.Fields.AddMarshaledJSONAt(15, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "bool3680845581",
|
||||
"name": "isRevoked",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "bool"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := app.Save(collection); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
tracer.Printf("done")
|
||||
return nil
|
||||
}, func(app core.App) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
218
migrations/1762142400_m0.4.3.go
Normal file
218
migrations/1762142400_m0.4.3.go
Normal file
@ -0,0 +1,218 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
tracer := NewTracer("v0.4.3")
|
||||
tracer.Printf("go ...")
|
||||
|
||||
// update collection `certificate`
|
||||
// - rename field `acmeRenewed` to `isRenewed`
|
||||
// - add field `isRevoked`
|
||||
{
|
||||
collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := collection.Fields.AddMarshaledJSONAt(14, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "bool810050391",
|
||||
"name": "isRenewed",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "bool"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := collection.Fields.AddMarshaledJSONAt(15, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "bool3680845581",
|
||||
"name": "isRevoked",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "bool"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := app.Save(collection); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// adapt to new workflow data structure
|
||||
{
|
||||
type dWorkflowNode struct {
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Data map[string]any `json:"data"`
|
||||
Blocks []*dWorkflowNode `json:"blocks,omitempty,omitzero"`
|
||||
}
|
||||
|
||||
var deepMigrateNode func(node *dWorkflowNode) (_node *dWorkflowNode, _migrated bool)
|
||||
var deepMigrateNodes func(nodes []*dWorkflowNode) (_nodes []*dWorkflowNode, _migrated bool)
|
||||
deepMigrateNode = func(node *dWorkflowNode) (*dWorkflowNode, bool) {
|
||||
migrated := false
|
||||
|
||||
if node.Type == "bizApply" {
|
||||
if node.Data != nil {
|
||||
if _, ok := node.Data["config"]; ok {
|
||||
nodeCfg := node.Data["config"].(map[string]any)
|
||||
if nodeCfg["keySource"] == nil || nodeCfg["keySource"] == "" {
|
||||
nodeCfg["keySource"] = "auto"
|
||||
node.Data["config"] = nodeCfg
|
||||
migrated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if node.Type == "bizUpload" {
|
||||
if node.Data != nil {
|
||||
if _, ok := node.Data["config"]; ok {
|
||||
nodeCfg := node.Data["config"].(map[string]any)
|
||||
if nodeCfg["source"] == nil || nodeCfg["source"] == "" {
|
||||
nodeCfg["source"] = "form"
|
||||
node.Data["config"] = nodeCfg
|
||||
migrated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(node.Blocks) > 0 {
|
||||
if newBlocks, changed := deepMigrateNodes(node.Blocks); changed {
|
||||
node.Blocks = newBlocks
|
||||
migrated = true
|
||||
}
|
||||
}
|
||||
|
||||
return node, migrated
|
||||
}
|
||||
deepMigrateNodes = func(nodes []*dWorkflowNode) ([]*dWorkflowNode, bool) {
|
||||
migrated := false
|
||||
|
||||
for i, node := range nodes {
|
||||
if newNode, changed := deepMigrateNode(node); changed {
|
||||
nodes[i] = newNode
|
||||
migrated = true
|
||||
}
|
||||
}
|
||||
|
||||
return nodes, migrated
|
||||
}
|
||||
|
||||
// update collection `workflow`
|
||||
// - migrate field `graphDraft` / `graphContent`
|
||||
{
|
||||
collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
records, err := app.FindAllRecords(collection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
changed := false
|
||||
|
||||
graphDraft := make(map[string]any)
|
||||
if err := record.UnmarshalJSONField("graphDraft", &graphDraft); err == nil {
|
||||
if _, ok := graphDraft["nodes"]; ok {
|
||||
nodes := make([]*dWorkflowNode, 0)
|
||||
if err := mapstructure.Decode(graphDraft["nodes"], &nodes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newNodes, migrated := deepMigrateNodes(nodes); migrated {
|
||||
graphDraft["nodes"] = newNodes
|
||||
record.Set("graphDraft", graphDraft)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
graphContent := make(map[string]any)
|
||||
if err := record.UnmarshalJSONField("graphContent", &graphContent); err == nil {
|
||||
if _, ok := graphContent["nodes"]; ok {
|
||||
nodes := make([]*dWorkflowNode, 0)
|
||||
if err := mapstructure.Decode(graphContent["nodes"], &nodes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newNodes, migrated := deepMigrateNodes(nodes); migrated {
|
||||
graphContent["nodes"] = newNodes
|
||||
record.Set("graphContent", graphContent)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
if err := app.Save(record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update collection `workflow_run`
|
||||
// - migrate field `graph`
|
||||
{
|
||||
collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
records, err := app.FindAllRecords(collection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
changed := false
|
||||
|
||||
graph := make(map[string]any)
|
||||
if err := record.UnmarshalJSONField("graph", &graph); err == nil {
|
||||
if _, ok := graph["nodes"]; ok {
|
||||
nodes := make([]*dWorkflowNode, 0)
|
||||
if err := mapstructure.Decode(graph["nodes"], &nodes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newNodes, migrated := deepMigrateNodes(nodes); migrated {
|
||||
graph["nodes"] = newNodes
|
||||
record.Set("graph", graph)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
if err := app.Save(record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracer.Printf("done")
|
||||
return nil
|
||||
}, func(app core.App) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@ -15,7 +15,7 @@ import (
|
||||
|
||||
"github.com/certimate-go/certimate/pkg/core"
|
||||
sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/tencentcloud-ssl"
|
||||
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
|
||||
xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname"
|
||||
)
|
||||
|
||||
type SSLDeployerProviderConfig struct {
|
||||
@ -196,7 +196,7 @@ func (d *SSLDeployerProvider) getMatchedDomainsByWildcard(ctx context.Context, w
|
||||
|
||||
if describeDomainsResp.Response.Domains != nil {
|
||||
for _, domain := range describeDomainsResp.Response.Domains {
|
||||
if lo.FromPtr(domain.Product) == "cdn" && xcert.MatchHostname(wildcardDomain, lo.FromPtr(domain.Domain)) {
|
||||
if lo.FromPtr(domain.Product) == "cdn" && xcerthostname.IsMatch(wildcardDomain, lo.FromPtr(domain.Domain)) {
|
||||
domains = append(domains, *domain.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ import (
|
||||
|
||||
"github.com/certimate-go/certimate/pkg/core"
|
||||
sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/tencentcloud-ssl"
|
||||
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
|
||||
xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname"
|
||||
)
|
||||
|
||||
type SSLDeployerProviderConfig struct {
|
||||
@ -196,7 +196,7 @@ func (d *SSLDeployerProvider) getMatchedDomainsByWildcard(ctx context.Context, w
|
||||
|
||||
if describeDomainsResp.Response.Domains != nil {
|
||||
for _, domain := range describeDomainsResp.Response.Domains {
|
||||
if lo.FromPtr(domain.Product) == "ecdn" && xcert.MatchHostname(wildcardDomain, lo.FromPtr(domain.Domain)) {
|
||||
if lo.FromPtr(domain.Product) == "ecdn" && xcerthostname.IsMatch(wildcardDomain, lo.FromPtr(domain.Domain)) {
|
||||
domains = append(domains, *domain.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ import (
|
||||
|
||||
"github.com/certimate-go/certimate/pkg/core"
|
||||
sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/tencentcloud-ssl"
|
||||
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
|
||||
xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname"
|
||||
)
|
||||
|
||||
type SSLDeployerProviderConfig struct {
|
||||
@ -121,7 +121,7 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke
|
||||
|
||||
domains = lo.Filter(domainsInZone, func(domain string, _ int) bool {
|
||||
for _, configDomain := range d.config.Domains {
|
||||
if xcert.MatchHostname(configDomain, domain) {
|
||||
if xcerthostname.IsMatch(configDomain, domain) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import (
|
||||
|
||||
"github.com/certimate-go/certimate/pkg/core"
|
||||
sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/volcengine-cdn"
|
||||
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
|
||||
xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname"
|
||||
)
|
||||
|
||||
type SSLDeployerProviderConfig struct {
|
||||
@ -180,7 +180,7 @@ func (d *SSLDeployerProvider) getMatchedDomainsByWildcard(ctx context.Context, w
|
||||
|
||||
if listCdnDomainsResp.Data != nil {
|
||||
for _, domain := range listCdnDomainsResp.Data {
|
||||
if xcert.MatchHostname(wildcardDomain, ve.StringValue(domain.Domain)) {
|
||||
if xcerthostname.IsMatch(wildcardDomain, ve.StringValue(domain.Domain)) {
|
||||
domains = append(domains, ve.StringValue(domain.Domain))
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/certimate-go/certimate/pkg/core"
|
||||
sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/volcengine-live"
|
||||
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
|
||||
xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname"
|
||||
)
|
||||
|
||||
type SSLDeployerProviderConfig struct {
|
||||
@ -167,7 +167,7 @@ func (d *SSLDeployerProvider) getMatchedDomainsByWildcard(ctx context.Context, w
|
||||
|
||||
if listDomainDetailResp.Result.DomainList != nil {
|
||||
for _, domain := range listDomainDetailResp.Result.DomainList {
|
||||
if xcert.MatchHostname(wildcardDomain, domain.Domain) {
|
||||
if xcerthostname.IsMatch(wildcardDomain, domain.Domain) {
|
||||
domains = append(domains, domain.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package cert
|
||||
package hostname
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
@ -14,7 +14,7 @@ import (
|
||||
//
|
||||
// 出参:
|
||||
// - 是否匹配。
|
||||
func MatchHostname(match, candidate string) bool {
|
||||
func IsMatch(match, candidate string) bool {
|
||||
if match == "" || candidate == "" {
|
||||
return false
|
||||
}
|
||||
@ -1,13 +1,13 @@
|
||||
package cert_test
|
||||
package hostname_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
|
||||
xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname"
|
||||
)
|
||||
|
||||
func TestCertUtil_Hostname(t *testing.T) {
|
||||
t.Run("MatchHostname", func(t *testing.T) {
|
||||
func TestCertHostnameUtil_IsMatch(t *testing.T) {
|
||||
t.Run("IsMatch", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
wildcard string
|
||||
target string
|
||||
@ -36,7 +36,7 @@ func TestCertUtil_Hostname(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := xcert.MatchHostname(tc.wildcard, tc.target)
|
||||
result := xcerthostname.IsMatch(tc.wildcard, tc.target)
|
||||
status := "✓"
|
||||
pf := t.Logf
|
||||
if result != tc.expected {
|
||||
46
pkg/utils/crypto/key/key.go
Normal file
46
pkg/utils/crypto/key/key.go
Normal file
@ -0,0 +1,46 @@
|
||||
package key
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type KeyAlgorithm = x509.PublicKeyAlgorithm
|
||||
|
||||
func GetPublicKeyAlgorithm(pubkey crypto.PublicKey) (_algorithm KeyAlgorithm, _size int, _error error) {
|
||||
switch t := pubkey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
size := t.N.BitLen()
|
||||
return x509.RSA, size, nil
|
||||
|
||||
case *ecdsa.PublicKey:
|
||||
size := t.Curve.Params().BitSize
|
||||
return x509.ECDSA, size, nil
|
||||
|
||||
case ed25519.PublicKey:
|
||||
return x509.Ed25519, 256, nil
|
||||
}
|
||||
|
||||
return x509.UnknownPublicKeyAlgorithm, 0, errors.New("unknown public key type")
|
||||
}
|
||||
|
||||
func GetPrivateKeyAlgorithm(privkey crypto.PrivateKey) (_algorithm KeyAlgorithm, _size int, _error error) {
|
||||
switch t := privkey.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
size := t.N.BitLen()
|
||||
return x509.RSA, size, nil
|
||||
|
||||
case *ecdsa.PrivateKey:
|
||||
size := t.Curve.Params().BitSize
|
||||
return x509.ECDSA, size, nil
|
||||
|
||||
case ed25519.PrivateKey:
|
||||
return x509.Ed25519, 256, nil
|
||||
}
|
||||
|
||||
return x509.UnknownPublicKeyAlgorithm, 0, errors.New("unknown private key type")
|
||||
}
|
||||
@ -3,14 +3,13 @@ import { ClientResponseError } from "pocketbase";
|
||||
import { type CertificateFormatType } from "@/domain/certificate";
|
||||
import { getPocketBase } from "@/repository/_pocketbase";
|
||||
|
||||
type ArchiveRespData = {
|
||||
fileBytes: string;
|
||||
};
|
||||
|
||||
export const archive = async (certificateId: string, format?: CertificateFormatType) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
const resp = await pb.send<BaseResponse<ArchiveRespData>>(`/api/certificates/${encodeURIComponent(certificateId)}/archive`, {
|
||||
type RespData = {
|
||||
fileBytes: string;
|
||||
};
|
||||
const resp = await pb.send<BaseResponse<RespData>>(`/api/certificates/${encodeURIComponent(certificateId)}/archive`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -30,7 +29,7 @@ export const archive = async (certificateId: string, format?: CertificateFormatT
|
||||
export const revoke = async (certificateId: string) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
const resp = await pb.send<BaseResponse<ArchiveRespData>>(`/api/certificates/${encodeURIComponent(certificateId)}/revoke`, {
|
||||
const resp = await pb.send<BaseResponse>(`/api/certificates/${encodeURIComponent(certificateId)}/revoke`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -44,14 +43,14 @@ export const revoke = async (certificateId: string) => {
|
||||
return resp;
|
||||
};
|
||||
|
||||
type ValidateCertificateResp = {
|
||||
isValid: boolean;
|
||||
domains: string;
|
||||
};
|
||||
|
||||
export const validateCertificate = async (certificate: string) => {
|
||||
const pb = getPocketBase();
|
||||
const resp = await pb.send<BaseResponse<ValidateCertificateResp>>(`/api/certificates/validate/certificate`, {
|
||||
|
||||
type RespData = {
|
||||
isValid: boolean;
|
||||
domains: string;
|
||||
};
|
||||
const resp = await pb.send<BaseResponse<RespData>>(`/api/certificates/validate/certificate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -68,13 +67,14 @@ export const validateCertificate = async (certificate: string) => {
|
||||
return resp;
|
||||
};
|
||||
|
||||
type ValidatePrivateKeyResp = {
|
||||
isValid: boolean;
|
||||
};
|
||||
|
||||
export const validatePrivateKey = async (privateKey: string) => {
|
||||
const pb = getPocketBase();
|
||||
const resp = await pb.send<BaseResponse<ValidatePrivateKeyResp>>(`/api/certificates/validate/private-key`, {
|
||||
|
||||
type RespData = {
|
||||
isValid: boolean;
|
||||
keyAlgorithm: string;
|
||||
};
|
||||
const resp = await pb.send<BaseResponse<RespData>>(`/api/certificates/validate/private-key`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@ -6,13 +6,12 @@ import { getPocketBase } from "@/repository/_pocketbase";
|
||||
export const getStats = async () => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
const resp = await pb.send<
|
||||
BaseResponse<{
|
||||
concurrency: number;
|
||||
pendingRunIds: string[];
|
||||
processingRunIds: string[];
|
||||
}>
|
||||
>(`/api/workflows/stats`, {
|
||||
type RespData = {
|
||||
concurrency: number;
|
||||
pendingRunIds: string[];
|
||||
processingRunIds: string[];
|
||||
};
|
||||
const resp = await pb.send<BaseResponse<RespData>>(`/api/workflows/stats`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { type FlowNodeEntity, type DragNodeProps as FlowgramDragNodeProps, getNodeForm } from "@flowgram.ai/fixed-layout-editor";
|
||||
import { type FlowNodeEntity, type DragNodeProps as FlowgramDragNodeProps } from "@flowgram.ai/fixed-layout-editor";
|
||||
import { Badge, Card } from "antd";
|
||||
|
||||
export interface DragNodeProps extends FlowgramDragNodeProps {
|
||||
@ -15,7 +15,7 @@ const DragNode = ({ dragStart, dragNodes }: DragNodeProps) => {
|
||||
<div className="relative w-[160px]">
|
||||
<Card className="bg-transparent shadow" styles={{ body: { padding: 0 } }}>
|
||||
<div className="overflow-hidden px-4 py-2 text-primary">
|
||||
<div className="truncate">{dragStart ? getNodeForm(dragStart)?.getValueIn("name") || `#${dragStart?.id}` : "\u00A0"}</div>
|
||||
<div className="truncate">{dragStart ? dragStart.form?.getValueIn("name") || `#${dragStart?.id}` : "\u00A0"}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { validatePrivateKey } from "@/api/certificates";
|
||||
import AccessEditDrawer from "@/components/access/AccessEditDrawer";
|
||||
import AccessSelect from "@/components/access/AccessSelect";
|
||||
import MultipleSplitValueInput from "@/components/MultipleSplitValueInput";
|
||||
@ -30,12 +31,14 @@ import ACMEDns01ProviderSelect from "@/components/provider/ACMEDns01ProviderSele
|
||||
import ACMEHttp01ProviderSelect from "@/components/provider/ACMEHttp01ProviderSelect";
|
||||
import CAProviderSelect from "@/components/provider/CAProviderSelect";
|
||||
import Show from "@/components/Show";
|
||||
import TextFileInput from "@/components/TextFileInput";
|
||||
import { type AccessModel } from "@/domain/access";
|
||||
import { ACME_DNS01_PROVIDERS, ACME_HTTP01_PROVIDERS, acmeDns01ProvidersMap, acmeHttp01ProvidersMap, caProvidersMap } from "@/domain/provider";
|
||||
import { type WorkflowNodeConfigForBizApply, defaultNodeConfigForBizApply } from "@/domain/workflow";
|
||||
import { useAntdForm, useZustandShallowSelector } from "@/hooks";
|
||||
import { useAccessesStore } from "@/stores/access";
|
||||
import { useContactEmailsStore } from "@/stores/contact";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
import { validDomainName, validIPv4Address, validIPv6Address } from "@/utils/validators";
|
||||
|
||||
import { FormNestedFieldsContextProvider, NodeFormContextProvider } from "./_context";
|
||||
@ -53,6 +56,10 @@ const MULTIPLE_INPUT_SEPARATOR = ";";
|
||||
const CHALLENGE_TYPE_DNS01 = "dns-01";
|
||||
const CHALLENGE_TYPE_HTTP01 = "http-01";
|
||||
|
||||
const KEY_SOURCE_AUTO = "auto" as const;
|
||||
const KEY_SOURCE_REUSE = "reuse" as const;
|
||||
const KEY_SOURCE_CUSTOM = "custom" as const;
|
||||
|
||||
export interface BizApplyNodeConfigFormProps {
|
||||
form: FormInstance;
|
||||
node: FlowNodeEntity;
|
||||
@ -92,9 +99,18 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps)
|
||||
const fieldChallengeType = Form.useWatch<string>("challengeType", { form: formInst, preserve: true });
|
||||
const fieldProvider = Form.useWatch<string>("provider", { form: formInst, preserve: true });
|
||||
const fieldProviderAccessId = Form.useWatch<string>("providerAccessId", { form: formInst, preserve: true });
|
||||
const fieldKeySource = Form.useWatch<string>("keySource", { form: formInst, preserve: true });
|
||||
const fieldCAProvider = Form.useWatch<string>("caProvider", { form: formInst, preserve: true });
|
||||
const fieldCAProviderAccessId = Form.useWatch<string>("caProviderAccessId", { form: formInst, preserve: true });
|
||||
|
||||
const resetFieldIfInvalid = (field: keyof z.infer<typeof formSchema>) => {
|
||||
const fieldSchame = formSchema.pick({ [field]: true });
|
||||
const fieldValue = formInst.getFieldValue(field);
|
||||
if (!fieldSchame.safeParse({ [field]: fieldValue }).success) {
|
||||
formInst.setFieldValue(field, void 0);
|
||||
}
|
||||
};
|
||||
|
||||
const NestedProviderConfigFields = useMemo(() => {
|
||||
/*
|
||||
注意:如果追加新的子组件,请保持以 ASCII 排序。
|
||||
@ -225,14 +241,6 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps)
|
||||
}, [fieldCAProvider, fieldCAProviderAccessId]);
|
||||
|
||||
const handleChallengeTypeChange = (value: string) => {
|
||||
const resetFieldIfInvalid = (field: keyof z.infer<typeof formSchema>) => {
|
||||
const fieldSchame = formSchema.pick({ [field]: true });
|
||||
const fieldValue = formInst.getFieldValue(field);
|
||||
if (!fieldSchame.safeParse({ [field]: fieldValue }).success) {
|
||||
formInst.setFieldValue(field, void 0);
|
||||
}
|
||||
};
|
||||
|
||||
switch (value) {
|
||||
case CHALLENGE_TYPE_DNS01:
|
||||
{
|
||||
@ -267,6 +275,40 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps)
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeySourceChange = (value: string) => {
|
||||
if (value === initialValues?.keySource) {
|
||||
formInst.resetFields(["keyContent"]);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
formInst.setFieldValue("keyContent", "");
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyContentChange = async (value: string) => {
|
||||
try {
|
||||
const resp = await validatePrivateKey(value);
|
||||
formInst.setFields([
|
||||
{
|
||||
name: "keyContent",
|
||||
value: value,
|
||||
},
|
||||
{
|
||||
name: "keyAlgorithm",
|
||||
value: resp.data.keyAlgorithm,
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
formInst.setFields([
|
||||
{
|
||||
name: "keyContent",
|
||||
value: value,
|
||||
errors: [getErrMsg(e)],
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCAProviderSelect = (value?: string | undefined) => {
|
||||
// 切换 CA 提供商时联动授权信息
|
||||
if (value == null || value === "") {
|
||||
@ -429,7 +471,28 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps)
|
||||
</Typography.Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item name="keyAlgorithm" label={t("workflow_node.apply.form.key_algorithm.label")} rules={[formRule]}>
|
||||
<Form.Item name="keySource" label={t("workflow_node.apply.form.key_source.label")} rules={[formRule]}>
|
||||
<Radio.Group block onChange={(e) => handleKeySourceChange(e.target.value)}>
|
||||
<Radio.Button value={KEY_SOURCE_AUTO}>{t("workflow_node.apply.form.key_source.option.auto.label")}</Radio.Button>
|
||||
<Radio.Button value={KEY_SOURCE_REUSE}>{t("workflow_node.apply.form.key_source.option.reuse.label")}</Radio.Button>
|
||||
<Radio.Button value={KEY_SOURCE_CUSTOM}>{t("workflow_node.apply.form.key_source.option.custom.label")}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="keyAlgorithm"
|
||||
label={t("workflow_node.apply.form.key_algorithm.label")}
|
||||
extra={
|
||||
fieldKeySource === KEY_SOURCE_REUSE ? (
|
||||
<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.key_algorithm.help_reuse") }}></span>
|
||||
) : fieldKeySource === KEY_SOURCE_CUSTOM ? (
|
||||
<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.key_algorithm.help_custom") }}></span>
|
||||
) : (
|
||||
void 0
|
||||
)
|
||||
}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Select
|
||||
options={["RSA2048", "RSA3072", "RSA4096", "RSA8192", "EC256", "EC384"].map((e) => ({
|
||||
label: e,
|
||||
@ -439,6 +502,16 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps)
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Show when={fieldKeySource === KEY_SOURCE_CUSTOM}>
|
||||
<Form.Item name="keyContent" label={t("workflow_node.apply.form.key_content.label")} rules={[formRule]}>
|
||||
<TextFileInput
|
||||
autoSize={{ minRows: 3, maxRows: 10 }}
|
||||
placeholder={t("workflow_node.apply.form.key_content.placeholder")}
|
||||
onChange={handleKeyContentChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Show>
|
||||
|
||||
<Form.Item className="relative" label={t("workflow_node.apply.form.ca_provider.label")}>
|
||||
<div className="absolute -top-[6px] right-0 -translate-y-full">
|
||||
<Show when={!fieldCAProvider}>
|
||||
@ -805,7 +878,7 @@ const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
|
||||
return {
|
||||
domains: "",
|
||||
contactEmail: "",
|
||||
...defaultNodeConfigForBizApply(),
|
||||
...(defaultNodeConfigForBizApply() as Nullish<z.infer<ReturnType<typeof getSchema>>>),
|
||||
};
|
||||
};
|
||||
|
||||
@ -828,7 +901,9 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
caProvider: z.string().nullish(),
|
||||
caProviderAccessId: z.string().nullish(),
|
||||
caProviderConfig: z.any().nullish(),
|
||||
keySource: z.enum([KEY_SOURCE_AUTO, KEY_SOURCE_REUSE, KEY_SOURCE_CUSTOM], t("workflow_node.apply.form.key_source.placeholder")),
|
||||
keyAlgorithm: z.string(t("workflow_node.apply.form.key_algorithm.placeholder")).nonempty(t("workflow_node.apply.form.key_algorithm.placeholder")),
|
||||
keyContent: z.string().nullish(),
|
||||
nameservers: z
|
||||
.string()
|
||||
.nullish()
|
||||
@ -888,6 +963,20 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
}
|
||||
}
|
||||
|
||||
if (values.keySource) {
|
||||
switch (values.keySource) {
|
||||
case KEY_SOURCE_CUSTOM:
|
||||
if (!values.keyContent) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: t("workflow_node.apply.form.key_content.placeholder"),
|
||||
path: ["keyContent"],
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (values.provider) {
|
||||
switch (values.challengeType) {
|
||||
case CHALLENGE_TYPE_DNS01:
|
||||
|
||||
@ -641,7 +641,7 @@ const getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n
|
||||
|
||||
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
|
||||
return {
|
||||
...defaultNodeConfigForBizDeploy(),
|
||||
...(defaultNodeConfigForBizDeploy() as Nullish<z.infer<ReturnType<typeof getSchema>>>),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -243,7 +243,7 @@ const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
|
||||
return {
|
||||
subject: "",
|
||||
message: "",
|
||||
...defaultNodeConfigForBizNotify(),
|
||||
...(defaultNodeConfigForBizNotify() as Nullish<z.infer<ReturnType<typeof getSchema>>>),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -185,10 +185,9 @@ const getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n
|
||||
|
||||
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
|
||||
return {
|
||||
source: UPLOAD_SOURCE_FORM,
|
||||
certificate: "",
|
||||
privateKey: "",
|
||||
...defaultNodeConfigForBizUpload(),
|
||||
...(defaultNodeConfigForBizUpload() as Nullish<z.infer<ReturnType<typeof getSchema>>>),
|
||||
};
|
||||
};
|
||||
|
||||
@ -197,7 +196,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
|
||||
return z
|
||||
.object({
|
||||
source: z.string(t("workflow_node.upload.form.source.placeholder")).nonempty(t("workflow_node.upload.form.source.placeholder")),
|
||||
source: z.enum([UPLOAD_SOURCE_FORM, UPLOAD_SOURCE_LOCAL, UPLOAD_SOURCE_URL], t("workflow_node.upload.form.source.placeholder")),
|
||||
certificate: z.string().max(20480, t("common.errmsg.string_max", { max: 20480 })),
|
||||
privateKey: z.string().max(20480, t("common.errmsg.string_max", { max: 20480 })),
|
||||
domains: z.string().nullish(),
|
||||
|
||||
@ -66,7 +66,7 @@ const getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n
|
||||
|
||||
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
|
||||
return {
|
||||
...defaultNodeConfigForDelay(),
|
||||
...(defaultNodeConfigForDelay() as Nullish<z.infer<ReturnType<typeof getSchema>>>),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -125,7 +125,7 @@ const getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n
|
||||
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
|
||||
return {
|
||||
trigger: WORKFLOW_TRIGGERS.MANUAL,
|
||||
...defaultNodeConfigForStart(),
|
||||
...(defaultNodeConfigForStart() as Nullish<z.infer<ReturnType<typeof getSchema>>>),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type FlowNodeEntity, getNodeForm, useClientContext, useRefresh } from "@flowgram.ai/fixed-layout-editor";
|
||||
import { type FlowNodeEntity, useClientContext, useRefresh } from "@flowgram.ai/fixed-layout-editor";
|
||||
import { IconEye, IconEyeOff, IconX } from "@tabler/icons-react";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { Anchor, type AnchorProps, App, Button, Drawer, Flex, type FormInstance, Tooltip, Typography } from "antd";
|
||||
@ -60,13 +60,13 @@ export const NodeConfigDrawer = ({ children, afterClose, anchor, footer = true,
|
||||
|
||||
const [isNodeDisabled, setIsNodeDisabled] = useState(() => {
|
||||
if (node) {
|
||||
return getNodeForm(node)?.getValueIn<boolean>("disabled");
|
||||
return node.form?.getValueIn<boolean>("disabled");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
useEffect(() => {
|
||||
const d1 = playground.config.onDataChange(() => refresh());
|
||||
const d2 = node?.onDataChange(() => setIsNodeDisabled(getNodeForm(node)?.getValueIn<boolean>("disabled")));
|
||||
const d2 = node?.onDataChange(() => setIsNodeDisabled(node.form?.getValueIn<boolean>("disabled")));
|
||||
|
||||
return () => {
|
||||
d1.dispose();
|
||||
@ -93,8 +93,8 @@ export const NodeConfigDrawer = ({ children, afterClose, anchor, footer = true,
|
||||
}
|
||||
|
||||
try {
|
||||
getNodeForm(node)!.setValueIn("config", formValues);
|
||||
getNodeForm(node)!.validate();
|
||||
node.form!.setValueIn("config", formValues);
|
||||
node.form!.validate();
|
||||
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
@ -152,7 +152,7 @@ export const NodeConfigDrawer = ({ children, afterClose, anchor, footer = true,
|
||||
};
|
||||
|
||||
const handleDisableNodeClick = () => {
|
||||
getNodeForm(node)!.setValueIn("disabled", !isNodeDisabled);
|
||||
node.form!.setValueIn("disabled", !isNodeDisabled);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -101,7 +101,9 @@ export type WorkflowNodeConfigForBizApply = {
|
||||
caProvider?: string;
|
||||
caProviderAccessId?: string;
|
||||
caProviderConfig?: Record<string, unknown>;
|
||||
keySource: string;
|
||||
keyAlgorithm: string;
|
||||
keyContent?: string;
|
||||
validityLifetime?: string;
|
||||
acmeProfile?: string;
|
||||
nameservers?: string;
|
||||
@ -115,6 +117,7 @@ export type WorkflowNodeConfigForBizApply = {
|
||||
export const defaultNodeConfigForBizApply = (): Partial<WorkflowNodeConfigForBizApply> => {
|
||||
return {
|
||||
challengeType: "dns-01" as const,
|
||||
keySource: "auto" as const,
|
||||
keyAlgorithm: "RSA2048" as const,
|
||||
skipBeforeExpiryDays: 30,
|
||||
};
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
"workflow_node.apply.form.contact_email.tooltip": "Contact information required for SSL certificate application. Please pay attention to the <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">rate limits</a>.",
|
||||
"workflow_node.apply.form.challenge_type.label": "Challenge type",
|
||||
"workflow_node.apply.form.challenge_type.placeholder": "Please select challenge type",
|
||||
"workflow_node.apply.form.challenge_type.tooltip": "It makes the CAs to validate that you control the domain names in the certificate. <a href=\"https://letsencrypt.org/docs/challenge-types/\" target=\"_blank\">Click here to learn more</a>.",
|
||||
"workflow_node.apply.form.challenge_type.tooltip": "It determines how the CAs verifies your control over the domain names. <br><a href=\"https://letsencrypt.org/docs/challenge-types/\" target=\"_blank\">Click here to learn more</a>.",
|
||||
"workflow_node.apply.form.provider.label": "Provider",
|
||||
"workflow_node.apply.form.provider.placeholder": "Please select provider",
|
||||
"workflow_node.apply.form.provider_dns01.label": "DNS provider",
|
||||
@ -78,8 +78,17 @@
|
||||
"workflow_node.apply.form.tencentcloud_eo_zone_id.label": "Tencent Cloud EdgeOne zone ID",
|
||||
"workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder": "Please enter Tencent Cloud EdgeOne zone ID",
|
||||
"workflow_node.apply.form.tencentcloud_eo_zone_id.tooltip": "For more information, see <a href=\"https://console.tencentcloud.com/edgeone\" target=\"_blank\">https://console.tencentcloud.com/edgeone</a>",
|
||||
"workflow_node.apply.form.key_algorithm.label": "Certificate key algorithm",
|
||||
"workflow_node.apply.form.key_algorithm.placeholder": "Please select certificate key algorithm",
|
||||
"workflow_node.apply.form.key_source.label": "Key source",
|
||||
"workflow_node.apply.form.key_source.placeholder": "Please select key source",
|
||||
"workflow_node.apply.form.key_source.option.auto.label": "Auto",
|
||||
"workflow_node.apply.form.key_source.option.reuse.label": "Reuse",
|
||||
"workflow_node.apply.form.key_source.option.custom.label": "Custom",
|
||||
"workflow_node.apply.form.key_algorithm.label": "Key algorithm",
|
||||
"workflow_node.apply.form.key_algorithm.placeholder": "Please select key algorithm",
|
||||
"workflow_node.apply.form.key_algorithm.help_reuse": "Notes: If there is an existing certificate, the original key algorithm will be used.",
|
||||
"workflow_node.apply.form.key_algorithm.help_custom": "Notes: Please ensure that the algorithm matches the private key.",
|
||||
"workflow_node.apply.form.key_content.label": "Private key (PEM format)",
|
||||
"workflow_node.apply.form.key_content.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
|
||||
"workflow_node.apply.form.ca_provider.label": "Certificate authority (Optional)",
|
||||
"workflow_node.apply.form.ca_provider.placeholder": "Please select a certificate authority",
|
||||
"workflow_node.apply.form.ca_provider.tooltip": "Used to issue SSL certificates.",
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
"certificate.props.certificate": "证书内容",
|
||||
"certificate.props.private_key": "私钥内容",
|
||||
"certificate.props.serial_number": "证书序列号",
|
||||
"certificate.props.key_algorithm": "证书算法",
|
||||
"certificate.props.key_algorithm": "私钥算法",
|
||||
"certificate.props.issuer": "颁发者",
|
||||
"certificate.props.created_at": "创建时间",
|
||||
"certificate.props.updated_at": "更新时间"
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
"settings.sslprovider.ca.title": "全局证书颁发机构",
|
||||
"settings.sslprovider.ca.tips": "如果你希望在每个工作流中选择不同的证书颁发机构,请前往授权凭据页面。",
|
||||
"settings.sslprovider.form.provider.label": "ACME 提供商",
|
||||
"settings.sslprovider.form.provider.help": "注意:不同服务商所支持的证书有效期、证书算法、多域名数量上限、是否允许泛域名等可能不同,切换服务商后请注意检查已有工作流的配置是否需要调整。",
|
||||
"settings.sslprovider.form.provider.help": "注意:不同服务商所支持的证书有效期、私钥算法、多域名数量上限、是否允许泛域名等可能不同,切换服务商后请注意检查已有工作流的配置是否需要调整。",
|
||||
"settings.sslprovider.form.letsencryptstaging_alert": "测试环境比生产环境有更宽松的速率限制,可进行测试性部署。<br><br>点击下方链接了解更多:<br><a href=\"https://letsencrypt.org/zh-cn/docs/staging-environment/\" target=\"_blank\">https://letsencrypt.org/zh-cn/docs/staging-environment/</a>",
|
||||
|
||||
"settings.persistence.tab": "数据持久化",
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
"workflow_node.apply.form.contact_email.tooltip": "申请签发 SSL 证书时所需的联系方式。请注意 Let's Encrypt 账户注册的<a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">速率限制</a>。",
|
||||
"workflow_node.apply.form.challenge_type.label": "质询方式",
|
||||
"workflow_node.apply.form.challenge_type.placeholder": "请选择质询方式",
|
||||
"workflow_node.apply.form.challenge_type.tooltip": "申请签发 SSL 证书时用于使证书颁发机构验证你对域名的控制权。<a href=\"https://letsencrypt.org/zh-cn/docs/challenge-types/\" target=\"_blank\">点此了解更多</a>。",
|
||||
"workflow_node.apply.form.challenge_type.tooltip": "表示证书颁发机构如何验证你对域名的控制权。<br><a href=\"https://letsencrypt.org/zh-cn/docs/challenge-types/\" target=\"_blank\">点此了解更多</a>。",
|
||||
"workflow_node.apply.form.provider.label": "提供商",
|
||||
"workflow_node.apply.form.provider.placeholder": "请选择提供商",
|
||||
"workflow_node.apply.form.provider_dns01.label": "DNS 提供商",
|
||||
@ -78,8 +78,17 @@
|
||||
"workflow_node.apply.form.tencentcloud_eo_zone_id.label": "腾讯云 EdgeOne 站点 ID",
|
||||
"workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder": "请输入腾讯云 EdgeOne 站点 ID",
|
||||
"workflow_node.apply.form.tencentcloud_eo_zone_id.tooltip": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/edgeone\" target=\"_blank\">https://console.cloud.tencent.com/edgeone</a>",
|
||||
"workflow_node.apply.form.key_algorithm.label": "证书算法",
|
||||
"workflow_node.apply.form.key_algorithm.placeholder": "请选择证书的算法",
|
||||
"workflow_node.apply.form.key_source.label": "私钥来源",
|
||||
"workflow_node.apply.form.key_source.placeholder": "请选择私钥来源",
|
||||
"workflow_node.apply.form.key_source.option.auto.label": "随机生成",
|
||||
"workflow_node.apply.form.key_source.option.reuse.label": "复用私钥",
|
||||
"workflow_node.apply.form.key_source.option.custom.label": "自定义",
|
||||
"workflow_node.apply.form.key_algorithm.label": "私钥算法",
|
||||
"workflow_node.apply.form.key_algorithm.placeholder": "请选择证书的私钥算法",
|
||||
"workflow_node.apply.form.key_algorithm.help_reuse": "提示:如果存在之前申请的证书,将以原私钥算法为准;否则才使用此选项。",
|
||||
"workflow_node.apply.form.key_algorithm.help_custom": "注意:请确保算法与私钥相匹配。",
|
||||
"workflow_node.apply.form.key_content.label": "私钥文件(PEM 格式)",
|
||||
"workflow_node.apply.form.key_content.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
|
||||
"workflow_node.apply.form.ca_provider.label": "证书颁发机构(可选)",
|
||||
"workflow_node.apply.form.ca_provider.placeholder": "请选择证书颁发机构",
|
||||
"workflow_node.apply.form.ca_provider.button": "设置",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user