diff --git a/.gitignore b/.gitignore index 7c1ff027..eb939316 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,6 @@ vendor pb_data build main -/ui/dist/* -!/ui/dist/.gitkeep -./dist -./certimate +/dist /docker/data +/certimate diff --git a/README.md b/README.md index f03cd297..dbfcd1ee 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ > [!WARNING] > 当前分支为 `next`,是 v0.3.x 的开发分支,目前还没有稳定,请勿在生产环境中使用。 -> -> 如需访问 v0.2.x 源码,请切换至 `main` 分支。 +> +> 如需访问之前的版本,请切换至 `main` 分支。 # 🔒Certimate @@ -84,11 +84,11 @@ make local.run | 华为云 | √ | √ | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB | | 七牛云 | | √ | 可部署到七牛云 CDN | | 多吉云 | | √ | 可部署到多吉云 CDN | -| 火山引擎 | √ | √ | 可签发在火山引擎注册的域名;可部署到火山引擎 Live、CDN | +| 火山引擎 | √ | √ | 可签发在火山引擎注册的域名;可部署到火山引擎 Live、CDN | | AWS | √ | | 可签发在 AWS Route53 托管的域名 | | CloudFlare | √ | | 可签发在 CloudFlare 注册的域名;CloudFlare 服务自带 SSL 证书 | | GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 | -| Namesilo | √ | | 可签发在 Namesilo 注册的域名 | +| NameSilo | √ | | 可签发在 NameSilo 注册的域名 | | PowerDNS | √ | | 可签发在 PowerDNS 托管的域名 | | HTTP 请求 | √ | | 可签发允许通过 HTTP 请求修改 DNS 的域名 | | 本地部署 | | √ | 可部署到本地服务器 | @@ -194,4 +194,3 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE. ## 十、Star 趋势图 [![Stargazers over time](https://starchart.cc/usual2970/certimate.svg?variant=adaptive)](https://starchart.cc/usual2970/certimate) - diff --git a/README_EN.md b/README_EN.md index 15c61cf7..88e03baf 100644 --- a/README_EN.md +++ b/README_EN.md @@ -2,7 +2,7 @@ > [!WARNING] > The current branch is `next`, which is the development branch for v0.3.x. It is currently unstable and should not be used in production environments. -> +> > To access the previous versions, please switch to the `main` branch. # 🔒Certimate @@ -76,7 +76,7 @@ password:1234567890 ## 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 | | Baidu Cloud | | √ | Supports deployment to Baidu Cloud CDN | @@ -87,7 +87,7 @@ password:1234567890 | 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 | +| 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 | diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index f74d97e9..aec21749 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -26,17 +26,23 @@ import ( "github.com/pocketbase/pocketbase/models" ) +/* +提供商类型常量值。 + + 注意:如果追加新的常量值,请保持以 ASCII 排序。 + NOTICE: If you add new constant, please keep ASCII order. +*/ const ( - configTypeAliyun = "aliyun" - configTypeTencent = "tencent" - configTypeHuaweiCloud = "huaweicloud" - configTypeAws = "aws" - configTypeCloudflare = "cloudflare" - configTypeNamesilo = "namesilo" - configTypeGodaddy = "godaddy" - configTypePdns = "pdns" - configTypeHttpreq = "httpreq" - configTypeVolcengine = "volcengine" + configTypeACMEHttpReq = "acmehttpreq" + configTypeAliyun = "aliyun" + configTypeAWS = "aws" + configTypeCloudflare = "cloudflare" + configTypeGoDaddy = "godaddy" + configTypeHuaweiCloud = "huaweicloud" + configTypeNameSilo = "namesilo" + configTypePowerDNS = "powerdns" + configTypeTencentCloud = "tencentcloud" + configTypeVolcEngine = "volcengine" ) const defaultSSLProvider = "letsencrypt" @@ -205,23 +211,23 @@ func GetWithTypeOption(t string, option *ApplyOption) (Applicant, error) { switch t { case configTypeAliyun: return NewAliyun(option), nil - case configTypeTencent: + case configTypeTencentCloud: return NewTencent(option), nil case configTypeHuaweiCloud: return NewHuaweiCloud(option), nil - case configTypeAws: + case configTypeAWS: return NewAws(option), nil case configTypeCloudflare: return NewCloudflare(option), nil - case configTypeNamesilo: + case configTypeNameSilo: return NewNamesilo(option), nil - case configTypeGodaddy: + case configTypeGoDaddy: return NewGodaddy(option), nil - case configTypePdns: + case configTypePowerDNS: return NewPdns(option), nil - case configTypeHttpreq: + case configTypeACMEHttpReq: return NewHttpreq(option), nil - case configTypeVolcengine: + case configTypeVolcEngine: return NewVolcengine(option), nil default: return nil, errors.New("unknown config type") @@ -244,7 +250,7 @@ type SSLProviderEab struct { } func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, error) { - record, _ := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='ssl-provider'") + record, _ := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='sslProvider'") sslProvider := &SSLProviderConfig{ Config: SSLProviderConfigContent{}, diff --git a/internal/certificate/service.go b/internal/certificate/service.go index 48c07928..8fdf060b 100644 --- a/internal/certificate/service.go +++ b/internal/certificate/service.go @@ -61,7 +61,7 @@ func buildMsg(records []domain.Certificate) *domain.NotifyMessage { // 查询模板信息 settingRepo := repository.NewSettingRepository() - setting, err := settingRepo.GetByName(context.Background(), "templates") + setting, err := settingRepo.GetByName(context.Background(), "notifyTemplates") subject := defaultExpireSubject message := defaultExpireMessage diff --git a/internal/deployer/aliyun_alb.go b/internal/deployer/aliyun_alb.go deleted file mode 100644 index 2e05320e..00000000 --- a/internal/deployer/aliyun_alb.go +++ /dev/null @@ -1,281 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "strings" - - aliyunAlb "github.com/alibabacloud-go/alb-20200616/v2/client" - aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client" - "github.com/alibabacloud-go/tea/tea" - xerrors "github.com/pkg/errors" - - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/core/uploader" - uploaderAliyunCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas" -) - -type AliyunALBDeployer struct { - option *DeployerOption - infos []string - - sdkClient *aliyunAlb.Client - sslUploader uploader.Uploader -} - -func NewAliyunALBDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.AliyunAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - client, err := (&AliyunALBDeployer{}).createSdkClient( - access.AccessKeyId, - access.AccessKeySecret, - option.DeployConfig.GetConfigAsString("region"), - ) - if err != nil { - 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: aliCasRegion, - }) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create ssl uploader") - } - - return &AliyunALBDeployer{ - option: option, - infos: make([]string, 0), - sdkClient: client, - sslUploader: uploader, - }, nil -} - -func (d *AliyunALBDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *AliyunALBDeployer) GetInfos() []string { - return d.infos -} - -func (d *AliyunALBDeployer) Deploy(ctx context.Context) error { - switch d.option.DeployConfig.GetConfigAsString("resourceType") { - case "loadbalancer": - if err := d.deployToLoadbalancer(ctx); err != nil { - return err - } - case "listener": - if err := d.deployToListener(ctx); err != nil { - return err - } - default: - return errors.New("unsupported resource type") - } - - return nil -} - -func (d *AliyunALBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunAlb.Client, error) { - if region == "" { - region = "cn-hangzhou" // ALB 服务默认区域:华东一杭州 - } - - aConfig := &aliyunOpen.Config{ - AccessKeyId: tea.String(accessKeyId), - AccessKeySecret: tea.String(accessKeySecret), - } - - var endpoint string - switch region { - case "cn-hangzhou-finance": - endpoint = "alb.cn-hangzhou.aliyuncs.com" - default: - endpoint = fmt.Sprintf("alb.%s.aliyuncs.com", region) - } - aConfig.Endpoint = tea.String(endpoint) - - client, err := aliyunAlb.NewClient(aConfig) - if err != nil { - return nil, err - } - - return client, nil -} - -func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error { - aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") - if aliLoadbalancerId == "" { - return errors.New("`loadbalancerId` is required") - } - - aliListenerIds := make([]string, 0) - - // 查询负载均衡实例的详细信息 - // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getloadbalancerattribute - getLoadBalancerAttributeReq := &aliyunAlb.GetLoadBalancerAttributeRequest{ - LoadBalancerId: tea.String(aliLoadbalancerId), - } - getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetLoadBalancerAttribute'") - } - - d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例", getLoadBalancerAttributeResp)) - - // 查询 HTTPS 监听列表 - // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners - listListenersPage := 1 - listListenersLimit := int32(100) - var listListenersToken *string = nil - for { - listListenersReq := &aliyunAlb.ListListenersRequest{ - MaxResults: tea.Int32(listListenersLimit), - NextToken: listListenersToken, - LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)}, - ListenerProtocol: tea.String("HTTPS"), - } - listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'") - } - - if listListenersResp.Body.Listeners != nil { - for _, listener := range listListenersResp.Body.Listeners { - aliListenerIds = append(aliListenerIds, *listener.ListenerId) - } - } - - if listListenersResp.Body.NextToken == nil { - break - } else { - listListenersToken = listListenersResp.Body.NextToken - listListenersPage += 1 - } - } - - d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例下的全部 HTTPS 监听", aliListenerIds)) - - // 查询 QUIC 监听列表 - // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners - listListenersPage = 1 - listListenersToken = nil - for { - listListenersReq := &aliyunAlb.ListListenersRequest{ - MaxResults: tea.Int32(listListenersLimit), - NextToken: listListenersToken, - LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)}, - ListenerProtocol: tea.String("QUIC"), - } - listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'") - } - - if listListenersResp.Body.Listeners != nil { - for _, listener := range listListenersResp.Body.Listeners { - aliListenerIds = append(aliListenerIds, *listener.ListenerId) - } - } - - if listListenersResp.Body.NextToken == nil { - break - } else { - listListenersToken = listListenersResp.Body.NextToken - listListenersPage += 1 - } - } - - d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例下的全部 QUIC 监听", aliListenerIds)) - - // 上传证书到 SSL - 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)) - - // 批量更新监听证书 - var errs []error - for _, aliListenerId := range aliListenerIds { - if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil { - errs = append(errs, err) - } - } - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (d *AliyunALBDeployer) deployToListener(ctx context.Context) error { - aliListenerId := d.option.DeployConfig.GetConfigAsString("listenerId") - if aliListenerId == "" { - return errors.New("`listenerId` is required") - } - - // 上传证书到 SSL - 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)) - - // 更新监听 - if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil { - return err - } - - return nil -} - -func (d *AliyunALBDeployer) updateListenerCertificate(ctx context.Context, aliListenerId string, aliCertId string) error { - // 查询监听的属性 - // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getlistenerattribute - getListenerAttributeReq := &aliyunAlb.GetListenerAttributeRequest{ - ListenerId: tea.String(aliListenerId), - } - getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetListenerAttribute'") - } - - d.infos = append(d.infos, toStr("已查询到 ALB 监听配置", getListenerAttributeResp)) - - // 修改监听的属性 - // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-updatelistenerattribute - updateListenerAttributeReq := &aliyunAlb.UpdateListenerAttributeRequest{ - ListenerId: tea.String(aliListenerId), - Certificates: []*aliyunAlb.UpdateListenerAttributeRequestCertificates{{ - CertificateId: tea.String(aliCertId), - }}, - } - updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'alb.UpdateListenerAttribute'") - } - - d.infos = append(d.infos, toStr("已更新 ALB 监听配置", updateListenerAttributeResp)) - - return nil -} diff --git a/internal/deployer/aliyun_cdn.go b/internal/deployer/aliyun_cdn.go deleted file mode 100644 index a23ce8a5..00000000 --- a/internal/deployer/aliyun_cdn.go +++ /dev/null @@ -1,88 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "fmt" - "time" - - aliyunCdn "github.com/alibabacloud-go/cdn-20180510/v5/client" - aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client" - "github.com/alibabacloud-go/tea/tea" - xerrors "github.com/pkg/errors" - - "github.com/usual2970/certimate/internal/domain" -) - -type AliyunCDNDeployer struct { - option *DeployerOption - infos []string - - sdkClient *aliyunCdn.Client -} - -func NewAliyunCDNDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.AliyunAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - client, err := (&AliyunCDNDeployer{}).createSdkClient( - access.AccessKeyId, - access.AccessKeySecret, - ) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create sdk client") - } - - return &AliyunCDNDeployer{ - option: option, - infos: make([]string, 0), - sdkClient: client, - }, nil -} - -func (d *AliyunCDNDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *AliyunCDNDeployer) GetInfos() []string { - return d.infos -} - -func (d *AliyunCDNDeployer) Deploy(ctx context.Context) error { - // 设置 CDN 域名域名证书 - // REF: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-setcdndomainsslcertificate - setCdnDomainSSLCertificateReq := &aliyunCdn.SetCdnDomainSSLCertificateRequest{ - DomainName: tea.String(d.option.DeployConfig.GetConfigAsString("domain")), - CertRegion: tea.String(d.option.DeployConfig.GetConfigOrDefaultAsString("region", "cn-hangzhou")), - CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())), - CertType: tea.String("upload"), - SSLProtocol: tea.String("on"), - SSLPub: tea.String(d.option.Certificate.Certificate), - SSLPri: tea.String(d.option.Certificate.PrivateKey), - } - setCdnDomainSSLCertificateResp, err := d.sdkClient.SetCdnDomainSSLCertificate(setCdnDomainSSLCertificateReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'cdn.SetCdnDomainSSLCertificate'") - } - - d.infos = append(d.infos, toStr("已设置 CDN 域名证书", setCdnDomainSSLCertificateResp)) - - return nil -} - -func (d *AliyunCDNDeployer) createSdkClient(accessKeyId, accessKeySecret string) (*aliyunCdn.Client, error) { - aConfig := &aliyunOpen.Config{ - AccessKeyId: tea.String(accessKeyId), - AccessKeySecret: tea.String(accessKeySecret), - Endpoint: tea.String("cdn.aliyuncs.com"), - } - - client, err := aliyunCdn.NewClient(aConfig) - if err != nil { - return nil, err - } - - return client, nil -} diff --git a/internal/deployer/aliyun_clb.go b/internal/deployer/aliyun_clb.go deleted file mode 100644 index 95d32b5d..00000000 --- a/internal/deployer/aliyun_clb.go +++ /dev/null @@ -1,286 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "errors" - "fmt" - - aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client" - aliyunSlb "github.com/alibabacloud-go/slb-20140515/v4/client" - "github.com/alibabacloud-go/tea/tea" - xerrors "github.com/pkg/errors" - - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/core/uploader" - uploaderAliyunSlb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-slb" -) - -type AliyunCLBDeployer struct { - option *DeployerOption - infos []string - - sdkClient *aliyunSlb.Client - sslUploader uploader.Uploader -} - -func NewAliyunCLBDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.AliyunAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - client, err := (&AliyunCLBDeployer{}).createSdkClient( - access.AccessKeyId, - access.AccessKeySecret, - option.DeployConfig.GetConfigAsString("region"), - ) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create sdk client") - } - - uploader, err := uploaderAliyunSlb.New(&uploaderAliyunSlb.AliyunSLBUploaderConfig{ - AccessKeyId: access.AccessKeyId, - AccessKeySecret: access.AccessKeySecret, - Region: option.DeployConfig.GetConfigAsString("region"), - }) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create ssl uploader") - } - - return &AliyunCLBDeployer{ - option: option, - infos: make([]string, 0), - sdkClient: client, - sslUploader: uploader, - }, nil -} - -func (d *AliyunCLBDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *AliyunCLBDeployer) GetInfos() []string { - return d.infos -} - -func (d *AliyunCLBDeployer) Deploy(ctx context.Context) error { - switch d.option.DeployConfig.GetConfigAsString("resourceType") { - case "loadbalancer": - if err := d.deployToLoadbalancer(ctx); err != nil { - return err - } - case "listener": - if err := d.deployToListener(ctx); err != nil { - return err - } - default: - return errors.New("unsupported resource type") - } - - return nil -} - -func (d *AliyunCLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Client, error) { - if region == "" { - region = "cn-hangzhou" // CLB(SLB) 服务默认区域:华东一杭州 - } - - aConfig := &aliyunOpen.Config{ - AccessKeyId: tea.String(accessKeyId), - AccessKeySecret: tea.String(accessKeySecret), - } - - var endpoint string - switch region { - case - "cn-hangzhou", - "cn-hangzhou-finance", - "cn-shanghai-finance-1", - "cn-shenzhen-finance-1": - endpoint = "slb.aliyuncs.com" - default: - endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region) - } - aConfig.Endpoint = tea.String(endpoint) - - client, err := aliyunSlb.NewClient(aConfig) - if err != nil { - return nil, err - } - - return client, nil -} - -func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context) error { - aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") - aliListenerPorts := make([]int32, 0) - if aliLoadbalancerId == "" { - return errors.New("`loadbalancerId` is required") - } - - // 查询负载均衡实例的详细信息 - // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute - describeLoadBalancerAttributeReq := &aliyunSlb.DescribeLoadBalancerAttributeRequest{ - RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), - LoadBalancerId: tea.String(aliLoadbalancerId), - } - describeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttribute(describeLoadBalancerAttributeReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerAttribute'") - } - - d.infos = append(d.infos, toStr("已查询到 CLB 负载均衡实例", describeLoadBalancerAttributeResp)) - - // 查询 HTTPS 监听列表 - // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerlisteners - listListenersPage := 1 - listListenersLimit := int32(100) - var listListenersToken *string = nil - for { - describeLoadBalancerListenersReq := &aliyunSlb.DescribeLoadBalancerListenersRequest{ - RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), - MaxResults: tea.Int32(listListenersLimit), - NextToken: listListenersToken, - LoadBalancerId: []*string{tea.String(aliLoadbalancerId)}, - ListenerProtocol: tea.String("https"), - } - describeLoadBalancerListenersResp, err := d.sdkClient.DescribeLoadBalancerListeners(describeLoadBalancerListenersReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerListeners'") - } - - if describeLoadBalancerListenersResp.Body.Listeners != nil { - for _, listener := range describeLoadBalancerListenersResp.Body.Listeners { - aliListenerPorts = append(aliListenerPorts, *listener.ListenerPort) - } - } - - if describeLoadBalancerListenersResp.Body.NextToken == nil { - break - } else { - listListenersToken = describeLoadBalancerListenersResp.Body.NextToken - listListenersPage += 1 - } - } - - d.infos = append(d.infos, toStr("已查询到 CLB 负载均衡实例下的全部 HTTPS 监听", aliListenerPorts)) - - // 上传证书到 SLB - 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)) - - // 批量更新监听证书 - var errs []error - for _, aliListenerPort := range aliListenerPorts { - if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, upres.CertId); err != nil { - errs = append(errs, err) - } - } - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (d *AliyunCLBDeployer) deployToListener(ctx context.Context) error { - aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") - if aliLoadbalancerId == "" { - return errors.New("`loadbalancerId` is required") - } - - aliListenerPort := d.option.DeployConfig.GetConfigAsInt32("listenerPort") - if aliListenerPort == 0 { - return errors.New("`listenerPort` is required") - } - - // 上传证书到 SLB - 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)) - - // 更新监听 - if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, upres.CertId); err != nil { - return err - } - - return nil -} - -func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, aliLoadbalancerId string, aliListenerPort int32, aliCertId string) error { - // 查询监听配置 - // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerhttpslistenerattribute - describeLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.DescribeLoadBalancerHTTPSListenerAttributeRequest{ - LoadBalancerId: tea.String(aliLoadbalancerId), - ListenerPort: tea.Int32(aliListenerPort), - } - describeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttribute(describeLoadBalancerHTTPSListenerAttributeReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute'") - } - - d.infos = append(d.infos, toStr("已查询到 CLB HTTPS 监听配置", describeLoadBalancerHTTPSListenerAttributeResp)) - - // 查询扩展域名 - // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describedomainextensions - describeDomainExtensionsReq := &aliyunSlb.DescribeDomainExtensionsRequest{ - RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), - LoadBalancerId: tea.String(aliLoadbalancerId), - ListenerPort: tea.Int32(aliListenerPort), - } - describeDomainExtensionsResp, err := d.sdkClient.DescribeDomainExtensions(describeDomainExtensionsReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeDomainExtensions'") - } - - d.infos = append(d.infos, toStr("已查询到 CLB 扩展域名", describeDomainExtensionsResp)) - - // 遍历修改扩展域名 - // 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 { - for _, domainExtension := range describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension { - if *domainExtension.ServerCertificateId != *describeLoadBalancerHTTPSListenerAttributeResp.Body.ServerCertificateId { - continue - } - - setDomainExtensionAttributeReq := &aliyunSlb.SetDomainExtensionAttributeRequest{ - RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), - DomainExtensionId: tea.String(*domainExtension.DomainExtensionId), - ServerCertificateId: tea.String(aliCertId), - } - _, err := d.sdkClient.SetDomainExtensionAttribute(setDomainExtensionAttributeReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetDomainExtensionAttribute'") - } - } - } - - // 修改监听配置 - // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute - // - // 注意修改监听配置要放在修改扩展域名之后 - setLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.SetLoadBalancerHTTPSListenerAttributeRequest{ - RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), - LoadBalancerId: tea.String(aliLoadbalancerId), - ListenerPort: tea.Int32(aliListenerPort), - ServerCertificateId: tea.String(aliCertId), - } - setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.SetLoadBalancerHTTPSListenerAttribute(setLoadBalancerHTTPSListenerAttributeReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute'") - } - - d.infos = append(d.infos, toStr("已更新 CLB HTTPS 监听配置", setLoadBalancerHTTPSListenerAttributeResp)) - - return nil -} diff --git a/internal/deployer/aliyun_dcdn.go b/internal/deployer/aliyun_dcdn.go deleted file mode 100644 index 0f969787..00000000 --- a/internal/deployer/aliyun_dcdn.go +++ /dev/null @@ -1,95 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "time" - - aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client" - aliyunDcdn "github.com/alibabacloud-go/dcdn-20180115/v3/client" - "github.com/alibabacloud-go/tea/tea" - xerrors "github.com/pkg/errors" - - "github.com/usual2970/certimate/internal/domain" -) - -type AliyunDCDNDeployer struct { - option *DeployerOption - infos []string - - sdkClient *aliyunDcdn.Client -} - -func NewAliyunDCDNDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.AliyunAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - client, err := (&AliyunDCDNDeployer{}).createSdkClient( - access.AccessKeyId, - access.AccessKeySecret, - ) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create sdk client") - } - - return &AliyunDCDNDeployer{ - option: option, - infos: make([]string, 0), - sdkClient: client, - }, nil -} - -func (d *AliyunDCDNDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *AliyunDCDNDeployer) GetInfos() []string { - return d.infos -} - -func (d *AliyunDCDNDeployer) Deploy(ctx context.Context) error { - // 支持泛解析域名,在 Aliyun DCDN 中泛解析域名表示为 .example.com - domain := d.option.DeployConfig.GetConfigAsString("domain") - if strings.HasPrefix(domain, "*") { - domain = strings.TrimPrefix(domain, "*") - } - - // 配置域名证书 - // REF: https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-setdcdndomainsslcertificate - setDcdnDomainSSLCertificateReq := &aliyunDcdn.SetDcdnDomainSSLCertificateRequest{ - DomainName: tea.String(domain), - CertRegion: tea.String(d.option.DeployConfig.GetConfigOrDefaultAsString("region", "cn-hangzhou")), - CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())), - CertType: tea.String("upload"), - SSLProtocol: tea.String("on"), - SSLPub: tea.String(d.option.Certificate.Certificate), - SSLPri: tea.String(d.option.Certificate.PrivateKey), - } - setDcdnDomainSSLCertificateResp, err := d.sdkClient.SetDcdnDomainSSLCertificate(setDcdnDomainSSLCertificateReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'dcdn.SetDcdnDomainSSLCertificate'") - } - - d.infos = append(d.infos, toStr("已配置 DCDN 域名证书", setDcdnDomainSSLCertificateResp)) - - return nil -} - -func (d *AliyunDCDNDeployer) createSdkClient(accessKeyId, accessKeySecret string) (*aliyunDcdn.Client, error) { - aConfig := &aliyunOpen.Config{ - AccessKeyId: tea.String(accessKeyId), - AccessKeySecret: tea.String(accessKeySecret), - Endpoint: tea.String("dcdn.aliyuncs.com"), - } - - client, err := aliyunDcdn.NewClient(aConfig) - if err != nil { - return nil, err - } - - return client, nil -} diff --git a/internal/deployer/aliyun_nlb.go b/internal/deployer/aliyun_nlb.go deleted file mode 100644 index bbaddd7b..00000000 --- a/internal/deployer/aliyun_nlb.go +++ /dev/null @@ -1,245 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "strings" - - aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client" - aliyunNlb "github.com/alibabacloud-go/nlb-20220430/v2/client" - "github.com/alibabacloud-go/tea/tea" - xerrors "github.com/pkg/errors" - - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/core/uploader" - uploaderAliyunCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas" -) - -type AliyunNLBDeployer struct { - option *DeployerOption - infos []string - - sdkClient *aliyunNlb.Client - sslUploader uploader.Uploader -} - -func NewAliyunNLBDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.AliyunAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - client, err := (&AliyunNLBDeployer{}).createSdkClient( - access.AccessKeyId, - access.AccessKeySecret, - option.DeployConfig.GetConfigAsString("region"), - ) - if err != nil { - 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: aliCasRegion, - }) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create ssl uploader") - } - - return &AliyunNLBDeployer{ - option: option, - infos: make([]string, 0), - sdkClient: client, - sslUploader: uploader, - }, nil -} - -func (d *AliyunNLBDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *AliyunNLBDeployer) GetInfos() []string { - return d.infos -} - -func (d *AliyunNLBDeployer) Deploy(ctx context.Context) error { - switch d.option.DeployConfig.GetConfigAsString("resourceType") { - case "loadbalancer": - if err := d.deployToLoadbalancer(ctx); err != nil { - return err - } - case "listener": - if err := d.deployToListener(ctx); err != nil { - return err - } - default: - return errors.New("unsupported resource type") - } - - return nil -} - -func (d *AliyunNLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunNlb.Client, error) { - if region == "" { - region = "cn-hangzhou" // NLB 服务默认区域:华东一杭州 - } - - aConfig := &aliyunOpen.Config{ - AccessKeyId: tea.String(accessKeyId), - AccessKeySecret: tea.String(accessKeySecret), - } - - var endpoint string - switch region { - default: - endpoint = fmt.Sprintf("nlb.%s.aliyuncs.com", region) - } - aConfig.Endpoint = tea.String(endpoint) - - client, err := aliyunNlb.NewClient(aConfig) - if err != nil { - return nil, err - } - - return client, nil -} - -func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context) error { - aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") - if aliLoadbalancerId == "" { - return errors.New("`loadbalancerId` is required") - } - - aliListenerIds := make([]string, 0) - - // 查询负载均衡实例的详细信息 - // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getloadbalancerattribute - getLoadBalancerAttributeReq := &aliyunNlb.GetLoadBalancerAttributeRequest{ - LoadBalancerId: tea.String(aliLoadbalancerId), - } - getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetLoadBalancerAttribute'") - } - - d.infos = append(d.infos, toStr("已查询到 NLB 负载均衡实例", getLoadBalancerAttributeResp)) - - // 查询 TCPSSL 监听列表 - // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-listlisteners - listListenersPage := 1 - listListenersLimit := int32(100) - var listListenersToken *string = nil - for { - listListenersReq := &aliyunNlb.ListListenersRequest{ - MaxResults: tea.Int32(listListenersLimit), - NextToken: listListenersToken, - LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)}, - ListenerProtocol: tea.String("TCPSSL"), - } - listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'nlb.ListListeners'") - } - - if listListenersResp.Body.Listeners != nil { - for _, listener := range listListenersResp.Body.Listeners { - aliListenerIds = append(aliListenerIds, *listener.ListenerId) - } - } - - if listListenersResp.Body.NextToken == nil { - break - } else { - listListenersToken = listListenersResp.Body.NextToken - listListenersPage += 1 - } - } - - d.infos = append(d.infos, toStr("已查询到 NLB 负载均衡实例下的全部 TCPSSL 监听", aliListenerIds)) - - // 上传证书到 SSL - 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)) - - // 批量更新监听证书 - var errs []error - for _, aliListenerId := range aliListenerIds { - if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil { - errs = append(errs, err) - } - } - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (d *AliyunNLBDeployer) deployToListener(ctx context.Context) error { - aliListenerId := d.option.DeployConfig.GetConfigAsString("listenerId") - if aliListenerId == "" { - return errors.New("`listenerId` is required") - } - - // 上传证书到 SSL - 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)) - - // 更新监听 - if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil { - return err - } - - return nil -} - -func (d *AliyunNLBDeployer) updateListenerCertificate(ctx context.Context, aliListenerId string, aliCertId string) error { - // 查询监听的属性 - // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getlistenerattribute - getListenerAttributeReq := &aliyunNlb.GetListenerAttributeRequest{ - ListenerId: tea.String(aliListenerId), - } - getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetListenerAttribute'") - } - - d.infos = append(d.infos, toStr("已查询到 NLB 监听配置", getListenerAttributeResp)) - - // 修改监听的属性 - // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-updatelistenerattribute - updateListenerAttributeReq := &aliyunNlb.UpdateListenerAttributeRequest{ - ListenerId: tea.String(aliListenerId), - CertificateIds: []*string{tea.String(aliCertId)}, - } - updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'nlb.UpdateListenerAttribute'") - } - - d.infos = append(d.infos, toStr("已更新 NLB 监听配置", updateListenerAttributeResp)) - - return nil -} diff --git a/internal/deployer/aliyun_oss.go b/internal/deployer/aliyun_oss.go deleted file mode 100644 index d0045d89..00000000 --- a/internal/deployer/aliyun_oss.go +++ /dev/null @@ -1,86 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "errors" - "fmt" - - "github.com/aliyun/aliyun-oss-go-sdk/oss" - xerrors "github.com/pkg/errors" - - "github.com/usual2970/certimate/internal/domain" -) - -type AliyunOSSDeployer struct { - option *DeployerOption - infos []string - - sdkClient *oss.Client -} - -func NewAliyunOSSDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.AliyunAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - client, err := (&AliyunOSSDeployer{}).createSdkClient( - access.AccessKeyId, - access.AccessKeySecret, - option.DeployConfig.GetConfigAsString("endpoint"), - ) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create sdk client") - } - - return &AliyunOSSDeployer{ - option: option, - infos: make([]string, 0), - sdkClient: client, - }, nil -} - -func (d *AliyunOSSDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *AliyunOSSDeployer) GetInfos() []string { - return d.infos -} - -func (d *AliyunOSSDeployer) Deploy(ctx context.Context) error { - aliBucket := d.option.DeployConfig.GetConfigAsString("bucket") - if aliBucket == "" { - return errors.New("`bucket` is required") - } - - // 为存储空间绑定自定义域名 - // REF: https://help.aliyun.com/zh/oss/developer-reference/putcname - err := d.sdkClient.PutBucketCnameWithCertificate(aliBucket, oss.PutBucketCname{ - Cname: d.option.DeployConfig.GetConfigAsString("domain"), - CertificateConfiguration: &oss.CertificateConfiguration{ - Certificate: d.option.Certificate.Certificate, - PrivateKey: d.option.Certificate.PrivateKey, - Force: true, - }, - }) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'oss.PutBucketCnameWithCertificate'") - } - - return nil -} - -func (d *AliyunOSSDeployer) createSdkClient(accessKeyId, accessKeySecret, endpoint string) (*oss.Client, error) { - if endpoint == "" { - endpoint = "oss.aliyuncs.com" - } - - client, err := oss.New(endpoint, accessKeyId, accessKeySecret) - if err != nil { - return nil, err - } - - return client, nil -} diff --git a/internal/deployer/baiducloud_cdn.go b/internal/deployer/baiducloud_cdn.go deleted file mode 100644 index 31d789df..00000000 --- a/internal/deployer/baiducloud_cdn.go +++ /dev/null @@ -1,80 +0,0 @@ -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 -} diff --git a/internal/deployer/byteplus_cdn.go b/internal/deployer/byteplus_cdn.go deleted file mode 100644 index 17c51522..00000000 --- a/internal/deployer/byteplus_cdn.go +++ /dev/null @@ -1,116 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - bytepluscdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/byteplus-cdn" - - "github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn" - xerrors "github.com/pkg/errors" - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/core/uploader" -) - -type ByteplusCDNDeployer struct { - option *DeployerOption - infos []string - sdkClient *cdn.CDN - sslUploader uploader.Uploader -} - -func NewByteplusCDNDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.ByteplusAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - client := cdn.NewInstance() - client.Client.SetAccessKey(access.AccessKey) - client.Client.SetSecretKey(access.SecretKey) - uploader, err := bytepluscdn.New(&bytepluscdn.ByteplusCDNUploaderConfig{ - AccessKey: access.AccessKey, - SecretKey: access.SecretKey, - }) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create ssl uploader") - } - return &ByteplusCDNDeployer{ - option: option, - infos: make([]string, 0), - sdkClient: client, - sslUploader: uploader, - }, nil -} - -func (d *ByteplusCDNDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *ByteplusCDNDeployer) GetInfos() []string { - return d.infos -} - -func (d *ByteplusCDNDeployer) Deploy(ctx context.Context) error { - apiCtx := context.Background() - // 上传证书 - upres, err := d.sslUploader.Upload(apiCtx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) - if err != nil { - return err - } - - d.infos = append(d.infos, toStr("已上传证书", upres)) - - domains := make([]string, 0) - configDomain := d.option.DeployConfig.GetConfigAsString("domain") - if strings.HasPrefix(configDomain, "*.") { - // 获取证书可以部署的域名 - // REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-describecertconfig-9ea17 - describeCertConfigReq := &cdn.DescribeCertConfigRequest{ - CertId: upres.CertId, - } - describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'") - } - for i := range describeCertConfigResp.Result.CertNotConfig { - // 当前未启用 HTTPS 的加速域名列表。 - domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain) - } - for i := range describeCertConfigResp.Result.OtherCertConfig { - // 已启用了 HTTPS 的加速域名列表。这些加速域名关联的证书不是您指定的证书。 - domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain) - } - for i := range describeCertConfigResp.Result.SpecifiedCertConfig { - // 已启用了 HTTPS 的加速域名列表。这些加速域名关联了您指定的证书。 - d.infos = append(d.infos, fmt.Sprintf("%s域名已配置该证书", describeCertConfigResp.Result.SpecifiedCertConfig[i].Domain)) - } - if len(domains) == 0 { - if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 { - // 所有匹配的域名都配置了该证书,跳过部署 - return nil - } else { - return xerrors.Errorf("未查询到匹配的域名: %s", configDomain) - } - } - } else { - domains = append(domains, configDomain) - } - // 部署证书 - // REF: https://github.com/byteplus-sdk/byteplus-sdk-golang/blob/master/service/cdn/api_list.go#L306 - for i := range domains { - batchDeployCertReq := &cdn.BatchDeployCertRequest{ - CertId: upres.CertId, - Domain: domains[i], - } - batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'cdn.BatchDeployCert'") - } else { - d.infos = append(d.infos, toStr(fmt.Sprintf("%s域名的证书已修改", domains[i]), batchDeployCertResp)) - } - } - - return nil -} diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 11fd6a9e..44fb4af6 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -2,41 +2,46 @@ package deployer import ( "context" - "encoding/json" - "errors" "fmt" "github.com/pocketbase/pocketbase/models" "github.com/usual2970/certimate/internal/applicant" "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/repository" ) +/* +提供商部署目标常量值。 + + 注意:如果追加新的常量值,请保持以 ASCII 排序。 + NOTICE: If you add new constant, please keep ASCII order. +*/ const ( - targetAliyunOSS = "aliyun-oss" - targetAliyunCDN = "aliyun-cdn" - targetAliyunDCDN = "aliyun-dcdn" - targetAliyunCLB = "aliyun-clb" - targetAliyunALB = "aliyun-alb" - targetAliyunNLB = "aliyun-nlb" - targetTencentCDN = "tencent-cdn" - targetTencentECDN = "tencent-ecdn" - targetTencentCLB = "tencent-clb" - targetTencentCOS = "tencent-cos" - targetTencentTEO = "tencent-teo" - targetHuaweiCloudCDN = "huaweicloud-cdn" - targetHuaweiCloudELB = "huaweicloud-elb" - targetBaiduCloudCDN = "baiducloud-cdn" - targetVolcEngineLive = "volcengine-live" - targetVolcEngineCDN = "volcengine-cdn" - targetBytePlusCDN = "byteplus-cdn" - targetQiniuCdn = "qiniu-cdn" - targetDogeCloudCdn = "dogecloud-cdn" - targetLocal = "local" - targetSSH = "ssh" - targetWebhook = "webhook" - targetK8sSecret = "k8s-secret" + targetAliyunALB = "aliyun-alb" + targetAliyunCDN = "aliyun-cdn" + targetAliyunCLB = "aliyun-clb" + targetAliyunDCDN = "aliyun-dcdn" + targetAliyunNLB = "aliyun-nlb" + targetAliyunOSS = "aliyun-oss" + targetBaiduCloudCDN = "baiducloud-cdn" + targetBytePlusCDN = "byteplus-cdn" + targetDogeCloudCDN = "dogecloud-cdn" + targetHuaweiCloudCDN = "huaweicloud-cdn" + targetHuaweiCloudELB = "huaweicloud-elb" + targetK8sSecret = "k8s-secret" + targetLocal = "local" + targetQiniuCDN = "qiniu-cdn" + targetSSH = "ssh" + targetTencentCloudCDN = "tencentcloud-cdn" + targetTencentCloudCLB = "tencentcloud-clb" + targetTencentCloudCOS = "tencentcloud-cos" + targetTencentCloudECDN = "tencentcloud-ecdn" + targetTencentCloudEO = "tencentcloud-eo" + targetVolcEngineCDN = "volcengine-cdn" + targetVolcEngineLive = "volcengine-live" + targetWebhook = "webhook" ) type DeployerOption struct { @@ -73,7 +78,7 @@ func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error } for _, deployConfig := range deployConfigs { - deployer, err := getWithDeployConfig(record, cert, deployConfig) + deployer, err := newWithDeployConfig(record, cert, deployConfig) if err != nil { return nil, err } @@ -85,10 +90,10 @@ func Gets(record *models.Record, cert *applicant.Certificate) ([]Deployer, error } func GetWithTypeAndOption(deployType string, option *DeployerOption) (Deployer, error) { - return getWithTypeAndOption(deployType, option) + return newWithTypeAndOption(deployType, option) } -func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, deployConfig domain.DeployConfig) (Deployer, error) { +func newWithDeployConfig(record *models.Record, cert *applicant.Certificate, deployConfig domain.DeployConfig) (Deployer, error) { accessRepo := repository.NewAccessRepository() access, err := accessRepo.GetById(context.Background(), deployConfig.Access) if err != nil { @@ -111,65 +116,38 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep } } - return getWithTypeAndOption(deployConfig.Type, option) + return newWithTypeAndOption(deployConfig.Type, option) } -func getWithTypeAndOption(deployType string, option *DeployerOption) (Deployer, error) { - switch deployType { - case targetAliyunOSS: - return NewAliyunOSSDeployer(option) - case targetAliyunCDN: - return NewAliyunCDNDeployer(option) - case targetAliyunDCDN: - return NewAliyunDCDNDeployer(option) - case targetAliyunCLB: - return NewAliyunCLBDeployer(option) - case targetAliyunALB: - return NewAliyunALBDeployer(option) - case targetAliyunNLB: - return NewAliyunNLBDeployer(option) - case targetTencentCDN: - return NewTencentCDNDeployer(option) - case targetTencentECDN: - return NewTencentECDNDeployer(option) - case targetTencentCLB: - return NewTencentCLBDeployer(option) - case targetTencentCOS: - return NewTencentCOSDeployer(option) - case targetTencentTEO: - return NewTencentTEODeployer(option) - case targetHuaweiCloudCDN: - 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: - return NewSSHDeployer(option) - case targetWebhook: - return NewWebhookDeployer(option) - case targetK8sSecret: - return NewK8sSecretDeployer(option) - case targetVolcEngineLive: - return NewVolcengineLiveDeployer(option) - case targetVolcEngineCDN: - return NewVolcengineCDNDeployer(option) - case targetBytePlusCDN: - return NewByteplusCDNDeployer(option) +func newWithTypeAndOption(deployType string, option *DeployerOption) (Deployer, error) { + deployer, logger, err := createDeployer(deployType, option.AccessRecord.Config, option.DeployConfig.Config) + if err != nil { + return nil, err } - return nil, errors.New("unsupported deploy target") + + return &proxyDeployer{ + option: option, + logger: logger, + deployer: deployer, + }, nil } -func toStr(tag string, data any) string { - if data == nil { - return tag - } - byts, _ := json.Marshal(data) - return tag + ":" + string(byts) +// TODO: 暂时使用代理模式以兼容之前版本代码,后续重新实现此处逻辑 +type proxyDeployer struct { + option *DeployerOption + logger deployer.Logger + deployer deployer.Deployer +} + +func (d *proxyDeployer) GetID() string { + return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) +} + +func (d *proxyDeployer) GetInfos() []string { + return d.logger.GetRecords() +} + +func (d *proxyDeployer) Deploy(ctx context.Context) error { + _, err := d.deployer.Deploy(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + return err } diff --git a/internal/deployer/dogecloud_cdn.go b/internal/deployer/dogecloud_cdn.go deleted file mode 100644 index 55e526c1..00000000 --- a/internal/deployer/dogecloud_cdn.go +++ /dev/null @@ -1,88 +0,0 @@ -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 -} diff --git a/internal/deployer/factory.go b/internal/deployer/factory.go index fee6f628..d02800e1 100644 --- a/internal/deployer/factory.go +++ b/internal/deployer/factory.go @@ -7,36 +7,39 @@ import ( "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/pkg/core/deployer" - providerAliyunAlb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-alb" - providerAliyunCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-cdn" - providerAliyunClb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-clb" - providerAliyunDcdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-dcdn" - providerAliyunNlb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-nlb" - providerAliyunOss "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-oss" - providerBaiduCloudCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cdn" - providerBytePlusCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn" - providerDogeCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn" - providerHuaweiCloudCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn" - providerHuaweiCloudElb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-elb" + providerAliyunALB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-alb" + providerAliyunCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-cdn" + providerAliyunCLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-clb" + providerAliyunDCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-dcdn" + providerAliyunNLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-nlb" + providerAliyunOSS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-oss" + providerBaiduCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cdn" + providerBytePlusCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn" + providerDogeCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn" + providerHuaweiCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn" + providerHuaweiCloudELB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-elb" providerK8sSecret "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/k8s-secret" providerLocal "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local" - providerQiniuCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn" + providerQiniuCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn" providerSSH "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh" - providerTencentCloudCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn" - providerTencentCloudClb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-clb" - providerTencentCloudCos "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cos" - providerTencentCloudEcdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ecdn" - providerTencentCloudTeo "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-teo" - providerVolcEngineCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-cdn" + providerTencentCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn" + providerTencentCloudCLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-clb" + providerTencentCloudCOD "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cos" + providerTencentCloudECDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ecdn" + providerTencentCloudEO "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-eo" + providerVolcEngineCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-cdn" providerVolcEngineLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live" providerWebhook "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook" "github.com/usual2970/certimate/internal/pkg/utils/maps" ) -// TODO: 该方法目前未实际使用,将在后续迭代中替换 func createDeployer(target string, accessConfig string, deployConfig map[string]any) (deployer.Deployer, deployer.Logger, error) { logger := deployer.NewDefaultLogger() + /* + 注意:如果追加新的常量值,请保持以 ASCII 排序。 + NOTICE: If you add new constant, please keep ASCII order. + */ switch target { case targetAliyunALB, targetAliyunCDN, targetAliyunCLB, targetAliyunDCDN, targetAliyunNLB, targetAliyunOSS: { @@ -47,18 +50,18 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] switch target { case targetAliyunALB: - deployer, err := providerAliyunAlb.NewWithLogger(&providerAliyunAlb.AliyunALBDeployerConfig{ + deployer, err := providerAliyunALB.NewWithLogger(&providerAliyunALB.AliyunALBDeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, Region: maps.GetValueAsString(deployConfig, "region"), - ResourceType: providerAliyunAlb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")), + ResourceType: providerAliyunALB.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")), LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"), ListenerId: maps.GetValueAsString(deployConfig, "listenerId"), }, logger) return deployer, logger, err case targetAliyunCDN: - deployer, err := providerAliyunCdn.NewWithLogger(&providerAliyunCdn.AliyunCDNDeployerConfig{ + deployer, err := providerAliyunCDN.NewWithLogger(&providerAliyunCDN.AliyunCDNDeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, Domain: maps.GetValueAsString(deployConfig, "domain"), @@ -66,18 +69,18 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] return deployer, logger, err case targetAliyunCLB: - deployer, err := providerAliyunClb.NewWithLogger(&providerAliyunClb.AliyunCLBDeployerConfig{ + deployer, err := providerAliyunCLB.NewWithLogger(&providerAliyunCLB.AliyunCLBDeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, Region: maps.GetValueAsString(deployConfig, "region"), - ResourceType: providerAliyunClb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")), + ResourceType: providerAliyunCLB.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")), LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"), ListenerPort: maps.GetValueAsInt32(deployConfig, "listenerPort"), }, logger) return deployer, logger, err case targetAliyunDCDN: - deployer, err := providerAliyunDcdn.NewWithLogger(&providerAliyunDcdn.AliyunDCDNDeployerConfig{ + deployer, err := providerAliyunDCDN.NewWithLogger(&providerAliyunDCDN.AliyunDCDNDeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, Domain: maps.GetValueAsString(deployConfig, "domain"), @@ -85,18 +88,18 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] return deployer, logger, err case targetAliyunNLB: - deployer, err := providerAliyunNlb.NewWithLogger(&providerAliyunNlb.AliyunNLBDeployerConfig{ + deployer, err := providerAliyunNLB.NewWithLogger(&providerAliyunNLB.AliyunNLBDeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, Region: maps.GetValueAsString(deployConfig, "region"), - ResourceType: providerAliyunNlb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")), + ResourceType: providerAliyunNLB.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")), LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"), ListenerId: maps.GetValueAsString(deployConfig, "listenerId"), }, logger) return deployer, logger, err case targetAliyunOSS: - deployer, err := providerAliyunOss.NewWithLogger(&providerAliyunOss.AliyunOSSDeployerConfig{ + deployer, err := providerAliyunOSS.NewWithLogger(&providerAliyunOSS.AliyunOSSDeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, Region: maps.GetValueAsString(deployConfig, "region"), @@ -117,7 +120,7 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err) } - deployer, err := providerBaiduCloudCdn.NewWithLogger(&providerBaiduCloudCdn.BaiduCloudCDNDeployerConfig{ + deployer, err := providerBaiduCloudCDN.NewWithLogger(&providerBaiduCloudCDN.BaiduCloudCDNDeployerConfig{ AccessKeyId: access.AccessKeyId, SecretAccessKey: access.SecretAccessKey, Domain: maps.GetValueAsString(deployConfig, "domain"), @@ -132,7 +135,7 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err) } - deployer, err := providerBytePlusCdn.NewWithLogger(&providerBytePlusCdn.BytePlusCDNDeployerConfig{ + deployer, err := providerBytePlusCDN.NewWithLogger(&providerBytePlusCDN.BytePlusCDNDeployerConfig{ AccessKey: access.AccessKey, SecretKey: access.SecretKey, Domain: maps.GetValueAsString(deployConfig, "domain"), @@ -140,14 +143,14 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] return deployer, logger, err } - case targetDogeCloudCdn: + case targetDogeCloudCDN: { access := &domain.DogeCloudAccess{} if err := json.Unmarshal([]byte(accessConfig), access); err != nil { return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err) } - deployer, err := providerDogeCdn.NewWithLogger(&providerDogeCdn.DogeCloudCDNDeployerConfig{ + deployer, err := providerDogeCDN.NewWithLogger(&providerDogeCDN.DogeCloudCDNDeployerConfig{ AccessKey: access.AccessKey, SecretKey: access.SecretKey, Domain: maps.GetValueAsString(deployConfig, "domain"), @@ -164,7 +167,7 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] switch target { case targetHuaweiCloudCDN: - deployer, err := providerHuaweiCloudCdn.NewWithLogger(&providerHuaweiCloudCdn.HuaweiCloudCDNDeployerConfig{ + deployer, err := providerHuaweiCloudCDN.NewWithLogger(&providerHuaweiCloudCDN.HuaweiCloudCDNDeployerConfig{ AccessKeyId: access.AccessKeyId, SecretAccessKey: access.SecretAccessKey, Region: maps.GetValueAsString(deployConfig, "region"), @@ -173,11 +176,11 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] return deployer, logger, err case targetHuaweiCloudELB: - deployer, err := providerHuaweiCloudElb.NewWithLogger(&providerHuaweiCloudElb.HuaweiCloudELBDeployerConfig{ + deployer, err := providerHuaweiCloudELB.NewWithLogger(&providerHuaweiCloudELB.HuaweiCloudELBDeployerConfig{ AccessKeyId: access.AccessKeyId, SecretAccessKey: access.SecretAccessKey, Region: maps.GetValueAsString(deployConfig, "region"), - ResourceType: providerHuaweiCloudElb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")), + ResourceType: providerHuaweiCloudELB.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")), CertificateId: maps.GetValueAsString(deployConfig, "certificateId"), LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"), ListenerId: maps.GetValueAsString(deployConfig, "listenerId"), @@ -223,14 +226,14 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] return deployer, logger, err } - case targetQiniuCdn: + case targetQiniuCDN: { access := &domain.QiniuAccess{} if err := json.Unmarshal([]byte(accessConfig), access); err != nil { return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err) } - deployer, err := providerQiniuCdn.NewWithLogger(&providerQiniuCdn.QiniuCDNDeployerConfig{ + deployer, err := providerQiniuCDN.NewWithLogger(&providerQiniuCDN.QiniuCDNDeployerConfig{ AccessKey: access.AccessKey, SecretKey: access.SecretKey, Domain: maps.GetValueAsString(deployConfig, "domain"), @@ -266,7 +269,7 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] return deployer, logger, err } - case targetTencentCDN, targetTencentCLB, targetTencentCOS, targetTencentECDN, targetTencentTEO: + case targetTencentCloudCDN, targetTencentCloudCLB, targetTencentCloudCOS, targetTencentCloudECDN, targetTencentCloudEO: { access := &domain.TencentAccess{} if err := json.Unmarshal([]byte(accessConfig), access); err != nil { @@ -274,28 +277,28 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] } switch target { - case targetTencentCDN: - deployer, err := providerTencentCloudCdn.NewWithLogger(&providerTencentCloudCdn.TencentCloudCDNDeployerConfig{ + case targetTencentCloudCDN: + deployer, err := providerTencentCloudCDN.NewWithLogger(&providerTencentCloudCDN.TencentCloudCDNDeployerConfig{ SecretId: access.SecretId, SecretKey: access.SecretKey, Domain: maps.GetValueAsString(deployConfig, "domain"), }, logger) return deployer, logger, err - case targetTencentCLB: - deployer, err := providerTencentCloudClb.NewWithLogger(&providerTencentCloudClb.TencentCloudCLBDeployerConfig{ + case targetTencentCloudCLB: + deployer, err := providerTencentCloudCLB.NewWithLogger(&providerTencentCloudCLB.TencentCloudCLBDeployerConfig{ SecretId: access.SecretId, SecretKey: access.SecretKey, Region: maps.GetValueAsString(deployConfig, "region"), - ResourceType: providerTencentCloudClb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")), + ResourceType: providerTencentCloudCLB.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")), LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"), ListenerId: maps.GetValueAsString(deployConfig, "listenerId"), Domain: maps.GetValueAsString(deployConfig, "domain"), }, logger) return deployer, logger, err - case targetTencentCOS: - deployer, err := providerTencentCloudCos.NewWithLogger(&providerTencentCloudCos.TencentCloudCOSDeployerConfig{ + case targetTencentCloudCOS: + deployer, err := providerTencentCloudCOD.NewWithLogger(&providerTencentCloudCOD.TencentCloudCOSDeployerConfig{ SecretId: access.SecretId, SecretKey: access.SecretKey, Region: maps.GetValueAsString(deployConfig, "region"), @@ -304,16 +307,16 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] }, logger) return deployer, logger, err - case targetTencentECDN: - deployer, err := providerTencentCloudEcdn.NewWithLogger(&providerTencentCloudEcdn.TencentCloudECDNDeployerConfig{ + case targetTencentCloudECDN: + deployer, err := providerTencentCloudECDN.NewWithLogger(&providerTencentCloudECDN.TencentCloudECDNDeployerConfig{ SecretId: access.SecretId, SecretKey: access.SecretKey, Domain: maps.GetValueAsString(deployConfig, "domain"), }, logger) return deployer, logger, err - case targetTencentTEO: - deployer, err := providerTencentCloudTeo.NewWithLogger(&providerTencentCloudTeo.TencentCloudTEODeployerConfig{ + case targetTencentCloudEO: + deployer, err := providerTencentCloudEO.NewWithLogger(&providerTencentCloudEO.TencentCloudEODeployerConfig{ SecretId: access.SecretId, SecretKey: access.SecretKey, ZoneId: maps.GetValueAsString(deployConfig, "zoneId"), @@ -335,7 +338,7 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] switch target { case targetVolcEngineCDN: - deployer, err := providerVolcEngineCdn.NewWithLogger(&providerVolcEngineCdn.VolcEngineCDNDeployerConfig{ + deployer, err := providerVolcEngineCDN.NewWithLogger(&providerVolcEngineCDN.VolcEngineCDNDeployerConfig{ AccessKey: access.AccessKey, SecretKey: access.SecretKey, Domain: maps.GetValueAsString(deployConfig, "domain"), @@ -362,9 +365,23 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err) } + variables := make(map[string]string) + if deployConfig != nil { + value, ok := deployConfig["variables"] + if ok { + kvs := make([]domain.KV, 0) + bts, _ := json.Marshal(value) + if err := json.Unmarshal(bts, &kvs); err == nil { + for _, kv := range kvs { + variables[kv.Key] = kv.Value + } + } + } + } + deployer, err := providerWebhook.NewWithLogger(&providerWebhook.WebhookDeployerConfig{ Url: access.Url, - Variables: nil, // TODO: 尚未实现 + Variables: variables, }, logger) return deployer, logger, err } diff --git a/internal/deployer/huaweicloud_cdn.go b/internal/deployer/huaweicloud_cdn.go deleted file mode 100644 index d3d4014f..00000000 --- a/internal/deployer/huaweicloud_cdn.go +++ /dev/null @@ -1,143 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global" - hcCdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2" - hcCdnModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model" - hcCdnRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/region" - xerrors "github.com/pkg/errors" - - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/core/uploader" - uploaderHcScm "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-scm" - "github.com/usual2970/certimate/internal/pkg/utils/cast" - hcCdnEx "github.com/usual2970/certimate/internal/pkg/vendors/huaweicloud-cdn-sdk" -) - -type HuaweiCloudCDNDeployer struct { - option *DeployerOption - infos []string - - sdkClient *hcCdnEx.Client - sslUploader uploader.Uploader -} - -func NewHuaweiCloudCDNDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.HuaweiCloudAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - client, err := (&HuaweiCloudCDNDeployer{}).createSdkClient( - access.AccessKeyId, - access.SecretAccessKey, - option.DeployConfig.GetConfigAsString("region"), - ) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create sdk client") - } - - uploader, err := uploaderHcScm.New(&uploaderHcScm.HuaweiCloudSCMUploaderConfig{ - AccessKeyId: access.AccessKeyId, - SecretAccessKey: access.SecretAccessKey, - Region: "", - }) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create ssl uploader") - } - - return &HuaweiCloudCDNDeployer{ - option: option, - infos: make([]string, 0), - sdkClient: client, - sslUploader: uploader, - }, nil -} - -func (d *HuaweiCloudCDNDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *HuaweiCloudCDNDeployer) GetInfos() []string { - return d.infos -} - -func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error { - // 上传证书到 SCM - 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://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html - showDomainFullConfigReq := &hcCdnModel.ShowDomainFullConfigRequest{ - DomainName: d.option.DeployConfig.GetConfigAsString("domain"), - } - showDomainFullConfigResp, err := d.sdkClient.ShowDomainFullConfig(showDomainFullConfigReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'cdn.ShowDomainFullConfig'") - } - - d.infos = append(d.infos, toStr("已查询到加速域名配置", showDomainFullConfigResp)) - - // 更新加速域名配置 - // REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html - // REF: https://support.huaweicloud.com/usermanual-cdn/cdn_01_0306.html - updateDomainMultiCertificatesReqBodyContent := &hcCdnEx.UpdateDomainMultiCertificatesExRequestBodyContent{} - updateDomainMultiCertificatesReqBodyContent.DomainName = d.option.DeployConfig.GetConfigAsString("domain") - updateDomainMultiCertificatesReqBodyContent.HttpsSwitch = 1 - updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(2) - updateDomainMultiCertificatesReqBodyContent.SCMCertificateId = cast.StringPtr(upres.CertId) - updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(upres.CertName) - updateDomainMultiCertificatesReqBodyContent = updateDomainMultiCertificatesReqBodyContent.MergeConfig(showDomainFullConfigResp.Configs) - updateDomainMultiCertificatesReq := &hcCdnEx.UpdateDomainMultiCertificatesExRequest{ - Body: &hcCdnEx.UpdateDomainMultiCertificatesExRequestBody{ - Https: updateDomainMultiCertificatesReqBodyContent, - }, - } - updateDomainMultiCertificatesResp, err := d.sdkClient.UploadDomainMultiCertificatesEx(updateDomainMultiCertificatesReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadDomainMultiCertificatesEx'") - } - - d.infos = append(d.infos, toStr("已更新加速域名配置", updateDomainMultiCertificatesResp)) - - return nil -} - -func (d *HuaweiCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcCdnEx.Client, error) { - if region == "" { - region = "cn-north-1" // CDN 服务默认区域:华北一北京 - } - - auth, err := global.NewCredentialsBuilder(). - WithAk(accessKeyId). - WithSk(secretAccessKey). - SafeBuild() - if err != nil { - return nil, err - } - - hcRegion, err := hcCdnRegion.SafeValueOf(region) - if err != nil { - return nil, err - } - - hcClient, err := hcCdn.CdnClientBuilder(). - WithRegion(hcRegion). - WithCredential(auth). - SafeBuild() - if err != nil { - return nil, err - } - - client := hcCdnEx.NewClient(hcClient) - return client, nil -} diff --git a/internal/deployer/huaweicloud_elb.go b/internal/deployer/huaweicloud_elb.go deleted file mode 100644 index ccc6f9f5..00000000 --- a/internal/deployer/huaweicloud_elb.go +++ /dev/null @@ -1,378 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "errors" - "fmt" - - "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" - "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global" - hcElb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3" - hcElbModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model" - hcElbRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region" - hcIam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3" - hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model" - hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region" - xerrors "github.com/pkg/errors" - "golang.org/x/exp/slices" - - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/core/uploader" - uploaderHcElb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-elb" - "github.com/usual2970/certimate/internal/pkg/utils/cast" -) - -type HuaweiCloudELBDeployer struct { - option *DeployerOption - infos []string - - sdkClient *hcElb.ElbClient - sslUploader uploader.Uploader -} - -func NewHuaweiCloudELBDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.HuaweiCloudAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - client, err := (&HuaweiCloudELBDeployer{}).createSdkClient( - access.AccessKeyId, - access.SecretAccessKey, - option.DeployConfig.GetConfigAsString("region"), - ) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create sdk client") - } - - uploader, err := uploaderHcElb.New(&uploaderHcElb.HuaweiCloudELBUploaderConfig{ - AccessKeyId: access.AccessKeyId, - SecretAccessKey: access.SecretAccessKey, - Region: option.DeployConfig.GetConfigAsString("region"), - }) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create ssl uploader") - } - - return &HuaweiCloudELBDeployer{ - option: option, - infos: make([]string, 0), - sdkClient: client, - sslUploader: uploader, - }, nil -} - -func (d *HuaweiCloudELBDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *HuaweiCloudELBDeployer) GetInfos() []string { - return d.infos -} - -func (d *HuaweiCloudELBDeployer) Deploy(ctx context.Context) error { - switch d.option.DeployConfig.GetConfigAsString("resourceType") { - case "certificate": - // 部署到指定证书 - if err := d.deployToCertificate(ctx); err != nil { - return err - } - case "loadbalancer": - // 部署到指定负载均衡器 - if err := d.deployToLoadbalancer(ctx); err != nil { - return err - } - case "listener": - // 部署到指定监听器 - if err := d.deployToListener(ctx); err != nil { - return err - } - default: - return errors.New("unsupported resource type") - } - - return nil -} - -func (d *HuaweiCloudELBDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) { - if region == "" { - region = "cn-north-4" // ELB 服务默认区域:华北四北京 - } - - projectId, err := (&HuaweiCloudELBDeployer{}).getSdkProjectId( - accessKeyId, - secretAccessKey, - region, - ) - if err != nil { - return nil, err - } - - auth, err := basic.NewCredentialsBuilder(). - WithAk(accessKeyId). - WithSk(secretAccessKey). - WithProjectId(projectId). - SafeBuild() - if err != nil { - return nil, err - } - - hcRegion, err := hcElbRegion.SafeValueOf(region) - if err != nil { - return nil, err - } - - hcClient, err := hcElb.ElbClientBuilder(). - WithRegion(hcRegion). - WithCredential(auth). - SafeBuild() - if err != nil { - return nil, err - } - - client := hcElb.NewElbClient(hcClient) - return client, nil -} - -func (u *HuaweiCloudELBDeployer) getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) { - if region == "" { - region = "cn-north-4" // IAM 服务默认区域:华北四北京 - } - - auth, err := global.NewCredentialsBuilder(). - WithAk(accessKeyId). - WithSk(secretAccessKey). - SafeBuild() - if err != nil { - return "", err - } - - hcRegion, err := hcIamRegion.SafeValueOf(region) - if err != nil { - return "", err - } - - hcClient, err := hcIam.IamClientBuilder(). - WithRegion(hcRegion). - WithCredential(auth). - SafeBuild() - if err != nil { - return "", err - } - - client := hcIam.NewIamClient(hcClient) - - request := &hcIamModel.KeystoneListProjectsRequest{ - Name: ®ion, - } - response, err := client.KeystoneListProjects(request) - if err != nil { - return "", err - } else if response.Projects == nil || len(*response.Projects) == 0 { - return "", errors.New("no project found") - } - - return (*response.Projects)[0].Id, nil -} - -func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context) error { - hcCertId := d.option.DeployConfig.GetConfigAsString("certificateId") - if hcCertId == "" { - return errors.New("`certificateId` is required") - } - - // 更新证书 - // REF: https://support.huaweicloud.com/api-elb/UpdateCertificate.html - updateCertificateReq := &hcElbModel.UpdateCertificateRequest{ - CertificateId: hcCertId, - Body: &hcElbModel.UpdateCertificateRequestBody{ - Certificate: &hcElbModel.UpdateCertificateOption{ - Certificate: cast.StringPtr(d.option.Certificate.Certificate), - PrivateKey: cast.StringPtr(d.option.Certificate.PrivateKey), - }, - }, - } - updateCertificateResp, err := d.sdkClient.UpdateCertificate(updateCertificateReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateCertificate'") - } - - d.infos = append(d.infos, toStr("已更新 ELB 证书", updateCertificateResp)) - - return nil -} - -func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error { - hcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") - if hcLoadbalancerId == "" { - return errors.New("`loadbalancerId` is required") - } - - hcListenerIds := make([]string, 0) - - // 查询负载均衡器详情 - // REF: https://support.huaweicloud.com/api-elb/ShowLoadBalancer.html - showLoadBalancerReq := &hcElbModel.ShowLoadBalancerRequest{ - LoadbalancerId: hcLoadbalancerId, - } - showLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowLoadBalancer'") - } - - d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器", showLoadBalancerResp)) - - // 查询监听器列表 - // REF: https://support.huaweicloud.com/api-elb/ListListeners.html - listListenersLimit := int32(2000) - var listListenersMarker *string = nil - for { - listListenersReq := &hcElbModel.ListListenersRequest{ - Limit: cast.Int32Ptr(listListenersLimit), - Marker: listListenersMarker, - Protocol: &[]string{"HTTPS", "TERMINATED_HTTPS"}, - LoadbalancerId: &[]string{showLoadBalancerResp.Loadbalancer.Id}, - } - listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListListeners'") - } - - if listListenersResp.Listeners != nil { - for _, listener := range *listListenersResp.Listeners { - hcListenerIds = append(hcListenerIds, listener.Id) - } - } - - if listListenersResp.Listeners == nil || len(*listListenersResp.Listeners) < int(listListenersLimit) { - break - } else { - listListenersMarker = listListenersResp.PageInfo.NextMarker - } - } - - d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器下的监听器", hcListenerIds)) - - // 上传证书到 SCM - 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)) - - // 批量更新监听器证书 - var errs []error - for _, hcListenerId := range hcListenerIds { - if err := d.modifyListenerCertificate(ctx, hcListenerId, upres.CertId); err != nil { - errs = append(errs, err) - } - } - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context) error { - hcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId") - if hcListenerId == "" { - return errors.New("`listenerId` is required") - } - - // 上传证书到 SCM - 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)) - - // 更新监听器证书 - if err := d.modifyListenerCertificate(ctx, hcListenerId, upres.CertId); err != nil { - return err - } - - return nil -} - -func (d *HuaweiCloudELBDeployer) modifyListenerCertificate(ctx context.Context, hcListenerId string, hcCertId string) error { - // 查询监听器详情 - // REF: https://support.huaweicloud.com/api-elb/ShowListener.html - showListenerReq := &hcElbModel.ShowListenerRequest{ - ListenerId: hcListenerId, - } - showListenerResp, err := d.sdkClient.ShowListener(showListenerReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowListener'") - } - - d.infos = append(d.infos, toStr("已查询到 ELB 监听器", showListenerResp)) - - // 更新监听器 - // REF: https://support.huaweicloud.com/api-elb/UpdateListener.html - updateListenerReq := &hcElbModel.UpdateListenerRequest{ - ListenerId: hcListenerId, - Body: &hcElbModel.UpdateListenerRequestBody{ - Listener: &hcElbModel.UpdateListenerOption{ - DefaultTlsContainerRef: cast.StringPtr(hcCertId), - }, - }, - } - if showListenerResp.Listener.SniContainerRefs != nil { - if len(showListenerResp.Listener.SniContainerRefs) > 0 { - // 如果开启 SNI,需替换同 SAN 的证书 - sniCertIds := make([]string, 0) - sniCertIds = append(sniCertIds, hcCertId) - - listOldCertificateReq := &hcElbModel.ListCertificatesRequest{ - Id: &showListenerResp.Listener.SniContainerRefs, - } - listOldCertificateResp, err := d.sdkClient.ListCertificates(listOldCertificateReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListCertificates'") - } - - showNewCertificateReq := &hcElbModel.ShowCertificateRequest{ - CertificateId: hcCertId, - } - showNewCertificateResp, err := d.sdkClient.ShowCertificate(showNewCertificateReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowCertificate'") - } - - for _, certificate := range *listOldCertificateResp.Certificates { - oldCertificate := certificate - newCertificate := showNewCertificateResp.Certificate - - if oldCertificate.SubjectAlternativeNames != nil && newCertificate.SubjectAlternativeNames != nil { - if slices.Equal(*oldCertificate.SubjectAlternativeNames, *newCertificate.SubjectAlternativeNames) { - continue - } - } else { - if oldCertificate.Domain == newCertificate.Domain { - continue - } - } - - sniCertIds = append(sniCertIds, certificate.Id) - } - - updateListenerReq.Body.Listener.SniContainerRefs = &sniCertIds - } - - if showListenerResp.Listener.SniMatchAlgo != "" { - updateListenerReq.Body.Listener.SniMatchAlgo = cast.StringPtr(showListenerResp.Listener.SniMatchAlgo) - } - } - updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateListener'") - } - - d.infos = append(d.infos, toStr("已更新 ELB 监听器", updateListenerResp)) - - return nil -} diff --git a/internal/deployer/k8s_secret.go b/internal/deployer/k8s_secret.go deleted file mode 100644 index 8a1c30ff..00000000 --- a/internal/deployer/k8s_secret.go +++ /dev/null @@ -1,136 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "strings" - - xerrors "github.com/pkg/errors" - k8sCore "k8s.io/api/core/v1" - k8sMeta "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/utils/x509" -) - -type K8sSecretDeployer struct { - option *DeployerOption - infos []string - - k8sClient *kubernetes.Clientset -} - -func NewK8sSecretDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.KubernetesAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - client, err := (&K8sSecretDeployer{}).createK8sClient(access) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create k8s client") - } - - return &K8sSecretDeployer{ - option: option, - infos: make([]string, 0), - k8sClient: client, - }, nil -} - -func (d *K8sSecretDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *K8sSecretDeployer) GetInfos() []string { - return d.infos -} - -func (d *K8sSecretDeployer) Deploy(ctx context.Context) error { - namespace := d.option.DeployConfig.GetConfigAsString("namespace") - secretName := d.option.DeployConfig.GetConfigAsString("secretName") - secretDataKeyForCrt := d.option.DeployConfig.GetConfigOrDefaultAsString("secretDataKeyForCrt", "tls.crt") - secretDataKeyForKey := d.option.DeployConfig.GetConfigOrDefaultAsString("secretDataKeyForKey", "tls.key") - if namespace == "" { - namespace = "default" - } - if secretName == "" { - return errors.New("`secretName` is required") - } - - certX509, err := x509.ParseCertificateFromPEM(d.option.Certificate.Certificate) - if err != nil { - return err - } - - secretPayload := k8sCore.Secret{ - TypeMeta: k8sMeta.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: k8sMeta.ObjectMeta{ - Name: secretName, - Annotations: map[string]string{ - "certimate/domains": d.option.Domain, - "certimate/alt-names": strings.Join(certX509.DNSNames, ","), - "certimate/common-name": certX509.Subject.CommonName, - "certimate/issuer-organization": strings.Join(certX509.Issuer.Organization, ","), - }, - }, - Type: k8sCore.SecretType("kubernetes.io/tls"), - } - secretPayload.Data = make(map[string][]byte) - secretPayload.Data[secretDataKeyForCrt] = []byte(d.option.Certificate.Certificate) - secretPayload.Data[secretDataKeyForKey] = []byte(d.option.Certificate.PrivateKey) - - // 获取 Secret 实例 - _, err = d.k8sClient.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, k8sMeta.GetOptions{}) - if err != nil { - _, err = d.k8sClient.CoreV1().Secrets(namespace).Create(context.TODO(), &secretPayload, k8sMeta.CreateOptions{}) - if err != nil { - return xerrors.Wrap(err, "failed to create k8s secret") - } else { - d.infos = append(d.infos, toStr("Certificate has been created in K8s Secret", nil)) - return nil - } - } - - // 更新 Secret 实例 - _, err = d.k8sClient.CoreV1().Secrets(namespace).Update(context.TODO(), &secretPayload, k8sMeta.UpdateOptions{}) - if err != nil { - return xerrors.Wrap(err, "failed to update k8s secret") - } - - d.infos = append(d.infos, toStr("Certificate has been updated to K8s Secret", nil)) - - return nil -} - -func (d *K8sSecretDeployer) createK8sClient(access *domain.KubernetesAccess) (*kubernetes.Clientset, error) { - var config *rest.Config - var err error - if access.KubeConfig == "" { - config, err = rest.InClusterConfig() - } else { - kubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(access.KubeConfig)) - if err != nil { - return nil, err - } - config, err = kubeConfig.ClientConfig() - } - if err != nil { - return nil, err - } - - client, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, err - } - - return client, nil -} diff --git a/internal/deployer/local.go b/internal/deployer/local.go deleted file mode 100644 index 3c693d30..00000000 --- a/internal/deployer/local.go +++ /dev/null @@ -1,163 +0,0 @@ -package deployer - -import ( - "bytes" - "context" - "errors" - "fmt" - "os/exec" - "runtime" - - xerrors "github.com/pkg/errors" - - "github.com/usual2970/certimate/internal/pkg/utils/fs" - "github.com/usual2970/certimate/internal/pkg/utils/x509" -) - -type LocalDeployer struct { - option *DeployerOption - infos []string -} - -const ( - certFormatPEM = "pem" - certFormatPFX = "pfx" - certFormatJKS = "jks" -) - -const ( - shellEnvSh = "sh" - shellEnvCmd = "cmd" - shellEnvPowershell = "powershell" -) - -func NewLocalDeployer(option *DeployerOption) (Deployer, error) { - return &LocalDeployer{ - option: option, - infos: make([]string, 0), - }, nil -} - -func (d *LocalDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *LocalDeployer) GetInfos() []string { - return []string{} -} - -func (d *LocalDeployer) Deploy(ctx context.Context) error { - // 执行前置命令 - preCommand := d.option.DeployConfig.GetConfigAsString("preCommand") - if preCommand != "" { - stdout, stderr, err := d.execCommand(preCommand) - if err != nil { - return xerrors.Wrapf(err, "failed to run pre-command, stdout: %s, stderr: %s", stdout, stderr) - } - - d.infos = append(d.infos, toStr("执行前置命令成功", stdout)) - } - - // 写入证书和私钥文件 - switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) { - case certFormatPEM: - if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil { - return err - } - - d.infos = append(d.infos, toStr("保存证书成功", nil)) - - if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil { - return err - } - - d.infos = append(d.infos, toStr("保存私钥成功", nil)) - - case certFormatPFX: - pfxData, err := x509.TransformCertificateFromPEMToPFX( - d.option.Certificate.Certificate, - d.option.Certificate.PrivateKey, - d.option.DeployConfig.GetConfigAsString("pfxPassword"), - ) - if err != nil { - return err - } - - if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil { - return err - } - - d.infos = append(d.infos, toStr("保存证书成功", nil)) - - case certFormatJKS: - jksData, err := x509.TransformCertificateFromPEMToJKS( - d.option.Certificate.Certificate, - d.option.Certificate.PrivateKey, - d.option.DeployConfig.GetConfigAsString("jksAlias"), - d.option.DeployConfig.GetConfigAsString("jksKeypass"), - d.option.DeployConfig.GetConfigAsString("jksStorepass"), - ) - if err != nil { - return err - } - - if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil { - return err - } - - d.infos = append(d.infos, toStr("保存证书成功", nil)) - - default: - return errors.New("unsupported format") - } - - // 执行命令 - command := d.option.DeployConfig.GetConfigAsString("command") - if command != "" { - stdout, stderr, err := d.execCommand(command) - if err != nil { - return xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr) - } - - d.infos = append(d.infos, toStr("执行命令成功", stdout)) - } - - return nil -} - -func (d *LocalDeployer) execCommand(command string) (string, string, error) { - var cmd *exec.Cmd - - switch d.option.DeployConfig.GetConfigAsString("shell") { - case shellEnvSh: - cmd = exec.Command("sh", "-c", command) - - case shellEnvCmd: - cmd = exec.Command("cmd", "/C", command) - - case shellEnvPowershell: - cmd = exec.Command("powershell", "-Command", command) - - case "": - if runtime.GOOS == "windows" { - cmd = exec.Command("cmd", "/C", command) - } else { - cmd = exec.Command("sh", "-c", command) - } - - default: - return "", "", errors.New("unsupported shell") - } - - var stdoutBuf bytes.Buffer - cmd.Stdout = &stdoutBuf - var stderrBuf bytes.Buffer - cmd.Stderr = &stderrBuf - - err := cmd.Run() - if err != nil { - return "", "", xerrors.Wrap(err, "failed to execute shell script") - } - - return stdoutBuf.String(), stderrBuf.String(), nil -} diff --git a/internal/deployer/qiniu_cdn.go b/internal/deployer/qiniu_cdn.go deleted file mode 100644 index bc638af9..00000000 --- a/internal/deployer/qiniu_cdn.go +++ /dev/null @@ -1,113 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - xerrors "github.com/pkg/errors" - "github.com/qiniu/go-sdk/v7/auth" - - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/core/uploader" - uploaderQiniu "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/qiniu-sslcert" - qiniuEx "github.com/usual2970/certimate/internal/pkg/vendors/qiniu-sdk" -) - -type QiniuCDNDeployer struct { - option *DeployerOption - infos []string - - sdkClient *qiniuEx.Client - sslUploader uploader.Uploader -} - -func NewQiniuCDNDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.QiniuAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - client, err := (&QiniuCDNDeployer{}).createSdkClient( - access.AccessKey, - access.SecretKey, - ) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create sdk client") - } - - uploader, err := uploaderQiniu.New(&uploaderQiniu.QiniuSSLCertUploaderConfig{ - AccessKey: access.AccessKey, - SecretKey: access.SecretKey, - }) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create ssl uploader") - } - - return &QiniuCDNDeployer{ - option: option, - infos: make([]string, 0), - sdkClient: client, - sslUploader: uploader, - }, nil -} - -func (d *QiniuCDNDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *QiniuCDNDeployer) GetInfos() []string { - return d.infos -} - -func (d *QiniuCDNDeployer) Deploy(ctx context.Context) error { - // 上传证书 - 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)) - - // 在七牛 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 - getDomainInfoResp, err := d.sdkClient.GetDomainInfo(domain) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDomainInfo'") - } - - d.infos = append(d.infos, toStr("已获取域名信息", getDomainInfoResp)) - - // 判断域名是否已启用 HTTPS。如果已启用,修改域名证书;否则,启用 HTTPS - // REF: https://developer.qiniu.com/fusion/4246/the-domain-name - if getDomainInfoResp.Https != nil && getDomainInfoResp.Https.CertID != "" { - modifyDomainHttpsConfResp, err := d.sdkClient.ModifyDomainHttpsConf(domain, upres.CertId, getDomainInfoResp.Https.ForceHttps, getDomainInfoResp.Https.Http2Enable) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'cdn.ModifyDomainHttpsConf'") - } - - d.infos = append(d.infos, toStr("已修改域名证书", modifyDomainHttpsConfResp)) - } else { - enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(domain, upres.CertId, true, true) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'cdn.EnableDomainHttps'") - } - - d.infos = append(d.infos, toStr("已将域名升级为 HTTPS", enableDomainHttpsResp)) - } - - return nil -} - -func (u *QiniuCDNDeployer) createSdkClient(accessKey, secretKey string) (*qiniuEx.Client, error) { - credential := auth.New(accessKey, secretKey) - client := qiniuEx.NewClient(credential) - return client, nil -} diff --git a/internal/deployer/ssh.go b/internal/deployer/ssh.go deleted file mode 100644 index 96f8bdd2..00000000 --- a/internal/deployer/ssh.go +++ /dev/null @@ -1,209 +0,0 @@ -package deployer - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - - xerrors "github.com/pkg/errors" - "github.com/pkg/sftp" - "golang.org/x/crypto/ssh" - - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/utils/x509" -) - -type SSHDeployer struct { - option *DeployerOption - infos []string -} - -func NewSSHDeployer(option *DeployerOption) (Deployer, error) { - return &SSHDeployer{ - option: option, - infos: make([]string, 0), - }, nil -} - -func (d *SSHDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *SSHDeployer) GetInfos() []string { - return d.infos -} - -func (d *SSHDeployer) Deploy(ctx context.Context) error { - access := &domain.SSHAccess{} - if err := json.Unmarshal([]byte(d.option.Access), access); err != nil { - return err - } - - // 连接 - client, err := d.createSshClient(access) - if err != nil { - return err - } - defer client.Close() - - d.infos = append(d.infos, toStr("SSH 连接成功", nil)) - - // 执行前置命令 - preCommand := d.option.DeployConfig.GetConfigAsString("preCommand") - if preCommand != "" { - stdout, stderr, err := d.sshExecCommand(client, preCommand) - if err != nil { - return xerrors.Wrapf(err, "failed to run pre-command: stdout: %s, stderr: %s", stdout, stderr) - } - - d.infos = append(d.infos, toStr("SSH 执行前置命令成功", stdout)) - } - - // 上传证书和私钥文件 - switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) { - case certFormatPEM: - if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil { - return err - } - - d.infos = append(d.infos, toStr("SSH 上传证书成功", nil)) - - if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil { - return err - } - - d.infos = append(d.infos, toStr("SSH 上传私钥成功", nil)) - - case certFormatPFX: - pfxData, err := x509.TransformCertificateFromPEMToPFX( - d.option.Certificate.Certificate, - d.option.Certificate.PrivateKey, - d.option.DeployConfig.GetConfigAsString("pfxPassword"), - ) - if err != nil { - return err - } - - if err := d.writeSftpFile(client, d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil { - return err - } - - d.infos = append(d.infos, toStr("SSH 上传证书成功", nil)) - - case certFormatJKS: - jksData, err := x509.TransformCertificateFromPEMToJKS( - d.option.Certificate.Certificate, - d.option.Certificate.PrivateKey, - d.option.DeployConfig.GetConfigAsString("jksAlias"), - d.option.DeployConfig.GetConfigAsString("jksKeypass"), - d.option.DeployConfig.GetConfigAsString("jksStorepass"), - ) - if err != nil { - return err - } - - if err := d.writeSftpFile(client, d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil { - return err - } - - d.infos = append(d.infos, toStr("SSH 上传证书成功", nil)) - - default: - return errors.New("unsupported format") - } - - // 执行命令 - command := d.option.DeployConfig.GetConfigAsString("command") - if command != "" { - stdout, stderr, err := d.sshExecCommand(client, command) - if err != nil { - return xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr) - } - - d.infos = append(d.infos, toStr("SSH 执行命令成功", stdout)) - } - - return nil -} - -func (d *SSHDeployer) createSshClient(access *domain.SSHAccess) (*ssh.Client, error) { - var authMethod ssh.AuthMethod - - if access.Key != "" { - var signer ssh.Signer - var err error - - if access.KeyPassphrase != "" { - signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(access.Key), []byte(access.KeyPassphrase)) - } else { - signer, err = ssh.ParsePrivateKey([]byte(access.Key)) - } - - if err != nil { - return nil, err - } - authMethod = ssh.PublicKeys(signer) - } else { - authMethod = ssh.Password(access.Password) - } - - return ssh.Dial("tcp", fmt.Sprintf("%s:%s", access.Host, access.Port), &ssh.ClientConfig{ - User: access.Username, - Auth: []ssh.AuthMethod{ - authMethod, - }, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - }) -} - -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") - } - - defer session.Close() - var stdoutBuf bytes.Buffer - session.Stdout = &stdoutBuf - var stderrBuf bytes.Buffer - session.Stderr = &stderrBuf - err = session.Run(command) - if err != nil { - return "", "", xerrors.Wrap(err, "failed to execute ssh script") - } - - return stdoutBuf.String(), stderrBuf.String(), nil -} - -func (d *SSHDeployer) writeSftpFileString(sshCli *ssh.Client, path string, content string) error { - return d.writeSftpFile(sshCli, path, []byte(content)) -} - -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") - } - defer sftpCli.Close() - - if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil { - return xerrors.Wrap(err, "failed to create remote directory") - } - - file, err := sftpCli.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC) - if err != nil { - return xerrors.Wrap(err, "failed to open remote file") - } - defer file.Close() - - _, err = file.Write(data) - if err != nil { - return xerrors.Wrap(err, "failed to write to remote file") - } - - return nil -} diff --git a/internal/deployer/ssh_test.go b/internal/deployer/ssh_test.go deleted file mode 100644 index c9b1e85a..00000000 --- a/internal/deployer/ssh_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package deployer - -import ( - "os" - "path" - "testing" -) - -func TestPath(t *testing.T) { - dir := path.Dir("./a/b/c") - os.MkdirAll(dir, 0o755) -} diff --git a/internal/deployer/tencent_cdn.go b/internal/deployer/tencent_cdn.go deleted file mode 100644 index f5230910..00000000 --- a/internal/deployer/tencent_cdn.go +++ /dev/null @@ -1,194 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - xerrors "github.com/pkg/errors" - tcCdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" - "golang.org/x/exp/slices" - - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/core/uploader" - uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl" -) - -type TencentCDNDeployer struct { - option *DeployerOption - infos []string - - sdkClients *tencentCDNDeployerSdkClients - sslUploader uploader.Uploader -} - -type tencentCDNDeployerSdkClients struct { - ssl *tcSsl.Client - cdn *tcCdn.Client -} - -func NewTencentCDNDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.TencentAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - clients, err := (&TencentCDNDeployer{}).createSdkClients( - access.SecretId, - access.SecretKey, - ) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create sdk clients") - } - - uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{ - SecretId: access.SecretId, - SecretKey: access.SecretKey, - }) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create ssl uploader") - } - - return &TencentCDNDeployer{ - option: option, - infos: make([]string, 0), - sdkClients: clients, - sslUploader: uploader, - }, nil -} - -func (d *TencentCDNDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *TencentCDNDeployer) GetInfos() []string { - return d.infos -} - -func (d *TencentCDNDeployer) Deploy(ctx context.Context) error { - // 上传证书到 SSL - 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)) - - // 获取待部署的 CDN 实例 - // 如果是泛域名,根据证书匹配 CDN 实例 - tcInstanceIds := make([]string, 0) - domain := d.option.DeployConfig.GetConfigAsString("domain") - if strings.HasPrefix(domain, "*") { - domains, err := d.getDomainsByCertificateId(upres.CertId) - if err != nil { - return err - } - - tcInstanceIds = domains - } else { - tcInstanceIds = append(tcInstanceIds, domain) - } - - // 跳过已部署的 CDN 实例 - if len(tcInstanceIds) > 0 { - deployedDomains, err := d.getDeployedDomainsByCertificateId(upres.CertId) - if err != nil { - return err - } - - temp := make([]string, 0) - for _, tcInstanceId := range tcInstanceIds { - if !slices.Contains(deployedDomains, tcInstanceId) { - temp = append(temp, tcInstanceId) - } - } - tcInstanceIds = temp - } - if len(tcInstanceIds) == 0 { - d.infos = append(d.infos, "已部署过或没有要部署的 CDN 实例") - return nil - } - - // 证书部署到 CDN 实例 - // REF: https://cloud.tencent.com/document/product/400/91667 - deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest() - deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) - deployCertificateInstanceReq.ResourceType = common.StringPtr("cdn") - deployCertificateInstanceReq.Status = common.Int64Ptr(1) - deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(tcInstanceIds) - deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'") - } - - d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response)) - - return nil -} - -func (d *TencentCDNDeployer) createSdkClients(secretId, secretKey string) (*tencentCDNDeployerSdkClients, error) { - credential := common.NewCredential(secretId, secretKey) - - sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile()) - if err != nil { - return nil, err - } - - cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile()) - if err != nil { - return nil, err - } - - return &tencentCDNDeployerSdkClients{ - ssl: sslClient, - cdn: cdnClient, - }, nil -} - -func (d *TencentCDNDeployer) getDomainsByCertificateId(tcCertId string) ([]string, error) { - // 获取证书中的可用域名 - // REF: https://cloud.tencent.com/document/product/228/42491 - describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest() - describeCertDomainsReq.CertId = common.StringPtr(tcCertId) - describeCertDomainsReq.Product = common.StringPtr("cdn") - describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq) - if err != nil { - return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'") - } - - domains := make([]string, 0) - if describeCertDomainsResp.Response.Domains == nil { - for _, domain := range describeCertDomainsResp.Response.Domains { - domains = append(domains, *domain) - } - } - - return domains, nil -} - -func (d *TencentCDNDeployer) getDeployedDomainsByCertificateId(tcCertId string) ([]string, error) { - // 根据证书查询关联 CDN 域名 - // REF: https://cloud.tencent.com/document/product/400/62674 - describeDeployedResourcesReq := tcSsl.NewDescribeDeployedResourcesRequest() - describeDeployedResourcesReq.CertificateIds = common.StringPtrs([]string{tcCertId}) - describeDeployedResourcesReq.ResourceType = common.StringPtr("cdn") - describeDeployedResourcesResp, err := d.sdkClients.ssl.DescribeDeployedResources(describeDeployedResourcesReq) - if err != nil { - return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeDeployedResources'") - } - - domains := make([]string, 0) - if describeDeployedResourcesResp.Response.DeployedResources != nil { - for _, deployedResource := range describeDeployedResourcesResp.Response.DeployedResources { - for _, resource := range deployedResource.Resources { - domains = append(domains, *resource) - } - } - } - - return domains, nil -} diff --git a/internal/deployer/tencent_clb.go b/internal/deployer/tencent_clb.go deleted file mode 100644 index c3feefc7..00000000 --- a/internal/deployer/tencent_clb.go +++ /dev/null @@ -1,328 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "errors" - "fmt" - - xerrors "github.com/pkg/errors" - tcClb "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb/v20180317" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" - - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/core/uploader" - uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl" -) - -type TencentCLBDeployer struct { - option *DeployerOption - infos []string - - sdkClients *tencentCLBDeployerSdkClients - sslUploader uploader.Uploader -} - -type tencentCLBDeployerSdkClients struct { - ssl *tcSsl.Client - clb *tcClb.Client -} - -func NewTencentCLBDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.TencentAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - clients, err := (&TencentCLBDeployer{}).createSdkClients( - access.SecretId, - access.SecretKey, - option.DeployConfig.GetConfigAsString("region"), - ) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create sdk clients") - } - - uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{ - SecretId: access.SecretId, - SecretKey: access.SecretKey, - }) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create ssl uploader") - } - - return &TencentCLBDeployer{ - option: option, - infos: make([]string, 0), - sdkClients: clients, - sslUploader: uploader, - }, nil -} - -func (d *TencentCLBDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *TencentCLBDeployer) GetInfos() []string { - return d.infos -} - -func (d *TencentCLBDeployer) Deploy(ctx context.Context) error { - switch d.option.DeployConfig.GetConfigAsString("resourceType") { - case "ssl-deploy": - // 通过 SSL 服务部署到云资源实例 - err := d.deployToInstanceUseSsl(ctx) - if err != nil { - return err - } - case "loadbalancer": - // 部署到指定负载均衡器 - if err := d.deployToLoadbalancer(ctx); err != nil { - return err - } - case "listener": - // 部署到指定监听器 - if err := d.deployToListener(ctx); err != nil { - return err - } - case "ruledomain": - // 部署到指定七层监听转发规则域名 - if err := d.deployToRuleDomain(ctx); err != nil { - return err - } - default: - return errors.New("unsupported resource type") - } - - return nil -} - -func (d *TencentCLBDeployer) createSdkClients(secretId, secretKey, region string) (*tencentCLBDeployerSdkClients, error) { - credential := common.NewCredential(secretId, secretKey) - - sslClient, err := tcSsl.NewClient(credential, region, profile.NewClientProfile()) - if err != nil { - return nil, err - } - - clbClient, err := tcClb.NewClient(credential, region, profile.NewClientProfile()) - if err != nil { - return nil, err - } - - return &tencentCLBDeployerSdkClients{ - ssl: sslClient, - clb: clbClient, - }, nil -} - -func (d *TencentCLBDeployer) deployToInstanceUseSsl(ctx context.Context) error { - tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") - tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId") - tcDomain := d.option.DeployConfig.GetConfigAsString("domain") - if tcLoadbalancerId == "" { - return errors.New("`loadbalancerId` is required") - } - if tcListenerId == "" { - return errors.New("`listenerId` is required") - } - - // 上传证书到 SSL - 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)) - - // 证书部署到 CLB 实例 - // REF: https://cloud.tencent.com/document/product/400/91667 - deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest() - deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) - deployCertificateInstanceReq.ResourceType = common.StringPtr("clb") - deployCertificateInstanceReq.Status = common.Int64Ptr(1) - if tcDomain == "" { - // 未开启 SNI,只需指定到监听器 - deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s", tcLoadbalancerId, tcListenerId)}) - } else { - // 开启 SNI,需指定到域名(支持泛域名) - deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", tcLoadbalancerId, tcListenerId, tcDomain)}) - } - deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'") - } - - d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response)) - - return nil -} - -func (d *TencentCLBDeployer) deployToLoadbalancer(ctx context.Context) error { - tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") - tcListenerIds := make([]string, 0) - if tcLoadbalancerId == "" { - return errors.New("`loadbalancerId` is required") - } - - // 查询负载均衡器详细信息 - // REF: https://cloud.tencent.com/document/api/214/46916 - describeLoadBalancersDetailReq := tcClb.NewDescribeLoadBalancersDetailRequest() - describeLoadBalancersDetailResp, err := d.sdkClients.clb.DescribeLoadBalancersDetail(describeLoadBalancersDetailReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeLoadBalancersDetail'") - } - - d.infos = append(d.infos, toStr("已查询到负载均衡详细信息", describeLoadBalancersDetailResp)) - - // 查询监听器列表 - // REF: https://cloud.tencent.com/document/api/214/30686 - describeListenersReq := tcClb.NewDescribeListenersRequest() - describeListenersReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId) - describeListenersResp, err := d.sdkClients.clb.DescribeListeners(describeListenersReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeListeners'") - } else { - if describeListenersResp.Response.Listeners != nil { - for _, listener := range describeListenersResp.Response.Listeners { - if listener.Protocol == nil || (*listener.Protocol != "HTTPS" && *listener.Protocol != "TCP_SSL" && *listener.Protocol != "QUIC") { - continue - } - - tcListenerIds = append(tcListenerIds, *listener.ListenerId) - } - } - } - - d.infos = append(d.infos, toStr("已查询到负载均衡器下的监听器", tcListenerIds)) - - // 上传证书到 SCM - 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)) - - // 批量更新监听器证书 - var errs []error - for _, tcListenerId := range tcListenerIds { - if err := d.modifyListenerCertificate(ctx, tcLoadbalancerId, tcListenerId, upres.CertId); err != nil { - errs = append(errs, err) - } - } - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (d *TencentCLBDeployer) deployToListener(ctx context.Context) error { - tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") - tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId") - if tcLoadbalancerId == "" { - return errors.New("`loadbalancerId` is required") - } - if tcListenerId == "" { - return errors.New("`listenerId` is required") - } - - // 上传证书到 SSL - 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)) - - // 更新监听器证书 - if err := d.modifyListenerCertificate(ctx, tcLoadbalancerId, tcListenerId, upres.CertId); err != nil { - return err - } - - return nil -} - -func (d *TencentCLBDeployer) deployToRuleDomain(ctx context.Context) error { - tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") - tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId") - tcDomain := d.option.DeployConfig.GetConfigAsString("domain") - if tcLoadbalancerId == "" { - return errors.New("`loadbalancerId` is required") - } - if tcListenerId == "" { - return errors.New("`listenerId` is required") - } - if tcDomain == "" { - return errors.New("`domain` is required") - } - - // 上传证书到 SSL - 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://cloud.tencent.com/document/api/214/38092 - modifyDomainAttributesReq := tcClb.NewModifyDomainAttributesRequest() - modifyDomainAttributesReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId) - modifyDomainAttributesReq.ListenerId = common.StringPtr(tcListenerId) - modifyDomainAttributesReq.Domain = common.StringPtr(tcDomain) - modifyDomainAttributesReq.Certificate = &tcClb.CertificateInput{ - SSLMode: common.StringPtr("UNIDIRECTIONAL"), - CertId: common.StringPtr(upres.CertId), - } - modifyDomainAttributesResp, err := d.sdkClients.clb.ModifyDomainAttributes(modifyDomainAttributesReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'clb.ModifyDomainAttributes'") - } - - d.infos = append(d.infos, toStr("已修改七层监听器转发规则的域名级别属性", modifyDomainAttributesResp.Response)) - - return nil -} - -func (d *TencentCLBDeployer) modifyListenerCertificate(ctx context.Context, tcLoadbalancerId, tcListenerId, tcCertId string) error { - // 查询监听器列表 - // REF: https://cloud.tencent.com/document/api/214/30686 - describeListenersReq := tcClb.NewDescribeListenersRequest() - describeListenersReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId) - describeListenersReq.ListenerIds = common.StringPtrs([]string{tcListenerId}) - describeListenersResp, err := d.sdkClients.clb.DescribeListeners(describeListenersReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeListeners'") - } - if len(describeListenersResp.Response.Listeners) == 0 { - d.infos = append(d.infos, toStr("未找到监听器", nil)) - return errors.New("listener not found") - } - - d.infos = append(d.infos, toStr("已查询到监听器属性", describeListenersResp.Response)) - - // 修改监听器属性 - // REF: https://cloud.tencent.com/document/product/214/30681 - modifyListenerReq := tcClb.NewModifyListenerRequest() - modifyListenerReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId) - modifyListenerReq.ListenerId = common.StringPtr(tcListenerId) - modifyListenerReq.Certificate = &tcClb.CertificateInput{CertId: common.StringPtr(tcCertId)} - if describeListenersResp.Response.Listeners[0].Certificate != nil && describeListenersResp.Response.Listeners[0].Certificate.SSLMode != nil { - modifyListenerReq.Certificate.SSLMode = describeListenersResp.Response.Listeners[0].Certificate.SSLMode - modifyListenerReq.Certificate.CertCaId = describeListenersResp.Response.Listeners[0].Certificate.CertCaId - } else { - modifyListenerReq.Certificate.SSLMode = common.StringPtr("UNIDIRECTIONAL") - } - modifyListenerResp, err := d.sdkClients.clb.ModifyListener(modifyListenerReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'clb.ModifyListener'") - } - - d.infos = append(d.infos, toStr("已修改监听器属性", modifyListenerResp.Response)) - - return nil -} diff --git a/internal/deployer/tencent_cos.go b/internal/deployer/tencent_cos.go deleted file mode 100644 index bfcd7b7d..00000000 --- a/internal/deployer/tencent_cos.go +++ /dev/null @@ -1,107 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "errors" - "fmt" - - xerrors "github.com/pkg/errors" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" - - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/core/uploader" - uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl" -) - -type TencentCOSDeployer struct { - option *DeployerOption - infos []string - - sdkClient *tcSsl.Client - sslUploader uploader.Uploader -} - -func NewTencentCOSDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.TencentAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - 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") - } - - uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{ - SecretId: access.SecretId, - SecretKey: access.SecretKey, - }) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create ssl uploader") - } - - return &TencentCOSDeployer{ - option: option, - infos: make([]string, 0), - sdkClient: client, - sslUploader: uploader, - }, nil -} - -func (d *TencentCOSDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *TencentCOSDeployer) GetInfos() []string { - return d.infos -} - -func (d *TencentCOSDeployer) Deploy(ctx context.Context) error { - tcRegion := d.option.DeployConfig.GetConfigAsString("region") - tcBucket := d.option.DeployConfig.GetConfigAsString("bucket") - tcDomain := d.option.DeployConfig.GetConfigAsString("domain") - if tcBucket == "" { - return errors.New("`bucket` is required") - } - - // 上传证书到 SSL - 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)) - - // 证书部署到 COS 实例 - // REF: https://cloud.tencent.com/document/product/400/91667 - deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest() - deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) - deployCertificateInstanceReq.ResourceType = common.StringPtr("cos") - deployCertificateInstanceReq.Status = common.Int64Ptr(1) - deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s#%s#%s", tcRegion, tcBucket, tcDomain)}) - deployCertificateInstanceResp, err := d.sdkClient.DeployCertificateInstance(deployCertificateInstanceReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'") - } - - d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response)) - - return nil -} - -func (d *TencentCOSDeployer) createSdkClient(secretId, secretKey, region string) (*tcSsl.Client, error) { - credential := common.NewCredential(secretId, secretKey) - client, err := tcSsl.NewClient(credential, region, profile.NewClientProfile()) - if err != nil { - return nil, err - } - - return client, nil -} diff --git a/internal/deployer/tencent_ecdn.go b/internal/deployer/tencent_ecdn.go deleted file mode 100644 index a61c5680..00000000 --- a/internal/deployer/tencent_ecdn.go +++ /dev/null @@ -1,154 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - xerrors "github.com/pkg/errors" - tcCdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" - - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/core/uploader" - uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl" -) - -type TencentECDNDeployer struct { - option *DeployerOption - infos []string - - sdkClients *tencentECDNDeployerSdkClients - sslUploader uploader.Uploader -} - -type tencentECDNDeployerSdkClients struct { - ssl *tcSsl.Client - cdn *tcCdn.Client -} - -func NewTencentECDNDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.TencentAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - clients, err := (&TencentECDNDeployer{}).createSdkClients( - access.SecretId, - access.SecretKey, - ) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create sdk clients") - } - - uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{ - SecretId: access.SecretId, - SecretKey: access.SecretKey, - }) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create ssl uploader") - } - - return &TencentECDNDeployer{ - option: option, - infos: make([]string, 0), - sdkClients: clients, - sslUploader: uploader, - }, nil -} - -func (d *TencentECDNDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *TencentECDNDeployer) GetInfos() []string { - return d.infos -} - -func (d *TencentECDNDeployer) Deploy(ctx context.Context) error { - // 上传证书到 SSL - 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)) - - // 获取待部署的 ECDN 实例 - // 如果是泛域名,根据证书匹配 ECDN 实例 - aliInstanceIds := make([]string, 0) - domain := d.option.DeployConfig.GetConfigAsString("domain") - if strings.HasPrefix(domain, "*") { - domains, err := d.getDomainsByCertificateId(upres.CertId) - if err != nil { - return err - } - - aliInstanceIds = domains - } else { - aliInstanceIds = append(aliInstanceIds, domain) - } - if len(aliInstanceIds) == 0 { - d.infos = append(d.infos, "没有要部署的 ECDN 实例") - return nil - } - - // 证书部署到 ECDN 实例 - // REF: https://cloud.tencent.com/document/product/400/91667 - deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest() - deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) - deployCertificateInstanceReq.ResourceType = common.StringPtr("ecdn") - deployCertificateInstanceReq.Status = common.Int64Ptr(1) - deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(aliInstanceIds) - deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'") - } - - d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response)) - - return nil -} - -func (d *TencentECDNDeployer) createSdkClients(secretId, secretKey string) (*tencentECDNDeployerSdkClients, error) { - credential := common.NewCredential(secretId, secretKey) - - sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile()) - if err != nil { - return nil, err - } - - cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile()) - if err != nil { - return nil, err - } - - return &tencentECDNDeployerSdkClients{ - ssl: sslClient, - cdn: cdnClient, - }, nil -} - -func (d *TencentECDNDeployer) getDomainsByCertificateId(tcCertId string) ([]string, error) { - // 获取证书中的可用域名 - // REF: https://cloud.tencent.com/document/product/228/42491 - describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest() - describeCertDomainsReq.CertId = common.StringPtr(tcCertId) - describeCertDomainsReq.Product = common.StringPtr("ecdn") - describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq) - if err != nil { - return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'") - } - - domains := make([]string, 0) - if describeCertDomainsResp.Response.Domains == nil { - for _, domain := range describeCertDomainsResp.Response.Domains { - domains = append(domains, *domain) - } - } - - return domains, nil -} diff --git a/internal/deployer/tencent_teo.go b/internal/deployer/tencent_teo.go deleted file mode 100644 index 583089dd..00000000 --- a/internal/deployer/tencent_teo.go +++ /dev/null @@ -1,119 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - xerrors "github.com/pkg/errors" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" - tcTeo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901" - - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/core/uploader" - uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl" -) - -type TencentTEODeployer struct { - option *DeployerOption - infos []string - - sdkClients *tencentTEODeployerSdkClients - sslUploader uploader.Uploader -} - -type tencentTEODeployerSdkClients struct { - ssl *tcSsl.Client - teo *tcTeo.Client -} - -func NewTencentTEODeployer(option *DeployerOption) (Deployer, error) { - access := &domain.TencentAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - - clients, err := (&TencentTEODeployer{}).createSdkClients( - access.SecretId, - access.SecretKey, - ) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create sdk clients") - } - - uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{ - SecretId: access.SecretId, - SecretKey: access.SecretKey, - }) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create ssl uploader") - } - - return &TencentTEODeployer{ - option: option, - infos: make([]string, 0), - sdkClients: clients, - sslUploader: uploader, - }, nil -} - -func (d *TencentTEODeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *TencentTEODeployer) GetInfos() []string { - return d.infos -} - -func (d *TencentTEODeployer) Deploy(ctx context.Context) error { - tcZoneId := d.option.DeployConfig.GetConfigAsString("zoneId") - if tcZoneId == "" { - return xerrors.New("`zoneId` is required") - } - - // 上传证书到 SSL - 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://cloud.tencent.com/document/product/1552/80764 - modifyHostsCertificateReq := tcTeo.NewModifyHostsCertificateRequest() - modifyHostsCertificateReq.ZoneId = common.StringPtr(tcZoneId) - modifyHostsCertificateReq.Mode = common.StringPtr("sslcert") - modifyHostsCertificateReq.Hosts = common.StringPtrs(strings.Split(strings.ReplaceAll(d.option.Domain, "\r\n", "\n"), "\n")) - modifyHostsCertificateReq.ServerCertInfo = []*tcTeo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}} - modifyHostsCertificateResp, err := d.sdkClients.teo.ModifyHostsCertificate(modifyHostsCertificateReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'teo.ModifyHostsCertificate'") - } - - d.infos = append(d.infos, toStr("已配置域名证书", modifyHostsCertificateResp.Response)) - - return nil -} - -func (d *TencentTEODeployer) createSdkClients(secretId, secretKey string) (*tencentTEODeployerSdkClients, error) { - credential := common.NewCredential(secretId, secretKey) - - sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile()) - if err != nil { - return nil, err - } - - teoClient, err := tcTeo.NewClient(credential, "", profile.NewClientProfile()) - if err != nil { - return nil, err - } - - return &tencentTEODeployerSdkClients{ - ssl: sslClient, - teo: teoClient, - }, nil -} diff --git a/internal/deployer/volcengine_cdn.go b/internal/deployer/volcengine_cdn.go deleted file mode 100644 index 6ba8a23d..00000000 --- a/internal/deployer/volcengine_cdn.go +++ /dev/null @@ -1,116 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - volcenginecdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-cdn" - - xerrors "github.com/pkg/errors" - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/core/uploader" - "github.com/volcengine/volc-sdk-golang/service/cdn" -) - -type VolcengineCDNDeployer struct { - option *DeployerOption - infos []string - sdkClient *cdn.CDN - sslUploader uploader.Uploader -} - -func NewVolcengineCDNDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.VolcEngineAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - client := cdn.NewInstance() - client.Client.SetAccessKey(access.AccessKeyId) - client.Client.SetSecretKey(access.SecretAccessKey) - uploader, err := volcenginecdn.New(&volcenginecdn.VolcEngineCDNUploaderConfig{ - AccessKeyId: access.AccessKeyId, - AccessKeySecret: access.SecretAccessKey, - }) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create ssl uploader") - } - return &VolcengineCDNDeployer{ - option: option, - infos: make([]string, 0), - sdkClient: client, - sslUploader: uploader, - }, nil -} - -func (d *VolcengineCDNDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *VolcengineCDNDeployer) GetInfos() []string { - return d.infos -} - -func (d *VolcengineCDNDeployer) Deploy(ctx context.Context) error { - apiCtx := context.Background() - // 上传证书 - upres, err := d.sslUploader.Upload(apiCtx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) - if err != nil { - return err - } - - d.infos = append(d.infos, toStr("已上传证书", upres)) - - domains := make([]string, 0) - configDomain := d.option.DeployConfig.GetConfigAsString("domain") - if strings.HasPrefix(configDomain, "*.") { - // 获取证书可以部署的域名 - // REF: https://www.volcengine.com/docs/6454/125711 - describeCertConfigReq := &cdn.DescribeCertConfigRequest{ - CertId: upres.CertId, - } - describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'") - } - for i := range describeCertConfigResp.Result.CertNotConfig { - // 当前未启用 HTTPS 的加速域名列表。 - domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain) - } - for i := range describeCertConfigResp.Result.OtherCertConfig { - // 已启用了 HTTPS 的加速域名列表。这些加速域名关联的证书不是您指定的证书。 - domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain) - } - for i := range describeCertConfigResp.Result.SpecifiedCertConfig { - // 已启用了 HTTPS 的加速域名列表。这些加速域名关联了您指定的证书。 - d.infos = append(d.infos, fmt.Sprintf("%s域名已配置该证书", describeCertConfigResp.Result.SpecifiedCertConfig[i].Domain)) - } - if len(domains) == 0 { - if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 { - // 所有匹配的域名都配置了该证书,跳过部署 - return nil - } else { - return xerrors.Errorf("未查询到匹配的域名: %s", configDomain) - } - } - } else { - domains = append(domains, configDomain) - } - // 部署证书 - // REF: https://www.volcengine.com/docs/6454/125712 - for i := range domains { - batchDeployCertReq := &cdn.BatchDeployCertRequest{ - CertId: upres.CertId, - Domain: domains[i], - } - batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'cdn.BatchDeployCert'") - } else { - d.infos = append(d.infos, toStr(fmt.Sprintf("%s域名的证书已修改", domains[i]), batchDeployCertResp)) - } - } - - return nil -} diff --git a/internal/deployer/volcengine_live.go b/internal/deployer/volcengine_live.go deleted file mode 100644 index 1795d79f..00000000 --- a/internal/deployer/volcengine_live.go +++ /dev/null @@ -1,148 +0,0 @@ -package deployer - -import ( - "context" - "encoding/json" - "fmt" - "regexp" - "strings" - - xerrors "github.com/pkg/errors" - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/core/uploader" - volcenginelive "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-live" - "github.com/usual2970/certimate/internal/pkg/utils/cast" - "github.com/volcengine/volc-sdk-golang/base" - live "github.com/volcengine/volc-sdk-golang/service/live/v20230101" -) - -type VolcengineLiveDeployer struct { - option *DeployerOption - infos []string - sdkClient *live.Live - sslUploader uploader.Uploader -} - -func NewVolcengineLiveDeployer(option *DeployerOption) (Deployer, error) { - access := &domain.VolcEngineAccess{} - if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, xerrors.Wrap(err, "failed to get access") - } - client := live.NewInstance() - client.SetCredential(base.Credentials{ - AccessKeyID: access.AccessKeyId, - SecretAccessKey: access.SecretAccessKey, - }) - uploader, err := volcenginelive.New(&volcenginelive.VolcEngineLiveUploaderConfig{ - AccessKeyId: access.AccessKeyId, - AccessKeySecret: access.SecretAccessKey, - }) - if err != nil { - return nil, xerrors.Wrap(err, "failed to create ssl uploader") - } - return &VolcengineLiveDeployer{ - option: option, - infos: make([]string, 0), - sdkClient: client, - sslUploader: uploader, - }, nil -} - -func (d *VolcengineLiveDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *VolcengineLiveDeployer) GetInfos() []string { - return d.infos -} - -func (d *VolcengineLiveDeployer) Deploy(ctx context.Context) error { - apiCtx := context.Background() - // 上传证书 - upres, err := d.sslUploader.Upload(apiCtx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) - if err != nil { - return err - } - - d.infos = append(d.infos, toStr("已上传证书", upres)) - - domains := make([]string, 0) - configDomain := d.option.DeployConfig.GetConfigAsString("domain") - if strings.HasPrefix(configDomain, "*.") { - // 如果是泛域名,获取所有的域名并匹配 - matchDomains, err := d.getDomainsByWildcardDomain(apiCtx, configDomain) - if err != nil { - d.infos = append(d.infos, toStr("获取域名列表失败", upres)) - return xerrors.Wrap(err, "failed to execute sdk request 'live.ListDomainDetail'") - } - if len(matchDomains) == 0 { - return xerrors.Errorf("未查询到匹配的域名: %s", configDomain) - } - domains = matchDomains - } else { - domains = append(domains, configDomain) - } - - // 部署证书 - // REF: https://www.volcengine.com/docs/6469/1186278#%E7%BB%91%E5%AE%9A%E8%AF%81%E4%B9%A6d - for i := range domains { - bindCertReq := &live.BindCertBody{ - ChainID: upres.CertId, - Domain: domains[i], - HTTPS: cast.BoolPtr(true), - } - bindCertResp, err := d.sdkClient.BindCert(apiCtx, bindCertReq) - if err != nil { - return xerrors.Wrap(err, "failed to execute sdk request 'live.BindCert'") - } else { - d.infos = append(d.infos, toStr(fmt.Sprintf("%s域名的证书已修改", domains[i]), bindCertResp)) - } - } - - return nil -} - -func (d *VolcengineLiveDeployer) getDomainsByWildcardDomain(ctx context.Context, wildcardDomain string) ([]string, error) { - pageNum := int32(1) - searchTotal := 0 - domains := make([]string, 0) - for { - listDomainDetailReq := &live.ListDomainDetailBody{ - PageNum: pageNum, - PageSize: 1000, - } - // 查询域名列表 - // REF: https://www.volcengine.com/docs/6469/1186277#%E6%9F%A5%E8%AF%A2%E5%9F%9F%E5%90%8D%E5%88%97%E8%A1%A8 - listDomainDetailResp, err := d.sdkClient.ListDomainDetail(ctx, listDomainDetailReq) - if err != nil { - return domains, err - } - if listDomainDetailResp.Result.DomainList != nil { - for _, item := range listDomainDetailResp.Result.DomainList { - if matchWildcardDomain(item.Domain, wildcardDomain) { - domains = append(domains, item.Domain) - } - } - } - searchTotal += len(listDomainDetailResp.Result.DomainList) - if int(listDomainDetailResp.Result.Total) > searchTotal { - pageNum++ - } else { - break - } - } - - return domains, nil -} - -func matchWildcardDomain(domain, wildcardDomain string) bool { - if strings.HasPrefix(wildcardDomain, "*.") { - if "*."+domain == wildcardDomain { - return true - } - regexPattern := "^([a-zA-Z0-9_-]+)\\." + regexp.QuoteMeta(wildcardDomain[2:]) + "$" - regex := regexp.MustCompile(regexPattern) - return regex.MatchString(domain) - } - return domain == wildcardDomain -} diff --git a/internal/deployer/webhook.go b/internal/deployer/webhook.go deleted file mode 100644 index 35c37235..00000000 --- a/internal/deployer/webhook.go +++ /dev/null @@ -1,66 +0,0 @@ -package deployer - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - - xerrors "github.com/pkg/errors" - - "github.com/usual2970/certimate/internal/domain" - xhttp "github.com/usual2970/certimate/internal/utils/http" -) - -type WebhookDeployer struct { - option *DeployerOption - infos []string -} - -func NewWebhookDeployer(option *DeployerOption) (Deployer, error) { - return &WebhookDeployer{ - option: option, - infos: make([]string, 0), - }, nil -} - -func (d *WebhookDeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *WebhookDeployer) GetInfos() []string { - return d.infos -} - -type webhookData struct { - Domain string `json:"domain"` - Certificate string `json:"certificate"` - PrivateKey string `json:"privateKey"` - Variables map[string]string `json:"variables"` -} - -func (d *WebhookDeployer) Deploy(ctx context.Context) error { - access := &domain.WebhookAccess{} - if err := json.Unmarshal([]byte(d.option.Access), access); err != nil { - return xerrors.Wrap(err, "failed to get access") - } - - data := &webhookData{ - Domain: d.option.Domain, - Certificate: d.option.Certificate.Certificate, - PrivateKey: d.option.Certificate.PrivateKey, - Variables: d.option.DeployConfig.GetConfigAsVariables(), - } - body, _ := json.Marshal(data) - resp, err := xhttp.Req(access.Url, http.MethodPost, bytes.NewReader(body), map[string]string{ - "Content-Type": "application/json", - }) - if err != nil { - return xerrors.Wrap(err, "failed to send webhook request") - } - - d.infos = append(d.infos, toStr("Webhook Response", string(resp))) - - return nil -} diff --git a/internal/domain/domains.go b/internal/domain/domains.go index 6a228fff..f3d04a53 100644 --- a/internal/domain/domains.go +++ b/internal/domain/domains.go @@ -23,7 +23,7 @@ type DeployConfig struct { Config map[string]any `json:"config"` } -// 以字符串形式获取配置项。 +// Deprecated: 以字符串形式获取配置项。 // // 入参: // - key: 配置项的键。 @@ -34,7 +34,7 @@ func (dc *DeployConfig) GetConfigAsString(key string) string { return maps.GetValueAsString(dc.Config, key) } -// 以字符串形式获取配置项。 +// Deprecated: 以字符串形式获取配置项。 // // 入参: // - key: 配置项的键。 @@ -46,7 +46,7 @@ func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue stri return maps.GetValueOrDefaultAsString(dc.Config, key, defaultValue) } -// 以 32 位整数形式获取配置项。 +// Deprecated: 以 32 位整数形式获取配置项。 // // 入参: // - key: 配置项的键。 @@ -57,7 +57,7 @@ func (dc *DeployConfig) GetConfigAsInt32(key string) int32 { return maps.GetValueAsInt32(dc.Config, key) } -// 以 32 位整数形式获取配置项。 +// Deprecated: 以 32 位整数形式获取配置项。 // // 入参: // - key: 配置项的键。 @@ -69,7 +69,7 @@ func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32 return maps.GetValueOrDefaultAsInt32(dc.Config, key, defaultValue) } -// 以布尔形式获取配置项。 +// Deprecated: 以布尔形式获取配置项。 // // 入参: // - key: 配置项的键。 @@ -80,7 +80,7 @@ func (dc *DeployConfig) GetConfigAsBool(key string) bool { return maps.GetValueAsBool(dc.Config, key) } -// 以布尔形式获取配置项。 +// Deprecated: 以布尔形式获取配置项。 // // 入参: // - key: 配置项的键。 @@ -92,7 +92,7 @@ func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool) return maps.GetValueOrDefaultAsBool(dc.Config, key, defaultValue) } -// 以变量字典形式获取配置项。 +// Deprecated: 以变量字典形式获取配置项。 // // 出参: // - 变量字典。 @@ -119,7 +119,7 @@ func (dc *DeployConfig) GetConfigAsVariables() map[string]string { return rs } -// GetDomain returns the domain from the deploy config +// Deprecated: GetDomain returns the domain from the deploy config, // if the domain is a wildcard domain, and wildcard is true, return the wildcard domain func (dc *DeployConfig) GetDomain(wildcard ...bool) string { val := dc.GetConfigAsString("domain") diff --git a/internal/domain/notify.go b/internal/domain/notify.go index 6c164f39..e307600c 100644 --- a/internal/domain/notify.go +++ b/internal/domain/notify.go @@ -1,13 +1,20 @@ package domain +/* +消息通知渠道常量值。 + + 注意:如果追加新的常量值,请保持以 ASCII 排序。 + NOTICE: If you add new constant, please keep ASCII order. +*/ const ( - NotifyChannelEmail = "email" - NotifyChannelWebhook = "webhook" - NotifyChannelDingtalk = "dingtalk" - NotifyChannelLark = "lark" - NotifyChannelTelegram = "telegram" - NotifyChannelServerChan = "serverchan" NotifyChannelBark = "bark" + NotifyChannelDingtalk = "dingtalk" + NotifyChannelEmail = "email" + NotifyChannelLark = "lark" + NotifyChannelServerChan = "serverchan" + NotifyChannelTelegram = "telegram" + NotifyChannelWebhook = "webhook" + NotifyChannelWeCom = "wecom" ) type NotifyTestPushReq struct { diff --git a/internal/notify/factory.go b/internal/notify/factory.go index e244b071..b77c7c91 100644 --- a/internal/notify/factory.go +++ b/internal/notify/factory.go @@ -12,11 +12,28 @@ import ( providerServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan" providerTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram" providerWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook" + providerWeCom "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecom" "github.com/usual2970/certimate/internal/pkg/utils/maps" ) func createNotifier(channel string, channelConfig map[string]any) (notifier.Notifier, error) { + /* + 注意:如果追加新的常量值,请保持以 ASCII 排序。 + NOTICE: If you add new constant, please keep ASCII order. + */ switch channel { + case domain.NotifyChannelBark: + return providerBark.New(&providerBark.BarkNotifierConfig{ + DeviceKey: maps.GetValueAsString(channelConfig, "deviceKey"), + ServerUrl: maps.GetValueAsString(channelConfig, "serverUrl"), + }) + + case domain.NotifyChannelDingtalk: + return providerDingTalk.New(&providerDingTalk.DingTalkNotifierConfig{ + AccessToken: maps.GetValueAsString(channelConfig, "accessToken"), + Secret: maps.GetValueAsString(channelConfig, "secret"), + }) + case domain.NotifyChannelEmail: return providerEmail.New(&providerEmail.EmailNotifierConfig{ SmtpHost: maps.GetValueAsString(channelConfig, "smtpHost"), @@ -28,37 +45,30 @@ func createNotifier(channel string, channelConfig map[string]any) (notifier.Noti ReceiverAddress: maps.GetValueAsString(channelConfig, "receiverAddress"), }) - case domain.NotifyChannelWebhook: - return providerWebhook.New(&providerWebhook.WebhookNotifierConfig{ - Url: maps.GetValueAsString(channelConfig, "url"), - }) - - case domain.NotifyChannelDingtalk: - return providerDingTalk.New(&providerDingTalk.DingTalkNotifierConfig{ - AccessToken: maps.GetValueAsString(channelConfig, "accessToken"), - Secret: maps.GetValueAsString(channelConfig, "secret"), - }) - case domain.NotifyChannelLark: return providerLark.New(&providerLark.LarkNotifierConfig{ WebhookUrl: maps.GetValueAsString(channelConfig, "webhookUrl"), }) + case domain.NotifyChannelServerChan: + return providerServerChan.New(&providerServerChan.ServerChanNotifierConfig{ + Url: maps.GetValueAsString(channelConfig, "url"), + }) + case domain.NotifyChannelTelegram: return providerTelegram.New(&providerTelegram.TelegramNotifierConfig{ ApiToken: maps.GetValueAsString(channelConfig, "apiToken"), ChatId: maps.GetValueAsInt64(channelConfig, "chatId"), }) - case domain.NotifyChannelServerChan: - return providerServerChan.New(&providerServerChan.ServerChanNotifierConfig{ + case domain.NotifyChannelWebhook: + return providerWebhook.New(&providerWebhook.WebhookNotifierConfig{ Url: maps.GetValueAsString(channelConfig, "url"), }) - case domain.NotifyChannelBark: - return providerBark.New(&providerBark.BarkNotifierConfig{ - DeviceKey: maps.GetValueAsString(channelConfig, "deviceKey"), - ServerUrl: maps.GetValueAsString(channelConfig, "serverUrl"), + case domain.NotifyChannelWeCom: + return providerWeCom.New(&providerWeCom.WeComNotifierConfig{ + WebhookUrl: maps.GetValueAsString(channelConfig, "webhookUrl"), }) } diff --git a/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go b/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go index 9c7131d1..b305156b 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go @@ -22,7 +22,7 @@ type HuaweiCloudCDNDeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` - // 华为云地域。 + // 华为云区域。 Region string `json:"region"` // 加速域名(不支持泛域名)。 Domain string `json:"domain"` diff --git a/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go b/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go index 7cbc4f12..d3c142f0 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go @@ -27,7 +27,7 @@ type HuaweiCloudELBDeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` - // 华为云地域。 + // 华为云区域。 Region string `json:"region"` // 部署资源类型。 ResourceType DeployResourceType `json:"resourceType"` diff --git a/internal/pkg/core/deployer/providers/tencentcloud-teo/tencentcloud_teo.go b/internal/pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo.go similarity index 85% rename from internal/pkg/core/deployer/providers/tencentcloud-teo/tencentcloud_teo.go rename to internal/pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo.go index 96c301ab..a1de42fa 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-teo/tencentcloud_teo.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo.go @@ -15,7 +15,7 @@ import ( providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl" ) -type TencentCloudTEODeployerConfig struct { +type TencentCloudEODeployerConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 @@ -26,25 +26,25 @@ type TencentCloudTEODeployerConfig struct { Domain string `json:"domain"` } -type TencentCloudTEODeployer struct { - config *TencentCloudTEODeployerConfig +type TencentCloudEODeployer struct { + config *TencentCloudEODeployerConfig logger deployer.Logger sdkClients *wSdkClients sslUploader uploader.Uploader } -var _ deployer.Deployer = (*TencentCloudTEODeployer)(nil) +var _ deployer.Deployer = (*TencentCloudEODeployer)(nil) type wSdkClients struct { ssl *tcSsl.Client teo *tcTeo.Client } -func New(config *TencentCloudTEODeployerConfig) (*TencentCloudTEODeployer, error) { +func New(config *TencentCloudEODeployerConfig) (*TencentCloudEODeployer, error) { return NewWithLogger(config, deployer.NewNilLogger()) } -func NewWithLogger(config *TencentCloudTEODeployerConfig, logger deployer.Logger) (*TencentCloudTEODeployer, error) { +func NewWithLogger(config *TencentCloudEODeployerConfig, logger deployer.Logger) (*TencentCloudEODeployer, error) { if config == nil { return nil, errors.New("config is nil") } @@ -66,7 +66,7 @@ func NewWithLogger(config *TencentCloudTEODeployerConfig, logger deployer.Logger return nil, xerrors.Wrap(err, "failed to create ssl uploader") } - return &TencentCloudTEODeployer{ + return &TencentCloudEODeployer{ logger: logger, config: config, sdkClients: clients, @@ -74,7 +74,7 @@ func NewWithLogger(config *TencentCloudTEODeployerConfig, logger deployer.Logger }, nil } -func (d *TencentCloudTEODeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { +func (d *TencentCloudEODeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { if d.config.ZoneId == "" { return nil, xerrors.New("config `zoneId` is required") } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-teo/tencentcloud_teo_test.go b/internal/pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go similarity index 73% rename from internal/pkg/core/deployer/providers/tencentcloud-teo/tencentcloud_teo_test.go rename to internal/pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go index 8d875e55..a45706ae 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-teo/tencentcloud_teo_test.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-teo" + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-eo" ) var ( @@ -21,7 +21,7 @@ var ( ) func init() { - argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_" + argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDEEO_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") @@ -35,12 +35,12 @@ func init() { Shell command to run this test: go test -v tencentcloud_cdn_test.go -args \ - --CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ - --CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_INPUTKEYPATH="/path/to/your-input-key.pem" \ - --CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_SECRETID="your-secret-id" \ - --CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_SECRETKEY="your-secret-key" \ - --CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_ZONEID="your-zone-id" \ - --CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_DOMAIN="example.com" + --CERTIMATE_DEPLOYER_TENCENTCLOUDEEO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_TENCENTCLOUDEEO_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_TENCENTCLOUDEEO_SECRETID="your-secret-id" \ + --CERTIMATE_DEPLOYER_TENCENTCLOUDEEO_SECRETKEY="your-secret-key" \ + --CERTIMATE_DEPLOYER_TENCENTCLOUDEEO_ZONEID="your-zone-id" \ + --CERTIMATE_DEPLOYER_TENCENTCLOUDEEO_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() @@ -56,7 +56,7 @@ func TestDeploy(t *testing.T) { fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) - deployer, err := provider.New(&provider.TencentCloudTEODeployerConfig{ + deployer, err := provider.New(&provider.TencentCloudEODeployerConfig{ SecretId: fSecretId, SecretKey: fSecretKey, ZoneId: fZoneId, diff --git a/internal/pkg/core/notifier/providers/lark/lark.go b/internal/pkg/core/notifier/providers/lark/lark.go index 0ab94fbb..4714e280 100644 --- a/internal/pkg/core/notifier/providers/lark/lark.go +++ b/internal/pkg/core/notifier/providers/lark/lark.go @@ -10,7 +10,7 @@ import ( ) type LarkNotifierConfig struct { - // 飞书 Webhook 地址。 + // 飞书机器人 Webhook 地址。 WebhookUrl string `json:"webhookUrl"` } diff --git a/internal/pkg/core/notifier/providers/wecom/wecom.go b/internal/pkg/core/notifier/providers/wecom/wecom.go new file mode 100644 index 00000000..20938009 --- /dev/null +++ b/internal/pkg/core/notifier/providers/wecom/wecom.go @@ -0,0 +1,58 @@ +package serverchan + +import ( + "context" + "errors" + "net/http" + + notifyHttp "github.com/nikoksr/notify/service/http" + + "github.com/usual2970/certimate/internal/pkg/core/notifier" +) + +type WeComNotifierConfig struct { + // 企业微信机器人 Webhook 地址。 + WebhookUrl string `json:"webhookUrl"` +} + +type WeComNotifier struct { + config *WeComNotifierConfig +} + +var _ notifier.Notifier = (*WeComNotifier)(nil) + +func New(config *WeComNotifierConfig) (*WeComNotifier, error) { + if config == nil { + return nil, errors.New("config is nil") + } + + return &WeComNotifier{ + config: config, + }, nil +} + +func (n *WeComNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { + srv := notifyHttp.New() + + srv.AddReceivers(¬ifyHttp.Webhook{ + URL: n.config.WebhookUrl, + Header: http.Header{}, + ContentType: "application/json", + Method: http.MethodPost, + BuildPayload: func(subject, message string) (payload any) { + return map[string]any{ + "msgtype": "text", + "text": map[string]string{ + "content": subject + "\n\n" + message, + }, + } + }, + }) + + err = srv.Send(ctx, subject, message) + if err != nil { + return nil, err + } + + return ¬ifier.NotifyResult{}, nil +} diff --git a/internal/pkg/core/notifier/providers/wecom/wecom_test.go b/internal/pkg/core/notifier/providers/wecom/wecom_test.go new file mode 100644 index 00000000..a9ac9d16 --- /dev/null +++ b/internal/pkg/core/notifier/providers/wecom/wecom_test.go @@ -0,0 +1,57 @@ +package serverchan_test + +import ( + "context" + "flag" + "fmt" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecom" +) + +const ( + mockSubject = "test_subject" + mockMessage = "test_message" +) + +var fWebhookUrl string + +func init() { + argsPrefix := "CERTIMATE_NOTIFIER_WECOM_" + + flag.StringVar(&fWebhookUrl, argsPrefix+"WEBHOOKURL", "", "") +} + +/* +Shell command to run this test: + + go test -v serverchan_test.go -args \ + --CERTIMATE_NOTIFIER_WECOM_WEBHOOKURL="https://example.com/your-webhook-url" \ +*/ +func TestNotify(t *testing.T) { + flag.Parse() + + t.Run("Notify", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl), + }, "\n")) + + notifier, err := provider.New(&provider.WeComNotifierConfig{ + WebhookUrl: fWebhookUrl, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + res, err := notifier.Notify(context.Background(), mockSubject, mockMessage) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go b/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go index 42c4747f..d8ea91c6 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go @@ -26,7 +26,7 @@ type HuaweiCloudELBUploaderConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` - // 华为云地域。 + // 华为云区域。 Region string `json:"region"` } diff --git a/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go b/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go index 1c219c79..5662f9b1 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go @@ -22,7 +22,7 @@ type HuaweiCloudSCMUploaderConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` - // 华为云地域。 + // 华为云区域。 Region string `json:"region"` } diff --git a/internal/pkg/vendors/qiniu-sdk/client.go b/internal/pkg/vendors/qiniu-sdk/client.go index 76c07ba9..1f036afd 100644 --- a/internal/pkg/vendors/qiniu-sdk/client.go +++ b/internal/pkg/vendors/qiniu-sdk/client.go @@ -6,10 +6,9 @@ import ( "fmt" "io" "net/http" + "strings" "github.com/qiniu/go-sdk/v7/auth" - - xhttp "github.com/usual2970/certimate/internal/utils/http" ) const qiniuHost = "https://api.qiniu.com" @@ -136,25 +135,29 @@ func (c *Client) UploadSslCert(name, commonName, certificate, privateKey string) } func (c *Client) sendReq(method string, path string, body io.Reader) ([]byte, error) { - req := xhttp.BuildReq(fmt.Sprintf("%s/%s", qiniuHost, path), method, body, map[string]string{ - "Content-Type": "application/json", - }) + path = strings.TrimPrefix(path, "/") + + req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", qiniuHost, path), body) + if err != nil { + return nil, err + } + req.Header.Add("Content-Type", "application/json") if err := c.mac.AddToken(auth.TokenQBox, req); err != nil { return nil, err } - respBody, err := xhttp.ToRequest(req) + 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 } - defer respBody.Close() - - res, err := io.ReadAll(respBody) - if err != nil { - return nil, err - } - - return res, nil + return r, nil } diff --git a/internal/utils/rand/rand.go b/internal/utils/rand/rand.go deleted file mode 100644 index 05fa684e..00000000 --- a/internal/utils/rand/rand.go +++ /dev/null @@ -1,24 +0,0 @@ -package rand - -import ( - "math/rand" - "time" -) - -// Deprecated: this will be removed in the future. -// 随机生成指定长度字符串 -func RandStr(n int) string { - seed := time.Now().UnixNano() - source := rand.NewSource(seed) - random := rand.New(source) - - letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - - // 使用循环生成指定长度的字符串 - result := make([]rune, n) - for i := range result { - result[i] = letters[random.Intn(len(letters))] - } - - return string(result) -} diff --git a/internal/utils/xtime/time.go b/internal/utils/xtime/time.go index 2c074e6c..149321dc 100644 --- a/internal/utils/xtime/time.go +++ b/internal/utils/xtime/time.go @@ -13,9 +13,3 @@ func BeijingTimeStr() string { // 格式化为字符串 return now.Format("2006-01-02 15:04:05") } - -func GetTimeAfter(d time.Duration) string { - t := time.Now().UTC() - - return t.Add(d).Format("2006-01-02 15:04:05") -} diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index 56da6fb3..46c7df9f 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/usual2970/certimate/internal/applicant" "github.com/usual2970/certimate/internal/deployer" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/repository" @@ -68,6 +69,13 @@ func (d *deployNode) Run(ctx context.Context) error { Domain: cert.SAN, Access: access.Config, AccessRecord: access, + Certificate: applicant.Certificate{ + CertUrl: cert.CertUrl, + CertStableUrl: cert.CertStableUrl, + PrivateKey: cert.PrivateKey, + Certificate: cert.Certificate, + IssuerCertificate: cert.IssuerCertificate, + }, DeployConfig: domain.DeployConfig{ Id: d.node.Id, Access: access.Id, diff --git a/migrations/1734398918_updated_access.go b/migrations/1734398918_updated_access.go new file mode 100644 index 00000000..e7e3cd98 --- /dev/null +++ b/migrations/1734398918_updated_access.go @@ -0,0 +1,108 @@ +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/schema" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e") + if err != nil { + return err + } + + // update + edit_configType := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "hwy7m03o", + "name": "configType", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "aliyun", + "acmehttpreq", + "aws", + "baiducloud", + "byteplus", + "cloudflare", + "dogecloud", + "godaddy", + "huaweicloud", + "k8s", + "local", + "namesilo", + "powerdns", + "qiniu", + "ssh", + "tencentcloud", + "volcengine", + "webhook" + ] + } + }`), edit_configType); err != nil { + return err + } + collection.Schema.AddField(edit_configType) + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e") + if err != nil { + return err + } + + // update + edit_configType := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "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", + "volcengine", + "byteplus" + ] + } + }`), edit_configType); err != nil { + return err + } + collection.Schema.AddField(edit_configType) + + return dao.SaveCollection(collection) + }) +} diff --git a/migrations/1734434522_updated_access.go b/migrations/1734434522_updated_access.go new file mode 100644 index 00000000..02c87f24 --- /dev/null +++ b/migrations/1734434522_updated_access.go @@ -0,0 +1,43 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e") + if err != nil { + return err + } + + if err := json.Unmarshal([]byte(`[ + "CREATE INDEX ` + "`" + `idx_wkoST0j` + "`" + ` ON ` + "`" + `access` + "`" + ` (` + "`" + `name` + "`" + `)" + ]`), &collection.Indexes); err != nil { + return err + } + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e") + if err != nil { + return err + } + + if err := json.Unmarshal([]byte(`[ + "CREATE UNIQUE INDEX ` + "`" + `idx_wkoST0j` + "`" + ` ON ` + "`" + `access` + "`" + ` (` + "`" + `name` + "`" + `)" + ]`), &collection.Indexes); err != nil { + return err + } + + return dao.SaveCollection(collection) + }) +} diff --git a/ui/.eslintrc.cjs b/ui/.eslintrc.cjs index 1281ed3a..a2faf5ac 100644 --- a/ui/.eslintrc.cjs +++ b/ui/.eslintrc.cjs @@ -13,6 +13,21 @@ module.exports = { parser: "@typescript-eslint/parser", plugins: ["react-refresh"], rules: { + "@typescript-eslint/no-explicit-any": [ + "warn", + { + ignoreRestArgs: true, + }, + ], + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], "react-refresh/only-export-components": [ "warn", { diff --git a/ui/.gitignore b/ui/.gitignore index 34153986..d603fbd2 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -1,3 +1,8 @@ +node_modules +dist +dist-ssr +!dist/.gitkeep + # Logs logs *.log @@ -6,10 +11,6 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* -./ui/.env -node_modules -dist-ssr -*.local # Editor directories and files .vscode/* @@ -22,4 +23,5 @@ dist-ssr *.sln *.sw? +*.local .env diff --git a/ui/README.md b/ui/README.md deleted file mode 100644 index 85a6989e..00000000 --- a/ui/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - project: ["./tsconfig.json", "./tsconfig.node.json", "./tsconfig.app.json"], - tsconfigRootDir: __dirname, - }, -}; -``` - -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/ui/components.json b/ui/components.json index b67a3e09..3d536d2b 100644 --- a/ui/components.json +++ b/ui/components.json @@ -12,6 +12,6 @@ }, "aliases": { "components": "@/components", - "utils": "@/lib/utils" + "utils": "@/components/ui/utils" } } diff --git a/ui/package-lock.json b/ui/package-lock.json index 2ba9c9c4..e286c84d 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -10,28 +10,18 @@ "dependencies": { "@ant-design/pro-components": "^2.8.2", "@hookform/resolvers": "^3.9.0", - "@radix-ui/react-accordion": "^1.2.0", - "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-popover": "^1.1.2", - "@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.1.1", - "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", - "@radix-ui/react-switch": "^1.1.0", - "@radix-ui/react-tabs": "^1.1.0", - "@radix-ui/react-toast": "^1.2.1", - "@radix-ui/react-tooltip": "^1.1.2", "@tanstack/react-table": "^8.20.5", "ahooks": "^3.8.4", "antd": "^5.22.2", "antd-zod": "^6.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", - "cmdk": "^1.0.0", "cron-parser": "^4.9.0", "i18next": "^23.15.1", "i18next-browser-languagedetector": "^8.0.0", @@ -60,6 +50,7 @@ "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.15.0", "@typescript-eslint/parser": "^7.15.0", + "@vitejs/plugin-legacy": "^5.4.3", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.19", "eslint": "^8.57.0", @@ -470,12 +461,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.26.2", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "dependencies": { - "@babel/highlight": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { @@ -483,30 +475,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.9", - "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.24.9.tgz", - "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==", + "version": "7.26.3", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.26.3.tgz", + "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.9", - "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.24.9.tgz", - "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", + "version": "7.26.0", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.9", - "@babel/helper-compilation-targets": "^7.24.8", - "@babel/helper-module-transforms": "^7.24.9", - "@babel/helpers": "^7.24.8", - "@babel/parser": "^7.24.8", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.8", - "@babel/types": "^7.24.9", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -531,29 +523,42 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.10", - "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.24.10.tgz", - "integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==", + "version": "7.26.3", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.26.3.tgz", + "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", "dev": true, "dependencies": { - "@babel/types": "^7.24.9", + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.8", - "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", - "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.24.8", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -570,67 +575,19 @@ "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", "dev": true, "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dev": true, - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.24.9", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz", - "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -639,100 +596,230 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "engines": { - "node": ">=6.9.0" + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.26.3", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", "dev": true, "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", + "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dev": true, "dependencies": { - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", + "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "dev": true, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helpers": { - "version": "7.24.8", - "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.24.8.tgz", - "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", "dev": true, "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.8" + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmmirror.com/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.24.8", - "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.24.8.tgz", - "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", + "version": "7.26.3", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", "dev": true, + "dependencies": { + "@babel/types": "^7.26.3" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -740,6 +827,744 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", + "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", + "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", + "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.26.0", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", + "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", + "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.26.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", + "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", + "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", + "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.26.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", + "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", + "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", + "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", + "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", + "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.24.7", "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", @@ -770,6 +1595,298 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.26.0", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", + "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", + "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", + "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", + "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", + "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.26.0", + "resolved": "https://registry.npmmirror.com/@babel/preset-env/-/preset-env-7.26.0.tgz", + "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.25.9", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.25.9", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.25.9", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.25.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.25.9", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.25.9", + "@babel/plugin-transform-typeof-symbol": "^7.25.9", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.38.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmmirror.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.26.0", "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.26.0.tgz", @@ -782,33 +1899,30 @@ } }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.25.9", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.8", - "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.24.8.tgz", - "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", + "version": "7.26.4", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.26.4.tgz", + "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.8", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.8", - "@babel/types": "^7.24.8", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.3", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -817,14 +1931,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.9", - "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.24.9.tgz", - "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==", + "version": "7.26.3", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1548,6 +2661,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -1625,65 +2749,6 @@ "resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.0.tgz", "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" }, - "node_modules/@radix-ui/react-accordion": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-accordion/-/react-accordion-1.2.0.tgz", - "integrity": "sha512-HJOzSX8dQqtsp/3jVxCU3CXEONF7/2jlGAB28oX8TTw1Dz8JYbEI1UcL8355PuLBE41/IRRMvCw7VkiK/jcUOQ==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-collapsible": "1.1.0", - "@radix-ui/react-collection": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collapsible": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz", - "integrity": "sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", @@ -1706,72 +2771,6 @@ } } }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.1.tgz", - "integrity": "sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-presence": "1.1.1", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.1.tgz", - "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", - "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-collection": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", @@ -2168,166 +3167,6 @@ } } }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-popover/-/react-popover-1.1.2.tgz", - "integrity": "sha512-u2HRUyWW+lOiA2g0Le0tMmT55FGOEWHwPFt1EPfbLly7uXQExFo5duNKqG2DzmFXIdqOeNd+TpE8baHWJCyP9w==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.1", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.0", - "@radix-ui/react-portal": "1.1.2", - "@radix-ui/react-presence": "1.1.1", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.6.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.1.tgz", - "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", - "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", - "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", - "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", - "dependencies": { - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", - "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/react-remove-scroll": { - "version": "2.6.0", - "resolved": "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", - "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==", - "dependencies": { - "react-remove-scroll-bar": "^2.3.6", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-popper": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", @@ -2427,37 +3266,6 @@ } } }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-radio-group/-/react-radio-group-1.2.0.tgz", - "integrity": "sha512-yv+oiLaicYMBpqgfpSPw6q+RyXlLdIpQWDHZbUKURxe+nEh53hFXPPlfhfQQtYkS5MMK/5IWIa76SksleQZSzw==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-roving-focus": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-use-size": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", @@ -2560,28 +3368,6 @@ } } }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-separator/-/react-separator-1.1.0.tgz", - "integrity": "sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==", - "dependencies": { - "@radix-ui/react-primitive": "2.0.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-slot": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", @@ -2599,129 +3385,6 @@ } } }, - "node_modules/@radix-ui/react-switch": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.1.0.tgz", - "integrity": "sha512-OBzy5WAj641k0AOSpKQtreDMe+isX0MQJ1IVyF03ucdF3DunOnROVrjWs8zsXUxC3zfZ6JL9HFVCUlMghz9dJw==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-use-size": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.0.tgz", - "integrity": "sha512-bZgOKB/LtZIij75FSuPzyEti/XBhJH52ExgtdVqjCIh+Nx/FW+LhnbXtbCzIi34ccyMsyOja8T0thCzoHFXNKA==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-roving-focus": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-toast/-/react-toast-1.2.1.tgz", - "integrity": "sha512-5trl7piMXcZiCq7MW6r8YYmu0bK5qDpTWz+FdEPdKyft2UixkspheYbjbrLXVN5NGKHFbOP7lm8eD0biiSqZqg==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-collection": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-tooltip/-/react-tooltip-1.1.2.tgz", - "integrity": "sha512-9XRsLwe6Yb9B/tlnYCPVUd/TFS4J7HuOZW345DCeC6vKIxQGMZdx21RK4VoZauPD5frgkXTYVS5y90L+3YBn4w==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -3559,6 +4222,32 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vitejs/plugin-legacy": { + "version": "5.4.3", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-legacy/-/plugin-legacy-5.4.3.tgz", + "integrity": "sha512-wsyXK9mascyplcqvww1gA1xYiy29iRHfyciw+a0t7qRNdzX6PdfSWmOoCi74epr87DujM+5J+rnnSv+4PazqVg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.25.8", + "@babel/preset-env": "^7.25.8", + "browserslist": "^4.24.0", + "browserslist-to-esbuild": "^2.1.1", + "core-js": "^3.38.1", + "magic-string": "^0.30.12", + "regenerator-runtime": "^0.14.1", + "systemjs": "^6.15.1" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "peerDependencies": { + "terser": "^5.4.0", + "vite": "^5.0.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.3.1", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", @@ -3653,18 +4342,6 @@ "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/antd": { "version": "5.22.2", "resolved": "https://registry.npmmirror.com/antd/-/antd-5.22.2.tgz", @@ -3826,6 +4503,54 @@ "postcss": "^8.1.0" } }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.12", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", + "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.3", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.6", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", + "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3862,9 +4587,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.2", - "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.23.2.tgz", - "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "version": "4.24.3", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.24.3.tgz", + "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", "dev": true, "funding": [ { @@ -3881,10 +4606,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001640", - "electron-to-chromium": "^1.4.820", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.1.0" + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -3893,6 +4618,31 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/browserslist-to-esbuild": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/browserslist-to-esbuild/-/browserslist-to-esbuild-2.1.1.tgz", + "integrity": "sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==", + "dev": true, + "dependencies": { + "meow": "^13.0.0" + }, + "bin": { + "browserslist-to-esbuild": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "browserslist": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "peer": true + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", @@ -3911,9 +4661,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001643", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz", - "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==", + "version": "1.0.30001689", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz", + "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==", "dev": true, "funding": [ { @@ -3930,20 +4680,6 @@ } ] }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", @@ -4015,381 +4751,6 @@ "node": ">=6" } }, - "node_modules/cmdk": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/cmdk/-/cmdk-1.0.0.tgz", - "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", - "dependencies": { - "@radix-ui/react-dialog": "1.0.5", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.0.1.tgz", - "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-dialog": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", - "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", - "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", - "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", - "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.0.1.tgz", - "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-portal": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", - "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-presence": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", - "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", - "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", - "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/react-remove-scroll": { - "version": "2.5.5", - "resolved": "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", - "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", - "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", @@ -4423,6 +4784,30 @@ "toggle-selection": "^1.0.6" } }, + "node_modules/core-js": { + "version": "3.39.0", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.39.0", + "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.39.0.tgz", + "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", + "dev": true, + "dependencies": { + "browserslist": "^4.24.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", @@ -4549,9 +4934,9 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/electron-to-chromium": { - "version": "1.5.2", - "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz", - "integrity": "sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==", + "version": "1.5.74", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz", + "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==", "dev": true }, "node_modules/emoji-regex": { @@ -4598,23 +4983,14 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eslint": { "version": "8.57.0", "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.0.tgz", @@ -5237,15 +5613,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", @@ -5507,15 +5874,15 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -5647,6 +6014,12 @@ "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5688,6 +6061,27 @@ "node": ">=12" } }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmmirror.com/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", @@ -5799,9 +6193,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.19", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true }, "node_modules/normalize-path": { @@ -7077,11 +7471,85 @@ "node": ">=8.10.0" } }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmmirror.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", @@ -7295,6 +7763,16 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7303,6 +7781,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", @@ -7456,18 +7945,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -7507,6 +7984,12 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/systemjs": { + "version": "6.15.1", + "resolved": "https://registry.npmmirror.com/systemjs/-/systemjs-6.15.1.tgz", + "integrity": "sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==", + "dev": true + }, "node_modules/tailwind-merge": { "version": "2.4.0", "resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.4.0.tgz", @@ -7560,6 +8043,32 @@ "tailwindcss": ">=3.0.0 || insiders" } }, + "node_modules/terser": { + "version": "5.37.0", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "peer": true + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", @@ -7598,15 +8107,6 @@ "resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz", "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7693,6 +8193,46 @@ "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==", "dev": true }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", @@ -7703,9 +8243,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -7722,8 +8262,8 @@ } ], "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" diff --git a/ui/package.json b/ui/package.json index 986fdd6a..a2feb4ee 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,28 +12,18 @@ "dependencies": { "@ant-design/pro-components": "^2.8.2", "@hookform/resolvers": "^3.9.0", - "@radix-ui/react-accordion": "^1.2.0", - "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-popover": "^1.1.2", - "@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.1.1", - "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", - "@radix-ui/react-switch": "^1.1.0", - "@radix-ui/react-tabs": "^1.1.0", - "@radix-ui/react-toast": "^1.2.1", - "@radix-ui/react-tooltip": "^1.1.2", "@tanstack/react-table": "^8.20.5", "ahooks": "^3.8.4", "antd": "^5.22.2", "antd-zod": "^6.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", - "cmdk": "^1.0.0", "cron-parser": "^4.9.0", "i18next": "^23.15.1", "i18next-browser-languagedetector": "^8.0.0", @@ -62,6 +52,7 @@ "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.15.0", "@typescript-eslint/parser": "^7.15.0", + "@vitejs/plugin-legacy": "^5.4.3", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.19", "eslint": "^8.57.0", diff --git a/ui/public/imgs/providers/google.svg b/ui/public/imgs/acme/google.svg similarity index 99% rename from ui/public/imgs/providers/google.svg rename to ui/public/imgs/acme/google.svg index 120a7921..78f81c93 100644 --- a/ui/public/imgs/providers/google.svg +++ b/ui/public/imgs/acme/google.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/letsencrypt.svg b/ui/public/imgs/acme/letsencrypt.svg similarity index 100% rename from ui/public/imgs/providers/letsencrypt.svg rename to ui/public/imgs/acme/letsencrypt.svg diff --git a/ui/public/imgs/acme/zerossl.svg b/ui/public/imgs/acme/zerossl.svg new file mode 100644 index 00000000..e4c2ac63 --- /dev/null +++ b/ui/public/imgs/acme/zerossl.svg @@ -0,0 +1 @@ + diff --git a/ui/public/imgs/providers/httpreq.svg b/ui/public/imgs/providers/acmehttpreq.svg similarity index 100% rename from ui/public/imgs/providers/httpreq.svg rename to ui/public/imgs/providers/acmehttpreq.svg diff --git a/ui/public/imgs/providers/k8s.svg b/ui/public/imgs/providers/kubernetes.svg similarity index 100% rename from ui/public/imgs/providers/k8s.svg rename to ui/public/imgs/providers/kubernetes.svg diff --git a/ui/public/imgs/providers/pdns.svg b/ui/public/imgs/providers/powerdns.svg similarity index 100% rename from ui/public/imgs/providers/pdns.svg rename to ui/public/imgs/providers/powerdns.svg diff --git a/ui/public/imgs/providers/tencent.svg b/ui/public/imgs/providers/tencentcloud.svg similarity index 100% rename from ui/public/imgs/providers/tencent.svg rename to ui/public/imgs/providers/tencentcloud.svg diff --git a/ui/public/imgs/providers/zerossl.svg b/ui/public/imgs/providers/zerossl.svg deleted file mode 100644 index 8563aece..00000000 --- a/ui/public/imgs/providers/zerossl.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/ui/src/App.tsx b/ui/src/App.tsx index d1c9cf2d..481631ff 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useState } from "react"; +import { useEffect, useLayoutEffect, useMemo, useState } from "react"; import { RouterProvider } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { App, ConfigProvider, theme, type ThemeConfig } from "antd"; @@ -9,30 +9,36 @@ import dayjs from "dayjs"; import "dayjs/locale/zh-cn"; import { localeNames } from "./i18n"; -import { useTheme } from "./hooks"; +import { useBrowserTheme } from "./hooks"; import { router } from "./router.tsx"; const RootApp = () => { const { i18n } = useTranslation(); - const { theme: browserTheme } = useTheme(); + const { theme: browserTheme } = useBrowserTheme(); - const antdLocalesMap: Record = { - [localeNames.ZH]: AntdLocaleZhCN, - [localeNames.EN]: AntdLocaleEnUs, - }; + const antdLocalesMap: Record = useMemo( + () => ({ + [localeNames.ZH]: AntdLocaleZhCN, + [localeNames.EN]: AntdLocaleEnUs, + }), + [] + ); const [antdLocale, setAntdLocale] = useState(antdLocalesMap[i18n.language]); const handleLanguageChanged = () => { setAntdLocale(antdLocalesMap[i18n.language]); dayjs.locale(i18n.language); }; i18n.on("languageChanged", handleLanguageChanged); - useLayoutEffect(handleLanguageChanged, [i18n]); + useLayoutEffect(handleLanguageChanged, [antdLocalesMap, i18n]); - const antdThemesMap: Record = { - ["light"]: { algorithm: theme.defaultAlgorithm }, - ["dark"]: { algorithm: theme.darkAlgorithm }, - }; + const antdThemesMap: Record = useMemo( + () => ({ + ["light"]: { algorithm: theme.defaultAlgorithm }, + ["dark"]: { algorithm: theme.darkAlgorithm }, + }), + [] + ); const [antdTheme, setAntdTheme] = useState(antdThemesMap[browserTheme]); useEffect(() => { setAntdTheme(antdThemesMap[browserTheme]); @@ -40,7 +46,7 @@ const RootApp = () => { const root = window.document.documentElement; root.classList.remove("light", "dark"); root.classList.add(browserTheme); - }, [browserTheme]); + }, [antdThemesMap, browserTheme]); return ( { +const Version = ({ className, style }: VersionProps) => { const { t } = useTranslation(); return ( - +
diff --git a/ui/src/components/access/AccessEditForm.tsx b/ui/src/components/access/AccessEditForm.tsx new file mode 100644 index 00000000..af10775e --- /dev/null +++ b/ui/src/components/access/AccessEditForm.tsx @@ -0,0 +1,174 @@ +import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useCreation, useDeepCompareEffect } from "ahooks"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { ACCESS_PROVIDER_TYPES, type AccessModel } from "@/domain/access"; +import AccessTypeSelect from "./AccessTypeSelect"; +import AccessEditFormACMEHttpReqConfig from "./AccessEditFormACMEHttpReqConfig"; +import AccessEditFormAliyunConfig from "./AccessEditFormAliyunConfig"; +import AccessEditFormAWSConfig from "./AccessEditFormAWSConfig"; +import AccessEditFormBaiduCloudConfig from "./AccessEditFormBaiduCloudConfig"; +import AccessEditFormBytePlusConfig from "./AccessEditFormBytePlusConfig"; +import AccessEditFormCloudflareConfig from "./AccessEditFormCloudflareConfig"; +import AccessEditFormDogeCloudConfig from "./AccessEditFormDogeCloudConfig"; +import AccessEditFormGoDaddyConfig from "./AccessEditFormGoDaddyConfig"; +import AccessEditFormHuaweiCloudConfig from "./AccessEditFormHuaweiCloudConfig"; +import AccessEditFormKubernetesConfig from "./AccessEditFormKubernetesConfig"; +import AccessEditFormLocalConfig from "./AccessEditFormLocalConfig"; +import AccessEditFormNameSiloConfig from "./AccessEditFormNameSiloConfig"; +import AccessEditFormPowerDNSConfig from "./AccessEditFormPowerDNSConfig"; +import AccessEditFormQiniuConfig from "./AccessEditFormQiniuConfig"; +import AccessEditFormSSHConfig from "./AccessEditFormSSHConfig"; +import AccessEditFormTencentCloudConfig from "./AccessEditFormTencentCloudConfig"; +import AccessEditFormVolcEngineConfig from "./AccessEditFormVolcEngineConfig"; +import AccessEditFormWebhookConfig from "./AccessEditFormWebhookConfig"; + +type AccessEditFormModelType = Partial>; +type AccessEditFormModes = "add" | "edit"; + +export type AccessEditFormProps = { + className?: string; + style?: React.CSSProperties; + disabled?: boolean; + loading?: boolean; + mode: AccessEditFormModes; + model?: AccessEditFormModelType; + onModelChange?: (model: AccessEditFormModelType) => void; +}; + +export type AccessEditFormInstance = { + getFieldsValue: () => AccessEditFormModelType; + resetFields: () => void; + validateFields: () => Promise; +}; + +const AccessEditForm = forwardRef(({ className, style, disabled, loading, mode, model, onModelChange }, ref) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + name: z + .string({ message: t("access.form.name.placeholder") }) + .trim() + .min(1, t("access.form.name.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + configType: z.nativeEnum(ACCESS_PROVIDER_TYPES, { message: t("access.form.type.placeholder") }), + config: z.any(), + }); + const formRule = createSchemaFieldRule(formSchema); + const [form] = Form.useForm>(); + + const [initialValues, setInitialValues] = useState>>(model as Partial>); + useDeepCompareEffect(() => { + setInitialValues(model as Partial>); + }, [model]); + + const [configType, setConfigType] = useState(model?.configType); + useEffect(() => { + setConfigType(model?.configType); + }, [model?.configType]); + + const [configFormInst] = Form.useForm(); + const configFormName = useCreation(() => `accessEditForm_config${Math.random().toString(36).substring(2, 10)}${new Date().getTime()}`, []); + const configFormComponent = useMemo(() => { + /* + 注意:如果追加新的子组件,请保持以 ASCII 排序。 + NOTICE: If you add new child component, please keep ASCII order. + */ + const configFormProps = { form: configFormInst, formName: configFormName, disabled: disabled, loading: loading, model: model?.config }; + switch (configType) { + case ACCESS_PROVIDER_TYPES.ACMEHTTPREQ: + return ; + case ACCESS_PROVIDER_TYPES.ALIYUN: + return ; + case ACCESS_PROVIDER_TYPES.AWS: + return ; + case ACCESS_PROVIDER_TYPES.BAIDUCLOUD: + return ; + case ACCESS_PROVIDER_TYPES.BYTEPLUS: + return ; + case ACCESS_PROVIDER_TYPES.CLOUDFLARE: + return ; + case ACCESS_PROVIDER_TYPES.DOGECLOUD: + return ; + case ACCESS_PROVIDER_TYPES.GODADDY: + return ; + case ACCESS_PROVIDER_TYPES.HUAWEICLOUD: + return ; + case ACCESS_PROVIDER_TYPES.KUBERNETES: + return ; + case ACCESS_PROVIDER_TYPES.LOCAL: + return ; + case ACCESS_PROVIDER_TYPES.NAMESILO: + return ; + case ACCESS_PROVIDER_TYPES.POWERDNS: + return ; + case ACCESS_PROVIDER_TYPES.QINIU: + return ; + case ACCESS_PROVIDER_TYPES.SSH: + return ; + case ACCESS_PROVIDER_TYPES.TENCENTCLOUD: + return ; + case ACCESS_PROVIDER_TYPES.VOLCENGINE: + return ; + case ACCESS_PROVIDER_TYPES.WEBHOOK: + return ; + } + }, [model, configType, configFormInst]); + + const handleFormProviderChange = (name: string) => { + if (name === configFormName) { + form.setFieldValue("config", configFormInst.getFieldsValue()); + onModelChange?.(form.getFieldsValue(true)); + } + }; + + const handleFormChange = (_: unknown, fields: AccessEditFormModelType) => { + if (fields.configType !== configType) { + setConfigType(fields.configType); + } + + onModelChange?.(fields); + }; + + useImperativeHandle(ref, () => ({ + getFieldsValue: () => { + return form.getFieldsValue(true); + }, + resetFields: () => { + return form.resetFields(); + }, + validateFields: () => { + const t1 = form.validateFields(); + const t2 = configFormInst.validateFields(); + return Promise.all([t1, t2]).then(() => t1); + }, + })); + + return ( + +
+
+ + + + + } + > + + +
+ + {configFormComponent} +
+
+ ); +}); + +export default AccessEditForm; diff --git a/ui/src/components/access/AccessEditFormACMEHttpReqConfig.tsx b/ui/src/components/access/AccessEditFormACMEHttpReqConfig.tsx new file mode 100644 index 00000000..d52e67c2 --- /dev/null +++ b/ui/src/components/access/AccessEditFormACMEHttpReqConfig.tsx @@ -0,0 +1,107 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, Select, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type ACMEHttpReqAccessConfig } from "@/domain/access"; + +type AccessEditFormACMEHttpReqConfigModelType = Partial; + +export type AccessEditFormACMEHttpReqConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormACMEHttpReqConfigModelType; + onModelChange?: (model: AccessEditFormACMEHttpReqConfigModelType) => void; +}; + +const initModel = () => { + return { + endpoint: "https://example.com/api/", + mode: "", + username: "", + password: "", + } as AccessEditFormACMEHttpReqConfigModelType; +}; + +const AccessEditFormACMEHttpReqConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormACMEHttpReqConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + endpoint: z.string().url(t("common.errmsg.url_invalid")), + mode: z.string().min(0, t("access.form.acmehttpreq_mode.placeholder")).nullish(), + username: z + .string() + .trim() + .min(0, t("access.form.acmehttpreq_username.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })) + .nullish(), + password: z + .string() + .trim() + .min(0, t("access.form.acmehttpreq_password.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })) + .nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormACMEHttpReqConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormACMEHttpReqConfig; diff --git a/ui/src/components/access/AccessEditFormAWSConfig.tsx b/ui/src/components/access/AccessEditFormAWSConfig.tsx new file mode 100644 index 00000000..e2973cce --- /dev/null +++ b/ui/src/components/access/AccessEditFormAWSConfig.tsx @@ -0,0 +1,111 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AWSAccessConfig } from "@/domain/access"; + +type AccessEditFormAWSConfigModelType = Partial; + +export type AccessEditFormAWSConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormAWSConfigModelType; + onModelChange?: (model: AccessEditFormAWSConfigModelType) => void; +}; + +const initModel = () => { + return { + accessKeyId: "", + secretAccessKey: "", + region: "us-east-1", + hostedZoneId: "", + } as AccessEditFormAWSConfigModelType; +}; + +const AccessEditFormAWSConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormAWSConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKeyId: z + .string() + .trim() + .min(1, t("access.form.aws_access_key_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretAccessKey: z + .string() + .trim() + .min(1, t("access.form.aws_secret_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + // TODO: 该字段仅用于申请证书,后续迁移到工作流表单中 + region: z + .string() + .trim() + .min(0, t("access.form.aws_region.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .nullish(), + // TODO: 该字段仅用于申请证书,后续迁移到工作流表单中 + hostedZoneId: z + .string() + .trim() + .min(0, t("access.form.aws_hosted_zone_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormAWSConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + + + } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormAWSConfig; diff --git a/ui/src/components/access/AccessEditFormAliyunConfig.tsx b/ui/src/components/access/AccessEditFormAliyunConfig.tsx new file mode 100644 index 00000000..229f2f30 --- /dev/null +++ b/ui/src/components/access/AccessEditFormAliyunConfig.tsx @@ -0,0 +1,77 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AliyunAccessConfig } from "@/domain/access"; + +type AccessEditFormAliyunConfigModelType = Partial; + +export type AccessEditFormAliyunConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormAliyunConfigModelType; + onModelChange?: (model: AccessEditFormAliyunConfigModelType) => void; +}; + +const initModel = () => { + return { + accessKeyId: "", + accessKeySecret: "", + } as AccessEditFormAliyunConfigModelType; +}; + +const AccessEditFormAliyunConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormAliyunConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKeyId: z + .string() + .trim() + .min(1, t("access.form.aliyun_access_key_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + accessKeySecret: z + .string() + .trim() + .min(1, t("access.form.aliyun_access_key_secret.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormAliyunConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormAliyunConfig; diff --git a/ui/src/components/access/AccessEditFormBaiduCloudConfig.tsx b/ui/src/components/access/AccessEditFormBaiduCloudConfig.tsx new file mode 100644 index 00000000..d4be8c31 --- /dev/null +++ b/ui/src/components/access/AccessEditFormBaiduCloudConfig.tsx @@ -0,0 +1,77 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type BaiduCloudAccessConfig } from "@/domain/access"; + +type AccessEditFormBaiduCloudConfigModelType = Partial; + +export type AccessEditFormBaiduCloudConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormBaiduCloudConfigModelType; + onModelChange?: (model: AccessEditFormBaiduCloudConfigModelType) => void; +}; + +const initModel = () => { + return { + accessKeyId: "", + secretAccessKey: "", + } as AccessEditFormBaiduCloudConfigModelType; +}; + +const AccessEditFormBaiduCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormBaiduCloudConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKeyId: z + .string() + .trim() + .min(1, t("access.form.baiducloud_access_key_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretAccessKey: z + .string() + .trim() + .min(1, t("access.form.baiducloud_secret_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormBaiduCloudConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormBaiduCloudConfig; diff --git a/ui/src/components/access/AccessEditFormBytePlusConfig.tsx b/ui/src/components/access/AccessEditFormBytePlusConfig.tsx new file mode 100644 index 00000000..a8b08635 --- /dev/null +++ b/ui/src/components/access/AccessEditFormBytePlusConfig.tsx @@ -0,0 +1,77 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type BytePlusAccessConfig } from "@/domain/access"; + +type AccessEditFormBytePlusConfigModelType = Partial; + +export type AccessEditFormBytePlusConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormBytePlusConfigModelType; + onModelChange?: (model: AccessEditFormBytePlusConfigModelType) => void; +}; + +const initModel = () => { + return { + accessKey: "", + secretKey: "", + } as AccessEditFormBytePlusConfigModelType; +}; + +const AccessEditFormBytePlusConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormBytePlusConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKey: z + .string() + .trim() + .min(1, t("access.form.byteplus_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretKey: z + .string() + .trim() + .min(1, t("access.form.byteplus_secret_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormBytePlusConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormBytePlusConfig; diff --git a/ui/src/components/access/AccessEditFormCloudflareConfig.tsx b/ui/src/components/access/AccessEditFormCloudflareConfig.tsx new file mode 100644 index 00000000..b6cb2e32 --- /dev/null +++ b/ui/src/components/access/AccessEditFormCloudflareConfig.tsx @@ -0,0 +1,62 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type CloudflareAccessConfig } from "@/domain/access"; + +type AccessEditFormCloudflareConfigModelType = Partial; + +export type AccessEditFormCloudflareConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormCloudflareConfigModelType; + onModelChange?: (model: AccessEditFormCloudflareConfigModelType) => void; +}; + +const initModel = () => { + return { + dnsApiToken: "", + } as AccessEditFormCloudflareConfigModelType; +}; + +const AccessEditFormCloudflareConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormCloudflareConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + dnsApiToken: z + .string() + .trim() + .min(1, t("access.form.cloudflare_dns_api_token.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormCloudflareConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + +
+ ); +}; + +export default AccessEditFormCloudflareConfig; diff --git a/ui/src/components/access/AccessEditFormDogeCloudConfig.tsx b/ui/src/components/access/AccessEditFormDogeCloudConfig.tsx new file mode 100644 index 00000000..c5f3502b --- /dev/null +++ b/ui/src/components/access/AccessEditFormDogeCloudConfig.tsx @@ -0,0 +1,77 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type DogeCloudAccessConfig } from "@/domain/access"; + +type AccessEditFormDogeCloudConfigModelType = Partial; + +export type AccessEditFormDogeCloudConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormDogeCloudConfigModelType; + onModelChange?: (model: AccessEditFormDogeCloudConfigModelType) => void; +}; + +const initModel = () => { + return { + accessKey: "", + secretKey: "", + } as AccessEditFormDogeCloudConfigModelType; +}; + +const AccessEditFormDogeCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormDogeCloudConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKey: z + .string() + .trim() + .min(1, t("access.form.dogecloud_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretKey: z + .string() + .trim() + .min(1, t("access.form.dogecloud_secret_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormDogeCloudConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormDogeCloudConfig; diff --git a/ui/src/components/access/AccessEditFormGoDaddyConfig.tsx b/ui/src/components/access/AccessEditFormGoDaddyConfig.tsx new file mode 100644 index 00000000..349ce453 --- /dev/null +++ b/ui/src/components/access/AccessEditFormGoDaddyConfig.tsx @@ -0,0 +1,77 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type GoDaddyAccessConfig } from "@/domain/access"; + +type AccessEditFormGoDaddyConfigModelType = Partial; + +export type AccessEditFormGoDaddyConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormGoDaddyConfigModelType; + onModelChange?: (model: AccessEditFormGoDaddyConfigModelType) => void; +}; + +const initModel = () => { + return { + apiKey: "", + apiSecret: "", + } as AccessEditFormGoDaddyConfigModelType; +}; + +const AccessEditFormGoDaddyConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormGoDaddyConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiKey: z + .string() + .trim() + .min(1, t("access.form.godaddy_api_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + apiSecret: z + .string() + .trim() + .min(1, t("access.form.godaddy_api_secret.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormGoDaddyConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormGoDaddyConfig; diff --git a/ui/src/components/access/AccessEditFormHuaweiCloudConfig.tsx b/ui/src/components/access/AccessEditFormHuaweiCloudConfig.tsx new file mode 100644 index 00000000..0e4556e4 --- /dev/null +++ b/ui/src/components/access/AccessEditFormHuaweiCloudConfig.tsx @@ -0,0 +1,94 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type HuaweiCloudAccessConfig } from "@/domain/access"; + +type AccessEditFormHuaweiCloudConfigModelType = Partial; + +export type AccessEditFormHuaweiCloudConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormHuaweiCloudConfigModelType; + onModelChange?: (model: AccessEditFormHuaweiCloudConfigModelType) => void; +}; + +const initModel = () => { + return { + accessKeyId: "", + secretAccessKey: "", + region: "cn-north-1", + } as AccessEditFormHuaweiCloudConfigModelType; +}; + +const AccessEditFormHuaweiCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormHuaweiCloudConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKeyId: z + .string() + .trim() + .min(1, t("access.form.huaweicloud_access_key_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretAccessKey: z + .string() + .trim() + .min(1, t("access.form.huaweicloud_secret_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + // TODO: 该字段仅用于申请证书,后续迁移到工作流表单中 + region: z + .string() + .trim() + .min(0, t("access.form.huaweicloud_region.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormHuaweiCloudConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormHuaweiCloudConfig; diff --git a/ui/src/components/access/AccessEditFormKubernetesConfig.tsx b/ui/src/components/access/AccessEditFormKubernetesConfig.tsx new file mode 100644 index 00000000..9042b9ff --- /dev/null +++ b/ui/src/components/access/AccessEditFormKubernetesConfig.tsx @@ -0,0 +1,82 @@ +import { useState } from "react"; +import { flushSync } from "react-dom"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Button, Form, Input, Upload, type FormInstance, type UploadFile, type UploadProps } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; +import { Upload as UploadIcon } from "lucide-react"; + +import { type KubernetesAccessConfig } from "@/domain/access"; +import { readFileContent } from "@/utils/file"; + +type AccessEditFormKubernetesConfigModelType = Partial; + +export type AccessEditFormKubernetesConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormKubernetesConfigModelType; + onModelChange?: (model: AccessEditFormKubernetesConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormKubernetesConfigModelType; +}; + +const AccessEditFormKubernetesConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormKubernetesConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + kubeConfig: z + .string() + .trim() + .min(0, t("access.form.k8s_kubeconfig.placeholder")) + .max(20480, t("common.errmsg.string_max", { max: 20480 })) + .nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + setKubeFileList(model?.kubeConfig?.trim() ? [{ uid: "-1", name: "kubeconfig", status: "done" }] : []); + }, [model]); + + const [kubeFileList, setKubeFileList] = useState([]); + + const handleFormChange = (_: unknown, fields: AccessEditFormKubernetesConfigModelType) => { + onModelChange?.(fields); + }; + + const handleUploadChange: UploadProps["onChange"] = async ({ file }) => { + if (file && file.status !== "removed") { + form.setFieldValue("kubeConfig", (await readFileContent(file.originFileObj ?? (file as unknown as File))).trim()); + setKubeFileList([file]); + } else { + form.setFieldValue("kubeConfig", ""); + setKubeFileList([]); + } + + flushSync(() => onModelChange?.(form.getFieldsValue(true))); + }; + + return ( +
+ } + > + +
+ ); +}; + +export default AccessEditFormKubernetesConfig; diff --git a/ui/src/components/access/AccessEditFormLocalConfig.tsx b/ui/src/components/access/AccessEditFormLocalConfig.tsx new file mode 100644 index 00000000..94e8e40a --- /dev/null +++ b/ui/src/components/access/AccessEditFormLocalConfig.tsx @@ -0,0 +1,31 @@ +import { useState } from "react"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, type FormInstance } from "antd"; + +import { type LocalAccessConfig } from "@/domain/access"; + +type AccessEditFormLocalConfigModelType = Partial; + +export type AccessEditFormLocalConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormLocalConfigModelType; + onModelChange?: (model: AccessEditFormLocalConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormLocalConfigModelType; +}; + +const AccessEditFormLocalConfig = ({ form, formName, disabled, loading, model }: AccessEditFormLocalConfigProps) => { + const [initialValues, setInitialValues] = useState(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + return
; +}; + +export default AccessEditFormLocalConfig; diff --git a/ui/src/components/access/AccessEditFormNameSiloConfig.tsx b/ui/src/components/access/AccessEditFormNameSiloConfig.tsx new file mode 100644 index 00000000..7ac252bd --- /dev/null +++ b/ui/src/components/access/AccessEditFormNameSiloConfig.tsx @@ -0,0 +1,62 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type NameSiloAccessConfig } from "@/domain/access"; + +type AccessEditFormNameSiloConfigModelType = Partial; + +export type AccessEditFormNameSiloConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormNameSiloConfigModelType; + onModelChange?: (model: AccessEditFormNameSiloConfigModelType) => void; +}; + +const initModel = () => { + return { + apiKey: "", + } as AccessEditFormNameSiloConfigModelType; +}; + +const AccessEditFormNameSiloConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormNameSiloConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiKey: z + .string() + .trim() + .min(1, t("access.form.namesilo_api_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormNameSiloConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + +
+ ); +}; + +export default AccessEditFormNameSiloConfig; diff --git a/ui/src/components/access/AccessEditFormPowerDNSConfig.tsx b/ui/src/components/access/AccessEditFormPowerDNSConfig.tsx new file mode 100644 index 00000000..5677c781 --- /dev/null +++ b/ui/src/components/access/AccessEditFormPowerDNSConfig.tsx @@ -0,0 +1,73 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type PowerDNSAccessConfig } from "@/domain/access"; + +type AccessEditFormPowerDNSConfigModelType = Partial; + +export type AccessEditFormPowerDNSConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormPowerDNSConfigModelType; + onModelChange?: (model: AccessEditFormPowerDNSConfigModelType) => void; +}; + +const initModel = () => { + return { + apiUrl: "", + apiKey: "", + } as AccessEditFormPowerDNSConfigModelType; +}; + +const AccessEditFormPowerDNSConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormPowerDNSConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiUrl: z.string().url(t("common.errmsg.url_invalid")), + apiKey: z + .string() + .trim() + .min(1, t("access.form.powerdns_api_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormPowerDNSConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormPowerDNSConfig; diff --git a/ui/src/components/access/AccessEditFormQiniuConfig.tsx b/ui/src/components/access/AccessEditFormQiniuConfig.tsx new file mode 100644 index 00000000..043af052 --- /dev/null +++ b/ui/src/components/access/AccessEditFormQiniuConfig.tsx @@ -0,0 +1,77 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type QiniuAccessConfig } from "@/domain/access"; + +type AccessEditFormQiniuConfigModelType = Partial; + +export type AccessEditFormQiniuConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormQiniuConfigModelType; + onModelChange?: (model: AccessEditFormQiniuConfigModelType) => void; +}; + +const initModel = () => { + return { + accessKey: "", + secretKey: "", + } as AccessEditFormQiniuConfigModelType; +}; + +const AccessEditFormQiniuConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormQiniuConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKey: z + .string() + .trim() + .min(1, t("access.form.qiniu_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretKey: z + .string() + .trim() + .min(1, t("access.form.qiniu_secret_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormQiniuConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormQiniuConfig; diff --git a/ui/src/components/access/AccessEditFormSSHConfig.tsx b/ui/src/components/access/AccessEditFormSSHConfig.tsx new file mode 100644 index 00000000..e4843f9b --- /dev/null +++ b/ui/src/components/access/AccessEditFormSSHConfig.tsx @@ -0,0 +1,165 @@ +import { useState } from "react"; +import { flushSync } from "react-dom"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Button, Form, Input, InputNumber, Upload, type FormInstance, type UploadFile, type UploadProps } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; +import { Upload as UploadIcon } from "lucide-react"; + +import { type SSHAccessConfig } from "@/domain/access"; +import { readFileContent } from "@/utils/file"; + +type AccessEditFormSSHConfigModelType = Partial; + +export type AccessEditFormSSHConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormSSHConfigModelType; + onModelChange?: (model: AccessEditFormSSHConfigModelType) => void; +}; + +const initModel = () => { + return { + host: "127.0.0.1", + port: 22, + username: "root", + } as AccessEditFormSSHConfigModelType; +}; + +const AccessEditFormSSHConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormSSHConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + host: z.string().refine( + (str) => { + const reIpv4 = + /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + const reIpv6 = + /^([\da-fA-F]{1,4}:){6}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)|::([\da−fA−F]1,4:)0,4((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|::([\da−fA−F]1,4:)0,4((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|^([\da-fA-F]{1,4}:):([\da-fA-F]{1,4}:){0,3}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)2:([\da−fA−F]1,4:)0,2((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)2:([\da−fA−F]1,4:)0,2((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|^([\da-fA-F]{1,4}:){3}:([\da-fA-F]{1,4}:){0,1}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)4:((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)4:((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|^([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}|:((:[\da−fA−F]1,4)1,6|:)|:((:[\da−fA−F]1,4)1,6|:)|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?|([\da−fA−F]1,4:)6:|([\da−fA−F]1,4:)6:/; + const reDomain = /^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/; + return reIpv4.test(str) || reIpv6.test(str) || reDomain.test(str); + }, + { message: t("common.errmsg.host_invalid") } + ), + port: z + .number() + .int() + .gte(1, t("common.errmsg.port_invalid")) + .lte(65535, t("common.errmsg.port_invalid")) + .transform((v) => +v), + username: z + .string() + .min(1, "access.form.ssh_username.placeholder") + .max(64, t("common.errmsg.string_max", { max: 64 })), + password: z + .string() + .min(0, "access.form.ssh_password.placeholder") + .max(64, t("common.errmsg.string_max", { max: 64 })) + .nullish(), + key: z + .string() + .min(0, "access.form.ssh_key.placeholder") + .max(20480, t("common.errmsg.string_max", { max: 20480 })) + .nullish(), + keyPassphrase: z + .string() + .min(0, "access.form.ssh_key_passphrase.placeholder") + .max(20480, t("common.errmsg.string_max", { max: 20480 })) + .nullish() + .refine((v) => !v || form.getFieldValue("key"), { message: t("access.form.ssh_key.placeholder") }), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + setKeyFileList(model?.key?.trim() ? [{ uid: "-1", name: "sshkey", status: "done" }] : []); + }, [model]); + + const [keyFileList, setKeyFileList] = useState([]); + + const handleFormChange = (_: unknown, fields: AccessEditFormSSHConfigModelType) => { + onModelChange?.(fields); + }; + + const handleUploadChange: UploadProps["onChange"] = async ({ file }) => { + if (file && file.status !== "removed") { + form.setFieldValue("kubeConfig", (await readFileContent(file.originFileObj ?? (file as unknown as File))).trim()); + setKeyFileList([file]); + } else { + form.setFieldValue("kubeConfig", ""); + setKeyFileList([]); + } + + flushSync(() => onModelChange?.(form.getFieldsValue(true))); + }; + + return ( +
+
+
+ + + +
+ +
+ + + +
+
+ +
+
+ + + +
+ +
+ } + > + + +
+
+ +
+
+ } + > + +
+ +
+ } + > + + +
+
+
+ ); +}; + +export default AccessEditFormSSHConfig; diff --git a/ui/src/components/access/AccessEditFormTencentCloudConfig.tsx b/ui/src/components/access/AccessEditFormTencentCloudConfig.tsx new file mode 100644 index 00000000..78c311e1 --- /dev/null +++ b/ui/src/components/access/AccessEditFormTencentCloudConfig.tsx @@ -0,0 +1,77 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type TencentCloudAccessConfig } from "@/domain/access"; + +type AccessEditFormTencentCloudConfigModelType = Partial; + +export type AccessEditFormTencentCloudConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormTencentCloudConfigModelType; + onModelChange?: (model: AccessEditFormTencentCloudConfigModelType) => void; +}; + +const initModel = () => { + return { + secretId: "", + secretKey: "", + } as AccessEditFormTencentCloudConfigModelType; +}; + +const AccessEditFormTencentCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormTencentCloudConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + secretId: z + .string() + .trim() + .min(1, t("access.form.tencentcloud_secret_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretKey: z + .string() + .trim() + .min(1, t("access.form.tencentcloud_secret_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormTencentCloudConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormTencentCloudConfig; diff --git a/ui/src/components/access/AccessEditFormVolcEngineConfig.tsx b/ui/src/components/access/AccessEditFormVolcEngineConfig.tsx new file mode 100644 index 00000000..b00e82c4 --- /dev/null +++ b/ui/src/components/access/AccessEditFormVolcEngineConfig.tsx @@ -0,0 +1,77 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type VolcEngineAccessConfig } from "@/domain/access"; + +type AccessEditFormVolcEngineConfigModelType = Partial; + +export type AccessEditFormVolcEngineConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormVolcEngineConfigModelType; + onModelChange?: (model: AccessEditFormVolcEngineConfigModelType) => void; +}; + +const initModel = () => { + return { + accessKeyId: "", + secretAccessKey: "", + } as AccessEditFormVolcEngineConfigModelType; +}; + +const AccessEditFormVolcEngineConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormVolcEngineConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKeyId: z + .string() + .trim() + .min(1, t("access.form.volcengine_access_key_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretAccessKey: z + .string() + .trim() + .min(1, t("access.form.volcengine_secret_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormVolcEngineConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormVolcEngineConfig; diff --git a/ui/src/components/access/AccessEditFormWebhookConfig.tsx b/ui/src/components/access/AccessEditFormWebhookConfig.tsx new file mode 100644 index 00000000..546dfc3b --- /dev/null +++ b/ui/src/components/access/AccessEditFormWebhookConfig.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type WebhookAccessConfig } from "@/domain/access"; + +type AccessEditFormWebhookConfigModelType = Partial; + +export type AccessEditFormWebhookConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormWebhookConfigModelType; + onModelChange?: (model: AccessEditFormWebhookConfigModelType) => void; +}; + +const initModel = () => { + return { + url: "", + } as AccessEditFormWebhookConfigModelType; +}; + +const AccessEditFormWebhookConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormWebhookConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + url: z + .string() + .min(1, { message: t("access.form.webhook_url.placeholder") }) + .url({ message: t("common.errmsg.url_invalid") }), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormWebhookConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ + + +
+ ); +}; + +export default AccessEditFormWebhookConfig; diff --git a/ui/src/components/access/AccessEditModal.tsx b/ui/src/components/access/AccessEditModal.tsx new file mode 100644 index 00000000..7e06cfb6 --- /dev/null +++ b/ui/src/components/access/AccessEditModal.tsx @@ -0,0 +1,117 @@ +import { cloneElement, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useControllableValue } from "ahooks"; +import { Modal, notification } from "antd"; + +import { type AccessModel } from "@/domain/access"; +import { useAccessStore } from "@/stores/access"; +import { getErrMsg } from "@/utils/error"; +import AccessEditForm, { type AccessEditFormInstance, type AccessEditFormProps } from "./AccessEditForm"; + +export type AccessEditModalProps = { + data?: AccessEditFormProps["model"]; + loading?: boolean; + mode: AccessEditFormProps["mode"]; + open?: boolean; + trigger?: React.ReactElement; + onOpenChange?: (open: boolean) => void; +}; + +const AccessEditModal = ({ data, loading, mode, trigger, ...props }: AccessEditModalProps) => { + const { t } = useTranslation(); + + const [notificationApi, NotificationContextHolder] = notification.useNotification(); + + const { createAccess, updateAccess } = useAccessStore(); + + const [open, setOpen] = useControllableValue(props, { + valuePropName: "open", + defaultValuePropName: "defaultOpen", + trigger: "onOpenChange", + }); + + const triggerEl = useMemo(() => { + if (!trigger) { + return null; + } + + return cloneElement(trigger, { + ...trigger.props, + onClick: () => { + setOpen(true); + trigger.props?.onClick?.(); + }, + }); + }, [trigger, setOpen]); + + const formRef = useRef(null); + const [formPending, setFormPending] = useState(false); + + const handleClickOk = async () => { + setFormPending(true); + try { + await formRef.current!.validateFields(); + } catch (err) { + setFormPending(false); + return Promise.reject(); + } + + try { + if (mode === "add") { + if (data?.id) { + throw "Invalid props: `data`"; + } + + await createAccess(formRef.current!.getFieldsValue() as AccessModel); + } else if (mode === "edit") { + if (!data?.id) { + throw "Invalid props: `data`"; + } + + await updateAccess({ ...data, ...formRef.current!.getFieldsValue() } as AccessModel); + } + + setOpen(false); + } catch (err) { + notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + + throw err; + } finally { + setFormPending(false); + } + }; + + const handleClickCancel = () => { + if (formPending) return Promise.reject(); + + setOpen(false); + }; + + return ( + <> + {NotificationContextHolder} + + {triggerEl} + + setOpen(false)} + cancelButtonProps={{ disabled: formPending }} + closable + confirmLoading={formPending} + destroyOnClose + loading={loading} + okText={mode === "edit" ? t("common.button.save") : t("common.button.submit")} + open={open} + title={t(`access.action.${mode}`)} + onOk={handleClickOk} + onCancel={handleClickCancel} + > +
+ +
+
+ + ); +}; + +export default AccessEditModal; diff --git a/ui/src/components/access/AccessSelect.tsx b/ui/src/components/access/AccessSelect.tsx new file mode 100644 index 00000000..3cbca057 --- /dev/null +++ b/ui/src/components/access/AccessSelect.tsx @@ -0,0 +1,76 @@ +import { useEffect, useState } from "react"; +import { Avatar, Select, Space, Typography, type SelectProps } from "antd"; + +import { accessProvidersMap, type AccessModel } from "@/domain/access"; +import { useAccessStore } from "@/stores/access"; + +export type AccessTypeSelectProps = Omit< + SelectProps, + "filterOption" | "filterSort" | "labelRender" | "loading" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender" +> & { + filter?: (record: AccessModel) => boolean; +}; + +const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => { + const { initialized, accesses, fetchAccesses } = useAccessStore(); + useEffect(() => { + fetchAccesses(); + }, [fetchAccesses]); + + const [options, setOptions] = useState>([]); + useEffect(() => { + const items = filter != null ? accesses.filter(filter) : accesses; + setOptions( + items.map((item) => ({ + key: item.id, + value: item.id, + label: item.name, + data: item, + })) + ); + }, [accesses, filter]); + + const renderOption = (key: string) => { + const access = accesses.find((e) => e.id === key); + if (!access) { + return ( + + + + {key} + + + ); + } + + const provider = accessProvidersMap.get(access.configType); + return ( + + + + {access.name} + + + ); + }; + + return ( + { + if (label) { + return renderOption(value as string); + } + + return {props.placeholder}; + }} + options={options} + optionFilterProp={undefined} + optionLabelProp={undefined} + optionRender={(option) => renderOption(option.data.value)} + /> + ); +}); + +export default AccessTypeSelect; diff --git a/ui/src/components/certificate/CertificateDetail.tsx b/ui/src/components/certificate/CertificateDetail.tsx index 02b7a91e..c242344c 100644 --- a/ui/src/components/certificate/CertificateDetail.tsx +++ b/ui/src/components/certificate/CertificateDetail.tsx @@ -2,15 +2,18 @@ import { useTranslation } from "react-i18next"; import { Button, Dropdown, Form, Input, message, Space, Tooltip } from "antd"; import { CopyToClipboard } from "react-copy-to-clipboard"; import { ChevronDown as ChevronDownIcon, Clipboard as ClipboardIcon, ThumbsUp as ThumbsUpIcon } from "lucide-react"; +import dayjs from "dayjs"; import { type CertificateModel } from "@/domain/certificate"; import { saveFiles2Zip } from "@/utils/file"; -type CertificateDetailProps = { +export type CertificateDetailProps = { + className?: string; + style?: React.CSSProperties; data: CertificateModel; }; -const CertificateDetail = ({ data }: CertificateDetailProps) => { +const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => { const { t } = useTranslation(); const [messageApi, MessageContextHolder] = message.useMessage(); @@ -32,10 +35,14 @@ const CertificateDetail = ({ data }: CertificateDetailProps) => { }; return ( -
+
{MessageContextHolder}
+ {data.san} + + {dayjs(data.expireAt).format("YYYY-MM-DD HH:mm:ss")} +
diff --git a/ui/src/components/certificate/CertificateDetailDrawer.tsx b/ui/src/components/certificate/CertificateDetailDrawer.tsx index 6aefd7d6..0966f611 100644 --- a/ui/src/components/certificate/CertificateDetailDrawer.tsx +++ b/ui/src/components/certificate/CertificateDetailDrawer.tsx @@ -1,25 +1,47 @@ -import { useEffect, useState } from "react"; +import { cloneElement, useMemo } from "react"; +import { useControllableValue } from "ahooks"; import { Drawer } from "antd"; import { type CertificateModel } from "@/domain/certificate"; import CertificateDetail from "./CertificateDetail"; -type CertificateDetailDrawerProps = { +export type CertificateDetailDrawerProps = { data?: CertificateModel; + loading?: boolean; open?: boolean; - onClose?: () => void; + trigger?: React.ReactElement; + onOpenChange?: (open: boolean) => void; }; -const CertificateDetailDrawer = ({ data, open, onClose }: CertificateDetailDrawerProps) => { - const [loading, setLoading] = useState(true); - useEffect(() => { - setLoading(data == null); - }, [data]); +const CertificateDetailDrawer = ({ data, loading, trigger, ...props }: CertificateDetailDrawerProps) => { + const [open, setOpen] = useControllableValue(props, { + valuePropName: "open", + defaultValuePropName: "defaultOpen", + trigger: "onOpenChange", + }); + + const triggerEl = useMemo(() => { + if (!trigger) { + return null; + } + + return cloneElement(trigger, { + ...trigger.props, + onClick: () => { + setOpen(true); + trigger.props?.onClick?.(); + }, + }); + }, [trigger, setOpen]); return ( - - {data ? : <>} - + <> + {triggerEl} + + setOpen(false)}> + {data ? : <>} + + ); }; diff --git a/ui/src/components/certimate/AccessAliyunForm.tsx b/ui/src/components/certimate/AccessAliyunForm.tsx deleted file mode 100644 index 14352078..00000000 --- a/ui/src/components/certimate/AccessAliyunForm.tsx +++ /dev/null @@ -1,197 +0,0 @@ -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 AccessModel, type AliyunConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessAliyunFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessAliyunForm = ({ data, op, onAfterReq }: AccessAliyunFormProps) => { - const { t } = useTranslation(); - - const { createAccess, updateAccess } = useAccessStore(); - - 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 })), - accessSecretId: z - .string() - .min(1, "access.authorization.form.access_key_secret.placeholder") - .max(64, t("common.errmsg.string_max", { max: 64 })), - }); - - let config: AliyunConfig = { - accessKeyId: "", - accessKeySecret: "", - }; - if (data) config = data.config as AliyunConfig; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "aliyun", - accessKeyId: config.accessKeyId, - accessSecretId: config.accessKeySecret, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - id: data.id as string, - name: data.name, - configType: data.configType, - usage: accessProvidersMap.get(data.configType)!.usage, - config: { - accessKeyId: data.accessKeyId, - accessKeySecret: data.accessSecretId, - }, - }; - - 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; - } - - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - - return; - } - }; - - return ( - <> - - { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.access_key_id.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.access_key_secret.label")} - - - - - - - )} - /> - - - -
- -
- - - - ); -}; - -export default AccessAliyunForm; diff --git a/ui/src/components/certimate/AccessAwsForm.tsx b/ui/src/components/certimate/AccessAwsForm.tsx deleted file mode 100644 index 28f3c69d..00000000 --- a/ui/src/components/certimate/AccessAwsForm.tsx +++ /dev/null @@ -1,241 +0,0 @@ -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 { AccessModel, accessProvidersMap, accessTypeFormSchema, type AwsConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessAwsFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessAwsForm = ({ data, op, onAfterReq }: AccessAwsFormProps) => { - const { t } = useTranslation(); - - const { createAccess, updateAccess } = useAccessStore(); - - 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, - region: z - .string() - .min(1, "access.authorization.form.region.placeholder") - .max(64, t("common.errmsg.string_max", { max: 64 })), - 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 })), - hostedZoneId: z - .string() - .min(0, "access.authorization.form.aws_hosted_zone_id.placeholder") - .max(64, t("common.errmsg.string_max", { max: 64 })), - }); - - let config: AwsConfig = { - region: "cn-north-1", - accessKeyId: "", - secretAccessKey: "", - hostedZoneId: "", - }; - if (data) config = data.config as AwsConfig; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "aws", - region: config.region, - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - hostedZoneId: config.hostedZoneId, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - id: data.id as string, - name: data.name, - configType: data.configType, - usage: accessProvidersMap.get(data.configType)!.usage, - config: { - region: data.region, - accessKeyId: data.accessKeyId, - secretAccessKey: data.secretAccessKey, - hostedZoneId: data.hostedZoneId, - }, - }; - - 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; - } - - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - - return; - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.region.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.access_key_id.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.secret_access_key.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.aws_hosted_zone_id.label")} - - - - - - - )} - /> - - - -
- -
- - - - ); -}; - -export default AccessAwsForm; diff --git a/ui/src/components/certimate/AccessBaiduCloudForm.tsx b/ui/src/components/certimate/AccessBaiduCloudForm.tsx deleted file mode 100644 index 033d34e4..00000000 --- a/ui/src/components/certimate/AccessBaiduCloudForm.tsx +++ /dev/null @@ -1,197 +0,0 @@ -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 AccessModel, type BaiduCloudConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessBaiduCloudFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessBaiduCloudForm = ({ data, op, onAfterReq }: AccessBaiduCloudFormProps) => { - const { t } = useTranslation(); - - const { createAccess, updateAccess } = useAccessStore(); - - 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>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "baiducloud", - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - 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; - } - - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - - return; - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.access_key_id.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.secret_access_key.label")} - - - - - - - )} - /> - - - -
- -
- - - - ); -}; - -export default AccessBaiduCloudForm; diff --git a/ui/src/components/certimate/AccessByteplusForm.tsx b/ui/src/components/certimate/AccessByteplusForm.tsx deleted file mode 100644 index f7fc213b..00000000 --- a/ui/src/components/certimate/AccessByteplusForm.tsx +++ /dev/null @@ -1,194 +0,0 @@ -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 AccessModel, type ByteplusConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessByteplusFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessByteplusForm = ({ data, op, onAfterReq }: AccessByteplusFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - 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, t("common.errmsg.string_max", { max: 64 })), - secretKey: z - .string() - .min(1, "access.authorization.form.secret_key.placeholder") - .max(64, t("common.errmsg.string_max", { max: 64 })), - }); - - let config: ByteplusConfig = { - accessKey: "", - secretKey: "", - }; - if (data) config = data.config as ByteplusConfig; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "byteplus", - accessKey: config.accessKey, - secretKey: config.secretKey, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - 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; - } - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - - return; - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.access_key.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.secret_key.label")} - - - - - - - )} - /> - - - -
- -
- - - - ); -}; - -export default AccessByteplusForm; diff --git a/ui/src/components/certimate/AccessCloudflareForm.tsx b/ui/src/components/certimate/AccessCloudflareForm.tsx deleted file mode 100644 index 0a347223..00000000 --- a/ui/src/components/certimate/AccessCloudflareForm.tsx +++ /dev/null @@ -1,168 +0,0 @@ -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 AccessModel, type CloudflareConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessCloudflareFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessCloudflareForm = ({ data, op, onAfterReq }: AccessCloudflareFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - 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, - dnsApiToken: z - .string() - .min(1, "access.authorization.form.cloud_dns_api_token.placeholder") - .max(64, t("common.errmsg.string_max", { max: 64 })), - }); - - let config: CloudflareConfig = { - dnsApiToken: "", - }; - if (data) config = data.config as CloudflareConfig; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "cloudflare", - dnsApiToken: config.dnsApiToken, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - id: data.id as string, - name: data.name, - configType: data.configType, - usage: accessProvidersMap.get(data.configType)!.usage, - config: { - dnsApiToken: data.dnsApiToken, - }, - }; - - 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; - } - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.cloud_dns_api_token.label")} - - - - - - - )} - /> - -
- -
- - - - ); -}; - -export default AccessCloudflareForm; diff --git a/ui/src/components/certimate/AccessDogeCloudForm.tsx b/ui/src/components/certimate/AccessDogeCloudForm.tsx deleted file mode 100644 index 13283ee1..00000000 --- a/ui/src/components/certimate/AccessDogeCloudForm.tsx +++ /dev/null @@ -1,188 +0,0 @@ -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 AccessModel, type DogeCloudConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessDogeCloudFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessDogeCloudForm = ({ data, op, onAfterReq }: AccessDogeCloudFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - 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>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "dogecloud", - accessKey: config.accessKey, - secretKey: config.secretKey, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - 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; - } - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - - return; - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.access_key.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.secret_key.label")} - - - - - - - )} - /> - - - -
- -
- - - - ); -}; - -export default AccessDogeCloudForm; diff --git a/ui/src/components/certimate/AccessEditDialog.tsx b/ui/src/components/certimate/AccessEditDialog.tsx deleted file mode 100644 index a9e8986f..00000000 --- a/ui/src/components/certimate/AccessEditDialog.tsx +++ /dev/null @@ -1,306 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import { cn } from "@/components/ui/utils"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { Label } from "@/components/ui/label"; -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"; -import AccessGodaddyForm from "./AccessGodaddyForm"; -import AccessPdnsForm from "./AccessPdnsForm"; -import AccessHttpreqForm from "./AccessHttpreqForm"; -import AccessLocalForm from "./AccessLocalForm"; -import AccessSSHForm from "./AccessSSHForm"; -import AccessWebhookForm from "./AccessWebhookForm"; -import AccessKubernetesForm from "./AccessKubernetesForm"; -import AccessVolcengineForm from "./AccessVolcengineForm"; -import AccessByteplusForm from "./AccessByteplusForm"; -import { AccessModel } from "@/domain/access"; -import { AccessTypeSelect } from "./AccessTypeSelect"; - -type AccessEditProps = { - op: "add" | "edit" | "copy"; - className?: string; - trigger: React.ReactNode; - data?: AccessModel; - outConfigType?: string; -}; - -const AccessEditDialog = ({ trigger, op, data, className, outConfigType }: AccessEditProps) => { - const { t } = useTranslation(); - - const [open, setOpen] = useState(false); - - const [configType, setConfigType] = useState(data?.configType || ""); - - useEffect(() => { - if (outConfigType) { - setConfigType(outConfigType); - } - }, [outConfigType]); - - let childComponent = <> ; - switch (configType) { - case "aliyun": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "tencent": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "huaweicloud": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "baiducloud": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "qiniu": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "dogecloud": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "aws": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "cloudflare": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "namesilo": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "godaddy": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "pdns": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "httpreq": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "local": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "ssh": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "webhook": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "k8s": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "volcengine": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - case "byteplus": - childComponent = ( - { - setOpen(false); - }} - /> - ); - break; - } - - return ( - { - if (openState) { - document.body.style.pointerEvents = "auto"; - } - setOpen(openState); - }} - open={open} - modal={false} - > - - {trigger} - - { - event.preventDefault(); - }} - > - - - { - { - ["add"]: t("access.action.add"), - ["edit"]: t("access.action.edit"), - ["copy"]: t("access.action.copy"), - }[op] - } - - - -
-
- - { - setConfigType(val); - }} - className="w-full mt-3" - placeholder={t("access.authorization.form.type.placeholder")} - searchPlaceholder={t("access.authorization.form.type.search.placeholder")} - /> -
- -
{childComponent}
-
-
-
-
- ); -}; - -export default AccessEditDialog; diff --git a/ui/src/components/certimate/AccessGodaddyForm.tsx b/ui/src/components/certimate/AccessGodaddyForm.tsx deleted file mode 100644 index 1c8f40a5..00000000 --- a/ui/src/components/certimate/AccessGodaddyForm.tsx +++ /dev/null @@ -1,190 +0,0 @@ -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 { Input } from "@/components/ui/input"; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { PbErrorData } from "@/domain/base"; -import { accessProvidersMap, accessTypeFormSchema, type AccessModel, type GodaddyConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessGodaddyFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessGodaddyForm = ({ data, op, onAfterReq }: AccessGodaddyFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - 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, - apiKey: z - .string() - .min(1, "access.authorization.form.godaddy_api_key.placeholder") - .max(64, t("common.errmsg.string_max", { max: 64 })), - apiSecret: z - .string() - .min(1, "access.authorization.form.godaddy_api_secret.placeholder") - .max(64, t("common.errmsg.string_max", { max: 64 })), - }); - - let config: GodaddyConfig = { - apiKey: "", - apiSecret: "", - }; - if (data) config = data.config as GodaddyConfig; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "godaddy", - apiKey: config.apiKey, - apiSecret: config.apiSecret, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - id: data.id as string, - name: data.name, - configType: data.configType, - usage: accessProvidersMap.get(data.configType)!.usage, - config: { - apiKey: data.apiKey, - apiSecret: data.apiSecret, - }, - }; - - 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; - } - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.godaddy_api_key.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.godaddy_api_secret.label")} - - - - - - - )} - /> - -
- -
- - - - ); -}; - -export default AccessGodaddyForm; diff --git a/ui/src/components/certimate/AccessHttpreqForm.tsx b/ui/src/components/certimate/AccessHttpreqForm.tsx deleted file mode 100644 index 38abf92f..00000000 --- a/ui/src/components/certimate/AccessHttpreqForm.tsx +++ /dev/null @@ -1,233 +0,0 @@ -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 AccessModel, type HttpreqConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessHttpreqFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessHttpreqForm = ({ data, op, onAfterReq }: AccessHttpreqFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - 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, - endpoint: z.string().url("common.errmsg.url_invalid"), - mode: z.enum(["RAW", ""]), - username: z - .string() - .min(1, "access.authorization.form.access_key_secret.placeholder") - .max(128, t("common.errmsg.string_max", { max: 128 })), - password: z - .string() - .min(1, "access.authorization.form.access_key_secret.placeholder") - .max(128, t("common.errmsg.string_max", { max: 128 })), - }); - - let config: HttpreqConfig = { - endpoint: "", - mode: "", - username: "", - password: "", - }; - if (data) config = data.config as HttpreqConfig; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "httpreq", - endpoint: config.endpoint, - mode: config.mode === "RAW" ? "RAW" : "", - username: config.username, - password: config.password, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - id: data.id as string, - name: data.name, - configType: data.configType, - usage: accessProvidersMap.get(data.configType)!.usage, - config: { - endpoint: data.endpoint, - mode: data.mode, - username: data.username, - password: data.password, - }, - }; - - 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; - } - - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - - return; - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.httpreq_endpoint.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.httpreq_mode.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.username.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.password.label")} - - - - - - - )} - /> - - - -
- -
- - - - ); -}; - -export default AccessHttpreqForm; diff --git a/ui/src/components/certimate/AccessHuaweicloudForm.tsx b/ui/src/components/certimate/AccessHuaweicloudForm.tsx deleted file mode 100644 index 16c7c746..00000000 --- a/ui/src/components/certimate/AccessHuaweicloudForm.tsx +++ /dev/null @@ -1,216 +0,0 @@ -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 AccessModel, type HuaweiCloudConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessHuaweiCloudFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessHuaweiCloudForm = ({ data, op, onAfterReq }: AccessHuaweiCloudFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - 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, - region: z - .string() - .min(1, "access.authorization.form.region.placeholder") - .max(64, t("common.errmsg.string_max", { max: 64 })), - 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: HuaweiCloudConfig = { - region: "cn-north-1", - accessKeyId: "", - secretAccessKey: "", - }; - if (data) config = data.config as HuaweiCloudConfig; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "huaweicloud", - region: config.region, - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - id: data.id as string, - name: data.name, - configType: data.configType, - usage: accessProvidersMap.get(data.configType)!.usage, - config: { - region: data.region, - 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; - } - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - - return; - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.region.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.access_key_id.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.secret_access_key.label")} - - - - - - - )} - /> - - - -
- -
- - - - ); -}; - -export default AccessHuaweiCloudForm; diff --git a/ui/src/components/certimate/AccessKubernetesForm.tsx b/ui/src/components/certimate/AccessKubernetesForm.tsx deleted file mode 100644 index 55ccc204..00000000 --- a/ui/src/components/certimate/AccessKubernetesForm.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useForm } from "react-hook-form"; -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 { readFileContent } from "@/utils/file"; -import { PbErrorData } from "@/domain/base"; -import { accessProvidersMap, accessTypeFormSchema, type AccessModel, type KubernetesConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessKubernetesFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessKubernetesForm = ({ data, op, onAfterReq }: AccessKubernetesFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - - const fileInputRef = useRef(null); - const [fileName, setFileName] = useState(""); - - 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, - kubeConfig: z - .string() - .min(0, "access.authorization.form.k8s_kubeconfig.placeholder") - .max(20480, t("common.errmsg.string_max", { max: 20480 })), - kubeConfigFile: z.any().optional(), - }); - - let config: KubernetesConfig & { kubeConfigFile?: string } = { - kubeConfig: "", - kubeConfigFile: "", - }; - if (data) config = data.config as typeof config; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "k8s", - kubeConfig: config.kubeConfig, - kubeConfigFile: config.kubeConfigFile, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - id: data.id as string, - name: data.name, - configType: data.configType, - usage: accessProvidersMap.get(data.configType)!.usage, - config: { - kubeConfig: data.kubeConfig, - }, - }; - - 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); - } else { - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - - return; - } - }; - - const handleFileChange = async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (!file) return; - const savedFile = file; - setFileName(savedFile.name); - const content = await readFileContent(savedFile); - form.setValue("kubeConfig", content); - }; - - const handleSelectFileClick = () => { - fileInputRef.current?.click(); - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - )} - /> - - ( - - {t("access.authorization.form.k8s_kubeconfig.label")} - -
- - -
-
- - -
- )} - /> - - - -
- -
- - - - ); -}; - -export default AccessKubernetesForm; diff --git a/ui/src/components/certimate/AccessLocalForm.tsx b/ui/src/components/certimate/AccessLocalForm.tsx deleted file mode 100644 index 427fad63..00000000 --- a/ui/src/components/certimate/AccessLocalForm.tsx +++ /dev/null @@ -1,147 +0,0 @@ -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 { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { PbErrorData } from "@/domain/base"; -import { accessProvidersMap, accessTypeFormSchema, type AccessModel } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessLocalFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessLocalForm = ({ data, op, onAfterReq }: AccessLocalFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - 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, - }); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "local", - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - id: data.id as string, - name: data.name, - configType: data.configType, - usage: accessProvidersMap.get(data.configType)!.usage, - - config: {}, - }; - - 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); - } else { - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - - return; - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - - -
- -
- - - - ); -}; - -export default AccessLocalForm; diff --git a/ui/src/components/certimate/AccessNamesiloForm.tsx b/ui/src/components/certimate/AccessNamesiloForm.tsx deleted file mode 100644 index 31bce833..00000000 --- a/ui/src/components/certimate/AccessNamesiloForm.tsx +++ /dev/null @@ -1,168 +0,0 @@ -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 AccessModel, type NamesiloConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessNamesiloFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessNamesiloForm = ({ data, op, onAfterReq }: AccessNamesiloFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - 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, - apiKey: z - .string() - .min(1, "access.authorization.form.namesilo_api_key.placeholder") - .max(64, t("common.errmsg.string_max", { max: 64 })), - }); - - let config: NamesiloConfig = { - apiKey: "", - }; - if (data) config = data.config as NamesiloConfig; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "namesilo", - apiKey: config.apiKey, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - id: data.id as string, - name: data.name, - configType: data.configType, - usage: accessProvidersMap.get(data.configType)!.usage, - config: { - apiKey: data.apiKey, - }, - }; - - 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; - } - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.namesilo_api_key.label")} - - - - - - - )} - /> - -
- -
- - - - ); -}; - -export default AccessNamesiloForm; diff --git a/ui/src/components/certimate/AccessPdnsForm.tsx b/ui/src/components/certimate/AccessPdnsForm.tsx deleted file mode 100644 index e75df10a..00000000 --- a/ui/src/components/certimate/AccessPdnsForm.tsx +++ /dev/null @@ -1,192 +0,0 @@ -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 AccessModel, type PdnsConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessPdnsFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessPdnsForm = ({ data, op, onAfterReq }: AccessPdnsFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - 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, - apiUrl: z.string().url("common.errmsg.url_invalid"), - apiKey: z - .string() - .min(1, "access.authorization.form.access_key_secret.placeholder") - .max(64, t("common.errmsg.string_max", { max: 64 })), - }); - - let config: PdnsConfig = { - apiUrl: "", - apiKey: "", - }; - if (data) config = data.config as PdnsConfig; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "pdns", - apiUrl: config.apiUrl, - apiKey: config.apiKey, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - id: data.id as string, - name: data.name, - configType: data.configType, - usage: accessProvidersMap.get(data.configType)!.usage, - config: { - apiUrl: data.apiUrl, - apiKey: data.apiKey, - }, - }; - - 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; - } - - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - - return; - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.pdns_api_url.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.pdns_api_key.label")} - - - - - - - )} - /> - - - -
- -
- - - - ); -}; - -export default AccessPdnsForm; diff --git a/ui/src/components/certimate/AccessQiniuForm.tsx b/ui/src/components/certimate/AccessQiniuForm.tsx deleted file mode 100644 index 36b644cb..00000000 --- a/ui/src/components/certimate/AccessQiniuForm.tsx +++ /dev/null @@ -1,188 +0,0 @@ -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 AccessModel, type QiniuConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessQiniuFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessQiniuForm = ({ data, op, onAfterReq }: AccessQiniuFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - 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: QiniuConfig = { - accessKey: "", - secretKey: "", - }; - if (data) config = data.config as QiniuConfig; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "qiniu", - accessKey: config.accessKey, - secretKey: config.secretKey, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - 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; - } - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - - return; - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.access_key.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.secret_key.label")} - - - - - - - )} - /> - - - -
- -
- - - - ); -}; - -export default AccessQiniuForm; diff --git a/ui/src/components/certimate/AccessSSHForm.tsx b/ui/src/components/certimate/AccessSSHForm.tsx deleted file mode 100644 index d9b40774..00000000 --- a/ui/src/components/certimate/AccessSSHForm.tsx +++ /dev/null @@ -1,348 +0,0 @@ -import { useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useForm } from "react-hook-form"; -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 { readFileContent } from "@/utils/file"; -import { PbErrorData } from "@/domain/base"; -import { accessProvidersMap, accessTypeFormSchema, type AccessModel, type SSHConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessSSHFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessSSHForm = ({ data, op, onAfterReq }: AccessSSHFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - - const fileInputRef = useRef(null); - - const [fileName, setFileName] = useState(""); - const { t } = useTranslation(); - - const domainReg = /^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/; - const ipReg = - /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; - - 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, - host: z.string().refine( - (str) => { - return ipReg.test(str) || domainReg.test(str); - }, - { - message: "common.errmsg.host_invalid", - } - ), - group: z.string().optional(), - port: z - .string() - .min(1, "access.authorization.form.ssh_port.placeholder") - .max(5, t("common.errmsg.string_max", { max: 5 })), - username: z - .string() - .min(1, "access.authorization.form.ssh_username.placeholder") - .max(64, t("common.errmsg.string_max", { max: 64 })), - password: z - .string() - .min(0, "access.authorization.form.ssh_password.placeholder") - .max(64, t("common.errmsg.string_max", { max: 64 })), - key: z - .string() - .min(0, "access.authorization.form.ssh_key.placeholder") - .max(20480, t("common.errmsg.string_max", { max: 20480 })), - keyFile: z.any().optional(), - keyPassphrase: z - .string() - .min(0, "access.authorization.form.ssh_key_passphrase.placeholder") - .max(2048, t("common.errmsg.string_max", { max: 2048 })), - }); - - let config: SSHConfig = { - host: "127.0.0.1", - port: "22", - username: "root", - password: "", - key: "", - keyFile: "", - keyPassphrase: "", - }; - if (data) config = data.config as SSHConfig; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "ssh", - group: data?.group, - host: config.host, - port: config.port, - username: config.username, - password: config.password, - key: config.key, - keyFile: config.keyFile, - keyPassphrase: config.keyPassphrase, - }, - }); - - const onSubmit = async (data: z.infer) => { - let group = data.group; - if (group == "emptyId") group = ""; - - const req: AccessModel = { - id: data.id as string, - name: data.name, - configType: data.configType, - usage: accessProvidersMap.get(data.configType)!.usage, - group: group, - config: { - host: data.host, - port: data.port, - username: data.username, - password: data.password, - key: data.key, - keyPassphrase: data.keyPassphrase, - }, - }; - - 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); - } else { - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - - return; - } - }; - - const handleFileChange = async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (!file) return; - const savedFile = file; - setFileName(savedFile.name); - const content = await readFileContent(savedFile); - form.setValue("key", content); - }; - - const handleSelectFileClick = () => { - fileInputRef.current?.click(); - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> -
- ( - - {t("access.authorization.form.ssh_host.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.ssh_port.label")} - - - - - - - )} - /> -
- - ( - - {t("access.authorization.form.ssh_username.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.ssh_password.label")} - - - - - - - )} - /> - - ( - - )} - /> - - ( - - {t("access.authorization.form.ssh_key.label")} - -
- - -
-
- - -
- )} - /> - - ( - - {t("access.authorization.form.ssh_key_passphrase.label")} - - - - - - - )} - /> - - - -
- -
- - - - ); -}; - -export default AccessSSHForm; diff --git a/ui/src/components/certimate/AccessTencentForm.tsx b/ui/src/components/certimate/AccessTencentForm.tsx deleted file mode 100644 index aa3246d5..00000000 --- a/ui/src/components/certimate/AccessTencentForm.tsx +++ /dev/null @@ -1,190 +0,0 @@ -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 AccessModel, type TencentConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessTencentFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessTencentForm = ({ data, op, onAfterReq }: AccessTencentFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - 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, - secretId: z - .string() - .min(1, "access.authorization.form.secret_id.placeholder") - .max(64, t("common.errmsg.string_max", { max: 64 })), - secretKey: z - .string() - .min(1, "access.authorization.form.secret_key.placeholder") - .max(64, t("common.errmsg.string_max", { max: 64 })), - }); - - let config: TencentConfig = { - secretId: "", - secretKey: "", - }; - if (data) config = data.config as TencentConfig; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "tencent", - secretId: config.secretId, - secretKey: config.secretKey, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - id: data.id as string, - name: data.name, - configType: data.configType, - usage: accessProvidersMap.get(data.configType)!.usage, - config: { - secretId: data.secretId, - 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; - } - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.secret_id.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.secret_key.label")} - - - - - - - )} - /> - -
- -
- - - - ); -}; - -export default AccessTencentForm; diff --git a/ui/src/components/certimate/AccessTypeSelect.tsx b/ui/src/components/certimate/AccessTypeSelect.tsx deleted file mode 100644 index 23f60dea..00000000 --- a/ui/src/components/certimate/AccessTypeSelect.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Check, ChevronsUpDown } from "lucide-react"; - -import { cn } from "@/components/ui/utils"; -import { Button } from "@/components/ui/button"; -import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { accessProvidersMap } from "@/domain/access"; -import { useTranslation } from "react-i18next"; -import { useEffect, useState } from "react"; - -type AccessTypeSelectProps = { - value: string; - onChange: (value: string) => void; - placeholder: string; - searchPlaceholder: string; - className?: string; -}; - -export function AccessTypeSelect({ value, onChange, placeholder, searchPlaceholder, className }: AccessTypeSelectProps) { - const [open, setOpen] = useState(false); - const [locValue, setLocValue] = useState(""); - const { t } = useTranslation(); - const [search, setSearch] = useState(""); - const filteredProviders = Array.from(accessProvidersMap.entries()); - - useEffect(() => { - setLocValue(value); - }, [value]); - - const handleOnSelect = (currentValue: string) => { - const newValue = currentValue === locValue ? "" : currentValue; - setLocValue(newValue); - setSearch(""); - setOpen(false); - onChange(newValue); - }; - - return ( - - - - - - - { - setSearch(val); - }} - /> - - {t("access.authorization.form.type.search.notfound")} - - {filteredProviders.map(([key, provider]) => ( - - -
- -
{t(provider.name)}
-
-
- ))} -
-
-
-
-
- ); -} diff --git a/ui/src/components/certimate/AccessVolcengineForm.tsx b/ui/src/components/certimate/AccessVolcengineForm.tsx deleted file mode 100644 index 1a62ac1b..00000000 --- a/ui/src/components/certimate/AccessVolcengineForm.tsx +++ /dev/null @@ -1,194 +0,0 @@ -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 AccessModel, type VolcengineConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessVolcengineFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessVolcengineForm = ({ data, op, onAfterReq }: AccessVolcengineFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - 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: VolcengineConfig = { - accessKeyId: "", - secretAccessKey: "", - }; - if (data) config = data.config as VolcengineConfig; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "volcengine", - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - 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; - } - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - - return; - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.access_key_id.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.secret_access_key.label")} - - - - - - - )} - /> - - - -
- -
- - - - ); -}; - -export default AccessVolcengineForm; diff --git a/ui/src/components/certimate/AccessWebhookForm.tsx b/ui/src/components/certimate/AccessWebhookForm.tsx deleted file mode 100644 index 6450d4d7..00000000 --- a/ui/src/components/certimate/AccessWebhookForm.tsx +++ /dev/null @@ -1,165 +0,0 @@ -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 { AccessModel, accessProvidersMap, accessTypeFormSchema, WebhookConfig } from "@/domain/access"; -import { save } from "@/repository/access"; -import { useAccessStore } from "@/stores/access"; - -type AccessWebhookFormProps = { - op: "add" | "edit" | "copy"; - data?: AccessModel; - onAfterReq: () => void; -}; - -const AccessWebhookForm = ({ data, op, onAfterReq }: AccessWebhookFormProps) => { - const { createAccess, updateAccess } = useAccessStore(); - 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, - url: z.string().url("common.errmsg.url_invalid"), - }); - - let config: WebhookConfig = { - url: "", - }; - if (data) config = data.config as WebhookConfig; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - id: data?.id, - name: data?.name || "", - configType: "webhook", - url: config.url, - }, - }); - - const onSubmit = async (data: z.infer) => { - const req: AccessModel = { - id: data.id as string, - name: data.name, - configType: data.configType, - usage: accessProvidersMap.get(data.configType)!.usage, - config: { - url: data.url, - }, - }; - - 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; - } - createAccess(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, { - type: "manual", - message: value.message, - }); - }); - } - }; - - return ( - <> -
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.config.label")} - - - - - - - )} - /> - - ( - - {t("access.authorization.form.webhook_url.label")} - - - - - - - )} - /> - -
- -
- - - - ); -}; - -export default AccessWebhookForm; diff --git a/ui/src/components/notification/NotifyChannelEditForm.tsx b/ui/src/components/notification/NotifyChannelEditForm.tsx new file mode 100644 index 00000000..d93a6732 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditForm.tsx @@ -0,0 +1,100 @@ +import { forwardRef, useImperativeHandle, useMemo, useState } from "react"; +import { useCreation, useDeepCompareEffect } from "ahooks"; +import { Form } from "antd"; + +import { NOTIFY_CHANNELS, type NotifyChannelsSettingsContent } from "@/domain/settings"; +import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields"; +import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields"; +import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields"; +import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields"; +import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields"; +import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields"; +import NotifyChannelEditFormWebhookFields from "./NotifyChannelEditFormWebhookFields"; +import NotifyChannelEditFormWeComFields from "./NotifyChannelEditFormWeComFields"; + +type NotifyChannelEditFormModelType = NotifyChannelsSettingsContent[keyof NotifyChannelsSettingsContent]; + +export type NotifyChannelEditFormProps = { + className?: string; + style?: React.CSSProperties; + channel: string; + disabled?: boolean; + loading?: boolean; + model?: NotifyChannelEditFormModelType; + onModelChange?: (model: NotifyChannelEditFormModelType) => void; +}; + +export type NotifyChannelEditFormInstance = { + getFieldsValue: () => NotifyChannelEditFormModelType; + resetFields: () => void; + validateFields: () => Promise; +}; + +const NotifyChannelEditForm = forwardRef( + ({ className, style, channel, disabled, loading, model, onModelChange }, ref) => { + const [form] = Form.useForm(); + const formName = useCreation(() => `notifyChannelEditForm_${Math.random().toString(36).substring(2, 10)}${new Date().getTime()}`, []); + const formFieldsComponent = useMemo(() => { + /* + 注意:如果追加新的子组件,请保持以 ASCII 排序。 + NOTICE: If you add new child component, please keep ASCII order. + */ + switch (channel) { + case NOTIFY_CHANNELS.BARK: + return ; + case NOTIFY_CHANNELS.DINGTALK: + return ; + case NOTIFY_CHANNELS.EMAIL: + return ; + case NOTIFY_CHANNELS.LARK: + return ; + case NOTIFY_CHANNELS.SERVERCHAN: + return ; + case NOTIFY_CHANNELS.TELEGRAM: + return ; + case NOTIFY_CHANNELS.WEBHOOK: + return ; + case NOTIFY_CHANNELS.WECOM: + return ; + } + }, [channel]); + + const [initialValues, setInitialValues] = useState(model); + useDeepCompareEffect(() => { + setInitialValues(model); + }, [model]); + + const handleFormChange = (_: unknown, fields: NotifyChannelEditFormModelType) => { + onModelChange?.(fields); + }; + + useImperativeHandle(ref, () => ({ + getFieldsValue: () => { + return form.getFieldsValue(true); + }, + resetFields: () => { + return form.resetFields(); + }, + validateFields: () => { + return form.validateFields(); + }, + })); + + return ( +
+ {formFieldsComponent} +
+ ); + } +); + +export default NotifyChannelEditForm; diff --git a/ui/src/components/notification/NotifyChannelEditFormBarkFields.tsx b/ui/src/components/notification/NotifyChannelEditFormBarkFields.tsx new file mode 100644 index 00000000..dd7ae6c5 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormBarkFields.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormBarkFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + serverUrl: z + .string({ message: t("settings.notification.channel.form.bark_server_url.placeholder") }) + .url({ message: t("common.errmsg.url_invalid") }) + .nullish(), + deviceKey: z + .string({ message: t("settings.notification.channel.form.bark_device_key.placeholder") }) + .min(1, t("settings.notification.channel.form.bark_device_key.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + } + > + + + + ); +}; + +export default NotifyChannelEditFormBarkFields; diff --git a/ui/src/components/notification/NotifyChannelEditFormDingTalkFields.tsx b/ui/src/components/notification/NotifyChannelEditFormDingTalkFields.tsx new file mode 100644 index 00000000..9543f142 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormDingTalkFields.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormDingTalkFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessToken: z + .string({ message: t("settings.notification.channel.form.dingtalk_access_token.placeholder") }) + .min(1, t("settings.notification.channel.form.dingtalk_access_token.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + secret: z + .string({ message: t("settings.notification.channel.form.dingtalk_secret.placeholder") }) + .min(1, t("settings.notification.channel.form.dingtalk_secret.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + } + > + + + + ); +}; + +export default NotifyChannelEditFormDingTalkFields; diff --git a/ui/src/components/notification/NotifyChannelEditFormEmailFields.tsx b/ui/src/components/notification/NotifyChannelEditFormEmailFields.tsx new file mode 100644 index 00000000..524d84ae --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormEmailFields.tsx @@ -0,0 +1,96 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input, InputNumber, Switch } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormEmailFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + smtpHost: z + .string({ message: t("settings.notification.channel.form.email_smtp_host.placeholder") }) + .min(1, t("settings.notification.channel.form.email_smtp_host.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + smtpPort: z + .number({ message: t("settings.notification.channel.form.email_smtp_port.placeholder") }) + .int() + .gte(1, t("common.errmsg.port_invalid")) + .lte(65535, t("common.errmsg.port_invalid")) + .transform((v) => +v), + smtpTLS: z.boolean().nullish(), + username: z + .string({ message: t("settings.notification.channel.form.email_username.placeholder") }) + .min(1, t("settings.notification.channel.form.email_username.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + password: z + .string({ message: t("settings.notification.channel.form.email_password.placeholder") }) + .min(1, t("settings.notification.channel.form.email_password.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + senderAddress: z + .string({ message: t("settings.notification.channel.form.email_sender_address.placeholder") }) + .min(1, t("settings.notification.channel.form.email_sender_address.placeholder")) + .email({ message: t("common.errmsg.email_invalid") }), + receiverAddress: z + .string({ message: t("settings.notification.channel.form.email_receiver_address.placeholder") }) + .min(1, t("settings.notification.channel.form.email_receiver_address.placeholder")) + .email({ message: t("common.errmsg.email_invalid") }), + }); + const formRule = createSchemaFieldRule(formSchema); + const form = Form.useFormInstance(); + + const handleTLSSwitchChange = (checked: boolean) => { + const oldPort = form.getFieldValue("smtpPort"); + const newPort = checked && (oldPort == null || oldPort === 25) ? 465 : !checked && (oldPort == null || oldPort === 465) ? 25 : oldPort; + if (newPort !== oldPort) { + form.setFieldValue("smtpPort", newPort); + } + }; + + return ( + <> +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ +
+
+ + + +
+ +
+ + + +
+
+ + + + + + + + + + ); +}; + +export default NotifyChannelEditFormEmailFields; diff --git a/ui/src/components/notification/NotifyChannelEditFormLarkFields.tsx b/ui/src/components/notification/NotifyChannelEditFormLarkFields.tsx new file mode 100644 index 00000000..1feaccce --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormLarkFields.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormLarkFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + webhookUrl: z + .string({ message: t("settings.notification.channel.form.lark_webhook_url.placeholder") }) + .min(1, t("settings.notification.channel.form.lark_webhook_url.placeholder")) + .url({ message: t("common.errmsg.url_invalid") }), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + ); +}; + +export default NotifyChannelEditFormLarkFields; diff --git a/ui/src/components/notification/NotifyChannelEditFormServerChanFields.tsx b/ui/src/components/notification/NotifyChannelEditFormServerChanFields.tsx new file mode 100644 index 00000000..bf6b49b9 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormServerChanFields.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormServerChanFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + url: z + .string({ message: t("settings.notification.channel.form.serverchan_url.placeholder") }) + .min(1, t("settings.notification.channel.form.serverchan_url.placeholder")) + .url({ message: t("common.errmsg.url_invalid") }), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + ); +}; + +export default NotifyChannelEditFormServerChanFields; diff --git a/ui/src/components/notification/NotifyChannelEditFormTelegramFields.tsx b/ui/src/components/notification/NotifyChannelEditFormTelegramFields.tsx new file mode 100644 index 00000000..4d6b31f4 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormTelegramFields.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormTelegramFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiToken: z + .string({ message: t("settings.notification.channel.form.telegram_api_token.placeholder") }) + .min(1, t("settings.notification.channel.form.telegram_api_token.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + chatId: z + .string({ message: t("settings.notification.channel.form.telegram_chat_id.placeholder") }) + .min(1, t("settings.notification.channel.form.telegram_chat_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + } + > + + + + ); +}; + +export default NotifyChannelEditFormTelegramFields; diff --git a/ui/src/components/notification/NotifyChannelEditFormWeComFields.tsx b/ui/src/components/notification/NotifyChannelEditFormWeComFields.tsx new file mode 100644 index 00000000..1dddbbc4 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormWeComFields.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormWeComFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + webhookUrl: z + .string({ message: t("settings.notification.channel.form.wecom_webhook_url.placeholder") }) + .min(1, t("settings.notification.channel.form.wecom_webhook_url.placeholder")) + .url({ message: t("common.errmsg.url_invalid") }), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + ); +}; + +export default NotifyChannelEditFormWeComFields; diff --git a/ui/src/components/notification/NotifyChannelEditFormWebhookFields.tsx b/ui/src/components/notification/NotifyChannelEditFormWebhookFields.tsx new file mode 100644 index 00000000..322781cb --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormWebhookFields.tsx @@ -0,0 +1,26 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormWebhookFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + url: z + .string({ message: t("settings.notification.channel.form.webhook_url.placeholder") }) + .min(1, t("settings.notification.channel.form.webhook_url.placeholder")) + .url({ message: t("common.errmsg.url_invalid") }), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( +
+ + + +
+ ); +}; + +export default NotifyChannelEditFormWebhookFields; diff --git a/ui/src/components/notification/NotifyChannels.tsx b/ui/src/components/notification/NotifyChannels.tsx new file mode 100644 index 00000000..76d09817 --- /dev/null +++ b/ui/src/components/notification/NotifyChannels.tsx @@ -0,0 +1,117 @@ +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareMemo } from "@ant-design/pro-components"; +import { Button, Collapse, message, notification, Skeleton, Space, Switch, type CollapseProps } from "antd"; + +import NotifyChannelEditForm, { type NotifyChannelEditFormInstance } from "./NotifyChannelEditForm"; +import NotifyTestButton from "./NotifyTestButton"; +import { notifyChannelsMap } from "@/domain/settings"; +import { useNotifyChannelStore } from "@/stores/notify"; +import { getErrMsg } from "@/utils/error"; + +type NotifyChannelProps = { + className?: string; + style?: React.CSSProperties; + channel: string; +}; + +const NotifyChannel = ({ className, style, channel }: NotifyChannelProps) => { + const { t } = useTranslation(); + + const [messageApi, MessageContextHolder] = message.useMessage(); + const [notificationApi, NotificationContextHolder] = notification.useNotification(); + + const { channels, setChannel } = useNotifyChannelStore(); + + const channelConfig = useDeepCompareMemo(() => channels[channel], [channels, channel]); + const [channelFormChanged, setChannelFormChanged] = useState(false); + const channelFormRef = useRef(null); + + const handleClickSubmit = async () => { + await channelFormRef.current!.validateFields(); + + try { + setChannel(channel, channelFormRef.current!.getFieldsValue()); + setChannelFormChanged(false); + + messageApi.success(t("common.text.operation_succeeded")); + } catch (err) { + notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + } + }; + + return ( +
+ {MessageContextHolder} + {NotificationContextHolder} + + setChannelFormChanged(true)} /> + + + + + {channelConfig != null ? : null} + +
+ ); +}; + +type NotifyChannelsSemanticDOM = "collapse" | "form"; + +export type NotifyChannelsProps = { + className?: string; + classNames?: Partial>; + style?: React.CSSProperties; + styles?: Partial>; +}; + +const NotifyChannels = ({ className, classNames, style, styles }: NotifyChannelsProps) => { + const { t, i18n } = useTranslation(); + + const { initialized, channels, setChannel, fetchChannels } = useNotifyChannelStore(); + useEffect(() => { + fetchChannels(); + }, [fetchChannels]); + + const channelCollapseItems: CollapseProps["items"] = useDeepCompareMemo( + () => + Array.from(notifyChannelsMap.values()).map((channel) => { + return { + key: `channel-${channel.type}`, + label: <>{t(channel.name)}, + children: , + extra: ( +
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} onMouseUp={(e) => e.stopPropagation()}> + handleSwitchChange(channel.type, checked)} + /> +
+ ), + forceRender: true, + }; + }), + [i18n.language, channels] + ); + + const handleSwitchChange = (channel: string, enabled: boolean) => { + setChannel(channel, { enabled }); + }; + + return ( +
+ {!initialized ? ( + + ) : ( + + )} +
+ ); +}; + +export default NotifyChannels; diff --git a/ui/src/components/notification/NotifyTemplate.tsx b/ui/src/components/notification/NotifyTemplate.tsx new file mode 100644 index 00000000..21a29cd3 --- /dev/null +++ b/ui/src/components/notification/NotifyTemplate.tsx @@ -0,0 +1,128 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useRequest } from "ahooks"; +import { Button, Form, Input, message, notification, Skeleton } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; +import { ClientResponseError } from "pocketbase"; + +import { defaultNotifyTemplate, SETTINGS_NAMES, type NotifyTemplatesSettingsContent } from "@/domain/settings"; +import { get as getSettings, save as saveSettings } from "@/repository/settings"; +import { getErrMsg } from "@/utils/error"; + +export type NotifyTemplateFormProps = { + className?: string; + style?: React.CSSProperties; +}; + +const NotifyTemplateForm = ({ className, style }: NotifyTemplateFormProps) => { + const { t } = useTranslation(); + + const [messageApi, MessageContextHolder] = message.useMessage(); + const [notificationApi, NotificationContextHolder] = notification.useNotification(); + + const formSchema = z.object({ + subject: z + .string() + .trim() + .min(1, t("settings.notification.template.form.subject.placeholder")) + .max(1000, t("common.errmsg.string_max", { max: 1000 })), + message: z + .string() + .trim() + .min(1, t("settings.notification.template.form.message.placeholder")) + .max(1000, t("common.errmsg.string_max", { max: 1000 })), + }); + const formRule = createSchemaFieldRule(formSchema); + const [form] = Form.useForm>(); + const [formPending, setFormPending] = useState(false); + + const [initialValues, setInitialValues] = useState>>(); + const [initialChanged, setInitialChanged] = useState(false); + + const { loading } = useRequest( + () => { + return getSettings(SETTINGS_NAMES.NOTIFY_TEMPLATES); + }, + { + onError: (err) => { + if (err instanceof ClientResponseError && err.isAbort) { + return; + } + + console.error(err); + }, + onFinally: (_, resp) => { + const template = resp?.content?.notifyTemplates?.[0] ?? defaultNotifyTemplate; + setInitialValues({ ...template }); + }, + } + ); + + const handleInputChange = () => { + setInitialChanged(true); + }; + + const handleFormFinish = async (fields: z.infer) => { + setFormPending(true); + + try { + const settings = await getSettings(SETTINGS_NAMES.NOTIFY_TEMPLATES); + await saveSettings({ + ...settings, + content: { + notifyTemplates: [fields], + }, + }); + + messageApi.success(t("common.text.operation_succeeded")); + } catch (err) { + notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + } finally { + setFormPending(false); + } + }; + + return ( +
+ {MessageContextHolder} + {NotificationContextHolder} + + {loading ? ( + + ) : ( +
+ + + + + + + + + + + +
+ )} +
+ ); +}; + +export default NotifyTemplateForm; diff --git a/ui/src/components/notification/NotifyTestButton.tsx b/ui/src/components/notification/NotifyTestButton.tsx new file mode 100644 index 00000000..f6667aa8 --- /dev/null +++ b/ui/src/components/notification/NotifyTestButton.tsx @@ -0,0 +1,54 @@ +import { useRequest } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { Button, message, notification, type ButtonProps } from "antd"; + +import { notifyTest } from "@/api/notify"; +import { getErrMsg } from "@/utils/error"; + +export type NotifyTestButtonProps = { + className?: string; + style?: React.CSSProperties; + channel: string; + disabled?: boolean; + size?: ButtonProps["size"]; +}; + +const NotifyTestButton = ({ className, style, channel, disabled, size }: NotifyTestButtonProps) => { + const { t } = useTranslation(); + + const [messageApi, MessageContextHolder] = message.useMessage(); + const [notificationApi, NotificationContextHolder] = notification.useNotification(); + + const { loading, run: executeNotifyTest } = useRequest( + () => { + return notifyTest(channel); + }, + { + refreshDeps: [channel], + manual: true, + onSuccess: () => { + messageApi.success(t("settings.notification.push_test.pushed")); + }, + onError: (err) => { + notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + }, + } + ); + + const handleClick = () => { + executeNotifyTest(); + }; + + return ( + <> + {MessageContextHolder} + {NotificationContextHolder} + + + + ); +}; + +export default NotifyTestButton; diff --git a/ui/src/components/notify/Bark.tsx b/ui/src/components/notify/Bark.tsx deleted file mode 100644 index f09f2bce..00000000 --- a/ui/src/components/notify/Bark.tsx +++ /dev/null @@ -1,262 +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 { getErrMsg } from "@/utils/error"; -import { NotifyChannels, NotifyChannelBark } from "@/domain/settings"; -import { save } from "@/repository/settings"; -import { useNotifyContext } from "@/providers/notify"; -import { notifyTest } from "@/api/notify"; -import Show from "@/components/Show"; - -type BarkSetting = { - id: string; - name: string; - data: NotifyChannelBark; -}; - -const Bark = () => { - const { config, setChannels } = useNotifyContext(); - const { t } = useTranslation(); - - const [changed, setChanged] = useState(false); - - const [bark, setBark] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - serverUrl: "", - deviceKey: "", - enabled: false, - }, - }); - - const [originBark, setOriginBark] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - serverUrl: "", - deviceKey: "", - enabled: false, - }, - }); - - useEffect(() => { - setChanged(false); - }, [config]); - - useEffect(() => { - const data = getDetailBark(); - setOriginBark({ - id: config.id ?? "", - name: "common.provider.bark", - data, - }); - }, [config]); - - useEffect(() => { - const data = getDetailBark(); - setBark({ - id: config.id ?? "", - name: "common.provider.bark", - data, - }); - }, [config]); - - const { toast } = useToast(); - - const checkChanged = (data: NotifyChannelBark) => { - if (data.serverUrl !== originBark.data.serverUrl || data.deviceKey !== originBark.data.deviceKey) { - setChanged(true); - } else { - setChanged(false); - } - }; - - const getDetailBark = () => { - const df: NotifyChannelBark = { - serverUrl: "", - deviceKey: "", - enabled: false, - }; - if (!config.content) { - return df; - } - const chanels = config.content as NotifyChannels; - if (!chanels.bark) { - return df; - } - - return chanels.bark as NotifyChannelBark; - }; - - const handleSaveClick = async () => { - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - bark: { - ...bark.data, - }, - }, - }); - - setChannels(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.config.saved.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - const [testing, setTesting] = useState(false); - const handlePushTestClick = async () => { - if (testing) return; - - try { - setTesting(true); - - await notifyTest("bark"); - - toast({ - title: t("settings.notification.push_test_message.succeeded.message"), - description: t("settings.notification.push_test_message.succeeded.message"), - }); - } catch (e) { - const msg = getErrMsg(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 = { - ...bark, - data: { - ...bark.data, - enabled: !bark.data.enabled, - }, - }; - setBark(newData); - - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - bark: { - ...newData.data, - }, - }, - }); - - setChannels(resp); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - return ( -
-
- - { - const newData = { - ...bark, - data: { - ...bark.data, - serverUrl: e.target.value, - }, - }; - - checkChanged(newData.data); - setBark(newData); - }} - /> -
- -
- - { - const newData = { - ...bark, - data: { - ...bark.data, - deviceKey: e.target.value, - }, - }; - - checkChanged(newData.data); - setBark(newData); - }} - /> -
- -
-
- - -
- -
- - - - - - - -
-
-
- ); -}; - -export default Bark; diff --git a/ui/src/components/notify/DingTalk.tsx b/ui/src/components/notify/DingTalk.tsx deleted file mode 100644 index 04e5b577..00000000 --- a/ui/src/components/notify/DingTalk.tsx +++ /dev/null @@ -1,260 +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 { getErrMsg } from "@/utils/error"; -import { NotifyChannelDingTalk, NotifyChannels } from "@/domain/settings"; -import { useNotifyContext } from "@/providers/notify"; -import { save } from "@/repository/settings"; -import Show from "@/components/Show"; -import { notifyTest } from "@/api/notify"; - -type DingTalkSetting = { - id: string; - name: string; - data: NotifyChannelDingTalk; -}; - -const DingTalk = () => { - const { config, setChannels } = useNotifyContext(); - const { t } = useTranslation(); - - const [changed, setChanged] = useState(false); - - const [dingtalk, setDingtalk] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - accessToken: "", - secret: "", - enabled: false, - }, - }); - - const [originDingtalk, setOriginDingtalk] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - accessToken: "", - secret: "", - enabled: false, - }, - }); - - useEffect(() => { - setChanged(false); - }, [config]); - - useEffect(() => { - const data = getDetailDingTalk(); - setOriginDingtalk({ - id: config.id ?? "", - name: "dingtalk", - data, - }); - }, [config]); - - useEffect(() => { - const data = getDetailDingTalk(); - setDingtalk({ - id: config.id ?? "", - name: "dingtalk", - data, - }); - }, [config]); - - const { toast } = useToast(); - - const getDetailDingTalk = () => { - const df: NotifyChannelDingTalk = { - accessToken: "", - secret: "", - enabled: false, - }; - if (!config.content) { - return df; - } - const chanels = config.content as NotifyChannels; - if (!chanels.dingtalk) { - return df; - } - - return chanels.dingtalk as NotifyChannelDingTalk; - }; - - const checkChanged = (data: NotifyChannelDingTalk) => { - if (data.accessToken !== originDingtalk.data.accessToken || data.secret !== originDingtalk.data.secret) { - setChanged(true); - } else { - setChanged(false); - } - }; - - const handleSaveClick = async () => { - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - dingtalk: { - ...dingtalk.data, - }, - }, - }); - - setChannels(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.config.saved.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } finally { - setTesting(false); - } - }; - - const [testing, setTesting] = useState(false); - const handlePushTestClick = async () => { - if (testing) return; - - try { - setTesting(true); - - await notifyTest("dingtalk"); - - toast({ - title: t("settings.notification.push_test_message.succeeded.message"), - description: t("settings.notification.push_test_message.succeeded.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("settings.notification.push_test_message.failed.message"), - description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - const handleSwitchChange = async () => { - const newData = { - ...dingtalk, - data: { - ...dingtalk.data, - enabled: !dingtalk.data.enabled, - }, - }; - setDingtalk(newData); - - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - dingtalk: { - ...newData.data, - }, - }, - }); - - setChannels(resp); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - return ( -
-
- - { - const newData = { - ...dingtalk, - data: { - ...dingtalk.data, - accessToken: e.target.value, - }, - }; - checkChanged(newData.data); - setDingtalk(newData); - }} - /> -
- -
- - { - const newData = { - ...dingtalk, - data: { - ...dingtalk.data, - secret: e.target.value, - }, - }; - checkChanged(newData.data); - setDingtalk(newData); - }} - /> -
- -
-
- - -
- -
- - - - - - - -
-
-
- ); -}; - -export default DingTalk; diff --git a/ui/src/components/notify/Email.tsx b/ui/src/components/notify/Email.tsx deleted file mode 100644 index 3defc2f3..00000000 --- a/ui/src/components/notify/Email.tsx +++ /dev/null @@ -1,384 +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 { getErrMsg } from "@/utils/error"; -import { NotifyChannelEmail, NotifyChannels } from "@/domain/settings"; -import { useNotifyContext } from "@/providers/notify"; -import { save } 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(false); - - const [mail, setMail] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - smtpHost: "", - smtpPort: 465, - smtpTLS: true, - username: "", - password: "", - senderAddress: "", - receiverAddress: "", - enabled: false, - }, - }); - - const [originMail, setOriginMail] = useState({ - 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 save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - email: { - ...mail.data, - }, - }, - }); - - setChannels(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.config.saved.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - const [testing, setTesting] = useState(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 = getErrMsg(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 save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - email: { - ...newData.data, - }, - }, - }); - - setChannels(resp); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - return ( -
-
-
- - { - const newData = { - ...mail, - data: { - ...mail.data, - smtpHost: e.target.value, - }, - }; - checkChanged(newData.data); - setMail(newData); - }} - /> -
- -
- - { - const newData = { - ...mail, - data: { - ...mail.data, - smtpPort: +e.target.value || 0, - }, - }; - checkChanged(newData.data); - setMail(newData); - }} - /> -
- -
- - { - 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); - }} - /> -
-
- -
-
- - { - const newData = { - ...mail, - data: { - ...mail.data, - username: e.target.value, - }, - }; - checkChanged(newData.data); - setMail(newData); - }} - /> -
- -
- - { - const newData = { - ...mail, - data: { - ...mail.data, - password: e.target.value, - }, - }; - checkChanged(newData.data); - setMail(newData); - }} - /> -
-
- -
- - { - const newData = { - ...mail, - data: { - ...mail.data, - senderAddress: e.target.value, - }, - }; - checkChanged(newData.data); - setMail(newData); - }} - /> -
- -
- - { - const newData = { - ...mail, - data: { - ...mail.data, - receiverAddress: e.target.value, - }, - }; - checkChanged(newData.data); - setMail(newData); - }} - /> -
- -
-
- - -
- -
- - - - - - - -
-
-
- ); -}; - -export default Mail; diff --git a/ui/src/components/notify/Lark.tsx b/ui/src/components/notify/Lark.tsx deleted file mode 100644 index e70278e9..00000000 --- a/ui/src/components/notify/Lark.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { Switch } from "@/components/ui/switch"; -import { Label } from "@/components/ui/label"; -import { useNotifyContext } from "@/providers/notify"; -import { NotifyChannelLark, NotifyChannels } from "@/domain/settings"; -import { useEffect, useState } from "react"; -import { save } from "@/repository/settings"; -import { getErrMsg } from "@/utils/error"; -import { useToast } from "@/components/ui/use-toast"; -import { useTranslation } from "react-i18next"; -import { notifyTest } from "@/api/notify"; -import Show from "@/components/Show"; - -type LarkSetting = { - id: string; - name: string; - data: NotifyChannelLark; -}; - -const Lark = () => { - const { config, setChannels } = useNotifyContext(); - const { t } = useTranslation(); - - const [changed, setChanged] = useState(false); - - const [lark, setLark] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - webhookUrl: "", - enabled: false, - }, - }); - - const [originLark, setOriginLark] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - webhookUrl: "", - enabled: false, - }, - }); - - useEffect(() => { - setChanged(false); - }, [config]); - - useEffect(() => { - const data = getDetailLark(); - setOriginLark({ - id: config.id ?? "", - name: "lark", - data, - }); - }, [config]); - - useEffect(() => { - const data = getDetailLark(); - setLark({ - id: config.id ?? "", - name: "lark", - data, - }); - }, [config]); - - const { toast } = useToast(); - - const checkChanged = (data: NotifyChannelLark) => { - if (data.webhookUrl !== originLark.data.webhookUrl) { - setChanged(true); - } else { - setChanged(false); - } - }; - - const getDetailLark = () => { - const df: NotifyChannelLark = { - webhookUrl: "", - enabled: false, - }; - if (!config.content) { - return df; - } - const chanels = config.content as NotifyChannels; - if (!chanels.lark) { - return df; - } - - return chanels.lark as NotifyChannelLark; - }; - - const handleSaveClick = async () => { - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - lark: { - ...lark.data, - }, - }, - }); - - setChannels(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.config.saved.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } finally { - setTesting(false); - } - }; - - const [testing, setTesting] = useState(false); - const handlePushTestClick = async () => { - if (testing) return; - - try { - setTesting(true); - - await notifyTest("lark"); - - toast({ - title: t("settings.notification.push_test_message.succeeded.message"), - description: t("settings.notification.push_test_message.succeeded.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("settings.notification.push_test_message.failed.message"), - description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - const handleSwitchChange = async () => { - const newData = { - ...lark, - data: { - ...lark.data, - enabled: !lark.data.enabled, - }, - }; - setLark(newData); - - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - lark: { - ...newData.data, - }, - }, - }); - - setChannels(resp); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - return ( -
-
- - { - const newData = { - ...lark, - data: { - ...lark.data, - webhookUrl: e.target.value, - }, - }; - - checkChanged(newData.data); - setLark(newData); - }} - /> -
- -
-
- - -
- -
- - - - - - - -
-
-
- ); -}; - -export default Lark; diff --git a/ui/src/components/notify/NotifyTemplate.tsx b/ui/src/components/notify/NotifyTemplate.tsx deleted file mode 100644 index 1ca6bd63..00000000 --- a/ui/src/components/notify/NotifyTemplate.tsx +++ /dev/null @@ -1,97 +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 { Textarea } from "@/components/ui/textarea"; -import { useToast } from "@/components/ui/use-toast"; -import { defaultNotifyTemplate, NotifyTemplates, NotifyTemplate as NotifyTemplateT } from "@/domain/settings"; -import { get, save } from "@/repository/settings"; - -const NotifyTemplate = () => { - const [id, setId] = useState(""); - const [templates, setTemplates] = useState([defaultNotifyTemplate]); - - const { toast } = useToast(); - const { t } = useTranslation(); - - useEffect(() => { - const featchData = async () => { - const resp = await get("templates"); - - if (resp.content) { - setTemplates((resp.content as NotifyTemplates).notifyTemplates); - setId(resp.id ? resp.id : ""); - } - }; - featchData(); - }, []); - - const handleTitleChange = (val: string) => { - const template = templates[0]; - - setTemplates([ - { - ...template, - title: val, - }, - ]); - }; - - const handleContentChange = (val: string) => { - const template = templates[0]; - - setTemplates([ - { - ...template, - content: val, - }, - ]); - }; - - const handleSaveClick = async () => { - const resp = await save({ - id: id, - content: { - notifyTemplates: templates, - }, - name: "templates", - }); - - if (resp.id) { - setId(resp.id); - } - - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.template.saved.message"), - }); - }; - - return ( -
- { - handleTitleChange(e.target.value); - }} - /> - -
{t("settings.notification.template.variables.tips.title")}
- - -
{t("settings.notification.template.variables.tips.content")}
-
- -
-
- ); -}; - -export default NotifyTemplate; diff --git a/ui/src/components/notify/ServerChan.tsx b/ui/src/components/notify/ServerChan.tsx deleted file mode 100644 index cb2ff1a6..00000000 --- a/ui/src/components/notify/ServerChan.tsx +++ /dev/null @@ -1,249 +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 { getErrMsg } from "@/utils/error"; -import { isValidURL } from "@/utils/url"; -import { NotifyChannels, NotifyChannelServerChan } from "@/domain/settings"; -import { save } from "@/repository/settings"; -import { useNotifyContext } from "@/providers/notify"; -import { notifyTest } from "@/api/notify"; -import Show from "@/components/Show"; - -type ServerChanSetting = { - id: string; - name: string; - data: NotifyChannelServerChan; -}; - -const ServerChan = () => { - const { config, setChannels } = useNotifyContext(); - const { t } = useTranslation(); - const [changed, setChanged] = useState(false); - - const [serverchan, setServerChan] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - url: "", - enabled: false, - }, - }); - - const [originServerChan, setOriginServerChan] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - url: "", - enabled: false, - }, - }); - - useEffect(() => { - setChanged(false); - }, [config]); - - useEffect(() => { - const data = getDetailServerChan(); - setOriginServerChan({ - id: config.id ?? "", - name: "serverchan", - data, - }); - }, [config]); - - useEffect(() => { - const data = getDetailServerChan(); - setServerChan({ - id: config.id ?? "", - name: "serverchan", - data, - }); - }, [config]); - - const { toast } = useToast(); - - const checkChanged = (data: NotifyChannelServerChan) => { - if (data.url !== originServerChan.data.url) { - setChanged(true); - } else { - setChanged(false); - } - }; - - const getDetailServerChan = () => { - const df: NotifyChannelServerChan = { - url: "", - enabled: false, - }; - if (!config.content) { - return df; - } - const chanels = config.content as NotifyChannels; - if (!chanels.serverchan) { - return df; - } - - return chanels.serverchan as NotifyChannelServerChan; - }; - - const handleSaveClick = async () => { - try { - serverchan.data.url = serverchan.data.url.trim(); - if (!isValidURL(serverchan.data.url)) { - toast({ - title: t("common.text.operation_failed"), - description: t("common.errmsg.url_invalid"), - variant: "destructive", - }); - return; - } - - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - serverchan: { - ...serverchan.data, - }, - }, - }); - - setChannels(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.config.saved.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - const [testing, setTesting] = useState(false); - const handlePushTestClick = async () => { - if (testing) return; - - try { - setTesting(true); - - await notifyTest("serverchan"); - - toast({ - title: t("settings.notification.push_test_message.succeeded.message"), - description: t("settings.notification.push_test_message.succeeded.message"), - }); - } catch (e) { - const msg = getErrMsg(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 = { - ...serverchan, - data: { - ...serverchan.data, - enabled: !serverchan.data.enabled, - }, - }; - setServerChan(newData); - - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - serverchan: { - ...newData.data, - }, - }, - }); - - setChannels(resp); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - return ( -
-
- - { - const newData = { - ...serverchan, - data: { - ...serverchan.data, - url: e.target.value, - }, - }; - - checkChanged(newData.data); - setServerChan(newData); - }} - /> -
- -
-
- - -
- -
- - - - - - - -
-
-
- ); -}; - -export default ServerChan; diff --git a/ui/src/components/notify/Telegram.tsx b/ui/src/components/notify/Telegram.tsx deleted file mode 100644 index 0fb3ad40..00000000 --- a/ui/src/components/notify/Telegram.tsx +++ /dev/null @@ -1,262 +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 { getErrMsg } from "@/utils/error"; -import { NotifyChannels, NotifyChannelTelegram } from "@/domain/settings"; -import { save } from "@/repository/settings"; -import { useNotifyContext } from "@/providers/notify"; -import { notifyTest } from "@/api/notify"; -import Show from "@/components/Show"; - -type TelegramSetting = { - id: string; - name: string; - data: NotifyChannelTelegram; -}; - -const Telegram = () => { - const { config, setChannels } = useNotifyContext(); - const { t } = useTranslation(); - - const [changed, setChanged] = useState(false); - - const [telegram, setTelegram] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - apiToken: "", - chatId: "", - enabled: false, - }, - }); - - const [originTelegram, setOriginTelegram] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - apiToken: "", - chatId: "", - enabled: false, - }, - }); - - useEffect(() => { - setChanged(false); - }, [config]); - - useEffect(() => { - const data = getDetailTelegram(); - setOriginTelegram({ - id: config.id ?? "", - name: "common.provider.telegram", - data, - }); - }, [config]); - - useEffect(() => { - const data = getDetailTelegram(); - setTelegram({ - id: config.id ?? "", - name: "common.provider.telegram", - data, - }); - }, [config]); - - const { toast } = useToast(); - - const checkChanged = (data: NotifyChannelTelegram) => { - if (data.apiToken !== originTelegram.data.apiToken || data.chatId !== originTelegram.data.chatId) { - setChanged(true); - } else { - setChanged(false); - } - }; - - const getDetailTelegram = () => { - const df: NotifyChannelTelegram = { - apiToken: "", - chatId: "", - enabled: false, - }; - if (!config.content) { - return df; - } - const chanels = config.content as NotifyChannels; - if (!chanels.telegram) { - return df; - } - - return chanels.telegram as NotifyChannelTelegram; - }; - - const handleSaveClick = async () => { - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - telegram: { - ...telegram.data, - }, - }, - }); - - setChannels(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.config.saved.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - const [testing, setTesting] = useState(false); - const handlePushTestClick = async () => { - if (testing) return; - - try { - setTesting(true); - - await notifyTest("telegram"); - - toast({ - title: t("settings.notification.push_test_message.succeeded.message"), - description: t("settings.notification.push_test_message.succeeded.message"), - }); - } catch (e) { - const msg = getErrMsg(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 = { - ...telegram, - data: { - ...telegram.data, - enabled: !telegram.data.enabled, - }, - }; - setTelegram(newData); - - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - telegram: { - ...newData.data, - }, - }, - }); - - setChannels(resp); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - return ( -
-
- - { - const newData = { - ...telegram, - data: { - ...telegram.data, - apiToken: e.target.value, - }, - }; - - checkChanged(newData.data); - setTelegram(newData); - }} - /> -
- -
- - { - const newData = { - ...telegram, - data: { - ...telegram.data, - chatId: e.target.value, - }, - }; - - checkChanged(newData.data); - setTelegram(newData); - }} - /> -
- -
-
- - -
- -
- - - - - - - -
-
-
- ); -}; - -export default Telegram; diff --git a/ui/src/components/notify/Webhook.tsx b/ui/src/components/notify/Webhook.tsx deleted file mode 100644 index 13145f21..00000000 --- a/ui/src/components/notify/Webhook.tsx +++ /dev/null @@ -1,249 +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 { getErrMsg } from "@/utils/error"; -import { isValidURL } from "@/utils/url"; -import { NotifyChannels, NotifyChannelWebhook } from "@/domain/settings"; -import { save } from "@/repository/settings"; -import { useNotifyContext } from "@/providers/notify"; -import { notifyTest } from "@/api/notify"; -import Show from "@/components/Show"; - -type WebhookSetting = { - id: string; - name: string; - data: NotifyChannelWebhook; -}; - -const Webhook = () => { - const { config, setChannels } = useNotifyContext(); - const { t } = useTranslation(); - const [changed, setChanged] = useState(false); - - const [webhook, setWebhook] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - url: "", - enabled: false, - }, - }); - - const [originWebhook, setOriginWebhook] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - url: "", - enabled: false, - }, - }); - - useEffect(() => { - setChanged(false); - }, [config]); - - useEffect(() => { - const data = getDetailWebhook(); - setOriginWebhook({ - id: config.id ?? "", - name: "webhook", - data, - }); - }, [config]); - - useEffect(() => { - const data = getDetailWebhook(); - setWebhook({ - id: config.id ?? "", - name: "webhook", - data, - }); - }, [config]); - - const { toast } = useToast(); - - const checkChanged = (data: NotifyChannelWebhook) => { - if (data.url !== originWebhook.data.url) { - setChanged(true); - } else { - setChanged(false); - } - }; - - const getDetailWebhook = () => { - const df: NotifyChannelWebhook = { - url: "", - enabled: false, - }; - if (!config.content) { - return df; - } - const chanels = config.content as NotifyChannels; - if (!chanels.webhook) { - return df; - } - - return chanels.webhook as NotifyChannelWebhook; - }; - - const handleSaveClick = async () => { - try { - webhook.data.url = webhook.data.url.trim(); - if (!isValidURL(webhook.data.url)) { - toast({ - title: t("common.text.operation_failed"), - description: t("common.errmsg.url_invalid"), - variant: "destructive", - }); - return; - } - - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - webhook: { - ...webhook.data, - }, - }, - }); - - setChannels(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.config.saved.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - const [testing, setTesting] = useState(false); - const handlePushTestClick = async () => { - if (testing) return; - - try { - setTesting(true); - - await notifyTest("webhook"); - - toast({ - title: t("settings.notification.push_test_message.succeeded.message"), - description: t("settings.notification.push_test_message.succeeded.message"), - }); - } catch (e) { - const msg = getErrMsg(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 = { - ...webhook, - data: { - ...webhook.data, - enabled: !webhook.data.enabled, - }, - }; - setWebhook(newData); - - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - webhook: { - ...newData.data, - }, - }, - }); - - setChannels(resp); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - return ( -
-
- - { - const newData = { - ...webhook, - data: { - ...webhook.data, - url: e.target.value, - }, - }; - - checkChanged(newData.data); - setWebhook(newData); - }} - /> -
- -
-
- - -
- -
- - - - - - - -
-
-
- ); -}; - -export default Webhook; diff --git a/ui/src/components/ui/accordion.tsx b/ui/src/components/ui/accordion.tsx deleted file mode 100644 index 1b7fcd37..00000000 --- a/ui/src/components/ui/accordion.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from "react"; -import * as AccordionPrimitive from "@radix-ui/react-accordion"; -import { ChevronDown } from "lucide-react"; - -import { cn } from "./utils"; - -const Accordion = AccordionPrimitive.Root; - -const AccordionItem = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => -); -AccordionItem.displayName = "AccordionItem"; - -const AccordionTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - svg]:rotate-180", className)} - {...props} - > - {children} - - - -)); -AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; - -const AccordionContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - -
{children}
-
-)); - -AccordionContent.displayName = AccordionPrimitive.Content.displayName; - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/ui/src/components/ui/alert.tsx b/ui/src/components/ui/alert.tsx deleted file mode 100644 index e5e48af8..00000000 --- a/ui/src/components/ui/alert.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "./utils"; - -const alertVariants = cva( - "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", - }, - }, - defaultVariants: { - variant: "default", - }, - } -); - -const Alert = React.forwardRef & VariantProps>( - ({ className, variant, ...props }, ref) =>
-); -Alert.displayName = "Alert"; - -const AlertTitle = React.forwardRef>(({ className, ...props }, ref) => ( -
-)); -AlertTitle.displayName = "AlertTitle"; - -const AlertDescription = React.forwardRef>(({ className, ...props }, ref) => ( -
-)); -AlertDescription.displayName = "AlertDescription"; - -export { Alert, AlertTitle, AlertDescription }; diff --git a/ui/src/components/ui/collapsible.tsx b/ui/src/components/ui/collapsible.tsx deleted file mode 100644 index 7d01e110..00000000 --- a/ui/src/components/ui/collapsible.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from "react"; -import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; - -import { cn } from "./utils"; - -const Collapsible = CollapsiblePrimitive.Root; - -const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; - -const CollapsibleContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -CollapsibleContent.displayName = CollapsiblePrimitive.CollapsibleContent.displayName; - -export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/ui/src/components/ui/command.tsx b/ui/src/components/ui/command.tsx deleted file mode 100644 index 423dba05..00000000 --- a/ui/src/components/ui/command.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import * as React from "react"; -import { type DialogProps } from "@radix-ui/react-dialog"; -import { Command as CommandPrimitive } from "cmdk"; -import { Search } from "lucide-react"; - -import { cn } from "./utils"; -import { Dialog, DialogContent } from "@/components/ui/dialog"; - -const Command = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - ) -); -Command.displayName = CommandPrimitive.displayName; - -interface CommandDialogProps extends DialogProps {} - -const CommandDialog = ({ children, ...props }: CommandDialogProps) => { - return ( - - - - {children} - - - - ); -}; - -const CommandInput = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( -
- - -
- ) -); - -CommandInput.displayName = CommandPrimitive.Input.displayName; - -const CommandList = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => -); - -CommandList.displayName = CommandPrimitive.List.displayName; - -const CommandEmpty = React.forwardRef, React.ComponentPropsWithoutRef>( - (props, ref) => -); - -CommandEmpty.displayName = CommandPrimitive.Empty.displayName; - -const CommandGroup = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - ) -); - -CommandGroup.displayName = CommandPrimitive.Group.displayName; - -const CommandSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ); -CommandSeparator.displayName = CommandPrimitive.Separator.displayName; - -const CommandItem = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - ) -); - -CommandItem.displayName = CommandPrimitive.Item.displayName; - -const CommandShortcut = ({ className, ...props }: React.HTMLAttributes) => { - return ; -}; -CommandShortcut.displayName = "CommandShortcut"; - -export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator }; diff --git a/ui/src/components/ui/popover.tsx b/ui/src/components/ui/popover.tsx deleted file mode 100644 index 3f188b41..00000000 --- a/ui/src/components/ui/popover.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from "react"; -import * as PopoverPrimitive from "@radix-ui/react-popover"; - -import { cn } from "./utils"; - -const Popover = PopoverPrimitive.Root; - -const PopoverTrigger = PopoverPrimitive.Trigger; - -const PopoverContent = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, align = "center", sideOffset = 4, ...props }, ref) => ( - - - - ) -); -PopoverContent.displayName = PopoverPrimitive.Content.displayName; - -export { Popover, PopoverTrigger, PopoverContent }; diff --git a/ui/src/components/ui/radio-group.tsx b/ui/src/components/ui/radio-group.tsx deleted file mode 100644 index 7712a518..00000000 --- a/ui/src/components/ui/radio-group.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from "react"; -import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; -import { Circle } from "lucide-react"; - -import { cn } from "./utils"; - -const RadioGroup = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => { - return ; - } -); -RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; - -const RadioGroupItem = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => { - return ( - - - - - - ); - } -); -RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; - -export { RadioGroup, RadioGroupItem }; diff --git a/ui/src/components/ui/separator.tsx b/ui/src/components/ui/separator.tsx deleted file mode 100644 index 57070755..00000000 --- a/ui/src/components/ui/separator.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from "react"; -import * as SeparatorPrimitive from "@radix-ui/react-separator"; - -import { cn } from "./utils"; - -const Separator = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( - - ) -); -Separator.displayName = SeparatorPrimitive.Root.displayName; - -export { Separator }; diff --git a/ui/src/components/ui/switch.tsx b/ui/src/components/ui/switch.tsx deleted file mode 100644 index 6c646216..00000000 --- a/ui/src/components/ui/switch.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from "react"; -import * as SwitchPrimitives from "@radix-ui/react-switch"; - -import { cn } from "./utils"; - -const Switch = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - - - ) -); -Switch.displayName = SwitchPrimitives.Root.displayName; - -export { Switch }; diff --git a/ui/src/components/ui/tabs.tsx b/ui/src/components/ui/tabs.tsx deleted file mode 100644 index f7008692..00000000 --- a/ui/src/components/ui/tabs.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from "react"; -import * as TabsPrimitive from "@radix-ui/react-tabs"; - -import { cn } from "./utils"; - -const Tabs = TabsPrimitive.Root; - -const TabsList = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - ) -); -TabsList.displayName = TabsPrimitive.List.displayName; - -const TabsTrigger = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - ) -); -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; - -const TabsContent = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - ) -); -TabsContent.displayName = TabsPrimitive.Content.displayName; - -export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/ui/src/components/ui/toast.tsx b/ui/src/components/ui/toast.tsx deleted file mode 100644 index 86ad83bb..00000000 --- a/ui/src/components/ui/toast.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import * as React from "react"; -import * as ToastPrimitives from "@radix-ui/react-toast"; -import { X } from "lucide-react"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "./utils"; - -const ToastProvider = ToastPrimitives.Provider; - -const ToastViewport = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - ) -); -ToastViewport.displayName = ToastPrimitives.Viewport.displayName; - -const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", - { - variants: { - variant: { - default: "border bg-background text-foreground", - destructive: "destructive group border-destructive bg-destructive text-destructive-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -); - -const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & VariantProps ->(({ className, variant, ...props }, ref) => { - return ; -}); -Toast.displayName = ToastPrimitives.Root.displayName; - -const ToastAction = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - ) -); -ToastAction.displayName = ToastPrimitives.Action.displayName; - -const ToastClose = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - - - ) -); -ToastClose.displayName = ToastPrimitives.Close.displayName; - -const ToastTitle = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => -); -ToastTitle.displayName = ToastPrimitives.Title.displayName; - -const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ); -ToastDescription.displayName = ToastPrimitives.Description.displayName; - -type ToastProps = React.ComponentPropsWithoutRef; - -type ToastActionElement = React.ReactElement; - -export { type ToastProps, type ToastActionElement, ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction }; diff --git a/ui/src/components/ui/toaster.tsx b/ui/src/components/ui/toaster.tsx deleted file mode 100644 index 1850e503..00000000 --- a/ui/src/components/ui/toaster.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast"; -import { useToast } from "@/components/ui/use-toast"; - -export function Toaster() { - const { toasts } = useToast(); - - return ( - - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - -
- {title && {title}} - {description && {description}} -
- {action} - -
- ); - })} - -
- ); -} diff --git a/ui/src/components/ui/tooltip.tsx b/ui/src/components/ui/tooltip.tsx deleted file mode 100644 index 2390d4cd..00000000 --- a/ui/src/components/ui/tooltip.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from "react"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; - -import { cn } from "./utils"; - -const TooltipProvider = TooltipPrimitive.Provider; - -const Tooltip = TooltipPrimitive.Root; - -const TooltipTrigger = TooltipPrimitive.Trigger; - -const TooltipContent = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, sideOffset = 4, ...props }, ref) => ( - - ) -); -TooltipContent.displayName = TooltipPrimitive.Content.displayName; - -type TooltipFastProps = TooltipPrimitive.TooltipContentProps & - TooltipPrimitive.TooltipProps & - React.RefAttributes & { - contentView?: JSX.Element; - }; - -const TooltipLink = React.forwardRef((props: React.PropsWithChildren, forwardedRef: React.ForwardedRef) => ( - - {props.children} - -)); - -function TooltipFast({ children, contentView, open, defaultOpen, onOpenChange, ...props }: TooltipFastProps) { - return ( - - - - {children} - - - {contentView} - - - - ); -} - -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, TooltipFast }; diff --git a/ui/src/components/ui/use-toast.ts b/ui/src/components/ui/use-toast.ts deleted file mode 100644 index 99abb413..00000000 --- a/ui/src/components/ui/use-toast.ts +++ /dev/null @@ -1,187 +0,0 @@ -// Inspired by react-hot-toast library -import * as React from "react"; - -import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; - -const TOAST_LIMIT = 1; -const TOAST_REMOVE_DELAY = 1000000; - -type ToasterToast = ToastProps & { - id: string; - title?: React.ReactNode; - description?: React.ReactNode; - action?: ToastActionElement; -}; - -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const; - -let count = 0; - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER; - return count.toString(); -} - -type ActionType = typeof actionTypes; - -type Action = - | { - type: ActionType["ADD_TOAST"]; - toast: ToasterToast; - } - | { - type: ActionType["UPDATE_TOAST"]; - toast: Partial; - } - | { - type: ActionType["DISMISS_TOAST"]; - toastId?: ToasterToast["id"]; - } - | { - type: ActionType["REMOVE_TOAST"]; - toastId?: ToasterToast["id"]; - }; - -interface State { - toasts: ToasterToast[]; -} - -const toastTimeouts = new Map>(); - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return; - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId); - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }); - }, TOAST_REMOVE_DELAY); - - toastTimeouts.set(toastId, timeout); -}; - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - }; - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)), - }; - - case "DISMISS_TOAST": { - const { toastId } = action; - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId); - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id); - }); - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - }; - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - }; - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - }; - } -}; - -const listeners: Array<(state: State) => void> = []; - -let memoryState: State = { toasts: [] }; - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action); - listeners.forEach((listener) => { - listener(memoryState); - }); -} - -type Toast = Omit; - -function toast({ ...props }: Toast) { - const id = genId(); - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }); - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss(); - }, - }, - }); - - return { - id: id, - dismiss, - update, - }; -} - -function useToast() { - const [state, setState] = React.useState(memoryState); - - React.useEffect(() => { - listeners.push(setState); - return () => { - const index = listeners.indexOf(setState); - if (index > -1) { - listeners.splice(index, 1); - } - }; - }, [state]); - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - }; -} - -export { useToast, toast }; diff --git a/ui/src/components/workflow/AccessSelect.tsx b/ui/src/components/workflow/AccessSelect.tsx index 8ced0156..827c82ad 100644 --- a/ui/src/components/workflow/AccessSelect.tsx +++ b/ui/src/components/workflow/AccessSelect.tsx @@ -13,7 +13,11 @@ type AccessSelectProps = { const AccessSelect = ({ value, onValueChange, providerType }: AccessSelectProps) => { const [localValue, setLocalValue] = React.useState(""); const { t } = useTranslation(); - const { accesses } = useAccessStore(); + const { accesses, fetchAccesses } = useAccessStore(); + + useEffect(() => { + fetchAccesses(); + }, []); useEffect(() => { setLocalValue(value); diff --git a/ui/src/components/workflow/ApplyForm.tsx b/ui/src/components/workflow/ApplyForm.tsx index fffcd163..baf18d4c 100644 --- a/ui/src/components/workflow/ApplyForm.tsx +++ b/ui/src/components/workflow/ApplyForm.tsx @@ -1,24 +1,22 @@ import { memo, useEffect } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { Collapse, Switch, Tooltip } from "antd"; import z from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; -import { ChevronsUpDown, Plus, CircleHelp } from "lucide-react"; +import { ChevronsUpDown as ChevronsUpDownIcon, Plus as PlusIcon, CircleHelp as CircleHelpIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select"; -import AccessEditDialog from "@/components/certimate/AccessEditDialog"; +import AccessEditModal from "@/components/access/AccessEditModal"; import EmailsEdit from "@/components/certimate/EmailsEdit"; import StringList from "@/components/certimate/StringList"; import { accessProvidersMap } from "@/domain/access"; import { useAccessStore } from "@/stores/access"; import { useContactStore } from "@/stores/contact"; -import { Switch } from "@/components/ui/switch"; -import { TooltipFast } from "@/components/ui/tooltip"; import { WorkflowNode, WorkflowNodeConfig } from "@/domain/workflow"; import { useWorkflowStore, WorkflowState } from "@/stores/workflow"; import { useShallow } from "zustand/shallow"; @@ -122,7 +120,7 @@ const ApplyForm = ({ data }: ApplyFormProps) => { - + {t("common.button.add")}
} @@ -165,14 +163,14 @@ const ApplyForm = ({ data }: ApplyFormProps) => {
{t("domain.application.form.access.label")}
- - + {t("common.button.add")}
} - op="add" /> @@ -211,135 +209,143 @@ const ApplyForm = ({ data }: ApplyFormProps) => {

- - -
- {t("domain.application.form.advanced_settings.label")} - -
-
- -
- {/* 证书算法 */} - ( - - {t("domain.application.form.key_algorithm.label")} - - - )} - /> - - {/* DNS */} - ( - - { - form.setValue("nameservers", val); - }} - valueType="dns" - > - - - - )} - /> - - {/* DNS 超时时间 */} - ( - - {t("domain.application.form.timeout.label")} - - { - form.setValue("timeout", parseInt(e.target.value)); - }} - /> - - - - - )} - /> - - {/* 禁用 CNAME 跟随 */} - ( - - -
- {t("domain.application.form.disable_follow_cname.label")} - - {t("domain.application.form.disable_follow_cname.tips")} - - {t("domain.application.form.disable_follow_cname.tips_link")} - -

- } - > - -
-
-
- -
- { - form.setValue(field.name, value); + {t("domain.application.form.advanced_settings.label")}, + children: ( +
+ {/* 证书算法 */} + ( + + {t("domain.application.form.key_algorithm.label")} + + + )} + /> + + {/* DNS */} + ( + + { + form.setValue("nameservers", val); + }} + valueType="dns" + > + + + + )} + /> + + {/* DNS 超时时间 */} + ( + + {t("domain.application.form.timeout.label")} + + { + form.setValue("timeout", parseInt(e.target.value)); + }} + /> + + + + + )} + /> + + {/* 禁用 CNAME 跟随 */} + ( + + +
+ {t("domain.application.form.disable_follow_cname.label")} + + {t("domain.application.form.disable_follow_cname.tips")} + + {t("domain.application.form.disable_follow_cname.tips_link")} + +

+ } + > + +
+
+
+ +
+ { + form.setValue(field.name, value); + }} + /> +
+
+ +
+ )} + /> +
+ ), + extra: , + forceRender: true, + showArrow: false, + }, + ]} + />
diff --git a/ui/src/components/workflow/DeployForm.tsx b/ui/src/components/workflow/DeployForm.tsx index b73baaa0..ca7846ee 100644 --- a/ui/src/components/workflow/DeployForm.tsx +++ b/ui/src/components/workflow/DeployForm.tsx @@ -60,14 +60,14 @@ const getForm = (data: WorkflowNode, defaultProivder?: string) => { return ; case "webhook": return ; - case "tencent-cdn": - case "tencent-ecdn": + case "tencentcloud-cdn": + case "tencentcloud-ecdn": return ; - case "tencent-clb": + case "tencentcloud-clb": return ; - case "tencent-cos": + case "tencentcloud-cos": return ; - case "tencent-teo": + case "tencentcloud-eo": return ; case "ssh": return ; diff --git a/ui/src/components/workflow/DeployToAliyunALB.tsx b/ui/src/components/workflow/DeployToAliyunALB.tsx index 947c0fcb..270a2e74 100644 --- a/ui/src/components/workflow/DeployToAliyunALB.tsx +++ b/ui/src/components/workflow/DeployToAliyunALB.tsx @@ -15,7 +15,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from " import { Button } from "../ui/button"; import AccessSelect from "./AccessSelect"; -import AccessEditDialog from "../certimate/AccessEditDialog"; +import AccessEditModal from "../access/AccessEditModal"; import { Plus } from "lucide-react"; const selectState = (state: WorkflowState) => ({ @@ -106,15 +106,15 @@ const DeployToAliyunALB = ({ data }: DeployFormProps) => {
{t("domain.deployment.form.access.label")}
- {t("common.button.add")}
} - op="add" - outConfigType="aliyun" /> diff --git a/ui/src/components/workflow/DeployToAliyunCDN.tsx b/ui/src/components/workflow/DeployToAliyunCDN.tsx index 3e4da4b7..5c031833 100644 --- a/ui/src/components/workflow/DeployToAliyunCDN.tsx +++ b/ui/src/components/workflow/DeployToAliyunCDN.tsx @@ -16,7 +16,7 @@ import { useEffect, useState } from "react"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; import { SelectLabel } from "@radix-ui/react-select"; import AccessSelect from "./AccessSelect"; -import AccessEditDialog from "../certimate/AccessEditDialog"; +import AccessEditModal from "../access/AccessEditModal"; import { Plus } from "lucide-react"; const selectState = (state: WorkflowState) => ({ @@ -86,15 +86,15 @@ const DeployToAliyunCDN = ({ data }: DeployFormProps) => {
{t("domain.deployment.form.access.label")}
- {t("common.button.add")}
} - op="add" - outConfigType="aliyun" /> diff --git a/ui/src/components/workflow/DeployToAliyunCLB.tsx b/ui/src/components/workflow/DeployToAliyunCLB.tsx index b7edb9bc..3562978f 100644 --- a/ui/src/components/workflow/DeployToAliyunCLB.tsx +++ b/ui/src/components/workflow/DeployToAliyunCLB.tsx @@ -15,7 +15,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from " import { Button } from "../ui/button"; import AccessSelect from "./AccessSelect"; -import AccessEditDialog from "../certimate/AccessEditDialog"; +import AccessEditModal from "../access/AccessEditModal"; import { Plus } from "lucide-react"; const selectState = (state: WorkflowState) => ({ @@ -106,15 +106,15 @@ const DeployToAliyunCLB = ({ data }: DeployFormProps) => {
{t("domain.deployment.form.access.label")}
- {t("common.button.add")}
} - op="add" - outConfigType="aliyun" /> diff --git a/ui/src/components/workflow/DeployToAliyunOss.tsx b/ui/src/components/workflow/DeployToAliyunOss.tsx index 7434d658..49536ccf 100644 --- a/ui/src/components/workflow/DeployToAliyunOss.tsx +++ b/ui/src/components/workflow/DeployToAliyunOss.tsx @@ -17,7 +17,7 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectVa import { SelectLabel } from "@radix-ui/react-select"; import AccessSelect from "./AccessSelect"; -import AccessEditDialog from "../certimate/AccessEditDialog"; +import AccessEditModal from "../access/AccessEditModal"; import { Plus } from "lucide-react"; const selectState = (state: WorkflowState) => ({ @@ -97,15 +97,15 @@ const DeployToAliyunOSS = ({ data }: DeployFormProps) => {
{t("domain.deployment.form.access.label")}
- {t("common.button.add")}
} - op="add" - outConfigType="aliyun" /> diff --git a/ui/src/components/workflow/DeployToBaiduCloudCDN.tsx b/ui/src/components/workflow/DeployToBaiduCloudCDN.tsx index c0d20a0b..c5449272 100644 --- a/ui/src/components/workflow/DeployToBaiduCloudCDN.tsx +++ b/ui/src/components/workflow/DeployToBaiduCloudCDN.tsx @@ -16,7 +16,7 @@ import { useEffect, useState } from "react"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; import { SelectLabel } from "@radix-ui/react-select"; import AccessSelect from "./AccessSelect"; -import AccessEditDialog from "../certimate/AccessEditDialog"; +import AccessEditModal from "../access/AccessEditModal"; import { Plus } from "lucide-react"; const selectState = (state: WorkflowState) => ({ @@ -86,15 +86,15 @@ const DeployToBaiduCloudCDN = ({ data }: DeployFormProps) => {
{t("domain.deployment.form.access.label")}
- {t("common.button.add")}
} - op="add" - outConfigType="baiducloud" /> diff --git a/ui/src/components/workflow/DeployToByteplusCDN.tsx b/ui/src/components/workflow/DeployToByteplusCDN.tsx index 22121ea7..c6dcb424 100644 --- a/ui/src/components/workflow/DeployToByteplusCDN.tsx +++ b/ui/src/components/workflow/DeployToByteplusCDN.tsx @@ -16,7 +16,7 @@ import { useEffect, useState } from "react"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; import { SelectLabel } from "@radix-ui/react-select"; import AccessSelect from "./AccessSelect"; -import AccessEditDialog from "../certimate/AccessEditDialog"; +import AccessEditModal from "../access/AccessEditModal"; import { Plus } from "lucide-react"; const selectState = (state: WorkflowState) => ({ @@ -86,15 +86,15 @@ const DeployToByteplusCDN = ({ data }: DeployFormProps) => {
{t("domain.deployment.form.access.label")}
- {t("common.button.add")}
} - op="add" - outConfigType="byteplus" /> diff --git a/ui/src/components/workflow/DeployToDogeCloudCDN.tsx b/ui/src/components/workflow/DeployToDogeCloudCDN.tsx index 4a8a0f92..24aa21cb 100644 --- a/ui/src/components/workflow/DeployToDogeCloudCDN.tsx +++ b/ui/src/components/workflow/DeployToDogeCloudCDN.tsx @@ -17,7 +17,7 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectVa import { SelectLabel } from "@radix-ui/react-select"; import AccessSelect from "./AccessSelect"; import { Plus } from "lucide-react"; -import AccessEditDialog from "../certimate/AccessEditDialog"; +import AccessEditModal from "../access/AccessEditModal"; const selectState = (state: WorkflowState) => ({ updateNode: state.updateNode, @@ -87,15 +87,15 @@ const DeployToDogeCloudCDN = ({ data }: DeployFormProps) => {
{t("domain.deployment.form.access.label")}
- {t("common.button.add")}
} - op="add" - outConfigType="dogecloud" /> diff --git a/ui/src/components/workflow/DeployToHuaweiCloudCDN.tsx b/ui/src/components/workflow/DeployToHuaweiCloudCDN.tsx index 2c337f9a..903bc4a9 100644 --- a/ui/src/components/workflow/DeployToHuaweiCloudCDN.tsx +++ b/ui/src/components/workflow/DeployToHuaweiCloudCDN.tsx @@ -17,7 +17,7 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectVa import { SelectLabel } from "@radix-ui/react-select"; import AccessSelect from "./AccessSelect"; -import AccessEditDialog from "../certimate/AccessEditDialog"; +import AccessEditModal from "../access/AccessEditModal"; import { Plus } from "lucide-react"; const selectState = (state: WorkflowState) => ({ @@ -92,15 +92,15 @@ const DeployToHuaweiCloudCDN = ({ data }: DeployFormProps) => {
{t("domain.deployment.form.access.label")}
- {t("common.button.add")} } - op="add" - outConfigType="huaweicloud" />
diff --git a/ui/src/components/workflow/DeployToHuaweiCloudELB.tsx b/ui/src/components/workflow/DeployToHuaweiCloudELB.tsx index bb6a87c5..d56b62ba 100644 --- a/ui/src/components/workflow/DeployToHuaweiCloudELB.tsx +++ b/ui/src/components/workflow/DeployToHuaweiCloudELB.tsx @@ -15,7 +15,7 @@ import { Button } from "../ui/button"; import { useEffect, useState } from "react"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; import { SelectLabel } from "@radix-ui/react-select"; -import AccessEditDialog from "../certimate/AccessEditDialog"; +import AccessEditModal from "../access/AccessEditModal"; import { Plus } from "lucide-react"; import AccessSelect from "./AccessSelect"; @@ -114,15 +114,15 @@ const DeployToHuaweiCloudELB = ({ data }: DeployFormProps) => {
{t("domain.deployment.form.access.label")}
- {t("common.button.add")} } - op="add" - outConfigType="huaweicloud" />
diff --git a/ui/src/components/workflow/DeployToKubernetesSecret.tsx b/ui/src/components/workflow/DeployToKubernetesSecret.tsx index f24c8c64..9cc418ba 100644 --- a/ui/src/components/workflow/DeployToKubernetesSecret.tsx +++ b/ui/src/components/workflow/DeployToKubernetesSecret.tsx @@ -15,7 +15,7 @@ import { Button } from "../ui/button"; import { useEffect, useState } from "react"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; import { SelectLabel } from "@radix-ui/react-select"; -import AccessEditDialog from "../certimate/AccessEditDialog"; +import AccessEditModal from "../access/AccessEditModal"; import { Plus } from "lucide-react"; import AccessSelect from "./AccessSelect"; @@ -101,15 +101,15 @@ const DeployToKubernetesSecret = ({ data }: DeployFormProps) => {
{t("domain.deployment.form.access.label")}
- {t("common.button.add")} } - op="add" - outConfigType="k8s" />
diff --git a/ui/src/components/workflow/DeployToLocal.tsx b/ui/src/components/workflow/DeployToLocal.tsx index 30f48e91..942f646b 100644 --- a/ui/src/components/workflow/DeployToLocal.tsx +++ b/ui/src/components/workflow/DeployToLocal.tsx @@ -16,7 +16,7 @@ import { WorkflowNode } from "@/domain/workflow"; import { Textarea } from "../ui/textarea"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/dropdown-menu"; import AccessSelect from "./AccessSelect"; -import AccessEditDialog from "../certimate/AccessEditDialog"; +import AccessEditModal from "../access/AccessEditModal"; import { Plus } from "lucide-react"; const selectState = (state: WorkflowState) => ({ @@ -41,13 +41,14 @@ const formSchema = z keyPath: z .string() .min(0, t("domain.deployment.form.file_key_path.placeholder")) - .max(255, t("common.errmsg.string_max", { max: 255 })), - pfxPassword: z.string().optional(), - jksAlias: z.string().optional(), - jksKeypass: z.string().optional(), - jksStorepass: z.string().optional(), - preCommand: z.string().optional(), - command: z.string().optional(), + .max(255, t("common.errmsg.string_max", { max: 255 })) + .nullish(), + pfxPassword: z.string().nullish(), + jksAlias: z.string().nullish(), + jksKeypass: z.string().nullish(), + jksStorepass: z.string().nullish(), + preCommand: z.string().nullish(), + postCommand: z.string().nullish(), shell: z.union([z.literal("sh"), z.literal("cmd"), z.literal("powershell")], { message: t("domain.deployment.form.shell.placeholder"), }), @@ -99,9 +100,9 @@ const DeployToLocal = ({ data }: DeployFormProps) => { jksAlias: (data.config?.jksAlias as string) || "", jksKeypass: (data.config?.jksKeypass as string) || "", jksStorepass: (data.config?.jksStorepass as string) || "", - preCommand: (data.config?.preCommand as string) || "", - command: (data.config?.command as string) || "service nginx reload", shell: (data.config?.shell as "sh" | "cmd" | "powershell") || "sh", + preCommand: (data.config?.preCommand as string) || "", + postCommand: (data.config?.postCommand as string) || "service nginx reload", }, }); @@ -128,7 +129,7 @@ const DeployToLocal = ({ data }: DeployFormProps) => { case "reload_nginx": { form.setValue("shell", "sh"); - form.setValue("command", "sudo service nginx reload"); + form.setValue("postCommand", "sudo service nginx reload"); } break; @@ -136,7 +137,7 @@ const DeployToLocal = ({ data }: DeployFormProps) => { { form.setValue("shell", "powershell"); form.setValue( - "command", + "postCommand", `# 请将以下变量替换为实际值 $pfxPath = "" # PFX 文件路径 $pfxPassword = "" # PFX 密码 @@ -173,7 +174,7 @@ Remove-Item -Path "$pfxPath" -Force { form.setValue("shell", "powershell"); form.setValue( - "command", + "postCommand", `# 请将以下变量替换为实际值 $pfxPath = "" # PFX 文件路径 $pfxPassword = "" # PFX 密码 @@ -211,15 +212,15 @@ Remove-Item -Path "$pfxPath" -Force
{t("domain.deployment.form.access.label")}
- {t("common.button.add")} } - op="add" - outConfigType="local" />
@@ -323,7 +324,7 @@ Remove-Item -Path "$pfxPath" -Force 密钥路径 - + @@ -338,7 +339,7 @@ Remove-Item -Path "$pfxPath" -Force PFX 密码 - + @@ -355,7 +356,7 @@ Remove-Item -Path "$pfxPath" -Force JKS 别名 - + @@ -369,7 +370,7 @@ Remove-Item -Path "$pfxPath" -Force JKS Keypass - + @@ -383,7 +384,7 @@ Remove-Item -Path "$pfxPath" -Force JKS Storepass - + @@ -422,7 +423,7 @@ Remove-Item -Path "$pfxPath" -Force {t("domain.deployment.form.shell_pre_command.label")} -