diff --git a/.gitignore b/.gitignore index bfb74e99..7c1ff027 100644 --- a/.gitignore +++ b/.gitignore @@ -10,11 +10,13 @@ *.sln *.sw? __debug_bin* + vendor pb_data build main -ui/dist +/ui/dist/* +!/ui/dist/.gitkeep ./dist ./certimate /docker/data diff --git a/README.md b/README.md index 3f5ef4fc..af94b0b1 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,8 @@ make local.run | CloudFlare | √ | | 可签发在 CloudFlare 注册的域名;CloudFlare 服务自带 SSL 证书 | | GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 | | Namesilo | √ | | 可签发在 Namesilo 注册的域名 | -| PowerDNS | √ | | 可签发通过PowerDNS管理的域名 | -| HTTP request | √ | | 可签发通过HTTP Request修改dns的域名 | +| PowerDNS | √ | | 可签发在 PowerDNS 托管的域名 | +| HTTP 请求 | √ | | 可签发允许通过 HTTP 请求修改 DNS 的域名 | | 本地部署 | | √ | 可部署到本地服务器 | | SSH | | √ | 可部署到 SSH 服务器 | | Webhook | | √ | 可部署时回调到 Webhook | diff --git a/README_EN.md b/README_EN.md index f8923c44..9cc42453 100644 --- a/README_EN.md +++ b/README_EN.md @@ -74,15 +74,14 @@ password:1234567890 | :-----------: | :----------: | :--------: | ------------------------------------------------------------------------------------------- | | Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN | | Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud CDN | -| Huawei Cloud | √ | √ | Supports domains registered on Huawei; supports deployment to Huawei Cloud CDN | +| Huawei Cloud | √ | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN | | Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud CDN | | AWS | √ | | Supports domains managed on AWS Route53 | | CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates | | GoDaddy | √ | | Supports domains registered on GoDaddy | | Namesilo | √ | | Supports domains registered on Namesilo | -| PowerDNS | √ | | Supports domains managed by PowerDNS | -| HTTP request | √ | | Supports domains dns managed by HTTP Request | - +| PowerDNS | √ | | Supports domains managed on PowerDNS | +| HTTP Request | √ | | Supports domains which allow managing DNS by HTTP request | | Local Deploy | | √ | Supports deployment to local servers | | SSH | | √ | Supports deployment to SSH servers | | Webhook | | √ | Supports callback to Webhook | diff --git a/go.mod b/go.mod index 6a8a5f15..0147a2cf 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,9 @@ go 1.22.0 toolchain go1.23.2 require ( + github.com/alibabacloud-go/cas-20200407/v3 v3.0.1 github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0 - github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9 + github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 github.com/alibabacloud-go/tea v1.2.2 github.com/alibabacloud-go/tea-utils/v2 v2.0.6 github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible @@ -69,15 +70,15 @@ require ( require ( github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/BurntSushi/toml v1.4.0 // indirect - github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect + github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect github.com/alibabacloud-go/dcdn-20180115/v3 v3.4.2 - github.com/alibabacloud-go/debug v1.0.0 // indirect + github.com/alibabacloud-go/debug v1.0.1 // indirect github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect github.com/alibabacloud-go/openapi-util v0.1.0 // indirect github.com/alibabacloud-go/tea-utils v1.4.5 // indirect github.com/alibabacloud-go/tea-xml v1.1.3 // indirect github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 // indirect - github.com/aliyun/credentials-go v1.3.1 // indirect + github.com/aliyun/credentials-go v1.3.10 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect diff --git a/go.sum b/go.sum index 51caa927..0513ac00 100644 --- a/go.sum +++ b/go.sum @@ -29,19 +29,36 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDe github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= -github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 h1:iC9YFYKDGEy3n/FtqJnOkZsene9olVspKmkX5A2YBEo= +github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA= +github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g= +github.com/alibabacloud-go/cas-20200407/v3 v3.0.1 h1:kAxd9IkdMaIX9aoBRA34q9WXKnkKTucil/zUlG4/3vo= +github.com/alibabacloud-go/cas-20200407/v3 v3.0.1/go.mod h1:gElMYWcjdjKgqq9/2YxE6BIUMs10ZNGM4PRiRlDXgBs= github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0 h1:yTKngw4rBR3hdpoo/uCyBffYXfPfjNjlaDL8nTYUIds= github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0/go.mod h1:HxQrwVKBx3/6bIwmdDcpqBpSQt2tpi/j4LfEhl+QFPk= +github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY= +github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI= +github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE= +github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8= +github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc= +github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.0/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.8/go.mod h1:CzQnh+94WDnJOnKZH5YRyouL+OOcdBnXY5VWAf0McgI= -github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9 h1:fxMCrZatZfXq5nLcgkmWBXmU3FLC1OR+m/SqVtMqflk= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9/go.mod h1:bb+Io8Sn2RuM3/Rpme6ll86jMyFSrD1bxeV/+v61KeU= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 h1:GEYkMApgpKEVDn6z12DcH1EGYpDYRB8JxsazM4Rywak= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10/go.mod h1:26a14FGhZVELuz2cc2AolvW4RHmIO3/HRwsdHhaIPDE= +github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg= +github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ= +github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo= +github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA= github.com/alibabacloud-go/dcdn-20180115/v3 v3.4.2 h1:WKMtPfhEmf8jX4FvdG7MFBJeCknPQ+FEHQppDcaCoU0= github.com/alibabacloud-go/dcdn-20180115/v3 v3.4.2/go.mod h1:dGuR8qQqofJKl99rVaWvObnP3bMkru3cdOtqJJ95048= github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= -github.com/alibabacloud-go/debug v1.0.0 h1:3eIEQWfay1fB24PQIEzXAswlVJtdQok8f3EVN5VrBnA= github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg= +github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= github.com/alibabacloud-go/openapi-util v0.0.11/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= @@ -53,9 +70,11 @@ github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9Q github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= github.com/alibabacloud-go/tea v1.1.10/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= github.com/alibabacloud-go/tea v1.1.12/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.1.19/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA= github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU= github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= @@ -82,8 +101,10 @@ github.com/aliyun/alibaba-cloud-sdk-go v1.63.15/go.mod h1:SOSDHfe1kX91v3W5QiBsWS github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= -github.com/aliyun/credentials-go v1.3.1 h1:uq/0v7kWrxmoLGpqjx7vtQ/s03f0zR//0br/xWDTE28= github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= +github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= +github.com/aliyun/credentials-go v1.3.10 h1:45Xxrae/evfzQL9V10zL3xX31eqgLWEaIdCoPipOEQA= +github.com/aliyun/credentials-go v1.3.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= @@ -487,6 +508,7 @@ golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= @@ -537,6 +559,7 @@ golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -576,6 +599,7 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -588,6 +612,7 @@ golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= diff --git a/internal/applicant/huaweicloud.go b/internal/applicant/huaweicloud.go index 94f72c7f..47776400 100644 --- a/internal/applicant/huaweicloud.go +++ b/internal/applicant/huaweicloud.go @@ -24,7 +24,12 @@ func (t *huaweicloud) Apply() (*Certificate, error) { access := &domain.HuaweiCloudAccess{} json.Unmarshal([]byte(t.option.Access), access) - os.Setenv("HUAWEICLOUD_REGION", access.Region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错 + region := access.Region + if region == "" { + region = "cn-north-1" + } + + os.Setenv("HUAWEICLOUD_REGION", region) // 华为云的 SDK 要求必须传一个区域,实际上 DNS-01 流程里用不到,但不传会报错 os.Setenv("HUAWEICLOUD_ACCESS_KEY_ID", access.AccessKeyId) os.Setenv("HUAWEICLOUD_SECRET_ACCESS_KEY", access.SecretAccessKey) os.Setenv("HUAWEICLOUD_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", t.option.Timeout)) diff --git a/internal/deployer/huaweicloud_cdn.go b/internal/deployer/huaweicloud_cdn.go index f5282c38..b6429be3 100644 --- a/internal/deployer/huaweicloud_cdn.go +++ b/internal/deployer/huaweicloud_cdn.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "time" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global" cdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2" @@ -11,7 +12,8 @@ import ( cdnRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/region" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/rand" + uploader "github.com/usual2970/certimate/internal/pkg/core/uploader" + "github.com/usual2970/certimate/internal/pkg/utils/cast" ) type HuaweiCloudCDNDeployer struct { @@ -40,16 +42,18 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error { return err } - client, err := d.createClient(access) + // TODO: CDN 服务与 DNS 服务所支持的区域可能不一致,这里暂时不传而是使用默认值,仅支持华为云国内版 + client, err := d.createClient("", access.AccessKeyId, access.SecretAccessKey) if err != nil { return err } - d.infos = append(d.infos, toStr("HuaweiCloudCdnClient 创建成功", nil)) + d.infos = append(d.infos, toStr("SDK 客户端创建成功", nil)) // 查询加速域名配置 + // REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html showDomainFullConfigReq := &cdnModel.ShowDomainFullConfigRequest{ - DomainName: getDeployString(d.option.DeployConfig, "domain"), + DomainName: d.option.DeployConfig.GetConfigAsString("domain"), } showDomainFullConfigResp, err := client.ShowDomainFullConfig(showDomainFullConfigReq) if err != nil { @@ -59,19 +63,46 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error { d.infos = append(d.infos, toStr("已查询到加速域名配置", showDomainFullConfigResp)) // 更新加速域名配置 - certName := fmt.Sprintf("%s-%s", d.option.DomainId, rand.RandStr(12)) - updateDomainMultiCertificatesReq := &cdnModel.UpdateDomainMultiCertificatesRequest{ - Body: &cdnModel.UpdateDomainMultiCertificatesRequestBody{ - Https: mergeHuaweiCloudCDNConfig(showDomainFullConfigResp.Configs, &cdnModel.UpdateDomainMultiCertificatesRequestBodyContent{ - DomainName: getDeployString(d.option.DeployConfig, "domain"), - HttpsSwitch: 1, - CertName: &certName, - Certificate: &d.option.Certificate.Certificate, - PrivateKey: &d.option.Certificate.PrivateKey, - }), + // REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html + // REF: https://support.huaweicloud.com/usermanual-cdn/cdn_01_0306.html + updateDomainMultiCertificatesReqBodyContent := &huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent{} + updateDomainMultiCertificatesReqBodyContent.DomainName = d.option.DeployConfig.GetConfigAsString("domain") + updateDomainMultiCertificatesReqBodyContent.HttpsSwitch = 1 + var updateDomainMultiCertificatesResp *cdnModel.UpdateDomainMultiCertificatesResponse + if d.option.DeployConfig.GetConfigAsBool("useSCM") { + uploader, err := uploader.NewHuaweiCloudSCMUploader(&uploader.HuaweiCloudSCMUploaderConfig{ + Region: "", // TODO: SCM 服务与 DNS 服务所支持的区域可能不一致,这里暂时不传而是使用默认值,仅支持华为云国内版 + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + }) + if err != nil { + return err + } + + // 上传证书到 SCM + uploadResult, err := uploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + + updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(2) + updateDomainMultiCertificatesReqBodyContent.SCMCertificateId = cast.StringPtr(uploadResult.CertId) + updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(uploadResult.CertName) + } else { + updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(0) + updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())) + updateDomainMultiCertificatesReqBodyContent.Certificate = cast.StringPtr(d.option.Certificate.Certificate) + updateDomainMultiCertificatesReqBodyContent.PrivateKey = cast.StringPtr(d.option.Certificate.PrivateKey) + } + updateDomainMultiCertificatesReqBodyContent = mergeHuaweiCloudCDNConfig(showDomainFullConfigResp.Configs, updateDomainMultiCertificatesReqBodyContent) + updateDomainMultiCertificatesReq := &huaweicloudCDNUpdateDomainMultiCertificatesRequest{ + Body: &huaweicloudCDNUpdateDomainMultiCertificatesRequestBody{ + Https: updateDomainMultiCertificatesReqBodyContent, }, } - updateDomainMultiCertificatesResp, err := client.UpdateDomainMultiCertificates(updateDomainMultiCertificatesReq) + updateDomainMultiCertificatesResp, err = executeHuaweiCloudCDNUploadDomainMultiCertificates(client, updateDomainMultiCertificatesReq) if err != nil { return err } @@ -81,22 +112,26 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error { return nil } -func (d *HuaweiCloudCDNDeployer) createClient(access *domain.HuaweiCloudAccess) (*cdn.CdnClient, error) { +func (d *HuaweiCloudCDNDeployer) createClient(region, accessKeyId, secretAccessKey string) (*cdn.CdnClient, error) { auth, err := global.NewCredentialsBuilder(). - WithAk(access.AccessKeyId). - WithSk(access.SecretAccessKey). + WithAk(accessKeyId). + WithSk(secretAccessKey). SafeBuild() if err != nil { return nil, err } - region, err := cdnRegion.SafeValueOf(access.Region) + if region == "" { + region = "cn-north-1" // CDN 服务默认区域:华北一北京 + } + + hcRegion, err := cdnRegion.SafeValueOf(region) if err != nil { return nil, err } hcClient, err := cdn.CdnClientBuilder(). - WithRegion(region). + WithRegion(hcRegion). WithCredential(auth). SafeBuild() if err != nil { @@ -107,25 +142,47 @@ func (d *HuaweiCloudCDNDeployer) createClient(access *domain.HuaweiCloudAccess) return client, nil } -func mergeHuaweiCloudCDNConfig(src *cdnModel.ConfigsGetBody, dest *cdnModel.UpdateDomainMultiCertificatesRequestBodyContent) *cdnModel.UpdateDomainMultiCertificatesRequestBodyContent { +type huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent struct { + cdnModel.UpdateDomainMultiCertificatesRequestBodyContent `json:",inline"` + + SCMCertificateId *string `json:"scm_certificate_id,omitempty"` +} + +type huaweicloudCDNUpdateDomainMultiCertificatesRequestBody struct { + Https *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent `json:"https,omitempty"` +} + +type huaweicloudCDNUpdateDomainMultiCertificatesRequest struct { + Body *huaweicloudCDNUpdateDomainMultiCertificatesRequestBody `json:"body,omitempty"` +} + +func executeHuaweiCloudCDNUploadDomainMultiCertificates(client *cdn.CdnClient, request *huaweicloudCDNUpdateDomainMultiCertificatesRequest) (*cdnModel.UpdateDomainMultiCertificatesResponse, error) { + // 华为云官方 SDK 中目前提供的字段缺失,这里暂时先需自定义请求 + // 可能需要等之后 SDK 更新 + + requestDef := cdn.GenReqDefForUpdateDomainMultiCertificates() + + if resp, err := client.HcClient.Sync(request, requestDef); err != nil { + return nil, err + } else { + return resp.(*cdnModel.UpdateDomainMultiCertificatesResponse), nil + } +} + +func mergeHuaweiCloudCDNConfig(src *cdnModel.ConfigsGetBody, dest *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent) *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent { if src == nil { return dest } // 华为云 API 中不传的字段表示使用默认值、而非保留原值,因此这里需要把原配置中的参数重新赋值回去 // 而且蛋疼的是查询接口返回的数据结构和更新接口传入的参数结构不一致,需要做很多转化 - // REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html - // REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html if *src.OriginProtocol == "follow" { - accessOriginWay := int32(1) - dest.AccessOriginWay = &accessOriginWay + dest.AccessOriginWay = cast.Int32Ptr(1) } else if *src.OriginProtocol == "http" { - accessOriginWay := int32(2) - dest.AccessOriginWay = &accessOriginWay + dest.AccessOriginWay = cast.Int32Ptr(2) } else if *src.OriginProtocol == "https" { - accessOriginWay := int32(3) - dest.AccessOriginWay = &accessOriginWay + dest.AccessOriginWay = cast.Int32Ptr(3) } if src.ForceRedirect != nil { @@ -141,8 +198,7 @@ func mergeHuaweiCloudCDNConfig(src *cdnModel.ConfigsGetBody, dest *cdnModel.Upda if src.Https != nil { if *src.Https.Http2Status == "on" { - http2 := int32(1) - dest.Http2 = &http2 + dest.Http2 = cast.Int32Ptr(1) } } diff --git a/internal/domain/domains.go b/internal/domain/domains.go index d706a1c7..78acbb3d 100644 --- a/internal/domain/domains.go +++ b/internal/domain/domains.go @@ -18,15 +18,78 @@ type DeployConfig struct { Config map[string]any `json:"config"` } -// GetDomain returns the domain from the deploy config -// if the domain is a wildcard domain, and wildcard is true, return the wildcard domain -func (d *DeployConfig) GetDomain(wildcard ...bool) string { - if _, ok := d.Config["domain"]; !ok { - return "" + +// 以字符串形式获取配置项。 +// +// 入参: +// - key: 配置项的键。 +// +// 出参: +// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回空字符串。 +func (dc *DeployConfig) GetConfigAsString(key string) string { + return dc.GetConfigOrDefaultAsString(key, "") +} + +// 以字符串形式获取配置项。 +// +// 入参: +// - key: 配置项的键。 +// - defaultValue: 默认值。 +// +// 出参: +// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回默认值。 +func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue string) string { + if dc.Config == nil { + return defaultValue } - val, ok := d.Config["domain"].(string) - if !ok { + if value, ok := dc.Config[key]; ok { + if result, ok := value.(string); ok { + return result + } + } + + return defaultValue +} + +// 以布尔形式获取配置项。 +// +// 入参: +// - key: 配置项的键。 +// +// 出参: +// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回 false。 +func (dc *DeployConfig) GetConfigAsBool(key string) bool { + return dc.GetConfigOrDefaultAsBool(key, false) +} + +// 以布尔形式获取配置项。 +// +// 入参: +// - key: 配置项的键。 +// - defaultValue: 默认值。 +// +// 出参: +// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回默认值。 +func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool) bool { + if dc.Config == nil { + return defaultValue + } + + if value, ok := dc.Config[key]; ok { + if result, ok := value.(bool); ok { + return result + } + } + + return defaultValue +} + +// 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") + if val == "" { return "" } diff --git a/internal/pkg/core/uploader/uploader.go b/internal/pkg/core/uploader/uploader.go new file mode 100644 index 00000000..7694cfcf --- /dev/null +++ b/internal/pkg/core/uploader/uploader.go @@ -0,0 +1,27 @@ +package uploader + +import "context" + +// 表示定义证书上传者的抽象类型接口。 +// 云服务商通常会提供 SSL 证书管理服务,可供用户集中管理证书。 +// 注意与 `Deployer` 区分,“上传”通常为“部署”的前置操作。 +type Uploader interface { + // 上传证书。 + // + // 入参: + // - ctx: + // - certPem:证书 PEM 内容 + // - privkeyPem:私钥 PEM 内容 + // + // 出参: + // - res: + // - err: + Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) +} + +// 表示证书上传结果的数据结构,包含上传后的证书 ID、名称和其他数据。 +type UploadResult struct { + CertId string `json:"certId"` + CertName string `json:"certName"` + CertData map[string]any `json:"certData,omitempty"` +} diff --git a/internal/pkg/core/uploader/uploader_aliyun_cas.go b/internal/pkg/core/uploader/uploader_aliyun_cas.go new file mode 100644 index 00000000..95ef9c57 --- /dev/null +++ b/internal/pkg/core/uploader/uploader_aliyun_cas.go @@ -0,0 +1,164 @@ +package uploader + +import ( + "context" + "fmt" + "strings" + "time" + + cas20200407 "github.com/alibabacloud-go/cas-20200407/v3/client" + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + util "github.com/alibabacloud-go/tea-utils/v2/service" + "github.com/alibabacloud-go/tea/tea" + + "github.com/usual2970/certimate/internal/pkg/utils/x509" +) + +type AliyunCASUploaderConfig struct { + Region string `json:"region"` + AccessKeyId string `json:"accessKeyId"` + AccessKeySecret string `json:"accessKeySecret"` +} + +type AliyunCASUploader struct { + config *AliyunCASUploaderConfig + sdkClient *cas20200407.Client + sdkRuntime *util.RuntimeOptions +} + +func NewAliyunCASUploader(config *AliyunCASUploaderConfig) (*AliyunCASUploader, error) { + client, err := (&AliyunCASUploader{config: config}).createSdkClient() + if err != nil { + return nil, fmt.Errorf("failed to create sdk client: %w", err) + } + + return &AliyunCASUploader{ + config: config, + sdkClient: client, + sdkRuntime: &util.RuntimeOptions{}, + }, nil +} + +func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) { + // 解析证书内容 + certX509, err := x509.ParseCertificateFromPEM(certPem) + if err != nil { + return nil, err + } + + // 查询证书列表,避免重复上传 + // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listusercertificateorder + // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-getusercertificatedetail + listUserCertificateOrderPage := int64(1) + listUserCertificateOrderLimit := int64(50) + for { + listUserCertificateOrderReq := &cas20200407.ListUserCertificateOrderRequest{ + CurrentPage: tea.Int64(listUserCertificateOrderPage), + ShowSize: tea.Int64(listUserCertificateOrderLimit), + OrderType: tea.String("CERT"), + } + listUserCertificateOrderResp, err := u.sdkClient.ListUserCertificateOrderWithOptions(listUserCertificateOrderReq, u.sdkRuntime) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'cas.ListUserCertificateOrder': %w", err) + } + + if listUserCertificateOrderResp.Body.CertificateOrderList != nil { + for _, certDetail := range listUserCertificateOrderResp.Body.CertificateOrderList { + if strings.EqualFold(certX509.SerialNumber.Text(16), *certDetail.SerialNo) { + getUserCertificateDetailReq := &cas20200407.GetUserCertificateDetailRequest{ + CertId: certDetail.CertificateId, + } + getUserCertificateDetailResp, err := u.sdkClient.GetUserCertificateDetailWithOptions(getUserCertificateDetailReq, u.sdkRuntime) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'cas.GetUserCertificateDetail': %w", err) + } + + var isSameCert bool + if *getUserCertificateDetailResp.Body.Cert == certPem { + isSameCert = true + } else { + cert, err := x509.ParseCertificateFromPEM(*getUserCertificateDetailResp.Body.Cert) + if err != nil { + continue + } + + isSameCert = x509.EqualCertificate(certX509, cert) + } + + // 如果已存在相同证书,直接返回已有的证书信息 + if isSameCert { + return &UploadResult{ + CertId: fmt.Sprintf("%d", tea.Int64Value(certDetail.CertificateId)), + CertName: *certDetail.Name, + }, nil + } + } + } + } + + if listUserCertificateOrderResp.Body.CertificateOrderList == nil || len(listUserCertificateOrderResp.Body.CertificateOrderList) < int(listUserCertificateOrderLimit) { + break + } + + listUserCertificateOrderPage += 1 + if listUserCertificateOrderPage > 99 { // 避免死循环 + break + } + } + + // 生成新证书名(需符合阿里云命名规则) + var certId, certName string + certName = fmt.Sprintf("certimate_%d", time.Now().UnixMilli()) + + // 上传新证书 + // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-uploadusercertificate + uploadUserCertificateReq := &cas20200407.UploadUserCertificateRequest{ + Name: tea.String(certName), + Cert: tea.String(certPem), + Key: tea.String(privkeyPem), + } + uploadUserCertificateResp, err := u.sdkClient.UploadUserCertificateWithOptions(uploadUserCertificateReq, u.sdkRuntime) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'cas.UploadUserCertificate': %w", err) + } + + certId = fmt.Sprintf("%d", tea.Int64Value(uploadUserCertificateResp.Body.CertId)) + return &UploadResult{ + CertId: certId, + CertName: certName, + }, nil +} + +func (u *AliyunCASUploader) createSdkClient() (*cas20200407.Client, error) { + region := u.config.Region + accessKeyId := u.config.AccessKeyId + accessKeySecret := u.config.AccessKeySecret + if region == "" { + region = "cn-hangzhou" // CAS 服务默认区域:华东一杭州 + } + + aConfig := &openapi.Config{ + AccessKeyId: tea.String(accessKeyId), + AccessKeySecret: tea.String(accessKeySecret), + } + + var endpoint string + switch region { + case "cn-hangzhou": + endpoint = "cas.aliyuncs.com" + case "ap-southeast-1": + endpoint = "cas.ap-southeast-1.aliyuncs.com" + case "eu-central-1": + endpoint = "cas.eu-central-1.aliyuncs.com" + default: + endpoint = fmt.Sprintf("cas.%s.aliyuncs.com", region) + } + aConfig.Endpoint = tea.String(endpoint) + + client, err := cas20200407.NewClient(aConfig) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/internal/pkg/core/uploader/uploader_huaweicloud_elb.go b/internal/pkg/core/uploader/uploader_huaweicloud_elb.go new file mode 100644 index 00000000..859b844a --- /dev/null +++ b/internal/pkg/core/uploader/uploader_huaweicloud_elb.go @@ -0,0 +1,159 @@ +package uploader + +import ( + "context" + "fmt" + "time" + + "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" + 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" + + "github.com/usual2970/certimate/internal/pkg/utils/cast" + "github.com/usual2970/certimate/internal/pkg/utils/x509" +) + +type HuaweiCloudELBUploaderConfig struct { + Region string `json:"region"` + ProjectId string `json:"projectId"` + AccessKeyId string `json:"accessKeyId"` + SecretAccessKey string `json:"secretAccessKey"` +} + +type HuaweiCloudELBUploader struct { + config *HuaweiCloudELBUploaderConfig + sdkClient *hcElb.ElbClient +} + +func NewHuaweiCloudELBUploader(config *HuaweiCloudELBUploaderConfig) (*HuaweiCloudELBUploader, error) { + client, err := (&HuaweiCloudELBUploader{config: config}).createSdkClient() + if err != nil { + return nil, fmt.Errorf("failed to create sdk client: %w", err) + } + + return &HuaweiCloudELBUploader{ + config: config, + sdkClient: client, + }, nil +} + +func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) { + // 解析证书内容 + newCert, err := x509.ParseCertificateFromPEM(certPem) + if err != nil { + return nil, err + } + + // 遍历查询已有证书,避免重复上传 + // REF: https://support.huaweicloud.com/api-elb/ListCertificates.html + listCertificatesPage := 1 + listCertificatesLimit := int32(2000) + var listCertificatesMarker *string = nil + for { + listCertificatesReq := &hcElbModel.ListCertificatesRequest{ + Limit: cast.Int32Ptr(listCertificatesLimit), + Marker: listCertificatesMarker, + Type: &[]string{"server"}, + } + listCertificatesResp, err := u.sdkClient.ListCertificates(listCertificatesReq) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'elb.ListCertificates': %w", err) + } + + if listCertificatesResp.Certificates != nil { + for _, certDetail := range *listCertificatesResp.Certificates { + var isSameCert bool + if certDetail.Certificate == certPem { + isSameCert = true + } else { + cert, err := x509.ParseCertificateFromPEM(certDetail.Certificate) + if err != nil { + continue + } + + isSameCert = x509.EqualCertificate(cert, newCert) + } + + // 如果已存在相同证书,直接返回已有的证书信息 + if isSameCert { + return &UploadResult{ + CertId: certDetail.Id, + CertName: certDetail.Name, + }, nil + } + } + } + + if listCertificatesResp.Certificates == nil || len(*listCertificatesResp.Certificates) < int(listCertificatesLimit) { + break + } + + listCertificatesMarker = listCertificatesResp.PageInfo.NextMarker + listCertificatesPage++ + if listCertificatesPage >= 9 { // 避免死循环 + break + } + } + + // 生成新证书名(需符合华为云命名规则) + var certId, certName string + certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) + + // 创建新证书 + // REF: https://support.huaweicloud.com/api-elb/CreateCertificate.html + createCertificateReq := &hcElbModel.CreateCertificateRequest{ + Body: &hcElbModel.CreateCertificateRequestBody{ + Certificate: &hcElbModel.CreateCertificateOption{ + ProjectId: cast.StringPtr(u.config.ProjectId), + Name: cast.StringPtr(certName), + Certificate: cast.StringPtr(certPem), + PrivateKey: cast.StringPtr(privkeyPem), + }, + }, + } + createCertificateResp, err := u.sdkClient.CreateCertificate(createCertificateReq) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'elb.CreateCertificate': %w", err) + } + + certId = createCertificateResp.Certificate.Id + certName = createCertificateResp.Certificate.Name + return &UploadResult{ + CertId: certId, + CertName: certName, + }, nil +} + +func (u *HuaweiCloudELBUploader) createSdkClient() (*hcElb.ElbClient, error) { + region := u.config.Region + accessKeyId := u.config.AccessKeyId + secretAccessKey := u.config.SecretAccessKey + if region == "" { + region = "cn-north-4" // ELB 服务默认区域:华北四北京 + } + + auth, err := basic.NewCredentialsBuilder(). + WithAk(accessKeyId). + WithSk(secretAccessKey). + 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 +} diff --git a/internal/pkg/core/uploader/uploader_huaweicloud_scm.go b/internal/pkg/core/uploader/uploader_huaweicloud_scm.go new file mode 100644 index 00000000..f397ca29 --- /dev/null +++ b/internal/pkg/core/uploader/uploader_huaweicloud_scm.go @@ -0,0 +1,167 @@ +package uploader + +import ( + "context" + "fmt" + "time" + + "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" + hcScm "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3" + hcScmModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/model" + hcScmRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/region" + + "github.com/usual2970/certimate/internal/pkg/utils/cast" + "github.com/usual2970/certimate/internal/pkg/utils/x509" +) + +type HuaweiCloudSCMUploaderConfig struct { + Region string `json:"region"` + AccessKeyId string `json:"accessKeyId"` + SecretAccessKey string `json:"secretAccessKey"` +} + +type HuaweiCloudSCMUploader struct { + config *HuaweiCloudSCMUploaderConfig + sdkClient *hcScm.ScmClient +} + +func NewHuaweiCloudSCMUploader(config *HuaweiCloudSCMUploaderConfig) (*HuaweiCloudSCMUploader, error) { + client, err := (&HuaweiCloudSCMUploader{config: config}).createSdkClient() + if err != nil { + return nil, fmt.Errorf("failed to create sdk client: %w", err) + } + + return &HuaweiCloudSCMUploader{ + config: config, + sdkClient: client, + }, nil +} + +func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) { + // 解析证书内容 + certX509, err := x509.ParseCertificateFromPEM(certPem) + if err != nil { + return nil, err + } + + // 遍历查询已有证书,避免重复上传 + // REF: https://support.huaweicloud.com/api-ccm/ListCertificates.html + // REF: https://support.huaweicloud.com/api-ccm/ExportCertificate_0.html + listCertificatesPage := 1 + listCertificatesLimit := int32(50) + listCertificatesOffset := int32(0) + for { + listCertificatesReq := &hcScmModel.ListCertificatesRequest{ + Limit: cast.Int32Ptr(listCertificatesLimit), + Offset: cast.Int32Ptr(listCertificatesOffset), + SortDir: cast.StringPtr("DESC"), + SortKey: cast.StringPtr("certExpiredTime"), + } + listCertificatesResp, err := u.sdkClient.ListCertificates(listCertificatesReq) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'scm.ListCertificates': %w", err) + } + + if listCertificatesResp.Certificates != nil { + for _, certDetail := range *listCertificatesResp.Certificates { + exportCertificateReq := &hcScmModel.ExportCertificateRequest{ + CertificateId: certDetail.Id, + } + exportCertificateResp, err := u.sdkClient.ExportCertificate(exportCertificateReq) + if err != nil { + if exportCertificateResp != nil && exportCertificateResp.HttpStatusCode == 404 { + continue + } + return nil, fmt.Errorf("failed to execute sdk request 'scm.ExportCertificate': %w", err) + } + + var isSameCert bool + if *exportCertificateResp.Certificate == certPem { + isSameCert = true + } else { + cert, err := x509.ParseCertificateFromPEM(*exportCertificateResp.Certificate) + if err != nil { + continue + } + + isSameCert = x509.EqualCertificate(certX509, cert) + } + + // 如果已存在相同证书,直接返回已有的证书信息 + if isSameCert { + return &UploadResult{ + CertId: certDetail.Id, + CertName: certDetail.Name, + }, nil + } + } + } + + if listCertificatesResp.Certificates == nil || len(*listCertificatesResp.Certificates) < int(listCertificatesLimit) { + break + } + + listCertificatesOffset += listCertificatesLimit + listCertificatesPage += 1 + if listCertificatesPage > 99 { // 避免死循环 + break + } + } + + // 生成新证书名(需符合华为云命名规则) + var certId, certName string + certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) + + // 上传新证书 + // REF: https://support.huaweicloud.com/api-ccm/ImportCertificate.html + importCertificateReq := &hcScmModel.ImportCertificateRequest{ + Body: &hcScmModel.ImportCertificateRequestBody{ + Name: certName, + Certificate: certPem, + PrivateKey: privkeyPem, + }, + } + importCertificateResp, err := u.sdkClient.ImportCertificate(importCertificateReq) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'scm.ImportCertificate': %w", err) + } + + certId = *importCertificateResp.CertificateId + return &UploadResult{ + CertId: certId, + CertName: certName, + }, nil +} + +func (u *HuaweiCloudSCMUploader) createSdkClient() (*hcScm.ScmClient, error) { + region := u.config.Region + accessKeyId := u.config.AccessKeyId + secretAccessKey := u.config.SecretAccessKey + if region == "" { + region = "cn-north-4" // SCM 服务默认区域:华北四北京 + } + + auth, err := basic.NewCredentialsBuilder(). + WithAk(accessKeyId). + WithSk(secretAccessKey). + SafeBuild() + if err != nil { + return nil, err + } + + hcRegion, err := hcScmRegion.SafeValueOf(region) + if err != nil { + return nil, err + } + + hcClient, err := hcScm.ScmClientBuilder(). + WithRegion(hcRegion). + WithCredential(auth). + SafeBuild() + if err != nil { + return nil, err + } + + client := hcScm.NewScmClient(hcClient) + return client, nil +} diff --git a/internal/pkg/core/uploader/uploader_tencentcloud_ssl.go b/internal/pkg/core/uploader/uploader_tencentcloud_ssl.go new file mode 100644 index 00000000..e099fe1a --- /dev/null +++ b/internal/pkg/core/uploader/uploader_tencentcloud_ssl.go @@ -0,0 +1,91 @@ +package uploader + +import ( + "context" + "fmt" + "time" + + "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/pkg/utils/cast" +) + +type TencentCloudSSLUploaderConfig struct { + Region string `json:"region"` + SecretId string `json:"secretId"` + SecretKey string `json:"secretKey"` +} + +type TencentCloudSSLUploader struct { + config *TencentCloudSSLUploaderConfig + sdkClient *tcSsl.Client +} + +func NewTencentCloudSSLUploader(config *TencentCloudSSLUploaderConfig) (*TencentCloudSSLUploader, error) { + client, err := (&TencentCloudSSLUploader{config: config}).createSdkClient() + if err != nil { + return nil, fmt.Errorf("failed to create sdk client: %w", err) + } + + return &TencentCloudSSLUploader{ + config: config, + sdkClient: client, + }, nil +} + +func (u *TencentCloudSSLUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) { + // 生成新证书名(需符合腾讯云命名规则) + var certId, certName string + certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) + + // 上传新证书 + // REF: https://cloud.tencent.com/document/product/400/41665 + uploadCertificateReq := &tcSsl.UploadCertificateRequest{ + Alias: cast.StringPtr(certName), + CertificatePublicKey: cast.StringPtr(certPem), + CertificatePrivateKey: cast.StringPtr(privkeyPem), + Repeatable: cast.BoolPtr(false), + } + uploadCertificateResp, err := u.sdkClient.UploadCertificate(uploadCertificateReq) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'ssl.UploadCertificate': %w", err) + } + + // 获取证书详情 + // REF: https://cloud.tencent.com/document/api/400/41673 + // + // P.S. 上传重复证书会返回上一次的证书 ID,这里需要重新获取一遍证书名(https://github.com/usual2970/certimate/pull/227) + describeCertificateDetailReq := &tcSsl.DescribeCertificateDetailRequest{ + CertificateId: uploadCertificateResp.Response.CertificateId, + } + describeCertificateDetailResp, err := u.sdkClient.DescribeCertificateDetail(describeCertificateDetailReq) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'ssl.DescribeCertificateDetail': %w", err) + } + + certId = *describeCertificateDetailResp.Response.CertificateId + certName = *describeCertificateDetailResp.Response.Alias + return &UploadResult{ + CertId: certId, + CertName: certName, + }, nil +} + +func (u *TencentCloudSSLUploader) createSdkClient() (*tcSsl.Client, error) { + region := u.config.Region + secretId := u.config.SecretId + secretKey := u.config.SecretKey + if region == "" { + region = "ap-guangzhou" // SSL 服务默认区域:广州 + } + + 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/pkg/utils/cast/cast.go b/internal/pkg/utils/cast/cast.go new file mode 100644 index 00000000..a83ad164 --- /dev/null +++ b/internal/pkg/utils/cast/cast.go @@ -0,0 +1,25 @@ +package cast + +func Int32Ptr(i int32) *int32 { + return &i +} + +func Int64Ptr(i int64) *int64 { + return &i +} + +func UInt32Ptr(i uint32) *uint32 { + return &i +} + +func UInt64Ptr(i uint64) *uint64 { + return &i +} + +func StringPtr(s string) *string { + return &s +} + +func BoolPtr(b bool) *bool { + return &b +} diff --git a/internal/pkg/utils/x509/x509.go b/internal/pkg/utils/x509/x509.go new file mode 100644 index 00000000..ca467478 --- /dev/null +++ b/internal/pkg/utils/x509/x509.go @@ -0,0 +1,48 @@ +package x509 + +import ( + "crypto/x509" + "encoding/pem" + "fmt" +) + +// 从 PEM 编码的证书字符串解析并返回一个 x509.Certificate 对象。 +// +// 入参: +// - certPem: 证书 PEM 内容。 +// +// 出参: +// - cert: +// - err: +func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error) { + pemData := []byte(certPem) + + block, _ := pem.Decode(pemData) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + + cert, err = x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse certificate: %w", err) + } + + return cert, nil +} + +// 比较两个 x509.Certificate 对象,判断它们是否是同一张证书。 +// 注意,这不是精确比较,而只是基于证书序列号和数字签名的快速判断,但对于权威 CA 签发的证书来说不会存在误判。 +// +// 入参: +// - a: 待比较的第一个 x509.Certificate 对象。 +// - b: 待比较的第二个 x509.Certificate 对象。 +// +// 出参: +// - 是否相同。 +func EqualCertificate(a, b *x509.Certificate) bool { + return string(a.Signature) == string(b.Signature) && + a.SignatureAlgorithm == b.SignatureAlgorithm && + a.SerialNumber.String() == b.SerialNumber.String() && + a.Issuer.SerialNumber == b.Issuer.SerialNumber && + a.Subject.SerialNumber == b.Subject.SerialNumber +} diff --git a/ui/dist/.gitkeep b/ui/dist/.gitkeep new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/ui/dist/.gitkeep @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/public/imgs/providers/httpreq.svg b/ui/public/imgs/providers/httpreq.svg index 621de609..88f2d6b2 100644 --- a/ui/public/imgs/providers/httpreq.svg +++ b/ui/public/imgs/providers/httpreq.svg @@ -1,28 +1,2 @@ - - - - - - - - - - - - - - \ No newline at end of file + + diff --git a/ui/public/imgs/providers/pdns.svg b/ui/public/imgs/providers/pdns.svg index 39a111e7..192466ed 100644 --- a/ui/public/imgs/providers/pdns.svg +++ b/ui/public/imgs/providers/pdns.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/ui/src/components/certimate/AccessAliyunForm.tsx b/ui/src/components/certimate/AccessAliyunForm.tsx index 9ce87a47..ad54a95e 100644 --- a/ui/src/components/certimate/AccessAliyunForm.tsx +++ b/ui/src/components/certimate/AccessAliyunForm.tsx @@ -8,9 +8,9 @@ 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 { Access, AliyunConfig, accessFormType, getUsageByConfigType } from "@/domain/access"; +import { accessProvidersMap, accessTypeFormSchema, type Access, type AliyunConfig } from "@/domain/access"; import { save } from "@/repository/access"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessAliyunFormProps = { op: "add" | "edit" | "copy"; @@ -19,7 +19,7 @@ type AccessAliyunFormProps = { }; const AccessAliyunForm = ({ data, op, onAfterReq }: AccessAliyunFormProps) => { - const { addAccess, updateAccess } = useConfig(); + const { addAccess, updateAccess } = useConfigContext(); const { t } = useTranslation(); const formSchema = z.object({ id: z.string().optional(), @@ -27,7 +27,7 @@ const AccessAliyunForm = ({ data, op, onAfterReq }: AccessAliyunFormProps) => { .string() .min(1, "access.authorization.form.name.placeholder") .max(64, t("common.errmsg.string_max", { max: 64 })), - configType: accessFormType, + configType: accessTypeFormSchema, accessKeyId: z .string() .min(1, "access.authorization.form.access_key_id.placeholder") @@ -60,7 +60,7 @@ const AccessAliyunForm = ({ data, op, onAfterReq }: AccessAliyunFormProps) => { id: data.id as string, name: data.name, configType: data.configType, - usage: getUsageByConfigType(data.configType), + usage: accessProvidersMap.get(data.configType)!.usage, config: { accessKeyId: data.accessKeyId, accessKeySecret: data.accessSecretId, @@ -98,98 +98,96 @@ const AccessAliyunForm = ({ data, op, onAfterReq }: AccessAliyunFormProps) => { return ( <> -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - + + { + 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.config.label")} - - - + ( + + {t("access.authorization.form.config.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.access_key_id.label")} - - - + ( + + {t("access.authorization.form.access_key_id.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.access_key_secret.label")} - - - + ( + + {t("access.authorization.form.access_key_secret.label")} + + + - - - )} - /> + + + )} + /> - + -
- -
- - -
+
+ +
+ + ); }; diff --git a/ui/src/components/certimate/AccessAwsForm.tsx b/ui/src/components/certimate/AccessAwsForm.tsx index a688dd8e..7b69a77f 100644 --- a/ui/src/components/certimate/AccessAwsForm.tsx +++ b/ui/src/components/certimate/AccessAwsForm.tsx @@ -8,9 +8,9 @@ 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 { Access, accessFormType, AwsConfig, getUsageByConfigType } from "@/domain/access"; +import { Access, accessProvidersMap, accessTypeFormSchema, type AwsConfig } from "@/domain/access"; import { save } from "@/repository/access"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessAwsFormProps = { op: "add" | "edit" | "copy"; @@ -19,7 +19,7 @@ type AccessAwsFormProps = { }; const AccessAwsForm = ({ data, op, onAfterReq }: AccessAwsFormProps) => { - const { addAccess, updateAccess } = useConfig(); + const { addAccess, updateAccess } = useConfigContext(); const { t } = useTranslation(); const formSchema = z.object({ id: z.string().optional(), @@ -27,7 +27,7 @@ const AccessAwsForm = ({ data, op, onAfterReq }: AccessAwsFormProps) => { .string() .min(1, "access.authorization.form.name.placeholder") .max(64, t("common.errmsg.string_max", { max: 64 })), - configType: accessFormType, + configType: accessTypeFormSchema, region: z .string() .min(1, "access.authorization.form.region.placeholder") @@ -72,7 +72,7 @@ const AccessAwsForm = ({ data, op, onAfterReq }: AccessAwsFormProps) => { id: data.id as string, name: data.name, configType: data.configType, - usage: getUsageByConfigType(data.configType), + usage: accessProvidersMap.get(data.configType)!.usage, config: { region: data.region, accessKeyId: data.accessKeyId, @@ -111,128 +111,126 @@ const AccessAwsForm = ({ data, op, onAfterReq }: AccessAwsFormProps) => { return ( <> -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - + + { + 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.config.label")} - - - + ( + + {t("access.authorization.form.config.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.region.label")} - - - + ( + + {t("access.authorization.form.region.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.access_key_id.label")} - - - + ( + + {t("access.authorization.form.access_key_id.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.secret_access_key.label")} - - - + ( + + {t("access.authorization.form.secret_access_key.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.aws_hosted_zone_id.label")} - - - + ( + + {t("access.authorization.form.aws_hosted_zone_id.label")} + + + - - - )} - /> + + + )} + /> - + -
- -
- - -
+
+ +
+ + ); }; diff --git a/ui/src/components/certimate/AccessCloudflareForm.tsx b/ui/src/components/certimate/AccessCloudflareForm.tsx index c167875b..a03c39b1 100644 --- a/ui/src/components/certimate/AccessCloudflareForm.tsx +++ b/ui/src/components/certimate/AccessCloudflareForm.tsx @@ -8,9 +8,9 @@ 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 { Access, accessFormType, CloudflareConfig, getUsageByConfigType } from "@/domain/access"; +import { accessProvidersMap, accessTypeFormSchema, type Access, type CloudflareConfig } from "@/domain/access"; import { save } from "@/repository/access"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessCloudflareFormProps = { op: "add" | "edit" | "copy"; @@ -19,7 +19,7 @@ type AccessCloudflareFormProps = { }; const AccessCloudflareForm = ({ data, op, onAfterReq }: AccessCloudflareFormProps) => { - const { addAccess, updateAccess } = useConfig(); + const { addAccess, updateAccess } = useConfigContext(); const { t } = useTranslation(); const formSchema = z.object({ id: z.string().optional(), @@ -27,7 +27,7 @@ const AccessCloudflareForm = ({ data, op, onAfterReq }: AccessCloudflareFormProp .string() .min(1, "access.authorization.form.name.placeholder") .max(64, t("common.errmsg.string_max", { max: 64 })), - configType: accessFormType, + configType: accessTypeFormSchema, dnsApiToken: z .string() .min(1, "access.authorization.form.cloud_dns_api_token.placeholder") @@ -54,7 +54,7 @@ const AccessCloudflareForm = ({ data, op, onAfterReq }: AccessCloudflareFormProp id: data.id as string, name: data.name, configType: data.configType, - usage: getUsageByConfigType(data.configType), + usage: accessProvidersMap.get(data.configType)!.usage, config: { dnsApiToken: data.dnsApiToken, }, @@ -88,81 +88,79 @@ const AccessCloudflareForm = ({ data, op, onAfterReq }: AccessCloudflareFormProp return ( <> -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - + + { + 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.config.label")} - - - + ( + + {t("access.authorization.form.config.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.cloud_dns_api_token.label")} - - - + ( + + {t("access.authorization.form.cloud_dns_api_token.label")} + + + - - - )} - /> + + + )} + /> -
- -
- - -
+
+ +
+ + ); }; diff --git a/ui/src/components/certimate/AccessEdit.tsx b/ui/src/components/certimate/AccessEditDialog.tsx similarity index 67% rename from ui/src/components/certimate/AccessEdit.tsx rename to ui/src/components/certimate/AccessEditDialog.tsx index 6d589cee..326a97f2 100644 --- a/ui/src/components/certimate/AccessEdit.tsx +++ b/ui/src/components/certimate/AccessEditDialog.tsx @@ -20,7 +20,7 @@ import AccessLocalForm from "./AccessLocalForm"; import AccessSSHForm from "./AccessSSHForm"; import AccessWebhookForm from "./AccessWebhookForm"; import AccessKubernetesForm from "./AccessKubernetesForm"; -import { Access, accessTypeMap } from "@/domain/access"; +import { Access, accessProvidersMap } from "@/domain/access"; type AccessEditProps = { op: "add" | "edit" | "copy"; @@ -29,18 +29,17 @@ type AccessEditProps = { data?: Access; }; -const AccessEdit = ({ trigger, op, data, className }: AccessEditProps) => { - const [open, setOpen] = useState(false); +const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) => { const { t } = useTranslation(); - const typeKeys = Array.from(accessTypeMap.keys()); + const [open, setOpen] = useState(false); const [configType, setConfigType] = useState(data?.configType || ""); - let form = <> ; + let childComponent = <> ; switch (configType) { case "aliyun": - form = ( + childComponent = ( { ); break; case "tencent": - form = ( + childComponent = ( { ); break; case "huaweicloud": - form = ( + childComponent = ( { ); break; case "qiniu": - form = ( + childComponent = ( { ); break; case "aws": - form = ( + childComponent = ( { ); break; case "cloudflare": - form = ( + childComponent = ( { ); break; case "namesilo": - form = ( + childComponent = ( { ); break; case "godaddy": - form = ( + childComponent = ( { ); break; case "pdns": - form = ( + childComponent = ( { ); break; case "httpreq": - form = ( - { - setOpen(false); - }} - /> - ); - break; + childComponent = ( + { + setOpen(false); + }} + /> + ); + break; case "local": - form = ( + childComponent = ( { ); break; case "ssh": - form = ( + childComponent = ( { ); break; case "webhook": - form = ( + childComponent = ( { ); break; case "k8s": - form = ( + childComponent = ( { - {op == "add" ? t("access.authorization.add") : op == "edit" ? t("access.authorization.edit") : t("access.authorization.copy")} + { + { + ["add"]: t("access.authorization.add"), + ["edit"]: t("access.authorization.edit"), + ["copy"]: t("access.authorization.copy"), + }[op] + }
- +
+ + +
- - - {form} +
{childComponent}
@@ -246,4 +252,4 @@ const AccessEdit = ({ trigger, op, data, className }: AccessEditProps) => { ); }; -export default AccessEdit; +export default AccessEditDialog; diff --git a/ui/src/components/certimate/AccessGodaddyForm.tsx b/ui/src/components/certimate/AccessGodaddyForm.tsx index a550af38..4c8b4eca 100644 --- a/ui/src/components/certimate/AccessGodaddyForm.tsx +++ b/ui/src/components/certimate/AccessGodaddyForm.tsx @@ -8,9 +8,9 @@ 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 { Access, accessFormType, getUsageByConfigType, GodaddyConfig } from "@/domain/access"; +import { accessProvidersMap, accessTypeFormSchema, type Access, type GodaddyConfig } from "@/domain/access"; import { save } from "@/repository/access"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessGodaddyFormProps = { op: "add" | "edit" | "copy"; @@ -19,7 +19,7 @@ type AccessGodaddyFormProps = { }; const AccessGodaddyForm = ({ data, op, onAfterReq }: AccessGodaddyFormProps) => { - const { addAccess, updateAccess } = useConfig(); + const { addAccess, updateAccess } = useConfigContext(); const { t } = useTranslation(); const formSchema = z.object({ id: z.string().optional(), @@ -27,7 +27,7 @@ const AccessGodaddyForm = ({ data, op, onAfterReq }: AccessGodaddyFormProps) => .string() .min(1, "access.authorization.form.name.placeholder") .max(64, t("common.errmsg.string_max", { max: 64 })), - configType: accessFormType, + configType: accessTypeFormSchema, apiKey: z .string() .min(1, "access.authorization.form.godaddy_api_key.placeholder") @@ -60,7 +60,7 @@ const AccessGodaddyForm = ({ data, op, onAfterReq }: AccessGodaddyFormProps) => id: data.id as string, name: data.name, configType: data.configType, - usage: getUsageByConfigType(data.configType), + usage: accessProvidersMap.get(data.configType)!.usage, config: { apiKey: data.apiKey, apiSecret: data.apiSecret, @@ -95,96 +95,94 @@ const AccessGodaddyForm = ({ data, op, onAfterReq }: AccessGodaddyFormProps) => return ( <> -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - + + { + 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.config.label")} - - - + ( + + {t("access.authorization.form.config.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.godaddy_api_key.label")} - - - + ( + + {t("access.authorization.form.godaddy_api_key.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.godaddy_api_secret.label")} - - - + ( + + {t("access.authorization.form.godaddy_api_secret.label")} + + + - - - )} - /> + + + )} + /> -
- -
- - -
+
+ +
+ + ); }; diff --git a/ui/src/components/certimate/AccessGroupEdit.tsx b/ui/src/components/certimate/AccessGroupEdit.tsx index 8f809fd3..68d881a3 100644 --- a/ui/src/components/certimate/AccessGroupEdit.tsx +++ b/ui/src/components/certimate/AccessGroupEdit.tsx @@ -12,7 +12,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from " import { Input } from "@/components/ui/input"; import { PbErrorData } from "@/domain/base"; import { update } from "@/repository/access_group"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessGroupEditProps = { className?: string; @@ -20,7 +20,7 @@ type AccessGroupEditProps = { }; const AccessGroupEdit = ({ className, trigger }: AccessGroupEditProps) => { - const { reloadAccessGroups } = useConfig(); + const { reloadAccessGroups } = useConfigContext(); const [open, setOpen] = useState(false); const { t } = useTranslation(); diff --git a/ui/src/components/certimate/AccessGroupList.tsx b/ui/src/components/certimate/AccessGroupList.tsx index a5b97998..e07344ca 100644 --- a/ui/src/components/certimate/AccessGroupList.tsx +++ b/ui/src/components/certimate/AccessGroupList.tsx @@ -19,16 +19,16 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } import { ScrollArea } from "@/components/ui/scroll-area"; import { useToast } from "@/components/ui/use-toast"; import AccessGroupEdit from "./AccessGroupEdit"; -import { getProviderInfo } from "@/domain/access"; +import { accessProvidersMap } from "@/domain/access"; import { getErrMessage } from "@/lib/error"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; import { remove } from "@/repository/access_group"; const AccessGroupList = () => { const { config: { accessGroups }, reloadAccessGroups, - } = useConfig(); + } = useConfigContext(); const { toast } = useToast(); @@ -86,11 +86,11 @@ const AccessGroupList = () => {
- provider + provider
{access.name}
-
{getProviderInfo(access.configType)![0]}
+
{accessProvidersMap.get(access.configType)!.name}
diff --git a/ui/src/components/certimate/AccessHttpreqForm.tsx b/ui/src/components/certimate/AccessHttpreqForm.tsx index b4120695..bf398170 100644 --- a/ui/src/components/certimate/AccessHttpreqForm.tsx +++ b/ui/src/components/certimate/AccessHttpreqForm.tsx @@ -8,9 +8,9 @@ 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 { Access, HttpreqConfig, accessFormType, getUsageByConfigType } from "@/domain/access"; +import { accessProvidersMap, accessTypeFormSchema, type Access, type HttpreqConfig } from "@/domain/access"; import { save } from "@/repository/access"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessHttpreqFormProps = { op: "add" | "edit" | "copy"; @@ -19,7 +19,7 @@ type AccessHttpreqFormProps = { }; const AccessHttpreqForm = ({ data, op, onAfterReq }: AccessHttpreqFormProps) => { - const { addAccess, updateAccess } = useConfig(); + const { addAccess, updateAccess } = useConfigContext(); const { t } = useTranslation(); const formSchema = z.object({ id: z.string().optional(), @@ -27,10 +27,9 @@ const AccessHttpreqForm = ({ data, op, onAfterReq }: AccessHttpreqFormProps) => .string() .min(1, "access.authorization.form.name.placeholder") .max(64, t("common.errmsg.string_max", { max: 64 })), - configType: accessFormType, + configType: accessTypeFormSchema, endpoint: z.string().url("common.errmsg.url_invalid"), - mode: z - .enum(["RAW", ""]), + mode: z.enum(["RAW", ""]), username: z .string() .min(1, "access.authorization.form.access_key_secret.placeholder") @@ -67,7 +66,7 @@ const AccessHttpreqForm = ({ data, op, onAfterReq }: AccessHttpreqFormProps) => id: data.id as string, name: data.name, configType: data.configType, - usage: getUsageByConfigType(data.configType), + usage: accessProvidersMap.get(data.configType)!.usage, config: { endpoint: data.endpoint, mode: data.mode, @@ -104,134 +103,131 @@ const AccessHttpreqForm = ({ data, op, onAfterReq }: AccessHttpreqFormProps) => return; } }; - const i18n_prefix = "access.authorization.form.httpreq"; + return ( <> -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - + + { + 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.config.label")} - - - + ( + + {t("access.authorization.form.config.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t(i18n_prefix + "_endpoint.label")} - - - + ( + + {t("access.authorization.form.httpreq_endpoint.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t(i18n_prefix + "_mode.label")} - - - + ( + + {t("access.authorization.form.httpreq_mode.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.username.label")} - - - + ( + + {t("access.authorization.form.username.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.password.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 index 41e4b165..af07611a 100644 --- a/ui/src/components/certimate/AccessHuaweicloudForm.tsx +++ b/ui/src/components/certimate/AccessHuaweicloudForm.tsx @@ -8,9 +8,9 @@ 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 { Access, accessFormType, HuaweiCloudConfig, getUsageByConfigType } from "@/domain/access"; +import { accessProvidersMap, accessTypeFormSchema, type Access, type HuaweiCloudConfig } from "@/domain/access"; import { save } from "@/repository/access"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessHuaweiCloudFormProps = { op: "add" | "edit" | "copy"; @@ -19,7 +19,7 @@ type AccessHuaweiCloudFormProps = { }; const AccessHuaweiCloudForm = ({ data, op, onAfterReq }: AccessHuaweiCloudFormProps) => { - const { addAccess, updateAccess } = useConfig(); + const { addAccess, updateAccess } = useConfigContext(); const { t } = useTranslation(); const formSchema = z.object({ id: z.string().optional(), @@ -27,7 +27,7 @@ const AccessHuaweiCloudForm = ({ data, op, onAfterReq }: AccessHuaweiCloudFormPr .string() .min(1, "access.authorization.form.name.placeholder") .max(64, t("common.errmsg.string_max", { max: 64 })), - configType: accessFormType, + configType: accessTypeFormSchema, region: z .string() .min(1, "access.authorization.form.region.placeholder") @@ -66,7 +66,7 @@ const AccessHuaweiCloudForm = ({ data, op, onAfterReq }: AccessHuaweiCloudFormPr id: data.id as string, name: data.name, configType: data.configType, - usage: getUsageByConfigType(data.configType), + usage: accessProvidersMap.get(data.configType)!.usage, config: { region: data.region, accessKeyId: data.accessKeyId, @@ -104,113 +104,111 @@ const AccessHuaweiCloudForm = ({ data, op, onAfterReq }: AccessHuaweiCloudFormPr return ( <> -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - + + { + 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.config.label")} - - - + ( + + {t("access.authorization.form.config.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.region.label")} - - - + ( + + {t("access.authorization.form.region.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.access_key_id.label")} - - - + ( + + {t("access.authorization.form.access_key_id.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.secret_access_key.label")} - - - + ( + + {t("access.authorization.form.secret_access_key.label")} + + + - - - )} - /> + + + )} + /> - + -
- -
- - -
+
+ +
+ + ); }; diff --git a/ui/src/components/certimate/AccessKubernetesForm.tsx b/ui/src/components/certimate/AccessKubernetesForm.tsx index a088cb99..bb84c89c 100644 --- a/ui/src/components/certimate/AccessKubernetesForm.tsx +++ b/ui/src/components/certimate/AccessKubernetesForm.tsx @@ -5,14 +5,14 @@ import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { ClientResponseError } from "pocketbase"; -import { Access, accessFormType, getUsageByConfigType, KubernetesConfig } from "@/domain/access"; 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 "@/lib/file"; import { PbErrorData } from "@/domain/base"; +import { accessProvidersMap, accessTypeFormSchema, type Access, type KubernetesConfig } from "@/domain/access"; import { save } from "@/repository/access"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessKubernetesFormProps = { op: "add" | "edit" | "copy"; @@ -21,7 +21,7 @@ type AccessKubernetesFormProps = { }; const AccessKubernetesForm = ({ data, op, onAfterReq }: AccessKubernetesFormProps) => { - const { addAccess, updateAccess } = useConfig(); + const { addAccess, updateAccess } = useConfigContext(); const fileInputRef = useRef(null); const [fileName, setFileName] = useState(""); @@ -34,7 +34,7 @@ const AccessKubernetesForm = ({ data, op, onAfterReq }: AccessKubernetesFormProp .string() .min(1, "access.authorization.form.name.placeholder") .max(64, t("common.errmsg.string_max", { max: 64 })), - configType: accessFormType, + configType: accessTypeFormSchema, kubeConfig: z .string() .min(1, "access.authorization.form.k8s_kubeconfig.placeholder") @@ -64,7 +64,7 @@ const AccessKubernetesForm = ({ data, op, onAfterReq }: AccessKubernetesFormProp id: data.id as string, name: data.name, configType: data.configType, - usage: getUsageByConfigType(data.configType), + usage: accessProvidersMap.get(data.configType)!.usage, config: { kubeConfig: data.kubeConfig, }, @@ -113,81 +113,79 @@ const AccessKubernetesForm = ({ data, op, onAfterReq }: AccessKubernetesFormProp return ( <> -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-3" - > - ( - - {t("access.authorization.form.name.label")} - - - + + { + e.stopPropagation(); + form.handleSubmit(onSubmit)(e); + }} + className="space-y-8" + > + ( + + {t("access.authorization.form.name.label")} + + + - - - )} - /> + + + )} + /> - ( - + )} + /> - ( - - {t("access.authorization.form.k8s_kubeconfig.label")} - -
- - -
-
+ ( + + {t("access.authorization.form.k8s_kubeconfig.label")} + +
+ + +
+
- -
- )} - /> + +
+ )} + /> - + -
- -
- - -
+
+ +
+ + ); }; diff --git a/ui/src/components/certimate/AccessLocalForm.tsx b/ui/src/components/certimate/AccessLocalForm.tsx index f80512f6..23184b66 100644 --- a/ui/src/components/certimate/AccessLocalForm.tsx +++ b/ui/src/components/certimate/AccessLocalForm.tsx @@ -8,9 +8,9 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from " import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { PbErrorData } from "@/domain/base"; -import { Access, accessFormType, getUsageByConfigType } from "@/domain/access"; +import { accessProvidersMap, accessTypeFormSchema, type Access } from "@/domain/access"; import { save } from "@/repository/access"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessLocalFormProps = { op: "add" | "edit" | "copy"; @@ -19,7 +19,7 @@ type AccessLocalFormProps = { }; const AccessLocalForm = ({ data, op, onAfterReq }: AccessLocalFormProps) => { - const { addAccess, updateAccess, reloadAccessGroups } = useConfig(); + const { addAccess, updateAccess, reloadAccessGroups } = useConfigContext(); const { t } = useTranslation(); const formSchema = z.object({ @@ -28,7 +28,7 @@ const AccessLocalForm = ({ data, op, onAfterReq }: AccessLocalFormProps) => { .string() .min(1, "access.authorization.form.name.placeholder") .max(64, t("common.errmsg.string_max", { max: 64 })), - configType: accessFormType, + configType: accessTypeFormSchema, }); const form = useForm>({ @@ -45,7 +45,7 @@ const AccessLocalForm = ({ data, op, onAfterReq }: AccessLocalFormProps) => { id: data.id as string, name: data.name, configType: data.configType, - usage: getUsageByConfigType(data.configType), + usage: accessProvidersMap.get(data.configType)!.usage, config: {}, }; @@ -82,68 +82,66 @@ const AccessLocalForm = ({ data, op, onAfterReq }: AccessLocalFormProps) => { return ( <> -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-3" - > - ( - - {t("access.authorization.form.name.label")} - - - + + { + 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.config.label")} - - - + ( + + {t("access.authorization.form.config.label")} + + + - - - )} - /> + + + )} + /> - + -
- -
- - -
+
+ +
+ + ); }; diff --git a/ui/src/components/certimate/AccessNamesiloForm.tsx b/ui/src/components/certimate/AccessNamesiloForm.tsx index 0a1bb4ee..ee8df30f 100644 --- a/ui/src/components/certimate/AccessNamesiloForm.tsx +++ b/ui/src/components/certimate/AccessNamesiloForm.tsx @@ -8,9 +8,9 @@ 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 { Access, accessFormType, getUsageByConfigType, NamesiloConfig } from "@/domain/access"; +import { accessProvidersMap, accessTypeFormSchema, type Access, type NamesiloConfig } from "@/domain/access"; import { save } from "@/repository/access"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessNamesiloFormProps = { op: "add" | "edit" | "copy"; @@ -19,7 +19,7 @@ type AccessNamesiloFormProps = { }; const AccessNamesiloForm = ({ data, op, onAfterReq }: AccessNamesiloFormProps) => { - const { addAccess, updateAccess } = useConfig(); + const { addAccess, updateAccess } = useConfigContext(); const { t } = useTranslation(); const formSchema = z.object({ id: z.string().optional(), @@ -27,7 +27,7 @@ const AccessNamesiloForm = ({ data, op, onAfterReq }: AccessNamesiloFormProps) = .string() .min(1, "access.authorization.form.name.placeholder") .max(64, t("common.errmsg.string_max", { max: 64 })), - configType: accessFormType, + configType: accessTypeFormSchema, apiKey: z .string() .min(1, "access.authorization.form.namesilo_api_key.placeholder") @@ -54,7 +54,7 @@ const AccessNamesiloForm = ({ data, op, onAfterReq }: AccessNamesiloFormProps) = id: data.id as string, name: data.name, configType: data.configType, - usage: getUsageByConfigType(data.configType), + usage: accessProvidersMap.get(data.configType)!.usage, config: { apiKey: data.apiKey, }, @@ -88,81 +88,79 @@ const AccessNamesiloForm = ({ data, op, onAfterReq }: AccessNamesiloFormProps) = return ( <> -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - + + { + 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.config.label")} - - - + ( + + {t("access.authorization.form.config.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.namesilo_api_key.label")} - - - + ( + + {t("access.authorization.form.namesilo_api_key.label")} + + + - - - )} - /> + + + )} + /> -
- -
- - -
+
+ +
+ + ); }; diff --git a/ui/src/components/certimate/AccessPdnsForm.tsx b/ui/src/components/certimate/AccessPdnsForm.tsx index a3c0813f..f527fc0c 100644 --- a/ui/src/components/certimate/AccessPdnsForm.tsx +++ b/ui/src/components/certimate/AccessPdnsForm.tsx @@ -8,9 +8,9 @@ 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 { Access, PdnsConfig, accessFormType, getUsageByConfigType } from "@/domain/access"; +import { accessProvidersMap, accessTypeFormSchema, type Access, type PdnsConfig } from "@/domain/access"; import { save } from "@/repository/access"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessPdnsFormProps = { op: "add" | "edit" | "copy"; @@ -19,7 +19,7 @@ type AccessPdnsFormProps = { }; const AccessPdnsForm = ({ data, op, onAfterReq }: AccessPdnsFormProps) => { - const { addAccess, updateAccess } = useConfig(); + const { addAccess, updateAccess } = useConfigContext(); const { t } = useTranslation(); const formSchema = z.object({ id: z.string().optional(), @@ -27,7 +27,7 @@ const AccessPdnsForm = ({ data, op, onAfterReq }: AccessPdnsFormProps) => { .string() .min(1, "access.authorization.form.name.placeholder") .max(64, t("common.errmsg.string_max", { max: 64 })), - configType: accessFormType, + configType: accessTypeFormSchema, apiUrl: z.string().url("common.errmsg.url_invalid"), apiKey: z .string() @@ -57,7 +57,7 @@ const AccessPdnsForm = ({ data, op, onAfterReq }: AccessPdnsFormProps) => { id: data.id as string, name: data.name, configType: data.configType, - usage: getUsageByConfigType(data.configType), + usage: accessProvidersMap.get(data.configType)!.usage, config: { apiUrl: data.apiUrl, apiKey: data.apiKey, @@ -95,101 +95,98 @@ const AccessPdnsForm = ({ data, op, onAfterReq }: AccessPdnsFormProps) => { return ( <> -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - + + { + 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.config.label")} - - - + ( + + {t("access.authorization.form.config.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.pdns_api_url.label")} - - - + ( + + {t("access.authorization.form.pdns_api_url.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.pdns_api_key.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 index 7d872027..bf9d93c4 100644 --- a/ui/src/components/certimate/AccessQiniuForm.tsx +++ b/ui/src/components/certimate/AccessQiniuForm.tsx @@ -8,9 +8,9 @@ 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 { Access, accessFormType, getUsageByConfigType, QiniuConfig } from "@/domain/access"; +import { accessProvidersMap, accessTypeFormSchema, type Access, type QiniuConfig } from "@/domain/access"; import { save } from "@/repository/access"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessQiniuFormProps = { op: "add" | "edit" | "copy"; @@ -19,7 +19,7 @@ type AccessQiniuFormProps = { }; const AccessQiniuForm = ({ data, op, onAfterReq }: AccessQiniuFormProps) => { - const { addAccess, updateAccess } = useConfig(); + const { addAccess, updateAccess } = useConfigContext(); const { t } = useTranslation(); const formSchema = z.object({ id: z.string().optional(), @@ -27,7 +27,7 @@ const AccessQiniuForm = ({ data, op, onAfterReq }: AccessQiniuFormProps) => { .string() .min(1, "access.authorization.form.name.placeholder") .max(64, t("common.errmsg.string_max", { max: 64 })), - configType: accessFormType, + 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), }); @@ -54,7 +54,7 @@ const AccessQiniuForm = ({ data, op, onAfterReq }: AccessQiniuFormProps) => { id: data.id as string, name: data.name, configType: data.configType, - usage: getUsageByConfigType(data.configType), + usage: accessProvidersMap.get(data.configType)!.usage, config: { accessKey: data.accessKey, secretKey: data.secretKey, @@ -91,98 +91,96 @@ const AccessQiniuForm = ({ data, op, onAfterReq }: AccessQiniuFormProps) => { return ( <> -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - + + { + 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.config.label")} - - - + ( + + {t("access.authorization.form.config.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.access_key.label")} - - - + ( + + {t("access.authorization.form.access_key.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.secret_key.label")} - - - + ( + + {t("access.authorization.form.secret_key.label")} + + + - - - )} - /> + + + )} + /> - + -
- -
- - -
+
+ +
+ + ); }; diff --git a/ui/src/components/certimate/AccessSSHForm.tsx b/ui/src/components/certimate/AccessSSHForm.tsx index 4151d341..440c585c 100644 --- a/ui/src/components/certimate/AccessSSHForm.tsx +++ b/ui/src/components/certimate/AccessSSHForm.tsx @@ -6,7 +6,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Plus } from "lucide-react"; import { ClientResponseError } from "pocketbase"; -import { Access, accessFormType, getUsageByConfigType, SSHConfig } from "@/domain/access"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; @@ -15,9 +14,10 @@ import AccessGroupEdit from "./AccessGroupEdit"; import { readFileContent } from "@/lib/file"; import { cn } from "@/lib/utils"; import { PbErrorData } from "@/domain/base"; +import { accessProvidersMap, accessTypeFormSchema, type Access, type SSHConfig } from "@/domain/access"; import { save } from "@/repository/access"; import { updateById } from "@/repository/access_group"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessSSHFormProps = { op: "add" | "edit" | "copy"; @@ -31,7 +31,7 @@ const AccessSSHForm = ({ data, op, onAfterReq }: AccessSSHFormProps) => { updateAccess, reloadAccessGroups, config: { accessGroups }, - } = useConfig(); + } = useConfigContext(); const fileInputRef = useRef(null); @@ -50,7 +50,7 @@ const AccessSSHForm = ({ data, op, onAfterReq }: AccessSSHFormProps) => { .string() .min(1, "access.authorization.form.name.placeholder") .max(64, t("common.errmsg.string_max", { max: 64 })), - configType: accessFormType, + configType: accessTypeFormSchema, host: z.string().refine( (str) => { return ipReg.test(str) || domainReg.test(str); @@ -119,7 +119,7 @@ const AccessSSHForm = ({ data, op, onAfterReq }: AccessSSHFormProps) => { id: data.id as string, name: data.name, configType: data.configType, - usage: getUsageByConfigType(data.configType), + usage: accessProvidersMap.get(data.configType)!.usage, group: group, config: { host: data.host, @@ -193,84 +193,113 @@ const AccessSSHForm = ({ data, op, onAfterReq }: AccessSSHFormProps) => { return ( <> -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-3" - > - ( - - {t("access.authorization.form.name.label")} - - - + + { + e.stopPropagation(); + form.handleSubmit(onSubmit)(e); + }} + className="space-y-8" + > + ( + + {t("access.authorization.form.name.label")} + + + - - - )} - /> + + + )} + /> - ( - - -
{t("access.authorization.form.ssh_group.label")}
- - - {t("common.add")} -
- } - /> - - - { + form.setValue("group", value); + }} + > + + + + + +
--
+
+ {accessGroups.map((item) => ( + +
{item.name}
- {accessGroups.map((item) => ( - -
{item.name}
-
- ))} -
- -
+ ))} + + + - - - )} - /> + + + )} + /> + ( + + {t("access.authorization.form.config.label")} + + + + + + + )} + /> + + ( + + {t("access.authorization.form.config.label")} + + + + + + + )} + /> +
( - - {t("access.authorization.form.config.label")} + + {t("access.authorization.form.ssh_host.label")} - + @@ -280,146 +309,115 @@ const AccessSSHForm = ({ data, op, onAfterReq }: AccessSSHFormProps) => { ( - - {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_port.label")} - + )} /> +
- ( - - {t("access.authorization.form.ssh_password.label")} - - - + ( + + {t("access.authorization.form.ssh_username.label")} + + + - - - )} - /> + + + )} + /> - ( - + )} + /> - ( - - {t("access.authorization.form.ssh_key.label")} - -
- - -
-
+ ( + - )} - /> + +
+ )} + /> - ( - - {t("access.authorization.form.ssh_key_passphrase.label")} - - - + ( + + {t("access.authorization.form.ssh_key.label")} + +
+ + +
+
- -
- )} - /> + +
+ )} + /> - + ( + + {t("access.authorization.form.ssh_key_passphrase.label")} + + + -
- -
- - - + +
+ )} + /> + + + +
+ +
+ + ); }; diff --git a/ui/src/components/certimate/AccessTencentForm.tsx b/ui/src/components/certimate/AccessTencentForm.tsx index 0e47fdde..40037d1e 100644 --- a/ui/src/components/certimate/AccessTencentForm.tsx +++ b/ui/src/components/certimate/AccessTencentForm.tsx @@ -8,9 +8,9 @@ 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 { Access, accessFormType, getUsageByConfigType, TencentConfig } from "@/domain/access"; +import { accessProvidersMap, accessTypeFormSchema, type Access, type TencentConfig } from "@/domain/access"; import { save } from "@/repository/access"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessTencentFormProps = { op: "add" | "edit" | "copy"; @@ -19,7 +19,7 @@ type AccessTencentFormProps = { }; const AccessTencentForm = ({ data, op, onAfterReq }: AccessTencentFormProps) => { - const { addAccess, updateAccess } = useConfig(); + const { addAccess, updateAccess } = useConfigContext(); const { t } = useTranslation(); const formSchema = z.object({ id: z.string().optional(), @@ -27,7 +27,7 @@ const AccessTencentForm = ({ data, op, onAfterReq }: AccessTencentFormProps) => .string() .min(1, "access.authorization.form.name.placeholder") .max(64, t("common.errmsg.string_max", { max: 64 })), - configType: accessFormType, + configType: accessTypeFormSchema, secretId: z .string() .min(1, "access.authorization.form.secret_id.placeholder") @@ -60,7 +60,7 @@ const AccessTencentForm = ({ data, op, onAfterReq }: AccessTencentFormProps) => id: data.id as string, name: data.name, configType: data.configType, - usage: getUsageByConfigType(data.configType), + usage: accessProvidersMap.get(data.configType)!.usage, config: { secretId: data.secretId, secretKey: data.secretKey, @@ -95,96 +95,94 @@ const AccessTencentForm = ({ data, op, onAfterReq }: AccessTencentFormProps) => return ( <> -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - + + { + 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.config.label")} - - - + ( + + {t("access.authorization.form.config.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.secret_id.label")} - - - + ( + + {t("access.authorization.form.secret_id.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.secret_key.label")} - - - + ( + + {t("access.authorization.form.secret_key.label")} + + + - - - )} - /> + + + )} + /> -
- -
- - -
+
+ +
+ + ); }; diff --git a/ui/src/components/certimate/AccessWebhookForm.tsx b/ui/src/components/certimate/AccessWebhookForm.tsx index 6cde4c59..1b8824d5 100644 --- a/ui/src/components/certimate/AccessWebhookForm.tsx +++ b/ui/src/components/certimate/AccessWebhookForm.tsx @@ -8,9 +8,9 @@ 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 { Access, accessFormType, getUsageByConfigType, WebhookConfig } from "@/domain/access"; +import { Access, accessProvidersMap, accessTypeFormSchema, WebhookConfig } from "@/domain/access"; import { save } from "@/repository/access"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type AccessWebhookFormProps = { op: "add" | "edit" | "copy"; @@ -19,7 +19,7 @@ type AccessWebhookFormProps = { }; const AccessWebhookForm = ({ data, op, onAfterReq }: AccessWebhookFormProps) => { - const { addAccess, updateAccess } = useConfig(); + const { addAccess, updateAccess } = useConfigContext(); const { t } = useTranslation(); const formSchema = z.object({ id: z.string().optional(), @@ -27,7 +27,7 @@ const AccessWebhookForm = ({ data, op, onAfterReq }: AccessWebhookFormProps) => .string() .min(1, "access.authorization.form.name.placeholder") .max(64, t("common.errmsg.string_max", { max: 64 })), - configType: accessFormType, + configType: accessTypeFormSchema, url: z.string().url("common.errmsg.url_invalid"), }); @@ -51,7 +51,7 @@ const AccessWebhookForm = ({ data, op, onAfterReq }: AccessWebhookFormProps) => id: data.id as string, name: data.name, configType: data.configType, - usage: getUsageByConfigType(data.configType), + usage: accessProvidersMap.get(data.configType)!.usage, config: { url: data.url, }, @@ -85,81 +85,79 @@ const AccessWebhookForm = ({ data, op, onAfterReq }: AccessWebhookFormProps) => return ( <> -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("access.authorization.form.name.label")} - - - + + { + 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.config.label")} - - - + ( + + {t("access.authorization.form.config.label")} + + + - - - )} - /> + + + )} + /> - ( - - {t("access.authorization.form.webhook_url.label")} - - - + ( + + {t("access.authorization.form.webhook_url.label")} + + + - - - )} - /> + + + )} + /> -
- -
- - -
+
+ +
+ + ); }; diff --git a/ui/src/components/certimate/DeployEdit.tsx b/ui/src/components/certimate/DeployEdit.tsx new file mode 100644 index 00000000..0b22fcd2 --- /dev/null +++ b/ui/src/components/certimate/DeployEdit.tsx @@ -0,0 +1,16 @@ +import { createContext, useContext } from "react"; + +import { DeployConfig } from "@/domain/domain"; + +type DeployEditContext = { + deploy: DeployConfig; + error: Record; + setDeploy: (deploy: DeployConfig) => void; + setError: (error: Record) => void; +}; + +export const Context = createContext({} as DeployEditContext); + +export const useDeployEditContext = () => { + return useContext(Context); +}; diff --git a/ui/src/components/certimate/DeployEditDialog.tsx b/ui/src/components/certimate/DeployEditDialog.tsx new file mode 100644 index 00000000..1e5b900e --- /dev/null +++ b/ui/src/components/certimate/DeployEditDialog.tsx @@ -0,0 +1,252 @@ +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Plus } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import AccessEditDialog from "./AccessEditDialog"; +import { Context as DeployEditContext } from "./DeployEdit"; +import DeployToAliyunOSS from "./DeployToAliyunOSS"; +import DeployToAliyunCDN from "./DeployToAliyunCDN"; +import DeployToTencentCDN from "./DeployToTencentCDN"; +import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN"; +import DeployToQiniuCDN from "./DeployToQiniuCDN"; +import DeployToSSH from "./DeployToSSH"; +import DeployToWebhook from "./DeployToWebhook"; +import DeployToKubernetesSecret from "./DeployToKubernetesSecret"; +import { deployTargetsMap, type DeployConfig } from "@/domain/domain"; +import { accessProvidersMap } from "@/domain/access"; +import { useConfigContext } from "@/providers/config"; + +type DeployEditDialogProps = { + trigger: React.ReactNode; + deployConfig?: DeployConfig; + onSave: (deploy: DeployConfig) => void; +}; + +const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogProps) => { + const { t } = useTranslation(); + + const { + config: { accesses }, + } = useConfigContext(); + + const [deployType, setDeployType] = useState(""); + + const [locDeployConfig, setLocDeployConfig] = useState({ + access: "", + type: "", + }); + + const [error, setError] = useState>({}); + + const [open, setOpen] = useState(false); + + useEffect(() => { + if (deployConfig) { + setLocDeployConfig({ ...deployConfig }); + } else { + setLocDeployConfig({ + access: "", + type: "", + }); + } + }, [deployConfig]); + + useEffect(() => { + setDeployType(locDeployConfig.type); + setError({}); + }, [locDeployConfig.type]); + + const setDeploy = useCallback( + (deploy: DeployConfig) => { + if (deploy.type !== locDeployConfig.type) { + setLocDeployConfig({ ...deploy, access: "", config: {} }); + } else { + setLocDeployConfig({ ...deploy }); + } + }, + [locDeployConfig.type] + ); + + const targetAccesses = accesses.filter((item) => { + if (item.usage == "apply") { + return false; + } + + if (locDeployConfig.type == "") { + return true; + } + + return item.configType === locDeployConfig.type.split("-")[0]; + }); + + const handleSaveClick = () => { + // 验证数据 + const newError = { ...error }; + newError.type = locDeployConfig.type === "" ? t("domain.deployment.form.access.placeholder") : ""; + newError.access = locDeployConfig.access === "" ? t("domain.deployment.form.access.placeholder") : ""; + setError(newError); + if (Object.values(newError).some((e) => !!e)) return; + + // 保存数据 + onSave(locDeployConfig); + + // 清理数据 + setLocDeployConfig({ + access: "", + type: "", + }); + setError({}); + + // 关闭弹框 + setOpen(false); + }; + + let childComponent = <>; + switch (deployType) { + case "aliyun-oss": + childComponent = ; + break; + case "aliyun-cdn": + case "aliyun-dcdn": + childComponent = ; + break; + case "tencent-cdn": + childComponent = ; + break; + case "huaweicloud-cdn": + childComponent = ; + break; + case "qiniu-cdn": + childComponent = ; + break; + case "ssh": + case "local": + childComponent = ; + break; + case "webhook": + childComponent = ; + break; + case "k8s-secret": + childComponent = ; + break; + } + + return ( + + + {trigger} + + + {t("domain.deployment.tab")} + + + + +
+ {/* 部署方式 */} +
+ + + + +
{error.type}
+
+ + {/* 授权配置 */} +
+
+ } + op="add" + /> + + + + +
{error.access}
+
+ + {/* 其他参数 */} +
{childComponent}
+ +
+ + + + +
+
+
+ ); +}; + +export default DeployEditDialog; diff --git a/ui/src/components/certimate/DeployList.tsx b/ui/src/components/certimate/DeployList.tsx index 6c3caf71..1e9866e0 100644 --- a/ui/src/components/certimate/DeployList.tsx +++ b/ui/src/components/certimate/DeployList.tsx @@ -1,35 +1,76 @@ -import { createContext, useCallback, useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { produce } from "immer"; import { nanoid } from "nanoid"; -import { EditIcon, Plus, Trash2 } from "lucide-react"; +import { EditIcon, Trash2 } from "lucide-react"; import Show from "@/components/Show"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; -import AccessEdit from "./AccessEdit"; -import KVList from "./KVList"; -import { DeployConfig, KVType, targetTypeKeys, targetTypeMap } from "@/domain/domain"; -import { accessTypeMap } from "@/domain/access"; -import { useConfig } from "@/providers/config"; +import DeployEditDialog from "./DeployEditDialog"; +import { DeployConfig } from "@/domain/domain"; +import { accessProvidersMap } from "@/domain/access"; +import { useConfigContext } from "@/providers/config"; -type DeployEditContextProps = { - deploy: DeployConfig; - error: Record; - setDeploy: (deploy: DeployConfig) => void; - setError: (error: Record) => void; +type DeployItemProps = { + item: DeployConfig; + onDelete: () => void; + onSave: (deploy: DeployConfig) => void; }; -const DeployEditContext = createContext({} as DeployEditContextProps); +const DeployItem = ({ item, onDelete, onSave }: DeployItemProps) => { + const { + config: { accesses }, + } = useConfigContext(); + const { t } = useTranslation(); -export const useDeployEditContext = () => { - return useContext(DeployEditContext); + const access = accesses.find((access) => access.id === item.access); + + const getTypeIcon = () => { + if (!access) { + return ""; + } + + return accessProvidersMap.get(access.configType)?.icon || ""; + }; + + const getTypeName = () => { + if (!access) { + return ""; + } + + return t(accessProvidersMap.get(access.configType)?.name || ""); + }; + + return ( +
+
+
+ +
+
+
{getTypeName()}
+
{access?.name}
+
+
+
+ } + deployConfig={item} + onSave={(deploy: DeployConfig) => { + onSave(deploy); + }} + /> + + { + onDelete(); + }} + /> +
+
+ ); }; type DeployListProps = { @@ -128,765 +169,3 @@ const DeployList = ({ deploys, onChange }: DeployListProps) => { }; export default DeployList; - -type DeployItemProps = { - item: DeployConfig; - onDelete: () => void; - onSave: (deploy: DeployConfig) => void; -}; - -const DeployItem = ({ item, onDelete, onSave }: DeployItemProps) => { - const { - config: { accesses }, - } = useConfig(); - const { t } = useTranslation(); - - const access = accesses.find((access) => access.id === item.access); - - const getTypeIcon = () => { - if (!access) { - return ""; - } - - const accessType = accessTypeMap.get(access.configType); - - if (accessType) { - return accessType[1]; - } - - return ""; - }; - - const getTypeName = () => { - if (!access) { - return ""; - } - - const accessType = targetTypeMap.get(item.type); - - if (accessType) { - return t(accessType[0]); - } - - return ""; - }; - - return ( -
-
-
- -
-
-
{getTypeName()}
-
{access?.name}
-
-
-
- } - deployConfig={item} - onSave={(deploy: DeployConfig) => { - onSave(deploy); - }} - /> - - { - onDelete(); - }} - /> -
-
- ); -}; - -type DeployEditDialogProps = { - trigger: React.ReactNode; - deployConfig?: DeployConfig; - onSave: (deploy: DeployConfig) => void; -}; - -const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogProps) => { - const { - config: { accesses }, - } = useConfig(); - - const [deployType, setDeployType] = useState(); - - const [locDeployConfig, setLocDeployConfig] = useState({ - access: "", - type: "", - }); - - const [error, setError] = useState>({}); - - const [open, setOpen] = useState(false); - - useEffect(() => { - if (deployConfig) { - setLocDeployConfig({ ...deployConfig }); - } else { - setLocDeployConfig({ - access: "", - type: "", - }); - } - }, [deployConfig]); - - useEffect(() => { - const temp = locDeployConfig.type.split("-"); - - let t; - if (temp && temp.length > 1) { - // TODO: code smell, maybe a dictionary is better - t = temp[0] === "k8s" ? temp[0] : temp[1]; - } else { - t = locDeployConfig.type; - } - - setDeployType(t as TargetType); - setError({}); - }, [locDeployConfig.type]); - - const setDeploy = useCallback( - (deploy: DeployConfig) => { - if (deploy.type !== locDeployConfig.type) { - setLocDeployConfig({ ...deploy, access: "", config: {} }); - } else { - setLocDeployConfig({ ...deploy }); - } - }, - [locDeployConfig.type] - ); - - const { t } = useTranslation(); - - const targetAccesses = accesses.filter((item) => { - if (item.usage == "apply") { - return false; - } - - if (locDeployConfig.type == "") { - return true; - } - const types = locDeployConfig.type.split("-"); - return item.configType === types[0]; - }); - - const handleSaveClick = () => { - // 验证数据 - // 保存数据 - // 清理数据 - // 关闭弹框 - const newError = { ...error }; - if (locDeployConfig.type === "") { - newError.type = t("domain.deployment.form.access.placeholder"); - } else { - newError.type = ""; - } - - if (locDeployConfig.access === "") { - newError.access = t("domain.deployment.form.access.placeholder"); - } else { - newError.access = ""; - } - setError(newError); - - for (const key in newError) { - if (newError[key] !== "") { - return; - } - } - - onSave(locDeployConfig); - - setLocDeployConfig({ - access: "", - type: "", - }); - - setError({}); - - setOpen(false); - }; - - return ( - - - {trigger} - - - {t("domain.deployment.tab")} - - - - {/* 部署方式 */} -
- - - - -
{error.type}
-
- - {/* 授权配置 */} -
-
- } - op="add" - /> - - - - -
{error.access}
- - - {/* 其他参数 */} - - - - - -
-
-
- ); -}; - -type TargetType = "oss" | "cdn" | "dcdn" | "local" | "ssh" | "webhook" | "k8s"; - -type DeployEditProps = { - type: TargetType; -}; - -const DeployEdit = ({ type }: DeployEditProps) => { - const getDeploy = () => { - switch (type) { - case "cdn": - return ; - case "dcdn": - return ; - case "oss": - return ; - case "ssh": - return ; - case "local": - return ; - case "webhook": - return ; - case "k8s": - return ; - default: - return ; - } - }; - return getDeploy(); -}; - -const DeployToSSH = () => { - const { t } = useTranslation(); - const { setError } = useDeployEditContext(); - - useEffect(() => { - setError({}); - }, []); - - const { deploy: data, setDeploy } = useDeployEditContext(); - - useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, - config: { - certPath: "/etc/nginx/ssl/nginx.crt", - keyPath: "/etc/nginx/ssl/nginx.key", - preCommand: "", - command: "sudo service nginx reload", - }, - }); - } - }, []); - return ( - <> -
-
- - { - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.certPath = e.target.value; - }); - setDeploy(newData); - }} - /> -
-
- - { - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.keyPath = e.target.value; - }); - setDeploy(newData); - }} - /> -
- -
- - -
- -
- - -
-
- - ); -}; - -const DeployToWebhook = () => { - const { deploy: data, setDeploy } = useDeployEditContext(); - - const { setError } = useDeployEditContext(); - - useEffect(() => { - setError({}); - }, []); - - return ( - <> - { - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.variables = variables; - }); - setDeploy(newData); - }} - /> - - ); -}; - -const DeployToOSS = () => { - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); - - const { t } = useTranslation(); - - useEffect(() => { - setError({}); - }, []); - - useEffect(() => { - const resp = domainSchema.safeParse(data.config?.domain); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - }, [data]); - - useEffect(() => { - const bucketResp = bucketSchema.safeParse(data.config?.domain); - if (!bucketResp.success) { - setError({ - ...error, - bucket: JSON.parse(bucketResp.error.message)[0].message, - }); - } else { - setError({ - ...error, - bucket: "", - }); - } - }, []); - - useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, - config: { - endpoint: "oss-cn-hangzhou.aliyuncs.com", - bucket: "", - domain: "", - }, - }); - } - }, []); - - const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }); - - const bucketSchema = z.string().min(1, { - message: t("domain.deployment.form.oss_bucket.placeholder"), - }); - - return ( -
-
- - - { - const temp = e.target.value; - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.endpoint = temp; - }); - setDeploy(newData); - }} - /> -
{error?.endpoint}
- - - { - const temp = e.target.value; - - const resp = bucketSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - bucket: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - bucket: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.bucket = temp; - }); - setDeploy(newData); - }} - /> -
{error?.bucket}
- - - { - const temp = e.target.value; - - const resp = domainSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.domain = temp; - }); - setDeploy(newData); - }} - /> -
{error?.domain}
-
-
- ); -}; - -const DeployToCDN = () => { - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); - - const { t } = useTranslation(); - - useEffect(() => { - setError({}); - }, []); - - useEffect(() => { - const resp = domainSchema.safeParse(data.config?.domain); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - }, [data]); - - const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }); - - return ( -
-
- - { - const temp = e.target.value; - - const resp = domainSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.domain = temp; - }); - setDeploy(newData); - }} - /> -
{error?.domain}
-
-
- ); -}; - -const DeployToKubernetes = () => { - const { t } = useTranslation(); - const { setError } = useDeployEditContext(); - - useEffect(() => { - setError({}); - }, []); - - const { deploy: data, setDeploy } = useDeployEditContext(); - - useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, - config: { - namespace: "default", - secretName: "", - secretDataKeyForCrt: "tls.crt", - secretDataKeyForKey: "tls.key", - }, - }); - } - }, []); - - return ( - <> -
-
- - { - const newData = produce(data, (draft) => { - draft.config ??= {}; - draft.config.namespace = e.target.value; - }); - setDeploy(newData); - }} - /> -
-
- - { - const newData = produce(data, (draft) => { - draft.config ??= {}; - draft.config.secretName = e.target.value; - }); - setDeploy(newData); - }} - /> -
-
- - { - const newData = produce(data, (draft) => { - draft.config ??= {}; - draft.config.secretDataKeyForCrt = e.target.value; - }); - setDeploy(newData); - }} - /> -
-
- - { - const newData = produce(data, (draft) => { - draft.config ??= {}; - draft.config.secretDataKeyForKey = e.target.value; - }); - setDeploy(newData); - }} - /> -
-
- - ); -}; - diff --git a/ui/src/components/certimate/DeployToAliyunCDN.tsx b/ui/src/components/certimate/DeployToAliyunCDN.tsx new file mode 100644 index 00000000..d6735473 --- /dev/null +++ b/ui/src/components/certimate/DeployToAliyunCDN.tsx @@ -0,0 +1,77 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { produce } from "immer"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useDeployEditContext } from "./DeployEdit"; + +const DeployToAliyunCDN = () => { + const { t } = useTranslation(); + + const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + + useEffect(() => { + setError({}); + }, []); + + useEffect(() => { + const resp = domainSchema.safeParse(data.config?.domain); + if (!resp.success) { + setError({ + ...error, + domain: JSON.parse(resp.error.message)[0].message, + }); + } else { + setError({ + ...error, + domain: "", + }); + } + }, [data]); + + const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }); + + return ( +
+
+ + { + const temp = e.target.value; + + const resp = domainSchema.safeParse(temp); + if (!resp.success) { + setError({ + ...error, + domain: JSON.parse(resp.error.message)[0].message, + }); + } else { + setError({ + ...error, + domain: "", + }); + } + + const newData = produce(data, (draft) => { + if (!draft.config) { + draft.config = {}; + } + draft.config.domain = temp; + }); + setDeploy(newData); + }} + /> +
{error?.domain}
+
+
+ ); +}; + +export default DeployToAliyunCDN; diff --git a/ui/src/components/certimate/DeployToAliyunOSS.tsx b/ui/src/components/certimate/DeployToAliyunOSS.tsx new file mode 100644 index 00000000..7a79da2c --- /dev/null +++ b/ui/src/components/certimate/DeployToAliyunOSS.tsx @@ -0,0 +1,164 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { produce } from "immer"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useDeployEditContext } from "./DeployEdit"; + +const DeployToAliyunOSS = () => { + const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + + const { t } = useTranslation(); + + useEffect(() => { + setError({}); + }, []); + + useEffect(() => { + const resp = domainSchema.safeParse(data.config?.domain); + if (!resp.success) { + setError({ + ...error, + domain: JSON.parse(resp.error.message)[0].message, + }); + } else { + setError({ + ...error, + domain: "", + }); + } + }, [data]); + + useEffect(() => { + const bucketResp = bucketSchema.safeParse(data.config?.domain); + if (!bucketResp.success) { + setError({ + ...error, + bucket: JSON.parse(bucketResp.error.message)[0].message, + }); + } else { + setError({ + ...error, + bucket: "", + }); + } + }, []); + + useEffect(() => { + if (!data.id) { + setDeploy({ + ...data, + config: { + endpoint: "oss-cn-hangzhou.aliyuncs.com", + bucket: "", + domain: "", + }, + }); + } + }, []); + + const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }); + + const bucketSchema = z.string().min(1, { + message: t("domain.deployment.form.oss_bucket.placeholder"), + }); + + return ( +
+
+ + { + const temp = e.target.value; + + const newData = produce(data, (draft) => { + if (!draft.config) { + draft.config = {}; + } + draft.config.endpoint = temp; + }); + setDeploy(newData); + }} + /> +
{error?.endpoint}
+
+ +
+ + { + const temp = e.target.value; + + const resp = bucketSchema.safeParse(temp); + if (!resp.success) { + setError({ + ...error, + bucket: JSON.parse(resp.error.message)[0].message, + }); + } else { + setError({ + ...error, + bucket: "", + }); + } + + const newData = produce(data, (draft) => { + if (!draft.config) { + draft.config = {}; + } + draft.config.bucket = temp; + }); + setDeploy(newData); + }} + /> +
{error?.bucket}
+
+ +
+ + { + const temp = e.target.value; + + const resp = domainSchema.safeParse(temp); + if (!resp.success) { + setError({ + ...error, + domain: JSON.parse(resp.error.message)[0].message, + }); + } else { + setError({ + ...error, + domain: "", + }); + } + + const newData = produce(data, (draft) => { + if (!draft.config) { + draft.config = {}; + } + draft.config.domain = temp; + }); + setDeploy(newData); + }} + /> +
{error?.domain}
+
+
+ ); +}; + +export default DeployToAliyunOSS; diff --git a/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx b/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx new file mode 100644 index 00000000..4e61c652 --- /dev/null +++ b/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx @@ -0,0 +1,77 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { produce } from "immer"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useDeployEditContext } from "./DeployEdit"; + +const DeployToHuaweiCloudCDN = () => { + const { t } = useTranslation(); + + const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + + useEffect(() => { + setError({}); + }, []); + + useEffect(() => { + const resp = domainSchema.safeParse(data.config?.domain); + if (!resp.success) { + setError({ + ...error, + domain: JSON.parse(resp.error.message)[0].message, + }); + } else { + setError({ + ...error, + domain: "", + }); + } + }, [data]); + + const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }); + + return ( +
+
+ + { + const temp = e.target.value; + + const resp = domainSchema.safeParse(temp); + if (!resp.success) { + setError({ + ...error, + domain: JSON.parse(resp.error.message)[0].message, + }); + } else { + setError({ + ...error, + domain: "", + }); + } + + const newData = produce(data, (draft) => { + if (!draft.config) { + draft.config = {}; + } + draft.config.domain = temp; + }); + setDeploy(newData); + }} + /> +
{error?.domain}
+
+
+ ); +}; + +export default DeployToHuaweiCloudCDN; diff --git a/ui/src/components/certimate/DeployToKubernetesSecret.tsx b/ui/src/components/certimate/DeployToKubernetesSecret.tsx new file mode 100644 index 00000000..c5129324 --- /dev/null +++ b/ui/src/components/certimate/DeployToKubernetesSecret.tsx @@ -0,0 +1,104 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { produce } from "immer"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useDeployEditContext } from "./DeployEdit"; + +const DeployToKubernetesSecret = () => { + const { t } = useTranslation(); + const { setError } = useDeployEditContext(); + + useEffect(() => { + setError({}); + }, []); + + const { deploy: data, setDeploy } = useDeployEditContext(); + + useEffect(() => { + if (!data.id) { + setDeploy({ + ...data, + config: { + namespace: "default", + secretName: "", + secretDataKeyForCrt: "tls.crt", + secretDataKeyForKey: "tls.key", + }, + }); + } + }, []); + + return ( + <> +
+
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.namespace = e.target.value; + }); + setDeploy(newData); + }} + /> +
+ +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.secretName = e.target.value; + }); + setDeploy(newData); + }} + /> +
+ +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.secretDataKeyForCrt = e.target.value; + }); + setDeploy(newData); + }} + /> +
+ +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.secretDataKeyForKey = e.target.value; + }); + setDeploy(newData); + }} + /> +
+
+ + ); +}; + +export default DeployToKubernetesSecret; diff --git a/ui/src/components/certimate/DeployToQiniuCDN.tsx b/ui/src/components/certimate/DeployToQiniuCDN.tsx new file mode 100644 index 00000000..327edce8 --- /dev/null +++ b/ui/src/components/certimate/DeployToQiniuCDN.tsx @@ -0,0 +1,77 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { produce } from "immer"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useDeployEditContext } from "./DeployEdit"; + +const DeployToQiniuCDN = () => { + const { t } = useTranslation(); + + const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + + useEffect(() => { + setError({}); + }, []); + + useEffect(() => { + const resp = domainSchema.safeParse(data.config?.domain); + if (!resp.success) { + setError({ + ...error, + domain: JSON.parse(resp.error.message)[0].message, + }); + } else { + setError({ + ...error, + domain: "", + }); + } + }, [data]); + + const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }); + + return ( +
+
+ + { + const temp = e.target.value; + + const resp = domainSchema.safeParse(temp); + if (!resp.success) { + setError({ + ...error, + domain: JSON.parse(resp.error.message)[0].message, + }); + } else { + setError({ + ...error, + domain: "", + }); + } + + const newData = produce(data, (draft) => { + if (!draft.config) { + draft.config = {}; + } + draft.config.domain = temp; + }); + setDeploy(newData); + }} + /> +
{error?.domain}
+
+
+ ); +}; + +export default DeployToQiniuCDN; diff --git a/ui/src/components/certimate/DeployToSSH.tsx b/ui/src/components/certimate/DeployToSSH.tsx new file mode 100644 index 00000000..80eb15fb --- /dev/null +++ b/ui/src/components/certimate/DeployToSSH.tsx @@ -0,0 +1,113 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { produce } from "immer"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { useDeployEditContext } from "./DeployEdit"; + +const DeployToSSH = () => { + const { t } = useTranslation(); + const { setError } = useDeployEditContext(); + + useEffect(() => { + setError({}); + }, []); + + const { deploy: data, setDeploy } = useDeployEditContext(); + + useEffect(() => { + if (!data.id) { + setDeploy({ + ...data, + config: { + certPath: "/etc/nginx/ssl/nginx.crt", + keyPath: "/etc/nginx/ssl/nginx.key", + preCommand: "", + command: "sudo service nginx reload", + }, + }); + } + }, []); + + return ( + <> +
+
+ + { + const newData = produce(data, (draft) => { + if (!draft.config) { + draft.config = {}; + } + draft.config.certPath = e.target.value; + }); + setDeploy(newData); + }} + /> +
+ +
+ + { + const newData = produce(data, (draft) => { + if (!draft.config) { + draft.config = {}; + } + draft.config.keyPath = e.target.value; + }); + setDeploy(newData); + }} + /> +
+ +
+ + +
+ +
+ + +
+
+ + ); +}; + +export default DeployToSSH; diff --git a/ui/src/components/certimate/DeployToTencentCDN.tsx b/ui/src/components/certimate/DeployToTencentCDN.tsx new file mode 100644 index 00000000..dd13b3b2 --- /dev/null +++ b/ui/src/components/certimate/DeployToTencentCDN.tsx @@ -0,0 +1,77 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { produce } from "immer"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useDeployEditContext } from "./DeployEdit"; + +const DeployToTencentCDN = () => { + const { t } = useTranslation(); + + const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + + useEffect(() => { + setError({}); + }, []); + + useEffect(() => { + const resp = domainSchema.safeParse(data.config?.domain); + if (!resp.success) { + setError({ + ...error, + domain: JSON.parse(resp.error.message)[0].message, + }); + } else { + setError({ + ...error, + domain: "", + }); + } + }, [data]); + + const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }); + + return ( +
+
+ + { + const temp = e.target.value; + + const resp = domainSchema.safeParse(temp); + if (!resp.success) { + setError({ + ...error, + domain: JSON.parse(resp.error.message)[0].message, + }); + } else { + setError({ + ...error, + domain: "", + }); + } + + const newData = produce(data, (draft) => { + if (!draft.config) { + draft.config = {}; + } + draft.config.domain = temp; + }); + setDeploy(newData); + }} + /> +
{error?.domain}
+
+
+ ); +}; + +export default DeployToTencentCDN; diff --git a/ui/src/components/certimate/DeployToWebhook.tsx b/ui/src/components/certimate/DeployToWebhook.tsx new file mode 100644 index 00000000..3134983a --- /dev/null +++ b/ui/src/components/certimate/DeployToWebhook.tsx @@ -0,0 +1,35 @@ +import { useEffect } from "react"; +import { produce } from "immer"; + +import { useDeployEditContext } from "./DeployEdit"; +import KVList from "./KVList"; +import { type KVType } from "@/domain/domain"; + +const DeployToWebhook = () => { + const { deploy: data, setDeploy } = useDeployEditContext(); + + const { setError } = useDeployEditContext(); + + useEffect(() => { + setError({}); + }, []); + + return ( + <> + { + const newData = produce(data, (draft) => { + if (!draft.config) { + draft.config = {}; + } + draft.config.variables = variables; + }); + setDeploy(newData); + }} + /> + + ); +}; + +export default DeployToWebhook; diff --git a/ui/src/components/certimate/EmailsEdit.tsx b/ui/src/components/certimate/EmailsEdit.tsx index ceb1d5d6..d4bd5af4 100644 --- a/ui/src/components/certimate/EmailsEdit.tsx +++ b/ui/src/components/certimate/EmailsEdit.tsx @@ -13,7 +13,7 @@ import { cn } from "@/lib/utils"; import { PbErrorData } from "@/domain/base"; import { EmailsSetting } from "@/domain/settings"; import { update } from "@/repository/settings"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; type EmailsEditProps = { className?: string; @@ -24,7 +24,7 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => { const { config: { emails }, setEmails, - } = useConfig(); + } = useConfigContext(); const [open, setOpen] = useState(false); const { t } = useTranslation(); diff --git a/ui/src/components/certimate/KVList.tsx b/ui/src/components/certimate/KVList.tsx index e823e2ed..4a6b4b10 100644 --- a/ui/src/components/certimate/KVList.tsx +++ b/ui/src/components/certimate/KVList.tsx @@ -88,7 +88,7 @@ const KVList = ({ variables, onValueChange }: KVListProps) => { +
{t("domain.deployment.form.variables.empty")}
- -
{t(titles[valueType])}
+ + +
{t(titles[valueType])}
- 0}> - { - addVal(val); - }} - valueType={valueType} - value={""} - trigger={ -
- + 0}> + { + addVal(val); + }} + valueType={valueType} + value={""} + trigger={ +
+ -
{t("common.add")}
+
{t("common.add")}
+
+ } + /> +
+ + + 0} + fallback={ +
+
{t("common.text." + valueType + ".empty")}
+ +
} - /> -
- - - 0} - fallback={ -
-
{t("common.text." + valueType + ".empty")}
- - -
- } - > -
- {list.map((item, index) => ( -
-
{item}
-
- } - value={item} - onValueChange={(val: string) => { - editVal(index, val); - }} - /> - { - onRemoveClick(index); - }} - /> + > +
+ {list.map((item, index) => ( +
+
{item}
+
+ } + value={item} + onValueChange={(val: string) => { + editVal(index, val); + }} + /> + { + onRemoveClick(index); + }} + /> +
-
- ))} -
- - + ))} +
+ + +
); diff --git a/ui/src/components/notify/DingTalk.tsx b/ui/src/components/notify/DingTalk.tsx index 585642f7..c3841144 100644 --- a/ui/src/components/notify/DingTalk.tsx +++ b/ui/src/components/notify/DingTalk.tsx @@ -8,7 +8,7 @@ import { Switch } from "@/components/ui/switch"; import { useToast } from "@/components/ui/use-toast"; import { getErrMessage } from "@/lib/error"; import { NotifyChannelDingTalk, NotifyChannels } from "@/domain/settings"; -import { useNotify } from "@/providers/notify"; +import { useNotifyContext } from "@/providers/notify"; import { update } from "@/repository/settings"; import Show from "@/components/Show"; import { notifyTest } from "@/api/notify"; @@ -20,7 +20,7 @@ type DingTalkSetting = { }; const DingTalk = () => { - const { config, setChannels } = useNotify(); + const { config, setChannels } = useNotifyContext(); const { t } = useTranslation(); const [changed, setChanged] = useState(false); @@ -241,4 +241,3 @@ const DingTalk = () => { }; export default DingTalk; - diff --git a/ui/src/components/notify/Lark.tsx b/ui/src/components/notify/Lark.tsx index eb6aa8f1..f8bf7534 100644 --- a/ui/src/components/notify/Lark.tsx +++ b/ui/src/components/notify/Lark.tsx @@ -2,7 +2,7 @@ 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 { useNotify } from "@/providers/notify"; +import { useNotifyContext } from "@/providers/notify"; import { NotifyChannelLark, NotifyChannels } from "@/domain/settings"; import { useEffect, useState } from "react"; import { update } from "@/repository/settings"; @@ -19,7 +19,7 @@ type LarkSetting = { }; const Lark = () => { - const { config, setChannels } = useNotify(); + const { config, setChannels } = useNotifyContext(); const { t } = useTranslation(); const [changed, setChanged] = useState(false); @@ -222,4 +222,3 @@ const Lark = () => { }; export default Lark; - diff --git a/ui/src/components/notify/Telegram.tsx b/ui/src/components/notify/Telegram.tsx index 7f30e03b..419126fc 100644 --- a/ui/src/components/notify/Telegram.tsx +++ b/ui/src/components/notify/Telegram.tsx @@ -9,7 +9,7 @@ import { useToast } from "@/components/ui/use-toast"; import { getErrMessage } from "@/lib/error"; import { NotifyChannels, NotifyChannelTelegram } from "@/domain/settings"; import { update } from "@/repository/settings"; -import { useNotify } from "@/providers/notify"; +import { useNotifyContext } from "@/providers/notify"; import { notifyTest } from "@/api/notify"; import Show from "@/components/Show"; @@ -20,7 +20,7 @@ type TelegramSetting = { }; const Telegram = () => { - const { config, setChannels } = useNotify(); + const { config, setChannels } = useNotifyContext(); const { t } = useTranslation(); const [changed, setChanged] = useState(false); @@ -244,4 +244,3 @@ const Telegram = () => { }; export default Telegram; - diff --git a/ui/src/components/notify/Webhook.tsx b/ui/src/components/notify/Webhook.tsx index 09409ded..b614b50c 100644 --- a/ui/src/components/notify/Webhook.tsx +++ b/ui/src/components/notify/Webhook.tsx @@ -10,7 +10,7 @@ import { getErrMessage } from "@/lib/error"; import { isValidURL } from "@/lib/url"; import { NotifyChannels, NotifyChannelWebhook } from "@/domain/settings"; import { update } from "@/repository/settings"; -import { useNotify } from "@/providers/notify"; +import { useNotifyContext } from "@/providers/notify"; import { notifyTest } from "@/api/notify"; import Show from "@/components/Show"; @@ -21,7 +21,7 @@ type WebhookSetting = { }; const Webhook = () => { - const { config, setChannels } = useNotify(); + const { config, setChannels } = useNotifyContext(); const { t } = useTranslation(); const [changed, setChanged] = useState(false); @@ -234,4 +234,3 @@ const Webhook = () => { }; export default Webhook; - diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index 988374b8..bc7733bc 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -1,27 +1,34 @@ import { z } from "zod"; -export const accessTypeMap: Map = new Map([ - ["aliyun", ["common.provider.aliyun", "/imgs/providers/aliyun.svg"]], - ["tencent", ["common.provider.tencent", "/imgs/providers/tencent.svg"]], - ["huaweicloud", ["common.provider.huaweicloud", "/imgs/providers/huaweicloud.svg"]], - ["qiniu", ["common.provider.qiniu", "/imgs/providers/qiniu.svg"]], - ["aws", ["common.provider.aws", "/imgs/providers/aws.svg"]], - ["cloudflare", ["common.provider.cloudflare", "/imgs/providers/cloudflare.svg"]], - ["namesilo", ["common.provider.namesilo", "/imgs/providers/namesilo.svg"]], - ["godaddy", ["common.provider.godaddy", "/imgs/providers/godaddy.svg"]], - ["pdns", ["common.provider.pdns", "/imgs/providers/pdns.svg"]], - ["httpreq", ["common.provider.httpreq", "/imgs/providers/httpreq.svg"]], - ["local", ["common.provider.local", "/imgs/providers/local.svg"]], - ["ssh", ["common.provider.ssh", "/imgs/providers/ssh.svg"]], - ["webhook", ["common.provider.webhook", "/imgs/providers/webhook.svg"]], - ["k8s", ["common.provider.kubernetes", "/imgs/providers/k8s.svg"]], -]); +type AccessUsages = "apply" | "deploy" | "all"; -export const getProviderInfo = (t: string) => { - return accessTypeMap.get(t); +type AccessProvider = { + type: string; + name: string; + icon: string; + usage: AccessUsages; }; -export const accessFormType = z.union( +export const accessProvidersMap: Map = new Map( + [ + ["aliyun", "common.provider.aliyun", "/imgs/providers/aliyun.svg", "all"], + ["tencent", "common.provider.tencent", "/imgs/providers/tencent.svg", "all"], + ["huaweicloud", "common.provider.huaweicloud", "/imgs/providers/huaweicloud.svg", "all"], + ["qiniu", "common.provider.qiniu", "/imgs/providers/qiniu.svg", "deploy"], + ["aws", "common.provider.aws", "/imgs/providers/aws.svg", "apply"], + ["cloudflare", "common.provider.cloudflare", "/imgs/providers/cloudflare.svg", "apply"], + ["namesilo", "common.provider.namesilo", "/imgs/providers/namesilo.svg", "apply"], + ["godaddy", "common.provider.godaddy", "/imgs/providers/godaddy.svg", "apply"], + ["pdns", "common.provider.pdns", "/imgs/providers/pdns.svg", "apply"], + ["httpreq", "common.provider.httpreq", "/imgs/providers/httpreq.svg", "apply"], + ["local", "common.provider.local", "/imgs/providers/local.svg", "deploy"], + ["ssh", "common.provider.ssh", "/imgs/providers/ssh.svg", "deploy"], + ["webhook", "common.provider.webhook", "/imgs/providers/webhook.svg", "deploy"], + ["k8s", "common.provider.kubernetes", "/imgs/providers/k8s.svg", "deploy"], + ].map(([type, name, icon, usage]) => [type, { type, name, icon, usage: usage as AccessUsages }]) +); + +export const accessTypeFormSchema = z.union( [ z.literal("aliyun"), z.literal("tencent"), @@ -41,13 +48,11 @@ export const accessFormType = z.union( { message: "access.authorization.form.type.placeholder" } ); -type AccessUsage = "apply" | "deploy" | "all"; - export type Access = { id: string; name: string; configType: string; - usage: AccessUsage; + usage: AccessUsages; group?: string; config: | AliyunConfig @@ -141,30 +146,3 @@ export type WebhookConfig = { export type KubernetesConfig = { kubeConfig: string; }; - -export const getUsageByConfigType = (configType: string): AccessUsage => { - switch (configType) { - case "aliyun": - case "tencent": - case "huaweicloud": - return "all"; - - case "qiniu": - case "local": - case "ssh": - case "webhook": - case "k8s": - return "deploy"; - - case "aws": - case "cloudflare": - case "namesilo": - case "godaddy": - case "pdns": - case "httpreq": - return "apply"; - - default: - return "all"; - } -}; diff --git a/ui/src/domain/domain.ts b/ui/src/domain/domain.ts index 6d31ee63..234abe2c 100644 --- a/ui/src/domain/domain.ts +++ b/ui/src/domain/domain.ts @@ -63,21 +63,23 @@ export type Statistic = { disabled: number; }; -export const getLastDeployment = (domain: Domain): Deployment | undefined => { - return domain.expand?.lastDeployment; +type DeployTarget = { + type: string; + name: string; + icon: string; }; -export const targetTypeMap: Map = new Map([ - ["aliyun-oss", ["common.provider.aliyun.oss", "/imgs/providers/aliyun.svg"]], - ["aliyun-cdn", ["common.provider.aliyun.cdn", "/imgs/providers/aliyun.svg"]], - ["aliyun-dcdn", ["common.provider.aliyun.dcdn", "/imgs/providers/aliyun.svg"]], - ["tencent-cdn", ["common.provider.tencent.cdn", "/imgs/providers/tencent.svg"]], - ["huaweicloud-cdn", ["common.provider.huaweicloud.cdn", "/imgs/providers/huaweicloud.svg"]], - ["qiniu-cdn", ["common.provider.qiniu.cdn", "/imgs/providers/qiniu.svg"]], - ["local", ["common.provider.local", "/imgs/providers/local.svg"]], - ["ssh", ["common.provider.ssh", "/imgs/providers/ssh.svg"]], - ["webhook", ["common.provider.webhook", "/imgs/providers/webhook.svg"]], - ["k8s-secret", ["common.provider.kubernetes.secret", "/imgs/providers/k8s.svg"]], -]); - -export const targetTypeKeys = Array.from(targetTypeMap.keys()); +export const deployTargetsMap: Map = new Map( + [ + ["aliyun-oss", "common.provider.aliyun.oss", "/imgs/providers/aliyun.svg"], + ["aliyun-cdn", "common.provider.aliyun.cdn", "/imgs/providers/aliyun.svg"], + ["aliyun-dcdn", "common.provider.aliyun.dcdn", "/imgs/providers/aliyun.svg"], + ["tencent-cdn", "common.provider.tencent.cdn", "/imgs/providers/tencent.svg"], + ["huaweicloud-cdn", "common.provider.huaweicloud.cdn", "/imgs/providers/huaweicloud.svg"], + ["qiniu-cdn", "common.provider.qiniu.cdn", "/imgs/providers/qiniu.svg"], + ["local", "common.provider.local", "/imgs/providers/local.svg"], + ["ssh", "common.provider.ssh", "/imgs/providers/ssh.svg"], + ["webhook", "common.provider.webhook", "/imgs/providers/webhook.svg"], + ["k8s-secret", "common.provider.kubernetes.secret", "/imgs/providers/k8s.svg"], + ].map(([type, name, icon]) => [type, { type, name, icon }]) +); diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index 15ae27ca..100b6fbe 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -38,6 +38,8 @@ "access.authorization.form.godaddy_api_key.placeholder": "Please enter GO_DADDY_API_KEY", "access.authorization.form.godaddy_api_secret.label": "GO_DADDY_API_SECRET", "access.authorization.form.godaddy_api_secret.placeholder": "Please enter GO_DADDY_API_SECRET", + "access.authorization.form.namesilo_api_key.label": "NAMESILO_API_KEY", + "access.authorization.form.namesilo_api_key.placeholder": "Please enter NAMESILO_API_KEY", "access.authorization.form.pdns_api_url.label": "PDNS_API_URL", "access.authorization.form.pdns_api_url.placeholder": "Please enter PDNS_API_URL", "access.authorization.form.pdns_api_key.label": "PDNS_API_KEY", @@ -46,8 +48,6 @@ "access.authorization.form.httpreq_endpoint.placeholder": "Please enter HTTPREQ_ENDPOINT", "access.authorization.form.httpreq_mode.label": "HTTPREQ_MODE", "access.authorization.form.httpreq_mode.placeholder": "Please enter HTTPREQ_MODE(RAW or '')", - "access.authorization.form.namesilo_api_key.label": "NAMESILO_API_KEY", - "access.authorization.form.namesilo_api_key.placeholder": "Please enter NAMESILO_API_KEY", "access.authorization.form.username.label": "Username", "access.authorization.form.username.placeholder": "Please enter username", "access.authorization.form.password.label": "Password", diff --git a/ui/src/i18n/locales/en/nls.domain.json b/ui/src/i18n/locales/en/nls.domain.json index 74e5ee57..36552760 100644 --- a/ui/src/i18n/locales/en/nls.domain.json +++ b/ui/src/i18n/locales/en/nls.domain.json @@ -37,7 +37,7 @@ "domain.application.form.access.placeholder": "Please select DNS provider authorization configuration", "domain.application.form.access.list": "Provider Authorization Configurations", "domain.application.form.advanced_settings.label": "Advanced Settings", - "domain.application.form.key_algorithm.label": "Certificate Key Algorithm", + "domain.application.form.key_algorithm.label": "Certificate Key Algorithm (Default: RSA2048)", "domain.application.form.key_algorithm.placeholder": "Please select certificate key algorithm", "domain.application.form.timeout.label": "DNS Propagation Timeout (Seconds)", "domain.application.form.timeoue.placeholder": "Please enter maximum waiting time for DNS propagation", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 70e2074f..41f761a2 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -39,6 +39,7 @@ "access.authorization.form.godaddy_api_secret.label": "GO_DADDY_API_SECRET", "access.authorization.form.godaddy_api_secret.placeholder": "请输入 GO_DADDY_API_SECRET", "access.authorization.form.namesilo_api_key.label": "NAMESILO_API_KEY", + "access.authorization.form.namesilo_api_key.placeholder": "请输入 NAMESILO_API_KEY", "access.authorization.form.pdns_api_url.label": "PDNS_API_URL", "access.authorization.form.pdns_api_url.placeholder": "请输入 PDNS_API_URL", "access.authorization.form.pdns_api_key.label": "PDNS_API_KEY", @@ -47,7 +48,6 @@ "access.authorization.form.httpreq_endpoint.placeholder": "请输入 请求端点", "access.authorization.form.httpreq_mode.label": "模式", "access.authorization.form.httpreq_mode.placeholder": "请输入模式( RAW or '')", - "access.authorization.form.namesilo_api_key.placeholder": "请输入 NAMESILO_API_KEY", "access.authorization.form.username.label": "用户名", "access.authorization.form.username.placeholder": "请输入用户名", "access.authorization.form.password.label": "密码", diff --git a/ui/src/i18n/locales/zh/nls.domain.json b/ui/src/i18n/locales/zh/nls.domain.json index 62767c5d..e2d87fc6 100644 --- a/ui/src/i18n/locales/zh/nls.domain.json +++ b/ui/src/i18n/locales/zh/nls.domain.json @@ -35,9 +35,9 @@ "domain.application.form.email.list": "邮箱列表", "domain.application.form.access.label": "DNS 服务商授权配置", "domain.application.form.access.placeholder": "请选择 DNS 服务商授权配置", - "domain.application.form.access.list": "已有的 DNS 服务商授权配置", + "domain.application.form.access.list": "DNS 服务商授权配置列表", "domain.application.form.advanced_settings.label": "高级设置", - "domain.application.form.key_algorithm.label": "数字证书算法", + "domain.application.form.key_algorithm.label": "数字证书算法(默认:RSA2048)", "domain.application.form.key_algorithm.placeholder": "请选择数字证书算法", "domain.application.form.timeout.label": "DNS 传播检查超时时间(单位:秒)", "domain.application.form.timeoue.placeholder": "请输入 DNS 传播检查超时时间", @@ -50,10 +50,10 @@ "domain.deployment.nodata": "暂无部署配置,请添加后开始部署证书吧", "domain.deployment.form.type.label": "部署方式", "domain.deployment.form.type.placeholder": "请选择部署方式", - "domain.deployment.form.type.list": "支持的部署方式", + "domain.deployment.form.type.list": "部署方式列表", "domain.deployment.form.access.label": "授权配置", "domain.deployment.form.access.placeholder": "请选择授权配置", - "domain.deployment.form.access.list": "已有的服务商授权配置", + "domain.deployment.form.access.list": "服务商授权配置列表", "domain.deployment.form.domain.label": "部署到域名(仅支持单个域名;不支持泛域名)", "domain.deployment.form.domain.placeholder": "请输入部署到的域名", "domain.deployment.form.ssh_key_path.label": "私钥保存路径", diff --git a/ui/src/pages/access/Access.tsx b/ui/src/pages/access/Access.tsx index e596f876..6fb3cd66 100644 --- a/ui/src/pages/access/Access.tsx +++ b/ui/src/pages/access/Access.tsx @@ -16,18 +16,18 @@ import { import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import AccessEdit from "@/components/certimate/AccessEdit"; +import AccessEditDialog from "@/components/certimate/AccessEditDialog"; import AccessGroupEdit from "@/components/certimate/AccessGroupEdit"; import AccessGroupList from "@/components/certimate/AccessGroupList"; import XPagination from "@/components/certimate/XPagination"; import { convertZulu2Beijing } from "@/lib/time"; -import { Access as AccessType, accessTypeMap } from "@/domain/access"; +import { Access as AccessType, accessProvidersMap } from "@/domain/access"; import { remove } from "@/repository/access"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; const Access = () => { const { t } = useTranslation(); - const { config, deleteAccess } = useConfig(); + const { config, deleteAccess } = useConfigContext(); const { accesses } = config; const perPage = 10; @@ -62,7 +62,7 @@ const Access = () => {
{t("access.page.title")}
{tab != "access_group" ? ( - {t("access.authorization.add")}} op="add" /> + {t("access.authorization.add")}} op="add" /> ) : ( {t("access.group.add")}} /> )} @@ -95,7 +95,7 @@ const Access = () => {
{t("access.authorization.nodata")}
- {t("access.authorization.add")}} op="add" className="mt-3" /> + {t("access.authorization.add")}} op="add" className="mt-3" />
) : ( <> @@ -119,14 +119,14 @@ const Access = () => { >
{access.name}
- -
{t(accessTypeMap.get(access.configType)?.[0] || "")}
+ +
{t(accessProvidersMap.get(access.configType)?.name || "")}
{access.created && convertZulu2Beijing(access.created)}
{access.updated && convertZulu2Beijing(access.updated)}
- {t("common.edit")} @@ -136,7 +136,7 @@ const Access = () => { data={access} /> - {t("common.copy")} diff --git a/ui/src/pages/domains/Edit.tsx b/ui/src/pages/domains/Edit.tsx index dc1d2bb9..fbd57444 100644 --- a/ui/src/pages/domains/Edit.tsx +++ b/ui/src/pages/domains/Edit.tsx @@ -15,24 +15,24 @@ import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Toaster } from "@/components/ui/toaster"; import { useToast } from "@/components/ui/use-toast"; -import AccessEdit from "@/components/certimate/AccessEdit"; +import AccessEditDialog from "@/components/certimate/AccessEditDialog"; import DeployList from "@/components/certimate/DeployList"; import EmailsEdit from "@/components/certimate/EmailsEdit"; import StringList from "@/components/certimate/StringList"; import { cn } from "@/lib/utils"; import { PbErrorData } from "@/domain/base"; -import { accessTypeMap } from "@/domain/access"; +import { accessProvidersMap } from "@/domain/access"; import { EmailsSetting } from "@/domain/settings"; import { DeployConfig, Domain } from "@/domain/domain"; import { save, get } from "@/repository/domains"; -import { useConfig } from "@/providers/config"; +import { useConfigContext } from "@/providers/config"; import { Switch } from "@/components/ui/switch"; import { TooltipFast } from "@/components/ui/tooltip"; const Edit = () => { const { config: { accesses, emails }, - } = useConfig(); + } = useConfigContext(); const [domain, setDomain] = useState({} as Domain); @@ -301,7 +301,7 @@ const Edit = () => {
{t("domain.application.form.access.label")}
- @@ -330,7 +330,7 @@ const Edit = () => { .map((item) => (
- +
{item.name}
diff --git a/ui/src/providers/config/index.tsx b/ui/src/providers/config/index.tsx index ccc884f1..0b98c129 100644 --- a/ui/src/providers/config/index.tsx +++ b/ui/src/providers/config/index.tsx @@ -16,23 +16,23 @@ export type ConfigData = { export type ConfigContext = { config: ConfigData; - deleteAccess: (id: string) => void; + setEmails: (email: Setting) => void; addAccess: (access: Access) => void; updateAccess: (access: Access) => void; - setEmails: (email: Setting) => void; + deleteAccess: (id: string) => void; setAccessGroups: (accessGroups: AccessGroup[]) => void; reloadAccessGroups: () => void; }; const Context = createContext({} as ConfigContext); -export const useConfig = () => useContext(Context); +export const useConfigContext = () => useContext(Context); -interface ContainerProps { +interface ConfigProviderProps { children: ReactNode; } -export const ConfigProvider = ({ children }: ContainerProps) => { +export const ConfigProvider = ({ children }: ConfigProviderProps) => { const [config, dispatchConfig] = useReducer(configReducer, { accesses: [], emails: { content: { emails: [] } }, @@ -96,10 +96,10 @@ export const ConfigProvider = ({ children }: ContainerProps) => { emails: config.emails, accessGroups: config.accessGroups, }, - deleteAccess, - addAccess, setEmails, + addAccess, updateAccess, + deleteAccess, setAccessGroups, reloadAccessGroups, }} diff --git a/ui/src/providers/notify/index.tsx b/ui/src/providers/notify/index.tsx index 66cdda27..a0e9c195 100644 --- a/ui/src/providers/notify/index.tsx +++ b/ui/src/providers/notify/index.tsx @@ -12,12 +12,13 @@ export type NotifyContext = { const Context = createContext({} as NotifyContext); -export const useNotify = () => useContext(Context); -interface ContainerProps { +export const useNotifyContext = () => useContext(Context); + +interface NotifyProviderProps { children: ReactNode; } -export const NotifyProvider = ({ children }: ContainerProps) => { +export const NotifyProvider = ({ children }: NotifyProviderProps) => { const [notify, dispatchNotify] = useReducer(notifyReducer, {}); useEffect(() => {