Compare commits

..

51 Commits

Author SHA1 Message Date
yoan
2ff923dd1b v0.2.19 2024-11-13 08:16:19 +08:00
usual2970
f4f13f91f2 Merge pull request #331 from fudiwei/bugfix/qiniu-wildcard-domain
bugfix #330
2024-11-13 08:14:32 +08:00
usual2970
034aa980e6 Merge pull request #329 from fudiwei/bugfix/aliyun-clb-deploy-error
bugfix #326
2024-11-13 08:14:19 +08:00
usual2970
6ac7a51ce0 Merge pull request #328 from fudiwei/bugfix/tencentcloud-deploy-config-not-saving
bugfix #324
2024-11-13 08:14:05 +08:00
usual2970
cf0c0e3e2c Merge pull request #327 from LeoChen98/fix-tencent-cos-instance-not-found
fixed: instance not found when deploying tencent COS
2024-11-13 08:13:49 +08:00
Fu Diwei
23e5cb5669 fix: #330 2024-11-12 21:41:06 +08:00
Fu Diwei
e4ba4c9b37 fix: #326 2024-11-12 20:35:31 +08:00
Fu Diwei
9ed64bdc9a fix: #324 2024-11-12 20:20:54 +08:00
Leo Chen
e9b6fb55ff fixed: instance possible not found when deploying tencent CLB via SSL api
修复了重构导致腾讯云CLB通过SSL接口部署时可能找不到实例的bug
2024-11-12 17:59:13 +08:00
Leo Chen
80caf881ae fixed: instance not found when deploying tencent COS
修复了重构导致腾讯云COS部署时找不到实例的bug
2024-11-12 17:56:41 +08:00
usual2970
c36db3545f Merge pull request #321 from fudiwei/feat/notifier
feat: notifiers
2024-11-11 18:16:30 +08:00
yoan
a367585ab4 v0.2.18 2024-11-11 07:58:13 +08:00
Fu Diwei
2994cb5c65 test: add unit test case for email notifier 2024-11-10 20:28:01 +08:00
Fu Diwei
1bedb31a3c fix: fix typo 2024-11-10 20:06:18 +08:00
Fu Diwei
8fecebc254 feat: show loading button when pushing test notifications 2024-11-10 20:00:19 +08:00
Fu Diwei
44497a0969 feat: new UI for notify settings 2024-11-10 19:52:50 +08:00
usual2970
5362371bda Merge pull request #319 from fudiwei/bugfix/aliyun-api-error
bugfix #318
2024-11-10 19:40:40 +08:00
Fu Diwei
8b04e96a7d feat: new UI for email notify settings 2024-11-10 18:21:43 +08:00
Fu Diwei
5d93334426 refactor: re-implement logic of notify 2024-11-10 18:03:20 +08:00
Fu Diwei
150b666d4b refactor: maps utils 2024-11-09 20:46:49 +08:00
Fu Diwei
94579d65c4 refactor: clean code 2024-11-09 20:29:13 +08:00
Fu Diwei
551b06b4e8 feat: notifier 2024-11-09 20:06:22 +08:00
Fu Diwei
76fc47a274 Merge branch 'main' into feat/notifier 2024-11-09 12:14:21 +08:00
yoan
35e1bfcd7f Update readme 2024-11-09 11:37:34 +08:00
Fu Diwei
24df7913fe feat: support aliyun global ALB/NLB 2024-11-09 09:54:49 +08:00
Fu Diwei
83674e4b35 refactor: ensure compile-time check for Uploader implementations 2024-11-09 09:47:14 +08:00
Fu Diwei
22d3aeb7b5 fix: #318 2024-11-09 09:41:05 +08:00
yoan
cf005711c0 v0.2.17 2024-11-08 08:11:04 +08:00
usual2970
0a00d0c52f Merge pull request #314 from fudiwei/bugfix/dogecloud-api-error
bugfix #313
2024-11-08 08:10:18 +08:00
usual2970
9aa17a0395 Merge pull request #315 from fudiwei/bugfix/qiniu-panic
bugfix #304
2024-11-08 08:09:41 +08:00
Fu Diwei
65ecdf7dc2 update README 2024-11-07 17:36:41 +08:00
Fu Diwei
0dfa5994cc fix: #304 2024-11-07 17:35:43 +08:00
Fu Diwei
5d2844fdb6 fix: #313 2024-11-07 15:01:46 +08:00
yoan
44332b9d07 v0.2.16 2024-11-07 08:09:25 +08:00
usual2970
20a23e148c Merge pull request #309 from fudiwei/bugfix/dogecloud-api-error
bugfix #308
2024-11-07 08:06:55 +08:00
RHQYZ
0bcb6206f4 fix #308 2024-11-06 11:07:24 +08:00
yoan
943b9827ee v0.2.15 2024-11-06 07:12:48 +08:00
usual2970
741f3ec212 Merge pull request #306 from fudiwei/bugfix/dogecloud-api-error
bugfix #303
2024-11-06 07:08:12 +08:00
Fu Diwei
8549a17675 fix: #303 2024-11-05 18:16:21 +08:00
yoan
718cfccbea resolve new sftp client failure 2024-11-05 08:35:37 +08:00
yoan
2458fa26d8 v0.2.14 2024-11-05 08:30:28 +08:00
yoan
ac24684d2b Merge branch 'main' of github.com:usual2970/certimate 2024-11-05 08:29:58 +08:00
yoan
106dbd9538 Merge branch 'fudiwei-feat/cloud-cdn' 2024-11-05 08:29:30 +08:00
yoan
f9efb2b800 migration 2024-11-05 08:28:35 +08:00
usual2970
897d124d5b Merge pull request #299 from fudiwei/bugfix/ssh-jks
bugfix #298
2024-11-05 08:15:13 +08:00
Fu Diwei
34daf9ccac refactor: clean code 2024-11-04 12:54:23 +08:00
Fu Diwei
269a97e81e feat: add baiducloud cdn deployer 2024-11-04 12:44:53 +08:00
Fu Diwei
2fd57621d8 fix: #298 2024-11-04 11:20:35 +08:00
Fu Diwei
76de837214 feat: add baiducloud provider 2024-11-04 11:11:00 +08:00
Fu Diwei
1e41020728 feat: add dogecloud cdn deployer 2024-11-04 10:34:05 +08:00
Fu Diwei
8a78e49bf0 feat: add dogecloud provider 2024-11-04 10:30:18 +08:00
75 changed files with 3718 additions and 1123 deletions

View File

@@ -75,8 +75,10 @@ make local.run
| :--------: | :----------: | :----------: | ----------------------------------------------------------------- |
| 阿里云 | √ | √ | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN、SLB |
| 腾讯云 | √ | √ | 可签发在腾讯云注册的域名;可部署到腾讯云 COS、CDN、ECDN、CLB、TEO |
| 百度智能云 | | √ | 可部署到百度智能云 CDN |
| 华为云 | √ | √ | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB |
| 七牛云 | | √ | 可部署到七牛云 CDN |
| 多吉云 | | √ | 可部署到多吉云 CDN |
| AWS | √ | | 可签发在 AWS Route53 托管的域名 |
| CloudFlare | √ | | 可签发在 CloudFlare 注册的域名CloudFlare 服务自带 SSL 证书 |
| GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 |
@@ -90,15 +92,13 @@ make local.run
## 四、系统截图
![login](https://i.imgur.com/SYjjbql.jpeg)
![dashboard](https://i.imgur.com/WMVbBId.jpeg)
![domains](https://i.imgur.com/8wit3ZA.jpeg)
![accesses](https://i.imgur.com/EWtOoJ0.jpeg)
![history](https://i.imgur.com/aaPtSW7.jpeg)
<div align="center">
<img src="https://i.imgur.com/SYjjbql.jpeg" title="Login page" width="95%"/>
<img src="https://i.imgur.com/WMVbBId.jpeg" title="Dashboard page" width="47%"/>
<img src="https://i.imgur.com/8wit3ZA.jpeg" title="Domains page" width="47%"/>
<img src="https://i.imgur.com/EWtOoJ0.jpeg" title="Accesses page" width="47%"/>
<img src="https://i.imgur.com/aaPtSW7.jpeg" title="History page" width="47%"/>
</div>
## 五、概念
@@ -188,3 +188,4 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.
## 十、Star 趋势图
[![Stargazers over time](https://starchart.cc/usual2970/certimate.svg?variant=adaptive)](https://starchart.cc/usual2970/certimate)

View File

@@ -70,34 +70,34 @@ password1234567890
## List of Supported Providers
| Provider | Registration | Deployment | Remarks |
| :-----------: | :----------: | :--------: | --------------------------------------------------------------------------------------------------------------------- |
| Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN,SLB |
| Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud COS, CDN, ECDN, CLB, TEO |
| Huawei Cloud | | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB |
| Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud CDN |
| AWS | | | Supports domains managed on AWS Route53 |
| CloudFlare | | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates |
| GoDaddy | √ | | Supports domains registered on GoDaddy |
| Namesilo | √ | | Supports domains registered on Namesilo |
| PowerDNS | √ | | Supports domains managed on PowerDNS |
| HTTP Request | √ | | Supports domains which allow managing DNS by HTTP request |
| Local Deploy | | | Supports deployment to local servers |
| SSH | | | Supports deployment to SSH servers |
| Webhook | | √ | Supports callback to Webhook |
| Kubernetes | | √ | Supports deployment to Kubernetes Secret |
| Provider | Registration | Deployment | Remarks |
| :-----------: | :----------: | :--------: | ----------------------------------------------------------------------------------------------------------- |
| Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN,SLB |
| Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud COS, CDN, ECDN, CLB, TEO |
| Baidu Cloud | | √ | Supports deployment to Baidu Cloud CDN |
| Huawei Cloud | | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB |
| Qiniu Cloud | | | Supports deployment to Qiniu Cloud CDN |
| Doge Cloud | | | Supports deployment to Doge Cloud CDN |
| AWS | √ | | Supports domains managed on AWS Route53 |
| CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates |
| GoDaddy | √ | | Supports domains registered on GoDaddy |
| Namesilo | √ | | Supports domains registered on Namesilo |
| PowerDNS | | | Supports domains managed on PowerDNS |
| HTTP Request | | | Supports domains which allow managing DNS by HTTP request |
| Local Deploy | | √ | Supports deployment to local servers |
| SSH | | √ | Supports deployment to SSH servers |
| Webhook | | √ | Supports callback to Webhook |
| Kubernetes | | √ | Supports deployment to Kubernetes Secret |
## Screenshots
![login](https://i.imgur.com/SYjjbql.jpeg)
![dashboard](https://i.imgur.com/WMVbBId.jpeg)
![domains](https://i.imgur.com/8wit3ZA.jpeg)
![accesses](https://i.imgur.com/EWtOoJ0.jpeg)
![history](https://i.imgur.com/aaPtSW7.jpeg)
<div align="center">
<img src="https://i.imgur.com/SYjjbql.jpeg" title="Login page" width="95%"/>
<img src="https://i.imgur.com/WMVbBId.jpeg" title="Dashboard page" width="47%"/>
<img src="https://i.imgur.com/8wit3ZA.jpeg" title="Domains page" width="47%"/>
<img src="https://i.imgur.com/EWtOoJ0.jpeg" title="Accesses page" width="47%"/>
<img src="https://i.imgur.com/aaPtSW7.jpeg" title="History page" width="47%"/>
</div>
## Concepts

3
go.mod
View File

@@ -13,6 +13,7 @@ require (
github.com/alibabacloud-go/slb-20140515/v4 v4.0.9
github.com/alibabacloud-go/tea v1.2.2
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/baidubce/bce-sdk-go v0.9.197
github.com/go-acme/lego/v4 v4.19.2
github.com/gojek/heimdall/v7 v7.0.3
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114
@@ -112,7 +113,7 @@ require (
github.com/cloudflare/cloudflare-go v0.104.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.6 // indirect

2
go.sum
View File

@@ -159,6 +159,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o=
github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/baidubce/bce-sdk-go v0.9.197 h1:TQqa4J+FTagrywhaTQ707ffE1eG3ix1s06eSZ/K+Wk0=
github.com/baidubce/bce-sdk-go v0.9.197/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/blinkbean/dingtalk v1.1.3 h1:MbidFZYom7DTFHD/YIs+eaI7kRy52kmWE/sy0xjo6E4=
github.com/blinkbean/dingtalk v1.1.3/go.mod h1:9BaLuGSBqY3vT5hstValh48DbsKO7vaHaJnG9pXwbto=
github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI=

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"
aliyunAlb "github.com/alibabacloud-go/alb-20200616/v2/client"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
@@ -39,10 +40,21 @@ func NewAliyunALBDeployer(option *DeployerOption) (Deployer, error) {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
aliCasRegion := option.DeployConfig.GetConfigAsString("region")
if aliCasRegion != "" {
// 阿里云 CAS 服务接入点是独立于 ALB 服务的
// 国内版接入点:华东一杭州
// 国际版接入点:亚太东南一新加坡
if !strings.HasPrefix(aliCasRegion, "cn-") {
aliCasRegion = "ap-southeast-1"
} else {
aliCasRegion = "cn-hangzhou"
}
}
uploader, err := uploaderAliyunCas.New(&uploaderAliyunCas.AliyunCASUploaderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Region: option.DeployConfig.GetConfigAsString("region"),
Region: aliCasRegion,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")

View File

@@ -246,10 +246,10 @@ func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, aliLo
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setdomainextensionattribute
//
// 这里仅修改跟被替换证书一致的扩展域名
if describeDomainExtensionsResp.Body.DomainExtensions == nil && describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension == nil {
if describeDomainExtensionsResp.Body.DomainExtensions != nil && describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension != nil {
for _, domainExtension := range describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension {
if *domainExtension.ServerCertificateId == *describeLoadBalancerHTTPSListenerAttributeResp.Body.ServerCertificateId {
break
if *domainExtension.ServerCertificateId != *describeLoadBalancerHTTPSListenerAttributeResp.Body.ServerCertificateId {
continue
}
setDomainExtensionAttributeReq := &aliyunSlb.SetDomainExtensionAttributeRequest{

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliyunNlb "github.com/alibabacloud-go/nlb-20220430/v2/client"
@@ -39,10 +40,21 @@ func NewAliyunNLBDeployer(option *DeployerOption) (Deployer, error) {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
aliCasRegion := option.DeployConfig.GetConfigAsString("region")
if aliCasRegion != "" {
// 阿里云 CAS 服务接入点是独立于 NLB 服务的
// 国内版接入点:华东一杭州
// 国际版接入点:亚太东南一新加坡
if !strings.HasPrefix(aliCasRegion, "cn-") {
aliCasRegion = "ap-southeast-1"
} else {
aliCasRegion = "cn-hangzhou"
}
}
uploader, err := uploaderAliyunCas.New(&uploaderAliyunCas.AliyunCASUploaderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Region: option.DeployConfig.GetConfigAsString("region"),
Region: aliCasRegion,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")

View File

@@ -0,0 +1,80 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"time"
bceCdn "github.com/baidubce/bce-sdk-go/services/cdn"
bceCdnApi "github.com/baidubce/bce-sdk-go/services/cdn/api"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
)
type BaiduCloudCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *bceCdn.Client
}
func NewBaiduCloudCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.BaiduCloudAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&BaiduCloudCDNDeployer{}).createSdkClient(
access.AccessKeyId,
access.SecretAccessKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &BaiduCloudCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
}, nil
}
func (d *BaiduCloudCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *BaiduCloudCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *BaiduCloudCDNDeployer) Deploy(ctx context.Context) error {
// 修改域名证书
// REF: https://cloud.baidu.com/doc/CDN/s/qjzuz2hp8
putCertResp, err := d.sdkClient.PutCert(
d.option.DeployConfig.GetConfigAsString("domain"),
&bceCdnApi.UserCertificate{
CertName: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
ServerData: d.option.Certificate.Certificate,
PrivateData: d.option.Certificate.PrivateKey,
},
"ON",
)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.PutCert'")
}
d.infos = append(d.infos, toStr("已修改域名证书", putCertResp))
return nil
}
func (d *BaiduCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey string) (*bceCdn.Client, error) {
client, err := bceCdn.NewClient(accessKeyId, secretAccessKey, "")
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -33,7 +33,9 @@ const (
targetTencentTEO = "tencent-teo"
targetHuaweiCloudCDN = "huaweicloud-cdn"
targetHuaweiCloudELB = "huaweicloud-elb"
targetBaiduCloudCDN = "baiducloud-cdn"
targetQiniuCdn = "qiniu-cdn"
targetDogeCloudCdn = "dogecloud-cdn"
targetLocal = "local"
targetSSH = "ssh"
targetWebhook = "webhook"
@@ -134,8 +136,12 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
return NewHuaweiCloudCDNDeployer(option)
case targetHuaweiCloudELB:
return NewHuaweiCloudELBDeployer(option)
case targetBaiduCloudCDN:
return NewBaiduCloudCDNDeployer(option)
case targetQiniuCdn:
return NewQiniuCDNDeployer(option)
case targetDogeCloudCdn:
return NewDogeCloudCDNDeployer(option)
case targetLocal:
return NewLocalDeployer(option)
case targetSSH:

View File

@@ -0,0 +1,88 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"strconv"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderDoge "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/dogecloud"
doge "github.com/usual2970/certimate/internal/pkg/vendors/dogecloud-sdk"
)
type DogeCloudCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *doge.Client
sslUploader uploader.Uploader
}
func NewDogeCloudCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.DogeCloudAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&DogeCloudCDNDeployer{}).createSdkClient(
access.AccessKey,
access.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploaderDoge.New(&uploaderDoge.DogeCloudUploaderConfig{
AccessKey: access.AccessKey,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &DogeCloudCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *DogeCloudCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *DogeCloudCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *DogeCloudCDNDeployer) Deploy(ctx context.Context) error {
// 上传证书到 CDN
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 绑定证书
// REF: https://docs.dogecloud.com/cdn/api-cert-bind
bindCdnCertId, _ := strconv.ParseInt(upres.CertId, 10, 64)
bindCdnCertResp, err := d.sdkClient.BindCdnCertWithDomain(bindCdnCertId, d.option.DeployConfig.GetConfigAsString("domain"))
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.BindCdnCert'")
}
d.infos = append(d.infos, toStr("已绑定证书", bindCdnCertResp))
return nil
}
func (d *DogeCloudCDNDeployer) createSdkClient(accessKey, secretKey string) (*doge.Client, error) {
client := doge.NewClient(accessKey, secretKey)
return client, nil
}

View File

@@ -105,6 +105,9 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
}
d.infos = append(d.infos, toStr("保存证书成功", nil))
default:
return errors.New("unsupported format")
}
// 执行命令

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
xerrors "github.com/pkg/errors"
"github.com/qiniu/go-sdk/v7/auth"
@@ -69,9 +70,14 @@ func (d *QiniuCDNDeployer) Deploy(ctx context.Context) error {
d.infos = append(d.infos, toStr("已上传证书", upres))
// 在七牛 CDN 中泛域名表示为 .example.com需去除前缀星号
domain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(domain, "*") {
domain = strings.TrimPrefix(domain, "*")
}
// 获取域名信息
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
domain := d.option.DeployConfig.GetConfigAsString("domain")
getDomainInfoResp, err := d.sdkClient.GetDomainInfo(domain)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDomainInfo'")

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@@ -13,7 +14,6 @@ import (
"golang.org/x/crypto/ssh"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/fs"
)
type SSHDeployer struct {
@@ -105,11 +105,14 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
return err
}
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
if err := d.writeSftpFile(client, d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
return err
}
d.infos = append(d.infos, toStr("保存证书成功", nil))
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
default:
return errors.New("unsupported format")
}
// 执行命令
@@ -156,8 +159,8 @@ func (d *SSHDeployer) createSshClient(access *domain.SSHAccess) (*ssh.Client, er
})
}
func (d *SSHDeployer) sshExecCommand(client *ssh.Client, command string) (string, string, error) {
session, err := client.NewSession()
func (d *SSHDeployer) sshExecCommand(sshCli *ssh.Client, command string) (string, string, error) {
session, err := sshCli.NewSession()
if err != nil {
return "", "", xerrors.Wrap(err, "failed to create ssh session")
}
@@ -175,12 +178,12 @@ func (d *SSHDeployer) sshExecCommand(client *ssh.Client, command string) (string
return stdoutBuf.String(), stderrBuf.String(), nil
}
func (d *SSHDeployer) writeSftpFileString(client *ssh.Client, path string, content string) error {
return d.writeSftpFile(client, path, []byte(content))
func (d *SSHDeployer) writeSftpFileString(sshCli *ssh.Client, path string, content string) error {
return d.writeSftpFile(sshCli, path, []byte(content))
}
func (d *SSHDeployer) writeSftpFile(client *ssh.Client, path string, data []byte) error {
sftpCli, err := sftp.NewClient(client)
func (d *SSHDeployer) writeSftpFile(sshCli *ssh.Client, path string, data []byte) error {
sftpCli, err := sftp.NewClient(sshCli)
if err != nil {
return xerrors.Wrap(err, "failed to create sftp client")
}

View File

@@ -70,8 +70,6 @@ func (d *TencentCLBDeployer) GetInfos() []string {
}
func (d *TencentCLBDeployer) Deploy(ctx context.Context) error {
// TODO: 直接部署方式
switch d.option.DeployConfig.GetConfigAsString("resourceType") {
case "ssl-deploy":
// 通过 SSL 服务部署到云资源实例
@@ -104,7 +102,7 @@ func (d *TencentCLBDeployer) Deploy(ctx context.Context) error {
func (d *TencentCLBDeployer) createSdkClients(secretId, secretKey, region string) (*tencentCLBDeployerSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
sslClient, err := tcSsl.NewClient(credential, region, profile.NewClientProfile())
if err != nil {
return nil, err
}

View File

@@ -33,6 +33,7 @@ func NewTencentCOSDeployer(option *DeployerOption) (Deployer, error) {
client, err := (&TencentCOSDeployer{}).createSdkClient(
access.SecretId,
access.SecretKey,
option.DeployConfig.GetConfigAsString("region"),
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
@@ -95,9 +96,9 @@ func (d *TencentCOSDeployer) Deploy(ctx context.Context) error {
return nil
}
func (d *TencentCOSDeployer) createSdkClient(secretId, secretKey string) (*tcSsl.Client, error) {
func (d *TencentCOSDeployer) createSdkClient(secretId, secretKey, region string) (*tcSsl.Client, error) {
credential := common.NewCredential(secretId, secretKey)
client, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
client, err := tcSsl.NewClient(credential, region, profile.NewClientProfile())
if err != nil {
return nil, err
}

View File

@@ -11,7 +11,12 @@ type TencentAccess struct {
}
type HuaweiCloudAccess struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
Region string `json:"region"`
}
type BaiduCloudAccess struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
}
@@ -32,6 +37,11 @@ type QiniuAccess struct {
SecretKey string `json:"secretKey"`
}
type DogeCloudAccess struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
}
type NameSiloAccess struct {
ApiKey string `json:"apiKey"`
}

View File

@@ -3,6 +3,8 @@ package domain
import (
"encoding/json"
"strings"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
)
type ApplyConfig struct {
@@ -29,7 +31,7 @@ type DeployConfig struct {
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回空字符串。
func (dc *DeployConfig) GetConfigAsString(key string) string {
return dc.GetConfigOrDefaultAsString(key, "")
return maps.GetValueAsString(dc.Config, key)
}
// 以字符串形式获取配置项。
@@ -41,17 +43,7 @@ func (dc *DeployConfig) GetConfigAsString(key string) string {
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue string) string {
if dc.Config == nil {
return defaultValue
}
if value, ok := dc.Config[key]; ok {
if result, ok := value.(string); ok {
return result
}
}
return defaultValue
return maps.GetValueOrDefaultAsString(dc.Config, key, defaultValue)
}
// 以 32 位整数形式获取配置项。
@@ -62,7 +54,7 @@ func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue stri
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回 0。
func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
return dc.GetConfigOrDefaultAsInt32(key, 0)
return maps.GetValueAsInt32(dc.Config, key)
}
// 以 32 位整数形式获取配置项。
@@ -74,17 +66,7 @@ func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 {
if dc.Config == nil {
return defaultValue
}
if value, ok := dc.Config[key]; ok {
if result, ok := value.(int32); ok {
return result
}
}
return defaultValue
return maps.GetValueOrDefaultAsInt32(dc.Config, key, defaultValue)
}
// 以布尔形式获取配置项。
@@ -95,7 +77,7 @@ func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回 false。
func (dc *DeployConfig) GetConfigAsBool(key string) bool {
return dc.GetConfigOrDefaultAsBool(key, false)
return maps.GetValueAsBool(dc.Config, key)
}
// 以布尔形式获取配置项。
@@ -107,17 +89,7 @@ func (dc *DeployConfig) GetConfigAsBool(key string) bool {
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool) bool {
if dc.Config == nil {
return defaultValue
}
if value, ok := dc.Config[key]; ok {
if result, ok := value.(bool); ok {
return result
}
}
return defaultValue
return maps.GetValueOrDefaultAsBool(dc.Config, key, defaultValue)
}
// 以变量字典形式获取配置项。

View File

@@ -1,12 +1,12 @@
package domain
const (
NotifyChannelDingtalk = "dingtalk"
NotifyChannelEmail = "email"
NotifyChannelWebhook = "webhook"
NotifyChannelTelegram = "telegram"
NotifyChannelDingtalk = "dingtalk"
NotifyChannelLark = "lark"
NotifyChannelTelegram = "telegram"
NotifyChannelServerChan = "serverchan"
NotifyChannelMail = "mail"
NotifyChannelBark = "bark"
)

View File

@@ -24,7 +24,7 @@ func (s *Setting) GetChannelContent(channel string) (map[string]any, error) {
v, ok := (*conf)[channel]
if !ok {
return nil, fmt.Errorf("channel %s not found", channel)
return nil, fmt.Errorf("channel \"%s\" not found", channel)
}
return v, nil

View File

@@ -12,19 +12,13 @@ import (
"github.com/usual2970/certimate/internal/utils/xtime"
)
type msg struct {
subject string
message string
}
const (
defaultExpireSubject = "您有{COUNT}张证书即将过期"
defaultExpireMsg = "有{COUNT}张证书即将过期,域名分别为{DOMAINS},请保持关注!"
defaultExpireSubject = "您有 {COUNT} 张证书即将过期"
defaultExpireMessage = "有 {COUNT} 张证书即将过期域名分别为 {DOMAINS}请保持关注!"
)
func PushExpireMsg() {
// 查询即将过期的证书
records, err := app.GetApp().Dao().FindRecordsByFilter("domains", "expiredAt<{:time}&&certUrl!=''", "-created", 500, 0,
dbx.Params{"time": xtime.GetTimeAfter(24 * time.Hour * 15)})
if err != nil {
@@ -34,12 +28,12 @@ func PushExpireMsg() {
// 组装消息
msg := buildMsg(records)
if msg == nil {
return
}
if err := Send(msg.subject, msg.message); err != nil {
// 发送通知
if err := SendToAllChannels(msg.Subject, msg.Message); err != nil {
app.GetApp().Logger().Error("send expire msg", "error", err)
}
}
@@ -53,22 +47,27 @@ type notifyTemplate struct {
Content string `json:"content"`
}
func buildMsg(records []*models.Record) *msg {
type notifyMessage struct {
Subject string
Message string
}
func buildMsg(records []*models.Record) *notifyMessage {
if len(records) == 0 {
return nil
}
// 查询模板信息
templateRecord, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='templates'")
title := defaultExpireSubject
content := defaultExpireMsg
subject := defaultExpireSubject
message := defaultExpireMessage
if err == nil {
var templates *notifyTemplates
templateRecord.UnmarshalJSONField("content", templates)
if templates != nil && len(templates.NotifyTemplates) > 0 {
title = templates.NotifyTemplates[0].Title
content = templates.NotifyTemplates[0].Content
subject = templates.NotifyTemplates[0].Title
message = templates.NotifyTemplates[0].Content
}
}
@@ -81,17 +80,17 @@ func buildMsg(records []*models.Record) *msg {
}
countStr := strconv.Itoa(count)
domainStr := strings.Join(domains, ",")
domainStr := strings.Join(domains, ";")
title = strings.ReplaceAll(title, "{COUNT}", countStr)
title = strings.ReplaceAll(title, "{DOMAINS}", domainStr)
subject = strings.ReplaceAll(subject, "{COUNT}", countStr)
subject = strings.ReplaceAll(subject, "{DOMAINS}", domainStr)
content = strings.ReplaceAll(content, "{COUNT}", countStr)
content = strings.ReplaceAll(content, "{DOMAINS}", domainStr)
message = strings.ReplaceAll(message, "{COUNT}", countStr)
message = strings.ReplaceAll(message, "{DOMAINS}", domainStr)
// 返回消息
return &msg{
subject: title,
message: content,
return &notifyMessage{
Subject: subject,
Message: message,
}
}

View File

@@ -0,0 +1,66 @@
package notify
import (
"errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
notifierBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
notifierDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
notifierEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
notifierLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
notifierServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
notifierTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
notifierWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
)
func createNotifier(channel string, channelConfig map[string]any) (notifier.Notifier, error) {
switch channel {
case domain.NotifyChannelEmail:
return notifierEmail.New(&notifierEmail.EmailNotifierConfig{
SmtpHost: maps.GetValueAsString(channelConfig, "smtpHost"),
SmtpPort: maps.GetValueAsInt32(channelConfig, "smtpPort"),
SmtpTLS: maps.GetValueOrDefaultAsBool(channelConfig, "smtpTLS", true),
Username: maps.GetValueOrDefaultAsString(channelConfig, "username", maps.GetValueAsString(channelConfig, "senderAddress")),
Password: maps.GetValueAsString(channelConfig, "password"),
SenderAddress: maps.GetValueAsString(channelConfig, "senderAddress"),
ReceiverAddress: maps.GetValueAsString(channelConfig, "receiverAddress"),
})
case domain.NotifyChannelWebhook:
return notifierWebhook.New(&notifierWebhook.WebhookNotifierConfig{
Url: maps.GetValueAsString(channelConfig, "url"),
})
case domain.NotifyChannelDingtalk:
return notifierDingTalk.New(&notifierDingTalk.DingTalkNotifierConfig{
AccessToken: maps.GetValueAsString(channelConfig, "accessToken"),
Secret: maps.GetValueAsString(channelConfig, "secret"),
})
case domain.NotifyChannelLark:
return notifierLark.New(&notifierLark.LarkNotifierConfig{
WebhookUrl: maps.GetValueAsString(channelConfig, "webhookUrl"),
})
case domain.NotifyChannelTelegram:
return notifierTelegram.New(&notifierTelegram.TelegramNotifierConfig{
ApiToken: maps.GetValueAsString(channelConfig, "apiToken"),
ChatId: maps.GetValueAsInt64(channelConfig, "chatId"),
})
case domain.NotifyChannelServerChan:
return notifierServerChan.New(&notifierServerChan.ServerChanNotifierConfig{
Url: maps.GetValueAsString(channelConfig, "url"),
})
case domain.NotifyChannelBark:
return notifierBark.New(&notifierBark.BarkNotifierConfig{
DeviceKey: maps.GetValueAsString(channelConfig, "deviceKey"),
ServerUrl: maps.GetValueAsString(channelConfig, "serverUrl"),
})
}
return nil, errors.New("unsupported notifier channel")
}

View File

@@ -1,56 +0,0 @@
package notify
import (
"context"
"fmt"
"net/mail"
"strconv"
"github.com/pocketbase/pocketbase/tools/mailer"
)
const defaultSmtpHostPort = "25"
type Mail struct {
username string
to string
client *mailer.SmtpClient
}
func NewMail(senderAddress, receiverAddresses, smtpHostAddr, smtpHostPort, password string) (*Mail, error) {
if smtpHostPort == "" {
smtpHostPort = defaultSmtpHostPort
}
port, err := strconv.Atoi(smtpHostPort)
if err != nil {
return nil, fmt.Errorf("invalid smtp port: %w", err)
}
client := mailer.SmtpClient{
Host: smtpHostAddr,
Port: port,
Username: senderAddress,
Password: password,
Tls: true,
}
return &Mail{
username: senderAddress,
client: &client,
to: receiverAddresses,
}, nil
}
func (m *Mail) Send(ctx context.Context, subject, content string) error {
message := &mailer.Message{
From: mail.Address{
Address: m.username,
},
To: []mail.Address{{Address: m.to}},
Subject: subject,
Text: content,
}
return m.client.Send(message)
}

View File

@@ -3,24 +3,16 @@ package notify
import (
"context"
"fmt"
"strconv"
stdhttp "net/http"
"golang.org/x/sync/errgroup"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
"github.com/usual2970/certimate/internal/utils/app"
notifyPackage "github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/bark"
"github.com/nikoksr/notify/service/dingding"
"github.com/nikoksr/notify/service/http"
"github.com/nikoksr/notify/service/lark"
"github.com/nikoksr/notify/service/telegram"
)
func Send(title, content string) error {
// 获取所有的推送渠道
notifiers, err := getNotifiers()
func SendToAllChannels(subject, message string) error {
notifiers, err := getEnabledNotifiers()
if err != nil {
return err
}
@@ -28,184 +20,56 @@ func Send(title, content string) error {
return nil
}
n := notifyPackage.New()
// 添加推送渠道
n.UseServices(notifiers...)
var eg errgroup.Group
for _, n := range notifiers {
if n == nil {
continue
}
// 发送消息
return n.Send(context.Background(), title, content)
eg.Go(func() error {
_, err := n.Notify(context.Background(), subject, message)
return err
})
}
err = eg.Wait()
return err
}
type sendTestParam struct {
Title string `json:"title"`
Content string `json:"content"`
Channel string `json:"channel"`
Conf map[string]any `json:"conf"`
}
func SendTest(param *sendTestParam) error {
notifier, err := getNotifier(param.Channel, param.Conf)
func SendToChannel(subject, message string, channel string, channelConfig map[string]any) error {
notifier, err := createNotifier(channel, channelConfig)
if err != nil {
return err
}
n := notifyPackage.New()
// 添加推送渠道
n.UseServices(notifier)
// 发送消息
return n.Send(context.Background(), param.Title, param.Content)
_, err = notifier.Notify(context.Background(), subject, message)
return err
}
func getNotifiers() ([]notifyPackage.Notifier, error) {
resp, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='notifyChannels'")
func getEnabledNotifiers() ([]notifier.Notifier, error) {
settings, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='notifyChannels'")
if err != nil {
return nil, fmt.Errorf("find notifyChannels error: %w", err)
}
notifiers := make([]notifyPackage.Notifier, 0)
rs := make(map[string]map[string]any)
if err := resp.UnmarshalJSONField("content", &rs); err != nil {
if err := settings.UnmarshalJSONField("content", &rs); err != nil {
return nil, fmt.Errorf("unmarshal notifyChannels error: %w", err)
}
notifiers := make([]notifier.Notifier, 0)
for k, v := range rs {
if !getBool(v, "enabled") {
if !maps.GetValueAsBool(v, "enabled") {
continue
}
notifier, err := getNotifier(k, v)
notifier, err := createNotifier(k, v)
if err != nil {
continue
}
notifiers = append(notifiers, notifier)
}
return notifiers, nil
}
func getNotifier(channel string, conf map[string]any) (notifyPackage.Notifier, error) {
switch channel {
case domain.NotifyChannelTelegram:
temp := getTelegramNotifier(conf)
if temp == nil {
return nil, fmt.Errorf("telegram notifier config error")
}
return temp, nil
case domain.NotifyChannelDingtalk:
return getDingTalkNotifier(conf), nil
case domain.NotifyChannelLark:
return getLarkNotifier(conf), nil
case domain.NotifyChannelWebhook:
return getWebhookNotifier(conf), nil
case domain.NotifyChannelServerChan:
return getServerChanNotifier(conf), nil
case domain.NotifyChannelMail:
return getMailNotifier(conf)
case domain.NotifyChannelBark:
return getBarkNotifier(conf), nil
}
return nil, fmt.Errorf("notifier not found")
}
func getWebhookNotifier(conf map[string]any) notifyPackage.Notifier {
rs := http.New()
rs.AddReceiversURLs(getString(conf, "url"))
return rs
}
func getTelegramNotifier(conf map[string]any) notifyPackage.Notifier {
rs, err := telegram.New(getString(conf, "apiToken"))
if err != nil {
return nil
}
chatId := getString(conf, "chatId")
id, err := strconv.ParseInt(chatId, 10, 64)
if err != nil {
return nil
}
rs.AddReceivers(id)
return rs
}
func getServerChanNotifier(conf map[string]any) notifyPackage.Notifier {
rs := http.New()
rs.AddReceivers(&http.Webhook{
URL: getString(conf, "url"),
Header: stdhttp.Header{},
ContentType: "application/json",
Method: stdhttp.MethodPost,
BuildPayload: func(subject, message string) (payload any) {
return map[string]string{
"text": subject,
"desp": message,
}
},
})
return rs
}
func getBarkNotifier(conf map[string]any) notifyPackage.Notifier {
deviceKey := getString(conf, "deviceKey")
serverURL := getString(conf, "serverUrl")
if serverURL == "" {
return bark.New(deviceKey)
}
return bark.NewWithServers(deviceKey, serverURL)
}
func getDingTalkNotifier(conf map[string]any) notifyPackage.Notifier {
return dingding.New(&dingding.Config{
Token: getString(conf, "accessToken"),
Secret: getString(conf, "secret"),
})
}
func getLarkNotifier(conf map[string]any) notifyPackage.Notifier {
return lark.NewWebhookService(getString(conf, "webhookUrl"))
}
func getMailNotifier(conf map[string]any) (notifyPackage.Notifier, error) {
rs, err := NewMail(getString(conf, "senderAddress"),
getString(conf, "receiverAddresses"),
getString(conf, "smtpHostAddr"),
getString(conf, "smtpHostPort"),
getString(conf, "password"),
)
if err != nil {
return nil, err
}
return rs, nil
}
func getString(conf map[string]any, key string) string {
if _, ok := conf[key]; !ok {
return ""
}
return conf[key].(string)
}
func getBool(conf map[string]any, key string) bool {
if _, ok := conf[key]; !ok {
return false
}
return conf[key].(bool)
}

View File

@@ -29,18 +29,13 @@ func NewNotifyService(settingRepo SettingRepository) *NotifyService {
func (n *NotifyService) Test(ctx context.Context, req *domain.NotifyTestPushReq) error {
setting, err := n.settingRepo.GetByName(ctx, "notifyChannels")
if err != nil {
return fmt.Errorf("get notify channels setting failed: %w", err)
return fmt.Errorf("failed to get notify channels settings: %w", err)
}
conf, err := setting.GetChannelContent(req.Channel)
channelConfig, err := setting.GetChannelContent(req.Channel)
if err != nil {
return fmt.Errorf("get notify channel %s config failed: %w", req.Channel, err)
return fmt.Errorf("failed to get notify channel \"%s\" config: %w", req.Channel, err)
}
return SendTest(&sendTestParam{
Title: notifyTestTitle,
Content: notifyTestBody,
Channel: req.Channel,
Conf: conf,
})
return SendToChannel(notifyTestTitle, notifyTestBody, req.Channel, channelConfig)
}

View File

@@ -0,0 +1,23 @@
package notifier
import "context"
// 表示定义消息通知器的抽象类型接口。
type Notifier interface {
// 发送通知。
//
// 入参:
// - ctx上下文。
// - subject通知主题。
// - message通知内容。
//
// 出参:
// - res发送结果。
// - err: 错误。
Notify(ctx context.Context, subject string, message string) (res *NotifyResult, err error)
}
// 表示通知发送结果的数据结构。
type NotifyResult struct {
NotificationData map[string]any `json:"notificationData,omitempty"`
}

View File

@@ -0,0 +1,48 @@
package bark
import (
"context"
"errors"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/bark"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type BarkNotifierConfig struct {
ServerUrl string `json:"serverUrl"`
DeviceKey string `json:"deviceKey"`
}
type BarkNotifier struct {
config *BarkNotifierConfig
}
var _ notifier.Notifier = (*BarkNotifier)(nil)
func New(config *BarkNotifierConfig) (*BarkNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &BarkNotifier{
config: config,
}, nil
}
func (n *BarkNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
var srv notify.Notifier
if n.config.ServerUrl == "" {
srv = bark.New(n.config.DeviceKey)
} else {
srv = bark.NewWithServers(n.config.DeviceKey, n.config.ServerUrl)
}
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,45 @@
package dingtalk
import (
"context"
"errors"
"github.com/nikoksr/notify/service/dingding"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type DingTalkNotifierConfig struct {
AccessToken string `json:"accessToken"`
Secret string `json:"secret"`
}
type DingTalkNotifier struct {
config *DingTalkNotifierConfig
}
var _ notifier.Notifier = (*DingTalkNotifier)(nil)
func New(config *DingTalkNotifierConfig) (*DingTalkNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &DingTalkNotifier{
config: config,
}, nil
}
func (n *DingTalkNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := dingding.New(&dingding.Config{
Token: n.config.AccessToken,
Secret: n.config.Secret,
})
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,95 @@
package email
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/smtp"
"github.com/domodwyer/mailyak/v3"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type EmailNotifierConfig struct {
SmtpHost string `json:"smtpHost"`
SmtpPort int32 `json:"smtpPort"`
SmtpTLS bool `json:"smtpTLS"`
Username string `json:"username"`
Password string `json:"password"`
SenderAddress string `json:"senderAddress"`
ReceiverAddress string `json:"receiverAddress"`
}
type EmailNotifier struct {
config *EmailNotifierConfig
}
var _ notifier.Notifier = (*EmailNotifier)(nil)
func New(config *EmailNotifierConfig) (*EmailNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &EmailNotifier{
config: config,
}, nil
}
func (n *EmailNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
var smtpAuth smtp.Auth
if n.config.Username != "" || n.config.Password != "" {
smtpAuth = smtp.PlainAuth("", n.config.Username, n.config.Password, n.config.SmtpHost)
}
var smtpAddr string
if n.config.SmtpPort == 0 {
if n.config.SmtpTLS {
smtpAddr = fmt.Sprintf("%s:465", n.config.SmtpHost)
} else {
smtpAddr = fmt.Sprintf("%s:25", n.config.SmtpHost)
}
} else {
smtpAddr = fmt.Sprintf("%s:%d", n.config.SmtpHost, n.config.SmtpPort)
}
var yak *mailyak.MailYak
if n.config.SmtpTLS {
yak, err = mailyak.NewWithTLS(smtpAddr, smtpAuth, newTlsConfig())
if err != nil {
return nil, err
}
} else {
yak = mailyak.New(smtpAddr, smtpAuth)
}
yak.From(n.config.SenderAddress)
yak.To(n.config.ReceiverAddress)
yak.Subject(subject)
yak.Plain().Set(message)
err = yak.Send()
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}
func newTlsConfig() *tls.Config {
var suiteIds []uint16
for _, suite := range tls.CipherSuites() {
suiteIds = append(suiteIds, suite.ID)
}
for _, suite := range tls.InsecureCipherSuites() {
suiteIds = append(suiteIds, suite.ID)
}
// 为兼容国内部分低版本 TLS 的 SMTP 服务商
return &tls.Config{
MinVersion: tls.VersionTLS10,
CipherSuites: suiteIds,
}
}

View File

@@ -0,0 +1,51 @@
package email_test
import (
"os"
"strconv"
"testing"
notifierEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
)
/*
Shell command to run this test:
CERTIMATE_NOTIFIER_EMAIL_SMTPPORT=465 \
CERTIMATE_NOTIFIER_EMAIL_SMTPTLS=true \
CERTIMATE_NOTIFIER_EMAIL_SMTPHOST="smtp.example.com" \
CERTIMATE_NOTIFIER_EMAIL_USERNAME="your-username" \
CERTIMATE_NOTIFIER_EMAIL_PASSWORD="your-password" \
CERTIMATE_NOTIFIER_EMAIL_SENDERADDRESS="sender@example.com" \
CERTIMATE_NOTIFIER_EMAIL_RECEIVERADDRESS="receiver@example.com" \
go test -v -run TestNotify email_test.go
*/
func TestNotify(t *testing.T) {
smtpPort, err := strconv.ParseInt(os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPPORT"), 10, 32)
if err != nil {
t.Errorf("invalid envvar: %+v", err)
panic(err)
}
smtpTLS, err := strconv.ParseBool(os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPTLS"))
if err != nil {
t.Errorf("invalid envvar: %+v", err)
panic(err)
}
res, err := notifierEmail.New(&notifierEmail.EmailNotifierConfig{
SmtpHost: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPHOST"),
SmtpPort: int32(smtpPort),
SmtpTLS: smtpTLS,
Username: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_USERNAME"),
Password: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_PASSWORD"),
SenderAddress: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SENDERADDRESS"),
ReceiverAddress: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_RECEIVERADDRESS"),
})
if err != nil {
t.Errorf("invalid envvar: %+v", err)
panic(err)
}
t.Logf("notify result: %v", res)
}

View File

@@ -0,0 +1,41 @@
package lark
import (
"context"
"errors"
"github.com/nikoksr/notify/service/lark"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type LarkNotifierConfig struct {
WebhookUrl string `json:"webhookUrl"`
}
type LarkNotifier struct {
config *LarkNotifierConfig
}
var _ notifier.Notifier = (*LarkNotifier)(nil)
func New(config *LarkNotifierConfig) (*LarkNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &LarkNotifier{
config: config,
}, nil
}
func (n *LarkNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := lark.NewWebhookService(n.config.WebhookUrl)
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,55 @@
package serverchan
import (
"context"
"errors"
"net/http"
notifyHttp "github.com/nikoksr/notify/service/http"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type ServerChanNotifierConfig struct {
Url string `json:"url"`
}
type ServerChanNotifier struct {
config *ServerChanNotifierConfig
}
var _ notifier.Notifier = (*ServerChanNotifier)(nil)
func New(config *ServerChanNotifierConfig) (*ServerChanNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &ServerChanNotifier{
config: config,
}, nil
}
func (n *ServerChanNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := notifyHttp.New()
srv.AddReceivers(&notifyHttp.Webhook{
URL: n.config.Url,
Header: http.Header{},
ContentType: "application/json",
Method: http.MethodPost,
BuildPayload: func(subject, message string) (payload any) {
return map[string]string{
"text": subject,
"desp": message,
}
},
})
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,47 @@
package telegram
import (
"context"
"errors"
"github.com/nikoksr/notify/service/telegram"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type TelegramNotifierConfig struct {
ApiToken string `json:"apiToken"`
ChatId int64 `json:"chatId"`
}
type TelegramNotifier struct {
config *TelegramNotifierConfig
}
var _ notifier.Notifier = (*TelegramNotifier)(nil)
func New(config *TelegramNotifierConfig) (*TelegramNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &TelegramNotifier{
config: config,
}, nil
}
func (n *TelegramNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv, err := telegram.New(n.config.ApiToken)
if err != nil {
return nil, err
}
srv.AddReceivers(n.config.ChatId)
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,43 @@
package webhook
import (
"context"
"errors"
"github.com/nikoksr/notify/service/http"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type WebhookNotifierConfig struct {
Url string `json:"url"`
}
type WebhookNotifier struct {
config *WebhookNotifierConfig
}
var _ notifier.Notifier = (*WebhookNotifier)(nil)
func New(config *WebhookNotifierConfig) (*WebhookNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &WebhookNotifier{
config: config,
}, nil
}
func (n *WebhookNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := http.New()
srv.AddReceiversURLs(n.config.Url)
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -2,6 +2,7 @@
import (
"context"
"errors"
"fmt"
"strings"
"time"
@@ -26,7 +27,13 @@ type AliyunCASUploader struct {
sdkClient *aliyunCas.Client
}
var _ uploader.Uploader = (*AliyunCASUploader)(nil)
func New(config *AliyunCASUploaderConfig) (*AliyunCASUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(
config.AccessKeyId,
config.AccessKeySecret,

View File

@@ -4,7 +4,9 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"regexp"
"strings"
"time"
@@ -28,7 +30,13 @@ type AliyunSLBUploader struct {
sdkClient *aliyunSlb.Client
}
var _ uploader.Uploader = (*AliyunSLBUploader)(nil)
func New(config *AliyunSLBUploaderConfig) (*AliyunSLBUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(
config.AccessKeyId,
config.AccessKeySecret,
@@ -82,6 +90,12 @@ func (u *AliyunSLBUploader) Upload(ctx context.Context, certPem string, privkeyP
var certId, certName string
certName = fmt.Sprintf("certimate_%d", time.Now().UnixMilli())
// 去除证书和私钥内容中的空白行,以符合阿里云 API 要求
// REF: https://github.com/usual2970/certimate/issues/326
re := regexp.MustCompile(`(?m)^\s*$\n?`)
certPem = strings.TrimSpace(re.ReplaceAllString(certPem, ""))
privkeyPem = strings.TrimSpace(re.ReplaceAllString(privkeyPem, ""))
// 上传新证书
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-uploadservercertificate
uploadServerCertificateReq := &aliyunSlb.UploadServerCertificateRequest{

View File

@@ -0,0 +1,68 @@
package dogecloud
import (
"context"
"errors"
"fmt"
"time"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
doge "github.com/usual2970/certimate/internal/pkg/vendors/dogecloud-sdk"
)
type DogeCloudUploaderConfig struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
}
type DogeCloudUploader struct {
config *DogeCloudUploaderConfig
sdkClient *doge.Client
}
var _ uploader.Uploader = (*DogeCloudUploader)(nil)
func New(config *DogeCloudUploaderConfig) (*DogeCloudUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(
config.AccessKey,
config.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &DogeCloudUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *DogeCloudUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 生成新证书名(需符合多吉云命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://docs.dogecloud.com/cdn/api-cert-upload
uploadSslCertResp, err := u.sdkClient.UploadCdnCert(certName, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadCdnCert'")
}
certId = fmt.Sprintf("%d", uploadSslCertResp.Data.Id)
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}
func createSdkClient(accessKey, secretKey string) (*doge.Client, error) {
client := doge.NewClient(accessKey, secretKey)
return client, nil
}

View File

@@ -32,7 +32,13 @@ type HuaweiCloudELBUploader struct {
sdkClient *hcElb.ElbClient
}
var _ uploader.Uploader = (*HuaweiCloudELBUploader)(nil)
func New(config *HuaweiCloudELBUploaderConfig) (*HuaweiCloudELBUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(
config.AccessKeyId,
config.SecretAccessKey,

View File

@@ -2,6 +2,7 @@
import (
"context"
"errors"
"fmt"
"time"
@@ -27,7 +28,13 @@ type HuaweiCloudSCMUploader struct {
sdkClient *hcScm.ScmClient
}
var _ uploader.Uploader = (*HuaweiCloudSCMUploader)(nil)
func New(config *HuaweiCloudSCMUploaderConfig) (*HuaweiCloudSCMUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(
config.AccessKeyId,
config.SecretAccessKey,

View File

@@ -2,6 +2,7 @@
import (
"context"
"errors"
"fmt"
"time"
@@ -23,7 +24,13 @@ type QiniuSSLCertUploader struct {
sdkClient *qiniuEx.Client
}
var _ uploader.Uploader = (*QiniuSSLCertUploader)(nil)
func New(config *QiniuSSLCertUploaderConfig) (*QiniuSSLCertUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(
config.AccessKey,
config.SecretKey,
@@ -51,7 +58,7 @@ func (u *QiniuSSLCertUploader) Upload(ctx context.Context, certPem string, privk
// 上传新证书
// REF: https://developer.qiniu.com/fusion/8593/interface-related-certificate
uploadSslCertResp, err := u.sdkClient.UploadSslCert(certName, certX509.Subject.CommonName, privkeyPem, certPem)
uploadSslCertResp, err := u.sdkClient.UploadSslCert(certName, certX509.Subject.CommonName, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadSslCert'")
}

View File

@@ -2,6 +2,7 @@
import (
"context"
"errors"
xerrors "github.com/pkg/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
@@ -21,7 +22,13 @@ type TencentCloudSSLUploader struct {
sdkClient *tcSsl.Client
}
var _ uploader.Uploader = (*TencentCloudSSLUploader)(nil)
func New(config *TencentCloudSSLUploaderConfig) (*TencentCloudSSLUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(
config.SecretId,
config.SecretKey,

View File

@@ -2,7 +2,7 @@
import "context"
// 表示定义证书上传的抽象类型接口。
// 表示定义证书上传的抽象类型接口。
// 云服务商通常会提供 SSL 证书管理服务,可供用户集中管理证书。
// 注意与 `Deployer` 区分,“上传”通常为“部署”的前置操作。
type Uploader interface {

View File

@@ -0,0 +1,164 @@
package maps
import "strconv"
// 以字符串形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是字符串,则返回空字符串。
func GetValueAsString(dict map[string]any, key string) string {
return GetValueOrDefaultAsString(dict, key, "")
}
// 以字符串形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是字符串,则返回默认值。
func GetValueOrDefaultAsString(dict map[string]any, key string, defaultValue string) string {
if dict == nil {
return defaultValue
}
if value, ok := dict[key]; ok {
if result, ok := value.(string); ok {
return result
}
}
return defaultValue
}
// 以 32 位整数形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 32 位整数,则返回 0。
func GetValueAsInt32(dict map[string]any, key string) int32 {
return GetValueOrDefaultAsInt32(dict, key, 0)
}
// 以 32 位整数形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 32 位整数,则返回默认值。
func GetValueOrDefaultAsInt32(dict map[string]any, key string, defaultValue int32) int32 {
if dict == nil {
return defaultValue
}
if value, ok := dict[key]; ok {
if result, ok := value.(int32); ok {
return result
}
// 兼容字符串类型的值
if str, ok := value.(string); ok {
if result, err := strconv.ParseInt(str, 10, 32); err == nil {
return int32(result)
}
}
}
return defaultValue
}
// 以 64 位整数形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 64 位整数,则返回 0。
func GetValueAsInt64(dict map[string]any, key string) int64 {
return GetValueOrDefaultAsInt64(dict, key, 0)
}
// 以 64 位整数形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 64 位整数,则返回默认值。
func GetValueOrDefaultAsInt64(dict map[string]any, key string, defaultValue int64) int64 {
if dict == nil {
return defaultValue
}
if value, ok := dict[key]; ok {
if result, ok := value.(int64); ok {
return result
}
// 兼容字符串类型的值
if str, ok := value.(string); ok {
if result, err := strconv.ParseInt(str, 10, 64); err == nil {
return result
}
}
}
return defaultValue
}
// 以布尔形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是布尔,则返回 false。
func GetValueAsBool(dict map[string]any, key string) bool {
return GetValueOrDefaultAsBool(dict, key, false)
}
// 以布尔形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是布尔,则返回默认值。
func GetValueOrDefaultAsBool(dict map[string]any, key string, defaultValue bool) bool {
if dict == nil {
return defaultValue
}
if value, ok := dict[key]; ok {
if result, ok := value.(bool); ok {
return result
}
// 兼容字符串类型的值
if str, ok := value.(string); ok {
if result, err := strconv.ParseBool(str); err == nil {
return result
}
}
}
return defaultValue
}

View File

@@ -0,0 +1,183 @@
package dogecloudsdk
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
const dogeHost = "https://api.dogecloud.com"
type Client struct {
accessKey string
secretKey string
}
func NewClient(accessKey, secretKey string) *Client {
return &Client{accessKey: accessKey, secretKey: secretKey}
}
func (c *Client) UploadCdnCert(note, cert, private string) (*UploadCdnCertResponse, error) {
req := &UploadCdnCertRequest{
Note: note,
Certificate: cert,
PrivateKey: private,
}
reqBts, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMap := make(map[string]interface{})
err = json.Unmarshal(reqBts, &reqMap)
if err != nil {
return nil, err
}
respBts, err := c.sendReq(http.MethodPost, "cdn/cert/upload.json", reqMap, true)
if err != nil {
return nil, err
}
resp := &UploadCdnCertResponse{}
err = json.Unmarshal(respBts, resp)
if err != nil {
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("dogecloud api error, code: %d, msg: %s", *resp.Code, *resp.Message)
}
return resp, nil
}
func (c *Client) BindCdnCertWithDomain(certId int64, domain string) (*BindCdnCertResponse, error) {
req := &BindCdnCertRequest{
CertId: certId,
Domain: &domain,
}
reqBts, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMap := make(map[string]interface{})
err = json.Unmarshal(reqBts, &reqMap)
if err != nil {
return nil, err
}
respBts, err := c.sendReq(http.MethodPost, "cdn/cert/bind.json", reqMap, true)
if err != nil {
return nil, err
}
resp := &BindCdnCertResponse{}
err = json.Unmarshal(respBts, resp)
if err != nil {
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("dogecloud api error, code: %d, msg: %s", *resp.Code, *resp.Message)
}
return resp, nil
}
func (c *Client) BindCdnCertWithDomainId(certId int64, domainId int64) (*BindCdnCertResponse, error) {
req := &BindCdnCertRequest{
CertId: certId,
DomainId: &domainId,
}
reqBts, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMap := make(map[string]interface{})
err = json.Unmarshal(reqBts, &reqMap)
if err != nil {
return nil, err
}
respBts, err := c.sendReq(http.MethodPost, "cdn/cert/bind.json", reqMap, true)
if err != nil {
return nil, err
}
resp := &BindCdnCertResponse{}
err = json.Unmarshal(respBts, resp)
if err != nil {
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("dogecloud api error, code: %d, msg: %s", *resp.Code, *resp.Message)
}
return resp, nil
}
// 调用多吉云的 API。
// https://docs.dogecloud.com/cdn/api-access-token?id=go
//
// 入参:
// - methodGET 或 POST
// - path是调用的 API 接口地址,包含 URL 请求参数 QueryString例如/console/vfetch/add.json?url=xxx&a=1&b=2
// - dataPOST 的数据,对象,例如 {a: 1, b: 2},传递此参数表示不是 GET 请求而是 POST 请求
// - jsonMode数据 data 是否以 JSON 格式请求,默认为 false 则使用表单形式a=1&b=2
func (c *Client) sendReq(method string, path string, data map[string]interface{}, jsonMode bool) ([]byte, error) {
body := ""
mime := ""
if jsonMode {
_body, err := json.Marshal(data)
if err != nil {
return nil, err
}
body = string(_body)
mime = "application/json"
} else {
values := url.Values{}
for k, v := range data {
values.Set(k, v.(string))
}
body = values.Encode()
mime = "application/x-www-form-urlencoded"
}
path = strings.TrimPrefix(path, "/")
signStr := "/" + path + "\n" + body
hmacObj := hmac.New(sha1.New, []byte(c.secretKey))
hmacObj.Write([]byte(signStr))
sign := hex.EncodeToString(hmacObj.Sum(nil))
auth := fmt.Sprintf("TOKEN %s:%s", c.accessKey, sign)
req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", dogeHost, path), strings.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", mime)
req.Header.Add("Authorization", auth)
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
r, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return r, nil
}

View File

@@ -0,0 +1,31 @@
package dogecloudsdk
type BaseResponse struct {
Code *int `json:"code,omitempty"`
Message *string `json:"msg,omitempty"`
}
type UploadCdnCertRequest struct {
Note string `json:"note"`
Certificate string `json:"cert"`
PrivateKey string `json:"private"`
}
type UploadCdnCertResponseData struct {
Id int64 `json:"id"`
}
type UploadCdnCertResponse struct {
BaseResponse
Data *UploadCdnCertResponseData `json:"data,omitempty"`
}
type BindCdnCertRequest struct {
CertId int64 `json:"id"`
DomainId *int64 `json:"did,omitempty"`
Domain *string `json:"domain,omitempty"`
}
type BindCdnCertResponse struct {
BaseResponse
}

View File

@@ -12,7 +12,7 @@ import (
xhttp "github.com/usual2970/certimate/internal/utils/http"
)
const qiniuHost = "http://api.qiniu.com"
const qiniuHost = "https://api.qiniu.com"
type Client struct {
mac *auth.Credentials
@@ -105,12 +105,12 @@ func (c *Client) EnableDomainHttps(domain, certId string, forceHttps, http2Enabl
return resp, nil
}
func (c *Client) UploadSslCert(name, commonName, pri, ca string) (*UploadSslCertResponse, error) {
func (c *Client) UploadSslCert(name, commonName, certificate, privateKey string) (*UploadSslCertResponse, error) {
req := &UploadSslCertRequest{
Name: name,
CommonName: commonName,
Pri: pri,
Ca: ca,
Name: name,
CommonName: commonName,
Certificate: certificate,
PrivateKey: privateKey,
}
reqBytes, err := json.Marshal(req)
@@ -129,7 +129,7 @@ func (c *Client) UploadSslCert(name, commonName, pri, ca string) (*UploadSslCert
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error)
return nil, fmt.Errorf("qiniu api error, code: %d, error: %s", *resp.Code, *resp.Error)
}
return resp, nil

View File

@@ -1,16 +1,20 @@
package qiniusdk
type BaseResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
}
type UploadSslCertRequest struct {
Name string `json:"name"`
CommonName string `json:"common_name"`
Pri string `json:"pri"`
Ca string `json:"ca"`
Name string `json:"name"`
CommonName string `json:"common_name"`
Certificate string `json:"ca"`
PrivateKey string `json:"pri"`
}
type UploadSslCertResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
CertID string `json:"certID"`
*BaseResponse
CertID string `json:"certID"`
}
type DomainInfoHttpsData struct {
@@ -20,8 +24,7 @@ type DomainInfoHttpsData struct {
}
type GetDomainInfoResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
BaseResponse
Name string `json:"name"`
Type string `json:"type"`
CName string `json:"cname"`
@@ -39,8 +42,7 @@ type ModifyDomainHttpsConfRequest struct {
}
type ModifyDomainHttpsConfResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
BaseResponse
}
type EnableDomainHttpsRequest struct {
@@ -48,6 +50,5 @@ type EnableDomainHttpsRequest struct {
}
type EnableDomainHttpsResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
BaseResponse
}

View File

@@ -0,0 +1,807 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
)
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `[
{
"id": "z3p974ainxjqlvs",
"created": "2024-07-29 10:02:48.334Z",
"updated": "2024-10-23 09:25:43.083Z",
"name": "domains",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "iuaerpl2",
"name": "domain",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "ukkhuw85",
"name": "email",
"type": "email",
"required": false,
"presentable": false,
"unique": false,
"options": {
"exceptDomains": null,
"onlyDomains": null
}
},
{
"system": false,
"id": "v98eebqq",
"name": "crontab",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "alc8e9ow",
"name": "access",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "4yzbv8urny5ja1e",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "topsc9bj",
"name": "certUrl",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "vixgq072",
"name": "certStableUrl",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "g3a3sza5",
"name": "privateKey",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "gr6iouny",
"name": "certificate",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "tk6vnrmn",
"name": "issuerCertificate",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "sjo6ibse",
"name": "csr",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "x03n1bkj",
"name": "expiredAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "srybpixz",
"name": "targetType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun-oss",
"aliyun-cdn",
"aliyun-dcdn",
"ssh",
"webhook",
"tencent-cdn",
"qiniu-cdn",
"local"
]
}
},
{
"system": false,
"id": "xy7yk0mb",
"name": "targetAccess",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "4yzbv8urny5ja1e",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "6jqeyggw",
"name": "enabled",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "hdsjcchf",
"name": "deployed",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "aiya3rev",
"name": "rightnow",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "ixznmhzc",
"name": "lastDeployedAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "ghtlkn5j",
"name": "lastDeployment",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "0a1o4e6sstp694f",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "zfnyj9he",
"name": "variables",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "1bspzuku",
"name": "group",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "teolp9pl72dxlxq",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "g65gfh7a",
"name": "nameservers",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "wwrzc3jo",
"name": "applyConfig",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
},
{
"system": false,
"id": "474iwy8r",
"name": "deployConfig",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_4ABO6EQ` + "`" + ` ON ` + "`" + `domains` + "`" + ` (` + "`" + `domain` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "4yzbv8urny5ja1e",
"created": "2024-07-29 10:04:39.685Z",
"updated": "2024-11-05 00:21:32.129Z",
"name": "access",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "geeur58v",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "iql7jpwx",
"name": "config",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
},
{
"system": false,
"id": "hwy7m03o",
"name": "configType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun",
"tencent",
"huaweicloud",
"qiniu",
"aws",
"cloudflare",
"namesilo",
"godaddy",
"pdns",
"httpreq",
"local",
"ssh",
"webhook",
"k8s",
"baiducloud",
"dogecloud"
]
}
},
{
"system": false,
"id": "lr33hiwg",
"name": "deleted",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "hsxcnlvd",
"name": "usage",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"apply",
"deploy",
"all"
]
}
},
{
"system": false,
"id": "c8egzzwj",
"name": "group",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "teolp9pl72dxlxq",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_wkoST0j` + "`" + ` ON ` + "`" + `access` + "`" + ` (` + "`" + `name` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "0a1o4e6sstp694f",
"created": "2024-07-30 06:30:27.801Z",
"updated": "2024-10-23 09:25:43.084Z",
"name": "deployments",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "farvlzk7",
"name": "domain",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "z3p974ainxjqlvs",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "jx5f69i3",
"name": "log",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
},
{
"system": false,
"id": "qbxdtg9q",
"name": "phase",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"check",
"apply",
"deploy"
]
}
},
{
"system": false,
"id": "rglrp1hz",
"name": "phaseSuccess",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "lt1g1blu",
"name": "deployedAt",
"type": "date",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "wledpzgb",
"name": "wholeSuccess",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "_pb_users_auth_",
"created": "2024-09-12 13:09:54.234Z",
"updated": "2024-10-23 09:25:43.085Z",
"name": "users",
"type": "auth",
"system": false,
"schema": [
{
"system": false,
"id": "users_name",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "users_avatar",
"name": "avatar",
"type": "file",
"required": false,
"presentable": false,
"unique": false,
"options": {
"mimeTypes": [
"image/jpeg",
"image/png",
"image/svg+xml",
"image/gif",
"image/webp"
],
"thumbs": null,
"maxSelect": 1,
"maxSize": 5242880,
"protected": false
}
}
],
"indexes": [],
"listRule": "id = @request.auth.id",
"viewRule": "id = @request.auth.id",
"createRule": "",
"updateRule": "id = @request.auth.id",
"deleteRule": "id = @request.auth.id",
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"onlyVerified": false,
"requireEmail": false
}
},
{
"id": "dy6ccjb60spfy6p",
"created": "2024-09-12 23:12:21.677Z",
"updated": "2024-10-23 09:25:43.085Z",
"name": "settings",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "1tcmdsdf",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "f9wyhypi",
"name": "content",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_RO7X9Vw` + "`" + ` ON ` + "`" + `settings` + "`" + ` (` + "`" + `name` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "teolp9pl72dxlxq",
"created": "2024-09-13 12:51:05.611Z",
"updated": "2024-10-23 09:25:43.086Z",
"name": "access_groups",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "7sajiv6i",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "xp8admif",
"name": "access",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "4yzbv8urny5ja1e",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": null,
"displayFields": null
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_RgRXp0R` + "`" + ` ON ` + "`" + `access_groups` + "`" + ` (` + "`" + `name` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "012d7abbod1hwvr",
"created": "2024-10-23 06:37:13.155Z",
"updated": "2024-10-23 09:25:43.086Z",
"name": "acme_accounts",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "fmjfn0yw",
"name": "ca",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "qqwijqzt",
"name": "email",
"type": "email",
"required": false,
"presentable": false,
"unique": false,
"options": {
"exceptDomains": null,
"onlyDomains": null
}
},
{
"system": false,
"id": "genxqtii",
"name": "key",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "1aoia909",
"name": "resource",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
}
]`
collections := []*models.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
return err
}
return daos.New(db).ImportCollections(collections, true, nil)
}, func(db dbx.Builder) error {
return nil
})
}

View File

@@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1684" width="200" height="200"><path d="M482.687 74.101c10.109-5.627 19.662-12.497 30.785-15.998 8.506-2.192 16.685 2.323 23.883 6.38 117.253 68.08 235.062 135.312 352.118 203.753-40.338 22.115-79.662 46.063-119.805 68.506-27.677 15.148-62.814 13.74-89.771-2.388-48.289-28.135-96.871-55.78-145.127-84.014-8.997-5.692-21.232-5.889-30.654-1.21-49.728 28.724-99.456 57.58-149.282 86.173-27.056 15.834-61.963 15.867-89.314 0.687a26252.906 26252.906 0 0 1-113.785-65.628c-0.229-1.178-0.72-3.533-0.949-4.744 110.776-63.533 221.256-127.722 331.9-191.517z" fill="#72AF2D" p-id="1685"></path><path d="M115.552 719.744c0.49-135.148-0.622-270.329 0.556-405.477 32.617 19.367 65.595 38.08 98.441 57.088 12.76 7.427 26.27 14.199 36.74 24.864 15.769 16.39 26.042 38.67 25.845 61.637 0.033 54.57 0.131 109.172-0.065 163.774-1.047 12.203 3.304 25.65 14.493 31.963 40.567 23.72 81.396 47.045 122.095 70.6 14.362 8.638 29.771 15.9 42.4 27.057 18.156 17.11 28.756 41.777 29.116 66.707-0.033 44.559-0.196 89.15 0.066 133.709-10.175-3.468-18.877-9.848-28.201-14.984-108.55-62.716-217.167-125.366-325.652-188.148-10.207-5.66-17.143-16.947-15.834-28.79z" fill="#118CCF" p-id="1686"></path><path d="M815.143 367.397c30.753-17.47 61.015-35.824 92.095-52.705 0.196 135.017-0.066 270.035 0.13 405.052 0.819 11.582-5.3 23.163-15.637 28.627-110.416 63.86-220.896 127.558-331.312 191.386-7.328 4.45-14.722 8.8-22.508 12.432-0.098-44.82 0.065-89.64-0.098-134.428-0.426-31.538 17.47-62.16 44.558-78.06 49.074-28.43 98.245-56.664 147.286-85.159 8.245-4.515 15.311-13.053 14.919-22.9 0.13-55.683 0.065-111.398 0-167.08-0.033-23.784 8.048-47.732 24.21-65.398 12.497-14.362 30.36-22.083 46.357-31.767z" fill="#DA4525" p-id="1687"></path></svg>

View File

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,28 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="-0.5 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Google-color</title>
<desc>Created with Sketch.</desc>
<defs>
</defs>
<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>
<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: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,194 @@
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { ClientResponseError } from "pocketbase";
import { Input } from "@/components/ui/input";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { PbErrorData } from "@/domain/base";
import { accessProvidersMap, accessTypeFormSchema, type Access, type BaiduCloudConfig } from "@/domain/access";
import { save } from "@/repository/access";
import { useConfigContext } from "@/providers/config";
type AccessBaiduCloudFormProps = {
op: "add" | "edit" | "copy";
data?: Access;
onAfterReq: () => void;
};
const AccessBaiduCloudForm = ({ data, op, onAfterReq }: AccessBaiduCloudFormProps) => {
const { addAccess, updateAccess } = useConfigContext();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z
.string()
.min(1, "access.authorization.form.name.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
configType: accessTypeFormSchema,
accessKeyId: z
.string()
.min(1, "access.authorization.form.access_key_id.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
secretAccessKey: z
.string()
.min(1, "access.authorization.form.secret_access_key.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
});
let config: BaiduCloudConfig = {
accessKeyId: "",
secretAccessKey: "",
};
if (data) config = data.config as BaiduCloudConfig;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name || "",
configType: "baiducloud",
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
const req: Access = {
id: data.id as string,
name: data.name,
configType: data.configType,
usage: accessProvidersMap.get(data.configType)!.usage,
config: {
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
},
};
try {
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id && op == "edit") {
updateAccess(req);
return;
}
addAccess(req);
} catch (e) {
const err = e as ClientResponseError;
Object.entries(err.response.data as PbErrorData).forEach(([key, value]) => {
form.setError(key as keyof z.infer<typeof formSchema>, {
type: "manual",
message: value.message,
});
});
return;
}
};
return (
<>
<Form {...form}>
<form
onSubmit={(e) => {
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.name.label")}</FormLabel>
<FormControl>
<Input placeholder={t("access.authorization.form.name.placeholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accessKeyId"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.access_key_id.label")}</FormLabel>
<FormControl>
<Input placeholder={t("access.authorization.form.access_key_id.placeholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="secretAccessKey"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.secret_access_key.label")}</FormLabel>
<FormControl>
<Input placeholder={t("access.authorization.form.secret_access_key.placeholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>
</>
);
};
export default AccessBaiduCloudForm;

View File

@@ -0,0 +1,188 @@
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { ClientResponseError } from "pocketbase";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PbErrorData } from "@/domain/base";
import { accessProvidersMap, accessTypeFormSchema, type Access, type DogeCloudConfig } from "@/domain/access";
import { save } from "@/repository/access";
import { useConfigContext } from "@/providers/config";
type AccessDogeCloudFormProps = {
op: "add" | "edit" | "copy";
data?: Access;
onAfterReq: () => void;
};
const AccessDogeCloudForm = ({ data, op, onAfterReq }: AccessDogeCloudFormProps) => {
const { addAccess, updateAccess } = useConfigContext();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z
.string()
.min(1, "access.authorization.form.name.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
configType: accessTypeFormSchema,
accessKey: z.string().min(1, "access.authorization.form.access_key.placeholder").max(64),
secretKey: z.string().min(1, "access.authorization.form.secret_key.placeholder").max(64),
});
let config: DogeCloudConfig = {
accessKey: "",
secretKey: "",
};
if (data) config = data.config as DogeCloudConfig;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name || "",
configType: "dogecloud",
accessKey: config.accessKey,
secretKey: config.secretKey,
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
const req: Access = {
id: data.id as string,
name: data.name,
configType: data.configType,
usage: accessProvidersMap.get(data.configType)!.usage,
config: {
accessKey: data.accessKey,
secretKey: data.secretKey,
},
};
try {
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id && op == "edit") {
updateAccess(req);
return;
}
addAccess(req);
} catch (e) {
const err = e as ClientResponseError;
Object.entries(err.response.data as PbErrorData).forEach(([key, value]) => {
form.setError(key as keyof z.infer<typeof formSchema>, {
type: "manual",
message: value.message,
});
});
return;
}
};
return (
<>
<Form {...form}>
<form
onSubmit={(e) => {
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.name.label")}</FormLabel>
<FormControl>
<Input placeholder={t("access.authorization.form.name.placeholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accessKey"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.access_key.label")}</FormLabel>
<FormControl>
<Input placeholder={t("access.authorization.form.access_key.placeholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="secretKey"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.secret_key.label")}</FormLabel>
<FormControl>
<Input placeholder={t("access.authorization.form.secret_key.placeholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>
</>
);
};
export default AccessDogeCloudForm;

View File

@@ -8,7 +8,9 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import AccessAliyunForm from "./AccessAliyunForm";
import AccessTencentForm from "./AccessTencentForm";
import AccessHuaweiCloudForm from "./AccessHuaweicloudForm";
import AccessBaiduCloudForm from "./AccessBaiduCloudForm";
import AccessQiniuForm from "./AccessQiniuForm";
import AccessDogeCloudForm from "./AccessDogeCloudForm";
import AccessAwsForm from "./AccessAwsForm";
import AccessCloudflareForm from "./AccessCloudflareForm";
import AccessNamesiloForm from "./AccessNamesiloForm";
@@ -71,6 +73,17 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
/>
);
break;
case "baiducloud":
childComponent = (
<AccessBaiduCloudForm
data={data}
op={op}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
case "qiniu":
childComponent = (
<AccessQiniuForm
@@ -82,6 +95,17 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
/>
);
break;
case "dogecloud":
childComponent = (
<AccessDogeCloudForm
data={data}
op={op}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
case "aws":
childComponent = (
<AccessAwsForm

View File

@@ -20,7 +20,9 @@ import DeployToTencentCOS from "./DeployToTencentCOS";
import DeployToTencentTEO from "./DeployToTencentTEO";
import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN";
import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB";
import DeployToBaiduCloudCDN from "./DeployToBaiduCloudCDN";
import DeployToQiniuCDN from "./DeployToQiniuCDN";
import DeployToDogeCloudCDN from "./DeployToDogeCloudCDN";
import DeployToLocal from "./DeployToLocal";
import DeployToSSH from "./DeployToSSH";
import DeployToWebhook from "./DeployToWebhook";
@@ -151,9 +153,15 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
case "huaweicloud-elb":
childComponent = <DeployToHuaweiCloudELB />;
break;
case "baiducloud-cdn":
childComponent = <DeployToBaiduCloudCDN />;
break;
case "qiniu-cdn":
childComponent = <DeployToQiniuCDN />;
break;
case "dogecloud-cdn":
childComponent = <DeployToDogeCloudCDN />;
break;
case "local":
childComponent = <DeployToLocal />;
break;

View File

@@ -0,0 +1,68 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
type DeployToBaiduCloudCDNConfigParams = {
domain?: string;
};
const DeployToBaiduCloudCDN = () => {
const { t } = useTranslation();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToBaiduCloudCDNConfigParams>();
useEffect(() => {
if (!config.id) {
setConfig({
...config,
config: {},
});
}
}, []);
useEffect(() => {
setErrors({});
}, []);
const formSchema = z.object({
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
useEffect(() => {
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToBaiduCloudCDN;

View File

@@ -0,0 +1,68 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
type DeployToDogeCloudCDNConfigParams = {
domain?: string;
};
const DeployToDogeCloudCDN = () => {
const { t } = useTranslation();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToDogeCloudCDNConfigParams>();
useEffect(() => {
if (!config.id) {
setConfig({
...config,
config: {},
});
}
}, []);
useEffect(() => {
setErrors({});
}, []);
const formSchema = z.object({
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
useEffect(() => {
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToDogeCloudCDN;

View File

@@ -44,9 +44,7 @@ const DeployToTencentCLB = () => {
}),
loadbalancerId: z.string().min(1, t("domain.deployment.form.tencent_clb_loadbalancer_id.placeholder")),
listenerId: z.string().optional(),
domain: z.string().regex(/^$|^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
domain: z.string().optional(),
})
.refine(
(data) => {
@@ -63,10 +61,20 @@ const DeployToTencentCLB = () => {
path: ["listenerId"],
}
)
.refine((data) => (data.resourceType === "ruledomain" ? !!data.domain?.trim() : true), {
message: t("domain.deployment.form.tencent_clb_ruledomain.placeholder"),
path: ["domain"],
});
.refine(
(data) => {
switch (data.resourceType) {
case "ssl-deploy":
case "ruledomain":
return !!data.domain?.trim() && /^$|^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/.test(data.domain);
}
return true;
},
{
message: t("domain.deployment.form.tencent_clb_ruledomain.placeholder"),
path: ["domain"],
}
);
useEffect(() => {
const res = formSchema.safeParse(config.config);

View File

@@ -123,22 +123,29 @@ const Bark = () => {
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("bark");
toast({
title: t("settings.notification.config.push.test.message.success.message"),
description: t("settings.notification.config.push.test.message.success.message"),
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("settings.notification.config.push.test.message.failed.message"),
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
@@ -177,67 +184,76 @@ const Bark = () => {
};
return (
<div>
<Input
placeholder={t("settings.notification.bark.serverUrl.placeholder")}
value={bark.data.serverUrl}
onChange={(e) => {
const newData = {
...bark,
data: {
...bark.data,
serverUrl: e.target.value,
},
};
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.bark.server_url.label")}</Label>
<Input
placeholder={t("settings.notification.bark.server_url.placeholder")}
value={bark.data.serverUrl}
onChange={(e) => {
const newData = {
...bark,
data: {
...bark.data,
serverUrl: e.target.value,
},
};
checkChanged(newData.data);
setBark(newData);
}}
/>
<Input
placeholder={t("settings.notification.bark.deviceKey.placeholder")}
value={bark.data.deviceKey}
onChange={(e) => {
const newData = {
...bark,
data: {
...bark.data,
deviceKey: e.target.value,
},
};
checkChanged(newData.data);
setBark(newData);
}}
/>
<div className="flex items-center space-x-1 mt-2">
<Switch id="airplane-mode" checked={bark.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
checkChanged(newData.data);
setBark(newData);
}}
/>
</div>
<div className="flex justify-end mt-2">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<div>
<Label>{t("settings.notification.bark.device_key.label")}</Label>
<Input
placeholder={t("settings.notification.bark.device_key.placeholder")}
value={bark.data.deviceKey}
onChange={(e) => {
const newData = {
...bark,
data: {
...bark.data,
deviceKey: e.target.value,
},
};
<Show when={!changed && bark.id != ""}>
<Button
variant="secondary"
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.config.push.test.message")}
</Button>
</Show>
checkChanged(newData.data);
setBark(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={bark.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<Show when={!changed && bark.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);

View File

@@ -120,23 +120,30 @@ const DingTalk = () => {
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("dingtalk");
toast({
title: t("settings.notification.config.push.test.message.success.message"),
description: t("settings.notification.config.push.test.message.success.message"),
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("settings.notification.config.push.test.message.failed.message"),
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
}
@@ -177,64 +184,74 @@ const DingTalk = () => {
};
return (
<div>
<Input
placeholder="AccessToken"
value={dingtalk.data.accessToken}
onChange={(e) => {
const newData = {
...dingtalk,
data: {
...dingtalk.data,
accessToken: e.target.value,
},
};
checkChanged(newData.data);
setDingtalk(newData);
}}
/>
<Input
placeholder={t("settings.notification.dingtalk.secret.placeholder")}
className="mt-2"
value={dingtalk.data.secret}
onChange={(e) => {
const newData = {
...dingtalk,
data: {
...dingtalk.data,
secret: e.target.value,
},
};
checkChanged(newData.data);
setDingtalk(newData);
}}
/>
<div className="flex items-center space-x-1 mt-2">
<Switch id="airplane-mode" checked={dingtalk.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.dingtalk.access_token.label")}</Label>
<Input
placeholder={t("settings.notification.dingtalk.access_token.placeholder")}
value={dingtalk.data.accessToken}
onChange={(e) => {
const newData = {
...dingtalk,
data: {
...dingtalk.data,
accessToken: e.target.value,
},
};
checkChanged(newData.data);
setDingtalk(newData);
}}
/>
</div>
<div className="flex justify-end mt-2">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<div>
<Label>{t("settings.notification.dingtalk.secret.label")}</Label>
<Input
placeholder={t("settings.notification.dingtalk.secret.placeholder")}
value={dingtalk.data.secret}
onChange={(e) => {
const newData = {
...dingtalk,
data: {
...dingtalk.data,
secret: e.target.value,
},
};
checkChanged(newData.data);
setDingtalk(newData);
}}
/>
</div>
<Show when={!changed && dingtalk.id != ""}>
<Button
variant="secondary"
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.config.push.test.message")}
</Button>
</Show>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={dingtalk.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<Show when={!changed && dingtalk.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);

View File

@@ -0,0 +1,384 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { getErrMessage } from "@/lib/error";
import { NotifyChannelEmail, NotifyChannels } from "@/domain/settings";
import { useNotifyContext } from "@/providers/notify";
import { update } from "@/repository/settings";
import Show from "@/components/Show";
import { notifyTest } from "@/api/notify";
type EmailSetting = {
id: string;
name: string;
data: NotifyChannelEmail;
};
const Mail = () => {
const { config, setChannels } = useNotifyContext();
const { t } = useTranslation();
const [changed, setChanged] = useState<boolean>(false);
const [mail, setMail] = useState<EmailSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
smtpHost: "",
smtpPort: 465,
smtpTLS: true,
username: "",
password: "",
senderAddress: "",
receiverAddress: "",
enabled: false,
},
});
const [originMail, setOriginMail] = useState<EmailSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
smtpHost: "",
smtpPort: 465,
smtpTLS: true,
username: "",
password: "",
senderAddress: "",
receiverAddress: "",
enabled: false,
},
});
useEffect(() => {
setChanged(false);
}, [config]);
useEffect(() => {
const data = getDetailMail();
setOriginMail({
id: config.id ?? "",
name: "email",
data,
});
}, [config]);
useEffect(() => {
const data = getDetailMail();
setMail({
id: config.id ?? "",
name: "email",
data,
});
}, [config]);
const { toast } = useToast();
const getDetailMail = () => {
const df: NotifyChannelEmail = {
smtpHost: "smtp.example.com",
smtpPort: 465,
smtpTLS: true,
username: "",
password: "",
senderAddress: "",
receiverAddress: "",
enabled: false,
};
if (!config.content) {
return df;
}
const chanels = config.content as NotifyChannels;
if (!chanels.email) {
return df;
}
return chanels.email as NotifyChannelEmail;
};
const checkChanged = (data: NotifyChannelEmail) => {
if (
data.smtpHost !== originMail.data.smtpHost ||
data.smtpPort !== originMail.data.smtpPort ||
data.smtpTLS !== originMail.data.smtpTLS ||
data.username !== originMail.data.username ||
data.password !== originMail.data.password ||
data.senderAddress !== originMail.data.senderAddress ||
data.receiverAddress !== originMail.data.receiverAddress
) {
setChanged(true);
} else {
setChanged(false);
}
};
const handleSaveClick = async () => {
try {
const resp = await update({
...config,
name: "notifyChannels",
content: {
...config.content,
email: {
...mail.data,
},
},
});
setChannels(resp);
toast({
title: t("common.save.succeeded.message"),
description: t("settings.notification.config.saved.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("common.save.failed.message"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("email");
toast({
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
const handleSwitchChange = async () => {
const newData = {
...mail,
data: {
...mail.data,
enabled: !mail.data.enabled,
},
};
setMail(newData);
try {
const resp = await update({
...config,
name: "notifyChannels",
content: {
...config.content,
email: {
...newData.data,
},
},
});
setChannels(resp);
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("common.save.failed.message"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
return (
<div className="flex flex-col space-y-4">
<div className="flex space-x-4">
<div className="w-2/5">
<Label>{t("settings.notification.email.smtp_host.label")}</Label>
<Input
placeholder={t("settings.notification.email.smtp_host.placeholder")}
value={mail.data.smtpHost}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpHost: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="w-2/5">
<Label>{t("settings.notification.email.smtp_port.label")}</Label>
<Input
type="number"
placeholder={t("settings.notification.email.smtp_port.placeholder")}
value={mail.data.smtpPort}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpPort: +e.target.value || 0,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="w-1/5">
<Label>{t("settings.notification.email.smtp_tls.label")}</Label>
<Switch
className="block mt-2"
checked={mail.data.smtpTLS}
onCheckedChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpPort: e && mail.data.smtpPort === 25 ? 465 : !e && mail.data.smtpPort === 465 ? 25 : mail.data.smtpPort,
smtpTLS: e,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
</div>
<div className="flex space-x-4">
<div className="w-1/2">
<Label>{t("settings.notification.email.username.label")}</Label>
<Input
placeholder={t("settings.notification.email.username.placeholder")}
value={mail.data.username}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
username: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="w-1/2">
<Label>{t("settings.notification.email.password.label")}</Label>
<Input
placeholder={t("settings.notification.email.password.placeholder")}
value={mail.data.password}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
password: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
</div>
<div>
<Label>{t("settings.notification.email.sender_address.label")}</Label>
<Input
placeholder={t("settings.notification.email.sender_address.placeholder")}
value={mail.data.senderAddress}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
senderAddress: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div>
<Label>{t("settings.notification.email.receiver_address.label")}</Label>
<Input
placeholder={t("settings.notification.email.receiver_address.placeholder")}
value={mail.data.receiverAddress}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
receiverAddress: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={mail.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<Show when={!changed && mail.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default Mail;

View File

@@ -116,23 +116,30 @@ const Lark = () => {
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("lark");
toast({
title: t("settings.notification.config.push.test.message.success.message"),
description: t("settings.notification.config.push.test.message.success.message"),
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("settings.notification.config.push.test.message.failed.message"),
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
}
@@ -173,49 +180,56 @@ const Lark = () => {
};
return (
<div>
<Input
placeholder="Webhook Url"
value={lark.data.webhookUrl}
onChange={(e) => {
const newData = {
...lark,
data: {
...lark.data,
webhookUrl: e.target.value,
},
};
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.lark.webhook_url.label")}</Label>
<Input
placeholder={t("settings.notification.lark.webhook_url.placeholder")}
value={lark.data.webhookUrl}
onChange={(e) => {
const newData = {
...lark,
data: {
...lark.data,
webhookUrl: e.target.value,
},
};
checkChanged(newData.data);
setLark(newData);
}}
/>
<div className="flex items-center space-x-1 mt-2">
<Switch id="airplane-mode" checked={lark.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
checkChanged(newData.data);
setLark(newData);
}}
/>
</div>
<div className="flex justify-end mt-2">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={lark.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<Show when={!changed && lark.id != ""}>
<Button
variant="secondary"
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.config.push.test.message")}
</Button>
</Show>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<Show when={!changed && lark.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);

View File

@@ -1,319 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { getErrMessage } from "@/lib/error";
import { NotifyChannelMail, NotifyChannels } from "@/domain/settings";
import { useNotifyContext } from "@/providers/notify";
import { update } from "@/repository/settings";
import Show from "@/components/Show";
import { notifyTest } from "@/api/notify";
type MailSetting = {
id: string;
name: string;
data: NotifyChannelMail;
};
const Mail = () => {
const { config, setChannels } = useNotifyContext();
const { t } = useTranslation();
const [changed, setChanged] = useState<boolean>(false);
const [mail, setmail] = useState<MailSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
senderAddress: "",
receiverAddresses: "",
smtpHostAddr: "",
smtpHostPort: "25",
username: "",
password: "",
enabled: false,
},
});
const [originMail, setoriginMail] = useState<MailSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
senderAddress: "",
receiverAddresses: "",
smtpHostAddr: "",
smtpHostPort: "25",
username: "",
password: "",
enabled: false,
},
});
useEffect(() => {
setChanged(false);
}, [config]);
useEffect(() => {
const data = getDetailMail();
setoriginMail({
id: config.id ?? "",
name: "mail",
data,
});
}, [config]);
useEffect(() => {
const data = getDetailMail();
setmail({
id: config.id ?? "",
name: "mail",
data,
});
}, [config]);
const { toast } = useToast();
const getDetailMail = () => {
const df: NotifyChannelMail = {
senderAddress: "",
receiverAddresses: "",
smtpHostAddr: "",
smtpHostPort: "25",
username: "",
password: "",
enabled: false,
};
if (!config.content) {
return df;
}
const chanels = config.content as NotifyChannels;
if (!chanels.mail) {
return df;
}
return chanels.mail as NotifyChannelMail;
};
const checkChanged = (data: NotifyChannelMail) => {
if (data.senderAddress !== originMail.data.senderAddress || data.receiverAddresses !== originMail.data.receiverAddresses || data.smtpHostAddr !== originMail.data.smtpHostAddr || data.smtpHostPort !== originMail.data.smtpHostPort || data.username !== originMail.data.username || data.password !== originMail.data.password) {
setChanged(true);
} else {
setChanged(false);
}
};
const handleSaveClick = async () => {
try {
const resp = await update({
...config,
name: "notifyChannels",
content: {
...config.content,
mail: {
...mail.data,
},
},
});
setChannels(resp);
toast({
title: t("common.save.succeeded.message"),
description: t("settings.notification.config.saved.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("common.save.failed.message"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
const handlePushTestClick = async () => {
try {
await notifyTest("mail");
toast({
title: t("settings.notification.config.push.test.message.success.message"),
description: t("settings.notification.config.push.test.message.success.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("settings.notification.config.push.test.message.failed.message"),
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
const handleSwitchChange = async () => {
const newData = {
...mail,
data: {
...mail.data,
enabled: !mail.data.enabled,
},
};
setmail(newData);
try {
const resp = await update({
...config,
name: "notifyChannels",
content: {
...config.content,
mail: {
...newData.data,
},
},
});
setChannels(resp);
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("common.save.failed.message"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
return (
<div>
<Input
placeholder={t("settings.notification.mail.sender_address.placeholder")}
value={mail.data.senderAddress}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
senderAddress: e.target.value,
},
};
checkChanged(newData.data);
setmail(newData);
}}
/>
<Input
placeholder={t("settings.notification.mail.receiver_address.placeholder")}
className="mt-2"
value={mail.data.receiverAddresses}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
receiverAddresses: e.target.value,
},
};
checkChanged(newData.data);
setmail(newData);
}}
/>
<Input
placeholder={t("settings.notification.mail.smtp_host.placeholder")}
className="mt-2"
value={mail.data.smtpHostAddr}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpHostAddr: e.target.value,
},
};
checkChanged(newData.data);
setmail(newData);
}}
/>
<Input
placeholder={t("settings.notification.mail.smtp_port.placeholder")}
className="mt-2"
value={mail.data.smtpHostPort}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpHostPort: e.target.value,
},
};
checkChanged(newData.data);
setmail(newData);
}}
/>
<Input
placeholder={t("settings.notification.mail.username.placeholder")}
className="mt-2"
value={mail.data.username}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
username: e.target.value,
},
};
checkChanged(newData.data);
setmail(newData);
}}
/>
<Input
placeholder={t("settings.notification.mail.password.placeholder")}
className="mt-2"
value={mail.data.password}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
password: e.target.value,
},
};
checkChanged(newData.data);
setmail(newData);
}}
/>
<div className="flex items-center space-x-1 mt-2">
<Switch id="airplane-mode" checked={mail.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex justify-end mt-2">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<Show when={!changed && mail.id != ""}>
<Button
variant="secondary"
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.config.push.test.message")}
</Button>
</Show>
</div>
</div>
);
};
export default Mail;

View File

@@ -97,7 +97,7 @@ const ServerChan = () => {
if (!isValidURL(serverchan.data.url)) {
toast({
title: t("common.save.failed.message"),
description: t("settings.notification.url.errmsg.invalid"),
description: t("common.errmsg.url_invalid"),
variant: "destructive",
});
return;
@@ -130,22 +130,29 @@ const ServerChan = () => {
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("serverchan");
toast({
title: t("settings.notification.config.push.test.message.success.message"),
description: t("settings.notification.config.push.test.message.success.message"),
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("settings.notification.config.push.test.message.failed.message"),
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
@@ -184,50 +191,56 @@ const ServerChan = () => {
};
return (
<div>
<Input
placeholder={t("settings.notification.serverchan.url.placeholder")}
value={serverchan.data.url}
onChange={(e) => {
const newData = {
...serverchan,
data: {
...serverchan.data,
url: e.target.value,
},
};
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.serverchan.url.label")}</Label>
<Input
placeholder={t("settings.notification.serverchan.url.placeholder")}
value={serverchan.data.url}
onChange={(e) => {
const newData = {
...serverchan,
data: {
...serverchan.data,
url: e.target.value,
},
};
checkChanged(newData.data);
setServerChan(newData);
}}
/>
<div className="flex items-center space-x-1 mt-2">
<Switch id="airplane-mode" checked={serverchan.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
checkChanged(newData.data);
setServerChan(newData);
}}
/>
</div>
<div className="flex justify-end mt-2">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={serverchan.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<Show when={!changed && serverchan.id != ""}>
<Button
variant="secondary"
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.config.push.test.message")}
</Button>
</Show>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<Show when={!changed && serverchan.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);

View File

@@ -123,22 +123,29 @@ const Telegram = () => {
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("telegram");
toast({
title: t("settings.notification.config.push.test.message.success.message"),
description: t("settings.notification.config.push.test.message.success.message"),
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("settings.notification.config.push.test.message.failed.message"),
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
@@ -177,67 +184,76 @@ const Telegram = () => {
};
return (
<div>
<Input
placeholder="ApiToken"
value={telegram.data.apiToken}
onChange={(e) => {
const newData = {
...telegram,
data: {
...telegram.data,
apiToken: e.target.value,
},
};
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.telegram.api_token.label")}</Label>
<Input
placeholder={t("settings.notification.telegram.api_token.placeholder")}
value={telegram.data.apiToken}
onChange={(e) => {
const newData = {
...telegram,
data: {
...telegram.data,
apiToken: e.target.value,
},
};
checkChanged(newData.data);
setTelegram(newData);
}}
/>
<Input
placeholder="ChatId"
value={telegram.data.chatId}
onChange={(e) => {
const newData = {
...telegram,
data: {
...telegram.data,
chatId: e.target.value,
},
};
checkChanged(newData.data);
setTelegram(newData);
}}
/>
<div className="flex items-center space-x-1 mt-2">
<Switch id="airplane-mode" checked={telegram.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
checkChanged(newData.data);
setTelegram(newData);
}}
/>
</div>
<div className="flex justify-end mt-2">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<div>
<Label>{t("settings.notification.telegram.chat_id.label")}</Label>
<Input
placeholder={t("settings.notification.telegram.chat_id.placeholder")}
value={telegram.data.chatId}
onChange={(e) => {
const newData = {
...telegram,
data: {
...telegram.data,
chatId: e.target.value,
},
};
<Show when={!changed && telegram.id != ""}>
<Button
variant="secondary"
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.config.push.test.message")}
</Button>
</Show>
checkChanged(newData.data);
setTelegram(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={telegram.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<Show when={!changed && telegram.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);

View File

@@ -97,7 +97,7 @@ const Webhook = () => {
if (!isValidURL(webhook.data.url)) {
toast({
title: t("common.save.failed.message"),
description: t("settings.notification.url.errmsg.invalid"),
description: t("common.errmsg.url_invalid"),
variant: "destructive",
});
return;
@@ -130,22 +130,29 @@ const Webhook = () => {
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("webhook");
toast({
title: t("settings.notification.config.push.test.message.success.message"),
description: t("settings.notification.config.push.test.message.success.message"),
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("settings.notification.config.push.test.message.failed.message"),
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
@@ -184,50 +191,56 @@ const Webhook = () => {
};
return (
<div>
<Input
placeholder="Url"
value={webhook.data.url}
onChange={(e) => {
const newData = {
...webhook,
data: {
...webhook.data,
url: e.target.value,
},
};
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.webhook.url.label")}</Label>
<Input
placeholder={t("settings.notification.webhook.url.placeholder")}
value={webhook.data.url}
onChange={(e) => {
const newData = {
...webhook,
data: {
...webhook.data,
url: e.target.value,
},
};
checkChanged(newData.data);
setWebhook(newData);
}}
/>
<div className="flex items-center space-x-1 mt-2">
<Switch id="airplane-mode" checked={webhook.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
checkChanged(newData.data);
setWebhook(newData);
}}
/>
</div>
<div className="flex justify-end mt-2">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={webhook.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<Show when={!changed && webhook.id != ""}>
<Button
variant="secondary"
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.config.push.test.message")}
</Button>
</Show>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<Show when={!changed && webhook.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);

View File

@@ -1,6 +1,7 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
@@ -32,11 +33,38 @@ const buttonVariants = cva(
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, loading, asChild = false, children, ...props }, ref) => {
if (asChild) {
return (
<Slot ref={ref} {...props}>
<>
{React.Children.map(children as React.ReactElement, (child: React.ReactElement) => {
return React.cloneElement(child, {
className: cn(buttonVariants({ variant, size }), className),
children: (
<>
{loading && <Loader2 className={cn("h-4 w-4 animate-spin", children && "mr-2")} />}
{child.props.children}
</>
),
});
})}
</>
</Slot>
);
}
return (
<button className={cn(buttonVariants({ variant, size, className }))} disabled={loading} ref={ref} {...props}>
<>
{loading && <Loader2 className={cn("h-4 w-4 animate-spin", children && "mr-2")} />}
{children}
</>
</button>
);
});
Button.displayName = "Button";

View File

@@ -15,7 +15,9 @@ export const accessProvidersMap: Map<AccessProvider["type"], AccessProvider> = n
["aliyun", "common.provider.aliyun", "/imgs/providers/aliyun.svg", "all", "阿里云:alibaba cloud"],
["tencent", "common.provider.tencent", "/imgs/providers/tencent.svg", "all", "腾讯云:tencent cloud"],
["huaweicloud", "common.provider.huaweicloud", "/imgs/providers/huaweicloud.svg", "all", "华为云:huawei cloud"],
["qiniu", "common.provider.qiniu", "/imgs/providers/qiniu.svg", "deploy", "七牛:qiniu"],
["baiducloud", "common.provider.baiducloud", "/imgs/providers/baiducloud.svg", "all", "百度智能云:百度云:baidu cloud"],
["qiniu", "common.provider.qiniu", "/imgs/providers/qiniu.svg", "deploy", "七牛云:qiniu"],
["dogecloud", "common.provider.dogecloud", "/imgs/providers/dogecloud.svg", "deploy", "多吉云:doge cloud"],
["aws", "common.provider.aws", "/imgs/providers/aws.svg", "apply", "亚马逊:amazon:aws"],
["cloudflare", "common.provider.cloudflare", "/imgs/providers/cloudflare.svg", "apply", "cloudflare:cf:cloud flare"],
["namesilo", "common.provider.namesilo", "/imgs/providers/namesilo.svg", "apply", "namesilo"],
@@ -34,7 +36,9 @@ export const accessTypeFormSchema = z.union(
z.literal("aliyun"),
z.literal("tencent"),
z.literal("huaweicloud"),
z.literal("baiducloud"),
z.literal("qiniu"),
z.literal("dogecloud"),
z.literal("aws"),
z.literal("cloudflare"),
z.literal("namesilo"),
@@ -60,6 +64,7 @@ export type Access = {
| TencentConfig
| HuaweiCloudConfig
| QiniuConfig
| DogeCloudConfig
| AwsConfig
| CloudflareConfig
| NamesiloConfig
@@ -91,11 +96,21 @@ export type HuaweiCloudConfig = {
secretAccessKey: string;
};
export type BaiduCloudConfig = {
accessKeyId: string;
secretAccessKey: string;
};
export type QiniuConfig = {
accessKey: string;
secretKey: string;
};
export type DogeCloudConfig = {
accessKey: string;
secretKey: string;
};
export type AwsConfig = {
region: string;
accessKeyId: string;

View File

@@ -85,7 +85,9 @@ export const deployTargetsMap: Map<DeployTarget["type"], DeployTarget> = new Map
["tencent-teo", "common.provider.tencent.teo", "/imgs/providers/tencent.svg"],
["huaweicloud-cdn", "common.provider.huaweicloud.cdn", "/imgs/providers/huaweicloud.svg"],
["huaweicloud-elb", "common.provider.huaweicloud.elb", "/imgs/providers/huaweicloud.svg"],
["baiducloud-cdn", "common.provider.baiducloud.cdn", "/imgs/providers/baiducloud.svg"],
["qiniu-cdn", "common.provider.qiniu.cdn", "/imgs/providers/qiniu.svg"],
["dogecloud-cdn", "common.provider.dogecloud.cdn", "/imgs/providers/dogecloud.svg"],
["local", "common.provider.local", "/imgs/providers/local.svg"],
["ssh", "common.provider.ssh", "/imgs/providers/ssh.svg"],
["webhook", "common.provider.webhook", "/imgs/providers/webhook.svg"],

View File

@@ -18,24 +18,40 @@ export type NotifyTemplate = {
};
export type NotifyChannels = {
email?: NotifyChannelEmail;
webhook?: NotifyChannel;
dingtalk?: NotifyChannel;
lark?: NotifyChannel;
telegram?: NotifyChannel;
webhook?: NotifyChannel;
serverchan?: NotifyChannel;
mail?: NotifyChannelMail;
bark?: NotifyChannelBark;
};
export type NotifyChannel =
| NotifyChannelEmail
| NotifyChannelWebhook
| NotifyChannelDingTalk
| NotifyChannelLark
| NotifyChannelTelegram
| NotifyChannelWebhook
| NotifyChannelServerChan
| NotifyChannelMail
| NotifyChannelBark;
export type NotifyChannelEmail = {
smtpHost: string;
smtpPort: number;
smtpTLS: boolean;
username: string;
password: string;
senderAddress: string;
receiverAddress: string;
enabled: boolean;
};
export type NotifyChannelWebhook = {
url: string;
enabled: boolean;
};
export type NotifyChannelDingTalk = {
accessToken: string;
secret: string;
@@ -53,26 +69,11 @@ export type NotifyChannelTelegram = {
enabled: boolean;
};
export type NotifyChannelWebhook = {
url: string;
enabled: boolean;
};
export type NotifyChannelServerChan = {
url: string;
enabled: boolean;
};
export type NotifyChannelMail = {
senderAddress: string;
receiverAddresses: string;
smtpHostAddr: string;
smtpHostPort: string;
username: string;
password: string;
enabled: boolean;
};
export type NotifyChannelBark = {
deviceKey: string;
serverUrl: string;

View File

@@ -1 +1 @@
export const version = "Certimate v0.2.13";
export const version = "Certimate v0.2.19";

View File

@@ -68,8 +68,12 @@
"common.provider.huaweicloud": "Huawei Cloud",
"common.provider.huaweicloud.cdn": "Huawei Cloud - CDN",
"common.provider.huaweicloud.elb": "Huawei Cloud - ELB",
"common.provider.baiducloud": "Baidu Cloud",
"common.provider.baiducloud.cdn": "Baidu Cloud - CDN",
"common.provider.qiniu": "Qiniu Cloud",
"common.provider.qiniu.cdn": "Qiniu Cloud - CDN",
"common.provider.dogecloud": "Doge Cloud",
"common.provider.dogecloud.cdn": "Doge Cloud - CDN",
"common.provider.aws": "AWS",
"common.provider.cloudflare": "Cloudflare",
"common.provider.namesilo": "Namesilo",
@@ -79,12 +83,12 @@
"common.provider.local": "Local Deployment",
"common.provider.ssh": "SSH Deployment",
"common.provider.webhook": "Webhook",
"common.provider.serverchan": "ServerChan",
"common.provider.kubernetes": "Kubernetes",
"common.provider.kubernetes.secret": "Kubernetes - Secret",
"common.provider.email": "Email",
"common.provider.dingtalk": "DingTalk",
"common.provider.telegram": "Telegram",
"common.provider.lark": "Lark",
"common.provider.mail": "Mail",
"common.provider.telegram": "Telegram",
"common.provider.serverchan": "ServerChan",
"common.provider.bark": "Bark"
}

View File

@@ -30,20 +30,40 @@
"settings.notification.config.enable": "Enable",
"settings.notification.config.saved.message": "Configuration saved successfully",
"settings.notification.config.failed.message": "Configuration save failed",
"settings.notification.config.push.test.message": "Send test notification",
"settings.notification.config.push.test.message.failed.message": "Send test notification failed",
"settings.notification.config.push.test.message.success.message": "Send test notification successfully",
"settings.notification.dingtalk.secret.placeholder": "Signature for signed addition",
"settings.notification.url.errmsg.invalid": "Invalid Url format",
"settings.notification.serverchan.url.placeholder": "Url, e.g. https://sctapi.ftqq.com/****************.send",
"settings.notification.mail.sender_address.placeholder": "Sender email address",
"settings.notification.mail.receiver_address.placeholder": "Receiver email address",
"settings.notification.mail.smtp_host.placeholder": "SMTP server address",
"settings.notification.mail.smtp_port.placeholder": "SMTP server port, if not set, default is 25",
"settings.notification.mail.username.placeholder": "username",
"settings.notification.mail.password.placeholder": "password",
"settings.notification.bark.serverUrl.placeholder": "Server URL, e.g. https://your-bark-server.com, leave it blank to use the bark default server",
"settings.notification.bark.deviceKey.placeholder": "Device Keye.g. XXXXXXXXXXXXXXXXXXXX",
"settings.notification.push_test_message": "Send test notification",
"settings.notification.push_test_message.succeeded.message": "Send test notification successfully",
"settings.notification.push_test_message.failed.message": "Send test notification failed",
"settings.notification.email.smtp_host.label": "SMTP Host",
"settings.notification.email.smtp_host.placeholder": "Please enter SMTP host",
"settings.notification.email.smtp_port.label": "SMTP Port",
"settings.notification.email.smtp_port.placeholder": "Please enter SMTP port",
"settings.notification.email.smtp_tls.label": "Use TLS/SSL",
"settings.notification.email.username.label": "Username",
"settings.notification.email.username.placeholder": "please enter username",
"settings.notification.email.password.label": "Password",
"settings.notification.email.password.placeholder": "please enter password",
"settings.notification.email.sender_address.label": "Sender Email Address",
"settings.notification.email.sender_address.placeholder": "Please enter sender email address",
"settings.notification.email.receiver_address.label": "Receiver Email Address",
"settings.notification.email.receiver_address.placeholder": "Please enter receiver email address",
"settings.notification.webhook.url.label": "Webhook URL",
"settings.notification.webhook.url.placeholder": "Please enter Webhook URL",
"settings.notification.dingtalk.access_token.label": "AccessToken",
"settings.notification.dingtalk.access_token.placeholder": "Please enter access token",
"settings.notification.dingtalk.secret.label": "Secret",
"settings.notification.dingtalk.secret.placeholder": "Please enter secret",
"settings.notification.lark.webhook_url.label": "Webhook URL",
"settings.notification.lark.webhook_url.placeholder": "Please enter Webhook URL",
"settings.notification.telegram.api_token.label": "API Token",
"settings.notification.telegram.api_token.placeholder": "Please enter API token",
"settings.notification.telegram.chat_id.label": "Chat ID",
"settings.notification.telegram.chat_id.placeholder": "Please enter Telegram chat ID",
"settings.notification.serverchan.url.label": "Server URL",
"settings.notification.serverchan.url.placeholder": "Please enter server URL (e.g. https://sctapi.ftqq.com/*****.send)",
"settings.notification.bark.server_url.label": "Server URL",
"settings.notification.bark.server_url.placeholder": "Please enter server URL (e.g. https://your-bark-server.com. Leave it blank to use the bark default server)",
"settings.notification.bark.device_key.label": "Device Key",
"settings.notification.bark.device_key.placeholder": "Please enter device key",
"settings.ca.tab": "Certificate Authority",
"settings.ca.provider.errmsg.empty": "Please select a Certificate Authority",

View File

@@ -68,8 +68,12 @@
"common.provider.huaweicloud": "华为云",
"common.provider.huaweicloud.cdn": "华为云 - 内容分发网络 CDN",
"common.provider.huaweicloud.elb": "华为云 - 弹性负载均衡 ELB",
"common.provider.baiducloud": "百度智能云",
"common.provider.baiducloud.cdn": "百度智能云 - 内容分发网络 CDN",
"common.provider.qiniu": "七牛云",
"common.provider.qiniu.cdn": "七牛云 - 内容分发网络 CDN",
"common.provider.dogecloud": "多吉云",
"common.provider.dogecloud.cdn": "多吉云 - 内容分发网络 CDN",
"common.provider.aws": "AWS",
"common.provider.cloudflare": "Cloudflare",
"common.provider.namesilo": "Namesilo",
@@ -79,12 +83,12 @@
"common.provider.local": "本地部署",
"common.provider.ssh": "SSH 部署",
"common.provider.webhook": "Webhook",
"common.provider.serverchan": "Server酱",
"common.provider.kubernetes": "Kubernetes",
"common.provider.kubernetes.secret": "Kubernetes - Secret",
"common.provider.email": "电子邮件",
"common.provider.dingtalk": "钉钉",
"common.provider.telegram": "Telegram",
"common.provider.lark": "飞书",
"common.provider.mail": "电子邮件",
"common.provider.telegram": "Telegram",
"common.provider.serverchan": "Server酱",
"common.provider.bark": "Bark"
}

View File

@@ -30,20 +30,40 @@
"settings.notification.config.enable": "是否启用",
"settings.notification.config.saved.message": "配置保存成功",
"settings.notification.config.failed.message": "配置保存失败",
"settings.notification.config.push.test.message": "推送测试消息",
"settings.notification.config.push.test.message.failed.message": "推送测试消息失败",
"settings.notification.config.push.test.message.success.message": "推送测试消息成功",
"settings.notification.dingtalk.secret.placeholder": "加签的签名",
"settings.notification.url.errmsg.invalid": "URL 格式不正确",
"settings.notification.serverchan.url.placeholder": "Url, 形如: https://sctapi.ftqq.com/****************.send",
"settings.notification.mail.sender_address.placeholder": "发送邮箱地址",
"settings.notification.mail.receiver_address.placeholder": "接收邮箱地址",
"settings.notification.mail.smtp_host.placeholder": "SMTP服务器地址",
"settings.notification.mail.smtp_port.placeholder": "SMTP服务器端口, 如果未设置, 默认为25",
"settings.notification.mail.username.placeholder": "用于登录到邮件服务器的用户名",
"settings.notification.mail.password.placeholder": "用于登录到邮件服务器的密码",
"settings.notification.bark.serverUrl.placeholder": "服务器URL形如: https://your-bark-server.com 留空则使用 Bark 默认服务器",
"settings.notification.bark.deviceKey.placeholder": "设备密钥,形如: XXXXXXXXXXXXXXXXXXXX",
"settings.notification.push_test_message": "推送测试消息",
"settings.notification.push_test_message.failed.message": "推送测试消息失败",
"settings.notification.push_test_message.succeeded.message": "推送测试消息成功",
"settings.notification.email.smtp_host.label": "SMTP 服务器地址",
"settings.notification.email.smtp_host.placeholder": "请输入 SMTP 服务器地址",
"settings.notification.email.smtp_port.label": "SMTP 服务器端口",
"settings.notification.email.smtp_port.placeholder": "请输入 SMTP 服务器端口",
"settings.notification.email.smtp_tls.label": "TLS/SSL 连接",
"settings.notification.email.username.label": "用户名",
"settings.notification.email.username.placeholder": "请输入用户名",
"settings.notification.email.password.label": "密码",
"settings.notification.email.password.placeholder": "请输入密码",
"settings.notification.email.sender_address.label": "发送邮箱地址",
"settings.notification.email.sender_address.placeholder": "请输入发送邮箱地址",
"settings.notification.email.receiver_address.label": "接收邮箱地址",
"settings.notification.email.receiver_address.placeholder": "请输入接收邮箱地址",
"settings.notification.webhook.url.label": "Webhook 回调地址",
"settings.notification.webhook.url.placeholder": "请输入 Webhook 回调地址",
"settings.notification.dingtalk.access_token.label": "AccessToken",
"settings.notification.dingtalk.access_token.placeholder": "请输入 AccessToken",
"settings.notification.dingtalk.secret.label": "签名密钥",
"settings.notification.dingtalk.secret.placeholder": "请输入签名密钥",
"settings.notification.lark.webhook_url.label": "Webhook URL",
"settings.notification.lark.webhook_url.placeholder": "请输入 Webhook URL",
"settings.notification.telegram.api_token.label": "API Token",
"settings.notification.telegram.api_token.placeholder": "请输入 API token",
"settings.notification.telegram.chat_id.label": "会话 ID",
"settings.notification.telegram.chat_id.placeholder": "请输入 Telegram 会话 ID",
"settings.notification.serverchan.url.label": "服务器 URL",
"settings.notification.serverchan.url.placeholder": "请输入服务器 URL形如: https://sctapi.ftqq.com/*****.send",
"settings.notification.bark.server_url.label": "服务器 URL",
"settings.notification.bark.server_url.placeholder": "请输入服务器 URL形如: https://your-bark-server.com留空则使用 Bark 默认服务器)",
"settings.notification.bark.device_key.label": "设备密钥",
"settings.notification.bark.device_key.placeholder": "请输入设备密钥",
"settings.ca.tab": "证书颁发机构CA",
"settings.ca.provider.errmsg.empty": "请选择证书分发机构",

View File

@@ -7,7 +7,7 @@ import NotifyTemplate from "@/components/notify/NotifyTemplate";
import Telegram from "@/components/notify/Telegram";
import Webhook from "@/components/notify/Webhook";
import ServerChan from "@/components/notify/ServerChan";
import Mail from "@/components/notify/Mail";
import Email from "@/components/notify/Email";
import Bark from "@/components/notify/Bark";
import { NotifyProvider } from "@/providers/notify";
@@ -27,51 +27,52 @@ const Notify = () => {
</AccordionItem>
</Accordion>
</div>
<div className="border rounded-md p-5 mt-7 shadow-lg">
<Accordion type={"single"} className="dark:text-stone-200">
<AccordionItem value="item-2" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.dingtalk")}</AccordionTrigger>
<AccordionItem value="item-email" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.email")}</AccordionTrigger>
<AccordionContent>
<DingTalk />
<Email />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.lark")}</AccordionTrigger>
<AccordionContent>
<Lark />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-4" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.telegram")}</AccordionTrigger>
<AccordionContent>
<Telegram />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-5" className="dark:border-stone-200">
<AccordionItem value="item-webhook" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.webhook")}</AccordionTrigger>
<AccordionContent>
<Webhook />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-6" className="dark:border-stone-200">
<AccordionItem value="item-dingtalk" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.dingtalk")}</AccordionTrigger>
<AccordionContent>
<DingTalk />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-lark" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.lark")}</AccordionTrigger>
<AccordionContent>
<Lark />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-telegram" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.telegram")}</AccordionTrigger>
<AccordionContent>
<Telegram />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-serverchan" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.serverchan")}</AccordionTrigger>
<AccordionContent>
<ServerChan />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-7" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.mail")}</AccordionTrigger>
<AccordionContent>
<Mail />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-8" className="dark:border-stone-200">
<AccordionItem value="item-bark" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.bark")}</AccordionTrigger>
<AccordionContent>
<Bark />