Compare commits

...

6 Commits

Author SHA1 Message Date
Fu Diwei
52d24ff2f2 feat: improve i18n 2024-12-23 22:46:07 +08:00
Fu Diwei
7a66bdf139 fix: fix typo 2024-12-23 22:46:01 +08:00
Fu Diwei
16bc12c15b feat update placeholder syntax in notify templates 2024-12-23 22:33:12 +08:00
Fu Diwei
0556d68a4e feat(ui): MultipleInput 2024-12-23 22:22:00 +08:00
Fu Diwei
586c7fa927 feat: create DNSProvider using independent config instead of envvar 2024-12-23 19:58:51 +08:00
Fu Diwei
9ef16ebcf9 refactor: clean code 2024-12-23 19:31:48 +08:00
28 changed files with 569 additions and 245 deletions

3
go.sum
View File

@ -392,6 +392,7 @@ github.com/gojek/heimdall/v7 v7.0.3/go.mod h1:Z43HtMid7ysSjmsedPTXAki6jcdcNVnjn5
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf h1:5xRGbUdOmZKoDXkGx5evVLehuCMpuO1hl701bEQqXOM=
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf/go.mod h1:QzhUKaYKJmcbTnCYCAVQrroCOY7vOOI8cSQ4NbuhYf0=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
@ -697,6 +698,8 @@ github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/pocketbase v0.22.21 h1:DGPCxn6co8VuTV0mton4NFO/ON49XiFMszRr+Mysy48=
github.com/pocketbase/pocketbase v0.22.21/go.mod h1:Cw5E4uoGhKItBIE2lJL3NfmiUr9Syk2xaNJ2G7Dssow=
github.com/pocketbase/pocketbase v0.23.12 h1:HB4THFbzaliF0C3wvpx+kNOZxIwCEMDqN3/17gn5N7E=
github.com/pocketbase/pocketbase v0.23.12/go.mod h1:OcFJNMO0Vzt3f9+lweMbup6iL7V13ckxu1pdEY6FeM0=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=

View File

@ -7,10 +7,10 @@ import (
"github.com/pocketbase/pocketbase/tools/cron"
)
var schedulerOnce sync.Once
var scheduler *cron.Cron
var schedulerOnce sync.Once
func GetScheduler() *cron.Cron {
schedulerOnce.Do(func() {
scheduler = cron.New()

View File

@ -0,0 +1,43 @@
package applicant
import (
"encoding/json"
"net/url"
"time"
"github.com/go-acme/lego/v4/providers/dns/httpreq"
"github.com/usual2970/certimate/internal/domain"
)
type acmeHttpReqApplicant struct {
option *ApplyOption
}
func NewACMEHttpReqApplicant(option *ApplyOption) Applicant {
return &acmeHttpReqApplicant{
option: option,
}
}
func (a *acmeHttpReqApplicant) Apply() (*Certificate, error) {
access := &domain.HttpreqAccess{}
json.Unmarshal([]byte(a.option.Access), access)
config := httpreq.NewDefaultConfig()
endpoint, _ := url.Parse(access.Endpoint)
config.Endpoint = endpoint
config.Mode = access.Mode
config.Username = access.Username
config.Password = access.Password
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
}
provider, err := httpreq.NewDNSProviderConfig(config)
if err != nil {
return nil, err
}
return apply(a.option, provider)
}

View File

@ -2,35 +2,38 @@ package applicant
import (
"encoding/json"
"fmt"
"os"
"time"
"github.com/go-acme/lego/v4/providers/dns/alidns"
"github.com/usual2970/certimate/internal/domain"
)
type aliyun struct {
type aliyunApplicant struct {
option *ApplyOption
}
func NewAliyun(option *ApplyOption) Applicant {
return &aliyun{
func NewAliyunApplicant(option *ApplyOption) Applicant {
return &aliyunApplicant{
option: option,
}
}
func (a *aliyun) Apply() (*Certificate, error) {
func (a *aliyunApplicant) Apply() (*Certificate, error) {
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("ALICLOUD_ACCESS_KEY", access.AccessKeyId)
os.Setenv("ALICLOUD_SECRET_KEY", access.AccessKeySecret)
os.Setenv("ALICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := alidns.NewDNSProvider()
config := alidns.NewDefaultConfig()
config.APIKey = access.AccessKeyId
config.SecretKey = access.AccessKeySecret
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
}
provider, err := alidns.NewDNSProviderConfig(config)
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
return apply(a.option, provider)
}

View File

@ -208,29 +208,33 @@ func GetWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
}
func GetWithTypeOption(t string, option *ApplyOption) (Applicant, error) {
/*
注意如果追加新的常量值请保持以 ASCII 排序
NOTICE: If you add new constant, please keep ASCII order.
*/
switch t {
case configTypeAliyun:
return NewAliyun(option), nil
case configTypeTencentCloud:
return NewTencent(option), nil
case configTypeHuaweiCloud:
return NewHuaweiCloud(option), nil
case configTypeAWS:
return NewAws(option), nil
case configTypeCloudflare:
return NewCloudflare(option), nil
case configTypeNameSilo:
return NewNamesilo(option), nil
case configTypeGoDaddy:
return NewGodaddy(option), nil
case configTypePowerDNS:
return NewPdns(option), nil
case configTypeACMEHttpReq:
return NewHttpreq(option), nil
return NewACMEHttpReqApplicant(option), nil
case configTypeAliyun:
return NewAliyunApplicant(option), nil
case configTypeAWS:
return NewAWSApplicant(option), nil
case configTypeCloudflare:
return NewCloudflareApplicant(option), nil
case configTypeGoDaddy:
return NewGoDaddyApplicant(option), nil
case configTypeHuaweiCloud:
return NewHuaweiCloudApplicant(option), nil
case configTypeNameSilo:
return NewNamesiloApplicant(option), nil
case configTypePowerDNS:
return NewPowerDNSApplicant(option), nil
case configTypeTencentCloud:
return NewTencentCloudApplicant(option), nil
case configTypeVolcEngine:
return NewVolcengine(option), nil
return NewVolcEngineApplicant(option), nil
default:
return nil, errors.New("unknown config type")
return nil, fmt.Errorf("unsupported applicant type: %s", t)
}
}

View File

@ -2,38 +2,40 @@ package applicant
import (
"encoding/json"
"fmt"
"os"
"time"
"github.com/go-acme/lego/v4/providers/dns/route53"
"github.com/usual2970/certimate/internal/domain"
)
type aws struct {
type awsApplicant struct {
option *ApplyOption
}
func NewAws(option *ApplyOption) Applicant {
return &aws{
func NewAWSApplicant(option *ApplyOption) Applicant {
return &awsApplicant{
option: option,
}
}
func (t *aws) Apply() (*Certificate, error) {
func (a *awsApplicant) Apply() (*Certificate, error) {
access := &domain.AwsAccess{}
json.Unmarshal([]byte(t.option.Access), access)
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("AWS_REGION", access.Region)
os.Setenv("AWS_ACCESS_KEY_ID", access.AccessKeyId)
os.Setenv("AWS_SECRET_ACCESS_KEY", access.SecretAccessKey)
os.Setenv("AWS_HOSTED_ZONE_ID", access.HostedZoneId)
os.Setenv("AWS_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
config := route53.NewDefaultConfig()
config.AccessKeyID = access.AccessKeyId
config.SecretAccessKey = access.SecretAccessKey
config.Region = access.Region
config.HostedZoneID = access.HostedZoneId
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
}
dnsProvider, err := route53.NewDNSProvider()
provider, err := route53.NewDNSProviderConfig(config)
if err != nil {
return nil, err
}
return apply(t.option, dnsProvider)
return apply(a.option, provider)
}

View File

@ -2,35 +2,37 @@ package applicant
import (
"encoding/json"
"fmt"
"os"
"time"
cf "github.com/go-acme/lego/v4/providers/dns/cloudflare"
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
"github.com/usual2970/certimate/internal/domain"
)
type cloudflare struct {
type cloudflareApplicant struct {
option *ApplyOption
}
func NewCloudflare(option *ApplyOption) Applicant {
return &cloudflare{
func NewCloudflareApplicant(option *ApplyOption) Applicant {
return &cloudflareApplicant{
option: option,
}
}
func (c *cloudflare) Apply() (*Certificate, error) {
func (a *cloudflareApplicant) Apply() (*Certificate, error) {
access := &domain.CloudflareAccess{}
json.Unmarshal([]byte(c.option.Access), access)
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("CLOUDFLARE_DNS_API_TOKEN", access.DnsApiToken)
os.Setenv("CLOUDFLARE_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", c.option.Timeout))
config := cloudflare.NewDefaultConfig()
config.AuthToken = access.DnsApiToken
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
}
provider, err := cf.NewDNSProvider()
provider, err := cloudflare.NewDNSProviderConfig(config)
if err != nil {
return nil, err
}
return apply(c.option, provider)
return apply(a.option, provider)
}

View File

@ -2,36 +2,38 @@ package applicant
import (
"encoding/json"
"fmt"
"os"
"time"
godaddyProvider "github.com/go-acme/lego/v4/providers/dns/godaddy"
"github.com/go-acme/lego/v4/providers/dns/godaddy"
"github.com/usual2970/certimate/internal/domain"
)
type godaddy struct {
type godaddyApplicant struct {
option *ApplyOption
}
func NewGodaddy(option *ApplyOption) Applicant {
return &godaddy{
func NewGoDaddyApplicant(option *ApplyOption) Applicant {
return &godaddyApplicant{
option: option,
}
}
func (a *godaddy) Apply() (*Certificate, error) {
func (a *godaddyApplicant) Apply() (*Certificate, error) {
access := &domain.GodaddyAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("GODADDY_API_KEY", access.ApiKey)
os.Setenv("GODADDY_API_SECRET", access.ApiSecret)
os.Setenv("GODADDY_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
config := godaddy.NewDefaultConfig()
config.APIKey = access.ApiKey
config.APISecret = access.ApiSecret
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
}
dnsProvider, err := godaddyProvider.NewDNSProvider()
provider, err := godaddy.NewDNSProviderConfig(config)
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
return apply(a.option, provider)
}

View File

@ -1,38 +0,0 @@
package applicant
import (
"encoding/json"
"fmt"
"os"
"github.com/go-acme/lego/v4/providers/dns/httpreq"
"github.com/usual2970/certimate/internal/domain"
)
type httpReq struct {
option *ApplyOption
}
func NewHttpreq(option *ApplyOption) Applicant {
return &httpReq{
option: option,
}
}
func (a *httpReq) Apply() (*Certificate, error) {
access := &domain.HttpreqAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("HTTPREQ_ENDPOINT", access.Endpoint)
os.Setenv("HTTPREQ_MODE", access.Mode)
os.Setenv("HTTPREQ_USERNAME", access.Username)
os.Setenv("HTTPREQ_PASSWORD", access.Password)
os.Setenv("HTTPREQ_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := httpreq.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
}

View File

@ -2,42 +2,45 @@ package applicant
import (
"encoding/json"
"fmt"
"os"
"time"
huaweicloudProvider "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
huaweicloud "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
"github.com/usual2970/certimate/internal/domain"
)
type huaweicloud struct {
type huaweicloudApplicant struct {
option *ApplyOption
}
func NewHuaweiCloud(option *ApplyOption) Applicant {
return &huaweicloud{
func NewHuaweiCloudApplicant(option *ApplyOption) Applicant {
return &huaweicloudApplicant{
option: option,
}
}
func (t *huaweicloud) Apply() (*Certificate, error) {
func (a *huaweicloudApplicant) Apply() (*Certificate, error) {
access := &domain.HuaweiCloudAccess{}
json.Unmarshal([]byte(t.option.Access), access)
json.Unmarshal([]byte(a.option.Access), access)
region := access.Region
if region == "" {
// 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
region = "cn-north-1"
}
os.Setenv("HUAWEICLOUD_REGION", region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错
os.Setenv("HUAWEICLOUD_ACCESS_KEY_ID", access.AccessKeyId)
os.Setenv("HUAWEICLOUD_SECRET_ACCESS_KEY", access.SecretAccessKey)
os.Setenv("HUAWEICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
config := huaweicloud.NewDefaultConfig()
config.AccessKeyID = access.AccessKeyId
config.SecretAccessKey = access.SecretAccessKey
config.Region = region
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
}
dnsProvider, err := huaweicloudProvider.NewDNSProvider()
provider, err := huaweicloud.NewDNSProviderConfig(config)
if err != nil {
return nil, err
}
return apply(t.option, dnsProvider)
return apply(a.option, provider)
}

View File

@ -2,35 +2,37 @@ package applicant
import (
"encoding/json"
"fmt"
"os"
"time"
namesiloProvider "github.com/go-acme/lego/v4/providers/dns/namesilo"
namesilo "github.com/go-acme/lego/v4/providers/dns/namesilo"
"github.com/usual2970/certimate/internal/domain"
)
type namesilo struct {
type namesiloApplicant struct {
option *ApplyOption
}
func NewNamesilo(option *ApplyOption) Applicant {
return &namesilo{
func NewNamesiloApplicant(option *ApplyOption) Applicant {
return &namesiloApplicant{
option: option,
}
}
func (a *namesilo) Apply() (*Certificate, error) {
func (a *namesiloApplicant) Apply() (*Certificate, error) {
access := &domain.NameSiloAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("NAMESILO_API_KEY", access.ApiKey)
os.Setenv("NAMESILO_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
config := namesilo.NewDefaultConfig()
config.APIKey = access.ApiKey
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
}
dnsProvider, err := namesiloProvider.NewDNSProvider()
provider, err := namesilo.NewDNSProviderConfig(config)
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
return apply(a.option, provider)
}

View File

@ -1,36 +0,0 @@
package applicant
import (
"encoding/json"
"fmt"
"os"
"github.com/go-acme/lego/v4/providers/dns/pdns"
"github.com/usual2970/certimate/internal/domain"
)
type powerdns struct {
option *ApplyOption
}
func NewPdns(option *ApplyOption) Applicant {
return &powerdns{
option: option,
}
}
func (a *powerdns) Apply() (*Certificate, error) {
access := &domain.PdnsAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("PDNS_API_URL", access.ApiUrl)
os.Setenv("PDNS_API_KEY", access.ApiKey)
os.Setenv("PDNS_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := pdns.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
}

View File

@ -0,0 +1,41 @@
package applicant
import (
"encoding/json"
"net/url"
"time"
"github.com/go-acme/lego/v4/providers/dns/pdns"
"github.com/usual2970/certimate/internal/domain"
)
type powerdnsApplicant struct {
option *ApplyOption
}
func NewPowerDNSApplicant(option *ApplyOption) Applicant {
return &powerdnsApplicant{
option: option,
}
}
func (a *powerdnsApplicant) Apply() (*Certificate, error) {
access := &domain.PdnsAccess{}
json.Unmarshal([]byte(a.option.Access), access)
config := pdns.NewDefaultConfig()
host, _ := url.Parse(access.ApiUrl)
config.Host = host
config.APIKey = access.ApiKey
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
}
provider, err := pdns.NewDNSProviderConfig(config)
if err != nil {
return nil, err
}
return apply(a.option, provider)
}

View File

@ -1,37 +0,0 @@
package applicant
import (
"encoding/json"
"fmt"
"os"
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
"github.com/usual2970/certimate/internal/domain"
)
type tencent struct {
option *ApplyOption
}
func NewTencent(option *ApplyOption) Applicant {
return &tencent{
option: option,
}
}
func (t *tencent) Apply() (*Certificate, error) {
access := &domain.TencentAccess{}
json.Unmarshal([]byte(t.option.Access), access)
os.Setenv("TENCENTCLOUD_SECRET_ID", access.SecretId)
os.Setenv("TENCENTCLOUD_SECRET_KEY", access.SecretKey)
os.Setenv("TENCENTCLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout))
dnsProvider, err := tencentcloud.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(t.option, dnsProvider)
}

View File

@ -0,0 +1,39 @@
package applicant
import (
"encoding/json"
"time"
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
"github.com/usual2970/certimate/internal/domain"
)
type tencentcloudApplicant struct {
option *ApplyOption
}
func NewTencentCloudApplicant(option *ApplyOption) Applicant {
return &tencentcloudApplicant{
option: option,
}
}
func (a *tencentcloudApplicant) Apply() (*Certificate, error) {
access := &domain.TencentAccess{}
json.Unmarshal([]byte(a.option.Access), access)
config := tencentcloud.NewDefaultConfig()
config.SecretID = access.SecretId
config.SecretKey = access.SecretKey
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
}
provider, err := tencentcloud.NewDNSProviderConfig(config)
if err != nil {
return nil, err
}
return apply(a.option, provider)
}

View File

@ -2,34 +2,37 @@ package applicant
import (
"encoding/json"
"fmt"
"os"
"time"
volcengineDns "github.com/go-acme/lego/v4/providers/dns/volcengine"
"github.com/go-acme/lego/v4/providers/dns/volcengine"
"github.com/usual2970/certimate/internal/domain"
)
type volcengine struct {
type volcengineApplicant struct {
option *ApplyOption
}
func NewVolcengine(option *ApplyOption) Applicant {
return &volcengine{
func NewVolcEngineApplicant(option *ApplyOption) Applicant {
return &volcengineApplicant{
option: option,
}
}
func (a *volcengine) Apply() (*Certificate, error) {
func (a *volcengineApplicant) Apply() (*Certificate, error) {
access := &domain.VolcEngineAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("VOLC_ACCESSKEY", access.AccessKeyId)
os.Setenv("VOLC_SECRETKEY", access.SecretAccessKey)
os.Setenv("VOLC_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := volcengineDns.NewDNSProvider()
config := volcengine.NewDefaultConfig()
config.AccessKey = access.AccessKeyId
config.SecretKey = access.SecretAccessKey
if a.option.Timeout != 0 {
config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second
}
provider, err := volcengine.NewDNSProviderConfig(config)
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
return apply(a.option, provider)
}

View File

@ -13,8 +13,8 @@ import (
)
const (
defaultExpireSubject = "您有 {COUNT} 张证书即将过期"
defaultExpireMessage = "有 {COUNT} 张证书即将过期,域名分别为 {DOMAINS},请保持关注!"
defaultExpireSubject = "您有 ${COUNT} 张证书即将过期"
defaultExpireMessage = "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!"
)
type CertificateRepository interface {
@ -88,11 +88,11 @@ func buildMsg(records []domain.Certificate) *domain.NotifyMessage {
countStr := strconv.Itoa(count)
domainStr := strings.Join(domains, ";")
subject = strings.ReplaceAll(subject, "{COUNT}", countStr)
subject = strings.ReplaceAll(subject, "{DOMAINS}", domainStr)
subject = strings.ReplaceAll(subject, "${COUNT}", countStr)
subject = strings.ReplaceAll(subject, "${DOMAINS}", domainStr)
message = strings.ReplaceAll(message, "{COUNT}", countStr)
message = strings.ReplaceAll(message, "{DOMAINS}", domainStr)
message = strings.ReplaceAll(message, "${COUNT}", countStr)
message = strings.ReplaceAll(message, "${DOMAINS}", domainStr)
// 返回消息
return &domain.NotifyMessage{

View File

@ -1,3 +1,5 @@
import { ClientResponseError } from "pocketbase";
import { getPocketBase } from "@/repository/pocketbase";
export const notifyTest = async (channel: string) => {
@ -14,7 +16,7 @@ export const notifyTest = async (channel: string) => {
});
if (resp.code != 0) {
throw new Error(resp.msg);
throw new ClientResponseError({ status: resp.code, response: resp, data: {} });
}
return resp;

View File

@ -1,4 +1,6 @@
import { Statistics } from "@/domain/statistics";
import { ClientResponseError } from "pocketbase";
import { type Statistics } from "@/domain/statistics";
import { getPocketBase } from "@/repository/pocketbase";
export const get = async () => {
@ -8,8 +10,8 @@ export const get = async () => {
method: "GET",
});
if (resp.code !== 0) {
throw new Error(resp.msg);
if (resp.code != 0) {
throw new ClientResponseError({ status: resp.code, response: resp, data: {} });
}
return resp.data as Statistics;

View File

@ -1,3 +1,5 @@
import { ClientResponseError } from "pocketbase";
import { getPocketBase } from "@/repository/pocketbase";
export const run = async (id: string) => {
@ -14,7 +16,7 @@ export const run = async (id: string) => {
});
if (resp.code != 0) {
throw new Error(resp.msg);
throw new ClientResponseError({ status: resp.code, response: resp, data: {} });
}
return resp;

View File

@ -0,0 +1,275 @@
import { forwardRef, useImperativeHandle, useMemo, useRef, type ChangeEvent } from "react";
import { useTranslation } from "react-i18next";
import { useControllableValue } from "ahooks";
import { Button, Input, Space, type InputRef, type InputProps } from "antd";
import { produce } from "immer";
import { ArrowDown as ArrowDownIcon, ArrowUp as ArrowUpIcon, Minus as MinusIcon, Plus as PlusIcon } from "lucide-react";
export type MultipleInputProps = Omit<InputProps, "count" | "defaultValue" | "showCount" | "value" | "onChange" | "onPressEnter" | "onClear"> & {
allowClear?: boolean;
defaultValue?: string[];
maxCount?: number;
minCount?: number;
showSortButton?: boolean;
value?: string[];
onChange?: (index: number, e: ChangeEvent<HTMLInputElement>) => void;
onCreate?: (index: number) => void;
onRemove?: (index: number) => void;
onSort?: (oldIndex: number, newIndex: number) => void;
onValueChange?: (value: string[]) => void;
};
const MultipleInput = ({
allowClear = false,
disabled,
maxCount,
minCount,
showSortButton = true,
onChange,
onCreate,
onSort,
onRemove,
...props
}: MultipleInputProps) => {
const { t } = useTranslation();
const itemRefs = useRef<MultipleInputItemInstance[]>([]);
const [value, setValue] = useControllableValue<string[]>(props, {
valuePropName: "value",
defaultValue: [],
defaultValuePropName: "defaultValue",
trigger: "onValueChange",
});
const handleCreate = () => {
const newValue = produce(value, (draft) => {
draft.push("");
});
setValue(newValue);
setTimeout(() => itemRefs.current[newValue.length - 1]?.focus(), 0);
onCreate?.(newValue.length - 1);
};
const handleInputChange = (index: number, e: ChangeEvent<HTMLInputElement>) => {
const newValue = produce(value, (draft) => {
draft[index] = e.target.value;
});
setValue(newValue);
onChange?.(index, e);
};
const handleInputBlur = (index: number) => {
if (!allowClear && !value[index]) {
const newValue = produce(value, (draft) => {
draft.splice(index, 1);
});
setValue(newValue);
}
};
const handleClickUp = (index: number) => {
if (index === 0) {
return;
}
const newValue = produce(value, (draft) => {
const temp = draft[index - 1];
draft[index - 1] = draft[index];
draft[index] = temp;
});
setValue(newValue);
onSort?.(index, index - 1);
};
const handleClickDown = (index: number) => {
if (index === value.length - 1) {
return;
}
const newValue = produce(value, (draft) => {
const temp = draft[index + 1];
draft[index + 1] = draft[index];
draft[index] = temp;
});
setValue(newValue);
onSort?.(index, index + 1);
};
const handleClickAdd = (index: number) => {
const newValue = produce(value, (draft) => {
draft.splice(index + 1, 0, "");
});
setValue(newValue);
setTimeout(() => itemRefs.current[index + 1]?.focus(), 0);
onCreate?.(index + 1);
};
const handleClickRemove = (index: number) => {
const newValue = produce(value, (draft) => {
draft.splice(index, 1);
});
setValue(newValue);
onRemove?.(index);
};
return value == null || value.length === 0 ? (
<Button block color="primary" disabled={disabled || maxCount === 0} size={props.size} variant="dashed" onClick={handleCreate}>
{t("common.button.add")}
</Button>
) : (
<Space className="w-full" direction="vertical" size="small">
{value.map((element, index) => {
const allowUp = index > 0;
const allowDown = index < value.length - 1;
const allowRemove = minCount == null || value.length > minCount;
const allowAdd = maxCount == null || value.length < maxCount;
return (
<MultipleInputItem
{...props}
ref={(ref) => (itemRefs.current[index] = ref!)}
allowAdd={allowAdd}
allowClear={allowClear}
allowDown={allowDown}
allowRemove={allowRemove}
allowUp={allowUp}
disabled={disabled}
defaultValue={undefined}
showSortButton={showSortButton}
value={element}
onBlur={() => handleInputBlur(index)}
onChange={(val) => handleInputChange(index, val)}
onClickAdd={() => handleClickAdd(index)}
onClickDown={() => handleClickDown(index)}
onClickUp={() => handleClickUp(index)}
onClickRemove={() => handleClickRemove(index)}
onValueChange={undefined}
/>
);
})}
</Space>
);
};
type MultipleInputItemProps = Omit<
MultipleInputProps,
"defaultValue" | "maxCount" | "minCount" | "preset" | "value" | "onChange" | "onCreate" | "onRemove" | "onSort" | "onValueChange"
> & {
allowAdd: boolean;
allowRemove: boolean;
allowUp: boolean;
allowDown: boolean;
defaultValue?: string;
value?: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
onClickAdd?: () => void;
onClickDown?: () => void;
onClickUp?: () => void;
onClickRemove?: () => void;
onValueChange?: (value: string) => void;
};
type MultipleInputItemInstance = {
focus: () => void;
blur: () => void;
select: () => void;
};
const MultipleInputItem = forwardRef<MultipleInputItemInstance, MultipleInputItemProps>(
(
{
allowAdd,
allowClear,
allowDown,
allowRemove,
allowUp,
disabled,
showSortButton,
onChange,
onClickAdd,
onClickDown,
onClickUp,
onClickRemove,
...props
}: MultipleInputItemProps,
ref
) => {
const inputRef = useRef<InputRef>(null);
const [value, setValue] = useControllableValue<string>(props, {
valuePropName: "value",
defaultValue: "",
defaultValuePropName: "defaultValue",
trigger: "onValueChange",
});
const upBtn = useMemo(() => {
if (!showSortButton) return null;
return <Button icon={<ArrowUpIcon size={14} />} color="default" disabled={disabled || !allowUp} shape="circle" variant="text" onClick={onClickUp} />;
}, [allowUp, disabled, showSortButton, onClickUp]);
const downBtn = useMemo(() => {
if (!showSortButton) return null;
return (
<Button icon={<ArrowDownIcon size={14} />} color="default" disabled={disabled || !allowDown} shape="circle" variant="text" onClick={onClickDown} />
);
}, [allowDown, disabled, showSortButton, onClickDown]);
const removeBtn = useMemo(() => {
return (
<Button icon={<MinusIcon size={14} />} color="default" disabled={disabled || !allowRemove} shape="circle" variant="text" onClick={onClickRemove} />
);
}, [allowRemove, disabled, onClickRemove]);
const addBtn = useMemo(() => {
return <Button icon={<PlusIcon size={14} />} color="default" disabled={disabled || !allowAdd} shape="circle" variant="text" onClick={onClickAdd} />;
}, [allowAdd, disabled, onClickAdd]);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
onChange?.(e);
};
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
blur: () => {
inputRef.current?.blur();
},
select: () => {
inputRef.current?.select();
},
}));
return (
<div className="flex flex-nowrap items-center space-x-2">
<div className="flex-grow">
<Input
{...props}
ref={inputRef}
className={undefined}
style={undefined}
allowClear={allowClear}
defaultValue={undefined}
value={value}
onChange={handleChange}
/>
</div>
<div>
{removeBtn}
{upBtn}
{downBtn}
{addBtn}
</div>
</div>
);
}
);
export default MultipleInput;

View File

@ -33,8 +33,8 @@ export type NotifyTemplate = {
};
export const defaultNotifyTemplate: NotifyTemplate = {
subject: "您有 {COUNT} 张证书即将过期",
message: "有 {COUNT} 张证书即将过期,域名分别为 {DOMAINS},请保持关注!",
subject: "您有 ${COUNT} 张证书即将过期",
message: "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!",
};
// #endregion

View File

@ -17,6 +17,9 @@ i18n
backend: {
loadPath: "/locales/{{lng}}.json",
},
detection: {
lookupLocalStorage: "certimate-ui-lang",
},
});
export const localeNames = {

View File

@ -19,10 +19,10 @@
"settings.notification.template.card.title": "Template",
"settings.notification.template.form.subject.label": "Subject",
"settings.notification.template.form.subject.placeholder": "Please enter notification subject",
"settings.notification.template.form.subject.tooltip": "Optional variables ({COUNT}: number of expiring soon)",
"settings.notification.template.form.subject.tooltip": "Optional variables (${COUNT}: number of expiring soon)",
"settings.notification.template.form.message.label": "Message",
"settings.notification.template.form.message.placeholder": "Please enter notification message",
"settings.notification.template.form.message.tooltip": "Optional variables ({COUNT}: number of expiring soon. {DOMAINS}: Domain list)",
"settings.notification.template.form.message.tooltip": "Optional variables (${COUNT}: number of expiring soon. ${DOMAINS}: Domain list)",
"settings.notification.channels.card.title": "Channels",
"settings.notification.channel.enabled.on": "On",
"settings.notification.channel.enabled.off": "Off",

View File

@ -13,7 +13,9 @@
"workflow.props.name.default": "Unnamed",
"workflow.props.description": "Description",
"workflow.props.description.placeholder": "Please enter description",
"workflow.props.execution_method": "Execution Method",
"workflow.props.trigger": "Trigger",
"workflow.props.trigger.auto": "Auto",
"workflow.props.trigger.manual": "Manual",
"workflow.props.state": "State",
"workflow.props.state.filter.enabled": "Enabled",
"workflow.props.state.filter.disabled": "Disabled",

View File

@ -19,10 +19,10 @@
"settings.notification.template.card.title": "通知模板",
"settings.notification.template.form.subject.label": "通知主题",
"settings.notification.template.form.subject.placeholder": "请输入通知主题",
"settings.notification.template.form.subject.tooltip": "可选的变量({COUNT}: 即将过期张数)",
"settings.notification.template.form.subject.tooltip": "可选的变量(${COUNT}: 即将过期张数)",
"settings.notification.template.form.message.label": "通知内容",
"settings.notification.template.form.message.placeholder": "请输入通知内容",
"settings.notification.template.form.message.tooltip": "可选的变量({COUNT}: 即将过期张数;{DOMAINS}: 域名列表)",
"settings.notification.template.form.message.tooltip": "可选的变量(${COUNT}: 即将过期张数;${DOMAINS}: 域名列表)",
"settings.notification.channels.card.title": "通知渠道",
"settings.notification.channel.enabled.on": "启用",
"settings.notification.channel.enabled.off": "未启用",

View File

@ -13,7 +13,9 @@
"workflow.props.name.default": "未命名工作流",
"workflow.props.description": "描述",
"workflow.props.description.placeholder": "请输入描述",
"workflow.props.execution_method": "执行方式",
"workflow.props.trigger": "触发方式",
"workflow.props.trigger.auto": "自动",
"workflow.props.trigger.manual": "手动",
"workflow.props.state": "启用状态",
"workflow.props.state.filter.enabled": "启用",
"workflow.props.state.filter.disabled": "未启用",

View File

@ -61,19 +61,19 @@ const WorkflowList = () => {
),
},
{
key: "type",
title: t("workflow.props.execution_method"),
key: "trigger",
title: t("workflow.props.trigger"),
ellipsis: true,
render: (_, record) => {
const method = record.type;
if (!method) {
const trigger = record.type;
if (!trigger) {
return "-";
} else if (method === "manual") {
return <Typography.Text>{t("workflow.node.start.form.executionMethod.options.manual")}</Typography.Text>;
} else if (method === "auto") {
} else if (trigger === "manual") {
return <Typography.Text>{t("workflow.props.trigger.manual")}</Typography.Text>;
} else if (trigger === "auto") {
return (
<Space className="max-w-full" direction="vertical" size={4}>
<Typography.Text>{t("workflow.node.start.form.executionMethod.options.auto")}</Typography.Text>
<Typography.Text>{t("workflow.props.trigger.auto")}</Typography.Text>
<Typography.Text type="secondary">{record.crontab ?? ""}</Typography.Text>
</Space>
);