Merge branch 'main' into feat/cloud-load-balance

This commit is contained in:
Fu Diwei 2024-10-21 14:55:19 +08:00
commit 68b9171390
15 changed files with 167 additions and 39 deletions

View File

@ -9,6 +9,7 @@ RUN \
npm install && \ npm install && \
npm run build npm run build
FROM golang:1.22-alpine AS builder FROM golang:1.22-alpine AS builder
WORKDIR /app WORKDIR /app
@ -16,14 +17,17 @@ WORKDIR /app
COPY ../. /app/ COPY ../. /app/
RUN rm -rf /app/ui/dist RUN rm -rf /app/ui/dist
COPY --from=front-builder /app/ui/dist /app/ui/dist COPY --from=front-builder /app/ui/dist /app/ui/dist
RUN go build -o certimate RUN go build -o certimate
FROM alpine:latest FROM alpine:latest
WORKDIR /app WORKDIR /app
COPY --from=builder /app/certimate . COPY --from=builder /app/certimate .
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"] ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]

View File

@ -7,6 +7,8 @@ import (
"crypto/rand" "crypto/rand"
"errors" "errors"
"fmt" "fmt"
"os"
"strconv"
"strings" "strings"
"github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/domain"
@ -63,12 +65,13 @@ type Certificate struct {
} }
type ApplyOption struct { type ApplyOption struct {
Email string `json:"email"` Email string `json:"email"`
Domain string `json:"domain"` Domain string `json:"domain"`
Access string `json:"access"` Access string `json:"access"`
KeyAlgorithm string `json:"keyAlgorithm"` KeyAlgorithm string `json:"keyAlgorithm"`
Nameservers string `json:"nameservers"` Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"` Timeout int64 `json:"timeout"`
DisableFollowCNAME bool `json:"disableFollowCNAME"`
} }
type ApplyUser struct { type ApplyUser struct {
@ -115,12 +118,13 @@ func Get(record *models.Record) (Applicant, error) {
} }
option := &ApplyOption{ option := &ApplyOption{
Email: applyConfig.Email, Email: applyConfig.Email,
Domain: record.GetString("domain"), Domain: record.GetString("domain"),
Access: access.GetString("config"), Access: access.GetString("config"),
KeyAlgorithm: applyConfig.KeyAlgorithm, KeyAlgorithm: applyConfig.KeyAlgorithm,
Nameservers: applyConfig.Nameservers, Nameservers: applyConfig.Nameservers,
Timeout: applyConfig.Timeout, Timeout: applyConfig.Timeout,
DisableFollowCNAME: applyConfig.DisableFollowCNAME,
} }
switch access.GetString("configType") { switch access.GetString("configType") {
@ -177,6 +181,10 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
return nil, err return nil, err
} }
// Some unified lego environment variables are configured here.
// link: https://github.com/go-acme/lego/issues/1867
os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(option.DisableFollowCNAME))
myUser := ApplyUser{ myUser := ApplyUser{
Email: option.Email, Email: option.Email,
key: privateKey, key: privateKey,

View File

@ -1,7 +1,7 @@
/* /*
* @Author: Bin * @Author: Bin
* @Date: 2024-09-17 * @Date: 2024-09-17
* @FilePath: /github.com/usual2970/certimate/internal/deployer/aliyun_esa.go * @FilePath: /certimate/internal/deployer/aliyun_esa.go
*/ */
package deployer package deployer
@ -9,6 +9,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
dcdn20180115 "github.com/alibabacloud-go/dcdn-20180115/v3/client" dcdn20180115 "github.com/alibabacloud-go/dcdn-20180115/v3/client"
@ -55,8 +56,15 @@ func (d *AliyunESADeployer) GetInfo() []string {
func (d *AliyunESADeployer) Deploy(ctx context.Context) error { func (d *AliyunESADeployer) Deploy(ctx context.Context) error {
certName := fmt.Sprintf("%s-%s-%s", d.option.Domain, d.option.DomainId, rand.RandStr(6)) certName := fmt.Sprintf("%s-%s-%s", d.option.Domain, d.option.DomainId, rand.RandStr(6))
// 支持泛解析域名,在 Aliyun DCND 中泛解析域名表示为 .example.com
domain := getDeployString(d.option.DeployConfig, "domain")
if strings.HasPrefix(domain, "*") {
domain = strings.TrimPrefix(domain, "*")
}
setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{ setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{
DomainName: tea.String(getDeployString(d.option.DeployConfig, "domain")), DomainName: tea.String(domain),
CertName: tea.String(certName), CertName: tea.String(certName),
CertType: tea.String("upload"), CertType: tea.String("upload"),
SSLProtocol: tea.String("on"), SSLProtocol: tea.String("on"),

View File

@ -1,11 +1,12 @@
package domain package domain
type ApplyConfig struct { type ApplyConfig struct {
Email string `json:"email"` Email string `json:"email"`
Access string `json:"access"` Access string `json:"access"`
KeyAlgorithm string `json:"keyAlgorithm"` KeyAlgorithm string `json:"keyAlgorithm"`
Nameservers string `json:"nameservers"` Nameservers string `json:"nameservers"`
Timeout int64 `json:"timeout"` Timeout int64 `json:"timeout"`
DisableFollowCNAME bool `json:"disableFollowCNAME"`
} }
type DeployConfig struct { type DeployConfig struct {

View File

@ -3,13 +3,17 @@ import { BookOpen } from "lucide-react";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { version } from "@/domain/version"; import { version } from "@/domain/version";
import { cn } from "@/lib/utils";
const Version = () => { type VersionProps = {
className?: string;
};
const Version = ({ className }: VersionProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="fixed right-0 bottom-0 w-full flex justify-between p-5"> <div className={cn("w-full flex pb-5 ", className)}>
<div className=""></div>
<div className="text-muted-foreground text-sm hover:text-stone-900 dark:hover:text-stone-200 flex"> <div className="text-muted-foreground text-sm hover:text-stone-900 dark:hover:text-stone-200 flex">
<a href="https://docs.certimate.me" target="_blank" className="flex items-center"> <a href="https://docs.certimate.me" target="_blank" className="flex items-center">
<BookOpen size={16} /> <BookOpen size={16} />
@ -25,3 +29,4 @@ const Version = () => {
}; };
export default Version; export default Version;

View File

@ -24,4 +24,31 @@ const TooltipContent = React.forwardRef<React.ElementRef<typeof TooltipPrimitive
); );
TooltipContent.displayName = TooltipPrimitive.Content.displayName; TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; type TooltipFastProps = TooltipPrimitive.TooltipContentProps &
TooltipPrimitive.TooltipProps &
React.RefAttributes<HTMLDivElement> & {
contentView?: JSX.Element;
};
const TooltipLink = React.forwardRef((props: React.PropsWithChildren, forwardedRef: React.ForwardedRef<HTMLAnchorElement>) => (
<a {...props} ref={forwardedRef}>
{props.children}
</a>
));
function TooltipFast({ children, contentView, open, defaultOpen, onOpenChange, ...props }: TooltipFastProps) {
return (
<TooltipProvider>
<Tooltip open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
<TooltipPrimitive.Trigger asChild>
<TooltipLink>{children}</TooltipLink>
</TooltipPrimitive.Trigger>
<TooltipContent side="top" align="center" {...props}>
{contentView}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, TooltipFast };

View File

@ -53,6 +53,7 @@ export type ApplyConfig = {
keyAlgorithm?: string; keyAlgorithm?: string;
nameservers?: string; nameservers?: string;
timeout?: number; timeout?: number;
disableFollowCNAME?: boolean;
}; };
export type Statistic = { export type Statistic = {

View File

@ -1 +1 @@
export const version = "Certimate v0.2.4"; export const version = "Certimate v0.2.5";

View File

@ -41,6 +41,9 @@
"domain.application.form.key_algorithm.placeholder": "Please select certificate key algorithm", "domain.application.form.key_algorithm.placeholder": "Please select certificate key algorithm",
"domain.application.form.timeout.label": "DNS Propagation Timeout (Seconds)", "domain.application.form.timeout.label": "DNS Propagation Timeout (Seconds)",
"domain.application.form.timeoue.placeholder": "Please enter maximum waiting time for DNS propagation", "domain.application.form.timeoue.placeholder": "Please enter maximum waiting time for DNS propagation",
"domain.application.form.disable_follow_cname.label": "Disable DNS CNAME following",
"domain.application.form.disable_follow_cname.tips": "This option will disable Acme DNS authentication CNAME follow. If you don't understand this option, just keep it by default. ",
"domain.application.form.disable_follow_cname.tips_link": "Learn more",
"domain.application.unsaved.message": "Please save applyment configuration first", "domain.application.unsaved.message": "Please save applyment configuration first",
"domain.deployment.tab": "Deploy Settings", "domain.deployment.tab": "Deploy Settings",

View File

@ -41,6 +41,9 @@
"domain.application.form.key_algorithm.placeholder": "请选择数字证书算法", "domain.application.form.key_algorithm.placeholder": "请选择数字证书算法",
"domain.application.form.timeout.label": "DNS 传播检查超时时间(单位:秒)", "domain.application.form.timeout.label": "DNS 传播检查超时时间(单位:秒)",
"domain.application.form.timeoue.placeholder": "请输入 DNS 传播检查超时时间", "domain.application.form.timeoue.placeholder": "请输入 DNS 传播检查超时时间",
"domain.application.form.disable_follow_cname.label": "禁用 DNS CNAME 跟随",
"domain.application.form.disable_follow_cname.tips": "该选项将禁用 Acme DNS 认证 CNAME 跟随,如果你不了解此选项保持默认即可,",
"domain.application.form.disable_follow_cname.tips_link": "了解更多",
"domain.application.unsaved.message": "请先保存申请配置", "domain.application.unsaved.message": "请先保存申请配置",
"domain.deployment.tab": "部署配置", "domain.deployment.tab": "部署配置",

View File

@ -21,6 +21,16 @@ export const getDate = (zuluTime: string) => {
return time.split(" ")[0]; return time.split(" ")[0];
}; };
export const getLeftDays = (zuluTime: string) => {
const time = convertZulu2Beijing(zuluTime);
const date = time.split(" ")[0];
const now = new Date();
const target = new Date(date);
const diff = target.getTime() - now.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
return days;
};
export function getTimeBefore(days: number): string { export function getTimeBefore(days: number): string {
// 获取当前时间 // 获取当前时间
const currentDate = new Date(); const currentDate = new Date();
@ -66,3 +76,4 @@ export function getTimeAfter(days: number): string {
return formattedDate; return formattedDate;
} }

View File

@ -72,6 +72,10 @@ export default function Dashboard() {
</Link> </Link>
</nav> </nav>
</div> </div>
<div className="">
<Version className="justify-center" />
</div>
</div> </div>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
@ -117,6 +121,9 @@ export default function Dashboard() {
{t("history.page.title")} {t("history.page.title")}
</Link> </Link>
</nav> </nav>
<div className="">
<Version className="justify-center" />
</div>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
<div className="w-full flex-1"></div> <div className="w-full flex-1"></div>
@ -137,8 +144,6 @@ export default function Dashboard() {
</header> </header>
<main className="flex flex-1 flex-col gap-4 p-4 lg:gap-6 lg:p-6 relative"> <main className="flex flex-1 flex-col gap-4 p-4 lg:gap-6 lg:p-6 relative">
<Outlet /> <Outlet />
<Version />
</main> </main>
</div> </div>
</div> </div>
@ -146,3 +151,4 @@ export default function Dashboard() {
</> </>
); );
} }

View File

@ -12,9 +12,10 @@ const LoginLayout = () => {
<div className="container"> <div className="container">
<Outlet /> <Outlet />
<Version /> <Version className="fixed right-0 bottom-0 justify-end pr-5" />
</div> </div>
); );
}; };
export default LoginLayout; export default LoginLayout;

View File

@ -4,7 +4,7 @@ import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import z from "zod"; import z from "zod";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { ChevronsUpDown, Plus } from "lucide-react"; import { ChevronsUpDown, Plus, CircleHelp } from "lucide-react";
import { ClientResponseError } from "pocketbase"; import { ClientResponseError } from "pocketbase";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -26,6 +26,8 @@ import { EmailsSetting } from "@/domain/settings";
import { DeployConfig, Domain } from "@/domain/domain"; import { DeployConfig, Domain } from "@/domain/domain";
import { save, get } from "@/repository/domains"; import { save, get } from "@/repository/domains";
import { useConfig } from "@/providers/config"; import { useConfig } from "@/providers/config";
import { Switch } from "@/components/ui/switch";
import { TooltipFast } from "@/components/ui/tooltip";
const Edit = () => { const Edit = () => {
const { const {
@ -64,6 +66,7 @@ const Edit = () => {
keyAlgorithm: z.string().optional(), keyAlgorithm: z.string().optional(),
nameservers: z.string().optional(), nameservers: z.string().optional(),
timeout: z.number().optional(), timeout: z.number().optional(),
disableFollowCNAME: z.boolean().optional(),
}); });
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
@ -76,6 +79,7 @@ const Edit = () => {
keyAlgorithm: "RSA2048", keyAlgorithm: "RSA2048",
nameservers: "", nameservers: "",
timeout: 60, timeout: 60,
disableFollowCNAME: true,
}, },
}); });
@ -89,6 +93,7 @@ const Edit = () => {
keyAlgorithm: domain.applyConfig?.keyAlgorithm, keyAlgorithm: domain.applyConfig?.keyAlgorithm,
nameservers: domain.applyConfig?.nameservers, nameservers: domain.applyConfig?.nameservers,
timeout: domain.applyConfig?.timeout, timeout: domain.applyConfig?.timeout,
disableFollowCNAME: domain.applyConfig?.disableFollowCNAME,
}); });
} }
}, [domain, form]); }, [domain, form]);
@ -108,6 +113,7 @@ const Edit = () => {
keyAlgorithm: data.keyAlgorithm, keyAlgorithm: data.keyAlgorithm,
nameservers: data.nameservers, nameservers: data.nameservers,
timeout: data.timeout, timeout: data.timeout,
disableFollowCNAME: data.disableFollowCNAME,
}, },
}; };
@ -176,7 +182,7 @@ const Edit = () => {
<> <>
<div className=""> <div className="">
<Toaster /> <Toaster />
<div className=" h-5 text-muted-foreground"> <div className="h-5 text-muted-foreground">
<Breadcrumb> <Breadcrumb>
<BreadcrumbList> <BreadcrumbList>
<BreadcrumbItem> <BreadcrumbItem>
@ -190,7 +196,7 @@ const Edit = () => {
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
</div> </div>
<div className="mt-5 flex w-full justify-center md:space-x-10 flex-col md:flex-row"> <div className="flex flex-col justify-center w-full mt-5 md:space-x-10 md:flex-row">
<div className="w-full md:w-[200px] text-muted-foreground space-x-3 md:space-y-3 flex-row md:flex-col flex md:mt-5"> <div className="w-full md:w-[200px] text-muted-foreground space-x-3 md:space-y-3 flex-row md:flex-col flex md:mt-5">
<div <div
className={cn("cursor-pointer text-right", tab === "apply" ? "text-primary" : "")} className={cn("cursor-pointer text-right", tab === "apply" ? "text-primary" : "")}
@ -247,11 +253,11 @@ const Edit = () => {
name="email" name="email"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="flex w-full justify-between"> <FormLabel className="flex justify-between w-full">
<div>{t("domain.application.form.email.label") + " " + t("domain.application.form.email.tips")}</div> <div>{t("domain.application.form.email.label") + " " + t("domain.application.form.email.tips")}</div>
<EmailsEdit <EmailsEdit
trigger={ trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center"> <div className="flex items-center font-normal cursor-pointer text-primary hover:underline">
<Plus size={14} /> <Plus size={14} />
{t("common.add")} {t("common.add")}
</div> </div>
@ -293,11 +299,11 @@ const Edit = () => {
name="access" name="access"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="flex w-full justify-between"> <FormLabel className="flex justify-between w-full">
<div>{t("domain.application.form.access.label")}</div> <div>{t("domain.application.form.access.label")}</div>
<AccessEditDialog <AccessEditDialog
trigger={ trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center"> <div className="flex items-center font-normal cursor-pointer text-primary hover:underline">
<Plus size={14} /> <Plus size={14} />
{t("common.add")} {t("common.add")}
</div> </div>
@ -344,8 +350,8 @@ const Edit = () => {
<Collapsible> <Collapsible>
<CollapsibleTrigger className="w-full my-4"> <CollapsibleTrigger className="w-full my-4">
<div className="flex items-center justify-between space-x-4"> <div className="flex items-center justify-between space-x-4">
<span className="flex-1 text-sm text-gray-600 text-left">{t("domain.application.form.advanced_settings.label")}</span> <span className="flex-1 text-sm text-left text-gray-600">{t("domain.application.form.advanced_settings.label")}</span>
<ChevronsUpDown className="h-4 w-4" /> <ChevronsUpDown className="w-4 h-4" />
</div> </div>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
@ -424,6 +430,49 @@ const Edit = () => {
</FormItem> </FormItem>
)} )}
/> />
{/* 禁用 CNAME 跟随 */}
<FormField
control={form.control}
name="disableFollowCNAME"
render={({ field }) => (
<FormItem>
<FormLabel>
<div className="flex">
<span className="mr-1">{t("domain.application.form.disable_follow_cname.label")} </span>
<TooltipFast
className="max-w-[20rem]"
contentView={
<p>
{t("domain.application.form.disable_follow_cname.tips")}
<a
className="text-primary"
target="_blank"
href="https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme/#the-advantages-of-a-cname"
>
{t("domain.application.form.disable_follow_cname.tips_link")}
</a>
</p>
}
>
<CircleHelp size={14} />
</TooltipFast>
</div>
</FormLabel>
<FormControl>
<div>
<Switch
defaultChecked={field.value}
onCheckedChange={(value) => {
form.setValue(field.name, value);
}}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>

View File

@ -26,7 +26,7 @@ import { Toaster } from "@/components/ui/toaster";
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { CustomFile, saveFiles2ZIP } from "@/lib/file"; import { CustomFile, saveFiles2ZIP } from "@/lib/file";
import { convertZulu2Beijing, getDate } from "@/lib/time"; import { convertZulu2Beijing, getDate, getLeftDays } from "@/lib/time";
import { Domain } from "@/domain/domain"; import { Domain } from "@/domain/domain";
import { list, remove, save, subscribeId, unsubscribeId } from "@/repository/domains"; import { list, remove, save, subscribeId, unsubscribeId } from "@/repository/domains";
@ -213,7 +213,7 @@ const Home = () => {
<div> <div>
{domain.expiredAt ? ( {domain.expiredAt ? (
<> <>
<div>{t("domain.props.expiry.date1", { date: 90 })}</div> <div>{t("domain.props.expiry.date1", { date: `${getLeftDays(domain.expiredAt)}/90` })}</div>
<div> <div>
{t("domain.props.expiry.date2", { {t("domain.props.expiry.date2", {
date: getDate(domain.expiredAt), date: getDate(domain.expiredAt),
@ -340,3 +340,4 @@ const Home = () => {
}; };
export default Home; export default Home;