diff --git a/ui/public/imgs/providers/google.svg b/ui/public/imgs/acme/google.svg similarity index 99% rename from ui/public/imgs/providers/google.svg rename to ui/public/imgs/acme/google.svg index 120a7921..78f81c93 100644 --- a/ui/public/imgs/providers/google.svg +++ b/ui/public/imgs/acme/google.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/letsencrypt.svg b/ui/public/imgs/acme/letsencrypt.svg similarity index 100% rename from ui/public/imgs/providers/letsencrypt.svg rename to ui/public/imgs/acme/letsencrypt.svg diff --git a/ui/public/imgs/acme/zerossl.svg b/ui/public/imgs/acme/zerossl.svg new file mode 100644 index 00000000..e4c2ac63 --- /dev/null +++ b/ui/public/imgs/acme/zerossl.svg @@ -0,0 +1 @@ + diff --git a/ui/public/imgs/providers/zerossl.svg b/ui/public/imgs/providers/zerossl.svg deleted file mode 100644 index 8563aece..00000000 --- a/ui/public/imgs/providers/zerossl.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/ui/src/components/notification/NotifyChannels.tsx b/ui/src/components/notification/NotifyChannels.tsx index 25f4c0f3..76d09817 100644 --- a/ui/src/components/notification/NotifyChannels.tsx +++ b/ui/src/components/notification/NotifyChannels.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDeepCompareMemo } from "@ant-design/pro-components"; -import { Button, Collapse, message, notification, Skeleton, Space, Switch, Tooltip, type CollapseProps } from "antd"; +import { Button, Collapse, message, notification, Skeleton, Space, Switch, type CollapseProps } from "antd"; import NotifyChannelEditForm, { type NotifyChannelEditFormInstance } from "./NotifyChannelEditForm"; import NotifyTestButton from "./NotifyTestButton"; @@ -45,9 +45,9 @@ const NotifyChannel = ({ className, style, channel }: NotifyChannelProps) => { {MessageContextHolder} {NotificationContextHolder} - setChannelFormChanged(true)} /> + setChannelFormChanged(true)} /> - + {t("common.button.save")} diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index 9419863b..8b5dc6b4 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -41,6 +41,8 @@ export const ACCESS_PROVIDER_TYPES = Object.freeze({ WEBHOOK: ACCESS_PROVIDER_TYPE_WEBHOOK, } as const); +export type AccessProviderTypes = (typeof ACCESS_PROVIDER_TYPES)[keyof typeof ACCESS_PROVIDER_TYPES]; + export const ACCESS_PROVIDER_USAGE_ALL = "all" as const; export const ACCESS_PROVIDER_USAGE_APPLY = "apply" as const; export const ACCESS_PROVIDER_USAGE_DEPLOY = "deploy" as const; @@ -50,11 +52,13 @@ export const ACCESS_PROVIDER_USAGES = Object.freeze({ DEPLOY: ACCESS_PROVIDER_USAGE_DEPLOY, } as const); +export type AccessProviderUsages = (typeof ACCESS_PROVIDER_USAGES)[keyof typeof ACCESS_PROVIDER_USAGES]; + // #region AccessModel export interface AccessModel extends BaseModel { name: string; configType: string; - usage: AccessUsages; + usage: AccessProviderUsages; config: /* 注意:如果追加新的类型,请保持以 ASCII 排序。 NOTICE: If you add new type, please keep ASCII order. @@ -136,7 +140,7 @@ export type KubernetesAccessConfig = { kubeConfig?: string; }; -export type LocalAccessConfig = never; +export type LocalAccessConfig = NonNullable; export type NameSiloAccessConfig = { apiKey: string; @@ -177,13 +181,11 @@ export type WebhookAccessConfig = { // #endregion // #region AccessProvider -export type AccessUsages = (typeof ACCESS_PROVIDER_USAGES)[keyof typeof ACCESS_PROVIDER_USAGES]; - export type AccessProvider = { type: string; name: string; icon: string; - usage: AccessUsages; + usage: AccessProviderUsages; }; export const accessProvidersMap: Map = new Map( @@ -210,6 +212,6 @@ export const accessProvidersMap: Map = n [ACCESS_PROVIDER_TYPE_WEBHOOK, "common.provider.webhook", "/imgs/providers/webhook.svg", "deploy"], [ACCESS_PROVIDER_TYPE_KUBERNETES, "common.provider.kubernetes", "/imgs/providers/kubernetes.svg", "deploy"], [ACCESS_PROVIDER_TYPE_ACMEHTTPREQ, "common.provider.acmehttpreq", "/imgs/providers/acmehttpreq.svg", "apply"], - ].map(([type, name, icon, usage]) => [type, { type, name, icon, usage: usage as AccessUsages }]) + ].map(([type, name, icon, usage]) => [type, { type, name, icon, usage: usage as AccessProviderUsages }]) ); // #endregion diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts index e6ee60bc..c280189f 100644 --- a/ui/src/domain/settings.ts +++ b/ui/src/domain/settings.ts @@ -9,7 +9,9 @@ export const SETTINGS_NAMES = Object.freeze({ SSL_PROVIDER: SETTINGS_NAME_SSLPROVIDER, } as const); -export interface SettingsModel extends BaseModel { +export type SettingsNames = (typeof SETTINGS_NAMES)[keyof typeof SETTINGS_NAMES]; + +export interface SettingsModel = NonNullable> extends BaseModel { name: string; content: T; } @@ -115,14 +117,36 @@ export const notifyChannelsMap: Map = new // #endregion // #region Settings: SSLProvider -export type SSLProvider = "letsencrypt" | "zerossl" | "gts"; +export const SSLPROVIDER_LETSENCRYPT = "letsencrypt" as const; +export const SSLPROVIDER_ZEROSSL = "zerossl" as const; +export const SSLPROVIDER_GOOGLETRUSTSERVICES = "gts" as const; +export const SSLPROVIDERS = Object.freeze({ + LETS_ENCRYPT: SSLPROVIDER_LETSENCRYPT, + ZERO_SSL: SSLPROVIDER_ZEROSSL, + GOOGLE_TRUST_SERVICES: SSLPROVIDER_GOOGLETRUSTSERVICES, +} as const); -export type SSLProviderSetting = { - provider: SSLProvider; +export type SSLProviders = (typeof SSLPROVIDERS)[keyof typeof SSLPROVIDERS]; + +export type SSLProviderSettingsContent = { + provider: (typeof SSLPROVIDERS)[keyof typeof SSLPROVIDERS]; config: { - [key: string]: { - [key: string]: string; - }; + [key: string]: Record | undefined; + letsencrypt?: SSLProviderLetsEncryptConfig; + zerossl?: SSLProviderZeroSSLConfig; + gts?: SSLProviderGoogleTrustServicesConfig; }; }; + +export type SSLProviderLetsEncryptConfig = NonNullable; + +export type SSLProviderZeroSSLConfig = { + eabKid: string; + eabHmacKey: string; +}; + +export type SSLProviderGoogleTrustServicesConfig = { + eabKid: string; + eabHmacKey: string; +}; // #endregion diff --git a/ui/src/i18n/locales/en/nls.settings.json b/ui/src/i18n/locales/en/nls.settings.json index 60e79051..742a2c31 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -36,10 +36,10 @@ "settings.notification.channel.form.bark_device_key.tooltip": "For more information, see https://bark.day.app/", "settings.notification.channel.form.dingtalk_access_token.label": "Robot AccessToken", "settings.notification.channel.form.dingtalk_access_token.placeholder": "Please enter Robot Access Token", - "settings.notification.channel.form.dingtalk_access_token.tooltip": "For more information, see https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages", + "settings.notification.channel.form.dingtalk_access_token.tooltip": "For more information, see https://open.dingtalk.com/document/orgapp/obtain-the-webhook-address-of-a-custom-robot", "settings.notification.channel.form.dingtalk_secret.label": "Robot Secret", "settings.notification.channel.form.dingtalk_secret.placeholder": "Please enter Robot Secret", - "settings.notification.channel.form.dingtalk_secret.tooltip": "For more information, see https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages", + "settings.notification.channel.form.dingtalk_secret.tooltip": "For more information, see https://open.dingtalk.com/document/orgapp/customize-robot-security-settings", "settings.notification.channel.form.email_smtp_host.label": "SMTP Host", "settings.notification.channel.form.email_smtp_host.placeholder": "Please enter SMTP host", "settings.notification.channel.form.email_smtp_port.label": "SMTP Port", @@ -68,9 +68,19 @@ "settings.notification.channel.form.webhook_url.label": "Webhook URL", "settings.notification.channel.form.webhook_url.placeholder": "Please enter Webhook URL", - "settings.ca.tab": "Certificate Authority", - "settings.ca.provider.errmsg.empty": "Please select a Certificate Authority", - "settings.ca.eab_kid.errmsg.empty": "Please enter EAB_KID", - "settings.ca.eab_hmac_key.errmsg.empty": "Please enter EAB_HMAC_KEY.", - "settings.ca.eab_kid_hmac_key.errmsg.empty": "Please enter EAB_KID and EAB_HMAC_KEY" + "settings.sslprovider.tab": "Certificate Authority", + "settings.sslprovider.form.provider.label": "ACME Provider", + "settings.sslprovider.provider.errmsg.empty": "Please select a Certificate Authority", + "settings.sslprovider.form.zerossl_eab_kid.label": "EAB KID", + "settings.sslprovider.form.zerossl_eab_kid.placeholder": "Please enter EAB KID", + "settings.sslprovider.form.zerossl_eab_kid.tooltip": "For more information, see https://zerossl.com/documentation/acme/", + "settings.sslprovider.form.zerossl_eab_hmac_key.label": "EAB HMAC Key", + "settings.sslprovider.form.zerossl_eab_hmac_key.placeholder": "Please enter EAB HMAC Key", + "settings.sslprovider.form.zerossl_eab_hmac_key.tooltip": "For more information, see https://zerossl.com/documentation/acme/", + "settings.sslprovider.form.gts_eab_kid.label": "EAB KID", + "settings.sslprovider.form.gts_eab_kid.placeholder": "Please enter EAB KID", + "settings.sslprovider.form.gts_eab_kid.tooltip": "For more information, see https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", + "settings.sslprovider.form.gts_eab_hmac_key.label": "EAB HMAC Key", + "settings.sslprovider.form.gts_eab_hmac_key.placeholder": "Please enter EAB HMAC Key", + "settings.sslprovider.form.gts_eab_hmac_key.tooltip": "For more information, see https://cloud.google.com/certificate-manager/docs/public-ca-tutorial" } diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index edee22d5..e8636ee6 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -88,7 +88,7 @@ "common.notifier.dingtalk": "钉钉", "common.notifier.email": "电子邮件", "common.notifier.lark": "飞书", - "common.notifier.serverchan": "Server酱", + "common.notifier.serverchan": "Server 酱", "common.notifier.telegram": "Telegram", "common.notifier.webhook": "Webhook" } diff --git a/ui/src/i18n/locales/zh/nls.settings.json b/ui/src/i18n/locales/zh/nls.settings.json index 276a90fa..9b33e0d2 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -36,10 +36,10 @@ "settings.notification.channel.form.bark_device_key.tooltip": "这是什么?请参阅 https://bark.day.app/", "settings.notification.channel.form.dingtalk_access_token.label": "机器人 AccessToken", "settings.notification.channel.form.dingtalk_access_token.placeholder": "请输入机器人 AccessToken", - "settings.notification.channel.form.dingtalk_access_token.tooltip": "这是什么?请参阅 https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages", + "settings.notification.channel.form.dingtalk_access_token.tooltip": "这是什么?请参阅 https://open.dingtalk.com/document/orgapp/obtain-the-webhook-address-of-a-custom-robot", "settings.notification.channel.form.dingtalk_secret.label": "机器人加签密钥", "settings.notification.channel.form.dingtalk_secret.placeholder": "请输入机器人加签密钥", - "settings.notification.channel.form.dingtalk_secret.tooltip": "这是什么?请参阅 https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages", + "settings.notification.channel.form.dingtalk_secret.tooltip": "这是什么?请参阅 https://open.dingtalk.com/document/orgapp/customize-robot-security-settings", "settings.notification.channel.form.email_smtp_host.label": "SMTP 服务器地址", "settings.notification.channel.form.email_smtp_host.placeholder": "请输入 SMTP 服务器地址", "settings.notification.channel.form.email_smtp_port.label": "SMTP 服务器端口", @@ -68,9 +68,19 @@ "settings.notification.channel.form.webhook_url.label": "Webhook 回调地址", "settings.notification.channel.form.webhook_url.placeholder": "请输入 Webhook 回调地址", - "settings.ca.tab": "证书颁发机构(CA)", - "settings.ca.provider.errmsg.empty": "请选择证书分发机构", - "settings.ca.eab_kid.errmsg.empty": "请输入EAB_KID", - "settings.ca.eab_hmac_key.errmsg.empty": "请输入EAB_HMAC_KEY", - "settings.ca.eab_kid_hmac_key.errmsg.empty": "请输入EAB_KID和EAB_HMAC_KEY" + "settings.sslprovider.tab": "证书颁发机构(CA)", + "settings.sslprovider.form.provider.label": "ACME 提供商", + "settings.sslprovider.provider.errmsg.empty": "请选择证书分发机构", + "settings.sslprovider.form.zerossl_eab_kid.label": "EAB KID", + "settings.sslprovider.form.zerossl_eab_kid.placeholder": "请输入 EAB KID", + "settings.sslprovider.form.zerossl_eab_kid.tooltip": "这是什么?请参阅 https://zerossl.com/documentation/acme/", + "settings.sslprovider.form.zerossl_eab_hmac_key.label": "EAB HMAC Key", + "settings.sslprovider.form.zerossl_eab_hmac_key.placeholder": "请输入 EAB HMAC Key", + "settings.sslprovider.form.zerossl_eab_hmac_key.tooltip": "这是什么?请参阅 https://zerossl.com/documentation/acme/", + "settings.sslprovider.form.gts_eab_kid.label": "EAB KID", + "settings.sslprovider.form.gts_eab_kid.placeholder": "请输入 EAB KID", + "settings.sslprovider.form.gts_eab_kid.tooltip": "这是什么?请参阅 https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", + "settings.sslprovider.form.gts_eab_hmac_key.label": "EAB HMAC Key", + "settings.sslprovider.form.gts_eab_hmac_key.placeholder": "请输入 EAB HMAC Key", + "settings.sslprovider.form.gts_eab_hmac_key.tooltip": "这是什么?请参阅 https://cloud.google.com/certificate-manager/docs/public-ca-tutorial" } diff --git a/ui/src/pages/settings/SSLProvider.tsx b/ui/src/pages/settings/SSLProvider.tsx deleted file mode 100644 index baafadaa..00000000 --- a/ui/src/pages/settings/SSLProvider.tsx +++ /dev/null @@ -1,445 +0,0 @@ -import { useContext, useEffect, useState, createContext } from "react"; -import { useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { produce } from "immer"; - -import { cn } from "@/components/ui/utils"; -import { Button } from "@/components/ui/button"; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { useToast } from "@/components/ui/use-toast"; -import { SETTINGS_NAMES, SSLProvider as SSLProviderType, SSLProviderSetting, SettingsModel } from "@/domain/settings"; -import { get, save } from "@/repository/settings"; -import { getErrMsg } from "@/utils/error"; - -type SSLProviderContext = { - setting: SettingsModel; - onSubmit: (data: SettingsModel) => void; - setConfig: (config: SettingsModel) => void; -}; - -const Context = createContext({} as SSLProviderContext); - -export const useSSLProviderContext = () => { - return useContext(Context); -}; - -const getConfigStr = (content: SSLProviderSetting, kind: string, key: string) => { - if (!content.config) { - return ""; - } - if (!content.config[kind]) { - return ""; - } - return content.config[kind][key] ?? ""; -}; - -const SSLProvider = () => { - const { t } = useTranslation(); - - const [config, setConfig] = useState>({ - content: { - provider: "letsencrypt", - config: {}, - }, - } as SettingsModel); - - const { toast } = useToast(); - - useEffect(() => { - const fetchData = async () => { - const setting = await get(SETTINGS_NAMES.SSL_PROVIDER); - - if (setting) { - setConfig(setting); - } - }; - 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 (config.content?.provider === val) { - return "border-primary dark:border-primary"; - } - - return ""; - }; - - const onSubmit = async (data: SettingsModel) => { - try { - console.log(data); - const resp = await save({ ...data }); - setConfig(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("common.text.operation_succeeded"), - }); - } catch (e) { - const message = getErrMsg(e); - toast({ - title: t("common.text.operation_failed"), - description: message, - variant: "destructive", - }); - } - }; - - return ( - <> - - - {t("common.text.ca")} - { - setProvider(val as SSLProviderType); - }} - value={config.content?.provider} - > - - - - - - {"Let's Encrypt"} - - - - - - - - - {"ZeroSSL"} - - - - - - - - - - {"Google Trust Services"} - - - - - - - - - > - ); -}; - -const SSLProviderForm = ({ kind }: { kind: string }) => { - const getForm = () => { - switch (kind) { - case "zerossl": - return ; - case "gts": - return ; - default: - return ; - } - }; - - return ( - <> - {getForm()} - > - ); -}; - -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 ( - - - ( - - kind - - - - - - - )} - /> - - - - - {t("common.button.save")} - - - - ); -}; - -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 ( - - - ( - - kind - - - - - - - )} - /> - ( - - EAB_KID - - - - - - - )} - /> - - ( - - EAB_HMAC_KEY - - - - - - - )} - /> - - - - - {t("common.button.save")} - - - - ); -}; - -const SSLProviderGoogleTrustServicesForm = () => { - 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 ( - - - ( - - kind - - - - - - - )} - /> - ( - - EAB_KID - - - - - - - )} - /> - - ( - - EAB_HMAC_KEY - - - - - - - )} - /> - - - - - {t("common.button.save")} - - - - ); -}; - -export default SSLProvider; diff --git a/ui/src/pages/settings/Settings.tsx b/ui/src/pages/settings/Settings.tsx index 9b70a454..530f8033 100644 --- a/ui/src/pages/settings/Settings.tsx +++ b/ui/src/pages/settings/Settings.tsx @@ -60,7 +60,7 @@ const Settings = () => { label: ( - {t("settings.ca.tab")} + {t("settings.sslprovider.tab")} ), }, diff --git a/ui/src/pages/settings/SettingsSSLProvider.tsx b/ui/src/pages/settings/SettingsSSLProvider.tsx new file mode 100644 index 00000000..5d63e0b4 --- /dev/null +++ b/ui/src/pages/settings/SettingsSSLProvider.tsx @@ -0,0 +1,301 @@ +import { createContext, useContext, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Button, Form, Input, message, notification, Skeleton } from "antd"; +import { CheckCard } from "@ant-design/pro-components"; +import { createSchemaFieldRule } from "antd-zod"; +import { produce } from "immer"; +import { z } from "zod"; + +import { SETTINGS_NAMES, SSLPROVIDERS, type SettingsModel, type SSLProviderSettingsContent, type SSLProviders } from "@/domain/settings"; +import { get as getSettings, save as saveSettings } from "@/repository/settings"; +import { getErrMsg } from "@/utils/error"; +import { useDeepCompareEffect } from "ahooks"; + +const SSLProviderContext = createContext( + {} as { + pending: boolean; + settings: SettingsModel; + updateSettings: (settings: MaybeModelRecordWithId>) => Promise; + } +); + +const SSLProviderEditFormLetsEncryptConfig = () => { + const { t } = useTranslation(); + + const { pending, settings, updateSettings } = useContext(SSLProviderContext); + + const [form] = Form.useForm(); + + const [initialValues, setInitialValues] = useState(settings?.content?.config?.[SSLPROVIDERS.LETS_ENCRYPT]); + const [initialChanged, setInitialChanged] = useState(false); + useDeepCompareEffect(() => { + setInitialValues(settings?.content?.config?.[SSLPROVIDERS.LETS_ENCRYPT]); + setInitialChanged(settings?.content?.provider !== SSLPROVIDERS.LETS_ENCRYPT); + }, [settings]); + + const handleFormChange = () => { + setInitialChanged(true); + }; + + const handleFormFinish = async (fields: NonNullable) => { + const newSettings = produce(settings, (draft) => { + draft.content ??= {} as SSLProviderSettingsContent; + draft.content.provider = SSLPROVIDERS.LETS_ENCRYPT; + + draft.content.config ??= {} as SSLProviderSettingsContent["config"]; + draft.content.config[SSLPROVIDERS.LETS_ENCRYPT] = fields; + }); + await updateSettings(newSettings); + + setInitialChanged(false); + }; + + return ( + + + + {t("common.button.save")} + + + + ); +}; + +const SSLProviderEditFormZeroSSLConfig = () => { + const { t } = useTranslation(); + + const { pending, settings, updateSettings } = useContext(SSLProviderContext); + + const formSchema = z.object({ + eabKid: z + .string({ message: t("settings.sslprovider.form.zerossl_eab_kid.placeholder") }) + .min(1, t("settings.sslprovider.form.zerossl_eab_kid.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + eabHmacKey: z + .string({ message: t("settings.sslprovider.form.zerossl_eab_hmac_key.placeholder") }) + .min(1, t("settings.sslprovider.form.zerossl_eab_hmac_key.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + }); + const formRule = createSchemaFieldRule(formSchema); + const [form] = Form.useForm>(); + + const [initialValues, setInitialValues] = useState(settings?.content?.config?.[SSLPROVIDERS.ZERO_SSL]); + const [initialChanged, setInitialChanged] = useState(false); + useDeepCompareEffect(() => { + setInitialValues(settings?.content?.config?.[SSLPROVIDERS.ZERO_SSL]); + setInitialChanged(settings?.content?.provider !== SSLPROVIDERS.ZERO_SSL); + }, [settings]); + + const handleFormChange = () => { + setInitialChanged(true); + }; + + const handleFormFinish = async (fields: z.infer) => { + const newSettings = produce(settings, (draft) => { + draft.content ??= {} as SSLProviderSettingsContent; + draft.content.provider = SSLPROVIDERS.ZERO_SSL; + + draft.content.config ??= {} as SSLProviderSettingsContent["config"]; + draft.content.config[SSLPROVIDERS.ZERO_SSL] = fields; + }); + await updateSettings(newSettings); + + setInitialChanged(false); + }; + + return ( + + } + > + + + + } + > + + + + + + {t("common.button.save")} + + + + ); +}; + +const SSLProviderEditFormGoogleTrustServicesConfig = () => { + const { t } = useTranslation(); + + const { pending, settings, updateSettings } = useContext(SSLProviderContext); + + const formSchema = z.object({ + eabKid: z + .string({ message: t("settings.sslprovider.form.gts_eab_kid.placeholder") }) + .min(1, t("settings.sslprovider.form.gts_eab_kid.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + eabHmacKey: z + .string({ message: t("settings.sslprovider.form.gts_eab_hmac_key.placeholder") }) + .min(1, t("settings.sslprovider.form.gts_eab_hmac_key.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + }); + const formRule = createSchemaFieldRule(formSchema); + const [form] = Form.useForm>(); + + const [initialValues, setInitialValues] = useState(settings?.content?.config?.[SSLPROVIDERS.GOOGLE_TRUST_SERVICES]); + const [initialChanged, setInitialChanged] = useState(false); + useDeepCompareEffect(() => { + setInitialValues(settings?.content?.config?.[SSLPROVIDERS.GOOGLE_TRUST_SERVICES]); + setInitialChanged(settings?.content?.provider !== SSLPROVIDERS.GOOGLE_TRUST_SERVICES); + }, [settings]); + + const handleFormChange = () => { + setInitialChanged(true); + }; + + const handleFormFinish = async (fields: z.infer) => { + const newSettings = produce(settings, (draft) => { + draft.content ??= {} as SSLProviderSettingsContent; + draft.content.provider = SSLPROVIDERS.GOOGLE_TRUST_SERVICES; + + draft.content.config ??= {} as SSLProviderSettingsContent["config"]; + draft.content.config[SSLPROVIDERS.GOOGLE_TRUST_SERVICES] = fields; + }); + await updateSettings(newSettings); + + setInitialChanged(false); + }; + + return ( + + } + > + + + + } + > + + + + + + {t("common.button.save")} + + + + ); +}; + +const SettingsSSLProvider = () => { + const { t } = useTranslation(); + + const [messageApi, MessageContextHolder] = message.useMessage(); + const [notificationApi, NotificationContextHolder] = notification.useNotification(); + + const [form] = Form.useForm(); + const [formPending, setFormPending] = useState(false); + + const [settings, setSettings] = useState>(); + const [loading, setLoading] = useState(true); + useEffect(() => { + const fetchData = async () => { + setLoading(true); + + const settings = await getSettings(SETTINGS_NAMES.SSL_PROVIDER); + setSettings(settings); + setFormProviderType(settings.content?.provider); + + setLoading(false); + }; + + fetchData(); + }, []); + + const [providerType, setFormProviderType] = useState(); + const providerFormComponent = useMemo(() => { + switch (providerType) { + case SSLPROVIDERS.LETS_ENCRYPT: + return ; + case SSLPROVIDERS.ZERO_SSL: + return ; + case SSLPROVIDERS.GOOGLE_TRUST_SERVICES: + return ; + } + }, [providerType]); + + const updateContextSettings = async (settings: MaybeModelRecordWithId>) => { + setFormPending(true); + + try { + const resp = await saveSettings(settings); + setSettings(resp); + setFormProviderType(resp.content?.provider); + + messageApi.success(t("common.text.operation_succeeded")); + } catch (err) { + notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + } finally { + setFormPending(false); + } + }; + + return ( + + {MessageContextHolder} + {NotificationContextHolder} + + {loading ? ( + + ) : ( + <> + + + setFormProviderType(value as SSLProviders)}> + } + size="small" + title="Let's Encrypt" + value={SSLPROVIDERS.LETS_ENCRYPT} + /> + } size="small" title="ZeroSSL" value={SSLPROVIDERS.ZERO_SSL} /> + } + size="small" + title="Google Trust Services" + value={SSLPROVIDERS.GOOGLE_TRUST_SERVICES} + /> + + + + + {providerFormComponent} + > + )} + + ); +}; + +export default SettingsSSLProvider; diff --git a/ui/src/repository/settings.ts b/ui/src/repository/settings.ts index 7bd2aec0..fe7c8a54 100644 --- a/ui/src/repository/settings.ts +++ b/ui/src/repository/settings.ts @@ -1,9 +1,9 @@ import { ClientResponseError } from "pocketbase"; -import { SETTINGS_NAMES, type SettingsModel } from "@/domain/settings"; +import { type SettingsModel, type SettingsNames } from "@/domain/settings"; import { getPocketBase } from "./pocketbase"; -export const get = async (name: (typeof SETTINGS_NAMES)[keyof typeof SETTINGS_NAMES]) => { +export const get = async >(name: SettingsNames) => { try { const resp = await getPocketBase().collection("settings").getFirstListItem>(`name='${name}'`, { requestKey: null, @@ -21,7 +21,7 @@ export const get = async (name: (typeof SETTINGS_NAMES)[keyof typeof SETTINGS } }; -export const save = async (record: MaybeModelRecordWithId>) => { +export const save = async >(record: MaybeModelRecordWithId>) => { if (record.id) { return await getPocketBase().collection("settings").update>(record.id, record); } diff --git a/ui/src/router.tsx b/ui/src/router.tsx index ef74a31d..357aaf81 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -12,7 +12,7 @@ import Settings from "./pages/settings/Settings"; import SettingsAccount from "./pages/settings/SettingsAccount"; import SettingsPassword from "./pages/settings/SettingsPassword"; import SettingsNotification from "./pages/settings/SettingsNotification"; -import SSLProvider from "./pages/settings/SSLProvider"; +import SettingsSSLProvider from "./pages/settings/SettingsSSLProvider"; export const router = createHashRouter([ { @@ -57,7 +57,7 @@ export const router = createHashRouter([ }, { path: "/settings/ssl-provider", - element: , + element: , }, ], }, diff --git a/ui/src/stores/notify/index.ts b/ui/src/stores/notify/index.ts index 9a53b63c..b61c1a93 100644 --- a/ui/src/stores/notify/index.ts +++ b/ui/src/stores/notify/index.ts @@ -22,10 +22,12 @@ export const useNotifyChannelStore = create((set, get) => { setChannel: async (channel, config) => { settings ??= await getSettings(SETTINGS_NAMES.NOTIFY_CHANNELS); - return get().setChannels({ - ...settings.content, - [channel]: { ...settings.content[channel], ...config }, - }); + return get().setChannels( + produce(settings, (draft) => { + draft.content ??= {}; + draft.content[channel] = { ...draft.content[channel], ...config }; + }) + ); }, setChannels: async (channels) => {