gts support

This commit is contained in:
yoan 2024-10-23 13:22:17 +08:00
parent bff18a7be7
commit 8bec234fe8
9 changed files with 447 additions and 167 deletions

View File

@ -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})

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="-0.5 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Google-color</title>
<desc>Created with Sketch.</desc>
<defs>
</defs>
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Color-" transform="translate(-401.000000, -860.000000)">
<g id="Google" transform="translate(401.000000, 860.000000)">
<path d="M9.82727273,24 C9.82727273,22.4757333 10.0804318,21.0144 10.5322727,19.6437333 L2.62345455,13.6042667 C1.08206818,16.7338667 0.213636364,20.2602667 0.213636364,24 C0.213636364,27.7365333 1.081,31.2608 2.62025,34.3882667 L10.5247955,28.3370667 C10.0772273,26.9728 9.82727273,25.5168 9.82727273,24" id="Fill-1" fill="#FBBC05">
</path>
<path d="M23.7136364,10.1333333 C27.025,10.1333333 30.0159091,11.3066667 32.3659091,13.2266667 L39.2022727,6.4 C35.0363636,2.77333333 29.6954545,0.533333333 23.7136364,0.533333333 C14.4268636,0.533333333 6.44540909,5.84426667 2.62345455,13.6042667 L10.5322727,19.6437333 C12.3545909,14.112 17.5491591,10.1333333 23.7136364,10.1333333" id="Fill-2" fill="#EB4335">
</path>
<path d="M23.7136364,37.8666667 C17.5491591,37.8666667 12.3545909,33.888 10.5322727,28.3562667 L2.62345455,34.3946667 C6.44540909,42.1557333 14.4268636,47.4666667 23.7136364,47.4666667 C29.4455,47.4666667 34.9177955,45.4314667 39.0249545,41.6181333 L31.5177727,35.8144 C29.3995682,37.1488 26.7323182,37.8666667 23.7136364,37.8666667" id="Fill-3" fill="#34A853">
</path>
<path d="M46.1454545,24 C46.1454545,22.6133333 45.9318182,21.12 45.6113636,19.7333333 L23.7136364,19.7333333 L23.7136364,28.8 L36.3181818,28.8 C35.6879545,31.8912 33.9724545,34.2677333 31.5177727,35.8144 L39.0249545,41.6181333 C43.3393409,37.6138667 46.1454545,31.6490667 46.1454545,24" id="Fill-4" fill="#4285F4">
</path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,7 +1,7 @@
export type Setting = {
export type Setting<T> = {
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;

View File

@ -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<SSLProviderSetting>;
onSubmit: (data: Setting<SSLProviderSetting>) => void;
setConfig: (config: Setting<SSLProviderSetting>) => 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<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
const [config, setConfig] = useState<Setting<SSLProviderSetting>>({
id: "",
content: {
provider: "letsencrypt",
config: {},
},
});
const [provider, setProvider] = useState("letsencrypt");
const [config, setConfig] = useState<Setting>();
const { toast } = useToast();
useEffect(() => {
const fetchData = async () => {
const setting = await getSetting("ssl-provider");
const setting = await getSetting<SSLProviderSetting>("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<typeof formSchema>) => {
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<SSLProviderSetting>) => {
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 (
<>
<div className="w-full md:max-w-[35em]">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 dark:text-stone-200">
<FormField
control={form.control}
name="provider"
render={({ field }) => (
<FormItem>
<FormLabel>{t("common.text.ca")}</FormLabel>
<FormControl>
<RadioGroup
{...field}
className="flex"
onValueChange={(val) => {
setProvider(val);
form.setValue("provider", val as SSLProviderType);
}}
value={provider}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="letsencrypt" id="letsencrypt" />
<Label htmlFor="letsencrypt">
<div className={cn("flex items-center space-x-2 border p-2 rounded cursor-pointer", getOptionCls("letsencrypt"))}>
<img src={"/imgs/providers/letsencrypt.svg"} className="h-6" />
<div>{"Let's Encrypt"}</div>
</div>
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="zerossl" id="zerossl" />
<Label htmlFor="zerossl">
<div className={cn("flex items-center space-x-2 border p-2 rounded cursor-pointer", getOptionCls("zerossl"))}>
<img src={"/imgs/providers/zerossl.svg"} className="h-6" />
<div>{"ZeroSSL"}</div>
</div>
</Label>
</div>
</RadioGroup>
</FormControl>
<FormField
control={form.control}
name="eabKid"
render={({ field }) => (
<FormItem hidden={provider !== "zerossl"}>
<FormLabel>EAB_KID</FormLabel>
<FormControl>
<Input placeholder={t("settings.ca.eab_kid.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="eabHmacKey"
render={({ field }) => (
<FormItem hidden={provider !== "zerossl"}>
<FormLabel>EAB_HMAC_KEY</FormLabel>
<FormControl>
<Input placeholder={t("settings.ca.eab_hmac_key.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit">{t("common.update")}</Button>
<Context.Provider value={{ onSubmit, setConfig, setting: config }}>
<div className="w-full md:max-w-[35em]">
<Label>{t("common.text.ca")}</Label>
<RadioGroup
className="flex mt-3"
onValueChange={(val) => {
setProvider(val as SSLProviderType);
}}
value={config.content?.provider}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="letsencrypt" id="letsencrypt" />
<Label htmlFor="letsencrypt">
<div className={cn("flex items-center space-x-2 border p-2 rounded cursor-pointer", getOptionCls("letsencrypt"))}>
<img src={"/imgs/providers/letsencrypt.svg"} className="h-6" />
<div>{"Let's Encrypt"}</div>
</div>
</Label>
</div>
</form>
</Form>
</div>
<div className="flex items-center space-x-2 ">
<RadioGroupItem value="zerossl" id="zerossl" />
<Label htmlFor="zerossl">
<div className={cn("flex items-center space-x-2 border p-2 rounded cursor-pointer", getOptionCls("zerossl"))}>
<img src={"/imgs/providers/zerossl.svg"} className="h-6" />
<div>{"ZeroSSL"}</div>
</div>
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="gts" id="gts" />
<Label htmlFor="gts">
<div className={cn("flex items-center space-x-2 border p-2 rounded cursor-pointer", getOptionCls("gts"))}>
<img src={"/imgs/providers/google.svg"} className="h-6" />
<div>{"Google Trust Services"}</div>
</div>
</Label>
</div>
</RadioGroup>
<SSLProviderForm kind={config.content?.provider ?? ""} />
</div>
</Context.Provider>
</>
);
};
const SSLProviderForm = ({ kind }: { kind: string }) => {
const getForm = () => {
switch (kind) {
case "zerossl":
return <SSLProviderZeroSSLForm />;
case "gts":
return <SSLProviderGtsForm />;
default:
return <SSLProviderLetsEncryptForm />;
}
};
return (
<>
<div className="mt-5">{getForm()}</div>
</>
);
};
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<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
kind: "letsencrypt",
},
});
const onLocalSubmit = async (data: z.infer<typeof formSchema>) => {
const newData = produce(setting, (draft) => {
if (!draft.content) {
draft.content = {
provider: data.kind,
config: {
letsencrypt: {},
},
};
}
});
onSubmit(newData);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onLocalSubmit)} className="space-y-8 dark:text-stone-200">
<FormField
control={form.control}
name="kind"
render={({ field }) => (
<FormItem hidden>
<FormLabel>kind</FormLabel>
<FormControl>
<Input {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t("common.update")}</Button>
</div>
</form>
</Form>
);
};
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<z.infer<typeof formSchema>>({
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<typeof formSchema>) => {
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 (
<Form {...form}>
<form onSubmit={form.handleSubmit(onLocalSubmit)} className="space-y-8 dark:text-stone-200">
<FormField
control={form.control}
name="kind"
render={({ field }) => (
<FormItem hidden>
<FormLabel>kind</FormLabel>
<FormControl>
<Input {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="eabKid"
render={({ field }) => (
<FormItem>
<FormLabel>EAB_KID</FormLabel>
<FormControl>
<Input placeholder={t("settings.ca.eab_kid.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="eabHmacKey"
render={({ field }) => (
<FormItem>
<FormLabel>EAB_HMAC_KEY</FormLabel>
<FormControl>
<Input placeholder={t("settings.ca.eab_hmac_key.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t("common.update")}</Button>
</div>
</form>
</Form>
);
};
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<z.infer<typeof formSchema>>({
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<typeof formSchema>) => {
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 (
<Form {...form}>
<form onSubmit={form.handleSubmit(onLocalSubmit)} className="space-y-8 dark:text-stone-200">
<FormField
control={form.control}
name="kind"
render={({ field }) => (
<FormItem hidden>
<FormLabel>kind</FormLabel>
<FormControl>
<Input {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="eabKid"
render={({ field }) => (
<FormItem>
<FormLabel>EAB_KID</FormLabel>
<FormControl>
<Input placeholder={t("settings.ca.eab_kid.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="eabHmacKey"
render={({ field }) => (
<FormItem>
<FormLabel>EAB_HMAC_KEY</FormLabel>
<FormControl>
<Input placeholder={t("settings.ca.eab_hmac_key.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t("common.update")}</Button>
</div>
</form>
</Form>
);
};
export default SSLProvider;

View File

@ -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<EmailsSetting>;
accessGroups: AccessGroup[];
};
export type ConfigContext = {
config: ConfigData;
setEmails: (email: Setting) => void;
setEmails: (email: Setting<EmailsSetting>) => 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<EmailsSetting>) => {
dispatchConfig({ type: "SET_EMAILS", payload: emails });
}, []);

View File

@ -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<EmailsSetting> }
| { type: "ADD_EMAIL"; payload: string }
| { type: "SET_ACCESS_GROUPS"; payload: AccessGroup[] };

View File

@ -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<NotifyChannels>;
setChannel: (data: { channel: string; data: NotifyChannel }) => void;
setChannels: (data: Setting) => void;
setChannels: (data: Setting<NotifyChannels>) => 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>("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<NotifyChannels>) => {
dispatchNotify({
type: "SET_CHANNELS",
payload: setting,

View File

@ -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<NotifyChannels>;
};
export const notifyReducer = (state: Setting, action: Action) => {
export const notifyReducer = (state: Setting<NotifyChannels>, action: Action) => {
switch (action.type) {
case "SET_CHANNEL": {
const channel = action.payload.channel;

View File

@ -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<Setting>("name='emails'");
const resp = await getPb().collection("settings").getFirstListItem<Setting<EmailsSetting>>("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 <T>(name: string) => {
try {
const resp = await getPb().collection("settings").getFirstListItem<Setting>(`name='${name}'`);
const resp = await getPb().collection("settings").getFirstListItem<Setting<T>>(`name='${name}'`);
return resp;
} catch (e) {
const rs: Setting = {
const rs: Setting<T> = {
name: name,
};
return rs;
}
};
export const update = async (setting: Setting) => {
export const update = async <T>(setting: Setting<T>) => {
const pb = getPb();
let resp: Setting;
let resp: Setting<T>;
if (setting.id) {
resp = await pb.collection("settings").update(setting.id, setting);
} else {