Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88b90986b1 | ||
|
|
44a6190e17 | ||
|
|
4475ed0dea | ||
|
|
6a23da3de3 | ||
|
|
0f1d5a7730 | ||
|
|
5b4c3bb668 | ||
|
|
ad49f9d788 | ||
|
|
397ceefa02 | ||
|
|
e11b1ca4e8 | ||
|
|
8e983e7286 | ||
|
|
f970ae7529 | ||
|
|
b0973b5ca8 | ||
|
|
4784bf9dba | ||
|
|
6b8dbf5235 | ||
|
|
48f698e84b | ||
|
|
ec0cdf8b96 | ||
|
|
2a6cc01eed | ||
|
|
acc1365101 | ||
|
|
c5409c78ba | ||
|
|
b97de6c06b | ||
|
|
4e3f499d76 | ||
|
|
3cebe51796 | ||
|
|
25bd17dc6e | ||
|
|
2525f54dc3 | ||
|
|
2127bb7e69 | ||
|
|
ed6d74f1ba | ||
|
|
02dd11f196 | ||
|
|
37b9ae30e2 | ||
|
|
0463dbcc75 | ||
|
|
111ef97d9c | ||
|
|
e8e854e392 | ||
|
|
ff43b9ab3e | ||
|
|
47c4ba9dd6 | ||
|
|
6ff738144a | ||
|
|
26028bb1eb | ||
|
|
eb4d5ddfd5 | ||
|
|
093ee006e4 | ||
|
|
9f8aa15af8 | ||
|
|
74d66a0131 | ||
|
|
626a86dea7 | ||
|
|
9ab029a296 | ||
|
|
8e1a81ae53 | ||
|
|
d76e1a3204 | ||
|
|
b585782007 | ||
|
|
2d198bcef7 | ||
|
|
0edcd9174f | ||
|
|
daa5b44f8e | ||
|
|
949660bc01 | ||
|
|
899a0b75b0 | ||
|
|
8cdb2afa69 | ||
|
|
00ec2ce33e | ||
|
|
2f7fd95684 | ||
|
|
55b1794004 | ||
|
|
e20972d4e7 | ||
|
|
749d727f50 | ||
|
|
9b524728c0 | ||
|
|
f81b4b9680 | ||
|
|
d2eaea7a44 | ||
|
|
f77c2dae23 | ||
|
|
a72737fdd5 | ||
|
|
4ab6b72e6f | ||
|
|
1468e74a6c | ||
|
|
09b5a21af1 | ||
|
|
6ad0d8e42f | ||
|
|
deb3b2f412 | ||
|
|
893391a3d1 | ||
|
|
7503d52857 | ||
|
|
fb860981d6 | ||
|
|
f302c7fb74 | ||
|
|
a8be2a77cf | ||
|
|
c2345e6118 |
10
.github/ISSUE_TEMPLATE/3-questions.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: "❓ Questions"
|
||||
description: "遇到了困难需要求助? / Have problem in use and need help?"
|
||||
title: "[Feature] 简要描述你遇到的问题"
|
||||
title: "简要描述你遇到的问题"
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -19,6 +19,14 @@ body:
|
||||
3. Yes, I've read the [documentation](https://docs.certimate.me/en/) and didn't find any similar.
|
||||
4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly.
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: 软件版本 / Release Version
|
||||
description: 请提供 Certimate 的具体版本。 / Please provide the specific version of Certimate.
|
||||
placeholder: (e.g. v1.0.0)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 问题描述 / Description
|
||||
|
||||
16
Dockerfile
@@ -1,33 +1,23 @@
|
||||
FROM node:20-alpine3.19 AS front-builder
|
||||
|
||||
FROM node:20-alpine3.19 AS webui-builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app/
|
||||
|
||||
RUN \
|
||||
cd /app/ui && \
|
||||
npm install && \
|
||||
npm run build
|
||||
|
||||
|
||||
|
||||
FROM golang:1.23-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ../. /app/
|
||||
|
||||
RUN rm -rf /app/ui/dist
|
||||
|
||||
COPY --from=front-builder /app/ui/dist /app/ui/dist
|
||||
|
||||
COPY --from=webui-builder /app/ui/dist /app/ui/dist
|
||||
RUN go build -o certimate
|
||||
|
||||
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/certimate .
|
||||
|
||||
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]
|
||||
@@ -41,7 +41,7 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
|
||||
- 支持 20+ 域名托管商(如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers));
|
||||
- 支持 70+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-host-providers));
|
||||
- 支持邮件、钉钉、飞书、企业微信、Webhook 等多种通知渠道;
|
||||
- 支持 Let's Encrypt、ZeroSSL、Google Trust Services 等多种 ACME 证书颁发机构;
|
||||
- 支持 Let's Encrypt、Buypass、Google Trust Services、SSL.com、ZeroSSL 等多种 ACME 证书颁发机构;
|
||||
- 更多特性等待探索。
|
||||
|
||||
## ⏱️ 快速启动
|
||||
@@ -71,7 +71,7 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
|
||||
|
||||
相关文章:
|
||||
|
||||
- [使用 CNAME 实现 DNS-01 challenge](https://docs.certimate.me/blog/cname)
|
||||
- [使用 CNAME 完成 ACME DNS-01 质询](https://docs.certimate.me/blog/cname)
|
||||
- [v0.3.0:第二个不向后兼容的大版本](https://docs.certimate.me/blog/v0.3.0)
|
||||
- [v0.2.0:第一个不向后兼容的大版本](https://docs.certimate.me/blog/v0.2.0)
|
||||
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
||||
|
||||
@@ -41,7 +41,7 @@ Certimate aims to provide users with a secure and user-friendly SSL certificate
|
||||
- Supports more than 20+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-dns-providers));
|
||||
- Supports more than 70+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-host-providers));
|
||||
- Supports multiple notification channels including email, DingTalk, Feishu, WeCom, Webhook, and more;
|
||||
- Supports multiple ACME CAs including Let's Encrypt, ZeroSSL, Google Trust Services, and more;
|
||||
- Supports multiple ACME CAs including Let's Encrypt, Buypass, Google Trust Services,SSL.com, ZeroSSL, and more;
|
||||
- More features waiting to be discovered.
|
||||
|
||||
## ⏱️ Fast Track
|
||||
@@ -69,7 +69,7 @@ Please visit the documentation site [docs.certimate.me](https://docs.certimate.m
|
||||
|
||||
Related articles:
|
||||
|
||||
- [使用 CNAME 实现 DNS-01 challenge](https://docs.certimate.me/blog/cname)
|
||||
- [使用 CNAME 完成 ACME DNS-01 质询](https://docs.certimate.me/blog/cname)
|
||||
- [v0.3.0:第二个不向后兼容的大版本](https://docs.certimate.me/blog/v0.3.0)
|
||||
- [v0.2.0:第一个不向后兼容的大版本](https://docs.certimate.me/blog/v0.2.0)
|
||||
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
||||
|
||||
3
go.mod
@@ -8,6 +8,7 @@ require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2
|
||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azcertificates v0.9.0
|
||||
github.com/Edgio/edgio-api v0.0.0-workspace
|
||||
github.com/G-Core/gcorelabscdn-go v1.0.28
|
||||
github.com/alibabacloud-go/alb-20200616/v2 v2.2.8
|
||||
github.com/alibabacloud-go/cas-20200407/v3 v3.0.4
|
||||
@@ -211,6 +212,8 @@ require (
|
||||
modernc.org/sqlite v1.36.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/Edgio/edgio-api v0.0.0-workspace => ./internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace
|
||||
|
||||
replace gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkcore@v1.0.0
|
||||
|
||||
replace gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkclouddns@v1.0.1
|
||||
|
||||
2
go.sum
@@ -498,6 +498,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
|
||||
@@ -1,38 +1,30 @@
|
||||
package applicant
|
||||
|
||||
const (
|
||||
sslProviderLetsEncrypt = "letsencrypt"
|
||||
sslProviderLetsEncryptStaging = "letsencrypt_staging"
|
||||
sslProviderZeroSSL = "zerossl"
|
||||
sslProviderGoogleTrustServices = "gts"
|
||||
)
|
||||
const defaultSSLProvider = sslProviderLetsEncrypt
|
||||
import "github.com/usual2970/certimate/internal/domain"
|
||||
|
||||
const (
|
||||
letsencryptUrl = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
letsencryptStagingUrl = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
zerosslUrl = "https://acme.zerossl.com/v2/DV90"
|
||||
gtsUrl = "https://dv.acme-v02.api.pki.goog/directory"
|
||||
sslProviderLetsEncrypt = string(domain.ApplyCAProviderTypeLetsEncrypt)
|
||||
sslProviderLetsEncryptStaging = string(domain.ApplyCAProviderTypeLetsEncryptStaging)
|
||||
sslProviderBuypass = string(domain.ApplyCAProviderTypeBuypass)
|
||||
sslProviderGoogleTrustServices = string(domain.ApplyCAProviderTypeGoogleTrustServices)
|
||||
sslProviderSSLCom = string(domain.ApplyCAProviderTypeSSLCom)
|
||||
sslProviderZeroSSL = string(domain.ApplyCAProviderTypeZeroSSL)
|
||||
|
||||
sslProviderDefault = sslProviderLetsEncrypt
|
||||
)
|
||||
|
||||
var sslProviderUrls = map[string]string{
|
||||
sslProviderLetsEncrypt: letsencryptUrl,
|
||||
sslProviderLetsEncryptStaging: letsencryptStagingUrl,
|
||||
sslProviderZeroSSL: zerosslUrl,
|
||||
sslProviderGoogleTrustServices: gtsUrl,
|
||||
sslProviderLetsEncrypt: "https://acme-v02.api.letsencrypt.org/directory",
|
||||
sslProviderLetsEncryptStaging: "https://acme-staging-v02.api.letsencrypt.org/directory",
|
||||
sslProviderBuypass: "https://api.buypass.com/acme/directory",
|
||||
sslProviderGoogleTrustServices: "https://dv.acme-v02.api.pki.goog/directory",
|
||||
sslProviderSSLCom: "https://acme.ssl.com/sslcom-dv-rsa",
|
||||
sslProviderSSLCom + "RSA": "https://acme.ssl.com/sslcom-dv-rsa",
|
||||
sslProviderSSLCom + "ECC": "https://acme.ssl.com/sslcom-dv-ecc",
|
||||
sslProviderZeroSSL: "https://acme.zerossl.com/v2/DV90",
|
||||
}
|
||||
|
||||
type acmeSSLProviderConfig struct {
|
||||
Config acmeSSLProviderConfigContent `json:"config"`
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
type acmeSSLProviderConfigContent struct {
|
||||
ZeroSSL acmeSSLProviderEabConfig `json:"zerossl"`
|
||||
GoogleTrustServices acmeSSLProviderEabConfig `json:"gts"`
|
||||
}
|
||||
|
||||
type acmeSSLProviderEabConfig struct {
|
||||
EabHmacKey string `json:"eabHmacKey"`
|
||||
EabKid string `json:"eabKid"`
|
||||
Config map[domain.ApplyCAProviderType]map[string]any `json:"config"`
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/certutil"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/maputil"
|
||||
"github.com/usual2970/certimate/internal/repository"
|
||||
)
|
||||
|
||||
@@ -76,16 +77,11 @@ func (u *acmeUser) getPrivateKeyPEM() string {
|
||||
return u.privkey
|
||||
}
|
||||
|
||||
type acmeAccountRepository interface {
|
||||
GetByCAAndEmail(ca, email string) (*domain.AcmeAccount, error)
|
||||
Save(ca, email, key string, resource *registration.Resource) error
|
||||
}
|
||||
|
||||
var registerGroup singleflight.Group
|
||||
|
||||
func registerAcmeUserWithSingleFlight(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) {
|
||||
resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", sslProviderConfig.Provider, user.GetEmail()), func() (interface{}, error) {
|
||||
return registerAcmeUser(client, sslProviderConfig, user)
|
||||
func registerAcmeUserWithSingleFlight(client *lego.Client, user *acmeUser, userRegisterOptions map[string]any) (*registration.Resource, error) {
|
||||
resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", user.CA, user.Email), func() (interface{}, error) {
|
||||
return registerAcmeUser(client, user, userRegisterOptions)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -95,45 +91,81 @@ func registerAcmeUserWithSingleFlight(client *lego.Client, sslProviderConfig *ac
|
||||
return resp.(*registration.Resource), nil
|
||||
}
|
||||
|
||||
func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) {
|
||||
func registerAcmeUser(client *lego.Client, user *acmeUser, userRegisterOptions map[string]any) (*registration.Resource, error) {
|
||||
var reg *registration.Resource
|
||||
var err error
|
||||
switch sslProviderConfig.Provider {
|
||||
case sslProviderZeroSSL:
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: sslProviderConfig.Config.ZeroSSL.EabKid,
|
||||
HmacEncoded: sslProviderConfig.Config.ZeroSSL.EabHmacKey,
|
||||
})
|
||||
case sslProviderGoogleTrustServices:
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: sslProviderConfig.Config.GoogleTrustServices.EabKid,
|
||||
HmacEncoded: sslProviderConfig.Config.GoogleTrustServices.EabHmacKey,
|
||||
})
|
||||
switch user.CA {
|
||||
case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging:
|
||||
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
|
||||
case sslProviderBuypass:
|
||||
{
|
||||
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
}
|
||||
|
||||
case sslProviderGoogleTrustServices:
|
||||
{
|
||||
access := domain.AccessConfigForGoogleTrustServices{}
|
||||
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: access.EabKid,
|
||||
HmacEncoded: access.EabHmacKey,
|
||||
})
|
||||
}
|
||||
|
||||
case sslProviderSSLCom:
|
||||
{
|
||||
access := domain.AccessConfigForSSLCom{}
|
||||
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: access.EabKid,
|
||||
HmacEncoded: access.EabHmacKey,
|
||||
})
|
||||
}
|
||||
|
||||
case sslProviderZeroSSL:
|
||||
{
|
||||
access := domain.AccessConfigForZeroSSL{}
|
||||
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: access.EabKid,
|
||||
HmacEncoded: access.EabHmacKey,
|
||||
})
|
||||
}
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("unsupported ssl provider: %s", sslProviderConfig.Provider)
|
||||
err = fmt.Errorf("unsupported ca provider: %s", user.CA)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repo := repository.NewAcmeAccountRepository()
|
||||
resp, err := repo.GetByCAAndEmail(sslProviderConfig.Provider, user.GetEmail())
|
||||
resp, err := repo.GetByCAAndEmail(user.CA, user.Email)
|
||||
if err == nil {
|
||||
user.privkey = resp.Key
|
||||
return resp.Resource, nil
|
||||
}
|
||||
|
||||
if _, err := repo.Save(context.Background(), &domain.AcmeAccount{
|
||||
CA: sslProviderConfig.Provider,
|
||||
Email: user.GetEmail(),
|
||||
CA: user.CA,
|
||||
Email: user.Email,
|
||||
Key: user.getPrivateKeyPEM(),
|
||||
Resource: reg,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to save registration: %w", err)
|
||||
return nil, fmt.Errorf("failed to save acme account registration: %w", err)
|
||||
}
|
||||
|
||||
return reg, nil
|
||||
|
||||
@@ -37,18 +37,21 @@ type Applicant interface {
|
||||
}
|
||||
|
||||
type applicantOptions struct {
|
||||
Domains []string
|
||||
ContactEmail string
|
||||
Provider domain.ApplyDNSProviderType
|
||||
ProviderAccessConfig map[string]any
|
||||
ProviderApplyConfig map[string]any
|
||||
KeyAlgorithm string
|
||||
Nameservers []string
|
||||
DnsPropagationTimeout int32
|
||||
DnsTTL int32
|
||||
DisableFollowCNAME bool
|
||||
ReplacedARIAcctId string
|
||||
ReplacedARICertId string
|
||||
Domains []string
|
||||
ContactEmail string
|
||||
Provider domain.ApplyDNSProviderType
|
||||
ProviderAccessConfig map[string]any
|
||||
ProviderExtendedConfig map[string]any
|
||||
CAProvider domain.ApplyCAProviderType
|
||||
CAProviderAccessConfig map[string]any
|
||||
CAProviderExtendedConfig map[string]any
|
||||
KeyAlgorithm string
|
||||
Nameservers []string
|
||||
DnsPropagationTimeout int32
|
||||
DnsTTL int32
|
||||
DisableFollowCNAME bool
|
||||
ReplacedARIAcct string
|
||||
ReplacedARICert string
|
||||
}
|
||||
|
||||
func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
|
||||
@@ -58,22 +61,55 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
|
||||
|
||||
nodeConfig := node.GetConfigForApply()
|
||||
options := &applicantOptions{
|
||||
Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }),
|
||||
ContactEmail: nodeConfig.ContactEmail,
|
||||
Provider: domain.ApplyDNSProviderType(nodeConfig.Provider),
|
||||
ProviderApplyConfig: nodeConfig.ProviderConfig,
|
||||
KeyAlgorithm: nodeConfig.KeyAlgorithm,
|
||||
Nameservers: sliceutil.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }),
|
||||
DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout,
|
||||
DnsTTL: nodeConfig.DnsTTL,
|
||||
DisableFollowCNAME: nodeConfig.DisableFollowCNAME,
|
||||
Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }),
|
||||
ContactEmail: nodeConfig.ContactEmail,
|
||||
Provider: domain.ApplyDNSProviderType(nodeConfig.Provider),
|
||||
ProviderAccessConfig: make(map[string]any),
|
||||
ProviderExtendedConfig: nodeConfig.ProviderConfig,
|
||||
CAProvider: domain.ApplyCAProviderType(nodeConfig.CAProvider),
|
||||
CAProviderAccessConfig: make(map[string]any),
|
||||
CAProviderExtendedConfig: nodeConfig.CAProviderConfig,
|
||||
KeyAlgorithm: nodeConfig.KeyAlgorithm,
|
||||
Nameservers: sliceutil.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }),
|
||||
DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout,
|
||||
DnsTTL: nodeConfig.DnsTTL,
|
||||
DisableFollowCNAME: nodeConfig.DisableFollowCNAME,
|
||||
}
|
||||
|
||||
accessRepo := repository.NewAccessRepository()
|
||||
if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil {
|
||||
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
|
||||
} else {
|
||||
options.ProviderAccessConfig = access.Config
|
||||
if nodeConfig.ProviderAccessId != "" {
|
||||
if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil {
|
||||
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
|
||||
} else {
|
||||
options.ProviderAccessConfig = access.Config
|
||||
}
|
||||
}
|
||||
if nodeConfig.CAProviderAccessId != "" {
|
||||
if access, err := accessRepo.GetById(context.Background(), nodeConfig.CAProviderAccessId); err != nil {
|
||||
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.CAProviderAccessId, err)
|
||||
} else {
|
||||
options.CAProviderAccessConfig = access.Config
|
||||
}
|
||||
}
|
||||
|
||||
settingsRepo := repository.NewSettingsRepository()
|
||||
if string(options.CAProvider) == "" {
|
||||
settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider")
|
||||
|
||||
sslProviderConfig := &acmeSSLProviderConfig{
|
||||
Config: make(map[domain.ApplyCAProviderType]map[string]any),
|
||||
Provider: sslProviderDefault,
|
||||
}
|
||||
if settings != nil {
|
||||
if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil {
|
||||
return nil, err
|
||||
} else if sslProviderConfig.Provider == "" {
|
||||
sslProviderConfig.Provider = sslProviderDefault
|
||||
}
|
||||
}
|
||||
|
||||
options.CAProvider = domain.ApplyCAProviderType(sslProviderConfig.Provider)
|
||||
options.CAProviderAccessConfig = sslProviderConfig.Config[options.CAProvider]
|
||||
}
|
||||
|
||||
certRepo := repository.NewCertificateRepository()
|
||||
@@ -88,8 +124,8 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
|
||||
lastCertX509, _ := certcrypto.ParsePEMCertificate([]byte(lastCertificate.Certificate))
|
||||
if lastCertX509 != nil {
|
||||
replacedARICertId, _ := certificate.MakeARICertID(lastCertX509)
|
||||
options.ReplacedARIAcctId = lastCertificate.ACMEAccountUrl
|
||||
options.ReplacedARICertId = replacedARICertId
|
||||
options.ReplacedARIAcct = lastCertificate.ACMEAccountUrl
|
||||
options.ReplacedARICert = replacedARICertId
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,24 +142,7 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
|
||||
}
|
||||
|
||||
func apply(challengeProvider challenge.Provider, options *applicantOptions) (*ApplyCertResult, error) {
|
||||
settingsRepo := repository.NewSettingsRepository()
|
||||
settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider")
|
||||
|
||||
sslProviderConfig := &acmeSSLProviderConfig{
|
||||
Config: acmeSSLProviderConfigContent{},
|
||||
Provider: defaultSSLProvider,
|
||||
}
|
||||
if settings != nil {
|
||||
if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if sslProviderConfig.Provider == "" {
|
||||
sslProviderConfig.Provider = defaultSSLProvider
|
||||
}
|
||||
|
||||
acmeUser, err := newAcmeUser(sslProviderConfig.Provider, options.ContactEmail)
|
||||
user, err := newAcmeUser(string(options.CAProvider), options.ContactEmail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -133,9 +152,16 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
|
||||
os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(options.DisableFollowCNAME))
|
||||
|
||||
// Create an ACME client config
|
||||
config := lego.NewConfig(acmeUser)
|
||||
config.CADirURL = sslProviderUrls[sslProviderConfig.Provider]
|
||||
config := lego.NewConfig(user)
|
||||
config.Certificate.KeyType = parseKeyAlgorithm(domain.CertificateKeyAlgorithmType(options.KeyAlgorithm))
|
||||
config.CADirURL = sslProviderUrls[user.CA]
|
||||
if user.CA == sslProviderSSLCom {
|
||||
if strings.HasPrefix(options.KeyAlgorithm, "RSA") {
|
||||
config.CADirURL = sslProviderUrls[sslProviderSSLCom+"RSA"]
|
||||
} else if strings.HasPrefix(options.KeyAlgorithm, "EC") {
|
||||
config.CADirURL = sslProviderUrls[sslProviderSSLCom+"ECC"]
|
||||
}
|
||||
}
|
||||
|
||||
// Create an ACME client
|
||||
client, err := lego.NewClient(config)
|
||||
@@ -152,12 +178,12 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
|
||||
client.Challenge.SetDNS01Provider(challengeProvider, challengeOptions...)
|
||||
|
||||
// New users need to register first
|
||||
if !acmeUser.hasRegistration() {
|
||||
reg, err := registerAcmeUserWithSingleFlight(client, sslProviderConfig, acmeUser)
|
||||
if !user.hasRegistration() {
|
||||
reg, err := registerAcmeUserWithSingleFlight(client, user, options.CAProviderAccessConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to register: %w", err)
|
||||
}
|
||||
acmeUser.Registration = reg
|
||||
user.Registration = reg
|
||||
}
|
||||
|
||||
// Obtain a certificate
|
||||
@@ -165,8 +191,8 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
|
||||
Domains: options.Domains,
|
||||
Bundle: true,
|
||||
}
|
||||
if options.ReplacedARICertId != "" && options.ReplacedARIAcctId != acmeUser.Registration.URI {
|
||||
certRequest.ReplacesCertID = options.ReplacedARICertId
|
||||
if options.ReplacedARIAcct == user.Registration.URI {
|
||||
certRequest.ReplacesCertID = options.ReplacedARICert
|
||||
}
|
||||
certResource, err := client.Certificate.Obtain(certRequest)
|
||||
if err != nil {
|
||||
@@ -177,7 +203,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
|
||||
CertificateFullChain: strings.TrimSpace(string(certResource.Certificate)),
|
||||
IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)),
|
||||
PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)),
|
||||
ACMEAccountUrl: acmeUser.Registration.URI,
|
||||
ACMEAccountUrl: user.Registration.URI,
|
||||
ACMECertUrl: certResource.CertURL,
|
||||
ACMECertStableUrl: certResource.CertStableURL,
|
||||
CSR: strings.TrimSpace(string(certResource.CSR)),
|
||||
@@ -198,6 +224,8 @@ func parseKeyAlgorithm(algo domain.CertificateKeyAlgorithmType) certcrypto.KeyTy
|
||||
return certcrypto.EC256
|
||||
case domain.CertificateKeyAlgorithmTypeEC384:
|
||||
return certcrypto.EC384
|
||||
case domain.CertificateKeyAlgorithmTypeEC512:
|
||||
return certcrypto.KeyType("P512")
|
||||
}
|
||||
|
||||
return certcrypto.RSA2048
|
||||
|
||||
@@ -86,8 +86,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
|
||||
applicant, err := pAWSRoute53.NewChallengeProvider(&pAWSRoute53.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: maputil.GetString(options.ProviderApplyConfig, "region"),
|
||||
HostedZoneId: maputil.GetString(options.ProviderApplyConfig, "hostedZoneId"),
|
||||
Region: maputil.GetString(options.ProviderExtendedConfig, "region"),
|
||||
HostedZoneId: maputil.GetString(options.ProviderExtendedConfig, "hostedZoneId"),
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
@@ -137,6 +137,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
|
||||
|
||||
applicant, err := pCloudflare.NewChallengeProvider(&pCloudflare.ChallengeProviderConfig{
|
||||
DnsApiToken: access.DnsApiToken,
|
||||
ZoneApiToken: access.ZoneApiToken,
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
@@ -278,7 +279,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
|
||||
applicant, err := pHuaweiCloud.NewChallengeProvider(&pHuaweiCloud.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: maputil.GetString(options.ProviderApplyConfig, "region"),
|
||||
Region: maputil.GetString(options.ProviderExtendedConfig, "region"),
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
@@ -295,7 +296,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
|
||||
applicant, err := pJDCloud.NewChallengeProvider(&pJDCloud.ChallengeProviderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
RegionId: maputil.GetString(options.ProviderApplyConfig, "regionId"),
|
||||
RegionId: maputil.GetString(options.ProviderExtendedConfig, "regionId"),
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
@@ -432,7 +433,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
|
||||
applicant, err := pTencentCloudEO.NewChallengeProvider(&pTencentCloudEO.ChallengeProviderConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
ZoneId: maputil.GetString(options.ProviderApplyConfig, "zoneId"),
|
||||
ZoneId: maputil.GetString(options.ProviderExtendedConfig, "zoneId"),
|
||||
DnsPropagationTimeout: options.DnsPropagationTimeout,
|
||||
DnsTTL: options.DnsTTL,
|
||||
})
|
||||
|
||||
@@ -32,18 +32,23 @@ func NewWithDeployNode(node *domain.WorkflowNode, certdata struct {
|
||||
}
|
||||
|
||||
nodeConfig := node.GetConfigForDeploy()
|
||||
|
||||
accessRepo := repository.NewAccessRepository()
|
||||
access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
|
||||
options := &deployerOptions{
|
||||
Provider: domain.DeployProviderType(nodeConfig.Provider),
|
||||
ProviderAccessConfig: make(map[string]any),
|
||||
ProviderDeployConfig: nodeConfig.ProviderConfig,
|
||||
}
|
||||
|
||||
deployer, err := createDeployer(&deployerOptions{
|
||||
Provider: domain.DeployProviderType(nodeConfig.Provider),
|
||||
ProviderAccessConfig: access.Config,
|
||||
ProviderDeployConfig: nodeConfig.ProviderConfig,
|
||||
})
|
||||
accessRepo := repository.NewAccessRepository()
|
||||
if nodeConfig.ProviderAccessId != "" {
|
||||
access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
|
||||
} else {
|
||||
options.ProviderAccessConfig = access.Config
|
||||
}
|
||||
}
|
||||
|
||||
deployer, err := createDeployer(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ import (
|
||||
pLocal "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local"
|
||||
pQiniuCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn"
|
||||
pQiniuPili "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-pili"
|
||||
pRainYunRCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/rainyun-rcdn"
|
||||
pSafeLine "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/safeline"
|
||||
pSSH "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh"
|
||||
pTencentCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn"
|
||||
@@ -73,6 +74,7 @@ import (
|
||||
pVolcEngineImageX "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-imagex"
|
||||
pVolcEngineLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live"
|
||||
pVolcEngineTOS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-tos"
|
||||
pWangsuCDNPro "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-cdnpro"
|
||||
pWebhook "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/maputil"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/sliceutil"
|
||||
@@ -370,8 +372,9 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeBaishanCDN:
|
||||
deployer, err := pBaishanCDN.NewDeployer(&pBaishanCDN.DeployerConfig{
|
||||
ApiToken: access.ApiToken,
|
||||
Domain: maputil.GetString(options.ProviderDeployConfig, "domain"),
|
||||
ApiToken: access.ApiToken,
|
||||
Domain: maputil.GetString(options.ProviderDeployConfig, "domain"),
|
||||
CertificateId: maputil.GetString(options.ProviderDeployConfig, "certificateId"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
@@ -680,6 +683,27 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeRainYunRCDN:
|
||||
{
|
||||
access := domain.AccessConfigForRainYun{}
|
||||
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeTencentCloudCDN:
|
||||
deployer, err := pRainYunRCDN.NewDeployer(&pRainYunRCDN.DeployerConfig{
|
||||
ApiKey: access.ApiKey,
|
||||
InstanceId: maputil.GetInt32(options.ProviderDeployConfig, "instanceId"),
|
||||
Domain: maputil.GetString(options.ProviderDeployConfig, "domain"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeSafeLine:
|
||||
{
|
||||
access := domain.AccessConfigForSafeLine{}
|
||||
@@ -980,6 +1004,30 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeWangsuCDNPro:
|
||||
{
|
||||
access := domain.AccessConfigForWangsu{}
|
||||
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
switch options.Provider {
|
||||
case domain.DeployProviderTypeWangsuCDNPro:
|
||||
deployer, err := pWangsuCDNPro.NewDeployer(&pWangsuCDNPro.DeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Environment: maputil.GetOrDefaultString(options.ProviderDeployConfig, "environment", "production"),
|
||||
Domain: maputil.GetString(options.ProviderDeployConfig, "domain"),
|
||||
CertificateId: maputil.GetString(options.ProviderDeployConfig, "certificateId"),
|
||||
WebhookId: maputil.GetString(options.ProviderDeployConfig, "webhookId"),
|
||||
})
|
||||
return deployer, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case domain.DeployProviderTypeWebhook:
|
||||
{
|
||||
access := domain.AccessConfigForWebhook{}
|
||||
|
||||
@@ -75,7 +75,8 @@ type AccessConfigForCdnfly struct {
|
||||
}
|
||||
|
||||
type AccessConfigForCloudflare struct {
|
||||
DnsApiToken string `json:"dnsApiToken"`
|
||||
DnsApiToken string `json:"dnsApiToken"`
|
||||
ZoneApiToken string `json:"zoneApiToken,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForClouDNS struct {
|
||||
@@ -125,6 +126,11 @@ type AccessConfigForGoDaddy struct {
|
||||
ApiSecret string `json:"apiSecret"`
|
||||
}
|
||||
|
||||
type AccessConfigForGoogleTrustServices struct {
|
||||
EabKid string `json:"eabKid"`
|
||||
EabHmacKey string `json:"eabHmacKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForHuaweiCloud struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
@@ -139,8 +145,6 @@ type AccessConfigForKubernetes struct {
|
||||
KubeConfig string `json:"kubeConfig,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForLocal struct{}
|
||||
|
||||
type AccessConfigForNamecheap struct {
|
||||
Username string `json:"username"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
@@ -193,6 +197,11 @@ type AccessConfigForSSH struct {
|
||||
KeyPassphrase string `json:"keyPassphrase,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForSSLCom struct {
|
||||
EabKid string `json:"eabKid"`
|
||||
EabHmacKey string `json:"eabHmacKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForTencentCloud struct {
|
||||
SecretId string `json:"secretId"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
@@ -219,6 +228,11 @@ type AccessConfigForVolcEngine struct {
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
}
|
||||
|
||||
type AccessConfigForWangsu struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
}
|
||||
|
||||
type AccessConfigForWebhook struct {
|
||||
Url string `json:"url"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
@@ -228,3 +242,8 @@ type AccessConfigForWestcn struct {
|
||||
Username string `json:"username"`
|
||||
ApiPassword string `json:"password"`
|
||||
}
|
||||
|
||||
type AccessConfigForZeroSSL struct {
|
||||
EabKid string `json:"eabKid"`
|
||||
EabHmacKey string `json:"eabHmacKey"`
|
||||
}
|
||||
|
||||
@@ -12,7 +12,11 @@ const (
|
||||
NotifyChannelTypeBark = NotifyChannelType("bark")
|
||||
NotifyChannelTypeDingTalk = NotifyChannelType("dingtalk")
|
||||
NotifyChannelTypeEmail = NotifyChannelType("email")
|
||||
NotifyChannelTypeGotify = NotifyChannelType("gotify")
|
||||
NotifyChannelTypeLark = NotifyChannelType("lark")
|
||||
NotifyChannelTypeMattermost = NotifyChannelType("mattermost")
|
||||
NotifyChannelTypePushover = NotifyChannelType("pushover")
|
||||
NotifyChannelTypePushPlus = NotifyChannelType("pushplus")
|
||||
NotifyChannelTypeServerChan = NotifyChannelType("serverchan")
|
||||
NotifyChannelTypeTelegram = NotifyChannelType("telegram")
|
||||
NotifyChannelTypeWebhook = NotifyChannelType("webhook")
|
||||
|
||||
@@ -9,55 +9,80 @@ type AccessProviderType string
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
const (
|
||||
AccessProviderType1Panel = AccessProviderType("1panel")
|
||||
AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq")
|
||||
AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai(预留)
|
||||
AccessProviderTypeAliyun = AccessProviderType("aliyun")
|
||||
AccessProviderTypeAWS = AccessProviderType("aws")
|
||||
AccessProviderTypeAzure = AccessProviderType("azure")
|
||||
AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud")
|
||||
AccessProviderTypeBaishan = AccessProviderType("baishan")
|
||||
AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel")
|
||||
AccessProviderTypeBytePlus = AccessProviderType("byteplus")
|
||||
AccessProviderTypeCacheFly = AccessProviderType("cachefly")
|
||||
AccessProviderTypeCdnfly = AccessProviderType("cdnfly")
|
||||
AccessProviderTypeCloudflare = AccessProviderType("cloudflare")
|
||||
AccessProviderTypeClouDNS = AccessProviderType("cloudns")
|
||||
AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud")
|
||||
AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 联通云(预留)
|
||||
AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 天翼云(预留)
|
||||
AccessProviderTypeDeSEC = AccessProviderType("desec")
|
||||
AccessProviderTypeDNSLA = AccessProviderType("dnsla")
|
||||
AccessProviderTypeDogeCloud = AccessProviderType("dogecloud")
|
||||
AccessProviderTypeDynv6 = AccessProviderType("dynv6")
|
||||
AccessProviderTypeEdgio = AccessProviderType("edgio")
|
||||
AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留)
|
||||
AccessProviderTypeGname = AccessProviderType("gname")
|
||||
AccessProviderTypeGcore = AccessProviderType("gcore")
|
||||
AccessProviderTypeGoDaddy = AccessProviderType("godaddy")
|
||||
AccessProviderTypeGoEdge = AccessProviderType("goedge") // GoEdge(预留)
|
||||
AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud")
|
||||
AccessProviderTypeJDCloud = AccessProviderType("jdcloud")
|
||||
AccessProviderTypeKubernetes = AccessProviderType("k8s")
|
||||
AccessProviderTypeLocal = AccessProviderType("local")
|
||||
AccessProviderTypeNamecheap = AccessProviderType("namecheap")
|
||||
AccessProviderTypeNameDotCom = AccessProviderType("namedotcom")
|
||||
AccessProviderTypeNameSilo = AccessProviderType("namesilo")
|
||||
AccessProviderTypeNS1 = AccessProviderType("ns1")
|
||||
AccessProviderTypePorkbun = AccessProviderType("porkbun")
|
||||
AccessProviderTypePowerDNS = AccessProviderType("powerdns")
|
||||
AccessProviderTypeQiniu = AccessProviderType("qiniu")
|
||||
AccessProviderTypeQingCloud = AccessProviderType("qingcloud") // 青云(预留)
|
||||
AccessProviderTypeRainYun = AccessProviderType("rainyun")
|
||||
AccessProviderTypeSafeLine = AccessProviderType("safeline")
|
||||
AccessProviderTypeSSH = AccessProviderType("ssh")
|
||||
AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud")
|
||||
AccessProviderTypeUCloud = AccessProviderType("ucloud")
|
||||
AccessProviderTypeUpyun = AccessProviderType("upyun")
|
||||
AccessProviderTypeVercel = AccessProviderType("vercel")
|
||||
AccessProviderTypeVolcEngine = AccessProviderType("volcengine")
|
||||
AccessProviderTypeWebhook = AccessProviderType("webhook")
|
||||
AccessProviderTypeWestcn = AccessProviderType("westcn")
|
||||
AccessProviderType1Panel = AccessProviderType("1panel")
|
||||
AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq")
|
||||
AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai(预留)
|
||||
AccessProviderTypeAliyun = AccessProviderType("aliyun")
|
||||
AccessProviderTypeAWS = AccessProviderType("aws")
|
||||
AccessProviderTypeAzure = AccessProviderType("azure")
|
||||
AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud")
|
||||
AccessProviderTypeBaishan = AccessProviderType("baishan")
|
||||
AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel")
|
||||
AccessProviderTypeBytePlus = AccessProviderType("byteplus")
|
||||
AccessProviderTypeBuypass = AccessProviderType("buypass")
|
||||
AccessProviderTypeCacheFly = AccessProviderType("cachefly")
|
||||
AccessProviderTypeCdnfly = AccessProviderType("cdnfly")
|
||||
AccessProviderTypeCloudflare = AccessProviderType("cloudflare")
|
||||
AccessProviderTypeClouDNS = AccessProviderType("cloudns")
|
||||
AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud")
|
||||
AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 联通云(预留)
|
||||
AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 天翼云(预留)
|
||||
AccessProviderTypeDeSEC = AccessProviderType("desec")
|
||||
AccessProviderTypeDNSLA = AccessProviderType("dnsla")
|
||||
AccessProviderTypeDogeCloud = AccessProviderType("dogecloud")
|
||||
AccessProviderTypeDynv6 = AccessProviderType("dynv6")
|
||||
AccessProviderTypeEdgio = AccessProviderType("edgio")
|
||||
AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留)
|
||||
AccessProviderTypeGname = AccessProviderType("gname")
|
||||
AccessProviderTypeGcore = AccessProviderType("gcore")
|
||||
AccessProviderTypeGoDaddy = AccessProviderType("godaddy")
|
||||
AccessProviderTypeGoEdge = AccessProviderType("goedge") // GoEdge(预留)
|
||||
AccessProviderTypeGoogleTrustServices = AccessProviderType("googletrustservices")
|
||||
AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud")
|
||||
AccessProviderTypeJDCloud = AccessProviderType("jdcloud")
|
||||
AccessProviderTypeKubernetes = AccessProviderType("k8s")
|
||||
AccessProviderTypeLetsEncrypt = AccessProviderType("letsencrypt")
|
||||
AccessProviderTypeLetsEncryptStaging = AccessProviderType("letsencryptstaging")
|
||||
AccessProviderTypeLocal = AccessProviderType("local")
|
||||
AccessProviderTypeNamecheap = AccessProviderType("namecheap")
|
||||
AccessProviderTypeNameDotCom = AccessProviderType("namedotcom")
|
||||
AccessProviderTypeNameSilo = AccessProviderType("namesilo")
|
||||
AccessProviderTypeNS1 = AccessProviderType("ns1")
|
||||
AccessProviderTypePorkbun = AccessProviderType("porkbun")
|
||||
AccessProviderTypePowerDNS = AccessProviderType("powerdns")
|
||||
AccessProviderTypeQiniu = AccessProviderType("qiniu")
|
||||
AccessProviderTypeQingCloud = AccessProviderType("qingcloud") // 青云(预留)
|
||||
AccessProviderTypeRainYun = AccessProviderType("rainyun")
|
||||
AccessProviderTypeSafeLine = AccessProviderType("safeline")
|
||||
AccessProviderTypeSSH = AccessProviderType("ssh")
|
||||
AccessProviderTypeSSLCOM = AccessProviderType("sslcom")
|
||||
AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud")
|
||||
AccessProviderTypeUCloud = AccessProviderType("ucloud")
|
||||
AccessProviderTypeUpyun = AccessProviderType("upyun")
|
||||
AccessProviderTypeVercel = AccessProviderType("vercel")
|
||||
AccessProviderTypeVolcEngine = AccessProviderType("volcengine")
|
||||
AccessProviderTypeWangsu = AccessProviderType("wangsu")
|
||||
AccessProviderTypeWebhook = AccessProviderType("webhook")
|
||||
AccessProviderTypeWestcn = AccessProviderType("westcn")
|
||||
AccessProviderTypeZeroSSL = AccessProviderType("zerossl")
|
||||
)
|
||||
|
||||
type ApplyCAProviderType string
|
||||
|
||||
/*
|
||||
申请证书 CA 提供商常量值。
|
||||
始终等于授权提供商类型。
|
||||
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new constant, please keep ASCII order.
|
||||
*/
|
||||
const (
|
||||
ApplyCAProviderTypeBuypass = ApplyCAProviderType(string(AccessProviderTypeBuypass))
|
||||
ApplyCAProviderTypeGoogleTrustServices = ApplyCAProviderType(string(AccessProviderTypeGoogleTrustServices))
|
||||
ApplyCAProviderTypeLetsEncrypt = ApplyCAProviderType(string(AccessProviderTypeLetsEncrypt))
|
||||
ApplyCAProviderTypeLetsEncryptStaging = ApplyCAProviderType(string(AccessProviderTypeLetsEncryptStaging))
|
||||
ApplyCAProviderTypeSSLCom = ApplyCAProviderType(string(AccessProviderTypeSSLCOM))
|
||||
ApplyCAProviderTypeZeroSSL = ApplyCAProviderType(string(AccessProviderTypeZeroSSL))
|
||||
)
|
||||
|
||||
type ApplyDNSProviderType string
|
||||
@@ -111,7 +136,7 @@ const (
|
||||
type DeployProviderType string
|
||||
|
||||
/*
|
||||
部署目标提供商常量值。
|
||||
部署证书主机提供商常量值。
|
||||
短横线前的部分始终等于授权提供商类型。
|
||||
|
||||
注意:如果追加新的常量值,请保持以 ASCII 排序。
|
||||
@@ -162,6 +187,7 @@ const (
|
||||
DeployProviderTypeQiniuCDN = DeployProviderType("qiniu-cdn")
|
||||
DeployProviderTypeQiniuKodo = DeployProviderType("qiniu-kodo")
|
||||
DeployProviderTypeQiniuPili = DeployProviderType("qiniu-pili")
|
||||
DeployProviderTypeRainYunRCDN = DeployProviderType("rainyun-rcdn")
|
||||
DeployProviderTypeSafeLine = DeployProviderType("safeline")
|
||||
DeployProviderTypeSSH = DeployProviderType("ssh")
|
||||
DeployProviderTypeTencentCloudCDN = DeployProviderType("tencentcloud-cdn")
|
||||
@@ -187,5 +213,6 @@ const (
|
||||
DeployProviderTypeVolcEngineImageX = DeployProviderType("volcengine-imagex")
|
||||
DeployProviderTypeVolcEngineLive = DeployProviderType("volcengine-live")
|
||||
DeployProviderTypeVolcEngineTOS = DeployProviderType("volcengine-tos")
|
||||
DeployProviderTypeWangsuCDNPro = DeployProviderType("wangsu-cdnpro")
|
||||
DeployProviderTypeWebhook = DeployProviderType("webhook")
|
||||
)
|
||||
|
||||
@@ -62,19 +62,22 @@ type WorkflowNode struct {
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForApply struct {
|
||||
Domains string `json:"domains"` // 域名列表,以半角分号分隔
|
||||
ContactEmail string `json:"contactEmail"` // 联系邮箱
|
||||
ChallengeType string `json:"challengeType"` // TODO: 验证方式。目前仅支持 dns-01
|
||||
Provider string `json:"provider"` // DNS 提供商
|
||||
ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID
|
||||
ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置
|
||||
KeyAlgorithm string `json:"keyAlgorithm"` // 密钥算法
|
||||
Nameservers string `json:"nameservers"` // DNS 服务器列表,以半角分号分隔
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout"` // DNS 传播超时时间(零值取决于提供商的默认值)
|
||||
DnsTTL int32 `json:"dnsTTL"` // DNS TTL(零值取决于提供商的默认值)
|
||||
DisableFollowCNAME bool `json:"disableFollowCNAME"` // 是否关闭 CNAME 跟随
|
||||
DisableARI bool `json:"disableARI"` // 是否关闭 ARI
|
||||
SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays"` // 证书到期前多少天前跳过续期(零值将使用默认值 30)
|
||||
Domains string `json:"domains"` // 域名列表,以半角分号分隔
|
||||
ContactEmail string `json:"contactEmail"` // 联系邮箱
|
||||
ChallengeType string `json:"challengeType"` // TODO: 验证方式。目前仅支持 dns-01
|
||||
Provider string `json:"provider"` // DNS 提供商
|
||||
ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID
|
||||
ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置
|
||||
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"` // 密钥算法
|
||||
Nameservers string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` // DNS 传播超时时间(零值取决于提供商的默认值)
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"` // DNS TTL(零值取决于提供商的默认值)
|
||||
DisableFollowCNAME bool `json:"disableFollowCNAME,omitempty"` // 是否关闭 CNAME 跟随
|
||||
DisableARI bool `json:"disableARI,omitempty"` // 是否关闭 ARI
|
||||
SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值将使用默认值 30)
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForUpload struct {
|
||||
@@ -84,11 +87,11 @@ type WorkflowNodeConfigForUpload struct {
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForDeploy struct {
|
||||
Certificate string `json:"certificate"` // 前序节点输出的证书,形如“${NodeId}#certificate”
|
||||
Provider string `json:"provider"` // 主机提供商
|
||||
ProviderAccessId string `json:"providerAccessId"` // 主机提供商授权记录 ID
|
||||
ProviderConfig map[string]any `json:"providerConfig"` // 主机提供商额外配置
|
||||
SkipOnLastSucceeded bool `json:"skipOnLastSucceeded"` // 上次部署成功时是否跳过
|
||||
Certificate string `json:"certificate"` // 前序节点输出的证书,形如“${NodeId}#certificate”
|
||||
Provider string `json:"provider"` // 主机提供商
|
||||
ProviderAccessId string `json:"providerAccessId,omitempty"` // 主机提供商授权记录 ID
|
||||
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 主机提供商额外配置
|
||||
SkipOnLastSucceeded bool `json:"skipOnLastSucceeded"` // 上次部署成功时是否跳过
|
||||
}
|
||||
|
||||
type WorkflowNodeConfigForNotify struct {
|
||||
@@ -97,73 +100,54 @@ type WorkflowNodeConfigForNotify struct {
|
||||
Message string `json:"message"` // 通知内容
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) getConfigString(key string) string {
|
||||
return maputil.GetString(n.Config, key)
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) getConfigBool(key string) bool {
|
||||
return maputil.GetBool(n.Config, key)
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) getConfigInt32(key string) int32 {
|
||||
return maputil.GetInt32(n.Config, key)
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) getConfigMap(key string) map[string]any {
|
||||
if val, ok := n.Config[key]; ok {
|
||||
if result, ok := val.(map[string]any); ok {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return make(map[string]any)
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
|
||||
skipBeforeExpiryDays := n.getConfigInt32("skipBeforeExpiryDays")
|
||||
skipBeforeExpiryDays := maputil.GetInt32(n.Config, "skipBeforeExpiryDays")
|
||||
if skipBeforeExpiryDays == 0 {
|
||||
skipBeforeExpiryDays = 30
|
||||
}
|
||||
|
||||
return WorkflowNodeConfigForApply{
|
||||
Domains: n.getConfigString("domains"),
|
||||
ContactEmail: n.getConfigString("contactEmail"),
|
||||
Provider: n.getConfigString("provider"),
|
||||
ProviderAccessId: n.getConfigString("providerAccessId"),
|
||||
ProviderConfig: n.getConfigMap("providerConfig"),
|
||||
KeyAlgorithm: n.getConfigString("keyAlgorithm"),
|
||||
Nameservers: n.getConfigString("nameservers"),
|
||||
DnsPropagationTimeout: n.getConfigInt32("dnsPropagationTimeout"),
|
||||
DnsTTL: n.getConfigInt32("dnsTTL"),
|
||||
DisableFollowCNAME: n.getConfigBool("disableFollowCNAME"),
|
||||
DisableARI: n.getConfigBool("disableARI"),
|
||||
Domains: maputil.GetString(n.Config, "domains"),
|
||||
ContactEmail: maputil.GetString(n.Config, "contactEmail"),
|
||||
Provider: maputil.GetString(n.Config, "provider"),
|
||||
ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"),
|
||||
ProviderConfig: maputil.GetAnyMap(n.Config, "providerConfig"),
|
||||
CAProvider: maputil.GetString(n.Config, "caProvider"),
|
||||
CAProviderAccessId: maputil.GetString(n.Config, "caProviderAccessId"),
|
||||
CAProviderConfig: maputil.GetAnyMap(n.Config, "caProviderConfig"),
|
||||
KeyAlgorithm: maputil.GetString(n.Config, "keyAlgorithm"),
|
||||
Nameservers: maputil.GetString(n.Config, "nameservers"),
|
||||
DnsPropagationTimeout: maputil.GetInt32(n.Config, "dnsPropagationTimeout"),
|
||||
DnsTTL: maputil.GetInt32(n.Config, "dnsTTL"),
|
||||
DisableFollowCNAME: maputil.GetBool(n.Config, "disableFollowCNAME"),
|
||||
DisableARI: maputil.GetBool(n.Config, "disableARI"),
|
||||
SkipBeforeExpiryDays: skipBeforeExpiryDays,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForUpload() WorkflowNodeConfigForUpload {
|
||||
return WorkflowNodeConfigForUpload{
|
||||
Certificate: n.getConfigString("certificate"),
|
||||
PrivateKey: n.getConfigString("privateKey"),
|
||||
Domains: n.getConfigString("domains"),
|
||||
Certificate: maputil.GetString(n.Config, "certificate"),
|
||||
PrivateKey: maputil.GetString(n.Config, "privateKey"),
|
||||
Domains: maputil.GetString(n.Config, "domains"),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy {
|
||||
return WorkflowNodeConfigForDeploy{
|
||||
Certificate: n.getConfigString("certificate"),
|
||||
Provider: n.getConfigString("provider"),
|
||||
ProviderAccessId: n.getConfigString("providerAccessId"),
|
||||
ProviderConfig: n.getConfigMap("providerConfig"),
|
||||
SkipOnLastSucceeded: n.getConfigBool("skipOnLastSucceeded"),
|
||||
Certificate: maputil.GetString(n.Config, "certificate"),
|
||||
Provider: maputil.GetString(n.Config, "provider"),
|
||||
ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"),
|
||||
ProviderConfig: maputil.GetAnyMap(n.Config, "providerConfig"),
|
||||
SkipOnLastSucceeded: maputil.GetBool(n.Config, "skipOnLastSucceeded"),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify {
|
||||
return WorkflowNodeConfigForNotify{
|
||||
Channel: n.getConfigString("channel"),
|
||||
Subject: n.getConfigString("subject"),
|
||||
Message: n.getConfigString("message"),
|
||||
Channel: maputil.GetString(n.Config, "channel"),
|
||||
Subject: maputil.GetString(n.Config, "subject"),
|
||||
Message: maputil.GetString(n.Config, "message"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@ import (
|
||||
pBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
|
||||
pDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
|
||||
pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
|
||||
pGotify "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify"
|
||||
pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
|
||||
pMattermost "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost"
|
||||
pPushover "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover"
|
||||
pPushPlus "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus"
|
||||
pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
|
||||
pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
|
||||
pWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
|
||||
@@ -45,11 +49,36 @@ func createNotifier(channel domain.NotifyChannelType, channelConfig map[string]a
|
||||
ReceiverAddress: maputil.GetString(channelConfig, "receiverAddress"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeGotify:
|
||||
return pGotify.NewNotifier(&pGotify.NotifierConfig{
|
||||
Url: maputil.GetString(channelConfig, "url"),
|
||||
Token: maputil.GetString(channelConfig, "token"),
|
||||
Priority: maputil.GetOrDefaultInt64(channelConfig, "priority", 1),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeLark:
|
||||
return pLark.NewNotifier(&pLark.NotifierConfig{
|
||||
WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeMattermost:
|
||||
return pMattermost.NewNotifier(&pMattermost.NotifierConfig{
|
||||
ServerUrl: maputil.GetString(channelConfig, "serverUrl"),
|
||||
ChannelId: maputil.GetString(channelConfig, "channelId"),
|
||||
Username: maputil.GetString(channelConfig, "username"),
|
||||
Password: maputil.GetString(channelConfig, "password"),
|
||||
})
|
||||
case domain.NotifyChannelTypePushover:
|
||||
return pPushover.NewNotifier(&pPushover.NotifierConfig{
|
||||
Token: maputil.GetString(channelConfig, "token"),
|
||||
User: maputil.GetString(channelConfig, "user"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypePushPlus:
|
||||
return pPushPlus.NewNotifier(&pPushPlus.NotifierConfig{
|
||||
Token: maputil.GetString(channelConfig, "token"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTypeServerChan:
|
||||
return pServerChan.NewNotifier(&pServerChan.NotifierConfig{
|
||||
Url: maputil.GetString(channelConfig, "url"),
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
type ChallengeProviderConfig struct {
|
||||
DnsApiToken string `json:"dnsApiToken"`
|
||||
ZoneApiToken string `json:"zoneApiToken,omitempty"`
|
||||
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
|
||||
DnsTTL int32 `json:"dnsTTL,omitempty"`
|
||||
}
|
||||
@@ -20,6 +21,7 @@ func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider,
|
||||
|
||||
providerConfig := cloudflare.NewDefaultConfig()
|
||||
providerConfig.AuthToken = config.DnsApiToken
|
||||
providerConfig.ZoneToken = config.ZoneApiToken
|
||||
if config.DnsPropagationTimeout != 0 {
|
||||
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ func (d *DeployerProvider) deployToWAF3(ctx context.Context, certPem string, pri
|
||||
InstanceId: tea.String(d.config.InstanceId),
|
||||
RegionId: tea.String(d.config.Region),
|
||||
Domain: tea.String(d.config.Domain),
|
||||
Listen: &aliwaf.ModifyDomainRequestListen{CertId: tea.String(upres.CertId)},
|
||||
Listen: &aliwaf.ModifyDomainRequestListen{CertId: tea.String(upres.ExtendedData["certIdentifier"].(string))},
|
||||
Redirect: &aliwaf.ModifyDomainRequestRedirect{Loadbalance: tea.String("iphash")},
|
||||
}
|
||||
modifyDomainReq = assign(modifyDomainReq, describeDomainDetailResp.Body)
|
||||
|
||||
@@ -21,6 +21,9 @@ type DeployerConfig struct {
|
||||
ApiToken string `json:"apiToken"`
|
||||
// 加速域名(支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
// 证书 ID。
|
||||
// 选填。
|
||||
CertificateId string `json:"certificateId,omitempty"`
|
||||
}
|
||||
|
||||
type DeployerProvider struct {
|
||||
@@ -62,63 +65,79 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
|
||||
return nil, errors.New("config `domain` is required")
|
||||
}
|
||||
|
||||
// 查询域名配置
|
||||
// REF: https://portal.baishancloud.com/track/document/api/1/1065
|
||||
getDomainConfigReq := &bssdk.GetDomainConfigRequest{
|
||||
Domains: d.config.Domain,
|
||||
Config: []string{"https"},
|
||||
}
|
||||
getDomainConfigResp, err := d.sdkClient.GetDomainConfig(getDomainConfigReq)
|
||||
d.logger.Debug("sdk request 'baishan.GetDomainConfig'", slog.Any("request", getDomainConfigReq), slog.Any("response", getDomainConfigResp))
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'baishan.GetDomainConfig'")
|
||||
} else if len(getDomainConfigResp.Data) == 0 {
|
||||
return nil, errors.New("domain config not found")
|
||||
}
|
||||
|
||||
// 新增证书
|
||||
// REF: https://portal.baishancloud.com/track/document/downloadPdf/1441
|
||||
certificateId := ""
|
||||
createCertificateReq := &bssdk.CreateCertificateRequest{
|
||||
Certificate: certPem,
|
||||
Key: privkeyPem,
|
||||
Name: fmt.Sprintf("certimate_%d", time.Now().UnixMilli()),
|
||||
}
|
||||
createCertificateResp, err := d.sdkClient.CreateCertificate(createCertificateReq)
|
||||
d.logger.Debug("sdk request 'baishan.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
|
||||
if err != nil {
|
||||
if createCertificateResp != nil {
|
||||
if createCertificateResp.GetCode() == 400699 && strings.Contains(createCertificateResp.GetMessage(), "this certificate is exists") {
|
||||
// 证书已存在,忽略新增证书接口错误
|
||||
re := regexp.MustCompile(`\d+`)
|
||||
certificateId = re.FindString(createCertificateResp.GetMessage())
|
||||
if d.config.CertificateId == "" {
|
||||
// 新增证书
|
||||
// REF: https://portal.baishancloud.com/track/document/downloadPdf/1441
|
||||
certificateId := ""
|
||||
createCertificateReq := &bssdk.CreateCertificateRequest{
|
||||
Certificate: certPem,
|
||||
Key: privkeyPem,
|
||||
Name: fmt.Sprintf("certimate_%d", time.Now().UnixMilli()),
|
||||
}
|
||||
createCertificateResp, err := d.sdkClient.CreateCertificate(createCertificateReq)
|
||||
d.logger.Debug("sdk request 'baishan.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
|
||||
if err != nil {
|
||||
if createCertificateResp != nil {
|
||||
if createCertificateResp.GetCode() == 400699 && strings.Contains(createCertificateResp.GetMessage(), "this certificate is exists") {
|
||||
// 证书已存在,忽略新增证书接口错误
|
||||
re := regexp.MustCompile(`\d+`)
|
||||
certificateId = re.FindString(createCertificateResp.GetMessage())
|
||||
}
|
||||
}
|
||||
|
||||
if certificateId == "" {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'baishan.CreateCertificate'")
|
||||
}
|
||||
} else {
|
||||
certificateId = createCertificateResp.Data.CertId.String()
|
||||
}
|
||||
|
||||
if certificateId == "" {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'baishan.CreateCertificate'")
|
||||
// 查询域名配置
|
||||
// REF: https://portal.baishancloud.com/track/document/api/1/1065
|
||||
getDomainConfigReq := &bssdk.GetDomainConfigRequest{
|
||||
Domains: d.config.Domain,
|
||||
Config: []string{"https"},
|
||||
}
|
||||
getDomainConfigResp, err := d.sdkClient.GetDomainConfig(getDomainConfigReq)
|
||||
d.logger.Debug("sdk request 'baishan.GetDomainConfig'", slog.Any("request", getDomainConfigReq), slog.Any("response", getDomainConfigResp))
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'baishan.GetDomainConfig'")
|
||||
} else if len(getDomainConfigResp.Data) == 0 {
|
||||
return nil, errors.New("domain config not found")
|
||||
}
|
||||
|
||||
// 设置域名配置
|
||||
// REF: https://portal.baishancloud.com/track/document/api/1/1045
|
||||
setDomainConfigReq := &bssdk.SetDomainConfigRequest{
|
||||
Domains: d.config.Domain,
|
||||
Config: &bssdk.DomainConfig{
|
||||
Https: &bssdk.DomainConfigHttps{
|
||||
CertId: json.Number(certificateId),
|
||||
ForceHttps: getDomainConfigResp.Data[0].Config.Https.ForceHttps,
|
||||
EnableHttp2: getDomainConfigResp.Data[0].Config.Https.EnableHttp2,
|
||||
EnableOcsp: getDomainConfigResp.Data[0].Config.Https.EnableOcsp,
|
||||
},
|
||||
},
|
||||
}
|
||||
setDomainConfigResp, err := d.sdkClient.SetDomainConfig(setDomainConfigReq)
|
||||
d.logger.Debug("sdk request 'baishan.SetDomainConfig'", slog.Any("request", setDomainConfigReq), slog.Any("response", setDomainConfigResp))
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'baishan.SetDomainConfig'")
|
||||
}
|
||||
} else {
|
||||
certificateId = createCertificateResp.Data.CertId.String()
|
||||
}
|
||||
|
||||
// 设置域名配置
|
||||
// REF: https://portal.baishancloud.com/track/document/api/1/1045
|
||||
setDomainConfigReq := &bssdk.SetDomainConfigRequest{
|
||||
Domains: d.config.Domain,
|
||||
Config: &bssdk.DomainConfig{
|
||||
Https: &bssdk.DomainConfigHttps{
|
||||
CertId: json.Number(certificateId),
|
||||
ForceHttps: getDomainConfigResp.Data[0].Config.Https.ForceHttps,
|
||||
EnableHttp2: getDomainConfigResp.Data[0].Config.Https.EnableHttp2,
|
||||
EnableOcsp: getDomainConfigResp.Data[0].Config.Https.EnableOcsp,
|
||||
},
|
||||
},
|
||||
}
|
||||
setDomainConfigResp, err := d.sdkClient.SetDomainConfig(setDomainConfigReq)
|
||||
d.logger.Debug("sdk request 'baishan.SetDomainConfig'", slog.Any("request", setDomainConfigReq), slog.Any("response", setDomainConfigResp))
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'baishan.SetDomainConfig'")
|
||||
// 替换证书
|
||||
// REF: https://portal.baishancloud.com/track/document/downloadPdf/1441
|
||||
createCertificateReq := &bssdk.CreateCertificateRequest{
|
||||
CertificateId: &d.config.CertificateId,
|
||||
Certificate: certPem,
|
||||
Key: privkeyPem,
|
||||
Name: fmt.Sprintf("certimate_%d", time.Now().UnixMilli()),
|
||||
}
|
||||
createCertificateResp, err := d.sdkClient.CreateCertificate(createCertificateReq)
|
||||
d.logger.Debug("sdk request 'baishan.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'baishan.CreateCertificate'")
|
||||
}
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
edgio "github.com/Edgio/edgio-api/applications/v7"
|
||||
edgiodtos "github.com/Edgio/edgio-api/applications/v7/dtos"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/certutil"
|
||||
edgsdk "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7"
|
||||
edgsdkdtos "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos"
|
||||
)
|
||||
|
||||
type DeployerConfig struct {
|
||||
@@ -24,7 +24,7 @@ type DeployerConfig struct {
|
||||
type DeployerProvider struct {
|
||||
config *DeployerConfig
|
||||
logger *slog.Logger
|
||||
sdkClient *edgsdk.EdgioClient
|
||||
sdkClient *edgio.EdgioClient
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*DeployerProvider)(nil)
|
||||
@@ -64,7 +64,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
|
||||
|
||||
// 上传 TLS 证书
|
||||
// REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts
|
||||
uploadTlsCertReq := edgsdkdtos.UploadTlsCertRequest{
|
||||
uploadTlsCertReq := edgiodtos.UploadTlsCertRequest{
|
||||
EnvironmentID: d.config.EnvironmentId,
|
||||
PrimaryCert: privateCertPem,
|
||||
IntermediateCert: intermediateCertPem,
|
||||
@@ -79,7 +79,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClient(clientId, clientSecret string) (*edgsdk.EdgioClient, error) {
|
||||
client := edgsdk.NewEdgioClient(clientId, clientSecret, "", "")
|
||||
func createSdkClient(clientId, clientSecret string) (*edgio.EdgioClient, error) {
|
||||
client := edgio.NewEdgioClient(clientId, clientSecret, "", "")
|
||||
return client, nil
|
||||
}
|
||||
|
||||
@@ -100,9 +100,15 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
|
||||
SSlEnabled: true,
|
||||
SSLData: int(updateResourceCertId),
|
||||
ProxySSLEnabled: getResourceResp.ProxySSLEnabled,
|
||||
ProxySSLCA: &getResourceResp.ProxySSLCA,
|
||||
ProxySSLData: &getResourceResp.ProxySSLData,
|
||||
Options: getResourceResp.Options,
|
||||
}
|
||||
if getResourceResp.ProxySSLCA != 0 {
|
||||
updateResourceReq.ProxySSLCA = &getResourceResp.ProxySSLCA
|
||||
}
|
||||
if getResourceResp.ProxySSLData != 0 {
|
||||
updateResourceReq.ProxySSLData = &getResourceResp.ProxySSLData
|
||||
}
|
||||
if getResourceResp.Options != nil {
|
||||
updateResourceReq.Options = getResourceResp.Options
|
||||
}
|
||||
updateResourceResp, err := d.sdkClient.Update(context.TODO(), d.config.ResourceId, updateResourceReq)
|
||||
d.logger.Debug("sdk request 'resources.Update'", slog.Int64("resourceId", d.config.ResourceId), slog.Any("request", updateResourceReq), slog.Any("response", updateResourceResp))
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package rainyunrcdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/rainyun-sslcenter"
|
||||
rainyunsdk "github.com/usual2970/certimate/internal/pkg/vendors/rainyun-sdk"
|
||||
)
|
||||
|
||||
type DeployerConfig struct {
|
||||
// 雨云 API 密钥。
|
||||
ApiKey string `json:"apiKey"`
|
||||
// RCDN 实例 ID。
|
||||
InstanceId int32 `json:"instanceId"`
|
||||
// 加速域名(支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type DeployerProvider struct {
|
||||
config *DeployerConfig
|
||||
logger *slog.Logger
|
||||
sdkClient *rainyunsdk.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*DeployerProvider)(nil)
|
||||
|
||||
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.ApiKey)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
|
||||
ApiKey: config.ApiKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &DeployerProvider{
|
||||
config: config,
|
||||
logger: slog.Default(),
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
|
||||
if logger == nil {
|
||||
d.logger = slog.Default()
|
||||
} else {
|
||||
d.logger = logger
|
||||
}
|
||||
d.sslUploader.WithLogger(logger)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 SSL 证书
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
} else {
|
||||
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
|
||||
}
|
||||
|
||||
// RCDN SSL 绑定域名
|
||||
// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-184214120
|
||||
certId, _ := strconv.Atoi(upres.CertId)
|
||||
rcdnInstanceSslBindReq := &rainyunsdk.RcdnInstanceSslBindRequest{
|
||||
CertId: int32(certId),
|
||||
Domains: []string{d.config.Domain},
|
||||
}
|
||||
rcdnInstanceSslBindResp, err := d.sdkClient.RcdnInstanceSslBind(d.config.InstanceId, rcdnInstanceSslBindReq)
|
||||
d.logger.Debug("sdk request 'rcdn.InstanceSslBind'", slog.Any("instanceId", d.config.InstanceId), slog.Any("request", rcdnInstanceSslBindReq), slog.Any("response", rcdnInstanceSslBindResp))
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'rcdn.InstanceSslBind'")
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClient(apiKey string) (*rainyunsdk.Client, error) {
|
||||
if apiKey == "" {
|
||||
return nil, errors.New("invalid rainyun api key")
|
||||
}
|
||||
|
||||
client := rainyunsdk.NewClient(apiKey)
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package rainyunrcdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/rainyun-rcdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fApiKey string
|
||||
fInstanceId int64
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_RAINYUNRCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
|
||||
flag.Int64Var(&fInstanceId, argsPrefix+"INSTANCEID", 0, "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v ./ucloud_ucdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_RAINYUNRCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_RAINYUNRCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_RAINYUNRCDN_APIKEY="your-api-key" \
|
||||
--CERTIMATE_DEPLOYER_RAINYUNRCDN_INSTANCEID="your-rcdn-instance-id" \
|
||||
--CERTIMATE_DEPLOYER_RAINYUNRCDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("APIKEY: %v", fApiKey),
|
||||
fmt.Sprintf("INSTANCEID: %v", fInstanceId),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
|
||||
PrivateKey: fApiKey,
|
||||
InstanceId: fInstanceId,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -107,7 +107,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
|
||||
// REF: https://cloud.tencent.com/document/product/400/91667
|
||||
deployCertificateInstanceReq := tcssl.NewDeployCertificateInstanceRequest()
|
||||
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
|
||||
deployCertificateInstanceReq.ResourceType = common.StringPtr("ecdn")
|
||||
deployCertificateInstanceReq.ResourceType = common.StringPtr("cdn")
|
||||
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
|
||||
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(instanceIds)
|
||||
deployCertificateInstanceResp, err := d.sdkClients.SSL.DeployCertificateInstance(deployCertificateInstanceReq)
|
||||
|
||||
@@ -182,7 +182,7 @@ func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId str
|
||||
return errors.New("config `listenerId` is required")
|
||||
}
|
||||
|
||||
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, cloudCertId); err != nil {
|
||||
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId str
|
||||
return errors.New("config `listenerId` is required")
|
||||
}
|
||||
|
||||
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, cloudCertId); err != nil {
|
||||
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
package wangsucdnpro
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/certutil"
|
||||
wangsucdn "github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/cdn"
|
||||
)
|
||||
|
||||
type DeployerConfig struct {
|
||||
// 网宿云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 网宿云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 网宿云环境。
|
||||
Environment string `json:"environment"`
|
||||
// 加速域名(支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
// 证书 ID。
|
||||
// 选填。
|
||||
CertificateId string `json:"certificateId,omitempty"`
|
||||
// Webhook ID。
|
||||
// 选填。
|
||||
WebhookId string `json:"webhookId,omitempty"`
|
||||
}
|
||||
|
||||
type DeployerProvider struct {
|
||||
config *DeployerConfig
|
||||
logger *slog.Logger
|
||||
sdkClient *wangsucdn.Client
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*DeployerProvider)(nil)
|
||||
|
||||
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
return &DeployerProvider{
|
||||
config: config,
|
||||
logger: slog.Default(),
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
|
||||
if logger == nil {
|
||||
d.logger = slog.Default()
|
||||
} else {
|
||||
d.logger = logger
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
if d.config.Domain == "" {
|
||||
return nil, errors.New("config `domain` is required")
|
||||
}
|
||||
|
||||
// 解析证书内容
|
||||
certX509, err := certutil.ParseCertificateFromPEM(certPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询已部署加速域名的详情
|
||||
getHostnameDetailResp, err := d.sdkClient.GetHostnameDetail(d.config.Domain)
|
||||
d.logger.Debug("sdk request 'cdn.GetHostnameDetail'", slog.String("hostname", d.config.Domain), slog.Any("response", getHostnameDetailResp))
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetHostnameDetail'")
|
||||
}
|
||||
|
||||
// 生成网宿云证书参数
|
||||
encryptedPrivateKey, err := encryptPrivateKey(privkeyPem, d.config.AccessKeySecret, time.Now().Unix())
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to encrypt private key")
|
||||
}
|
||||
certificateNewVersionInfo := &wangsucdn.CertificateVersion{
|
||||
PrivateKey: tea.String(encryptedPrivateKey),
|
||||
Certificate: tea.String(certPem),
|
||||
IdentificationInfo: &wangsucdn.CertificateVersionIdentificationInfo{
|
||||
CommonName: tea.String(certX509.Subject.CommonName),
|
||||
SubjectAlternativeNames: &certX509.DNSNames,
|
||||
},
|
||||
}
|
||||
|
||||
// 网宿云证书 URL 中包含证书 ID 及版本号
|
||||
// 格式:
|
||||
// http://open.chinanetcenter.com/cdn/certificates/5dca2205f9e9cc0001df7b33
|
||||
// http://open.chinanetcenter.com/cdn/certificates/329f12c1fe6708c23c31e91f/versions/5
|
||||
var wangsuCertUrl string
|
||||
var wangsuCertId, wangsuCertVer string
|
||||
|
||||
// 如果原证书 ID 为空,则创建证书;否则更新证书。
|
||||
timestamp := time.Now().Unix()
|
||||
if d.config.CertificateId == "" {
|
||||
// 创建证书
|
||||
createCertificateReq := &wangsucdn.CreateCertificateRequest{
|
||||
Timestamp: timestamp,
|
||||
Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
|
||||
AutoRenew: tea.String("Off"),
|
||||
NewVersion: certificateNewVersionInfo,
|
||||
}
|
||||
createCertificateResp, err := d.sdkClient.CreateCertificate(createCertificateReq)
|
||||
d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.CreateCertificate'")
|
||||
}
|
||||
|
||||
wangsuCertUrl = createCertificateResp.CertificateUrl
|
||||
d.logger.Info("ssl certificate uploaded", slog.Any("certUrl", wangsuCertUrl))
|
||||
|
||||
wangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl)
|
||||
if len(wangsuCertIdMatches) > 1 {
|
||||
wangsuCertId = wangsuCertIdMatches[1]
|
||||
}
|
||||
|
||||
wangsuCertVer = "1"
|
||||
} else {
|
||||
// 更新证书
|
||||
updateCertificateReq := &wangsucdn.UpdateCertificateRequest{
|
||||
Timestamp: timestamp,
|
||||
Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
|
||||
AutoRenew: tea.String("Off"),
|
||||
NewVersion: certificateNewVersionInfo,
|
||||
}
|
||||
updateCertificateResp, err := d.sdkClient.UpdateCertificate(d.config.CertificateId, updateCertificateReq)
|
||||
d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("certificateId", d.config.CertificateId), slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp))
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UpdateCertificate'")
|
||||
}
|
||||
|
||||
wangsuCertUrl = updateCertificateResp.CertificateUrl
|
||||
d.logger.Info("ssl certificate uploaded", slog.Any("certUrl", wangsuCertUrl))
|
||||
|
||||
wangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl)
|
||||
if len(wangsuCertIdMatches) > 1 {
|
||||
wangsuCertId = wangsuCertIdMatches[1]
|
||||
}
|
||||
|
||||
wangsuCertVerMatches := regexp.MustCompile(`/versions/(\d+)`).FindStringSubmatch(wangsuCertUrl)
|
||||
if len(wangsuCertVerMatches) > 1 {
|
||||
wangsuCertVer = wangsuCertVerMatches[1]
|
||||
}
|
||||
}
|
||||
|
||||
// 创建部署任务
|
||||
// REF: https://www.wangsu.com/document/api-doc/27034
|
||||
createDeploymentTaskReq := &wangsucdn.CreateDeploymentTaskRequest{
|
||||
Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
|
||||
Target: tea.String(d.config.Environment),
|
||||
Actions: &[]wangsucdn.DeploymentTaskAction{
|
||||
{
|
||||
Action: tea.String("deploy_cert"),
|
||||
CertificateId: tea.String(wangsuCertId),
|
||||
Version: tea.String(wangsuCertVer),
|
||||
},
|
||||
},
|
||||
}
|
||||
if d.config.WebhookId != "" {
|
||||
createDeploymentTaskReq.Webhook = tea.String(d.config.WebhookId)
|
||||
}
|
||||
createDeploymentTaskResp, err := d.sdkClient.CreateDeploymentTask(createDeploymentTaskReq)
|
||||
d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("request", createDeploymentTaskReq), slog.Any("response", createDeploymentTaskResp))
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.CreateDeploymentTask'")
|
||||
}
|
||||
|
||||
// 循环获取部署任务详细信息,等待任务状态变更
|
||||
// REF: https://www.wangsu.com/document/api-doc/27038
|
||||
var wangsuTaskId string
|
||||
wangsuTaskMatches := regexp.MustCompile(`/deploymentTasks/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl)
|
||||
if len(wangsuTaskMatches) > 1 {
|
||||
wangsuTaskId = wangsuTaskMatches[1]
|
||||
}
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
getDeploymentTaskDetailResp, err := d.sdkClient.GetDeploymentTaskDetail(wangsuTaskId)
|
||||
d.logger.Debug("sdk request 'cdn.GetDeploymentTaskDetail'", slog.Any("taskId", wangsuTaskId), slog.Any("response", getDeploymentTaskDetailResp))
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDeploymentTaskDetail'")
|
||||
}
|
||||
|
||||
if getDeploymentTaskDetailResp.Status == "failed" {
|
||||
return nil, errors.New("unexpected deployment task status")
|
||||
} else if getDeploymentTaskDetailResp.Status == "succeeded" {
|
||||
break
|
||||
}
|
||||
|
||||
d.logger.Info("waiting for deployment task completion ...")
|
||||
time.Sleep(time.Second * 15)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, accessKeySecret string) (*wangsucdn.Client, error) {
|
||||
if accessKeyId == "" {
|
||||
return nil, errors.New("invalid wangsu access key id")
|
||||
}
|
||||
|
||||
if accessKeySecret == "" {
|
||||
return nil, errors.New("invalid wangsu access key secret")
|
||||
}
|
||||
|
||||
return wangsucdn.NewClient(accessKeyId, accessKeySecret), nil
|
||||
}
|
||||
|
||||
func encryptPrivateKey(privkeyPem string, secretKey string, timestamp int64) (string, error) {
|
||||
date := time.Unix(timestamp, 0).UTC()
|
||||
dateStr := date.Format("Mon, 02 Jan 2006 15:04:05 GMT")
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(secretKey))
|
||||
mac.Write([]byte(dateStr))
|
||||
aesivkey := mac.Sum(nil)
|
||||
aesivkeyHex := hex.EncodeToString(aesivkey)
|
||||
|
||||
if len(aesivkeyHex) != 64 {
|
||||
return "", fmt.Errorf("invalid hmac length: %d", len(aesivkeyHex))
|
||||
}
|
||||
ivHex := aesivkeyHex[:32]
|
||||
keyHex := aesivkeyHex[32:64]
|
||||
|
||||
iv, err := hex.DecodeString(ivHex)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode iv: %w", err)
|
||||
}
|
||||
|
||||
key, err := hex.DecodeString(keyHex)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode key: %w", err)
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
plainBytes := []byte(privkeyPem)
|
||||
padlen := aes.BlockSize - len(plainBytes)%aes.BlockSize
|
||||
if padlen > 0 {
|
||||
paddata := bytes.Repeat([]byte{byte(padlen)}, padlen)
|
||||
plainBytes = append(plainBytes, paddata...)
|
||||
}
|
||||
|
||||
encBytes := make([]byte, len(plainBytes))
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
mode.CryptBlocks(encBytes, plainBytes)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(encBytes), nil
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package wangsucdnpro_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-cdnpro"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fAccessKeySecret string
|
||||
fEnvironment string
|
||||
fDomain string
|
||||
fCertificateId string
|
||||
fWebhookId string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_WANGSUCDNPRO_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
|
||||
flag.StringVar(&fEnvironment, argsPrefix+"ENVIRONMENT", "production", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "")
|
||||
flag.StringVar(&fWebhookId, argsPrefix+"WEBHOOKID", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v ./wangsu_cdnpro_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_ACCESSKEYSECRET="your-access-key-secret" \
|
||||
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_ENVIRONMENT="production" \
|
||||
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_DOMAIN="example.com" \
|
||||
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_CERTIFICATEID="your-certificate-id"\
|
||||
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_WEBHOOKID="your-webhook-id"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("ENVIRONMENT: %v", fEnvironment),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
|
||||
fmt.Sprintf("WEBHOOKID: %v", fWebhookId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Environment: fEnvironment,
|
||||
Domain: fDomain,
|
||||
CertificateId: fCertificateId,
|
||||
WebhookId: fWebhookId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package notifier
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
104
internal/pkg/core/notifier/providers/gotify/gotify.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package gotify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||
)
|
||||
|
||||
type NotifierConfig struct {
|
||||
// Gotify 服务地址
|
||||
// 示例:https://gotify.example.com
|
||||
Url string `json:"url"`
|
||||
// Gotify Token
|
||||
Token string `json:"token"`
|
||||
// Gotify 消息优先级
|
||||
Priority int64 `json:"priority"`
|
||||
}
|
||||
|
||||
type NotifierProvider struct {
|
||||
config *NotifierConfig
|
||||
logger *slog.Logger
|
||||
// 未来将移除
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
var _ notifier.Notifier = (*NotifierProvider)(nil)
|
||||
|
||||
func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
return &NotifierProvider{
|
||||
config: config,
|
||||
httpClient: http.DefaultClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
|
||||
if logger == nil {
|
||||
n.logger = slog.Default()
|
||||
} else {
|
||||
n.logger = logger
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
|
||||
// Gotify 原生实现, notify 库没有实现, 等待合并
|
||||
reqBody := &struct {
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Priority int64 `json:"priority"`
|
||||
}{
|
||||
Title: subject,
|
||||
Message: message,
|
||||
Priority: n.config.Priority,
|
||||
}
|
||||
|
||||
// Make request
|
||||
body, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "encode message body")
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
fmt.Sprintf("%s/message", n.config.Url),
|
||||
bytes.NewReader(body),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create new request")
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", n.config.Token))
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
// Send request to gotify service
|
||||
resp, err := n.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "send request to gotify server")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read response and verify success
|
||||
result, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "read response")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("gotify returned status code %d: %s", resp.StatusCode, string(result))
|
||||
}
|
||||
return ¬ifier.NotifyResult{}, nil
|
||||
}
|
||||
68
internal/pkg/core/notifier/providers/gotify/gotify_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package gotify_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify"
|
||||
)
|
||||
|
||||
const (
|
||||
mockSubject = "test_subject"
|
||||
mockMessage = "test_message"
|
||||
)
|
||||
|
||||
var (
|
||||
fUrl string
|
||||
fToken string
|
||||
fPriority int64
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_NOTIFIER_GOTIFY_"
|
||||
flag.StringVar(&fUrl, argsPrefix+"URL", "", "")
|
||||
flag.StringVar(&fToken, argsPrefix+"TOKEN", "", "")
|
||||
flag.Int64Var(&fPriority, argsPrefix+"PRIORITY", 0, "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v ./gotify_test.go -args \
|
||||
--CERTIMATE_NOTIFIER_GOTIFY_URL="https://example.com" \
|
||||
--CERTIMATE_NOTIFIER_GOTIFY_TOKEN="your-gotify-application-token" \
|
||||
--CERTIMATE_NOTIFIER_GOTIFY_PRIORITY="your-message-priority"
|
||||
*/
|
||||
func TestNotify(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Notify", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("URL: %v", fUrl),
|
||||
fmt.Sprintf("TOKEN: %v", fToken),
|
||||
fmt.Sprintf("PRIORITY: %d", fPriority),
|
||||
}, "\n"))
|
||||
|
||||
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
|
||||
Url: fUrl,
|
||||
Token: fToken,
|
||||
Priority: fPriority,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package mattermost
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/nikoksr/notify/service/mattermost"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type NotifierConfig struct {
|
||||
// Mattermost 服务地址。
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
// 频道ID
|
||||
ChannelId string `json:"channelId"`
|
||||
// 用户名
|
||||
Username string `json:"username"`
|
||||
// 密码
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type NotifierProvider struct {
|
||||
config *NotifierConfig
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
var _ notifier.Notifier = (*NotifierProvider)(nil)
|
||||
|
||||
func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
return &NotifierProvider{
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
|
||||
if logger == nil {
|
||||
n.logger = slog.Default()
|
||||
} else {
|
||||
n.logger = logger
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
|
||||
srv := mattermost.New(n.config.ServerUrl)
|
||||
|
||||
if err := srv.LoginWithCredentials(ctx, n.config.Username, n.config.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
srv.AddReceivers(n.config.ChannelId)
|
||||
|
||||
// 复写消息样式
|
||||
srv.PreSend(func(req *http.Request) error {
|
||||
m := map[string]interface{}{
|
||||
"channel_id": n.config.ChannelId,
|
||||
"props": map[string]interface{}{
|
||||
"attachments": []map[string]interface{}{
|
||||
{
|
||||
"title": subject,
|
||||
"text": message,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if body, err := json.Marshal(m); err != nil {
|
||||
return err
|
||||
} else {
|
||||
req.ContentLength = int64(len(body))
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err = srv.Send(ctx, subject, message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ¬ifier.NotifyResult{}, nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package mattermost_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost"
|
||||
)
|
||||
|
||||
const (
|
||||
mockSubject = "test_subject"
|
||||
mockMessage = "test_message"
|
||||
)
|
||||
|
||||
var (
|
||||
fServerUrl string
|
||||
fChannelId string
|
||||
fUsername string
|
||||
fPassword string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_NOTIFIER_MATTERMOST_"
|
||||
|
||||
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
|
||||
flag.StringVar(&fChannelId, argsPrefix+"CHANNELID", "", "")
|
||||
flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "")
|
||||
flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v ./mattermost_test.go -args \
|
||||
--CERTIMATE_NOTIFIER_MATTERMOST_SERVERURL="https://example.com/your-server-url" \
|
||||
--CERTIMATE_NOTIFIER_MATTERMOST_CHANNELID="your-chanel-id" \
|
||||
--CERTIMATE_NOTIFIER_MATTERMOST_USERNAME="your-username" \
|
||||
--CERTIMATE_NOTIFIER_MATTERMOST_PASSWORD="your-password"
|
||||
*/
|
||||
func TestNotify(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Notify", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("SERVERURL: %v", fServerUrl),
|
||||
fmt.Sprintf("CHANNELID: %v", fChannelId),
|
||||
fmt.Sprintf("USERNAME: %v", fUsername),
|
||||
fmt.Sprintf("PASSWORD: %v", fPassword),
|
||||
}, "\n"))
|
||||
|
||||
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
|
||||
ServerUrl: fServerUrl,
|
||||
ChannelId: fChannelId,
|
||||
Username: fUsername,
|
||||
Password: fPassword,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
102
internal/pkg/core/notifier/providers/pushover/pushover.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package pushover
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||
)
|
||||
|
||||
type NotifierConfig struct {
|
||||
Token string `json:"token"` // 应用 API Token
|
||||
User string `json:"user"` // 用户/分组 Key
|
||||
}
|
||||
|
||||
type NotifierProvider struct {
|
||||
config *NotifierConfig
|
||||
logger *slog.Logger
|
||||
// 未来将移除
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
var _ notifier.Notifier = (*NotifierProvider)(nil)
|
||||
|
||||
func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
return &NotifierProvider{
|
||||
config: config,
|
||||
httpClient: http.DefaultClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
|
||||
if logger == nil {
|
||||
n.logger = slog.Default()
|
||||
} else {
|
||||
n.logger = logger
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Notify 发送通知
|
||||
// 参考文档:https://pushover.net/api
|
||||
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
|
||||
// 请求体
|
||||
reqBody := &struct {
|
||||
Token string `json:"token"`
|
||||
User string `json:"user"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Token: n.config.Token,
|
||||
User: n.config.User,
|
||||
Title: subject,
|
||||
Message: message,
|
||||
}
|
||||
|
||||
// Make request
|
||||
body, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "encode message body")
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
"https://api.pushover.net/1/messages.json",
|
||||
bytes.NewReader(body),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create new request")
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
// Send request to pushover service
|
||||
resp, err := n.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "send request to pushover server")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
result, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "read response")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("pushover returned status code %d: %s", resp.StatusCode, string(result))
|
||||
}
|
||||
|
||||
return ¬ifier.NotifyResult{}, nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package pushover_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover"
|
||||
)
|
||||
|
||||
const (
|
||||
mockSubject = "test_subject"
|
||||
mockMessage = "test_message"
|
||||
)
|
||||
|
||||
var (
|
||||
fToken string
|
||||
fUser string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_NOTIFIER_PUSHOVER_"
|
||||
flag.StringVar(&fToken, argsPrefix+"TOKEN", "", "")
|
||||
flag.StringVar(&fUser, argsPrefix+"USER", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v ./pushover_test.go -args \
|
||||
--CERTIMATE_NOTIFIER_PUSHOVER_TOKEN="your-pushover-token" \
|
||||
--CERTIMATE_NOTIFIER_PUSHOVER_USER="your-pushover-user" \
|
||||
*/
|
||||
func TestNotify(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Notify", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("TOKEN: %v", fToken),
|
||||
}, "\n"))
|
||||
|
||||
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
|
||||
Token: fToken,
|
||||
User: fUser,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
113
internal/pkg/core/notifier/providers/pushplus/pushplus.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package pushplus
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||
)
|
||||
|
||||
type NotifierConfig struct {
|
||||
// PushPlus Token
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type NotifierProvider struct {
|
||||
config *NotifierConfig
|
||||
logger *slog.Logger
|
||||
// 未来将移除
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
var _ notifier.Notifier = (*NotifierProvider)(nil)
|
||||
|
||||
func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
return &NotifierProvider{
|
||||
config: config,
|
||||
httpClient: http.DefaultClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
|
||||
if logger == nil {
|
||||
n.logger = slog.Default()
|
||||
} else {
|
||||
n.logger = logger
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Notify 发送通知
|
||||
// 参考文档:https://pushplus.plus/doc/guide/api.html
|
||||
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
|
||||
// 请求体
|
||||
reqBody := &struct {
|
||||
Token string `json:"token"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
}{
|
||||
Token: n.config.Token,
|
||||
Title: subject,
|
||||
Content: message,
|
||||
}
|
||||
|
||||
// Make request
|
||||
body, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "encode message body")
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
"https://www.pushplus.plus/send",
|
||||
bytes.NewReader(body),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create new request")
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
// Send request to pushplus service
|
||||
resp, err := n.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "send request to pushplus server")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
result, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "read response")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("pushplus returned status code %d: %s", resp.StatusCode, string(result))
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var errorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
if err := json.Unmarshal(result, &errorResponse); err != nil {
|
||||
return nil, errors.Wrap(err, "decode response")
|
||||
}
|
||||
|
||||
if errorResponse.Code != 200 {
|
||||
return nil, fmt.Errorf("pushplus returned error: %s", errorResponse.Msg)
|
||||
}
|
||||
|
||||
return ¬ifier.NotifyResult{}, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package pushplus_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus"
|
||||
)
|
||||
|
||||
const (
|
||||
mockSubject = "test_subject"
|
||||
mockMessage = "test_message"
|
||||
)
|
||||
|
||||
var fToken string
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_NOTIFIER_PUSHPLUS_"
|
||||
flag.StringVar(&fToken, argsPrefix+"TOKEN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v ./pushplus_test.go -args \
|
||||
--CERTIMATE_NOTIFIER_PUSHPLUS_TOKEN="your-pushplus-token" \
|
||||
*/
|
||||
func TestNotify(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Notify", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("TOKEN: %v", fToken),
|
||||
}, "\n"))
|
||||
|
||||
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
|
||||
Token: fToken,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
|
||||
|
||||
func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
|
||||
// 遍历证书列表,避免重复上传
|
||||
if res, err := u.getExistCert(ctx, certPem, privkeyPem); err != nil {
|
||||
if res, err := u.getCertIfExists(ctx, certPem, privkeyPem); err != nil {
|
||||
return nil, err
|
||||
} else if res != nil {
|
||||
u.logger.Info("ssl certificate already exists")
|
||||
@@ -82,7 +82,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
|
||||
}
|
||||
|
||||
// 遍历证书列表,获取刚刚上传证书 ID
|
||||
if res, err := u.getExistCert(ctx, certPem, privkeyPem); err != nil {
|
||||
if res, err := u.getCertIfExists(ctx, certPem, privkeyPem); err != nil {
|
||||
return nil, err
|
||||
} else if res == nil {
|
||||
return nil, fmt.Errorf("no ssl certificate found, may be upload failed (code: %d, message: %s)", uploadWebsiteSSLResp.GetCode(), uploadWebsiteSSLResp.GetMessage())
|
||||
@@ -91,7 +91,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UploaderProvider) getExistCert(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
|
||||
func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
|
||||
searchWebsiteSSLPageNumber := int32(1)
|
||||
searchWebsiteSSLPageSize := int32(100)
|
||||
for {
|
||||
|
||||
@@ -116,6 +116,10 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
|
||||
return &uploader.UploadResult{
|
||||
CertId: fmt.Sprintf("%d", tea.Int64Value(certDetail.CertificateId)),
|
||||
CertName: *certDetail.Name,
|
||||
ExtendedData: map[string]any{
|
||||
"instanceId": tea.StringValue(getUserCertificateDetailResp.Body.InstanceId),
|
||||
"certIdentifier": tea.StringValue(getUserCertificateDetailResp.Body.CertIdentifier),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -129,8 +133,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
|
||||
}
|
||||
|
||||
// 生成新证书名(需符合阿里云命名规则)
|
||||
var certId, certName string
|
||||
certName = fmt.Sprintf("certimate_%d", time.Now().UnixMilli())
|
||||
certName := fmt.Sprintf("certimate_%d", time.Now().UnixMilli())
|
||||
|
||||
// 上传新证书
|
||||
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-uploadusercertificate
|
||||
@@ -145,10 +148,25 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cas.UploadUserCertificate'")
|
||||
}
|
||||
|
||||
certId = fmt.Sprintf("%d", tea.Int64Value(uploadUserCertificateResp.Body.CertId))
|
||||
// 获取证书详情
|
||||
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-getusercertificatedetail
|
||||
getUserCertificateDetailReq := &alicas.GetUserCertificateDetailRequest{
|
||||
CertId: uploadUserCertificateResp.Body.CertId,
|
||||
CertFilter: tea.Bool(true),
|
||||
}
|
||||
getUserCertificateDetailResp, err := u.sdkClient.GetUserCertificateDetail(getUserCertificateDetailReq)
|
||||
u.logger.Debug("sdk request 'cas.GetUserCertificateDetail'", slog.Any("request", getUserCertificateDetailReq), slog.Any("response", getUserCertificateDetailResp))
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cas.GetUserCertificateDetail'")
|
||||
}
|
||||
|
||||
return &uploader.UploadResult{
|
||||
CertId: certId,
|
||||
CertId: fmt.Sprintf("%d", tea.Int64Value(getUserCertificateDetailResp.Body.Id)),
|
||||
CertName: certName,
|
||||
ExtendedData: map[string]any{
|
||||
"instanceId": tea.StringValue(getUserCertificateDetailResp.Body.InstanceId),
|
||||
"certIdentifier": tea.StringValue(getUserCertificateDetailResp.Body.CertIdentifier),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
|
||||
|
||||
// 获取证书列表,避免重复上传
|
||||
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ListCertificates.html
|
||||
listCertificatesNextToken := new(string)
|
||||
var listCertificatesNextToken *string = nil
|
||||
listCertificatesMaxItems := int32(1000)
|
||||
for {
|
||||
listCertificatesReq := &awsacm.ListCertificatesInput{
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package rainyunsslcenter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/certutil"
|
||||
rainyunsdk "github.com/usual2970/certimate/internal/pkg/vendors/rainyun-sdk"
|
||||
)
|
||||
|
||||
type UploaderConfig struct {
|
||||
// 雨云 API 密钥。
|
||||
ApiKey string `json:"ApiKey"`
|
||||
}
|
||||
|
||||
type UploaderProvider struct {
|
||||
config *UploaderConfig
|
||||
logger *slog.Logger
|
||||
sdkClient *rainyunsdk.Client
|
||||
}
|
||||
|
||||
var _ uploader.Uploader = (*UploaderProvider)(nil)
|
||||
|
||||
func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
|
||||
if config == nil {
|
||||
panic("config is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.ApiKey)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
return &UploaderProvider{
|
||||
config: config,
|
||||
logger: slog.Default(),
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
|
||||
if logger == nil {
|
||||
u.logger = slog.Default()
|
||||
} else {
|
||||
u.logger = logger
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
|
||||
if res, err := u.getCertIfExists(ctx, certPem); err != nil {
|
||||
return nil, err
|
||||
} else if res != nil {
|
||||
u.logger.Info("ssl certificate already exists")
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// SSL 证书上传
|
||||
// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046
|
||||
sslCenterCreateReq := &rainyunsdk.SslCenterCreateRequest{
|
||||
Cert: certPem,
|
||||
Key: privkeyPem,
|
||||
}
|
||||
sslCenterCreateResp, err := u.sdkClient.SslCenterCreate(sslCenterCreateReq)
|
||||
u.logger.Debug("sdk request 'sslcenter.Create'", slog.Any("request", sslCenterCreateReq), slog.Any("response", sslCenterCreateResp))
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.Create'")
|
||||
}
|
||||
|
||||
if res, err := u.getCertIfExists(ctx, certPem); err != nil {
|
||||
return nil, err
|
||||
} else if res == nil {
|
||||
return nil, errors.New("rainyun sslcenter: no certificate found")
|
||||
} else {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) {
|
||||
// 解析证书内容
|
||||
certX509, err := certutil.ParseCertificateFromPEM(certPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 遍历 SSL 证书列表,避免重复上传
|
||||
// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046
|
||||
// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943048
|
||||
sslCenterListPage := int32(1)
|
||||
sslCenterListPerPage := int32(100)
|
||||
for {
|
||||
sslCenterListReq := &rainyunsdk.SslCenterListRequest{
|
||||
Filters: &rainyunsdk.SslCenterListFilters{
|
||||
Domain: &certX509.Subject.CommonName,
|
||||
},
|
||||
Page: &sslCenterListPage,
|
||||
PerPage: &sslCenterListPerPage,
|
||||
}
|
||||
sslCenterListResp, err := u.sdkClient.SslCenterList(sslCenterListReq)
|
||||
u.logger.Debug("sdk request 'sslcenter.List'", slog.Any("request", sslCenterListReq), slog.Any("response", sslCenterListResp))
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.List'")
|
||||
}
|
||||
|
||||
if sslCenterListResp.Data != nil && sslCenterListResp.Data.Records != nil {
|
||||
for _, sslItem := range sslCenterListResp.Data.Records {
|
||||
// 先对比证书的多域名
|
||||
if sslItem.Domain != strings.Join(certX509.DNSNames, ", ") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 再对比证书的有效期
|
||||
if sslItem.StartDate != certX509.NotBefore.Unix() || sslItem.ExpireDate != certX509.NotAfter.Unix() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 最后对比证书内容
|
||||
sslCenterGetResp, err := u.sdkClient.SslCenterGet(sslItem.ID)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.Get'")
|
||||
}
|
||||
|
||||
var isSameCert bool
|
||||
if sslCenterGetResp.Data != nil {
|
||||
if sslCenterGetResp.Data.Cert == certPem {
|
||||
isSameCert = true
|
||||
} else {
|
||||
oldCertX509, err := certutil.ParseCertificateFromPEM(sslCenterGetResp.Data.Cert)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
isSameCert = certutil.EqualCertificate(certX509, oldCertX509)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果已存在相同证书,直接返回
|
||||
if isSameCert {
|
||||
return &uploader.UploadResult{
|
||||
CertId: fmt.Sprintf("%d", sslItem.ID),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sslCenterListResp.Data == nil || len(sslCenterListResp.Data.Records) < int(sslCenterListPerPage) {
|
||||
break
|
||||
} else {
|
||||
sslCenterListPage++
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func createSdkClient(apiKey string) (*rainyunsdk.Client, error) {
|
||||
if apiKey == "" {
|
||||
return nil, errors.New("invalid rainyun api key")
|
||||
}
|
||||
|
||||
client := rainyunsdk.NewClient(apiKey)
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package rainyunsslcenter_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/rainyun-sslcenter"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fApiKey string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_UPLOADER_RAINYUNSSLCENTER_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v ./rainyun_sslcenter_test.go -args \
|
||||
--CERTIMATE_UPLOADER_RAINYUNSSLCENTER_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_UPLOADER_RAINYUNSSLCENTER_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_UPLOADER_RAINYUNSSLCENTER_APIKEY="your-api-key"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("APIKEY: %v", fApiKey),
|
||||
}, "\n"))
|
||||
|
||||
uploader, err := provider.NewUploader(&provider.UploaderConfig{
|
||||
ApiKey: fApiKey,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
sres, _ := json.Marshal(res)
|
||||
t.Logf("ok: %s", string(sres))
|
||||
})
|
||||
}
|
||||
@@ -89,10 +89,10 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
|
||||
u.logger.Debug("sdk request 'ussl.UploadNormalCertificate'", slog.Any("request", uploadNormalCertificateReq), slog.Any("response", uploadNormalCertificateResp))
|
||||
if err != nil {
|
||||
if uploadNormalCertificateResp != nil && uploadNormalCertificateResp.GetRetCode() == 80035 {
|
||||
if res, err := u.getExistCert(ctx, certPem); err != nil {
|
||||
if res, err := u.getCertIfExists(ctx, certPem); err != nil {
|
||||
return nil, err
|
||||
} else if res == nil {
|
||||
return nil, errors.New("no certificate found")
|
||||
return nil, errors.New("ucloud ssl: no certificate found")
|
||||
} else {
|
||||
u.logger.Info("ssl certificate already exists")
|
||||
return res, nil
|
||||
@@ -112,7 +112,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *UploaderProvider) getExistCert(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) {
|
||||
func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) {
|
||||
// 解析证书内容
|
||||
certX509, err := certutil.ParseCertificateFromPEM(certPem)
|
||||
if err != nil {
|
||||
|
||||
@@ -74,6 +74,18 @@ func GetOrDefaultInt32(dict map[string]any, key string, defaultValue int32) int3
|
||||
}
|
||||
}
|
||||
|
||||
if result, ok := value.(int64); ok {
|
||||
if result != 0 {
|
||||
return int32(result)
|
||||
}
|
||||
}
|
||||
|
||||
if result, ok := value.(int); ok {
|
||||
if result != 0 {
|
||||
return int32(result)
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容字符串类型的值
|
||||
if str, ok := value.(string); ok {
|
||||
if result, err := strconv.ParseInt(str, 10, 32); err == nil {
|
||||
@@ -126,6 +138,12 @@ func GetOrDefaultInt64(dict map[string]any, key string, defaultValue int64) int6
|
||||
}
|
||||
}
|
||||
|
||||
if result, ok := value.(int); ok {
|
||||
if result != 0 {
|
||||
return int64(result)
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容字符串类型的值
|
||||
if str, ok := value.(string); ok {
|
||||
if result, err := strconv.ParseInt(str, 10, 64); err == nil {
|
||||
@@ -180,3 +198,25 @@ func GetOrDefaultBool(dict map[string]any, key string, defaultValue bool) bool {
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// 以 `map[string]any` 形式从字典中获取指定键的值。
|
||||
//
|
||||
// 入参:
|
||||
// - dict: 字典。
|
||||
// - key: 键。
|
||||
//
|
||||
// 出参:
|
||||
// - 字典中键对应的 `map[string]any` 对象。
|
||||
func GetAnyMap(dict map[string]any, key string) map[string]any {
|
||||
if dict == nil {
|
||||
return make(map[string]any)
|
||||
}
|
||||
|
||||
if val, ok := dict[key]; ok {
|
||||
if result, ok := val.(map[string]any); ok {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return make(map[string]any)
|
||||
}
|
||||
|
||||
2
internal/pkg/vendors/1panel-sdk/client.go
vendored
@@ -79,7 +79,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("1panel api error: failed to send request: %w", err)
|
||||
} else if resp.IsError() {
|
||||
return resp, fmt.Errorf("1panel api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
|
||||
return resp, fmt.Errorf("1panel api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
|
||||
2
internal/pkg/vendors/baishan-sdk/client.go
vendored
@@ -75,7 +75,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("baishan api error: failed to send request: %w", err)
|
||||
} else if resp.IsError() {
|
||||
return resp, fmt.Errorf("baishan api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
|
||||
return resp, fmt.Errorf("baishan api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
|
||||
7
internal/pkg/vendors/baishan-sdk/models.go
vendored
@@ -27,9 +27,10 @@ func (r *baseResponse) GetMessage() string {
|
||||
}
|
||||
|
||||
type CreateCertificateRequest struct {
|
||||
Certificate string `json:"certificate"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
CertificateId *string `json:"cert_id,omitempty"`
|
||||
Certificate string `json:"certificate"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type CreateCertificateResponse struct {
|
||||
|
||||
2
internal/pkg/vendors/btpanel-sdk/client.go
vendored
@@ -86,7 +86,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response,
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("baota api error: failed to send request: %w", err)
|
||||
} else if resp.IsError() {
|
||||
return resp, fmt.Errorf("baota api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
|
||||
return resp, fmt.Errorf("baota api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
|
||||
2
internal/pkg/vendors/cachefly-sdk/client.go
vendored
@@ -59,7 +59,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("cachefly api error: failed to send request: %w", err)
|
||||
} else if resp.IsError() {
|
||||
return resp, fmt.Errorf("cachefly api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
|
||||
return resp, fmt.Errorf("cachefly api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
|
||||
7
internal/pkg/vendors/cdnfly-sdk/api.go
vendored
@@ -3,17 +3,18 @@ package cdnflysdk
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (c *Client) GetSite(req *GetSiteRequest) (*GetSiteResponse, error) {
|
||||
resp := &GetSiteResponse{}
|
||||
err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/v1/sites/%s", req.Id), req, resp)
|
||||
err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/v1/sites/%s", url.PathEscape(req.Id)), req, resp)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (c *Client) UpdateSite(req *UpdateSiteRequest) (*UpdateSiteResponse, error) {
|
||||
resp := &UpdateSiteResponse{}
|
||||
err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/sites/%s", req.Id), req, resp)
|
||||
err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/sites/%s", url.PathEscape(req.Id)), req, resp)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
@@ -25,6 +26,6 @@ func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertif
|
||||
|
||||
func (c *Client) UpdateCertificate(req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {
|
||||
resp := &UpdateCertificateResponse{}
|
||||
err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/certs/%s", req.Id), req, resp)
|
||||
err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/certs/%s", url.PathEscape(req.Id)), req, resp)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
2
internal/pkg/vendors/cdnfly-sdk/client.go
vendored
@@ -65,7 +65,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("cdnfly api error: failed to send request: %w", err)
|
||||
} else if resp.IsError() {
|
||||
return resp, fmt.Errorf("cdnfly api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
|
||||
return resp, fmt.Errorf("cdnfly api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
|
||||
2
internal/pkg/vendors/dnsla-sdk/client.go
vendored
@@ -60,7 +60,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("dnsla api error: failed to send request: %w", err)
|
||||
} else if resp.IsError() {
|
||||
return resp, fmt.Errorf("dnsla api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
|
||||
return resp, fmt.Errorf("dnsla api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
|
||||
@@ -6,9 +6,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Edgio/edgio-api/applications/v7/dtos"
|
||||
"github.com/go-resty/resty/v2"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos"
|
||||
)
|
||||
|
||||
// AccessTokenResponse represents the response from the token endpoint.
|
||||
@@ -3,7 +3,7 @@ package edgio_api
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos"
|
||||
"github.com/Edgio/edgio-api/applications/v7/dtos"
|
||||
)
|
||||
|
||||
type EdgioClientInterface interface {
|
||||
3
internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/go.mod
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/Edgio/edgio-api
|
||||
|
||||
go 1.23.0
|
||||
2
internal/pkg/vendors/gname-sdk/client.go
vendored
@@ -82,7 +82,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response,
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("gname api error: failed to send request: %w", err)
|
||||
} else if resp.IsError() {
|
||||
return resp, fmt.Errorf("gname api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
|
||||
return resp, fmt.Errorf("gname api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
|
||||
30
internal/pkg/vendors/rainyun-sdk/api.go
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
package rainyunsdk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (c *Client) SslCenterList(req *SslCenterListRequest) (*SslCenterListResponse, error) {
|
||||
resp := &SslCenterListResponse{}
|
||||
err := c.sendRequestWithResult(http.MethodGet, "/product/sslcenter", req, resp)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (c *Client) SslCenterGet(id int32) (*SslCenterGetResponse, error) {
|
||||
resp := &SslCenterGetResponse{}
|
||||
err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/product/sslcenter/%d", id), nil, resp)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (c *Client) SslCenterCreate(req *SslCenterCreateRequest) (*SslCenterCreateResponse, error) {
|
||||
resp := &SslCenterCreateResponse{}
|
||||
err := c.sendRequestWithResult(http.MethodPost, "/product/sslcenter/", req, resp)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (c *Client) RcdnInstanceSslBind(id int32, req *RcdnInstanceSslBindRequest) (*RcdnInstanceSslBindResponse, error) {
|
||||
resp := &RcdnInstanceSslBindResponse{}
|
||||
err := c.sendRequestWithResult(http.MethodPost, fmt.Sprintf("/product/rcdn/instance/%d/ssl_bind", id), req, resp)
|
||||
return resp, err
|
||||
}
|
||||
74
internal/pkg/vendors/rainyun-sdk/client.go
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
package rainyunsdk
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
apiKey string
|
||||
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func NewClient(apiKey string) *Client {
|
||||
client := resty.New()
|
||||
|
||||
return &Client{
|
||||
apiKey: apiKey,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) WithTimeout(timeout time.Duration) *Client {
|
||||
c.client.SetTimeout(timeout)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) sendRequest(method string, path string, params interface{}) (*resty.Response, error) {
|
||||
req := c.client.R().SetHeader("x-api-key", c.apiKey)
|
||||
req.Method = method
|
||||
req.URL = "https://api.v2.rainyun.com" + path
|
||||
if strings.EqualFold(method, http.MethodGet) {
|
||||
if params != nil {
|
||||
jsonb, _ := json.Marshal(params)
|
||||
req = req.SetQueryParam("options", string(jsonb))
|
||||
}
|
||||
} else {
|
||||
req = req.
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(params)
|
||||
}
|
||||
|
||||
resp, err := req.Send()
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("rainyun api error: failed to send request: %w", err)
|
||||
} else if resp.IsError() {
|
||||
return resp, fmt.Errorf("rainyun api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) sendRequestWithResult(method string, path string, params interface{}, result BaseResponse) error {
|
||||
resp, err := c.sendRequest(method, path, params)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
json.Unmarshal(resp.Body(), &result)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(resp.Body(), &result); err != nil {
|
||||
return fmt.Errorf("rainyun api error: failed to parse response: %w", err)
|
||||
} else if errcode := result.GetCode(); errcode/100 != 2 {
|
||||
return fmt.Errorf("rainyun api error: %d - %s", errcode, result.GetMessage())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
83
internal/pkg/vendors/rainyun-sdk/models.go
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
package rainyunsdk
|
||||
|
||||
type BaseResponse interface {
|
||||
GetCode() int32
|
||||
GetMessage() string
|
||||
}
|
||||
|
||||
type baseResponse struct {
|
||||
Code *int32 `json:"code,omitempty"`
|
||||
Message *string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (r *baseResponse) GetCode() int32 {
|
||||
if r.Code != nil {
|
||||
return *r.Code
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *baseResponse) GetMessage() string {
|
||||
if r.Message != nil {
|
||||
return *r.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type SslCenterListFilters struct {
|
||||
Domain *string `json:"Domain,omitempty"`
|
||||
}
|
||||
|
||||
type SslCenterListRequest struct {
|
||||
Filters *SslCenterListFilters `json:"columnFilters,omitempty"`
|
||||
Sort []*string `json:"sort,omitempty"`
|
||||
Page *int32 `json:"page,omitempty"`
|
||||
PerPage *int32 `json:"perPage,omitempty"`
|
||||
}
|
||||
|
||||
type SslCenterListResponse struct {
|
||||
baseResponse
|
||||
Data *struct {
|
||||
TotalRecords int32 `json:"TotalRecords"`
|
||||
Records []*struct {
|
||||
ID int32 `json:"ID"`
|
||||
UID int32 `json:"UID"`
|
||||
Domain string `json:"Domain"`
|
||||
Issuer string `json:"Issuer"`
|
||||
StartDate int64 `json:"StartDate"`
|
||||
ExpireDate int64 `json:"ExpDate"`
|
||||
UploadTime int64 `json:"UploadTime"`
|
||||
} `json:"Records"`
|
||||
} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type SslCenterGetResponse struct {
|
||||
baseResponse
|
||||
Data *struct {
|
||||
Cert string `json:"Cert"`
|
||||
Key string `json:"Key"`
|
||||
Domain string `json:"DomainName"`
|
||||
Issuer string `json:"Issuer"`
|
||||
StartDate int64 `json:"StartDate"`
|
||||
ExpireDate int64 `json:"ExpDate"`
|
||||
RemainDays int32 `json:"RemainDays"`
|
||||
} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type SslCenterCreateRequest struct {
|
||||
Cert string `json:"cert"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type SslCenterCreateResponse struct {
|
||||
baseResponse
|
||||
}
|
||||
|
||||
type RcdnInstanceSslBindRequest struct {
|
||||
CertId int32 `json:"cert_id"`
|
||||
Domains []string `json:"domains"`
|
||||
}
|
||||
|
||||
type RcdnInstanceSslBindResponse struct {
|
||||
baseResponse
|
||||
}
|
||||
2
internal/pkg/vendors/safeline-sdk/client.go
vendored
@@ -47,7 +47,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response,
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("safeline api error: failed to send request: %w", err)
|
||||
} else if resp.IsError() {
|
||||
return resp, fmt.Errorf("safeline api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
|
||||
return resp, fmt.Errorf("safeline api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
|
||||
@@ -64,7 +64,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("upyun api error: failed to send request: %w", err)
|
||||
} else if resp.IsError() {
|
||||
return resp, fmt.Errorf("upyun api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
|
||||
return resp, fmt.Errorf("upyun api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
|
||||
58
internal/pkg/vendors/wangsu-sdk/cdn/api.go
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
package cdn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) {
|
||||
resp := &CreateCertificateResponse{}
|
||||
r, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/certificates", req, resp, func(r *resty.Request) {
|
||||
r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp))
|
||||
})
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
resp.CertificateUrl = r.Header().Get("Location")
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (c *Client) UpdateCertificate(certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {
|
||||
resp := &UpdateCertificateResponse{}
|
||||
r, err := c.client.SendRequestWithResult(http.MethodPatch, fmt.Sprintf("/cdn/certificates/%s", url.PathEscape(certificateId)), req, resp, func(r *resty.Request) {
|
||||
r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp))
|
||||
})
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
resp.CertificateUrl = r.Header().Get("Location")
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (c *Client) GetHostnameDetail(hostname string) (*GetHostnameDetailResponse, error) {
|
||||
resp := &GetHostnameDetailResponse{}
|
||||
_, err := c.client.SendRequestWithResult(http.MethodGet, fmt.Sprintf("/cdn/hostnames/%s", url.PathEscape(hostname)), nil, resp)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (c *Client) CreateDeploymentTask(req *CreateDeploymentTaskRequest) (*CreateDeploymentTaskResponse, error) {
|
||||
resp := &CreateDeploymentTaskResponse{}
|
||||
r, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/deploymentTasks", req, resp)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
resp.DeploymentTaskUrl = r.Header().Get("Location")
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (c *Client) GetDeploymentTaskDetail(deploymentTaskId string) (*GetDeploymentTaskDetailResponse, error) {
|
||||
resp := &GetDeploymentTaskDetailResponse{}
|
||||
_, err := c.client.SendRequestWithResult(http.MethodGet, fmt.Sprintf("/cdn/deploymentTasks/%s", deploymentTaskId), nil, resp)
|
||||
return resp, err
|
||||
}
|
||||
20
internal/pkg/vendors/wangsu-sdk/cdn/client.go
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
package cdn
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/openapi"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
client *openapi.Client
|
||||
}
|
||||
|
||||
func NewClient(accessKey, secretKey string) *Client {
|
||||
return &Client{client: openapi.NewClient(accessKey, secretKey)}
|
||||
}
|
||||
|
||||
func (c *Client) WithTimeout(timeout time.Duration) *Client {
|
||||
c.client.WithTimeout(timeout)
|
||||
return c
|
||||
}
|
||||
107
internal/pkg/vendors/wangsu-sdk/cdn/models.go
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
package cdn
|
||||
|
||||
import (
|
||||
"github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/openapi"
|
||||
)
|
||||
|
||||
type baseResponse struct {
|
||||
RequestId *string `json:"-"`
|
||||
Code *string `json:"code,omitempty"`
|
||||
Message *string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
var _ openapi.Result = (*baseResponse)(nil)
|
||||
|
||||
func (r *baseResponse) SetRequestId(requestId string) {
|
||||
r.RequestId = &requestId
|
||||
}
|
||||
|
||||
type CertificateVersion struct {
|
||||
Comments *string `json:"comments,omitempty"`
|
||||
PrivateKey *string `json:"privateKey,omitempty"`
|
||||
Certificate *string `json:"certificate,omitempty"`
|
||||
ChainCert *string `json:"chainCert,omitempty"`
|
||||
IdentificationInfo *CertificateVersionIdentificationInfo `json:"identificationInfo,omitempty"`
|
||||
}
|
||||
|
||||
type CertificateVersionIdentificationInfo struct {
|
||||
Country *string `json:"country,omitempty"`
|
||||
State *string `json:"state,omitempty"`
|
||||
City *string `json:"city,omitempty"`
|
||||
Company *string `json:"company,omitempty"`
|
||||
Department *string `json:"department,omitempty"`
|
||||
CommonName *string `json:"commonName,omitempty" required:"true"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
SubjectAlternativeNames *[]string `json:"subjectAlternativeNames,omitempty" required:"true"`
|
||||
}
|
||||
|
||||
type CreateCertificateRequest struct {
|
||||
Timestamp int64 `json:"-"`
|
||||
Name *string `json:"name,omitempty" required:"true"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
AutoRenew *string `json:"autoRenew,omitempty"`
|
||||
ForceRenew *bool `json:"forceRenew,omitempty"`
|
||||
NewVersion *CertificateVersion `json:"newVersion,omitempty" required:"true"`
|
||||
}
|
||||
|
||||
type CreateCertificateResponse struct {
|
||||
baseResponse
|
||||
CertificateUrl string `json:"-"`
|
||||
}
|
||||
|
||||
type UpdateCertificateRequest struct {
|
||||
Timestamp int64 `json:"-"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
AutoRenew *string `json:"autoRenew,omitempty"`
|
||||
ForceRenew *bool `json:"forceRenew,omitempty"`
|
||||
NewVersion *CertificateVersion `json:"newVersion,omitempty" required:"true"`
|
||||
}
|
||||
|
||||
type UpdateCertificateResponse struct {
|
||||
baseResponse
|
||||
CertificateUrl string `json:"-"`
|
||||
}
|
||||
|
||||
type HostnameProperty struct {
|
||||
PropertyId string `json:"propertyId"`
|
||||
Version int32 `json:"version"`
|
||||
CertificateId *string `json:"certificateId,omitempty"`
|
||||
}
|
||||
|
||||
type GetHostnameDetailResponse struct {
|
||||
baseResponse
|
||||
Hostname string `json:"hostname"`
|
||||
PropertyInProduction *HostnameProperty `json:"propertyInProduction,omitempty"`
|
||||
PropertyInStaging *HostnameProperty `json:"propertyInStaging,omitempty"`
|
||||
}
|
||||
|
||||
type DeploymentTaskAction struct {
|
||||
Action *string `json:"action,omitempty" required:"true"`
|
||||
PropertyId *string `json:"propertyId,omitempty"`
|
||||
CertificateId *string `json:"certificateId,omitempty"`
|
||||
Version *string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type CreateDeploymentTaskRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Target *string `json:"target,omitempty" required:"true"`
|
||||
Actions *[]DeploymentTaskAction `json:"actions,omitempty" required:"true"`
|
||||
Webhook *string `json:"webhook,omitempty"`
|
||||
}
|
||||
|
||||
type CreateDeploymentTaskResponse struct {
|
||||
baseResponse
|
||||
DeploymentTaskUrl string `json:"-"`
|
||||
}
|
||||
|
||||
type GetDeploymentTaskDetailResponse struct {
|
||||
baseResponse
|
||||
Target string `json:"target"`
|
||||
Actions []DeploymentTaskAction `json:"actions"`
|
||||
Status string `json:"status"`
|
||||
StatusDetails string `json:"statusDetails"`
|
||||
SubmissionTime string `json:"submissionTime"`
|
||||
FinishTime string `json:"finishTime"`
|
||||
ApiRequestId string `json:"apiRequestId"`
|
||||
}
|
||||
187
internal/pkg/vendors/wangsu-sdk/openapi/client.go
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
accessKey string
|
||||
secretKey string
|
||||
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
type Result interface {
|
||||
SetRequestId(requestId string)
|
||||
}
|
||||
|
||||
func NewClient(accessKey, secretKey string) *Client {
|
||||
client := resty.New().
|
||||
SetBaseURL("https://open.chinanetcenter.com").
|
||||
SetHeader("Host", "open.chinanetcenter.com").
|
||||
SetHeader("Accept", "application/json").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
|
||||
// Step 1: Get request method
|
||||
method := req.Method
|
||||
method = strings.ToUpper(method)
|
||||
|
||||
// Step 2: Get request path
|
||||
path := "/"
|
||||
if req.URL != nil {
|
||||
path = req.URL.Path
|
||||
}
|
||||
|
||||
// Step 3: Get unencoded query string
|
||||
queryString := ""
|
||||
if method != http.MethodPost && req.URL != nil {
|
||||
queryString = req.URL.RawQuery
|
||||
|
||||
s, err := url.QueryUnescape(queryString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
queryString = s
|
||||
}
|
||||
|
||||
// Step 4: Get canonical headers & signed headers
|
||||
canonicalHeaders := "" +
|
||||
"content-type:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Content-Type"))) + "\n" +
|
||||
"host:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Host"))) + "\n"
|
||||
signedHeaders := "content-type;host"
|
||||
|
||||
// Step 5: Get request payload
|
||||
payload := ""
|
||||
if method != http.MethodGet && req.Body != nil {
|
||||
reader, err := req.GetBody()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer reader.Close()
|
||||
|
||||
payloadb, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload = string(payloadb)
|
||||
}
|
||||
hashedPayload := sha256.Sum256([]byte(payload))
|
||||
hashedPayloadHex := strings.ToLower(hex.EncodeToString(hashedPayload[:]))
|
||||
|
||||
// Step 6: Get timestamp
|
||||
var reqtime time.Time
|
||||
timestampString := req.Header.Get("x-cnc-timestamp")
|
||||
if timestampString == "" {
|
||||
reqtime = time.Now().UTC()
|
||||
timestampString = fmt.Sprintf("%d", reqtime.Unix())
|
||||
} else {
|
||||
timestamp, err := strconv.ParseInt(timestampString, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqtime = time.Unix(timestamp, 0).UTC()
|
||||
}
|
||||
|
||||
// Step 7: Get canonical request string
|
||||
canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, queryString, canonicalHeaders, signedHeaders, hashedPayloadHex)
|
||||
hashedCanonicalRequest := sha256.Sum256([]byte(canonicalRequest))
|
||||
hashedCanonicalRequestHex := strings.ToLower(hex.EncodeToString(hashedCanonicalRequest[:]))
|
||||
|
||||
// Step 8: String to sign
|
||||
const SignAlgorithmHeader = "CNC-HMAC-SHA256"
|
||||
stringToSign := fmt.Sprintf("%s\n%s\n%s", SignAlgorithmHeader, timestampString, hashedCanonicalRequestHex)
|
||||
hmac := hmac.New(sha256.New, []byte(secretKey))
|
||||
hmac.Write([]byte(stringToSign))
|
||||
sign := hmac.Sum(nil)
|
||||
signHex := strings.ToLower(hex.EncodeToString(sign))
|
||||
|
||||
// Step 9: Add headers to request
|
||||
req.Header.Set("x-cnc-accessKey", accessKey)
|
||||
req.Header.Set("x-cnc-timestamp", timestampString)
|
||||
req.Header.Set("x-cnc-auth-method", "AKSK")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("%s Credential=%s, SignedHeaders=%s, Signature=%s", SignAlgorithmHeader, accessKey, signedHeaders, signHex))
|
||||
req.Header.Set("Date", reqtime.Format("Mon, 02 Jan 2006 15:04:05 GMT"))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return &Client{
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) WithTimeout(timeout time.Duration) *Client {
|
||||
c.client.SetTimeout(timeout)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) sendRequest(method string, path string, params interface{}, configureReq ...func(req *resty.Request)) (*resty.Response, error) {
|
||||
req := c.client.R()
|
||||
req.Method = method
|
||||
req.URL = path
|
||||
if strings.EqualFold(method, http.MethodGet) {
|
||||
qs := make(map[string]string)
|
||||
if params != nil {
|
||||
temp := make(map[string]any)
|
||||
jsonb, _ := json.Marshal(params)
|
||||
json.Unmarshal(jsonb, &temp)
|
||||
for k, v := range temp {
|
||||
if v != nil {
|
||||
qs[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req = req.SetQueryParams(qs)
|
||||
} else {
|
||||
req = req.SetBody(params)
|
||||
}
|
||||
|
||||
for _, fn := range configureReq {
|
||||
fn(req)
|
||||
}
|
||||
|
||||
resp, err := req.Send()
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("wangsu api error: failed to send request: %w", err)
|
||||
} else if resp.IsError() {
|
||||
return resp, fmt.Errorf("wangsu api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) SendRequestWithResult(method string, path string, params interface{}, result Result, configureReq ...func(req *resty.Request)) (*resty.Response, error) {
|
||||
resp, err := c.sendRequest(method, path, params, configureReq...)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
json.Unmarshal(resp.Body(), &result)
|
||||
result.SetRequestId(resp.Header().Get("x-cnc-request-id"))
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(resp.Body(), &result); err != nil {
|
||||
return resp, fmt.Errorf("wangsu api error: failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
result.SetRequestId(resp.Header().Get("x-cnc-request-id"))
|
||||
return resp, nil
|
||||
}
|
||||
@@ -109,12 +109,24 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo
|
||||
if currentNodeConfig.ContactEmail != lastNodeConfig.ContactEmail {
|
||||
return false, "the configuration item 'ContactEmail' changed"
|
||||
}
|
||||
if currentNodeConfig.Provider != lastNodeConfig.Provider {
|
||||
return false, "the configuration item 'Provider' changed"
|
||||
}
|
||||
if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId {
|
||||
return false, "the configuration item 'ProviderAccessId' changed"
|
||||
}
|
||||
if !maps.Equal(currentNodeConfig.ProviderConfig, lastNodeConfig.ProviderConfig) {
|
||||
return false, "the configuration item 'ProviderConfig' changed"
|
||||
}
|
||||
if currentNodeConfig.CAProvider != lastNodeConfig.CAProvider {
|
||||
return false, "the configuration item 'CAProvider' changed"
|
||||
}
|
||||
if currentNodeConfig.CAProviderAccessId != lastNodeConfig.CAProviderAccessId {
|
||||
return false, "the configuration item 'CAProviderAccessId' changed"
|
||||
}
|
||||
if !maps.Equal(currentNodeConfig.CAProviderConfig, lastNodeConfig.CAProviderConfig) {
|
||||
return false, "the configuration item 'CAProviderConfig' changed"
|
||||
}
|
||||
if currentNodeConfig.KeyAlgorithm != lastNodeConfig.KeyAlgorithm {
|
||||
return false, "the configuration item 'KeyAlgorithm' changed"
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -179,20 +177,20 @@ func init() {
|
||||
}
|
||||
|
||||
for _, workflowRun := range workflowRuns {
|
||||
type oldWorkflowRunLogRecord struct {
|
||||
type dWorkflowRunLogRecord struct {
|
||||
Time string `json:"time"`
|
||||
Level string `json:"level"`
|
||||
Content string `json:"content"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
type oldWorkflowRunLog struct {
|
||||
NodeId string `json:"nodeId"`
|
||||
NodeName string `json:"nodeName"`
|
||||
Records []oldWorkflowRunLogRecord `json:"records"`
|
||||
Error string `json:"error"`
|
||||
type dWorkflowRunLog struct {
|
||||
NodeId string `json:"nodeId"`
|
||||
NodeName string `json:"nodeName"`
|
||||
Records []dWorkflowRunLogRecord `json:"records"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
logs := make([]oldWorkflowRunLog, 0)
|
||||
logs := make([]dWorkflowRunLog, 0)
|
||||
if err := workflowRun.UnmarshalJSONField("logs", &logs); err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -259,8 +257,20 @@ func init() {
|
||||
return err
|
||||
}
|
||||
|
||||
type dWorkflowNode struct {
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Config map[string]any `json:"config"`
|
||||
Inputs []map[string]any `json:"inputs"`
|
||||
Outputs []map[string]any `json:"outputs"`
|
||||
Next *dWorkflowNode `json:"next,omitempty"`
|
||||
Branches []dWorkflowNode `json:"branches,omitempty"`
|
||||
Validated bool `json:"validated"`
|
||||
}
|
||||
|
||||
for _, workflowRun := range workflowRuns {
|
||||
node := &domain.WorkflowNode{}
|
||||
node := &dWorkflowNode{}
|
||||
for _, workflowOutput := range workflowOutputs {
|
||||
if workflowOutput.GetString("runId") != workflowRun.Get("id") {
|
||||
continue
|
||||
@@ -270,8 +280,8 @@ func init() {
|
||||
continue
|
||||
}
|
||||
|
||||
if node.Type != domain.WorkflowNodeTypeApply {
|
||||
node = &domain.WorkflowNode{}
|
||||
if node.Type != "apply" {
|
||||
node = &dWorkflowNode{}
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -286,7 +296,7 @@ func init() {
|
||||
} else {
|
||||
workflow, _ := app.FindRecordById("workflow", workflowRun.GetString("workflowId"))
|
||||
if workflow != nil {
|
||||
rootNode := &domain.WorkflowNode{}
|
||||
rootNode := &dWorkflowNode{}
|
||||
if err := workflow.UnmarshalJSONField("content", rootNode); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -294,9 +304,9 @@ func init() {
|
||||
rootNode.Next = node
|
||||
workflowRun.Set("detail", rootNode)
|
||||
} else {
|
||||
rootNode := &domain.WorkflowNode{
|
||||
rootNode := &dWorkflowNode{
|
||||
Id: core.GenerateDefaultRandomId(),
|
||||
Type: domain.WorkflowNodeTypeStart,
|
||||
Type: "start",
|
||||
Name: "开始",
|
||||
Config: map[string]any{
|
||||
"trigger": "manual",
|
||||
|
||||
173
migrations/1743264000_upgrade.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
// update collection `settings`
|
||||
{
|
||||
collection, err := app.FindCollectionByNameOrId("dy6ccjb60spfy6p")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
records, err := app.FindRecordsByFilter(collection, "name='sslProvider'", "-created", 1, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(records) == 1 {
|
||||
record := records[0]
|
||||
|
||||
content := make(map[string]any)
|
||||
if err := record.UnmarshalJSONField("content", &content); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if provider, ok := content["provider"]; ok {
|
||||
if providerStr, ok := provider.(string); ok {
|
||||
if providerStr == "letsencrypt_staging" {
|
||||
content["provider"] = "letsencryptstaging"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config, ok := content["config"]; ok {
|
||||
if configMap, ok := config.(map[string]any); ok {
|
||||
if _, ok := configMap["letsencrypt_staging"]; ok {
|
||||
configMap["letsencryptstaging"] = configMap["letsencrypt_staging"]
|
||||
delete(configMap, "letsencrypt_staging")
|
||||
}
|
||||
if _, ok := configMap["gts"]; ok {
|
||||
configMap["googletrustservices"] = configMap["gts"]
|
||||
delete(configMap, "gts")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record.Set("content", content)
|
||||
if err := app.Save(record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update collection `access`
|
||||
{
|
||||
collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "hwy7m03o",
|
||||
"maxSelect": 1,
|
||||
"name": "provider",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"1panel",
|
||||
"acmehttpreq",
|
||||
"akamai",
|
||||
"aliyun",
|
||||
"aws",
|
||||
"azure",
|
||||
"baiducloud",
|
||||
"baishan",
|
||||
"baotapanel",
|
||||
"byteplus",
|
||||
"buypass",
|
||||
"cachefly",
|
||||
"cdnfly",
|
||||
"cloudflare",
|
||||
"cloudns",
|
||||
"cmcccloud",
|
||||
"ctcccloud",
|
||||
"cucccloud",
|
||||
"desec",
|
||||
"dnsla",
|
||||
"dogecloud",
|
||||
"dynv6",
|
||||
"edgio",
|
||||
"fastly",
|
||||
"gname",
|
||||
"gcore",
|
||||
"godaddy",
|
||||
"goedge",
|
||||
"googletrustservices",
|
||||
"huaweicloud",
|
||||
"jdcloud",
|
||||
"k8s",
|
||||
"letsencrypt",
|
||||
"letsencryptstaging",
|
||||
"local",
|
||||
"namecheap",
|
||||
"namedotcom",
|
||||
"namesilo",
|
||||
"ns1",
|
||||
"porkbun",
|
||||
"powerdns",
|
||||
"qiniu",
|
||||
"qingcloud",
|
||||
"rainyun",
|
||||
"safeline",
|
||||
"ssh",
|
||||
"sslcom",
|
||||
"tencentcloud",
|
||||
"ucloud",
|
||||
"upyun",
|
||||
"vercel",
|
||||
"volcengine",
|
||||
"webhook",
|
||||
"westcn",
|
||||
"zerossl"
|
||||
]
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := app.Save(collection); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update collection `acme_accounts`
|
||||
{
|
||||
collection, err := app.FindCollectionByNameOrId("012d7abbod1hwvr")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
records, err := app.FindRecordsByFilter(collection, "ca='letsencrypt_staging' || ca='gts'", "-created", 0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
ca := record.GetString("ca")
|
||||
if ca == "letsencrypt_staging" {
|
||||
record.Set("ca", "letsencryptstaging")
|
||||
} else if ca == "gts" {
|
||||
record.Set("ca", "googletrustservices")
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := app.Save(record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, func(app core.App) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
91
migrations/1744192800_upgrade.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "hwy7m03o",
|
||||
"maxSelect": 1,
|
||||
"name": "provider",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"1panel",
|
||||
"acmehttpreq",
|
||||
"akamai",
|
||||
"aliyun",
|
||||
"aws",
|
||||
"azure",
|
||||
"baiducloud",
|
||||
"baishan",
|
||||
"baotapanel",
|
||||
"byteplus",
|
||||
"buypass",
|
||||
"cachefly",
|
||||
"cdnfly",
|
||||
"cloudflare",
|
||||
"cloudns",
|
||||
"cmcccloud",
|
||||
"ctcccloud",
|
||||
"cucccloud",
|
||||
"desec",
|
||||
"dnsla",
|
||||
"dogecloud",
|
||||
"dynv6",
|
||||
"edgio",
|
||||
"fastly",
|
||||
"gname",
|
||||
"gcore",
|
||||
"godaddy",
|
||||
"goedge",
|
||||
"googletrustservices",
|
||||
"huaweicloud",
|
||||
"jdcloud",
|
||||
"k8s",
|
||||
"letsencrypt",
|
||||
"letsencryptstaging",
|
||||
"local",
|
||||
"namecheap",
|
||||
"namedotcom",
|
||||
"namesilo",
|
||||
"ns1",
|
||||
"porkbun",
|
||||
"powerdns",
|
||||
"qiniu",
|
||||
"qingcloud",
|
||||
"rainyun",
|
||||
"safeline",
|
||||
"ssh",
|
||||
"sslcom",
|
||||
"tencentcloud",
|
||||
"ucloud",
|
||||
"upyun",
|
||||
"vercel",
|
||||
"volcengine",
|
||||
"wangsu",
|
||||
"webhook",
|
||||
"westcn",
|
||||
"zerossl"
|
||||
]
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg width="200" height="200" viewBox="-0.5 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Color-" transform="translate(-401.000000, -860.000000)"><g id="Google" transform="translate(401.000000, 860.000000)"><path d="M9.82727273,24 C9.82727273,22.4757333 10.0804318,21.0144 10.5322727,19.6437333 L2.62345455,13.6042667 C1.08206818,16.7338667 0.213636364,20.2602667 0.213636364,24 C0.213636364,27.7365333 1.081,31.2608 2.62025,34.3882667 L10.5247955,28.3370667 C10.0772273,26.9728 9.82727273,25.5168 9.82727273,24" id="Fill-1" fill="#FBBC05"></path><path d="M23.7136364,10.1333333 C27.025,10.1333333 30.0159091,11.3066667 32.3659091,13.2266667 L39.2022727,6.4 C35.0363636,2.77333333 29.6954545,0.533333333 23.7136364,0.533333333 C14.4268636,0.533333333 6.44540909,5.84426667 2.62345455,13.6042667 L10.5322727,19.6437333 C12.3545909,14.112 17.5491591,10.1333333 23.7136364,10.1333333" id="Fill-2" fill="#EB4335"></path><path d="M23.7136364,37.8666667 C17.5491591,37.8666667 12.3545909,33.888 10.5322727,28.3562667 L2.62345455,34.3946667 C6.44540909,42.1557333 14.4268636,47.4666667 23.7136364,47.4666667 C29.4455,47.4666667 34.9177955,45.4314667 39.0249545,41.6181333 L31.5177727,35.8144 C29.3995682,37.1488 26.7323182,37.8666667 23.7136364,37.8666667" id="Fill-3" fill="#34A853"></path><path d="M46.1454545,24 C46.1454545,22.6133333 45.9318182,21.12 45.6113636,19.7333333 L23.7136364,19.7333333 L23.7136364,28.8 L36.3181818,28.8 C35.6879545,31.8912 33.9724545,34.2677333 31.5177727,35.8144 L39.0249545,41.6181333 C43.3393409,37.6138667 46.1454545,31.6490667 46.1454545,24" id="Fill-4" fill="#4285F4"></path></g></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
BIN
ui/public/imgs/providers/buypass.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
1
ui/public/imgs/providers/google.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M925.915429 521.545143c0.512-29.622857-2.56-59.282286-9.033143-88.100572H512v160.036572h237.714286a210.468571 210.468571 0 0 1-88.137143 139.885714l-0.804572 5.302857 127.963429 99.181715 8.850286 0.841142c81.298286-75.154286 128.365714-185.782857 128.365714-317.147428" fill="#4285F4"></path><path d="M512 943.177143c116.370286 0 214.198857-38.363429 285.622857-104.484572l-136.045714-105.472a254.829714 254.829714 0 0 1-149.430857 43.117715 259.949714 259.949714 0 0 1-245.394286-179.273143l-5.12 0.512-133.083429 102.912-1.682285 4.754286a429.860571 429.860571 0 0 0 385.097143 237.897142" fill="#34A853"></path><path d="M266.605714 597.211429a264.045714 264.045714 0 0 1-14.336-85.357715c0.182857-28.964571 4.937143-57.782857 13.970286-85.394285l-0.146286-5.632-134.802285-104.594286-4.425143 2.011428a430.262857 430.262857 0 0 0 0 387.181715l139.702857-108.214857" fill="#FBBC05"></path><path d="M512 247.515429a237.933714 237.933714 0 0 1 166.656 64.256l121.673143-118.784a414.646857 414.646857 0 0 0-288.512-112.128 430.811429 430.811429 0 0 0-385.316572 237.714285l139.410286 108.251429A260.973714 260.973714 0 0 1 512 247.478857" fill="#EB4335"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
1
ui/public/imgs/providers/sslcom.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 72 72" fill="none"><circle cx="36" cy="36" r="35" stroke="#A0CC40" stroke-width="2"></circle><path d="M13.4924 16.2017H20.2845L18.8546 22.9937H12.0625L13.4924 16.2017Z" fill="#A0CC40"></path><path d="M19.8362 25.943H26.6282L25.1983 32.7351H18.4062L19.8362 25.943Z" fill="#A0CC40"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M21.9812 22.9613C21.8448 22.8247 22.8688 17.4717 23.1888 16.6485C23.3515 16.2297 24.34 16.2017 38.924 16.2017C52.2002 16.2017 54.5064 16.2555 54.6262 16.5678C54.7033 16.769 54.5273 18.1065 54.2349 19.5397C53.8001 21.6709 53.5664 22.5316 53.8871 22.8758C54.1437 23.1511 54.755 23.0959 55.902 23.0959C58.0542 23.0959 58.0979 23.1081 57.9587 23.6704C57.8806 23.9865 56.3682 31.0817 54.5978 39.4377C52.8278 47.7937 51.32 54.8314 51.2472 55.0773C51.1231 55.4958 50.2388 55.5242 37.3542 55.5242C29.7857 55.5242 23.5459 55.4381 23.4879 55.3327C23.4302 55.2275 24.902 48.0172 26.7589 39.31C28.6161 30.6029 30.0866 23.3926 30.027 23.2874C29.9675 23.1819 28.1632 23.0959 26.0174 23.0959C23.8716 23.0959 22.0553 23.0354 21.9812 22.9613ZM35.6711 44.6994C36.8749 45.2482 38.2735 45.5226 39.8669 45.5226C40.9646 45.5226 42.0445 45.3898 43.1067 45.1243C44.1867 44.8587 45.2224 44.3895 46.2138 43.7168L43.7175 40.0786C43.2572 40.415 42.7527 40.6717 42.2038 40.8487C41.655 41.0081 41.1062 41.0877 40.5574 41.0877C39.6191 41.0877 38.902 40.9019 38.4063 40.5301C38.0832 40.2668 37.8693 39.8861 37.7646 39.3882H47.9399C48.0285 38.9987 48.0993 38.5915 48.1524 38.1666C48.2232 37.7417 48.2586 37.3079 48.2586 36.8653C48.2586 35.6084 47.9665 34.5019 47.3823 33.5458C46.798 32.5898 45.9571 31.8374 44.8594 31.2886C43.7618 30.7398 42.434 30.4653 40.876 30.4653C39.0702 30.4653 37.4857 30.8283 36.1225 31.5541C34.777 32.28 33.7325 33.2714 32.9889 34.5284C32.2453 35.7854 31.8736 37.2106 31.8736 38.8039C31.8736 40.1671 32.2099 41.3533 32.8827 42.3624C33.5554 43.3716 34.4849 44.1505 35.6711 44.6994ZM38.0084 36.4936H42.7816C42.7785 36.2493 42.7364 36.028 42.6553 35.8297C42.5137 35.4225 42.2658 35.1126 41.9117 34.9002C41.5576 34.67 41.1239 34.555 40.6105 34.555C39.8846 34.555 39.3092 34.7497 38.8843 35.1392C38.4965 35.4948 38.2045 35.9462 38.0084 36.4936Z" fill="#A0CC40"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
ui/public/imgs/providers/wangsu.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" x="0" y="0" width="200" height="200" viewBox="0 0 340 100"><g><path d="M290,61.5c-4.1,4.6-29.3,36.7-56.9,26.7c-3.6-1.3-7.2-3.3-10.9-6.2c3.8,0.2,7.5,0,10.9-0.3 c5.3-0.5,9.8-1.5,12.4-2.9c6.1-3,4.5-13.2-12.4-16.2c-0.4-0.1-0.9-0.2-1.3-0.2c-11.3-1.7-19.5,1.7-23.7,5.3 c-0.1,0-0.3-0.1-0.4-0.2c-2.6-1.1-5.2-2.2-7.8-3.2c20-0.9,23.4-14.1,4.8-26.4c-7.5-5-16.3-9.4-25.6-13.4 c-10.7-4.5-22.3-8.5-33.7-11.7c-7.9-2.2-15.7-4.1-23.1-5.7C109.9,4.6,98.7,2.8,90,2C71.1,0.3,58.1,1.5,50.8,5.8 C47.7,7.6,46,10,46,13v0.3c0,1.6,0.5,3.3,1.4,5.2c3,6.3,11.1,13.5,24.1,21.5c11.5,7.1,28.8,15.2,43.5,20.3 c0.1,0.8,0.2,1.6,0.6,2.6c0.9,2.4,3.1,5.2,6.7,8.3c0.5,0.5,1.1,1,1.8,1.5c5.7,4.5,12.7,8.7,21.1,12.2l0.2,0.1 c10.9,4.6,22.6,8.2,33.7,10.8c16.7,3.8,32.2,5.4,42.6,4.9c0.5-0.2,0.7-0.4,0.4-0.6c-0.1-0.1-0.2-0.2-0.4-0.2 c-5.7-0.4-12.9-1.2-21.7-2.3c-7.3-1-14.3-2.3-21-3.8c-12.1-2.7-23.1-6.2-33-10.5c-0.2-0.1-0.4-0.2-0.7-0.3 c-6.7-2.9-10.5-5.7-11.4-8.2c-1.2-3.4,1.9-5.2,5-6c2,0.7,4.1,1.3,6.4,1.9c10.2,2.9,22.2,5.5,33.7,7.4c11.1,1.9,21.7,3.2,29.5,3.6 c7.6,6.7,16.1,10.2,24.6,11.3c15.2,1.8,30.4-4.1,41.1-13.8c7.3-6.6,13.2-13,16.3-17L290,61.5z M145.3,30.3 c8.5,0.3,17.4,1.3,26.7,3.1c2.5,0.5,4.8,1,7,1.5c6.7,1.6,11.5,3.3,14.5,5.2c3.4,2.1,4.1,3.7,2.1,4.9c-1.5,0.9-4.6,1.5-9.4,2 c-2.4,0.2-4.8,0.4-7.2,0.4c-2.4,0.1-4.9,0-7.4,0c-4.9-0.2-9.8-0.9-14.8-1.8c-4.9-1-8.3-2-10-3c-1.3-0.8-1.6-1.4-1-1.9 c0.4-0.2,1.1-0.4,2.3-0.5c0.9-0.1,1.5-0.4,2.1-0.7c1.1-0.7,1.4-1.6,0.8-2.9c-0.7-1.3-2-2.6-4-3.8c-0.5-0.3-1.1-0.6-1.7-1 c-0.7-0.4-1.6-0.8-2.6-1.3C143.6,30.3,144.4,30.3,145.3,30.3z M145.3,51c-6-0.5-11.4-0.5-16.2,0.1c-2.7,0.3-5,0.7-6.9,1.3 c-1.3,0.4-2.5,0.9-3.4,1.4c-0.3-0.1-0.6-0.2-1-0.4c-12.8-4.5-26.4-14.1-19.6-18.1c4.8-2.8,12.8-4.4,24-4.8c2-0.1,4.1-0.1,6.3-0.1 c-1.7,5.1,2.7,10.8,12.9,17.1c1.3,0.8,2.6,1.6,3.8,2.3c0.9,0.5,1.7,1,2.6,1.4C147,51.1,146.1,51.1,145.3,51z M206.7,79.4 c-8.5-0.8-18.3-2.9-27.7-5.2c-10.6-2.7-20.7-5.7-27.7-7.9c6.4-1.5,14.8-2.2,25.6-2c0.7,0,1.4,0,2.1,0c10.8,0.4,20,1.9,27.8,4.6 C204.6,71.5,203.8,74.9,206.7,79.4z M233.1,66.9c0.4,0,0.7,0,1.1,0.1c3.6,0.4,6.7,0.9,9.8,3.1c3.5,2.4,3.5,4.4,2.1,6.5 c-1.3,2.1-7.1,2.9-13,3.2c-4.6,0.2-9.3,0.1-12.2,0C216.1,74.7,221.9,66.4,233.1,66.9z" fill="#005BAC"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
118
ui/src/components/access/AccessEditDrawer.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { Button, Drawer, Space, notification } from "antd";
|
||||
|
||||
import { type AccessModel } from "@/domain/access";
|
||||
import { useTriggerElement, useZustandShallowSelector } from "@/hooks";
|
||||
import { useAccessesStore } from "@/stores/access";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
import AccessForm, { type AccessFormInstance, type AccessFormProps } from "./AccessForm";
|
||||
|
||||
export type AccessEditDrawerProps = {
|
||||
data?: AccessFormProps["initialValues"];
|
||||
loading?: boolean;
|
||||
open?: boolean;
|
||||
range?: AccessFormProps["range"];
|
||||
scene: AccessFormProps["scene"];
|
||||
trigger?: React.ReactNode;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
afterSubmit?: (record: AccessModel) => void;
|
||||
};
|
||||
|
||||
const AccessEditDrawer = ({ data, loading, trigger, scene, range, afterSubmit, ...props }: AccessEditDrawerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
|
||||
const { createAccess, updateAccess } = useAccessesStore(useZustandShallowSelector(["createAccess", "updateAccess"]));
|
||||
|
||||
const [open, setOpen] = useControllableValue<boolean>(props, {
|
||||
valuePropName: "open",
|
||||
defaultValuePropName: "defaultOpen",
|
||||
trigger: "onOpenChange",
|
||||
});
|
||||
|
||||
const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });
|
||||
|
||||
const formRef = useRef<AccessFormInstance>(null);
|
||||
const [formPending, setFormPending] = useState(false);
|
||||
|
||||
const handleOkClick = async () => {
|
||||
setFormPending(true);
|
||||
try {
|
||||
await formRef.current!.validateFields();
|
||||
} catch (err) {
|
||||
setFormPending(false);
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
let values: AccessModel = formRef.current!.getFieldsValue();
|
||||
|
||||
if (scene === "add") {
|
||||
if (data?.id) {
|
||||
throw "Invalid props: `data`";
|
||||
}
|
||||
|
||||
values = await createAccess(values);
|
||||
} else if (scene === "edit") {
|
||||
if (!data?.id) {
|
||||
throw "Invalid props: `data`";
|
||||
}
|
||||
|
||||
values = await updateAccess({ ...data, ...values });
|
||||
} else {
|
||||
throw "Invalid props: `scene`";
|
||||
}
|
||||
|
||||
afterSubmit?.(values);
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
setFormPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelClick = () => {
|
||||
if (formPending) return;
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{NotificationContextHolder}
|
||||
|
||||
{triggerEl}
|
||||
|
||||
<Drawer
|
||||
afterOpenChange={setOpen}
|
||||
closable={!formPending}
|
||||
destroyOnClose
|
||||
footer={
|
||||
<Space className="w-full justify-end">
|
||||
<Button onClick={handleCancelClick}>{t("common.button.cancel")}</Button>
|
||||
<Button loading={formPending} type="primary" onClick={handleOkClick}>
|
||||
{scene === "edit" ? t("common.button.save") : t("common.button.submit")}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
loading={loading}
|
||||
maskClosable={!formPending}
|
||||
open={open}
|
||||
title={t(`access.action.${scene}`)}
|
||||
width={720}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<AccessForm ref={formRef} initialValues={data} range={range} scene={scene === "add" ? "add" : "edit"} />
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessEditDrawer;
|
||||
@@ -14,13 +14,14 @@ export type AccessEditModalProps = {
|
||||
data?: AccessFormProps["initialValues"];
|
||||
loading?: boolean;
|
||||
open?: boolean;
|
||||
preset: AccessFormProps["preset"];
|
||||
range?: AccessFormProps["range"];
|
||||
scene: AccessFormProps["scene"];
|
||||
trigger?: React.ReactNode;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
afterSubmit?: (record: AccessModel) => void;
|
||||
};
|
||||
|
||||
const AccessEditModal = ({ data, loading, trigger, preset, afterSubmit, ...props }: AccessEditModalProps) => {
|
||||
const AccessEditModal = ({ data, loading, trigger, scene, range, afterSubmit, ...props }: AccessEditModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
@@ -50,13 +51,13 @@ const AccessEditModal = ({ data, loading, trigger, preset, afterSubmit, ...props
|
||||
try {
|
||||
let values: AccessModel = formRef.current!.getFieldsValue();
|
||||
|
||||
if (preset === "add") {
|
||||
if (scene === "add") {
|
||||
if (data?.id) {
|
||||
throw "Invalid props: `data`";
|
||||
}
|
||||
|
||||
values = await createAccess(values);
|
||||
} else if (preset === "edit") {
|
||||
} else if (scene === "edit") {
|
||||
if (!data?.id) {
|
||||
throw "Invalid props: `data`";
|
||||
}
|
||||
@@ -96,15 +97,15 @@ const AccessEditModal = ({ data, loading, trigger, preset, afterSubmit, ...props
|
||||
confirmLoading={formPending}
|
||||
destroyOnClose
|
||||
loading={loading}
|
||||
okText={preset === "edit" ? t("common.button.save") : t("common.button.submit")}
|
||||
okText={scene === "edit" ? t("common.button.save") : t("common.button.submit")}
|
||||
open={open}
|
||||
title={t(`access.action.${preset}`)}
|
||||
title={t(`access.action.${scene}`)}
|
||||
width={480}
|
||||
onOk={handleOkClick}
|
||||
onCancel={handleCancelClick}
|
||||
>
|
||||
<div className="pb-2 pt-4">
|
||||
<AccessForm ref={formRef} initialValues={data} preset={preset === "add" ? "add" : "edit"} />
|
||||
<AccessForm ref={formRef} initialValues={data} range={range} scene={scene === "add" ? "add" : "edit"} />
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
||||
|
||||
import AccessProviderSelect from "@/components/provider/AccessProviderSelect";
|
||||
import { type AccessModel } from "@/domain/access";
|
||||
import { ACCESS_PROVIDERS } from "@/domain/provider";
|
||||
import { ACCESS_PROVIDERS, ACCESS_USAGES } from "@/domain/provider";
|
||||
import { useAntdForm, useAntdFormName } from "@/hooks";
|
||||
|
||||
import AccessForm1PanelConfig from "./AccessForm1PanelConfig";
|
||||
@@ -31,10 +31,10 @@ import AccessFormEdgioConfig from "./AccessFormEdgioConfig";
|
||||
import AccessFormGcoreConfig from "./AccessFormGcoreConfig";
|
||||
import AccessFormGnameConfig from "./AccessFormGnameConfig";
|
||||
import AccessFormGoDaddyConfig from "./AccessFormGoDaddyConfig";
|
||||
import AccessFormGoogleTrustServicesConfig from "./AccessFormGoogleTrustServicesConfig";
|
||||
import AccessFormHuaweiCloudConfig from "./AccessFormHuaweiCloudConfig";
|
||||
import AccessFormJDCloudConfig from "./AccessFormJDCloudConfig";
|
||||
import AccessFormKubernetesConfig from "./AccessFormKubernetesConfig";
|
||||
import AccessFormLocalConfig from "./AccessFormLocalConfig";
|
||||
import AccessFormNamecheapConfig from "./AccessFormNamecheapConfig";
|
||||
import AccessFormNameDotComConfig from "./AccessFormNameDotComConfig";
|
||||
import AccessFormNameSiloConfig from "./AccessFormNameSiloConfig";
|
||||
@@ -45,23 +45,28 @@ import AccessFormQiniuConfig from "./AccessFormQiniuConfig";
|
||||
import AccessFormRainYunConfig from "./AccessFormRainYunConfig";
|
||||
import AccessFormSafeLineConfig from "./AccessFormSafeLineConfig";
|
||||
import AccessFormSSHConfig from "./AccessFormSSHConfig";
|
||||
import AccessFormSSLComConfig from "./AccessFormSSLComConfig";
|
||||
import AccessFormTencentCloudConfig from "./AccessFormTencentCloudConfig";
|
||||
import AccessFormUCloudConfig from "./AccessFormUCloudConfig";
|
||||
import AccessFormUpyunConfig from "./AccessFormUpyunConfig";
|
||||
import AccessFormVercelConfig from "./AccessFormVercelConfig";
|
||||
import AccessFormVolcEngineConfig from "./AccessFormVolcEngineConfig";
|
||||
import AccessFormWangsuConfig from "./AccessFormWangsuConfig";
|
||||
import AccessFormWebhookConfig from "./AccessFormWebhookConfig";
|
||||
import AccessFormWestcnConfig from "./AccessFormWestcnConfig";
|
||||
import AccessFormZeroSSLConfig from "./AccessFormZeroSSLConfig";
|
||||
|
||||
type AccessFormFieldValues = Partial<MaybeModelRecord<AccessModel>>;
|
||||
type AccessFormPresets = "add" | "edit";
|
||||
type AccessFormRanges = "both-dns-hosting" | "ca-only" | "notify-only";
|
||||
type AccessFormScenes = "add" | "edit";
|
||||
|
||||
export type AccessFormProps = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
disabled?: boolean;
|
||||
initialValues?: AccessFormFieldValues;
|
||||
preset: AccessFormPresets;
|
||||
range?: AccessFormRanges;
|
||||
scene: AccessFormScenes;
|
||||
onValuesChange?: (values: AccessFormFieldValues) => void;
|
||||
};
|
||||
|
||||
@@ -71,7 +76,7 @@ export type AccessFormInstance = {
|
||||
validateFields: FormInstance<AccessFormFieldValues>["validateFields"];
|
||||
};
|
||||
|
||||
const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className, style, disabled, initialValues, preset, onValuesChange }, ref) => {
|
||||
const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className, style, disabled, initialValues, range, scene, onValuesChange }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -80,7 +85,14 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
.min(1, t("access.form.name.placeholder"))
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 }))
|
||||
.trim(),
|
||||
provider: z.nativeEnum(ACCESS_PROVIDERS, { message: t("access.form.provider.placeholder") }),
|
||||
provider: z.nativeEnum(ACCESS_PROVIDERS, {
|
||||
message:
|
||||
range === "ca-only"
|
||||
? t("access.form.certificate_authority.placeholder")
|
||||
: range === "notify-only"
|
||||
? t("access.form.notification_channel.placeholder")
|
||||
: t("access.form.provider.placeholder"),
|
||||
}),
|
||||
config: z.any(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
@@ -88,6 +100,35 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
initialValues: initialValues,
|
||||
});
|
||||
|
||||
const providerLabel = useMemo(() => {
|
||||
switch (range) {
|
||||
case "ca-only":
|
||||
return t("access.form.certificate_authority.label");
|
||||
case "notify-only":
|
||||
return t("access.form.notification_channel.label");
|
||||
}
|
||||
|
||||
return t("access.form.provider.label");
|
||||
}, [range]);
|
||||
const providerPlaceholder = useMemo(() => {
|
||||
switch (range) {
|
||||
case "ca-only":
|
||||
return t("access.form.certificate_authority.placeholder");
|
||||
case "notify-only":
|
||||
return t("access.form.notification_channel.placeholder");
|
||||
}
|
||||
|
||||
return t("access.form.provider.placeholder");
|
||||
}, [range]);
|
||||
const providerTooltip = useMemo(() => {
|
||||
switch (range) {
|
||||
case "both-dns-hosting":
|
||||
return <span dangerouslySetInnerHTML={{ __html: t("access.form.provider.tooltip") }}></span>;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [range]);
|
||||
|
||||
const fieldProvider = Form.useWatch("provider", formInst);
|
||||
|
||||
const [nestedFormInst] = Form.useForm();
|
||||
@@ -147,6 +188,8 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
return <AccessFormGnameConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.GODADDY:
|
||||
return <AccessFormGoDaddyConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.GOOGLETRUSTSERVICES:
|
||||
return <AccessFormGoogleTrustServicesConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.EDGIO:
|
||||
return <AccessFormEdgioConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.HUAWEICLOUD:
|
||||
@@ -155,8 +198,6 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
return <AccessFormJDCloudConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.KUBERNETES:
|
||||
return <AccessFormKubernetesConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.LOCAL:
|
||||
return <AccessFormLocalConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.NAMECHEAP:
|
||||
return <AccessFormNamecheapConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.NAMEDOTCOM:
|
||||
@@ -177,6 +218,8 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
return <AccessFormSafeLineConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.SSH:
|
||||
return <AccessFormSSHConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.SSLCOM:
|
||||
return <AccessFormSSLComConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.TENCENTCLOUD:
|
||||
return <AccessFormTencentCloudConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.UCLOUD:
|
||||
@@ -187,10 +230,14 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
return <AccessFormVercelConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.VOLCENGINE:
|
||||
return <AccessFormVolcEngineConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.WANGSU:
|
||||
return <AccessFormWangsuConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.WEBHOOK:
|
||||
return <AccessFormWebhookConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.WESTCN:
|
||||
return <AccessFormWestcnConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.ZEROSSL:
|
||||
return <AccessFormZeroSSLConfig {...nestedFormProps} />;
|
||||
}
|
||||
}, [disabled, initialValues?.config, fieldProvider, nestedFormInst, nestedFormName]);
|
||||
|
||||
@@ -235,13 +282,25 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
<Input placeholder={t("access.form.name.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label={t("access.form.provider.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.provider.tooltip") }}></span>}
|
||||
>
|
||||
<AccessProviderSelect disabled={preset !== "add"} placeholder={t("access.form.provider.placeholder")} showSearch={!disabled} />
|
||||
<Form.Item name="provider" label={providerLabel} rules={[formRule]} tooltip={providerTooltip}>
|
||||
<AccessProviderSelect
|
||||
filter={(record) => {
|
||||
if (range == null) return true;
|
||||
|
||||
switch (range) {
|
||||
case "both-dns-hosting":
|
||||
return record.usages.includes(ACCESS_USAGES.DNS) || record.usages.includes(ACCESS_USAGES.HOSTING);
|
||||
case "ca-only":
|
||||
return record.usages.includes(ACCESS_USAGES.CA);
|
||||
case "notify-only":
|
||||
return record.usages.includes(ACCESS_USAGES.NOTIFICATION);
|
||||
}
|
||||
}}
|
||||
disabled={scene !== "add"}
|
||||
placeholder={providerPlaceholder}
|
||||
showOptionTags={range == null || (range === "both-dns-hosting" ? { [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : false)}
|
||||
showSearch={!disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
|
||||
@@ -30,6 +30,11 @@ const AccessFormCloudflareConfig = ({ form: formInst, formName, disabled, initia
|
||||
.min(1, t("access.form.cloudflare_dns_api_token.placeholder"))
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 }))
|
||||
.trim(),
|
||||
zoneApiToken: z
|
||||
.string()
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 }))
|
||||
.trim()
|
||||
.nullish(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
@@ -54,6 +59,15 @@ const AccessFormCloudflareConfig = ({ form: formInst, formName, disabled, initia
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("access.form.cloudflare_dns_api_token.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="zoneApiToken"
|
||||
label={t("access.form.cloudflare_zone_api_token.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.cloudflare_zone_api_token.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("access.form.cloudflare_zone_api_token.placeholder")} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, type FormInstance, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { type AccessConfigForGoogleTrustServices } from "@/domain/access";
|
||||
|
||||
type AccessFormGoogleTrustServicesConfigFieldValues = Nullish<AccessConfigForGoogleTrustServices>;
|
||||
|
||||
export type AccessFormGoogleTrustServicesConfigProps = {
|
||||
form: FormInstance;
|
||||
formName: string;
|
||||
disabled?: boolean;
|
||||
initialValues?: AccessFormGoogleTrustServicesConfigFieldValues;
|
||||
onValuesChange?: (values: AccessFormGoogleTrustServicesConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const initFormModel = (): AccessFormGoogleTrustServicesConfigFieldValues => {
|
||||
return {
|
||||
eabKid: "",
|
||||
eabHmacKey: "",
|
||||
};
|
||||
};
|
||||
|
||||
const AccessFormGoogleTrustServicesConfig = ({
|
||||
form: formInst,
|
||||
formName,
|
||||
disabled,
|
||||
initialValues,
|
||||
onValuesChange,
|
||||
}: AccessFormGoogleTrustServicesConfigProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
eabKid: z
|
||||
.string()
|
||||
.min(1, t("access.form.googletrustservices_eab_kid.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.trim(),
|
||||
eabHmacKey: z
|
||||
.string()
|
||||
.min(1, t("access.form.googletrustservices_eab_hmac_key.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.trim(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
|
||||
onValuesChange?.(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={formInst}
|
||||
disabled={disabled}
|
||||
initialValues={initialValues ?? initFormModel()}
|
||||
layout="vertical"
|
||||
name={formName}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
<Form.Item
|
||||
name="eabKid"
|
||||
label={t("access.form.googletrustservices_eab_kid.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.googletrustservices_eab_kid.tooltip") }}></span>}
|
||||
>
|
||||
<Input autoComplete="new-password" placeholder={t("access.form.googletrustservices_eab_kid.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="eabHmacKey"
|
||||
label={t("access.form.googletrustservices_eab_hmac_key.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.googletrustservices_eab_hmac_key.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("access.form.googletrustservices_eab_hmac_key.placeholder")} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessFormGoogleTrustServicesConfig;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Form, type FormInstance } from "antd";
|
||||
|
||||
import { type AccessConfigForLocal } from "@/domain/access";
|
||||
|
||||
type AccessFormLocalConfigFieldValues = Nullish<AccessConfigForLocal>;
|
||||
|
||||
export type AccessFormLocalConfigProps = {
|
||||
form: FormInstance;
|
||||
formName: string;
|
||||
disabled?: boolean;
|
||||
initialValues?: AccessFormLocalConfigFieldValues;
|
||||
onValuesChange?: (values: AccessFormLocalConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const initFormModel = (): AccessFormLocalConfigFieldValues => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const AccessFormLocalConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormLocalConfigProps) => {
|
||||
const handleFormChange = (_: unknown, values: any) => {
|
||||
onValuesChange?.(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={formInst}
|
||||
disabled={disabled}
|
||||
initialValues={initialValues ?? initFormModel()}
|
||||
layout="vertical"
|
||||
name={formName}
|
||||
onValuesChange={handleFormChange}
|
||||
></Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessFormLocalConfig;
|
||||
@@ -7,7 +7,7 @@ import { z } from "zod";
|
||||
|
||||
import { type AccessConfigForSSH } from "@/domain/access";
|
||||
import { readFileContent } from "@/utils/file";
|
||||
import { validDomainName, validIPv4Address, validIPv6Address } from "@/utils/validators";
|
||||
import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators";
|
||||
|
||||
type AccessFormSSHConfigFieldValues = Nullish<AccessConfigForSSH>;
|
||||
|
||||
@@ -34,11 +34,13 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
|
||||
host: z
|
||||
.string({ message: t("access.form.ssh_host.placeholder") })
|
||||
.refine((v) => validDomainName(v) || validIPv4Address(v) || validIPv6Address(v), t("common.errmsg.host_invalid")),
|
||||
port: z
|
||||
.number({ message: t("access.form.ssh_port.placeholder") })
|
||||
.int()
|
||||
.gte(1, t("common.errmsg.port_invalid"))
|
||||
.lte(65535, t("common.errmsg.port_invalid")),
|
||||
port: z.preprocess(
|
||||
(v) => Number(v),
|
||||
z
|
||||
.number({ message: t("access.form.ssh_port.placeholder") })
|
||||
.int(t("access.form.ssh_port.placeholder"))
|
||||
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
|
||||
),
|
||||
username: z
|
||||
.string()
|
||||
.min(1, "access.form.ssh_username.placeholder")
|
||||
|
||||
76
ui/src/components/access/AccessFormSSLComConfig.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, type FormInstance, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { type AccessConfigForSSLCom } from "@/domain/access";
|
||||
|
||||
type AccessFormSSLComConfigFieldValues = Nullish<AccessConfigForSSLCom>;
|
||||
|
||||
export type AccessFormSSLComConfigProps = {
|
||||
form: FormInstance;
|
||||
formName: string;
|
||||
disabled?: boolean;
|
||||
initialValues?: AccessFormSSLComConfigFieldValues;
|
||||
onValuesChange?: (values: AccessFormSSLComConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const initFormModel = (): AccessFormSSLComConfigFieldValues => {
|
||||
return {
|
||||
eabKid: "",
|
||||
eabHmacKey: "",
|
||||
};
|
||||
};
|
||||
|
||||
const AccessFormSSLComConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormSSLComConfigProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
eabKid: z
|
||||
.string()
|
||||
.min(1, t("access.form.sslcom_eab_kid.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.trim(),
|
||||
eabHmacKey: z
|
||||
.string()
|
||||
.min(1, t("access.form.sslcom_eab_hmac_key.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.trim(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
|
||||
onValuesChange?.(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={formInst}
|
||||
disabled={disabled}
|
||||
initialValues={initialValues ?? initFormModel()}
|
||||
layout="vertical"
|
||||
name={formName}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
<Form.Item
|
||||
name="eabKid"
|
||||
label={t("access.form.sslcom_eab_kid.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.sslcom_eab_kid.tooltip") }}></span>}
|
||||
>
|
||||
<Input autoComplete="new-password" placeholder={t("access.form.sslcom_eab_kid.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="eabHmacKey"
|
||||
label={t("access.form.sslcom_eab_hmac_key.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.sslcom_eab_hmac_key.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("access.form.sslcom_eab_hmac_key.placeholder")} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessFormSSLComConfig;
|
||||
76
ui/src/components/access/AccessFormWangsuConfig.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, type FormInstance, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { type AccessConfigForWangsu } from "@/domain/access";
|
||||
|
||||
type AccessFormWangsuConfigFieldValues = Nullish<AccessConfigForWangsu>;
|
||||
|
||||
export type AccessFormWangsuConfigProps = {
|
||||
form: FormInstance;
|
||||
formName: string;
|
||||
disabled?: boolean;
|
||||
initialValues?: AccessFormWangsuConfigFieldValues;
|
||||
onValuesChange?: (values: AccessFormWangsuConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const initFormModel = (): AccessFormWangsuConfigFieldValues => {
|
||||
return {
|
||||
accessKeyId: "",
|
||||
accessKeySecret: "",
|
||||
};
|
||||
};
|
||||
|
||||
const AccessFormWangsuConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange: onValuesChange }: AccessFormWangsuConfigProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
accessKeyId: z
|
||||
.string()
|
||||
.min(1, t("access.form.wangsu_access_key_id.placeholder"))
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 }))
|
||||
.trim(),
|
||||
accessKeySecret: z
|
||||
.string()
|
||||
.min(1, t("access.form.wangsu_access_key_secret.placeholder"))
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 }))
|
||||
.trim(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
|
||||
onValuesChange?.(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={formInst}
|
||||
disabled={disabled}
|
||||
initialValues={initialValues ?? initFormModel()}
|
||||
layout="vertical"
|
||||
name={formName}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
<Form.Item
|
||||
name="accessKeyId"
|
||||
label={t("access.form.wangsu_access_key_id.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.wangsu_access_key_id.tooltip") }}></span>}
|
||||
>
|
||||
<Input autoComplete="new-password" placeholder={t("access.form.wangsu_access_key_id.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="accessKeySecret"
|
||||
label={t("access.form.wangsu_access_key_secret.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.wangsu_access_key_secret.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("access.form.wangsu_access_key_secret.placeholder")} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessFormWangsuConfig;
|
||||
76
ui/src/components/access/AccessFormZeroSSLConfig.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, type FormInstance, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { type AccessConfigForZeroSSL } from "@/domain/access";
|
||||
|
||||
type AccessFormZeroSSLConfigFieldValues = Nullish<AccessConfigForZeroSSL>;
|
||||
|
||||
export type AccessFormZeroSSLConfigProps = {
|
||||
form: FormInstance;
|
||||
formName: string;
|
||||
disabled?: boolean;
|
||||
initialValues?: AccessFormZeroSSLConfigFieldValues;
|
||||
onValuesChange?: (values: AccessFormZeroSSLConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const initFormModel = (): AccessFormZeroSSLConfigFieldValues => {
|
||||
return {
|
||||
eabKid: "",
|
||||
eabHmacKey: "",
|
||||
};
|
||||
};
|
||||
|
||||
const AccessFormZeroSSLConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormZeroSSLConfigProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
eabKid: z
|
||||
.string()
|
||||
.min(1, t("access.form.zerossl_eab_kid.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.trim(),
|
||||
eabHmacKey: z
|
||||
.string()
|
||||
.min(1, t("access.form.zerossl_eab_hmac_key.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.trim(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
|
||||
onValuesChange?.(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={formInst}
|
||||
disabled={disabled}
|
||||
initialValues={initialValues ?? initFormModel()}
|
||||
layout="vertical"
|
||||
name={formName}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
<Form.Item
|
||||
name="eabKid"
|
||||
label={t("access.form.zerossl_eab_kid.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.zerossl_eab_kid.tooltip") }}></span>}
|
||||
>
|
||||
<Input autoComplete="new-password" placeholder={t("access.form.zerossl_eab_kid.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="eabHmacKey"
|
||||
label={t("access.form.zerossl_eab_hmac_key.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.zerossl_eab_hmac_key.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("access.form.zerossl_eab_hmac_key.placeholder")} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessFormZeroSSLConfig;
|
||||
@@ -29,7 +29,6 @@ const CertificateDetailDrawer = ({ data, loading, trigger, ...props }: Certifica
|
||||
|
||||
<Drawer
|
||||
afterOpenChange={setOpen}
|
||||
closable
|
||||
destroyOnClose
|
||||
open={open}
|
||||
loading={loading}
|
||||
|
||||
@@ -7,7 +7,11 @@ import { useAntdForm } from "@/hooks";
|
||||
import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields";
|
||||
import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields";
|
||||
import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields";
|
||||
import NotifyChannelEditFormGotifyFields from "./NotifyChannelEditFormGotifyFields.tsx";
|
||||
import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields";
|
||||
import NotifyChannelEditFormMattermostFields from "./NotifyChannelEditFormMattermostFields.tsx";
|
||||
import NotifyChannelEditFormPushoverFields from "./NotifyChannelEditFormPushoverFields";
|
||||
import NotifyChannelEditFormPushPlusFields from "./NotifyChannelEditFormPushPlusFields";
|
||||
import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields";
|
||||
import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields";
|
||||
import NotifyChannelEditFormWebhookFields from "./NotifyChannelEditFormWebhookFields";
|
||||
@@ -48,8 +52,16 @@ const NotifyChannelEditForm = forwardRef<NotifyChannelEditFormInstance, NotifyCh
|
||||
return <NotifyChannelEditFormDingTalkFields />;
|
||||
case NOTIFY_CHANNELS.EMAIL:
|
||||
return <NotifyChannelEditFormEmailFields />;
|
||||
case NOTIFY_CHANNELS.GOTIFY:
|
||||
return <NotifyChannelEditFormGotifyFields />;
|
||||
case NOTIFY_CHANNELS.LARK:
|
||||
return <NotifyChannelEditFormLarkFields />;
|
||||
case NOTIFY_CHANNELS.MATTERMOST:
|
||||
return <NotifyChannelEditFormMattermostFields />;
|
||||
case NOTIFY_CHANNELS.PUSHOVER:
|
||||
return <NotifyChannelEditFormPushoverFields />;
|
||||
case NOTIFY_CHANNELS.PUSHPLUS:
|
||||
return <NotifyChannelEditFormPushPlusFields />;
|
||||
case NOTIFY_CHANNELS.SERVERCHAN:
|
||||
return <NotifyChannelEditFormServerChanFields />;
|
||||
case NOTIFY_CHANNELS.TELEGRAM:
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Form, Input, InputNumber, Switch } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { validPortNumber } from "@/utils/validators";
|
||||
|
||||
const NotifyChannelEditFormEmailFields = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -11,11 +13,13 @@ const NotifyChannelEditFormEmailFields = () => {
|
||||
.string({ message: t("settings.notification.channel.form.email_smtp_host.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.email_smtp_host.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 })),
|
||||
smtpPort: z
|
||||
.number({ message: t("settings.notification.channel.form.email_smtp_port.placeholder") })
|
||||
.int()
|
||||
.gte(1, t("common.errmsg.port_invalid"))
|
||||
.lte(65535, t("common.errmsg.port_invalid")),
|
||||
smtpPort: z.preprocess(
|
||||
(v) => Number(v),
|
||||
z
|
||||
.number({ message: t("settings.notification.channel.form.email_smtp_port.placeholder") })
|
||||
.int(t("settings.notification.channel.form.email_smtp_port.placeholder"))
|
||||
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
|
||||
),
|
||||
smtpTLS: z.boolean().nullish(),
|
||||
username: z
|
||||
.string({ message: t("settings.notification.channel.form.email_username.placeholder") })
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const NotifyChannelEditFormGotifyFields = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
url: z.string({ message: t("settings.notification.channel.form.gotify_url.placeholder") }).url({ message: t("common.errmsg.url_invalid") }),
|
||||
token: z.string({ message: t("settings.notification.channel.form.gotify_token.placeholder") }),
|
||||
priority: z.preprocess(val => Number(val), z.number({ message: t("settings.notification.channel.form.gotify_priority.placeholder") }).gte(0, { message: t("settings.notification.channel.form.gotify_priority.error.gte0") })),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="url"
|
||||
label={t("settings.notification.channel.form.gotify_url.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.gotify_url.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("settings.notification.channel.form.gotify_url.placeholder")} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="token"
|
||||
label={t("settings.notification.channel.form.gotify_token.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.gotify_token.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("settings.notification.channel.form.gotify_token.placeholder")} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="priority"
|
||||
label={t("settings.notification.channel.form.gotify_priority.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.gotify_priority.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("settings.notification.channel.form.gotify_priority.placeholder")} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifyChannelEditFormGotifyFields;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const NotifyChannelEditFormMattermostFields = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
serverUrl: z
|
||||
.string({ message: t("settings.notification.channel.form.mattermost_server_url.placeholder") })
|
||||
.url(t("common.errmsg.url_invalid")),
|
||||
channelId: z
|
||||
.string({ message: t("settings.notification.channel.form.mattermost_channel_id.placeholder") })
|
||||
.nonempty(t("settings.notification.channel.form.mattermost_channel_id.placeholder")),
|
||||
username: z
|
||||
.string({ message: t("settings.notification.channel.form.mattermost_username.placeholder") })
|
||||
.nonempty(t("settings.notification.channel.form.mattermost_username.placeholder")),
|
||||
password: z
|
||||
.string({ message: t("settings.notification.channel.form.mattermost_password.placeholder") })
|
||||
.nonempty(t("settings.notification.channel.form.mattermost_password.placeholder")),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="serverUrl"
|
||||
label={t("settings.notification.channel.form.mattermost_server_url.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.mattermost_server_url.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("settings.notification.channel.form.mattermost_server_url.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="channelId"
|
||||
label={t("settings.notification.channel.form.mattermost_channel_id.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.mattermost_channel_id.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("settings.notification.channel.form.mattermost_channel_id.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="username"
|
||||
label={t("settings.notification.channel.form.mattermost_username.label")}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Input placeholder={t("settings.notification.channel.form.mattermost_username.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={t("settings.notification.channel.form.mattermost_password.label")}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Input.Password placeholder={t("settings.notification.channel.form.mattermost_password.placeholder")} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifyChannelEditFormMattermostFields;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const NotifyChannelEditFormPushPlusFields = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
token: z.string({ message: t("settings.notification.channel.form.pushplus_token.placeholder") }),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="token"
|
||||
label={t("settings.notification.channel.form.pushplus_token.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.pushplus_token.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("settings.notification.channel.form.pushplus_token.placeholder")} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifyChannelEditFormPushPlusFields;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const NotifyChannelEditFormPushoverFields = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
token: z
|
||||
.string({ message: t("settings.notification.channel.form.pushover_token.placeholder") })
|
||||
.nonempty(t("settings.notification.channel.form.pushover_token.placeholder")),
|
||||
user: z
|
||||
.string({ message: t("settings.notification.channel.form.pushover_user.placeholder") })
|
||||
.nonempty(t("settings.notification.channel.form.pushover_user.placeholder")),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="token"
|
||||
label={t("settings.notification.channel.form.pushover_token.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.pushover_token.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("settings.notification.channel.form.pushover_token.placeholder")} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="user"
|
||||
label={t("settings.notification.channel.form.pushover_user.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.pushover_user.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("settings.notification.channel.form.pushover_user.placeholder")} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifyChannelEditFormPushoverFields;
|
||||