diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index 1d5177bf..c5de24ef 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -38,6 +38,7 @@ import ( pPorkbun "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/porkbun" pPowerDNS "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/powerdns" pRainYun "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/rainyun" + pSpaceship "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/spaceship" pTencentCloud "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/tencentcloud" pTencentCloudEO "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/tencentcloud-eo" pUCloudUDNR "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/ucloud-udnr" @@ -582,6 +583,22 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi return applicant, err } + case domain.ACMEDns01ProviderTypeSpaceship: + { + access := domain.AccessConfigForSpaceship{} + if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + applicant, err := pSpaceship.NewChallengeProvider(&pSpaceship.ChallengeProviderConfig{ + ApiKey: access.ApiKey, + ApiSecret: access.ApiSecret, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + } + case domain.ACMEDns01ProviderTypeTencentCloud, domain.ACMEDns01ProviderTypeTencentCloudDNS, domain.ACMEDns01ProviderTypeTencentCloudEO: { access := domain.AccessConfigForTencentCloud{} diff --git a/internal/domain/access.go b/internal/domain/access.go index 29d07513..178a9d57 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -324,6 +324,11 @@ type AccessConfigForSlackBot struct { DefaultChannelId string `json:"defaultChannelId,omitempty"` } +type AccessConfigForSpaceship struct { + ApiKey string `json:"apiKey"` + ApiSecret string `json:"apiSecret"` +} + type AccessConfigForSSH struct { Host string `json:"host"` Port int32 `json:"port"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 3688d5e3..6deedee8 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -74,6 +74,7 @@ const ( AccessProviderTypeRatPanel = AccessProviderType("ratpanel") AccessProviderTypeSafeLine = AccessProviderType("safeline") AccessProviderTypeSlackBot = AccessProviderType("slackbot") + AccessProviderTypeSpaceship = AccessProviderType("spaceship") AccessProviderTypeSSH = AccessProviderType("ssh") AccessProviderTypeSSLCOM = AccessProviderType("sslcom") AccessProviderTypeTelegramBot = AccessProviderType("telegrambot") @@ -159,6 +160,7 @@ const ( ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun) ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS) ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun) + ACMEDns01ProviderTypeSpaceship = ACMEDns01ProviderType(AccessProviderTypeSpaceship) ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS] ACMEDns01ProviderTypeTencentCloudDNS = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-dns") ACMEDns01ProviderTypeTencentCloudEO = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-eo") diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/spaceship/spaceship.go b/pkg/core/ssl-applicator/acme-dns01/providers/spaceship/spaceship.go new file mode 100644 index 00000000..c739c11c --- /dev/null +++ b/pkg/core/ssl-applicator/acme-dns01/providers/spaceship/spaceship.go @@ -0,0 +1,40 @@ +package spaceship + +import ( + "errors" + "time" + + "github.com/go-acme/lego/v4/providers/dns/spaceship" + + "github.com/certimate-go/certimate/pkg/core" +) + +type ChallengeProviderConfig struct { + ApiKey string `json:"apiKey"` + ApiSecret string `json:"apiSecret"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `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 := spaceship.NewDefaultConfig() + providerConfig.APIKey = config.ApiKey + providerConfig.APISecret = config.ApiSecret + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = int(config.DnsTTL) + } + + provider, err := spaceship.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/ui/public/imgs/providers/spaceship.png b/ui/public/imgs/providers/spaceship.png new file mode 100644 index 00000000..79d6fe2a Binary files /dev/null and b/ui/public/imgs/providers/spaceship.png differ diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index 09553347..d49fe661 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -68,6 +68,7 @@ import AccessFormRainYunConfig from "./AccessFormRainYunConfig"; import AccessFormRatPanelConfig from "./AccessFormRatPanelConfig"; import AccessFormSafeLineConfig from "./AccessFormSafeLineConfig"; import AccessFormSlackBotConfig from "./AccessFormSlackBotConfig"; +import AccessFormSpaceshipConfig from "./AccessFormSpaceshipConfig"; import AccessFormSSHConfig from "./AccessFormSSHConfig"; import AccessFormSSLComConfig from "./AccessFormSSLComConfig"; import AccessFormTelegramBotConfig from "./AccessFormTelegramBotConfig"; @@ -301,6 +302,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.SLACKBOT: return ; + case ACCESS_PROVIDERS.SPACESHIP: + return ; case ACCESS_PROVIDERS.SSH: return ; case ACCESS_PROVIDERS.TELEGRAMBOT: diff --git a/ui/src/components/access/AccessFormSpaceshipConfig.tsx b/ui/src/components/access/AccessFormSpaceshipConfig.tsx new file mode 100644 index 00000000..7437a2f6 --- /dev/null +++ b/ui/src/components/access/AccessFormSpaceshipConfig.tsx @@ -0,0 +1,68 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod/v4"; + +import { type AccessConfigForSpaceship } from "@/domain/access"; + +type AccessFormSpaceshipConfigFieldValues = Nullish; + +export type AccessFormSpaceshipConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormSpaceshipConfigFieldValues; + onValuesChange?: (values: AccessFormSpaceshipConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormSpaceshipConfigFieldValues => { + return { + apiKey: "", + apiSecret: "", + }; +}; + +const AccessFormSpaceshipConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormSpaceshipConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiKey: z.string().nonempty(t("access.form.spaceship_api_key.placeholder")), + apiSecret: z.string().nonempty(t("access.form.spaceship_api_secret.placeholder")), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessFormSpaceshipConfig; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index a2dbde73..df0e8a06 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -62,6 +62,7 @@ export interface AccessModel extends BaseModel { | AccessConfigForRatPanel | AccessConfigForSafeLine | AccessConfigForSlackBot + | AccessConfigForSpaceship | AccessConfigForSSH | AccessConfigForSSLCom | AccessConfigForTelegramBot @@ -389,6 +390,11 @@ export type AccessConfigForSlackBot = { defaultChannelId?: string; }; +export type AccessConfigForSpaceship = { + apiKey: string; + apiSecret: string; +}; + export type AccessConfigForSSH = { host: string; port: number; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index fb1e2e12..12511f81 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -65,6 +65,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ RATPANEL: "ratpanel", SAFELINE: "safeline", SLACKBOT: "slackbot", + SPACESHIP: "spaceship", SSH: "ssh", SSLCOM: "sslcom", TELEGRAMBOT: "telegrambot", @@ -164,6 +165,7 @@ export const accessProvidersMap: Maphttps://www.youtube.com/watch?v=Uz5Yi5C2pwQ", + "access.form.spaceship_api_key.label": "Spaceship API key", + "access.form.spaceship_api_key.placeholder": "Please enter Spaceship API key", + "access.form.spaceship_api_key.tooltip": "For more information, see https://www.spaceship.com/application/api-manager/", + "access.form.spaceship_api_secret.label": "Spaceship API secret", + "access.form.spaceship_api_secret.placeholder": "Please enter Spaceship API secret", + "access.form.spaceship_api_secret.tooltip": "For more information, see https://www.spaceship.com/application/api-manager/", "access.form.ssh_host.label": "Server host", "access.form.ssh_host.placeholder": "Please enter server host", "access.form.ssh_port.label": "Server port", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index f2a07351..fe76a727 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -130,6 +130,7 @@ "provider.ratpanel.site": "RatPanel - Website", "provider.safeline": "SafeLine", "provider.slackbot": "Slack Bot", + "provider.spaceship": "Spaceship", "provider.ssh": "Remote host (SSH)", "provider.sslcom": "SSL.com", "provider.telegrambot": "Telegram Bot", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index a3852926..837ccc17 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -387,6 +387,12 @@ "access.form.slackbot_default_channel_id.label": "默认的 Slack 频道 ID(可选)", "access.form.slackbot_default_channel_id.placeholder": "请输入默认的 Slack 频道 ID", "access.form.slackbot_default_channel_id.tooltip": "如何获取此参数?请参阅 https://www.youtube.com/watch?v=Uz5Yi5C2pwQ", + "access.form.spaceship_api_key.label": "Spaceship API Key", + "access.form.spaceship_api_key.placeholder": "请输入 Spaceship API Key", + "access.form.spaceship_api_key.tooltip": "这是什么?请参阅 https://www.spaceship.com/application/api-manager/", + "access.form.spaceship_api_secret.label": "Spaceship API Secret", + "access.form.spaceship_api_secret.placeholder": "请输入 Spaceship API Secret", + "access.form.spaceship_api_secret.tooltip": "这是什么?请参阅 https://www.spaceship.com/application/api-manager/", "access.form.ssh_host.label": "服务器地址", "access.form.ssh_host.placeholder": "请输入服务器地址", "access.form.ssh_port.label": "服务器端口", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 36e01d37..fa16398a 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -130,6 +130,7 @@ "provider.ratpanel.site": "耗子面板 - 网站", "provider.safeline": "雷池", "provider.slackbot": "Slack 机器人", + "provider.spaceship": "Spaceship", "provider.ssh": "远程主机(SSH)", "provider.sslcom": "SSL.com", "provider.telegrambot": "Telegram 机器人",