diff --git a/internal/certmgmt/deployers/sp_axisnow.go b/internal/certmgmt/deployers/sp_axisnow.go new file mode 100644 index 000000000..48d114a1a --- /dev/null +++ b/internal/certmgmt/deployers/sp_axisnow.go @@ -0,0 +1,24 @@ +package deployers + +import ( + "fmt" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core" + dplyimpl "github.com/certimate-go/certimate/pkg/core/deployer/providers/axisnow" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + Registries.MustRegister(domain.DeploymentProviderTypeAxisNow, func(options *ProviderFactoryOptions) (core.Deployer, error) { + credentials := domain.AccessConfigForAxisNow{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{ + ApiToken: credentials.ApiToken, + }) + return provider, err + }) +} diff --git a/internal/domain/access.go b/internal/domain/access.go index a7fb37b22..a2ddf4487 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -87,6 +87,10 @@ type AccessConfigForAWS struct { SecretAccessKey string `json:"secretAccessKey"` } +type AccessConfigForAxisNow struct { + ApiToken string `json:"apiToken"` +} + type AccessConfigForAzure struct { TenantId string `json:"tenantId"` ClientId string `json:"clientId"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 707705cbb..51931f0b6 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -25,6 +25,7 @@ const ( AccessProviderTypeAPISIX = AccessProviderType("apisix") AccessProviderTypeArvanCloud = AccessProviderType("arvancloud") AccessProviderTypeAWS = AccessProviderType("aws") + AccessProviderTypeAxisNow = AccessProviderType("axisnow") AccessProviderTypeAzure = AccessProviderType("azure") AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud") AccessProviderTypeBaishan = AccessProviderType("baishan") @@ -342,6 +343,7 @@ const ( DeploymentProviderTypeAWSCloudFront = DeploymentProviderType(AccessProviderTypeAWS + "-cloudfront") DeploymentProviderTypeAWSIAM = DeploymentProviderType(AccessProviderTypeAWS + "-iam") DeploymentProviderTypeAWSNLB = DeploymentProviderType(AccessProviderTypeAWS + "-nlb") + DeploymentProviderTypeAxisNow = DeploymentProviderType(AccessProviderTypeAxisNow) DeploymentProviderTypeAzureKeyVault = DeploymentProviderType(AccessProviderTypeAzure + "-keyvault") DeploymentProviderTypeBaiduCloudAppBLB = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-appblb") DeploymentProviderTypeBaiduCloudBLB = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-blb") diff --git a/pkg/core/certmgr/providers/axisnow/axisnow.go b/pkg/core/certmgr/providers/axisnow/axisnow.go new file mode 100644 index 000000000..92ceaefed --- /dev/null +++ b/pkg/core/certmgr/providers/axisnow/axisnow.go @@ -0,0 +1,138 @@ +package axisnow + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/samber/lo" + + "github.com/certimate-go/certimate/pkg/core" + axisnowsdk "github.com/certimate-go/certimate/pkg/sdk3rd/axisnow" + xcert "github.com/certimate-go/certimate/pkg/utils/cert" +) + +type ( + Provider = core.Certmgr + UploadResult = core.CertmgrUploadResult + ReplaceResult = core.CertmgrReplaceResult +) + +type CertmgrConfig struct { + // AxisNow API 令牌。 + ApiToken string `json:"apiToken"` +} + +type Certmgr struct { + config *CertmgrConfig + logger *slog.Logger + sdkClient *axisnowsdk.Client +} + +var _ Provider = (*Certmgr)(nil) + +func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { + if config == nil { + return nil, fmt.Errorf("the configuration of the certmgr provider is nil") + } + + client, err := createSDKClient(config.ApiToken) + if err != nil { + return nil, fmt.Errorf("could not create client: %w", err) + } + + return &Certmgr{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, nil +} + +func (c *Certmgr) SetLogger(logger *slog.Logger) { + if logger == nil { + c.logger = slog.New(slog.DiscardHandler) + } else { + c.logger = logger + } +} + +func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*UploadResult, error) { + // 查询证书列表,避免重复上传 + // REF: https://developers.axisnow.io/api-reference/main/client/v1/#/operations/certificate-setting-list-certificates + listCertificatesPage := 1 + listCertificatesPerPage := 100 + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + listCertificatesReq := &axisnowsdk.ListCertificatesRequest{ + Page: lo.ToPtr(listCertificatesPage), + PerPage: lo.ToPtr(listCertificatesPerPage), + } + listCertificatesResp, err := c.sdkClient.ListCertificatesWithContext(ctx, listCertificatesReq) + c.logger.Debug("sdk request 'Certificates.ListCertificates'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'Certificates.ListCertificates': %w", err) + } else { + if listCertificatesResp.Results == nil { + break + } + + for _, certItem := range listCertificatesResp.Results { + if xcert.EqualCertificatesFromPEM(certPEM, certItem.Certificate) { + // 如果已存在相同证书,直接返回 + c.logger.Info("ssl certificate already exists") + return &UploadResult{ + CertId: certItem.UUID, + CertName: certItem.Name, + }, nil + } + } + + if len(listCertificatesResp.Results) < listCertificatesPerPage || + (listCertificatesResp.ResultInfo != nil && listCertificatesPage*listCertificatesPerPage >= listCertificatesResp.ResultInfo.TotalCount) { + break + } + + listCertificatesPage++ + } + } + + // 添加证书 + // REF: https://developers.axisnow.io/api-reference/main/client/v1/#/operations/certificate-setting-add-certificate + addCertificateReq := &axisnowsdk.AddCertificateRequest{ + Type: lo.ToPtr("upload"), + Name: lo.ToPtr(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())), + Certificate: lo.ToPtr(certPEM), + PrivateKey: lo.ToPtr(privkeyPEM), + } + addCertificateResp, err := c.sdkClient.AddCertificateWithContext(ctx, addCertificateReq) + c.logger.Debug("sdk request 'Certificates.AddCertificate'", slog.Any("request", addCertificateReq), slog.Any("response", addCertificateResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'Certificates.AddCertificate': %w", err) + } + + return &UploadResult{ + CertId: addCertificateResp.Result.UUID, + CertName: addCertificateResp.Result.Name, + }, nil +} + +func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*ReplaceResult, error) { + return nil, core.ErrUnsupported +} + +func createSDKClient(apiToken string) (*axisnowsdk.Client, error) { + client, err := axisnowsdk.NewClient( + axisnowsdk.WithApiToken(apiToken), + ) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/pkg/core/certmgr/providers/axisnow/axisnow_test.go b/pkg/core/certmgr/providers/axisnow/axisnow_test.go new file mode 100644 index 000000000..2870243ce --- /dev/null +++ b/pkg/core/certmgr/providers/axisnow/axisnow_test.go @@ -0,0 +1,44 @@ +package axisnow_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + impl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/axisnow" + it "github.com/certimate-go/certimate/pkg/core/certmgr/testing" +) + +var ( + fp = it.Args("AXISNOW_") + fTestCertPath string + fTestKeyPath string + fApiToken string +) + +func init() { + fp.DefineString(&fTestCertPath, "TESTCERTPATH") + fp.DefineString(&fTestKeyPath, "TESTKEYPATH") + fp.DefineString(&fApiToken, "APITOKEN") +} + +/* +Shell command to run this test: + + go test -v ./axisnow_test.go -args \ + --AXISNOW_TESTCERTPATH="/path/to/your-test-cert.pem" \ + --AXISNOW_TESTKEYPATH="/path/to/your-test-key.pem" \ + --AXISNOW_APITOKEN="your-api-token" +*/ +func TestProvider(t *testing.T) { + fp.Parse() + + t.Run("Upload", func(t *testing.T) { + provider, err := impl.NewCertmgr(&impl.CertmgrConfig{ + ApiToken: fApiToken, + }) + require.NoError(t, err) + + it.TestUpload(t, provider, it.TestUploadArgs{CertPath: fTestCertPath, KeyPath: fTestKeyPath}) + }) +} diff --git a/pkg/core/deployer/providers/axisnow/axisnow.go b/pkg/core/deployer/providers/axisnow/axisnow.go new file mode 100644 index 000000000..b56b619f1 --- /dev/null +++ b/pkg/core/deployer/providers/axisnow/axisnow.go @@ -0,0 +1,69 @@ +package axisnow + +import ( + "context" + "fmt" + "log/slog" + + "github.com/certimate-go/certimate/pkg/core" + cmgrimpl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/axisnow" +) + +type ( + Provider = core.Deployer + DeployResult = core.DeployerDeployResult +) + +type DeployerConfig struct { + // AxisNow API 令牌。 + ApiToken string `json:"apiToken"` +} + +type Deployer struct { + config *DeployerConfig + logger *slog.Logger + sdkCertmgr core.Certmgr +} + +var _ Provider = (*Deployer)(nil) + +func NewDeployer(config *DeployerConfig) (*Deployer, error) { + if config == nil { + return nil, fmt.Errorf("the configuration of the deployer provider is nil") + } + + pcertmgr, err := cmgrimpl.NewCertmgr(&cmgrimpl.CertmgrConfig{ + ApiToken: config.ApiToken, + }) + if err != nil { + return nil, fmt.Errorf("could not create certmgr: %w", err) + } + + return &Deployer{ + config: config, + logger: slog.Default(), + sdkCertmgr: pcertmgr, + }, nil +} + +func (d *Deployer) SetLogger(logger *slog.Logger) { + if logger == nil { + d.logger = slog.New(slog.DiscardHandler) + } else { + d.logger = logger + } + + d.sdkCertmgr.SetLogger(logger) +} + +func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) { + // 上传证书 + upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) + if err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + + return &DeployResult{}, nil +} diff --git a/pkg/sdk3rd/axisnow/api_add_certificate.go b/pkg/sdk3rd/axisnow/api_add_certificate.go new file mode 100644 index 000000000..b708e651d --- /dev/null +++ b/pkg/sdk3rd/axisnow/api_add_certificate.go @@ -0,0 +1,40 @@ +package axisnow + +import ( + "context" + "net/http" +) + +type AddCertificateRequest struct { + Name *string `json:"name,omitempty"` + Type *string `json:"type,omitempty"` + Certificate *string `json:"certificate,omitempty"` + PrivateKey *string `json:"private_key,omitempty"` +} + +type AddCertificateResponse struct { + sdkResponseBase + + Result *Certificate `json:"result,omitempty"` +} + +func (c *Client) AddCertificate(req *AddCertificateRequest) (*AddCertificateResponse, error) { + return c.AddCertificateWithContext(context.Background(), req) +} + +func (c *Client) AddCertificateWithContext(ctx context.Context, req *AddCertificateRequest) (*AddCertificateResponse, error) { + httpreq, err := c.newRequest(http.MethodPost, "/certificates") + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &AddCertificateResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/axisnow/api_list_certificates.go b/pkg/sdk3rd/axisnow/api_list_certificates.go new file mode 100644 index 000000000..06e8233e9 --- /dev/null +++ b/pkg/sdk3rd/axisnow/api_list_certificates.go @@ -0,0 +1,53 @@ +package axisnow + +import ( + "context" + "net/http" + + qs "github.com/google/go-querystring/query" +) + +type ListCertificatesRequest struct { + Filter *string `json:"filter,omitempty" url:"filter,omitempty"` + Page *int `json:"page,omitempty" url:"page,omitempty"` + PerPage *int `json:"per_page,omitempty" url:"per_page,omitempty"` +} + +type ListCertificatesResponse struct { + sdkResponseBase + + ResultInfo *struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + Count int `json:"count"` + TotalCount int `json:"total_count"` + SubscribedTotal int `json:"subscribed_total"` + } `json:"result_info,omitempty"` + Results []*Certificate `json:"result,omitempty"` +} + +func (c *Client) ListCertificates(req *ListCertificatesRequest) (*ListCertificatesResponse, error) { + return c.ListCertificatesWithContext(context.Background(), req) +} + +func (c *Client) ListCertificatesWithContext(ctx context.Context, req *ListCertificatesRequest) (*ListCertificatesResponse, error) { + httpreq, err := c.newRequest(http.MethodGet, "/certificates") + if err != nil { + return nil, err + } else { + values, err := qs.Values(req) + if err != nil { + return nil, err + } + + httpreq.SetQueryParamsFromValues(values) + httpreq.SetContext(ctx) + } + + result := &ListCertificatesResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/axisnow/client.go b/pkg/sdk3rd/axisnow/client.go new file mode 100644 index 000000000..fc0cf3a97 --- /dev/null +++ b/pkg/sdk3rd/axisnow/client.go @@ -0,0 +1,100 @@ +// A simple SDK client for AxisNow. +// API documentation: https://developers.axisnow.io/api-reference/main/client/v1/#/ +package axisnow + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/certimate-go/certimate/internal/app" +) + +type Client struct { + rc *resty.Client +} + +func NewClient(optFns ...OptionsFunc) (*Client, error) { + opts := &Options{} + for _, fn := range optFns { + fn(opts) + } + + if opts.ApiToken == "" { + return nil, fmt.Errorf("sdkerr: unset apiToken") + } + + httper := resty.New(). + SetBaseURL("https://api.axisnow.io/client/v1"). + SetHeader("Accept", "application/json"). + SetHeader("Authorization", "Bearer "+opts.ApiToken). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", app.AppUserAgent) + + return &Client{rc: httper}, nil +} + +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.rc.SetTimeout(timeout) + return c +} + +func (c *Client) newRequest(method string, path string) (*resty.Request, error) { + if method == "" { + return nil, fmt.Errorf("sdkerr: unset method") + } + if path == "" { + return nil, fmt.Errorf("sdkerr: unset path") + } + + req := c.rc.R() + req.Method = method + req.URL = path + + // WARN: + // DO NOT CALL `req.SetResult` or `req.SetError` AGAIN! USE `doRequestWithResult` INSTEAD. + return req, nil +} + +func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := c.doRequest(req) + if err != nil { + if resp != nil { + json.Unmarshal(resp.Body(), &res) + } + return resp, err + } + + if len(resp.Body()) != 0 { + if err := json.Unmarshal(resp.Body(), &res); err != nil { + return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) + } else { + if err := res.GetAPIError(); err != nil { + return resp, fmt.Errorf("sdkerr: api error: %s", err.Error()) + } + } + } + + return resp, nil +} diff --git a/pkg/sdk3rd/axisnow/models.go b/pkg/sdk3rd/axisnow/models.go new file mode 100644 index 000000000..d46265cd0 --- /dev/null +++ b/pkg/sdk3rd/axisnow/models.go @@ -0,0 +1,29 @@ +package axisnow + +type Certificate struct { + UUID string `json:"uuid,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + SubjectName []string `json:"subject_name,omitempty"` + Issuer CertificateIssuer `json:"issuer,omitempty"` + Subject CertificateSubject `json:"subject,omitempty"` + IssueTime int64 `json:"issue_time,omitempty"` + ExpireTime int64 `json:"expire_time,omitempty"` + Certificate string `json:"certificate,omitempty"` + ReferencedCount int `json:"referenced_count,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` +} + +type CertificateIssuer struct { + Country string `json:"country,omitempty"` + Organization string `json:"organization,omitempty"` + OrganizationUnit string `json:"organization_unit,omitempty"` + CommonName string `json:"common_name,omitempty"` +} + +type CertificateSubject struct { + Organization string `json:"organization,omitempty"` + OrganizationUnit string `json:"organization_unit,omitempty"` + CommonName string `json:"common_name,omitempty"` +} diff --git a/pkg/sdk3rd/axisnow/options.go b/pkg/sdk3rd/axisnow/options.go new file mode 100644 index 000000000..038ebdff1 --- /dev/null +++ b/pkg/sdk3rd/axisnow/options.go @@ -0,0 +1,13 @@ +package axisnow + +type Options struct { + ApiToken string +} + +type OptionsFunc func(*Options) + +func WithApiToken(apiToken string) OptionsFunc { + return func(o *Options) { + o.ApiToken = apiToken + } +} diff --git a/pkg/sdk3rd/axisnow/types.go b/pkg/sdk3rd/axisnow/types.go new file mode 100644 index 000000000..f26f4d09a --- /dev/null +++ b/pkg/sdk3rd/axisnow/types.go @@ -0,0 +1,46 @@ +package axisnow + +import ( + "fmt" + "strings" +) + +type sdkResponse interface { + GetAPIError() error + GetSuccess() bool +} + +type sdkResponseBase struct { + Success bool `json:"success"` + Errors sdkAPIErrors `json:"errors,omitempty"` +} + +type sdkAPIError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type sdkAPIErrors []sdkAPIError + +func (e sdkAPIErrors) Error() string { + builder := &strings.Builder{} + + for _, item := range e { + fmt.Fprintf(builder, "[%d] %s", item.Code, item.Message) + } + + return strings.TrimSpace(builder.String()) +} + +func (r *sdkResponseBase) GetSuccess() bool { + return r.Success +} + +func (r *sdkResponseBase) GetAPIError() error { + if len(r.Errors) > 0 { + return r.Errors + } + return nil +} + +var _ sdkResponse = (*sdkResponseBase)(nil) diff --git a/ui/public/imgs/providers/axisnow.png b/ui/public/imgs/providers/axisnow.png new file mode 100644 index 000000000..bea9f00b1 Binary files /dev/null and b/ui/public/imgs/providers/axisnow.png differ diff --git a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx index 920d9466d..376ebe3db 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx @@ -14,6 +14,7 @@ import AccessConfigFieldsProviderAliyun from "./AccessConfigFieldsProviderAliyun import AccessConfigFieldsProviderAPISIX from "./AccessConfigFieldsProviderAPISIX"; import AccessConfigFieldsProviderArvanCloud from "./AccessConfigFieldsProviderArvanCloud"; import AccessConfigFieldsProviderAWS from "./AccessConfigFieldsProviderAWS"; +import AccessConfigFieldsProviderAxisNow from "./AccessConfigFieldsProviderAxisNow"; import AccessConfigFieldsProviderAzure from "./AccessConfigFieldsProviderAzure"; import AccessConfigFieldsProviderBaiduCloud from "./AccessConfigFieldsProviderBaiduCloud"; import AccessConfigFieldsProviderBaishan from "./AccessConfigFieldsProviderBaishan"; @@ -141,6 +142,7 @@ const providerComponentMap: Partial { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + <> + } + > + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + apiToken: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { + const { t: _ } = i18n; + + return z.object({ + apiToken: z.string().nonempty(), + }); +}; + +const _default = Object.assign(AccessConfigFormFieldsProviderAxisNow, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 100e63c32..ee113aab6 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -29,6 +29,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ APISIX: "apisix", ARVANCLOUD: "arvancloud", AWS: "aws", + AXISNOW: "axisnow", AZURE: "azure", BAIDUCLOUD: "baiducloud", BAISHAN: "baishan", @@ -216,6 +217,7 @@ export const accessProvidersMap: Maphttps://docs.aws.amazon.com/en_us/IAM/latest/UserGuide/id_credentials_access-keys.html" }, + "axisnow_api_token": { + "label": "AxisNow API token", + "placeholder": "Please enter AxisNow API token", + "tooltip": "For more information, see https://www.axisnow.io/zh/docs/edge/tenant/api-tokens" + }, "azure_tenant_id": { "label": "Azure tenant ID", "placeholder": "Please enter Azure tenant ID", diff --git a/ui/src/i18n/resources/en/nls.provider.json b/ui/src/i18n/resources/en/nls.provider.json index 8b33868d2..a12c53bae 100644 --- a/ui/src/i18n/resources/en/nls.provider.json +++ b/ui/src/i18n/resources/en/nls.provider.json @@ -69,6 +69,7 @@ "aws_lightsail": "AWS - Lightsail", "aws_nlb": "AWS - NLB (Network Load Balancer)", "aws_route53": "AWS - Route53", + "axisnow": "AxisNow", "azure": "Azure (Microsoft Azure)", "azure_dns": "Azure - DNS", "azure_keyvault": "Azure - KeyVault", diff --git a/ui/src/i18n/resources/zh/nls.access.json b/ui/src/i18n/resources/zh/nls.access.json index 60dffd138..ad045dee6 100644 --- a/ui/src/i18n/resources/zh/nls.access.json +++ b/ui/src/i18n/resources/zh/nls.access.json @@ -243,6 +243,11 @@ "placeholder": "请输入 AWS SecretAccessKey", "tooltip": "这是什么?请参阅 https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/id_credentials_access-keys.html" }, + "axisnow_api_token": { + "label": "AxisNow API 令牌", + "placeholder": "请输入 AxisNow API 令牌", + "tooltip": "这是什么?请参阅 https://www.axisnow.io/zh/docs/edge/tenant/api-tokens" + }, "azure_tenant_id": { "label": "Azure 租户 ID", "placeholder": "请输入 Azure 租户 ID", diff --git a/ui/src/i18n/resources/zh/nls.provider.json b/ui/src/i18n/resources/zh/nls.provider.json index 9d3396023..11166a60c 100644 --- a/ui/src/i18n/resources/zh/nls.provider.json +++ b/ui/src/i18n/resources/zh/nls.provider.json @@ -69,6 +69,7 @@ "aws_lightsail": "AWS - Lightsail", "aws_nlb": "AWS - 网络负载均衡器 NLB", "aws_route53": "AWS - Route53", + "axisnow": "AxisNow", "azure": "Azure(Microsoft Azure)", "azure_dns": "Azure - DNS", "azure_keyvault": "Azure - KeyVault",