From 614d4770e0ee7fd841f966d1288bdfa096a2ed4f Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sat, 15 Nov 2025 20:23:05 +0800 Subject: [PATCH] feat(provider): new acme dns-01 provider: xinnet --- internal/certapply/applicators/sp_xinnet.go | 28 ++++ internal/domain/access.go | 5 + internal/domain/provider.go | 2 + .../acme-dns01/providers/xinnet/dnsla.go | 39 +++++ .../providers/xinnet/internal/lego.go | 149 ++++++++++++++++++ pkg/sdk3rd/xinnet/api_dns_create.go | 42 +++++ pkg/sdk3rd/xinnet/api_dns_delete.go | 35 ++++ pkg/sdk3rd/xinnet/client.go | 149 ++++++++++++++++++ pkg/sdk3rd/xinnet/types.go | 39 +++++ ui/public/imgs/providers/xinnet.png | Bin 0 -> 9409 bytes .../forms/AccessConfigFieldsProvider.tsx | 2 + .../forms/AccessConfigFieldsProvider35cn.tsx | 15 +- .../AccessConfigFieldsProviderWestcn.tsx | 15 +- .../AccessConfigFieldsProviderXinnet.tsx | 63 ++++++++ ui/src/domain/provider.ts | 4 + ui/src/i18n/locales/en/nls.access.json | 13 +- ui/src/i18n/locales/en/nls.provider.json | 1 + ui/src/i18n/locales/zh/nls.access.json | 11 +- ui/src/i18n/locales/zh/nls.provider.json | 2 + 19 files changed, 589 insertions(+), 25 deletions(-) create mode 100644 internal/certapply/applicators/sp_xinnet.go create mode 100644 pkg/core/ssl-applicator/acme-dns01/providers/xinnet/dnsla.go create mode 100644 pkg/core/ssl-applicator/acme-dns01/providers/xinnet/internal/lego.go create mode 100644 pkg/sdk3rd/xinnet/api_dns_create.go create mode 100644 pkg/sdk3rd/xinnet/api_dns_delete.go create mode 100644 pkg/sdk3rd/xinnet/client.go create mode 100644 pkg/sdk3rd/xinnet/types.go create mode 100644 ui/public/imgs/providers/xinnet.png create mode 100644 ui/src/components/access/forms/AccessConfigFieldsProviderXinnet.tsx 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 0000000000000000000000000000000000000000..f4099d929f3ec200ea388e2e405b1af0693e9fcf GIT binary patch literal 9409 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 Z=5i?3iv;IR|A{RCDhlfIjj|RI{{sz~s{#N3 literal 0 HcmV?d00001 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": "文件存储",