From 8bec234fe8bd035702a6972a91a1cc72f333a8fa Mon Sep 17 00:00:00 2001 From: yoan <536464346@qq.com> Date: Wed, 23 Oct 2024 13:22:17 +0800 Subject: [PATCH] gts support --- internal/applicant/applicant.go | 20 +- ui/public/imgs/providers/google.svg | 28 ++ ui/src/domain/settings.ts | 6 +- ui/src/pages/setting/SSLProvider.tsx | 520 +++++++++++++++++++-------- ui/src/providers/config/index.tsx | 8 +- ui/src/providers/config/reducer.ts | 2 +- ui/src/providers/notify/index.tsx | 10 +- ui/src/providers/notify/reducer.tsx | 6 +- ui/src/repository/settings.ts | 14 +- 9 files changed, 447 insertions(+), 167 deletions(-) create mode 100644 ui/public/imgs/providers/google.svg diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index cdaab89e..0a00e096 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -39,16 +39,19 @@ const defaultSSLProvider = "letsencrypt" const ( sslProviderLetsencrypt = "letsencrypt" sslProviderZeroSSL = "zerossl" + sslProviderGts = "gts" ) const ( zerosslUrl = "https://acme.zerossl.com/v2/DV90" letsencryptUrl = "https://acme-v02.api.letsencrypt.org/directory" + gtsUrl = "https://dv.acme-v02.api.pki.goog/directory" ) var sslProviderUrls = map[string]string{ sslProviderLetsencrypt: letsencryptUrl, sslProviderZeroSSL: zerosslUrl, + sslProviderGts: gtsUrl, } const defaultEmail = "536464346@qq.com" @@ -157,10 +160,13 @@ type SSLProviderConfig struct { } type SSLProviderConfigContent struct { - Zerossl struct { - EabHmacKey string `json:"eabHmacKey"` - EabKid string `json:"eabKid"` - } + Zerossl SSLProviderEab `json:"zerossl"` + Gts SSLProviderEab `json:"gts"` +} + +type SSLProviderEab struct { + EabHmacKey string `json:"eabHmacKey"` + EabKid string `json:"eabKid"` } func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, error) { @@ -247,6 +253,12 @@ func getReg(client *lego.Client, sslProvider *SSLProviderConfig) (*registration. Kid: sslProvider.Config.Zerossl.EabKid, HmacEncoded: sslProvider.Config.Zerossl.EabHmacKey, }) + case sslProviderGts: + reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ + TermsOfServiceAgreed: true, + Kid: sslProvider.Config.Gts.EabKid, + HmacEncoded: sslProvider.Config.Gts.EabHmacKey, + }) case sslProviderLetsencrypt: reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) diff --git a/ui/public/imgs/providers/google.svg b/ui/public/imgs/providers/google.svg new file mode 100644 index 00000000..d5f4dbeb --- /dev/null +++ b/ui/public/imgs/providers/google.svg @@ -0,0 +1,28 @@ + + + + + Google-color + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts index 8ee2dad0..62f00670 100644 --- a/ui/src/domain/settings.ts +++ b/ui/src/domain/settings.ts @@ -1,7 +1,7 @@ -export type Setting = { +export type Setting = { id?: string; name?: string; - content?: EmailsSetting | NotifyTemplates | NotifyChannels | SSLProviderSetting; + content?: T; }; export type EmailsSetting = { @@ -53,7 +53,7 @@ export const defaultNotifyTemplate: NotifyTemplate = { content: "有 {COUNT} 张证书即将过期,域名分别为 {DOMAINS},请保持关注!", }; -export type SSLProvider = "letsencrypt" | "zerossl"; +export type SSLProvider = "letsencrypt" | "zerossl" | "gts"; export type SSLProviderSetting = { provider: SSLProvider; diff --git a/ui/src/pages/setting/SSLProvider.tsx b/ui/src/pages/setting/SSLProvider.tsx index f5c1b28e..4894d462 100644 --- a/ui/src/pages/setting/SSLProvider.tsx +++ b/ui/src/pages/setting/SSLProvider.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useContext, useEffect, useState, createContext } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { z } from "zod"; @@ -14,93 +14,71 @@ import { getErrMessage } from "@/lib/error"; import { cn } from "@/lib/utils"; import { SSLProvider as SSLProviderType, SSLProviderSetting, Setting } from "@/domain/settings"; import { getSetting, update } from "@/repository/settings"; +import { produce } from "immer"; + +type SSLProviderContext = { + setting: Setting; + onSubmit: (data: Setting) => void; + setConfig: (config: Setting) => void; +}; + +const Context = createContext({} as SSLProviderContext); + +export const useSSLProviderContext = () => { + return useContext(Context); +}; const SSLProvider = () => { const { t } = useTranslation(); - const formSchema = z.object({ - provider: z.enum(["letsencrypt", "zerossl"], { - message: t("settings.ca.provider.errmsg.empty"), - }), - eabKid: z.string().optional(), - eabHmacKey: z.string().optional(), - }); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { + const [config, setConfig] = useState>({ + id: "", + content: { provider: "letsencrypt", + config: {}, }, }); - const [provider, setProvider] = useState("letsencrypt"); - - const [config, setConfig] = useState(); - const { toast } = useToast(); useEffect(() => { const fetchData = async () => { - const setting = await getSetting("ssl-provider"); + const setting = await getSetting("ssl-provider"); if (setting) { setConfig(setting); - const content = setting.content as SSLProviderSetting; - - form.setValue("provider", content.provider); - form.setValue("eabKid", content.config[content.provider].eabKid); - form.setValue("eabHmacKey", content.config[content.provider].eabHmacKey); - setProvider(content.provider); - } else { - form.setValue("provider", "letsencrypt"); - setProvider("letsencrypt"); } }; fetchData(); }, []); + const setProvider = (val: SSLProviderType) => { + const newData = produce(config, (draft) => { + if (draft.content) { + draft.content.provider = val; + } else { + draft.content = { + provider: val, + config: {}, + }; + } + }); + setConfig(newData); + }; + const getOptionCls = (val: string) => { - if (provider === val) { + if (config.content?.provider === val) { return "border-primary"; } return ""; }; - const onSubmit = async (values: z.infer) => { - if (values.provider === "zerossl") { - if (!values.eabKid) { - form.setError("eabKid", { - message: t("settings.ca.eab_kid_hmac_key.errmsg.empty"), - }); - } - if (!values.eabHmacKey) { - form.setError("eabHmacKey", { - message: t("settings.ca.eab_kid_hmac_key.errmsg.empty"), - }); - } - if (!values.eabKid || !values.eabHmacKey) { - return; - } - } - - const setting: Setting = { - id: config?.id, - name: "ssl-provider", - content: { - provider: values.provider, - config: { - letsencrypt: {}, - zerossl: { - eabKid: values.eabKid ?? "", - eabHmacKey: values.eabHmacKey ?? "", - }, - }, - }, - }; - + const onSubmit = async (data: Setting) => { try { - await update(setting); + console.log(data); + const resp = await update({ ...data }); + setConfig(resp); toast({ title: t("common.update.succeeded.message"), description: t("common.update.succeeded.message"), @@ -117,89 +95,351 @@ const SSLProvider = () => { return ( <> -
-
- - ( - - {t("common.text.ca")} - - { - setProvider(val); - form.setValue("provider", val as SSLProviderType); - }} - value={provider} - > -
- - -
-
- - -
-
-
- - ( - - )} - /> - - ( - - )} - /> - - -
- )} - /> - -
- + +
+ + { + setProvider(val as SSLProviderType); + }} + value={config.content?.provider} + > +
+ +
- - -
+
+ + +
+ +
+ + +
+ + + +
+ ); }; +const SSLProviderForm = ({ kind }: { kind: string }) => { + const getForm = () => { + switch (kind) { + case "zerossl": + return ; + case "gts": + return ; + default: + return ; + } + }; + + return ( + <> +
{getForm()}
+ + ); +}; + +const getConfigStr = (content: SSLProviderSetting, kind: string, key: string) => { + if (!content.config) { + return ""; + } + if (!content.config[kind]) { + return ""; + } + return content.config[kind][key] ?? ""; +}; + +const SSLProviderLetsEncryptForm = () => { + const { t } = useTranslation(); + + const { setting, onSubmit } = useSSLProviderContext(); + + const formSchema = z.object({ + kind: z.literal("letsencrypt"), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + kind: "letsencrypt", + }, + }); + + const onLocalSubmit = async (data: z.infer) => { + const newData = produce(setting, (draft) => { + if (!draft.content) { + draft.content = { + provider: data.kind, + config: { + letsencrypt: {}, + }, + }; + } + }); + onSubmit(newData); + }; + + return ( +
+ + ( + + )} + /> + + + +
+ +
+ + + ); +}; +const SSLProviderZeroSSLForm = () => { + const { t } = useTranslation(); + + const { setting, onSubmit } = useSSLProviderContext(); + + const formSchema = z.object({ + kind: z.literal("zerossl"), + eabKid: z.string().min(1, { message: t("settings.ca.eab_kid_hmac_key.errmsg.empty") }), + eabHmacKey: z.string().min(1, { message: t("settings.ca.eab_kid_hmac_key.errmsg.empty") }), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + kind: "zerossl", + eabKid: "", + eabHmacKey: "", + }, + }); + + useEffect(() => { + if (setting.content) { + const content = setting.content; + + form.reset({ + eabKid: getConfigStr(content, "zerossl", "eabKid"), + eabHmacKey: getConfigStr(content, "zerossl", "eabHmacKey"), + }); + } + }, [setting]); + + const onLocalSubmit = async (data: z.infer) => { + const newData = produce(setting, (draft) => { + if (!draft.content) { + draft.content = { + provider: "zerossl", + config: { + zerossl: {}, + }, + }; + } + + draft.content.config.zerossl = { + eabKid: data.eabKid, + eabHmacKey: data.eabHmacKey, + }; + }); + onSubmit(newData); + }; + + return ( +
+ + ( + + )} + /> + ( + + EAB_KID + + + + + + + )} + /> + + ( + + EAB_HMAC_KEY + + + + + + + )} + /> + + + +
+ +
+ + + ); +}; + +const SSLProviderGtsForm = () => { + const { t } = useTranslation(); + + const { setting, onSubmit } = useSSLProviderContext(); + + const formSchema = z.object({ + kind: z.literal("gts"), + eabKid: z.string().min(1, { message: t("settings.ca.eab_kid_hmac_key.errmsg.empty") }), + eabHmacKey: z.string().min(1, { message: t("settings.ca.eab_kid_hmac_key.errmsg.empty") }), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + kind: "gts", + eabKid: "", + eabHmacKey: "", + }, + }); + + useEffect(() => { + if (setting.content) { + const content = setting.content; + + form.reset({ + eabKid: getConfigStr(content, "gts", "eabKid"), + eabHmacKey: getConfigStr(content, "gts", "eabHmacKey"), + }); + } + }, [setting]); + + const onLocalSubmit = async (data: z.infer) => { + const newData = produce(setting, (draft) => { + if (!draft.content) { + draft.content = { + provider: "gts", + config: { + zerossl: {}, + }, + }; + } + + draft.content.config.gts = { + eabKid: data.eabKid, + eabHmacKey: data.eabHmacKey, + }; + }); + onSubmit(newData); + }; + + return ( +
+ + ( + + )} + /> + ( + + EAB_KID + + + + + + + )} + /> + + ( + + EAB_HMAC_KEY + + + + + + + )} + /> + + + +
+ +
+ + + ); +}; + export default SSLProvider; diff --git a/ui/src/providers/config/index.tsx b/ui/src/providers/config/index.tsx index 0b98c129..d484859c 100644 --- a/ui/src/providers/config/index.tsx +++ b/ui/src/providers/config/index.tsx @@ -2,7 +2,7 @@ import { createContext, ReactNode, useCallback, useContext, useEffect, useReduce import { Access } from "@/domain/access"; import { AccessGroup } from "@/domain/access_groups"; -import { Setting } from "@/domain/settings"; +import { EmailsSetting, Setting } from "@/domain/settings"; import { list } from "@/repository/access"; import { list as getAccessGroups } from "@/repository/access_group"; import { getEmails } from "@/repository/settings"; @@ -10,13 +10,13 @@ import { configReducer } from "./reducer"; export type ConfigData = { accesses: Access[]; - emails: Setting; + emails: Setting; accessGroups: AccessGroup[]; }; export type ConfigContext = { config: ConfigData; - setEmails: (email: Setting) => void; + setEmails: (email: Setting) => void; addAccess: (access: Access) => void; updateAccess: (access: Access) => void; deleteAccess: (id: string) => void; @@ -68,7 +68,7 @@ export const ConfigProvider = ({ children }: ConfigProviderProps) => { dispatchConfig({ type: "SET_ACCESS_GROUPS", payload: accessGroups }); }, []); - const setEmails = useCallback((emails: Setting) => { + const setEmails = useCallback((emails: Setting) => { dispatchConfig({ type: "SET_EMAILS", payload: emails }); }, []); diff --git a/ui/src/providers/config/reducer.ts b/ui/src/providers/config/reducer.ts index 0367f009..fd45386c 100644 --- a/ui/src/providers/config/reducer.ts +++ b/ui/src/providers/config/reducer.ts @@ -8,7 +8,7 @@ type Action = | { type: "DELETE_ACCESS"; payload: string } | { type: "UPDATE_ACCESS"; payload: Access } | { type: "SET_ACCESSES"; payload: Access[] } - | { type: "SET_EMAILS"; payload: Setting } + | { type: "SET_EMAILS"; payload: Setting } | { type: "ADD_EMAIL"; payload: string } | { type: "SET_ACCESS_GROUPS"; payload: AccessGroup[] }; diff --git a/ui/src/providers/notify/index.tsx b/ui/src/providers/notify/index.tsx index a0e9c195..47f55676 100644 --- a/ui/src/providers/notify/index.tsx +++ b/ui/src/providers/notify/index.tsx @@ -1,13 +1,13 @@ import { ReactNode, useContext, createContext, useEffect, useReducer, useCallback } from "react"; -import { NotifyChannel, Setting } from "@/domain/settings"; +import { NotifyChannel, NotifyChannels, Setting } from "@/domain/settings"; import { getSetting } from "@/repository/settings"; import { notifyReducer } from "./reducer"; export type NotifyContext = { - config: Setting; + config: Setting; setChannel: (data: { channel: string; data: NotifyChannel }) => void; - setChannels: (data: Setting) => void; + setChannels: (data: Setting) => void; }; const Context = createContext({} as NotifyContext); @@ -23,7 +23,7 @@ export const NotifyProvider = ({ children }: NotifyProviderProps) => { useEffect(() => { const featchData = async () => { - const chanels = await getSetting("notifyChannels"); + const chanels = await getSetting("notifyChannels"); dispatchNotify({ type: "SET_CHANNELS", payload: chanels, @@ -39,7 +39,7 @@ export const NotifyProvider = ({ children }: NotifyProviderProps) => { }); }, []); - const setChannels = useCallback((setting: Setting) => { + const setChannels = useCallback((setting: Setting) => { dispatchNotify({ type: "SET_CHANNELS", payload: setting, diff --git a/ui/src/providers/notify/reducer.tsx b/ui/src/providers/notify/reducer.tsx index d5a36efc..8bdaecc9 100644 --- a/ui/src/providers/notify/reducer.tsx +++ b/ui/src/providers/notify/reducer.tsx @@ -1,4 +1,4 @@ -import { NotifyChannel, Setting } from "@/domain/settings"; +import { NotifyChannel, NotifyChannels, Setting } from "@/domain/settings"; type Action = | { @@ -10,10 +10,10 @@ type Action = } | { type: "SET_CHANNELS"; - payload: Setting; + payload: Setting; }; -export const notifyReducer = (state: Setting, action: Action) => { +export const notifyReducer = (state: Setting, action: Action) => { switch (action.type) { case "SET_CHANNEL": { const channel = action.payload.channel; diff --git a/ui/src/repository/settings.ts b/ui/src/repository/settings.ts index 0734cafd..5894c50c 100644 --- a/ui/src/repository/settings.ts +++ b/ui/src/repository/settings.ts @@ -1,9 +1,9 @@ -import { Setting } from "@/domain/settings"; +import { EmailsSetting, Setting } from "@/domain/settings"; import { getPb } from "./api"; export const getEmails = async () => { try { - const resp = await getPb().collection("settings").getFirstListItem("name='emails'"); + const resp = await getPb().collection("settings").getFirstListItem>("name='emails'"); return resp; } catch (e) { return { @@ -12,21 +12,21 @@ export const getEmails = async () => { } }; -export const getSetting = async (name: string) => { +export const getSetting = async (name: string) => { try { - const resp = await getPb().collection("settings").getFirstListItem(`name='${name}'`); + const resp = await getPb().collection("settings").getFirstListItem>(`name='${name}'`); return resp; } catch (e) { - const rs: Setting = { + const rs: Setting = { name: name, }; return rs; } }; -export const update = async (setting: Setting) => { +export const update = async (setting: Setting) => { const pb = getPb(); - let resp: Setting; + let resp: Setting; if (setting.id) { resp = await pb.collection("settings").update(setting.id, setting); } else {