Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30ef5841d6 | ||
|
|
217ba85ff8 | ||
|
|
71e2555391 | ||
|
|
f036eb1cf2 | ||
|
|
1347066549 | ||
|
|
7fc149f67d | ||
|
|
dfba5ee638 | ||
|
|
9ba79f996f | ||
|
|
cd85000908 | ||
|
|
995349ab3e | ||
|
|
4fa8031318 | ||
|
|
3f45bb1629 | ||
|
|
0e139e6284 | ||
|
|
82dbfc6de3 | ||
|
|
9b2937d601 | ||
|
|
3375839a40 | ||
|
|
7f5ff6fab5 | ||
|
|
5160b4c3d9 | ||
|
|
85234b21c7 | ||
|
|
223af9e09d | ||
|
|
49fdf8213a | ||
|
|
7a48101015 |
42
README.md
42
README.md
@@ -1,4 +1,4 @@
|
||||
[中文](README.md) | [English](README_EN.md)
|
||||
[中文](README.md) | [English](README_EN.md)
|
||||
|
||||
# 🔒Certimate
|
||||
|
||||
@@ -18,13 +18,8 @@ Certimate 就是为了解决上述问题而产生的,它具有以下特点:
|
||||
* [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
||||
* [域名变量及部署授权组介绍](https://docs.certimate.me/blog/multi-deployer)
|
||||
|
||||
|
||||
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。使用文档请访问[https://docs.certimate.me](https://docs.certimate.me)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 一、安装
|
||||
|
||||
安装 Certimate 非常简单,你可以选择以下方式之一进行安装:
|
||||
@@ -40,12 +35,11 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
|
||||
> [!NOTE]
|
||||
> MacOS 在执行二进制文件时会提示:无法打开“certimate”,因为Apple无法检查其是否包含恶意软件。可在系统设置> 隐私与安全性> 安全性 中点击 "仍然允许",然后再次尝试执行二进制文件。
|
||||
|
||||
|
||||
### 2. Docker 安装
|
||||
|
||||
```bash
|
||||
|
||||
git clone git@github.com:usual2970/certimate.git && cd certimate/docker && docker compose up -d
|
||||
mkdir -p ~/.certimate && cd ~/.certimate && curl -O https://raw.githubusercontent.com/usual2970/certimate/refs/heads/main/docker/docker-compose.yml && docker compose up -d
|
||||
|
||||
```
|
||||
|
||||
@@ -58,7 +52,6 @@ go mod vendor
|
||||
go run main.go serve
|
||||
```
|
||||
|
||||
|
||||
## 二、使用
|
||||
|
||||
执行完上述安装操作后,在浏览器中访问 `http://127.0.0.1:8090` 即可访问 Certimate 管理页面。
|
||||
@@ -72,17 +65,14 @@ go run main.go serve
|
||||
|
||||
## 三、支持的服务商列表
|
||||
|
||||
| 服务商 | 是否域名服务商 | 是否部署服务 | 备注 |
|
||||
|------|------|-----|------|
|
||||
| 阿里云| 是 | 是 | 支持阿里云注册的域名,支持部署到阿里云 CDN,OSS |
|
||||
| 腾讯云| 是 | 是 | 支持腾讯云注册的域名,支持部署到腾讯云 CDN |
|
||||
| 七牛云| 否 | 是 | 七牛云没有注册域名服务,支持部署到七牛云 CDN |
|
||||
|CloudFlare| 是 | 否 | 支持 CloudFlare 注册的域名,CloudFlare 服务自带SSL证书 |
|
||||
|SSH| 否 | 是 | 支持部署到 SSH 服务器 |
|
||||
|WEBHOOK| 否 | 是 | 支持回调到 WEBHOOK |
|
||||
|
||||
|
||||
|
||||
| 服务商 | 是否域名服务商 | 是否部署服务 | 备注 |
|
||||
| ---------- | -------------- | ------------ | ------------------------------------------------------ |
|
||||
| 阿里云 | 是 | 是 | 支持阿里云注册的域名,支持部署到阿里云 CDN,OSS |
|
||||
| 腾讯云 | 是 | 是 | 支持腾讯云注册的域名,支持部署到腾讯云 CDN |
|
||||
| 七牛云 | 否 | 是 | 七牛云没有注册域名服务,支持部署到七牛云 CDN |
|
||||
| CloudFlare | 是 | 否 | 支持 CloudFlare 注册的域名,CloudFlare 服务自带SSL证书 |
|
||||
| SSH | 否 | 是 | 支持部署到 SSH 服务器 |
|
||||
| WEBHOOK | 否 | 是 | 支持回调到 WEBHOOK |
|
||||
|
||||
## 四、系统截图
|
||||
|
||||
@@ -96,7 +86,6 @@ go run main.go serve
|
||||
|
||||

|
||||
|
||||
|
||||
## 五、概念
|
||||
|
||||
Certimate 的工作流程如下:
|
||||
@@ -140,7 +129,6 @@ Certimate 申请证书后,会自动将证书部署到你指定的目标上,
|
||||
|
||||
## 六、常见问题
|
||||
|
||||
|
||||
Q: 提供saas服务吗?
|
||||
|
||||
> A: 不提供,目前仅支持self-hosted(私有部署)。
|
||||
@@ -153,8 +141,6 @@ Q: 自动续期证书?
|
||||
|
||||
> A: 已经申请的证书会在过期前10天自动续期。每天会检查一次证书是否快要过期,快要过期时会自动重新申请证书并部署到目标服务上。
|
||||
|
||||
|
||||
|
||||
## 七、贡献
|
||||
|
||||
Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.md)。你可以使用它做任何你想做的事,甚至把它当作一个付费服务提供给用户。
|
||||
@@ -168,8 +154,10 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.
|
||||
|
||||
## 八、加入社区
|
||||
|
||||
* [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
|
||||
* [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
|
||||
* 微信群聊(超200人需邀请入群,可先加作者好友)
|
||||
|
||||
* 微信群聊
|
||||
<img src="https://i.imgur.com/8xwsLTA.png" width="400"/>
|
||||
|
||||
<img src="https://i.imgur.com/zSHEoIm.png" width="400"/>
|
||||
## 九、Star History
|
||||
[](https://starchart.cc/usual2970/certimate)
|
||||
|
||||
45
README_EN.md
45
README_EN.md
@@ -1,10 +1,9 @@
|
||||
[中文](README.md) | [English](README_EN.md)
|
||||
[中文](README.md) | [English](README_EN.md)
|
||||
|
||||
# 🔒Certimate
|
||||
|
||||
For individuals managing personal projects or those responsible for IT operations in small businesses who need to manage multiple domain names, applying for certificates manually comes with several drawbacks:
|
||||
|
||||
|
||||
1. 😱Troublesome: Applying for and deploying certificates isn’t difficult, but it can be quite a hassle, especially when managing multiple domains.
|
||||
2. 😭Easily forgotten: The current free certificate has a validity period of only 90 days, requiring regular renewal operations. This increases the workload and makes it easy to forget, which can result in the website becoming inaccessible.
|
||||
|
||||
@@ -19,20 +18,14 @@ Related articles:
|
||||
* [Why Certimate?](https://docs.certimate.me/blog/why-certimate)
|
||||
* [Introduction to Domain Variables and Deployment Authorization Groups](https://docs.certimate.me/blog/multi-deployer)
|
||||
|
||||
|
||||
Certimate aims to provide users with a secure and user-friendly SSL certificate management solution. For usage documentation, please visit.[https://docs.certimate.me](https://docs.certimate.me)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Installing Certimate is very simple, you can choose one of the following methods for installation:
|
||||
|
||||
### 1. Binary File
|
||||
|
||||
|
||||
You can download the precompiled binary files directly from the [Releases page](https://github.com/usual2970/certimate/releases), and after extracting them, execute:
|
||||
|
||||
```bash
|
||||
@@ -42,12 +35,11 @@ You can download the precompiled binary files directly from the [Releases page](
|
||||
> [!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.
|
||||
|
||||
|
||||
### 2. Docker Installation
|
||||
|
||||
```bash
|
||||
|
||||
git clone git@github.com:usual2970/certimate.git && cd certimate/docker && docker compose up -d
|
||||
mkdir -p ~/.certimate && cd ~/.certimate && curl -O https://raw.githubusercontent.com/usual2970/certimate/refs/heads/main/docker/docker-compose.yml && docker compose up -d
|
||||
|
||||
```
|
||||
|
||||
@@ -60,7 +52,6 @@ go mod vendor
|
||||
go run main.go serve
|
||||
```
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
After completing the installation steps above, you can access the Certimate management page by visiting http://127.0.0.1:8090 in your browser.
|
||||
@@ -74,19 +65,16 @@ password:1234567890
|
||||
|
||||
## List of Supported Providers
|
||||
|
||||
| Provider | Domain Registrar | Deployment Service | Remarks |
|
||||
| ------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------- |
|
||||
| Alibaba Cloud | Yes | Yes | Supports domains registered with Alibaba Cloud; supports deployment to Alibaba Cloud CDN and OSS. |
|
||||
| Tencent Cloud | Yes | Yes | Supports domains registered with Tencent Cloud; supports deployment to Tencent Cloud CDN. |
|
||||
| Qiniu Cloud | No | Yes | Qiniu Cloud does not offer domain registration services; supports deployment to Qiniu Cloud CDN. |
|
||||
| Cloudflare | Yes | No | Supports domains registered with Cloudflare; Cloudflare services come with SSL certificates. |
|
||||
| SSH | No | Yes | Supports deployment to SSH servers. |
|
||||
| WEBHOOK | No | Yes | Supports callbacks to WEBHOOK. |
|
||||
|
||||
| Provider | Domain Registrar | Deployment Service | Remarks |
|
||||
|--------------|------------------|--------------------|------------------------------------------------------|
|
||||
| Alibaba Cloud| Yes | Yes | Supports domains registered with Alibaba Cloud; supports deployment to Alibaba Cloud CDN and OSS. |
|
||||
| Tencent Cloud| Yes | Yes | Supports domains registered with Tencent Cloud; supports deployment to Tencent Cloud CDN. |
|
||||
| Qiniu Cloud | No | Yes | Qiniu Cloud does not offer domain registration services; supports deployment to Qiniu Cloud CDN. |
|
||||
| Cloudflare | Yes | No | Supports domains registered with Cloudflare; Cloudflare services come with SSL certificates. |
|
||||
| SSH | No | Yes | Supports deployment to SSH servers. |
|
||||
| WEBHOOK | No | Yes | Supports callbacks to WEBHOOK. |
|
||||
|
||||
|
||||
|
||||
## Screenshots
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||
@@ -98,7 +86,6 @@ password:1234567890
|
||||
|
||||

|
||||
|
||||
|
||||
## Concepts
|
||||
|
||||
The workflow of Certimate is as follows:
|
||||
@@ -140,8 +127,7 @@ After Certimate applies for the certificate, it will automatically deploy the ce
|
||||
|
||||
The authorization information for the deployment service provider is the same as that for the DNS provider, with the distinction that the DNS provider's authorization information is used to prove that the domain belongs to you, while the deployment service provider's authorization information is used to provide authorization for the certificate deployment.
|
||||
|
||||
## FAQ
|
||||
|
||||
## FAQ
|
||||
|
||||
Q: Do you provide SaaS services?
|
||||
|
||||
@@ -155,8 +141,6 @@ Q: Automatic Certificate Renewal?
|
||||
|
||||
> A: Certificates that have already been issued will be automatically renewed 10 days before expiration. The system checks once a day to see if any certificates are nearing expiration, and if so, it will automatically reapply for the certificate and deploy it to the target service.
|
||||
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Certimate is a free and open-source project, licensed under the [MIT License](LICENSE.md). You can use it for anything you want, even offering it as a paid service to users.
|
||||
@@ -170,8 +154,7 @@ Support for more service providers, UI enhancements, bug fixes, and documentatio
|
||||
|
||||
## Join the Community
|
||||
|
||||
* [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
|
||||
|
||||
* Wechat Group
|
||||
* [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
|
||||
* Wechat Group
|
||||
|
||||
<img src="https://i.imgur.com/zSHEoIm.png" width="400"/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -71,7 +71,7 @@ func (s *ssh) Deploy(ctx context.Context) error {
|
||||
|
||||
// 执行前置命令
|
||||
if access.PreCommand != "" {
|
||||
err, stdout, stderr := s.sshExecCommand(client, access.Command)
|
||||
err, stdout, stderr := s.sshExecCommand(client, access.PreCommand)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
||||
}
|
||||
|
||||
@@ -31,11 +31,12 @@ func Send(title, content string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
n := notifyPackage.New()
|
||||
// 添加推送渠道
|
||||
notifyPackage.UseServices(notifiers...)
|
||||
n.UseServices(notifiers...)
|
||||
|
||||
// 发送消息
|
||||
return notifyPackage.Send(context.Background(), title, content)
|
||||
return n.Send(context.Background(), title, content)
|
||||
}
|
||||
|
||||
func getNotifiers() ([]notifyPackage.Notifier, error) {
|
||||
|
||||
332
ui/dist/assets/index-8E76olt7.js
vendored
Normal file
332
ui/dist/assets/index-8E76olt7.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ui/dist/assets/index-DOft-CKV.css
vendored
Normal file
1
ui/dist/assets/index-DOft-CKV.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ui/dist/assets/index-I--T0qY3.css
vendored
1
ui/dist/assets/index-I--T0qY3.css
vendored
File diff suppressed because one or more lines are too long
322
ui/dist/assets/index-TzNEc_kS.js
vendored
322
ui/dist/assets/index-TzNEc_kS.js
vendored
File diff suppressed because one or more lines are too long
4
ui/dist/index.html
vendored
4
ui/dist/index.html
vendored
@@ -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-TzNEc_kS.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-I--T0qY3.css">
|
||||
<script type="module" crossorigin src="/assets/index-8E76olt7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DOft-CKV.css">
|
||||
</head>
|
||||
<body class="bg-background">
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -24,9 +24,11 @@ import { PbErrorData } from "@/domain/base";
|
||||
|
||||
const AccessAliyunForm = ({
|
||||
data,
|
||||
op,
|
||||
onAfterReq,
|
||||
}: {
|
||||
data?: Access;
|
||||
op: "add" | "edit" | "copy";
|
||||
onAfterReq: () => void;
|
||||
}) => {
|
||||
const { addAccess, updateAccess } = useConfig();
|
||||
@@ -69,6 +71,7 @@ const AccessAliyunForm = ({
|
||||
};
|
||||
|
||||
try {
|
||||
req.id = op == "copy" ? "" : req.id;
|
||||
const rs = await save(req);
|
||||
|
||||
onAfterReq();
|
||||
@@ -76,10 +79,11 @@ const AccessAliyunForm = ({
|
||||
req.id = rs.id;
|
||||
req.created = rs.created;
|
||||
req.updated = rs.updated;
|
||||
if (data.id) {
|
||||
if (data.id && op == "edit") {
|
||||
updateAccess(req);
|
||||
return;
|
||||
}
|
||||
console.log(req);
|
||||
addAccess(req);
|
||||
} catch (e) {
|
||||
const err = e as ClientResponseError;
|
||||
|
||||
@@ -23,9 +23,11 @@ import { PbErrorData } from "@/domain/base";
|
||||
|
||||
const AccessCloudflareForm = ({
|
||||
data,
|
||||
op,
|
||||
onAfterReq,
|
||||
}: {
|
||||
data?: Access;
|
||||
op: "add" | "edit" | "copy";
|
||||
onAfterReq: () => void;
|
||||
}) => {
|
||||
const { addAccess, updateAccess } = useConfig();
|
||||
@@ -65,6 +67,7 @@ const AccessCloudflareForm = ({
|
||||
};
|
||||
|
||||
try {
|
||||
req.id = op == "copy" ? "" : req.id;
|
||||
const rs = await save(req);
|
||||
|
||||
onAfterReq();
|
||||
@@ -72,7 +75,7 @@ const AccessCloudflareForm = ({
|
||||
req.id = rs.id;
|
||||
req.created = rs.created;
|
||||
req.updated = rs.updated;
|
||||
if (data.id) {
|
||||
if (data.id && op == "edit") {
|
||||
updateAccess(req);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ import AccessGodaddyFrom from "./AccessGodaddyForm";
|
||||
import AccessLocalForm from "./AccessLocalForm";
|
||||
|
||||
type TargetConfigEditProps = {
|
||||
op: "add" | "edit";
|
||||
op: "add" | "edit" | "copy";
|
||||
className?: string;
|
||||
trigger: React.ReactNode;
|
||||
data?: Access;
|
||||
@@ -60,6 +60,7 @@ export function AccessEdit({
|
||||
form = (
|
||||
<AccessTencentForm
|
||||
data={data}
|
||||
op={op}
|
||||
onAfterReq={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
@@ -70,6 +71,7 @@ export function AccessEdit({
|
||||
form = (
|
||||
<AccessAliyunForm
|
||||
data={data}
|
||||
op={op}
|
||||
onAfterReq={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
@@ -80,6 +82,7 @@ export function AccessEdit({
|
||||
form = (
|
||||
<AccessSSHForm
|
||||
data={data}
|
||||
op={op}
|
||||
onAfterReq={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
@@ -90,6 +93,7 @@ export function AccessEdit({
|
||||
form = (
|
||||
<WebhookForm
|
||||
data={data}
|
||||
op={op}
|
||||
onAfterReq={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
@@ -100,6 +104,7 @@ export function AccessEdit({
|
||||
form = (
|
||||
<AccessCloudflareForm
|
||||
data={data}
|
||||
op={op}
|
||||
onAfterReq={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
@@ -110,6 +115,7 @@ export function AccessEdit({
|
||||
form = (
|
||||
<AccessQiniuForm
|
||||
data={data}
|
||||
op={op}
|
||||
onAfterReq={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
@@ -120,6 +126,7 @@ export function AccessEdit({
|
||||
form = (
|
||||
<AccessNamesiloForm
|
||||
data={data}
|
||||
op={op}
|
||||
onAfterReq={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
@@ -130,6 +137,7 @@ export function AccessEdit({
|
||||
form = (
|
||||
<AccessGodaddyFrom
|
||||
data={data}
|
||||
op={op}
|
||||
onAfterReq={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
@@ -140,6 +148,7 @@ export function AccessEdit({
|
||||
form = (
|
||||
<AccessLocalForm
|
||||
data={data}
|
||||
op={op}
|
||||
onAfterReq={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
@@ -159,7 +168,7 @@ export function AccessEdit({
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[600px] w-full dark:text-stone-200">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{op == "add" ? t('access.add') : t('access.edit')}</DialogTitle>
|
||||
<DialogTitle>{op == "add" ? t('access.add') : op == "edit" ? t('access.edit') : t('access.copy')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[80vh]">
|
||||
<div className="container py-3">
|
||||
|
||||
@@ -28,9 +28,11 @@ import { PbErrorData } from "@/domain/base";
|
||||
|
||||
const AccessGodaddyFrom = ({
|
||||
data,
|
||||
op,
|
||||
onAfterReq,
|
||||
}: {
|
||||
data?: Access;
|
||||
op: "add" | "edit" | "copy";
|
||||
onAfterReq: () => void;
|
||||
}) => {
|
||||
const { addAccess, updateAccess } = useConfig();
|
||||
@@ -74,6 +76,7 @@ const AccessGodaddyFrom = ({
|
||||
};
|
||||
|
||||
try {
|
||||
req.id = op == "copy" ? "" : req.id;
|
||||
const rs = await save(req);
|
||||
|
||||
onAfterReq();
|
||||
@@ -81,7 +84,7 @@ const AccessGodaddyFrom = ({
|
||||
req.id = rs.id;
|
||||
req.created = rs.created;
|
||||
req.updated = rs.updated;
|
||||
if (data.id) {
|
||||
if (data.id && op == "edit") {
|
||||
updateAccess(req);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useConfig } from "@/providers/config";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -26,21 +27,24 @@ import { PbErrorData } from "@/domain/base";
|
||||
|
||||
const AccessLocalForm = ({
|
||||
data,
|
||||
op,
|
||||
onAfterReq,
|
||||
}: {
|
||||
data?: Access;
|
||||
op: "add" | "edit" | "copy";
|
||||
onAfterReq: () => void;
|
||||
}) => {
|
||||
const { addAccess, updateAccess, reloadAccessGroups } = useConfig();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().min(1).max(64),
|
||||
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
|
||||
configType: accessFormType,
|
||||
|
||||
command: z.string().min(1).max(2048),
|
||||
certPath: z.string().min(0).max(2048),
|
||||
keyPath: z.string().min(0).max(2048),
|
||||
command: z.string().min(1, 'access.form.ssh.command.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
||||
certPath: z.string().min(0, 'access.form.ssh.cert.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
||||
keyPath: z.string().min(0, 'access.form.ssh.key.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
||||
});
|
||||
|
||||
let config: LocalConfig = {
|
||||
@@ -54,7 +58,7 @@ const AccessLocalForm = ({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
id: data?.id,
|
||||
name: data?.name,
|
||||
name: data?.name || '',
|
||||
configType: "local",
|
||||
certPath: config.certPath,
|
||||
keyPath: config.keyPath,
|
||||
@@ -77,6 +81,7 @@ const AccessLocalForm = ({
|
||||
};
|
||||
|
||||
try {
|
||||
req.id = op == "copy" ? "" : req.id;
|
||||
const rs = await save(req);
|
||||
|
||||
onAfterReq();
|
||||
@@ -84,7 +89,7 @@ const AccessLocalForm = ({
|
||||
req.id = rs.id;
|
||||
req.created = rs.created;
|
||||
req.updated = rs.updated;
|
||||
if (data.id) {
|
||||
if (data.id && op == "edit") {
|
||||
updateAccess(req);
|
||||
} else {
|
||||
addAccess(req);
|
||||
@@ -123,9 +128,9 @@ const AccessLocalForm = ({
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>名称</FormLabel>
|
||||
<FormLabel>{t('name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="请输入授权名称" {...field} />
|
||||
<Input placeholder={t('access.form.name.not.empty')} {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@@ -138,7 +143,7 @@ const AccessLocalForm = ({
|
||||
name="id"
|
||||
render={({ field }) => (
|
||||
<FormItem className="hidden">
|
||||
<FormLabel>配置类型</FormLabel>
|
||||
<FormLabel>{t('access.form.config.field')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@@ -153,7 +158,7 @@ const AccessLocalForm = ({
|
||||
name="configType"
|
||||
render={({ field }) => (
|
||||
<FormItem className="hidden">
|
||||
<FormLabel>配置类型</FormLabel>
|
||||
<FormLabel>{t('access.form.config.field')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@@ -168,9 +173,9 @@ const AccessLocalForm = ({
|
||||
name="certPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>证书保存路径</FormLabel>
|
||||
<FormLabel>{t('access.form.ssh.cert.path')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="请输入证书上传路径" {...field} />
|
||||
<Input placeholder={t('access.form.ssh.cert.path.not.empty')} {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@@ -183,9 +188,9 @@ const AccessLocalForm = ({
|
||||
name="keyPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>私钥保存路径</FormLabel>
|
||||
<FormLabel>{t('access.form.ssh.key.path')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="请输入私钥上传路径" {...field} />
|
||||
<Input placeholder={t('access.form.ssh.key.path.not.empty')} {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@@ -198,9 +203,9 @@ const AccessLocalForm = ({
|
||||
name="command"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Command</FormLabel>
|
||||
<FormLabel>{t('access.form.ssh.command')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="请输入要执行的命令" {...field} />
|
||||
<Textarea placeholder={t('access.form.ssh.command.not.empty')} {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@@ -211,7 +216,7 @@ const AccessLocalForm = ({
|
||||
<FormMessage />
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">保存</Button>
|
||||
<Button type="submit">{t('save')}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -23,9 +23,11 @@ import { PbErrorData } from "@/domain/base";
|
||||
|
||||
const AccessNamesiloForm = ({
|
||||
data,
|
||||
op,
|
||||
onAfterReq,
|
||||
}: {
|
||||
data?: Access;
|
||||
op: "add" | "edit" | "copy";
|
||||
onAfterReq: () => void;
|
||||
}) => {
|
||||
const { addAccess, updateAccess } = useConfig();
|
||||
@@ -64,6 +66,7 @@ const AccessNamesiloForm = ({
|
||||
};
|
||||
|
||||
try {
|
||||
req.id = op == "copy" ? "" : req.id;
|
||||
const rs = await save(req);
|
||||
|
||||
onAfterReq();
|
||||
@@ -71,7 +74,7 @@ const AccessNamesiloForm = ({
|
||||
req.id = rs.id;
|
||||
req.created = rs.created;
|
||||
req.updated = rs.updated;
|
||||
if (data.id) {
|
||||
if (data.id && op == "edit") {
|
||||
updateAccess(req);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@ import { PbErrorData } from "@/domain/base";
|
||||
|
||||
const AccessQiniuForm = ({
|
||||
data,
|
||||
op,
|
||||
onAfterReq,
|
||||
}: {
|
||||
data?: Access;
|
||||
op: "add" | "edit" | "copy";
|
||||
onAfterReq: () => void;
|
||||
}) => {
|
||||
const { addAccess, updateAccess } = useConfig();
|
||||
@@ -69,6 +71,7 @@ const AccessQiniuForm = ({
|
||||
};
|
||||
|
||||
try {
|
||||
req.id = op == "copy" ? "" : req.id;
|
||||
const rs = await save(req);
|
||||
|
||||
onAfterReq();
|
||||
@@ -76,7 +79,7 @@ const AccessQiniuForm = ({
|
||||
req.id = rs.id;
|
||||
req.created = rs.created;
|
||||
req.updated = rs.updated;
|
||||
if (data.id) {
|
||||
if (data.id && op == "edit") {
|
||||
updateAccess(req);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -39,9 +39,11 @@ import { updateById } from "@/repository/access_group";
|
||||
|
||||
const AccessSSHForm = ({
|
||||
data,
|
||||
op,
|
||||
onAfterReq,
|
||||
}: {
|
||||
data?: Access;
|
||||
op: "add" | "edit" | "copy";
|
||||
onAfterReq: () => void;
|
||||
}) => {
|
||||
const {
|
||||
@@ -80,8 +82,8 @@ const AccessSSHForm = ({
|
||||
password: z.string().min(0, 'password.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
|
||||
key: z.string().min(0, 'access.form.ssh.key.not.empty').max(20480, t('zod.rule.string.max', { max: 20480 })),
|
||||
keyFile: z.any().optional(),
|
||||
|
||||
preCommand: z.string().min(0).max(2048).optional(),
|
||||
|
||||
preCommand: z.string().min(0).max(2048, t('zod.rule.string.max', { max: 2048 })).optional(),
|
||||
command: z.string().min(1, 'access.form.ssh.command.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
||||
certPath: z.string().min(0, 'access.form.ssh.cert.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
||||
keyPath: z.string().min(0, 'access.form.ssh.key.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
|
||||
@@ -122,7 +124,6 @@ const AccessSSHForm = ({
|
||||
});
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||
console.log(data);
|
||||
let group = data.group;
|
||||
if (group == "emptyId") group = "";
|
||||
|
||||
@@ -146,6 +147,7 @@ const AccessSSHForm = ({
|
||||
};
|
||||
|
||||
try {
|
||||
req.id = op == "copy" ? "" : req.id;
|
||||
const rs = await save(req);
|
||||
|
||||
onAfterReq();
|
||||
@@ -153,7 +155,7 @@ const AccessSSHForm = ({
|
||||
req.id = rs.id;
|
||||
req.created = rs.created;
|
||||
req.updated = rs.updated;
|
||||
if (data.id) {
|
||||
if (data.id && op == "edit") {
|
||||
updateAccess(req);
|
||||
} else {
|
||||
addAccess(req);
|
||||
@@ -480,9 +482,9 @@ const AccessSSHForm = ({
|
||||
name="preCommand"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>前置 Command</FormLabel>
|
||||
<FormLabel>{t('access.form.ssh.pre.command')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="请输入要在部署证书前执行的前置命令" {...field} />
|
||||
<Textarea placeholder={t('access.form.ssh.pre.command.not.empty')} {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -23,9 +23,11 @@ import { PbErrorData } from "@/domain/base";
|
||||
|
||||
const AccessTencentForm = ({
|
||||
data,
|
||||
op,
|
||||
onAfterReq,
|
||||
}: {
|
||||
data?: Access;
|
||||
op: "add" | "edit" | "copy";
|
||||
onAfterReq: () => void;
|
||||
}) => {
|
||||
const { addAccess, updateAccess } = useConfig();
|
||||
@@ -68,6 +70,7 @@ const AccessTencentForm = ({
|
||||
};
|
||||
|
||||
try {
|
||||
req.id = op == "copy" ? "" : req.id;
|
||||
const rs = await save(req);
|
||||
|
||||
onAfterReq();
|
||||
@@ -75,7 +78,7 @@ const AccessTencentForm = ({
|
||||
req.id = rs.id;
|
||||
req.created = rs.created;
|
||||
req.updated = rs.updated;
|
||||
if (data.id) {
|
||||
if (data.id && op == "edit") {
|
||||
updateAccess(req);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -23,9 +23,11 @@ import { PbErrorData } from "@/domain/base";
|
||||
|
||||
const WebhookForm = ({
|
||||
data,
|
||||
op,
|
||||
onAfterReq,
|
||||
}: {
|
||||
data?: Access;
|
||||
op: "add" | "edit" | "copy";
|
||||
onAfterReq: () => void;
|
||||
}) => {
|
||||
const { addAccess, updateAccess } = useConfig();
|
||||
@@ -64,6 +66,7 @@ const WebhookForm = ({
|
||||
};
|
||||
|
||||
try {
|
||||
req.id = op == "copy" ? "" : req.id;
|
||||
const rs = await save(req);
|
||||
|
||||
onAfterReq();
|
||||
@@ -71,7 +74,7 @@ const WebhookForm = ({
|
||||
req.id = rs.id;
|
||||
req.created = rs.created;
|
||||
req.updated = rs.updated;
|
||||
if (data.id) {
|
||||
if (data.id && op == "edit") {
|
||||
updateAccess(req);
|
||||
return;
|
||||
}
|
||||
|
||||
245
ui/src/components/certimate/StringList.tsx
Normal file
245
ui/src/components/certimate/StringList.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -9,7 +9,7 @@ export const accessTypeMap: Map<string, [string, string]> = new Map([
|
||||
["qiniu", ["qiniu", "/imgs/providers/qiniu.svg"]],
|
||||
["ssh", ["ssh", "/imgs/providers/ssh.svg"]],
|
||||
["webhook", ["webhook", "/imgs/providers/webhook.svg"]],
|
||||
["local", ["local", "/imgs/providers/local.svg"]],
|
||||
["local", ["local.deployment", "/imgs/providers/local.svg"]],
|
||||
]);
|
||||
|
||||
export const getProviderInfo = (t: string) => {
|
||||
|
||||
@@ -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 = {
|
||||
@@ -47,7 +63,7 @@ export const targetTypeMap: Map<string, [string, string]> = new Map([
|
||||
["ssh", ["ssh", "/imgs/providers/ssh.svg"]],
|
||||
["qiniu-cdn", ["qiniu.cdn", "/imgs/providers/qiniu.svg"]],
|
||||
["webhook", ["webhook", "/imgs/providers/webhook.svg"]],
|
||||
["local", ["local", "/imgs/providers/local.svg"]],
|
||||
["local", ["local.deployment", "/imgs/providers/local.svg"]],
|
||||
]);
|
||||
|
||||
export const targetTypeKeys = Array.from(targetTypeMap.keys());
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const version = "Certimate v0.1.12";
|
||||
export const version = "Certimate v0.1.17";
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"edit": "Edit",
|
||||
"copy": "Copy",
|
||||
"succeed": "Successful",
|
||||
"add": "Add",
|
||||
"document": "Document",
|
||||
@@ -42,7 +43,7 @@
|
||||
"ding.talk": "Ding Talk",
|
||||
"telegram": "Telegram",
|
||||
"webhook": "Webhook",
|
||||
"local": "Local Deployment",
|
||||
"local.deployment": "Local Deployment",
|
||||
"tencent": "Tencent",
|
||||
"tencent.cdn": "Tencent-CDN",
|
||||
"aliyun": "Alibaba Cloud",
|
||||
@@ -82,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",
|
||||
@@ -154,6 +156,8 @@
|
||||
"access.management": "Authorization Management",
|
||||
"access.add": "Add Authorization",
|
||||
"access.edit": "Edit Authorization",
|
||||
"access.copy": "Copy Authorization",
|
||||
"access.delete.confirm": "Are you sure you want to delete the deployment authorization?",
|
||||
"access.all": "All Authorizations",
|
||||
"access.list": "Authorization List",
|
||||
"access.type": "Provider",
|
||||
@@ -200,10 +204,12 @@
|
||||
"access.form.ssh.key": "Key (Log in using certificate)",
|
||||
"access.form.ssh.key.not.empty": "Please enter Key",
|
||||
"access.form.ssh.key.file.not.empty": "Please select file",
|
||||
"access.form.ssh.cert.path": "Certificate Upload Path",
|
||||
"access.form.ssh.cert.path.not.empty": "Please enter certificate upload path",
|
||||
"access.form.ssh.key.path": "Private Key Upload Path",
|
||||
"access.form.ssh.key.path.not.empty": "Please enter private key upload path",
|
||||
"access.form.ssh.cert.path": "Certificate Save Path",
|
||||
"access.form.ssh.cert.path.not.empty": "Please enter certificate save path",
|
||||
"access.form.ssh.key.path": "Private Key Save Path",
|
||||
"access.form.ssh.key.path.not.empty": "Please enter private key save path",
|
||||
"access.form.ssh.pre.command": "Pre-deployment Command",
|
||||
"access.form.ssh.pre.command.not.empty": "Command to be executed before deploying the certificate",
|
||||
"access.form.ssh.command": "Command",
|
||||
"access.form.ssh.command.not.empty": "Please enter command",
|
||||
"access.form.ding.access.token.placeholder": "Signature for signed addition"
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
"edit": "编辑",
|
||||
"copy": "复制",
|
||||
"succeed": "成功",
|
||||
"add": "新增",
|
||||
"document": "文档",
|
||||
@@ -42,7 +43,7 @@
|
||||
"ding.talk": "钉钉",
|
||||
"telegram": "Telegram",
|
||||
"webhook": "Webhook",
|
||||
"local": "本地部署",
|
||||
"local.deployment": "本地部署",
|
||||
"tencent": "腾讯云",
|
||||
"tencent.cdn": "腾讯云-CDN",
|
||||
"aliyun": "阿里云",
|
||||
@@ -82,6 +83,7 @@
|
||||
"pagination.prev": "上一页",
|
||||
"domain": "域名",
|
||||
"domain.add": "新增域名",
|
||||
"domain.edit": "编辑域名",
|
||||
"domain.delete": "删除域名",
|
||||
"domain.not.empty.verify.message": "请输入域名",
|
||||
"domain.management.name": "域名列表",
|
||||
@@ -154,6 +156,8 @@
|
||||
"access.management": "授权管理",
|
||||
"access.add": "添加授权",
|
||||
"access.edit": "编辑授权",
|
||||
"access.copy": "复制授权",
|
||||
"access.delete.confirm": "确定要删除授权吗?",
|
||||
"access.all": "所有授权",
|
||||
"access.list": "授权列表",
|
||||
"access.type": "服务商",
|
||||
@@ -200,10 +204,12 @@
|
||||
"access.form.ssh.key": "Key(使用证书登录)",
|
||||
"access.form.ssh.key.not.empty": "请输入 Key",
|
||||
"access.form.ssh.key.file.not.empty": "请选择文件",
|
||||
"access.form.ssh.cert.path": "证书上传路径",
|
||||
"access.form.ssh.cert.path.not.empty": "请输入证书上传路径",
|
||||
"access.form.ssh.key.path": "私钥上传路径",
|
||||
"access.form.ssh.key.path.not.empty": "请输入私钥上传路径",
|
||||
"access.form.ssh.cert.path": "证书保存路径",
|
||||
"access.form.ssh.cert.path.not.empty": "请输入证书保存路径",
|
||||
"access.form.ssh.key.path": "私钥保存路径",
|
||||
"access.form.ssh.key.path.not.empty": "请输入私钥保存路径",
|
||||
"access.form.ssh.pre.command": "前置 Command",
|
||||
"access.form.ssh.pre.command.not.empty": "在部署证书前执行的前置命令",
|
||||
"access.form.ssh.command": "Command",
|
||||
"access.form.ssh.command.not.empty": "请输入要执行的命令",
|
||||
"access.form.ding.access.token.placeholder": "加签的签名"
|
||||
|
||||
@@ -12,6 +12,15 @@ import { remove } from "@/repository/access";
|
||||
import { t } from "i18next";
|
||||
import { Key } from "lucide-react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent, AlertDialogDescription, AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger
|
||||
} from "@/components/ui/alert-dialog.tsx";
|
||||
|
||||
const Access = () => {
|
||||
const { config, deleteAccess } = useConfig();
|
||||
@@ -149,15 +158,45 @@ const Access = () => {
|
||||
data={access}
|
||||
/>
|
||||
<Separator orientation="vertical" className="h-4 mx-2" />
|
||||
<Button
|
||||
variant={"link"}
|
||||
className="p-0"
|
||||
onClick={() => {
|
||||
handleDelete(access);
|
||||
}}
|
||||
>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
<AccessEdit
|
||||
trigger={
|
||||
<Button variant={"link"} className="p-0">
|
||||
{t("copy")}
|
||||
</Button>
|
||||
}
|
||||
op="copy"
|
||||
data={access}
|
||||
/>
|
||||
<Separator orientation="vertical" className="h-4 mx-2" />
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant={"link"} size={"sm"}>
|
||||
{t('delete')}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="dark:text-gray-200">
|
||||
{t('access.group.delete')}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t('access.delete.confirm')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="dark:text-gray-200">
|
||||
{t('cancel')}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
handleDelete(access);
|
||||
}}
|
||||
>
|
||||
{t('confirm')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -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]">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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]">
|
||||
|
||||
Reference in New Issue
Block a user