Compare commits

...

9 Commits

Author SHA1 Message Date
yoan
39bffe3389 v0.1.18 2024-10-11 07:53:32 +08:00
usual2970
3f2767b28b Merge pull request #183 from LeoChen98/feat-tencent-cdn-extensive-support
add feat: support for tencent cdn extensive domain
2024-10-11 07:02:24 +08:00
Leo Chen
312c6e685a change var name style 2024-10-10 22:45:19 +08:00
Leo Chen
d2b6ab75b7 add feat: support for tencent cdn extensive domain 2024-10-10 19:01:32 +08:00
usual2970
dc16294b3d Update README_EN.md 2024-10-09 12:33:42 +08:00
usual2970
77dfcef168 Update README.md 2024-10-09 12:31:53 +08:00
yoan
30ef5841d6 dark mode style fix 2024-10-09 09:26:39 +08:00
yoan
217ba85ff8 v0.1.16 2024-10-09 09:04:23 +08:00
yoan
71e2555391 multiple domain support 2024-10-08 22:02:00 +08:00
20 changed files with 853 additions and 451 deletions

View File

@@ -32,6 +32,11 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
./certimate serve
```
或运行以下命令自动给 Certimate 自身添加证书
```bash
./certimate serve 你的域名
```
> [!NOTE]
> MacOS 在执行二进制文件时会提示无法打开“certimate”因为Apple无法检查其是否包含恶意软件。可在系统设置> 隐私与安全性> 安全性 中点击 "仍然允许",然后再次尝试执行二进制文件。
@@ -155,7 +160,7 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.
## 八、加入社区
* [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
* 微信群聊(需邀请入群,可先加作者好友)
* 微信群聊(超200人需邀请入群,可先加作者好友)
<img src="https://i.imgur.com/8xwsLTA.png" width="400"/>

View File

@@ -32,6 +32,12 @@ You can download the precompiled binary files directly from the [Releases page](
./certimate serve
```
Or run the following command to automatically add a certificate to Certimate itself.
```bash
./certimate serve yourDomain
```
> [!NOTE]
> When executing the binary file on macOS, you may see a prompt saying: “Cannot open certimate because Apple cannot check it for malicious software.” You can go to System Preferences > Security & Privacy > General, then click “Allow Anyway,” and try executing the binary file again.

3
go.mod
View File

@@ -18,7 +18,8 @@ require (
github.com/pocketbase/dbx v1.10.1
github.com/pocketbase/pocketbase v0.22.18
github.com/qiniu/go-sdk/v7 v7.22.0
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
golang.org/x/crypto v0.26.0
)

5
go.sum
View File

@@ -381,9 +381,12 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017 h1:OymmfmyFkvHirY3WHsoRT3cdTEsqygLbMn8jM41erK4=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017/go.mod h1:gnLxGXlLmF+jDqWR1/RVoF/UUwxQxomQhkc0oN7KeuI=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992 h1:266lOve+E8vzhnrb/Mr05Ee+oxXD9C82JiusY/AZqXw=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017 h1:SXrldOXwgomYuATVAuz5ofpTjB+99qVELgdy5R5kMgI=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 h1:LoYv5u+gUoFpU/AmIuTRG/2KiEkdm9gCC0dTvk8WITQ=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898/go.mod h1:c1j6YQ+vCbeA8kJ59Im4UnMd1GxovlpPBDhGZoewfn8=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992 h1:A6O89OlCJQUpNxGqC/E5By04UNKBryIt5olQIGOx8mg=

View File

@@ -172,13 +172,7 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
}
myUser.Registration = reg
domains := []string{option.Domain}
// 如果是通配置符域名,把根域名也加入
if strings.HasPrefix(option.Domain, "*.") && len(strings.Split(option.Domain, ".")) == 3 {
rootDomain := strings.TrimPrefix(option.Domain, "*.")
domains = append(domains, rootDomain)
}
domains := strings.Split(option.Domain, ";")
request := certificate.ObtainRequest{
Domains: domains,

View File

@@ -5,11 +5,14 @@ import (
"certimate/internal/utils/rand"
"context"
"encoding/json"
"encoding/base64"
"fmt"
"strings"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
cdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
)
type tencentCdn struct {
@@ -89,14 +92,29 @@ func (t *tencentCdn) deploy(certId string) error {
// 实例化要请求产品的client对象,clientProfile是可选的
client, _ := ssl.NewClient(t.credential, "", cpf)
// 实例化一个请求对象,每个接口都会对应一个request对象
request := ssl.NewDeployCertificateInstanceRequest()
request.CertificateId = common.StringPtr(certId)
request.InstanceIdList = common.StringPtrs([]string{t.option.Domain})
request.ResourceType = common.StringPtr("cdn")
request.Status = common.Int64Ptr(1)
// 如果是泛域名就从cdn列表下获取SSL证书中的可用域名
if(strings.Contains(t.option.Domain, "*")){
list, errGetList := t.getDomainList()
if errGetList != nil {
return fmt.Errorf("failed to get certificate domain list: %w", errGetList)
}
if list == nil || len(list) == 0 {
return fmt.Errorf("failed to get certificate domain list: empty list.")
}
request.InstanceIdList = common.StringPtrs(list)
}else{ // 否则直接使用传入的域名
request.InstanceIdList = common.StringPtrs([]string{t.option.Domain})
}
// 返回的resp是一个DeployCertificateInstanceResponse的实例与请求对象对应
resp, err := client.DeployCertificateInstance(request)
@@ -106,3 +124,27 @@ func (t *tencentCdn) deploy(certId string) error {
t.infos = append(t.infos, toStr("部署证书", resp.Response))
return nil
}
func (t *tencentCdn) getDomainList() ([]string, error) {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "cdn.tencentcloudapi.com"
client, _ := cdn.NewClient(t.credential, "", cpf)
request := cdn.NewDescribeCertDomainsRequest()
cert := base64.StdEncoding.EncodeToString([]byte(t.option.Certificate.Certificate))
request.Cert = &cert
response, err := client.DescribeCertDomains(request)
if err != nil {
return nil, fmt.Errorf("failed to get domain list: %w", err)
}
domains := make([]string, 0)
for _, domain := range response.Response.Domains {
domains = append(domains, *domain)
}
return domains, nil
}

332
ui/dist/assets/index-DOR0Uh6g.js vendored Normal file
View File

File diff suppressed because one or more lines are too long

1
ui/dist/assets/index-DOft-CKV.css vendored Normal file
View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

4
ui/dist/index.html vendored
View File

@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Certimate - Your Trusted SSL Automation Partner</title>
<script type="module" crossorigin src="/assets/index-Dn4jGLHB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-I--T0qY3.css">
<script type="module" crossorigin src="/assets/index-DOR0Uh6g.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DOft-CKV.css">
</head>
<body class="bg-background">
<div id="root"></div>

View File

@@ -0,0 +1,245 @@
import { cn } from "@/lib/utils";
import Show from "../Show";
import { useCallback, useEffect, useMemo, useState } from "react";
import { FormControl, FormLabel } from "../ui/form";
import { Button } from "../ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { Input } from "../ui/input";
import { z } from "zod";
import { useTranslation } from "react-i18next";
import { Edit, Plus, Trash2 } from "lucide-react";
type StringListProps = {
className?: string;
value: string;
valueType?: "domain" | "ip";
onValueChange: (value: string) => void;
};
const titles: Record<string, string> = {
domain: "domain",
ip: "IP",
};
const StringList = ({
value,
className,
onValueChange,
valueType = "domain",
}: StringListProps) => {
const [list, setList] = useState<string[]>([]);
const { t } = useTranslation();
useMemo(() => {
if (value) {
setList(value.split(";"));
}
}, [value]);
useEffect(() => {
const changeList = () => {
onValueChange(list.join(";"));
};
changeList();
}, [list]);
const addVal = (val: string) => {
if (list.includes(val)) {
return;
}
setList([...list, val]);
};
const editVal = (index: number, val: string) => {
const newList = [...list];
newList[index] = val;
setList(newList);
};
const onRemoveClick = (index: number) => {
const newList = [...list];
newList.splice(index, 1);
setList(newList);
};
return (
<>
<div className={cn(className)}>
<FormLabel className="flex justify-between items-center">
<div>{t(titles[valueType])}</div>
<Show when={list.length > 0}>
<StringEdit
op="add"
onValueChange={(val: string) => {
addVal(val);
}}
valueType={valueType}
value={""}
trigger={
<div className="flex items-center text-primary">
<Plus size={16} className="cursor-pointer " />
<div className="text-sm ">{t("add")}</div>
</div>
}
/>
</Show>
</FormLabel>
<FormControl>
<Show
when={list.length > 0}
fallback={
<div className="border rounded-md p-3 text-sm mt-2 flex flex-col items-center">
<div className="text-muted-foreground"></div>
<StringEdit
value={""}
trigger={t("add")}
onValueChange={addVal}
valueType={valueType}
/>
</div>
}
>
<div className="border rounded-md p-3 text-sm mt-2 text-gray-700 space-y-2 dark:text-white dark:border-stone-700 dark:bg-stone-950">
{list.map((item, index) => (
<div key={index} className="flex justify-between items-center">
<div>{item}</div>
<div className="flex space-x-2">
<StringEdit
op="edit"
valueType={valueType}
trigger={
<Edit
size={16}
className="cursor-pointer text-gray-600 dark:text-white"
/>
}
value={item}
onValueChange={(val: string) => {
editVal(index, val);
}}
/>
<Trash2
size={16}
className="cursor-pointer"
onClick={() => {
onRemoveClick(index);
}}
/>
</div>
</div>
))}
</div>
</Show>
</FormControl>
</div>
</>
);
};
export default StringList;
type ValueType = "domain" | "ip";
type StringEditProps = {
value: string;
trigger: React.ReactNode;
onValueChange: (value: string) => void;
valueType: ValueType;
op?: "add" | "edit";
};
const StringEdit = ({
trigger,
value,
onValueChange,
op = "add",
valueType,
}: StringEditProps) => {
const [currentValue, setCurrentValue] = useState<string>("");
const [open, setOpen] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const { t } = useTranslation();
useEffect(() => {
setCurrentValue(value);
}, [value]);
const domainSchema = z
.string()
.regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("domain.not.empty.verify.message"),
});
const ipSchema = z.string().ip({ message: t("ip.not.empty.verify.message") });
const schedules: Record<ValueType, z.ZodString> = {
domain: domainSchema,
ip: ipSchema,
};
const onSaveClick = useCallback(() => {
const schema = schedules[valueType];
const resp = schema.safeParse(currentValue);
if (!resp.success) {
setError(JSON.parse(resp.error.message)[0].message);
return;
}
setCurrentValue("");
setOpen(false);
setError("");
onValueChange(currentValue);
}, [currentValue]);
return (
<Dialog
open={open}
onOpenChange={(open) => {
setOpen(open);
}}
>
<DialogTrigger className="text-primary">{trigger}</DialogTrigger>
<DialogContent className="dark:text-white">
<DialogHeader>
<DialogTitle className="dark:text-white">
{t(titles[valueType])}
</DialogTitle>
</DialogHeader>
<Input
value={currentValue}
className="dark:text-white"
onChange={(e) => {
setCurrentValue(e.target.value);
}}
/>
<Show when={error.length > 0}>
<div className="text-red-500 text-sm">{error}</div>
</Show>
<DialogFooter>
<Button
onClick={() => {
onSaveClick();
}}
>
{op === "add" ? t("add") : t("confirm")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -26,6 +26,22 @@ export type Domain = {
expand?: {
lastDeployment?: Deployment;
};
applyConfig?: ApplyConfig;
deployConfig?: DeployConfig[];
};
export type DeployConfig = {
access: string;
type: string;
config?: Record<string, string>;
};
export type ApplyConfig = {
access: string;
email: string;
timeout?: number;
nameservers?: string;
};
export type Statistic = {

View File

@@ -1 +1 @@
export const version = "Certimate v0.1.15";
export const version = "Certimate v0.1.18";

View File

@@ -83,6 +83,7 @@
"pagination.prev": "Previous",
"domain": "Domain",
"domain.add": "Add Domain",
"domain.edit":"Edit Domain",
"domain.delete": "Delete Domain",
"domain.not.empty.verify.message": "Please enter domain",
"domain.management.name": "Domain List",

View File

@@ -83,6 +83,7 @@
"pagination.prev": "上一页",
"domain": "域名",
"domain.add": "新增域名",
"domain.edit": "编辑域名",
"domain.delete": "删除域名",
"domain.not.empty.verify.message": "请输入域名",
"domain.management.name": "域名列表",

View File

@@ -57,7 +57,7 @@ const Dashboard = () => {
return (
<div className="flex flex-col">
<div className="flex justify-between items-center">
<div className="text-muted-foreground">{t('dashboard')}</div>
<div className="text-muted-foreground">{t("dashboard")}</div>
</div>
<div className="flex mt-10 gap-5 flex-col flex-wrap md:flex-row">
<div className="w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg border">
@@ -66,7 +66,7 @@ const Dashboard = () => {
</div>
<div>
<div className="text-muted-foreground font-semibold">
{t('dashboard.all')}
{t("dashboard.all")}
</div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
@@ -91,7 +91,7 @@ const Dashboard = () => {
</div>
<div>
<div className="text-muted-foreground font-semibold">
{t('dashboard.near.expired')}
{t("dashboard.near.expired")}
</div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
@@ -120,7 +120,7 @@ const Dashboard = () => {
</div>
<div>
<div className="text-muted-foreground font-semibold">
{t('dashboard.enabled')}
{t("dashboard.enabled")}
</div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
@@ -144,7 +144,9 @@ const Dashboard = () => {
<Ban size={48} strokeWidth={1} className="text-gray-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold">{t('dashboard.not.enabled')}</div>
<div className="text-muted-foreground font-semibold">
{t("dashboard.not.enabled")}
</div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.disabled ? (
@@ -168,22 +170,19 @@ const Dashboard = () => {
<div>
<div className="text-muted-foreground mt-5 text-sm">
{t('deployment.log.name')}
{t("deployment.log.name")}
</div>
{deployments?.length == 0 ? (
<>
<Alert className="max-w-[40em] mt-10">
<AlertTitle>{t('no.data')}</AlertTitle>
<AlertTitle>{t("no.data")}</AlertTitle>
<AlertDescription>
<div className="flex items-center mt-5">
<div>
<Smile className="text-yellow-400" size={36} />
</div>
<div className="ml-2">
{" "}
{t('deployment.log.empty')}
</div>
<div className="ml-2"> {t("deployment.log.empty")}</div>
</div>
<div className="mt-2 flex justify-end">
<Button
@@ -191,7 +190,7 @@ const Dashboard = () => {
navigate("/edit");
}}
>
{t('domain.add')}
{t("domain.add")}
</Button>
</div>
</AlertDescription>
@@ -200,16 +199,18 @@ const Dashboard = () => {
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48">{t('domain')}</div>
<div className="w-48">{t("domain")}</div>
<div className="w-24">{t('deployment.log.status')}</div>
<div className="w-56">{t('deployment.log.stage')}</div>
<div className="w-56 sm:ml-2 text-center">{t('deployment.log.last.execution.time')}</div>
<div className="w-24">{t("deployment.log.status")}</div>
<div className="w-56">{t("deployment.log.stage")}</div>
<div className="w-56 sm:ml-2 text-center">
{t("deployment.log.last.execution.time")}
</div>
<div className="grow">{t('operation')}</div>
<div className="grow">{t("operation")}</div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
{t('deployment.log.name')}
{t("deployment.log.name")}
</div>
{deployments?.map((deployment) => (
@@ -218,7 +219,14 @@ const Dashboard = () => {
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center">
{deployment.expand.domain?.domain}
{deployment.expand.domain?.domain
.split(";")
.map((domain: string) => (
<>
{domain}
<br />
</>
))}
</div>
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
<DeployState deployment={deployment} />
@@ -236,14 +244,14 @@ const Dashboard = () => {
<Sheet>
<SheetTrigger asChild>
<Button variant={"link"} className="p-0">
{t('deployment.log.detail.button.text')}
{t("deployment.log.detail.button.text")}
</Button>
</SheetTrigger>
<SheetContent className="sm:max-w-5xl">
<SheetHeader>
<SheetTitle>
{deployment.expand.domain?.domain}-{deployment.id}
{t('deployment.log.detail')}
{t("deployment.log.detail")}
</SheetTitle>
</SheetHeader>
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">

View File

@@ -1,4 +1,3 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -39,6 +38,7 @@ import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { EmailsSetting } from "@/domain/settings";
import { useTranslation } from "react-i18next";
import StringList from "@/components/certimate/StringList";
const Edit = () => {
const {
@@ -70,16 +70,16 @@ const Edit = () => {
const formSchema = z.object({
id: z.string().optional(),
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: 'domain.not.empty.verify.message',
domain: z.string().min(1, {
message: "domain.not.empty.verify.message",
}),
email: z.string().email('email.valid.message').optional(),
email: z.string().email("email.valid.message").optional(),
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
message: 'domain.management.edit.dns.access.not.empty.message',
message: "domain.management.edit.dns.access.not.empty.message",
}),
targetAccess: z.string().optional(),
targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, {
message: 'domain.management.edit.target.type.not.empty.message',
message: "domain.management.edit.target.type.not.empty.message",
}),
variables: z.string().optional(),
group: z.string().optional(),
@@ -140,11 +140,11 @@ const Edit = () => {
if (group == "" && targetAccess == "") {
form.setError("group", {
type: "manual",
message: 'domain.management.edit.target.access.verify.msg',
message: "domain.management.edit.target.access.verify.msg",
});
form.setError("targetAccess", {
type: "manual",
message: 'domain.management.edit.target.access.verify.msg',
message: "domain.management.edit.target.access.verify.msg",
});
return;
}
@@ -164,13 +164,13 @@ const Edit = () => {
try {
await save(req);
let description = t('domain.management.edit.succeed.tips');
let description = t("domain.management.edit.succeed.tips");
if (req.id == "") {
description = t('domain.management.add.succeed.tips');
description = t("domain.management.add.succeed.tips");
}
toast({
title: t('succeed'),
title: t("succeed"),
description,
});
navigate("/domains");
@@ -195,7 +195,7 @@ const Edit = () => {
<div className="">
<Toaster />
<div className=" h-5 text-muted-foreground">
{domain?.id ? t('domain.edit') : t('domain.add')}
{domain?.id ? t("domain.edit") : t("domain.add")}
</div>
<div className="mt-5 flex w-full justify-center md:space-x-10 flex-col 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">
@@ -208,7 +208,7 @@ const Edit = () => {
setTab("base");
}}
>
{t('basic.setting')}
{t("basic.setting")}
</div>
<div
className={cn(
@@ -219,7 +219,7 @@ const Edit = () => {
setTab("advance");
}}
>
{t('advanced.setting')}
{t("advanced.setting")}
</div>
</div>
@@ -234,10 +234,15 @@ const Edit = () => {
name="domain"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel>{t('domain')}</FormLabel>
<FormControl>
<Input placeholder={t('domain.not.empty.verify.message')} {...field} />
</FormControl>
<>
<StringList
value={field.value}
valueType="domain"
onValueChange={(domain: string) => {
form.setValue("domain", domain);
}}
/>
</>
<FormMessage />
</FormItem>
@@ -249,12 +254,15 @@ const Edit = () => {
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel className="flex w-full justify-between">
<div>{t('email') + t('domain.management.edit.email.description')}</div>
<div>
{t("email") +
t("domain.management.edit.email.description")}
</div>
<EmailsEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t('add')}
{t("add")}
</div>
}
/>
@@ -268,11 +276,15 @@ const Edit = () => {
}}
>
<SelectTrigger>
<SelectValue placeholder={t('domain.management.edit.email.not.empty.message')} />
<SelectValue
placeholder={t(
"domain.management.edit.email.not.empty.message"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t('email.list')}</SelectLabel>
<SelectLabel>{t("email.list")}</SelectLabel>
{(emails.content as EmailsSetting).emails.map(
(item) => (
<SelectItem key={item} value={item}>
@@ -295,12 +307,14 @@ const Edit = () => {
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel className="flex w-full justify-between">
<div>{t('domain.management.edit.dns.access.label')}</div>
<div>
{t("domain.management.edit.dns.access.label")}
</div>
<AccessEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t('add')}
{t("add")}
</div>
}
op="add"
@@ -315,11 +329,17 @@ const Edit = () => {
}}
>
<SelectTrigger>
<SelectValue placeholder={t('domain.management.edit.access.not.empty.message')} />
<SelectValue
placeholder={t(
"domain.management.edit.access.not.empty.message"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t('domain.management.edit.access.label')}</SelectLabel>
<SelectLabel>
{t("domain.management.edit.access.label")}
</SelectLabel>
{accesses
.filter((item) => item.usage != "deploy")
.map((item) => (
@@ -351,7 +371,9 @@ const Edit = () => {
name="targetType"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel>{t('domain.management.edit.target.type')}</FormLabel>
<FormLabel>
{t("domain.management.edit.target.type")}
</FormLabel>
<FormControl>
<Select
{...field}
@@ -361,11 +383,17 @@ const Edit = () => {
}}
>
<SelectTrigger>
<SelectValue placeholder={t('domain.management.edit.target.type.not.empty.message')} />
<SelectValue
placeholder={t(
"domain.management.edit.target.type.not.empty.message"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t('domain.management.edit.target.type')}</SelectLabel>
<SelectLabel>
{t("domain.management.edit.target.type")}
</SelectLabel>
{targetTypeKeys.map((key) => (
<SelectItem key={key} value={key}>
<div className="flex items-center space-x-2">
@@ -373,7 +401,9 @@ const Edit = () => {
className="w-6"
src={targetTypeMap.get(key)?.[1]}
/>
<div>{t(targetTypeMap.get(key)?.[0] || '')}</div>
<div>
{t(targetTypeMap.get(key)?.[0] || "")}
</div>
</div>
</SelectItem>
))}
@@ -392,12 +422,12 @@ const Edit = () => {
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel className="w-full flex justify-between">
<div>{t('domain.management.edit.target.access')}</div>
<div>{t("domain.management.edit.target.access")}</div>
<AccessEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t('add')}
{t("add")}
</div>
}
op="add"
@@ -411,12 +441,19 @@ const Edit = () => {
}}
>
<SelectTrigger>
<SelectValue placeholder={t('domain.management.edit.target.access.not.empty.message')} />
<SelectValue
placeholder={t(
"domain.management.edit.target.access.not.empty.message"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>
{t('domain.management.edit.target.access.content.label')} {form.getValues().targetAccess}
{t(
"domain.management.edit.target.access.content.label"
)}{" "}
{form.getValues().targetAccess}
</SelectLabel>
<SelectItem value="emptyId">
<div className="flex items-center space-x-2">
@@ -452,9 +489,7 @@ const Edit = () => {
render={({ field }) => (
<FormItem hidden={tab != "advance" || targetType != "ssh"}>
<FormLabel className="w-full flex justify-between">
<div>
{t('domain.management.edit.group.label')}
</div>
<div>{t("domain.management.edit.group.label")}</div>
</FormLabel>
<FormControl>
<Select
@@ -466,7 +501,11 @@ const Edit = () => {
}}
>
<SelectTrigger>
<SelectValue placeholder={t('domain.management.edit.group.not.empty.message')} />
<SelectValue
placeholder={t(
"domain.management.edit.group.not.empty.message"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="emptyId">
@@ -511,10 +550,12 @@ const Edit = () => {
name="variables"
render={({ field }) => (
<FormItem hidden={tab != "advance"}>
<FormLabel>{t('variables')}</FormLabel>
<FormLabel>{t("variables")}</FormLabel>
<FormControl>
<Textarea
placeholder={t('domain.management.edit.variables.placeholder')}
placeholder={t(
"domain.management.edit.variables.placeholder"
)}
{...field}
className="placeholder:whitespace-pre-wrap"
/>
@@ -530,10 +571,12 @@ const Edit = () => {
name="nameservers"
render={({ field }) => (
<FormItem hidden={tab != "advance"}>
<FormLabel>{t('dns')}</FormLabel>
<FormLabel>{t("dns")}</FormLabel>
<FormControl>
<Textarea
placeholder={t('domain.management.edit.dns.placeholder')}
placeholder={t(
"domain.management.edit.dns.placeholder"
)}
{...field}
className="placeholder:whitespace-pre-wrap"
/>
@@ -545,7 +588,7 @@ const Edit = () => {
/>
<div className="flex justify-end">
<Button type="submit">{t('save')}</Button>
<Button type="submit">{t("save")}</Button>
</div>
</form>
</Form>

View File

@@ -41,7 +41,7 @@ const Home = () => {
const toast = useToast();
const navigate = useNavigate();
const { t } = useTranslation()
const { t } = useTranslation();
const location = useLocation();
const query = new URLSearchParams(location.search);
@@ -129,12 +129,12 @@ const Home = () => {
await save(domain);
toast.toast({
title: t('operation.succeed'),
description: t('domain.management.start.deploy.succeed.tips'),
title: t("operation.succeed"),
description: t("domain.management.start.deploy.succeed.tips"),
});
} catch (e) {
toast.toast({
title: t('domain.management.execution.failed'),
title: t("domain.management.execution.failed"),
description: (
// 这里的 text 只是占位作用,实际文案在 src/i18n/locales/[lang].json
<Trans i18nKey="domain.management.execution.failed.tips">
@@ -142,7 +142,9 @@ const Home = () => {
<Link
to={`/history?domain=${domain.id}`}
className="underline text-blue-500"
>text2</Link>
>
text2
</Link>
text3
</Trans>
),
@@ -176,10 +178,10 @@ const Home = () => {
<div className="">
<Toaster />
<div className="flex justify-between items-center">
<div className="text-muted-foreground">{t('domain.management.name')}</div>
<Button onClick={handleCreateClick}>
{t('domain.add')}
</Button>
<div className="text-muted-foreground">
{t("domain.management.name")}
</div>
<Button onClick={handleCreateClick}>{t("domain.add")}</Button>
</div>
{!domains.length ? (
@@ -190,26 +192,32 @@ const Home = () => {
</span>
<div className="text-center text-sm text-muted-foreground mt-3">
{t('domain.management.empty')}
{t("domain.management.empty")}
</div>
<Button onClick={handleCreateClick} className="mt-3">
{t('domain.add')}
{t("domain.add")}
</Button>
</div>
</>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-36">{t('domain')}</div>
<div className="w-40">{t('domain.management.expiry.date')}</div>
<div className="w-32">{t('domain.management.last.execution.status')}</div>
<div className="w-64">{t('domain.management.last.execution.stage')}</div>
<div className="w-40 sm:ml-2">{t('domain.management.last.execution.time')}</div>
<div className="w-24">{t('domain.management.enable')}</div>
<div className="grow">{t('operation')}</div>
<div className="w-36">{t("domain")}</div>
<div className="w-40">{t("domain.management.expiry.date")}</div>
<div className="w-32">
{t("domain.management.last.execution.status")}
</div>
<div className="w-64">
{t("domain.management.last.execution.stage")}
</div>
<div className="w-40 sm:ml-2">
{t("domain.management.last.execution.time")}
</div>
<div className="w-24">{t("domain.management.enable")}</div>
<div className="grow">{t("operation")}</div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
{t('domain')}
{t("domain")}
</div>
{domains.map((domain) => (
@@ -217,15 +225,26 @@ const Home = () => {
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
key={domain.id}
>
<div className="sm:w-36 w-full pt-1 sm:pt-0 flex items-center">
{domain.domain}
<div className="sm:w-36 w-full pt-1 sm:pt-0 flex items-center truncate">
{domain.domain.split(";").map((item) => (
<>
{item}
<br />
</>
))}
</div>
<div className="sm:w-40 w-full pt-1 sm:pt-0 flex items-center">
<div>
{domain.expiredAt ? (
<>
<div>{t('domain.management.expiry.date1', { date: 90 })}</div>
<div>{t('domain.management.expiry.date2', { date: getDate(domain.expiredAt) })}</div>
<div>
{t("domain.management.expiry.date1", { date: 90 })}
</div>
<div>
{t("domain.management.expiry.date2", {
date: getDate(domain.expiredAt),
})}
</div>
</>
) : (
"---"
@@ -269,7 +288,7 @@ const Home = () => {
</TooltipTrigger>
<TooltipContent>
<div className="border rounded-sm px-3 bg-background text-muted-foreground text-xs">
{domain.enabled ? t('disable') : t('enable')}
{domain.enabled ? t("disable") : t("enable")}
</div>
</TooltipContent>
</Tooltip>
@@ -281,7 +300,7 @@ const Home = () => {
className="p-0"
onClick={() => handleHistoryClick(domain.id)}
>
{t('deployment.log.name')}
{t("deployment.log.name")}
</Button>
<Show when={domain.enabled ? true : false}>
<Separator orientation="vertical" className="h-4 mx-2" />
@@ -290,7 +309,7 @@ const Home = () => {
className="p-0"
onClick={() => handleRightNowClick(domain)}
>
{t('domain.management.start.deploying')}
{t("domain.management.start.deploying")}
</Button>
</Show>
@@ -307,7 +326,7 @@ const Home = () => {
className="p-0"
onClick={() => handleForceClick(domain)}
>
{t('domain.management.forced.deployment')}
{t("domain.management.forced.deployment")}
</Button>
</Show>
@@ -318,7 +337,7 @@ const Home = () => {
className="p-0"
onClick={() => handleDownloadClick(domain)}
>
{t('download')}
{t("download")}
</Button>
</Show>
@@ -328,24 +347,26 @@ const Home = () => {
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"link"} className="p-0">
{t('delete')}
{t("delete")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('domain.delete')}</AlertDialogTitle>
<AlertDialogTitle>
{t("domain.delete")}
</AlertDialogTitle>
<AlertDialogDescription>
{t('domain.management.delete.confirm')}
{t("domain.management.delete.confirm")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleDeleteClick(domain.id);
}}
>
{t('confirm')}
{t("confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -357,7 +378,7 @@ const Home = () => {
className="p-0"
onClick={() => handleEditClick(domain.id)}
>
{t('edit')}
{t("edit")}
</Button>
</>
)}

View File

@@ -40,20 +40,17 @@ const History = () => {
return (
<ScrollArea className="h-[80vh] overflow-hidden">
<div className="text-muted-foreground">{t('deployment.log.name')}</div>
<div className="text-muted-foreground">{t("deployment.log.name")}</div>
{!deployments?.length ? (
<>
<Alert className="max-w-[40em] mx-auto mt-20">
<AlertTitle>{t('no.data')}</AlertTitle>
<AlertTitle>{t("no.data")}</AlertTitle>
<AlertDescription>
<div className="flex items-center mt-5">
<div>
<Smile className="text-yellow-400" size={36} />
</div>
<div className="ml-2">
{" "}
{t('deployment.log.empty')}
</div>
<div className="ml-2"> {t("deployment.log.empty")}</div>
</div>
<div className="mt-2 flex justify-end">
<Button
@@ -61,7 +58,7 @@ const History = () => {
navigate("/");
}}
>
{t('domain.add')}
{t("domain.add")}
</Button>
</div>
</AlertDescription>
@@ -70,16 +67,18 @@ const History = () => {
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48">{t('domain')}</div>
<div className="w-48">{t("domain")}</div>
<div className="w-24">{t('deployment.log.status')}</div>
<div className="w-56">{t('deployment.log.stage')}</div>
<div className="w-56 sm:ml-2 text-center">{t('deployment.log.last.execution.time')}</div>
<div className="w-24">{t("deployment.log.status")}</div>
<div className="w-56">{t("deployment.log.stage")}</div>
<div className="w-56 sm:ml-2 text-center">
{t("deployment.log.last.execution.time")}
</div>
<div className="grow">{t('operation')}</div>
<div className="grow">{t("operation")}</div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
{t('deployment.log.name')}
{t("deployment.log.name")}
</div>
{deployments?.map((deployment) => (
@@ -88,7 +87,14 @@ const History = () => {
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center">
{deployment.expand.domain?.domain}
{deployment.expand.domain?.domain
.split(";")
.map((domain: string) => (
<>
{domain}
<br />
</>
))}
</div>
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
<DeployState deployment={deployment} />
@@ -106,14 +112,14 @@ const History = () => {
<Sheet>
<SheetTrigger asChild>
<Button variant={"link"} className="p-0">
{t('deployment.log.detail.button.text')}
{t("deployment.log.detail.button.text")}
</Button>
</SheetTrigger>
<SheetContent className="sm:max-w-5xl">
<SheetHeader>
<SheetTitle>
{deployment.expand.domain?.domain}-{deployment.id}
{t('deployment.log.detail')}
{t("deployment.log.detail")}
</SheetTitle>
</SheetHeader>
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">