diff --git a/internal/certapply/applicators/sp_xinnet.go b/internal/certapply/applicators/sp_xinnet.go new file mode 100644 index 000000000..cc953409d --- /dev/null +++ b/internal/certapply/applicators/sp_xinnet.go @@ -0,0 +1,28 @@ +package applicators + +import ( + "fmt" + + "github.com/go-acme/lego/v4/challenge" + + "github.com/certimate-go/certimate/internal/domain" + xinnet "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/xinnet" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeXinnet, func(options *ProviderFactoryOptions) (challenge.Provider, error) { + credentials := domain.AccessConfigForXinnet{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := xinnet.NewChallengeProvider(&xinnet.ChallengeProviderConfig{ + AgentId: credentials.AgentId, + ApiPassword: credentials.ApiPassword, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return provider, err + }) +} diff --git a/internal/domain/access.go b/internal/domain/access.go index f28c116de..8dd5f03e1 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -528,6 +528,11 @@ type AccessConfigForWestcn struct { ApiPassword string `json:"apiPassword"` } +type AccessConfigForXinnet struct { + AgentId string `json:"agentId"` + ApiPassword string `json:"apiPassword"` +} + type AccessConfigForZeroSSL struct { AccessConfigForACMEExternalAccountBinding } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 81b641372..26cdbdabf 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -108,6 +108,7 @@ const ( AccessProviderTypeWebhook = AccessProviderType("webhook") AccessProviderTypeWeComBot = AccessProviderType("wecombot") AccessProviderTypeWestcn = AccessProviderType("westcn") + AccessProviderTypeXinnet = AccessProviderType("xinnet") AccessProviderTypeZeroSSL = AccessProviderType("zerossl") ) @@ -209,6 +210,7 @@ const ( ACMEDns01ProviderTypeVolcEngineDNS = ACMEDns01ProviderType(AccessProviderTypeVolcEngine + "-dns") ACMEDns01ProviderTypeVultr = ACMEDns01ProviderType(AccessProviderTypeVultr) ACMEDns01ProviderTypeWestcn = ACMEDns01ProviderType(AccessProviderTypeWestcn) + ACMEDns01ProviderTypeXinnet = ACMEDns01ProviderType(AccessProviderTypeXinnet) ) type ACMEHttp01ProviderType string diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/xinnet/dnsla.go b/pkg/core/ssl-applicator/acme-dns01/providers/xinnet/dnsla.go new file mode 100644 index 000000000..894992861 --- /dev/null +++ b/pkg/core/ssl-applicator/acme-dns01/providers/xinnet/dnsla.go @@ -0,0 +1,39 @@ +package xinnet + +import ( + "errors" + "time" + + "github.com/certimate-go/certimate/pkg/core" + "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/xinnet/internal" +) + +type ChallengeProviderConfig struct { + AgentId string `json:"agentId"` + ApiPassword string `json:"apiPassword"` + DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (core.ACMEChallenger, error) { + if config == nil { + return nil, errors.New("the configuration of the acme challenge provider is nil") + } + + providerConfig := internal.NewDefaultConfig() + providerConfig.AgentID = config.AgentId + providerConfig.AppSecret = config.ApiPassword + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = config.DnsTTL + } + + provider, err := internal.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/xinnet/internal/lego.go b/pkg/core/ssl-applicator/acme-dns01/providers/xinnet/internal/lego.go new file mode 100644 index 000000000..6a4a0d3a1 --- /dev/null +++ b/pkg/core/ssl-applicator/acme-dns01/providers/xinnet/internal/lego.go @@ -0,0 +1,149 @@ +package internal + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/samber/lo" + + xinnetsdk "github.com/certimate-go/certimate/pkg/sdk3rd/xinnet" +) + +const ( + envNamespace = "DNSLA_" + + EnvAgentId = envNamespace + "AGENT_ID" + EnvAppSecret = envNamespace + "APP_SECRET" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +type Config struct { + AgentID string + AppSecret string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPTimeout time.Duration +} + +type DNSProvider struct { + client *xinnetsdk.Client + config *Config + + recordIDs map[string]*int64 + recordIDsMu sync.Mutex +} + +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, 600), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + } +} + +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAgentId, EnvAppSecret) + if err != nil { + return nil, fmt.Errorf("xinnet: %w", err) + } + + config := NewDefaultConfig() + config.AgentID = values[EnvAgentId] + config.AppSecret = values[EnvAppSecret] + + return NewDNSProviderConfig(config) +} + +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("xinnet: the configuration of the DNS provider is nil") + } + + client, err := xinnetsdk.NewClient(config.AgentID, config.AppSecret) + if err != nil { + return nil, err + } else { + client.SetTimeout(config.HTTPTimeout) + } + + return &DNSProvider{ + client: client, + config: config, + recordIDs: make(map[string]*int64), + recordIDsMu: sync.Mutex{}, + }, nil +} + +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("xinnet: could not find zone for domain %q: %w", domain, err) + } + + // REF: https://apidoc.xin.cn/doc-7283900 + xinnetDnsCreateReq := &xinnetsdk.DnsCreateRequest{ + DomainName: lo.ToPtr(dns01.UnFqdn(authZone)), + RecordName: lo.ToPtr(dns01.UnFqdn(info.EffectiveFQDN)), + Type: lo.ToPtr("TXT"), + Value: lo.ToPtr(info.Value), + Line: lo.ToPtr("默认"), + Ttl: lo.ToPtr(int32(d.config.TTL)), + } + xinnetDnsCreateResp, err := d.client.DnsCreate(xinnetDnsCreateReq) + if err != nil { + return fmt.Errorf("xinnet: error when create record: %w", err) + } + + d.recordIDsMu.Lock() + d.recordIDs[token] = xinnetDnsCreateResp.Data + d.recordIDsMu.Unlock() + + return nil +} + +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("xinnet: could not find zone for domain %q: %w", domain, err) + } + + d.recordIDsMu.Lock() + recordID, ok := d.recordIDs[token] + d.recordIDsMu.Unlock() + if !ok { + return fmt.Errorf("xinnet: unknown record ID for '%s'", info.EffectiveFQDN) + } + + // REF: https://apidoc.xin.cn/doc-7283901 + xinnetDnsDeleteReq := &xinnetsdk.DnsDeleteRequest{ + DomainName: lo.ToPtr(dns01.UnFqdn(authZone)), + RecordId: recordID, + } + if _, err := d.client.DnsDelete(xinnetDnsDeleteReq); err != nil { + return fmt.Errorf("xinnet: error when delete record: %w", err) + } + + return nil +} + +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/pkg/sdk3rd/xinnet/api_dns_create.go b/pkg/sdk3rd/xinnet/api_dns_create.go new file mode 100644 index 000000000..449e63f7b --- /dev/null +++ b/pkg/sdk3rd/xinnet/api_dns_create.go @@ -0,0 +1,42 @@ +package xinnet + +import ( + "context" +) + +type DnsCreateRequest struct { + DomainName *string `json:"domainName,omitempty"` + RecordName *string `json:"recordName,omitempty"` + Type *string `json:"type,omitempty"` + Value *string `json:"value,omitempty"` + Line *string `json:"line,omitempty"` + Ttl *int32 `json:"ttl,omitempty"` + Mx *int32 `json:"mx,omitempty"` + Status *int32 `json:"status,omitempty"` +} + +type DnsCreateResponse struct { + apiResponseBase + Data *int64 `json:"data,omitempty"` +} + +func (c *Client) DnsCreate(req *DnsCreateRequest) (*DnsCreateResponse, error) { + return c.DnsCreateWithContext(context.Background(), req) +} + +func (c *Client) DnsCreateWithContext(ctx context.Context, req *DnsCreateRequest) (*DnsCreateResponse, error) { + httpreq, err := c.newRequest("/dns/create/") + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &DnsCreateResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/xinnet/api_dns_delete.go b/pkg/sdk3rd/xinnet/api_dns_delete.go new file mode 100644 index 000000000..027a35717 --- /dev/null +++ b/pkg/sdk3rd/xinnet/api_dns_delete.go @@ -0,0 +1,35 @@ +package xinnet + +import ( + "context" +) + +type DnsDeleteRequest struct { + DomainName *string `json:"domainName,omitempty"` + RecordId *int64 `json:"recordId,omitempty"` +} + +type DnsDeleteResponse struct { + apiResponseBase +} + +func (c *Client) DnsDelete(req *DnsDeleteRequest) (*DnsDeleteResponse, error) { + return c.DnsDeleteWithContext(context.Background(), req) +} + +func (c *Client) DnsDeleteWithContext(ctx context.Context, req *DnsDeleteRequest) (*DnsDeleteResponse, error) { + httpreq, err := c.newRequest("/dns/delete/") + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &DnsDeleteResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/xinnet/client.go b/pkg/sdk3rd/xinnet/client.go new file mode 100644 index 000000000..0317545c8 --- /dev/null +++ b/pkg/sdk3rd/xinnet/client.go @@ -0,0 +1,149 @@ +package xinnet + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/go-resty/resty/v2" +) + +type Client struct { + client *resty.Client +} + +func NewClient(agentId, appSecret string) (*Client, error) { + if agentId == "" { + return nil, fmt.Errorf("sdkerr: unset agentId") + } + if appSecret == "" { + return nil, fmt.Errorf("sdkerr: unset appSecret") + } + + client := resty.New(). + SetBaseURL("https://apiv2.xinnet.com/api"). + SetHeader("Accept", "application/json"). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). + SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + // 生成时间戳 + timestamp := time.Now().UTC().Format("20060102T150405Z") + + // 获取请求路径,注意结尾必须是 "/" + urlPath := "/" + if req.URL != nil { + urlPath = req.URL.Path + + if !strings.HasSuffix(urlPath, "/") { + urlPath += "/" + } + } + + // 获取请求方法 + requestMethod := req.Method + + // 获取请求体 + requestBody := "" + if req.Body != nil { + reader, err := req.GetBody() + if err != nil { + return err + } + + defer reader.Close() + + payloadb, err := io.ReadAll(reader) + if err != nil { + return err + } + + requestBody = string(payloadb) + } + + // 计算签名 + algorithm := "HMAC-SHA256" + stringToSign := algorithm + "\n" + + timestamp + "\n" + + requestMethod + "\n" + + urlPath + "\n" + + requestBody + key := []byte(appSecret) + h := hmac.New(sha256.New, key) + h.Write([]byte(stringToSign)) + signature := hex.EncodeToString(h.Sum(nil)) + + // 设置请求头 + req.Header.Set("timestamp", timestamp) + req.Header.Set("authorization", fmt.Sprintf("%s Access=%s, Signature=%s", algorithm, agentId, signature)) + + return nil + }) + + return &Client{client}, nil +} + +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.client.SetTimeout(timeout) + return c +} + +func (c *Client) newRequest(path string) (*resty.Request, error) { + if path == "" { + return nil, fmt.Errorf("sdkerr: unset path") + } + + req := c.client.R() + req.Method = http.MethodPost + req.URL = path + return req, nil +} + +func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + // WARN: + // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. + + 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 apiResponse) (*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", err) + } else { + if tcode := res.GetCode(); tcode != "0" { + return resp, fmt.Errorf("sdkerr: code='%s', msg='%s'", tcode, res.GetMessage()) + } + } + } + + return resp, nil +} diff --git a/pkg/sdk3rd/xinnet/types.go b/pkg/sdk3rd/xinnet/types.go new file mode 100644 index 000000000..8da491ee2 --- /dev/null +++ b/pkg/sdk3rd/xinnet/types.go @@ -0,0 +1,39 @@ +package xinnet + +type apiResponse interface { + GetCode() string + GetMessage() string + GetRequestId() string +} + +type apiResponseBase struct { + Code *string `json:"code,omitempty"` + Message *string `json:"message,omitempty"` + RequestId *string `json:"requestId,omitempty"` +} + +func (r *apiResponseBase) GetCode() string { + if r.Code == nil { + return "" + } + + return *r.Code +} + +func (r *apiResponseBase) GetMessage() string { + if r.Message == nil { + return "" + } + + return *r.Message +} + +func (r *apiResponseBase) GetRequestId() string { + if r.RequestId == nil { + return "" + } + + return *r.RequestId +} + +var _ apiResponse = (*apiResponseBase)(nil) diff --git a/ui/public/imgs/providers/xinnet.png b/ui/public/imgs/providers/xinnet.png new file mode 100644 index 000000000..f4099d929 Binary files /dev/null and b/ui/public/imgs/providers/xinnet.png differ diff --git a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx index 3b03967e2..e6b8baaea 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx @@ -95,6 +95,7 @@ import AccessConfigFieldsProviderWangsu from "./AccessConfigFieldsProviderWangsu import AccessConfigFieldsProviderWebhook from "./AccessConfigFieldsProviderWebhook"; import AccessConfigFieldsProviderWeComBot from "./AccessConfigFieldsProviderWeComBot"; import AccessConfigFieldsProviderWestcn from "./AccessConfigFieldsProviderWestcn"; +import AccessConfigFieldsProviderXinnet from "./AccessConfigFieldsProviderXinnet"; import AccessConfigFieldsProviderZeroSSL from "./AccessConfigFieldsProviderZeroSSL"; const providerComponentMap: Partial>> = { @@ -195,6 +196,7 @@ const providerComponentMap: Partial { @@ -17,13 +19,7 @@ const AccessConfigFormFieldsProvider35cn = () => { return ( <> - } - > + @@ -32,10 +28,13 @@ const AccessConfigFormFieldsProvider35cn = () => { initialValue={initialValues.apiPassword} label={t("access.form.35cn_api_password.label")} rules={[formRule]} - tooltip={} > + + + } /> + ); }; diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderWestcn.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderWestcn.tsx index 9d613312b..e6d1a68d8 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderWestcn.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderWestcn.tsx @@ -3,6 +3,8 @@ import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import Tips from "@/components/Tips"; + import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderWestcn = () => { @@ -17,13 +19,7 @@ const AccessConfigFormFieldsProviderWestcn = () => { return ( <> - } - > + @@ -32,10 +28,13 @@ const AccessConfigFormFieldsProviderWestcn = () => { initialValue={initialValues.apiPassword} label={t("access.form.westcn_api_password.label")} rules={[formRule]} - tooltip={} > + + + } /> + ); }; diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderXinnet.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderXinnet.tsx new file mode 100644 index 000000000..6798e9399 --- /dev/null +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderXinnet.tsx @@ -0,0 +1,63 @@ +import { getI18n, useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import Tips from "@/components/Tips"; + +import { useFormNestedFieldsContext } from "./_context"; + +const AccessConfigFormFieldsProviderXinnet = () => { + 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 { + agentId: "", + apiPassword: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { + const { t } = i18n; + + return z.object({ + agentId: z.string().nonempty(t("access.form.xinnet_agent_id.placeholder")), + apiPassword: z.string().nonempty(t("access.form.xinnet_api_password.placeholder")), + }); +}; + +const _default = Object.assign(AccessConfigFormFieldsProviderXinnet, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 4fa6afba2..31b4c6b70 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -111,6 +111,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ WEBHOOK: "webhook", WECOMBOT: "wecombot", WESTCN: "westcn", + XINNET: "xinnet", ZEROSSL: "zerossl", } as const); @@ -214,6 +215,7 @@ export const accessProvidersMap: Maphttps://docs.1panel.pro/dev_manual/api_manual/", "access.form.35cn_username.label": "35.cn agent username", "access.form.35cn_username.placeholder": "Please enter West.cn agent username", - "access.form.35cn_username.tooltip": "For more information, see https://www.35.com/agent/mode-api.asp", "access.form.35cn_api_password.label": "35.cn agent API password", "access.form.35cn_api_password.placeholder": "Please enter West.cn agent API password", - "access.form.35cn_api_password.tooltip": "For more information, see https://www.35.com/agent/mode-api.asp", + "access.form.35cn_agent.guide": "West.cn API only supports calls from agents. Learn more about this:
https://www.35.com/agent/mode-api.asp", "access.form.acmeca_endpoint.label": "Endpoint", "access.form.acmeca_endpoint.placeholder": "Please enter endpoint", "access.form.acmeca_endpoint.tooltip": "For more information, see https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1", @@ -632,12 +631,16 @@ "access.form.webhook_preset_data.option.common.label": "General template", "access.form.wecombot_webhook_url.label": "WeCom bot Webhook URL", "access.form.wecombot_webhook_url.placeholder": "Please enter WeCom bot Webhook URL", - "access.form.wecombot_webhook_url.tooltip": "For more information, see https://open.work.weixin.qq.com/help2/pc/18401", + "access.form.wecombot_webhook_url.tooltip": "For more information, see https://www.west.cn/CustomerCenter/doc/apiv2.html", "access.form.westcn_username.label": "West.cn agent username", "access.form.westcn_username.placeholder": "Please enter West.cn agent username", - "access.form.westcn_username.tooltip": "For more information, see https://www.west.cn/CustomerCenter/doc/apiv2.html", "access.form.westcn_api_password.label": "West.cn agent API password", "access.form.westcn_api_password.placeholder": "Please enter West.cn agent API password", - "access.form.westcn_api_password.tooltip": "For more information, see https://www.west.cn/CustomerCenter/doc/apiv2.html", + "access.form.westcn_agent.guide": "West.cn API only supports calls from agents. Learn more about this:
https://www.west.cn/CustomerCenter/doc/apiv2.html", + "access.form.xinnet_agent_id.label": "Xinnet agent ID", + "access.form.xinnet_agent_id.placeholder": "Please enter Xinnet agent ID", + "access.form.xinnet_api_password.label": "Xinnet agent API password", + "access.form.xinnet_api_password.placeholder": "Please enter Xinnet agent API password", + "access.form.xinnet_agent.guide": "Xinnet API only supports calls from agents. Learn more about this:
https://apidoc.xin.cn/doc-7283837", "access.form.zerossl_eab.guide": "Learn more about using EAB key in ZeroSSL:
https://zerossl.com/documentation/acme/" } diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index cd4677e64..30bd7c9e8 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -200,6 +200,7 @@ "provider.webhook": "Webhook", "provider.wecombot": "WeCom Bot", "provider.westcn": "West.cn", + "provider.xinnet": "Xinnet", "provider.zerossl": "ZeroSSL", "provider.category.all": "All", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index bc641a4ff..9475c8a8c 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -59,10 +59,9 @@ "access.form.1panel_api_key.tooltip": "这是什么?请参阅 https://1panel.cn/docs/dev_manual/api_manual/", "access.form.35cn_username.label": "三五互联代理商用户名", "access.form.35cn_username.placeholder": "请输入三五互联代理商用户名", - "access.form.35cn_username.tooltip": "这是什么?请参阅 https://www.35.com/agent/mode-api.asp", "access.form.35cn_api_password.label": "三五互联代理商 API 密码", "access.form.35cn_api_password.placeholder": "请输入三五互联代理商 API 密码", - "access.form.35cn_api_password.tooltip": "这是什么?请参阅 https://www.35.com/agent/mode-api.asp", + "access.form.35cn_agent.guide": "三五互联 API 仅支持代理商调用。点击下方链接了解更多:
https://www.35.com/agent/mode-api.asp", "access.form.acmeca_endpoint.label": "服务端点", "access.form.acmeca_endpoint.placeholder": "请输入服务端点", "access.form.acmeca_endpoint.tooltip": "这是什么?请参阅 https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1", @@ -634,9 +633,13 @@ "access.form.wecombot_webhook_url.tooltip": "这是什么?请参阅 https://open.work.weixin.qq.com/help2/pc/18401", "access.form.westcn_username.label": "西部数码代理商用户名", "access.form.westcn_username.placeholder": "请输入西部数码代理商用户名", - "access.form.westcn_username.tooltip": "这是什么?请参阅 https://www.west.cn/CustomerCenter/doc/apiv2.html", "access.form.westcn_api_password.label": "西部数码代理商 API 密码", "access.form.westcn_api_password.placeholder": "请输入西部数码代理商 API 密码", - "access.form.westcn_api_password.tooltip": "这是什么?请参阅 https://www.west.cn/CustomerCenter/doc/apiv2.html", + "access.form.westcn_agent.guide": "西部数码 API 仅支持代理商调用。点击下方链接了解更多:
https://www.west.cn/CustomerCenter/doc/apiv2.html", + "access.form.xinnet_agent_id.label": "新网数码代理商编号", + "access.form.xinnet_agent_id.placeholder": "请输入新网数码代理商编号", + "access.form.xinnet_api_password.label": "新网数码代理商 API 密码", + "access.form.xinnet_api_password.placeholder": "请输入新网数码代理商 API 密码", + "access.form.xinnet_agent.guide": "新网数码 API 仅支持代理商调用。点击下方链接了解更多:
https://apidoc.xin.cn/doc-7283837", "access.form.zerossl_eab.guide": "点击下方链接了解如何获取 ZeroSSL EAB:
https://zerossl.com/documentation/acme/" } diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 8440253c3..17490ae2f 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -200,7 +200,9 @@ "provider.webhook": "Webhook", "provider.wecombot": "企业微信群机器人", "provider.westcn": "西部数码", + "provider.xinnet": "新网数码", "provider.zerossl": "ZeroSSL", + "provider.category.all": "全部", "provider.category.cdn": "CDN", "provider.category.storage": "文件存储",