feat: reusing certificates key

This commit is contained in:
Fu Diwei 2025-11-03 13:38:09 +08:00
parent 17f3c1b63f
commit 186a18db6d
33 changed files with 586 additions and 255 deletions

View File

@ -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 {

View File

@ -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,

View File

@ -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
}

View File

@ -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 {

View File

@ -30,5 +30,6 @@ type CertificateValidatePrivateKeyReq struct {
}
type CertificateValidatePrivateKeyResp struct {
IsValid bool `json:"isValid"`
IsValid bool `json:"isValid"`
KeyAlgorithm string `json:"keyAlgorithm,omitempty"`
}

View File

@ -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
}

View File

@ -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 ""

View File

@ -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)
}
}
}

View File

@ -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
})
}

View 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
})
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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 {

View 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")
}

View File

@ -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",

View File

@ -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",

View File

@ -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>

View File

@ -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:

View File

@ -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>>>),
};
};

View File

@ -243,7 +243,7 @@ const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
subject: "",
message: "",
...defaultNodeConfigForBizNotify(),
...(defaultNodeConfigForBizNotify() as Nullish<z.infer<ReturnType<typeof getSchema>>>),
};
};

View File

@ -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(),

View File

@ -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>>>),
};
};

View File

@ -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>>>),
};
};

View File

@ -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 (

View File

@ -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,
};

View File

@ -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.",

View File

@ -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": "更新时间"

View File

@ -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": "数据持久化",

View File

@ -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": "设置",