Compare commits

...

13 Commits

Author SHA1 Message Date
usual2970
2cca82eb95 Merge pull request #61 from usual2970/feat/settings
Feat/settings
2024-09-21 06:38:04 +08:00
yoan
30beee6027 support email modification 2024-09-21 06:37:11 +08:00
yoan
b649348162 Merge branch 'main' into feat/settings 2024-09-21 06:35:16 +08:00
yoan
b912c5e688 support email update 2024-09-21 06:34:32 +08:00
usual2970
5981200df2 Merge pull request #59 from minibear2021/patch-1
Update README.md
2024-09-21 06:25:23 +08:00
Chen Gang
f9e7bfd606 Update README.md 2024-09-20 17:37:28 +08:00
yoan
7f6549bdf3 add wechat group 2024-09-20 10:26:33 +08:00
yoan
2af26dbfe0 add community 2024-09-19 18:06:18 +08:00
yoan
b7f382e16f add telegram group 2024-09-19 15:57:04 +08:00
yoan
7762955989 add telegram group 2024-09-19 15:52:10 +08:00
yoan
1ab603b506 add telegram group 2024-09-19 15:47:09 +08:00
yoan
b432cbfd3f push image to Dockerhub 2024-09-19 09:55:37 +08:00
yoan
e4d76113f8 push image to Dockerhub 2024-09-19 09:51:19 +08:00
19 changed files with 627 additions and 396 deletions

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 加入频道讨论
url: https://t.me/+ZXphsppxUg41YmVl
about: 加入到电报频道寻求更多帮助

View File

@@ -4,6 +4,12 @@ on:
push:
tags:
- "*"
workflow_dispatch:
inputs:
tag:
description: "Tag version to be used for Docker image"
required: true
default: "v0.1.9"
jobs:
build-and-push:
@@ -19,7 +25,22 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
usual2970/certimate
registry.cn-shanghai.aliyuncs.com/usual2970/certimate
- name: Log in to DOCKERHUB
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Log in to ALIYUNCS
uses: docker/login-action@v3
with:
@@ -27,10 +48,6 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract Git tag
id: get_tag
run: echo "tag=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
@@ -38,6 +55,4 @@ jobs:
file: ./Dockerfile_build
platforms: linux/amd64,linux/arm64
push: true
tags: |
registry.cn-shanghai.aliyuncs.com/usual2970/certimate:${{ env.tag }}
registry.cn-shanghai.aliyuncs.com/usual2970/certimate:latest
tags: ${{ steps.meta.outputs.tags }}

View File

@@ -35,6 +35,7 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
- [3. 部署服务商授权信息](#3-部署服务商授权信息)
- [六、常见问题](#六常见问题)
- [七、贡献](#七贡献)
- [八、加入社区](#八加入社区)
@@ -114,7 +115,7 @@ go run main.go serve
Certimate 的工作流程如下:
* 用户通过 Certimate 管理页面填写申请证书的信息包括域名、dns 服务商的授权信息、以及要部署到的服务商的授权信息。
* Certimate 向证书商的 API 发起申请请求,获取 SSL 证书。
* Certimate 向证书商的 API 发起申请请求,获取 SSL 证书。
* Certimate 存储证书信息,包括证书内容、私钥、证书有效期等,并在证书即将过期时自动续期。
* Certimate 向服务商的 API 发起部署请求,将证书部署到服务商的服务器上。
@@ -178,4 +179,10 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE.
支持更多服务商、UI 的优化改进、BUG 修复、文档完善等,欢迎大家提交 PR。
## 八、加入社区
* [Telegram-a new era of messaging](https://t.me/+ZXphsppxUg41YmVl)
* 微信群聊
<img src="https://i.imgur.com/lJUfTeD.png" width="400"/>

BIN
certimate
View File

Binary file not shown.

303
ui/dist/assets/index-BLKGMHXS.js vendored Normal file
View File

File diff suppressed because one or more lines are too long

1
ui/dist/assets/index-BmYeXvQX.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-liy7dSav.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-ChWRjRip.css">
<script type="module" crossorigin src="/assets/index-BLKGMHXS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BmYeXvQX.css">
</head>
<body class="bg-background">
<div id="root"></div>

View File

@@ -0,0 +1,49 @@
import { Deployment } from "@/domain/deployment";
import { CircleCheck, CircleX } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
type DeployStateProps = {
deployment: Deployment;
};
const DeployState = ({ deployment }: DeployStateProps) => {
// 获取指定阶段的错误信息
const error = (state: "check" | "apply" | "deploy") => {
if (!deployment.log[state]) {
return "";
}
return deployment.log[state][deployment.log[state].length - 1].error;
};
return (
<>
{deployment.phase === "deploy" && deployment.phaseSuccess ? (
<CircleCheck size={16} className="text-green-700" />
) : (
<>
{error(deployment.phase).length ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild className="cursor-pointer">
<CircleX size={16} className="text-red-700" />
</TooltipTrigger>
<TooltipContent className="max-w-[35em]">
{error(deployment.phase)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<CircleX size={16} className="text-red-700" />
)}
</>
)}
</>
);
};
export default DeployState;

View File

@@ -1 +1 @@
export const version = "Certimate v0.1.8";
export const version = "Certimate v0.1.9";

View File

@@ -46,12 +46,12 @@ export default function Dashboard() {
};
const handleSettingClick = () => {
navigate("/setting/password");
navigate("/setting/account");
};
return (
<>
<ConfigProvider>
<div className="grid min-h-screen w-full md:grid-cols-[220px_1fr] lg:grid-cols-[280px_1fr]">
<div className="grid min-h-screen w-full md:grid-cols-[180px_1fr] lg:grid-cols-[200px_1fr] 2xl:md:grid-cols-[280px_1fr] ">
<div className="hidden border-r dark:border-stone-500 bg-muted/40 md:block">
<div className="flex h-full max-h-screen flex-col gap-2">
<div className="flex h-14 items-center border-b dark:border-stone-500 px-4 lg:h-[60px] lg:px-6">

View File

@@ -1,20 +1,57 @@
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Toaster } from "@/components/ui/toaster";
import { Outlet } from "react-router-dom";
import { KeyRound, UserRound } from "lucide-react";
import { useEffect, useState } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
const SettingLayout = () => {
const location = useLocation();
const [tabValue, setTabValue] = useState("account");
const navigate = useNavigate();
useEffect(() => {
const pathname = location.pathname;
const tabValue = pathname.split("/")[2];
setTabValue(tabValue);
}, [location]);
return (
<div>
<Toaster />
<div className="text-muted-foreground border-b dark:border-stone-500 py-5">
</div>
<div className="w-full sm:w-[35em] mt-10 flex flex-col p-3 mx-auto">
{/* <div className="text-muted-foreground">
<span className="transition-all text-sm bg-gray-400 px-3 py-1 rounded-sm text-white cursor-pointer">
密码
</span>
</div> */}
<Outlet />
<div className="w-full mt-5 p-3 flex justify-center">
<Tabs defaultValue="account" className="" value={tabValue}>
<TabsList>
<TabsTrigger
value="account"
onClick={() => {
navigate("/setting/account");
}}
className="px-5"
>
<UserRound size={14} />
<div className="ml-1"></div>
</TabsTrigger>
<TabsTrigger
value="password"
onClick={() => {
navigate("/setting/password");
}}
className="px-5"
>
<KeyRound size={14} />
<div className="ml-1"></div>
</TabsTrigger>
</TabsList>
<TabsContent value={tabValue}>
<div className="mt-5 w-full md:w-[45em]">
<Outlet />
</div>
</TabsContent>
</Tabs>
</div>
</div>
);

View File

@@ -1,4 +1,5 @@
import DeployProgress from "@/components/certimate/DeployProgress";
import DeployState from "@/components/certimate/DeployState";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
@@ -17,8 +18,6 @@ import { statistics } from "@/repository/domains";
import {
Ban,
CalendarX2,
CircleCheck,
CircleX,
LoaderPinwheel,
Smile,
SquareSigma,
@@ -204,11 +203,7 @@ const Dashboard = () => {
{deployment.expand.domain?.domain}
</div>
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
{deployment.phase === "deploy" && deployment.phaseSuccess ? (
<CircleCheck size={16} className="text-green-700" />
) : (
<CircleX size={16} className="text-red-700" />
)}
<DeployState deployment={deployment} />
</div>
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center">
<DeployProgress

View File

@@ -1,4 +1,5 @@
import DeployProgress from "@/components/certimate/DeployProgress";
import DeployState from "@/components/certimate/DeployState";
import XPagination from "@/components/certimate/XPagination";
import Show from "@/components/Show";
import {
@@ -31,7 +32,7 @@ import {
} from "@/repository/domains";
import { TooltipContent, TooltipProvider } from "@radix-ui/react-tooltip";
import { CircleCheck, CircleX, Earth } from "lucide-react";
import { Earth } from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
@@ -196,12 +197,12 @@ const Home = () => {
) : (
<>
<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-40"></div>
<div className="w-48"></div>
<div className="w-36"></div>
<div className="w-40"></div>
<div className="w-32"></div>
<div className="w-64"></div>
<div className="w-40 sm:ml-2"></div>
<div className="w-32"></div>
<div className="w-24"></div>
<div className="grow"></div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
@@ -213,10 +214,10 @@ 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-40 w-full pt-1 sm:pt-0 flex items-center">
<div className="sm:w-36 w-full pt-1 sm:pt-0 flex items-center">
{domain.domain}
</div>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center">
<div className="sm:w-40 w-full pt-1 sm:pt-0 flex items-center">
<div>
{domain.expiredAt ? (
<>
@@ -231,12 +232,7 @@ const Home = () => {
<div className="sm:w-32 w-full pt-1 sm:pt-0 flex items-center">
{domain.lastDeployedAt && domain.expand?.lastDeployment ? (
<>
{domain.expand.lastDeployment?.phase === "deploy" &&
domain.expand.lastDeployment?.phaseSuccess ? (
<CircleCheck size={16} className="text-green-700" />
) : (
<CircleX size={16} className="text-red-700" />
)}
<DeployState deployment={domain.expand.lastDeployment} />
</>
) : (
"---"
@@ -257,7 +253,7 @@ const Home = () => {
? convertZulu2Beijing(domain.lastDeployedAt)
: "---"}
</div>
<div className="sm:w-32 flex items-center">
<div className="sm:w-24 flex items-center">
<TooltipProvider>
<Tooltip>
<TooltipTrigger>

View File

@@ -1,4 +1,5 @@
import DeployProgress from "@/components/certimate/DeployProgress";
import DeployState from "@/components/certimate/DeployState";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
@@ -13,7 +14,7 @@ import {
import { Deployment, DeploymentListReq, Log } from "@/domain/deployment";
import { convertZulu2Beijing } from "@/lib/time";
import { list } from "@/repository/deployment";
import { CircleCheck, CircleX, Smile } from "lucide-react";
import { Smile } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
@@ -88,11 +89,7 @@ const History = () => {
{deployment.expand.domain?.domain}
</div>
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
{deployment.phase === "deploy" && deployment.phaseSuccess ? (
<CircleCheck size={16} className="text-green-700" />
) : (
<CircleX size={16} className="text-red-700" />
)}
<DeployState deployment={deployment} />
</div>
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center">
<DeployProgress

View File

@@ -0,0 +1,109 @@
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useToast } from "@/components/ui/use-toast";
import { getErrMessage } from "@/lib/error";
import { getPb } from "@/repository/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { z } from "zod";
const formSchema = z.object({
email: z.string().email("请输入正确的邮箱"),
});
const Account = () => {
const { toast } = useToast();
const navigate = useNavigate();
const [changed, setChanged] = useState(false);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: getPb().authStore.model?.email,
},
});
const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
await getPb().admins.update(getPb().authStore.model?.id, {
email: values.email,
});
getPb().authStore.clear();
toast({
title: "修改账户邮箱功",
description: "请重新登录",
});
setTimeout(() => {
navigate("/login");
}, 500);
} catch (e) {
const message = getErrMessage(e);
toast({
title: "修改账户邮箱失败",
description: message,
variant: "destructive",
});
}
};
return (
<>
<div className="w-full md:max-w-[35em]">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 dark:text-stone-200"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
placeholder="请输入邮箱"
{...field}
type="email"
onChange={(e) => {
setChanged(true);
form.setValue("email", e.target.value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
{changed ? (
<Button type="submit"></Button>
) : (
<Button type="submit" disabled variant={"secondary"}>
</Button>
)}
</div>
</form>
</Form>
</div>
</>
);
};
export default Account;

View File

@@ -84,64 +84,70 @@ const Password = () => {
return (
<>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 dark:text-stone-200"
>
<FormField
control={form.control}
name="oldPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="当前密码" {...field} type="password" />
</FormControl>
<div className="w-full md:max-w-[35em]">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 dark:text-stone-200"
>
<FormField
control={form.control}
name="oldPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="当前密码" {...field} type="password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="newPassword" {...field} type="password" />
</FormControl>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
placeholder="newPassword"
{...field}
type="password"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
placeholder="confirmPassword"
{...field}
type="password"
/>
</FormControl>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
placeholder="confirmPassword"
{...field}
type="password"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
</div>
</form>
</Form>
</div>
</>
);
};

View File

@@ -10,6 +10,7 @@ import LoginLayout from "./pages/LoginLayout";
import Password from "./pages/setting/Password";
import SettingLayout from "./pages/SettingLayout";
import Dashboard from "./pages/dashboard/Dashboard";
import Account from "./pages/setting/Account";
export const router = createHashRouter([
{
@@ -44,6 +45,10 @@ export const router = createHashRouter([
path: "/setting/password",
element: <Password />,
},
{
path: "/setting/account",
element: <Account />,
},
],
},
],