feat: support removing certificates

This commit is contained in:
Fu Diwei 2025-01-16 21:53:51 +08:00
parent 831f0ee5d9
commit 3a2baba746
17 changed files with 48 additions and 30 deletions

View File

@ -13,7 +13,7 @@ import (
) )
const ( const (
defaultExpireSubject = "有 ${COUNT} 张证书即将过期" defaultExpireSubject = "有 ${COUNT} 张证书即将过期"
defaultExpireMessage = "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!" defaultExpireMessage = "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!"
) )
@ -36,7 +36,7 @@ func (s *CertificateService) InitSchedule(ctx context.Context) error {
err := scheduler.Add("certificate", "0 0 * * *", func() { err := scheduler.Add("certificate", "0 0 * * *", func() {
certs, err := s.repo.ListExpireSoon(context.Background()) certs, err := s.repo.ListExpireSoon(context.Background())
if err != nil { if err != nil {
app.GetLogger().Error("failed to get expire soon certificate", "err", err) app.GetLogger().Error("failed to get certificates which expire soon", "err", err)
return return
} }
@ -46,7 +46,7 @@ func (s *CertificateService) InitSchedule(ctx context.Context) error {
} }
if err := notify.SendToAllChannels(notification.Subject, notification.Message); err != nil { if err := notify.SendToAllChannels(notification.Subject, notification.Message); err != nil {
app.GetLogger().Error("failed to send expire soon certificate", "err", err) app.GetLogger().Error("failed to send notification", "err", err)
} }
}) })
if err != nil { if err != nil {

View File

@ -9,7 +9,6 @@ import (
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
"github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/app"
"github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/types"
) )
type AccessRepository struct{} type AccessRepository struct{}
@ -27,7 +26,7 @@ func (r *AccessRepository) GetById(ctx context.Context, id string) (*domain.Acce
return nil, err return nil, err
} }
if !types.IsNil(record.Get("deleted")) { if !record.GetDateTime("deleted").Time().IsZero() {
return nil, domain.ErrRecordNotFound return nil, domain.ErrRecordNotFound
} }

View File

@ -10,7 +10,6 @@ import (
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
"github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/app"
"github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/types"
) )
type CertificateRepository struct{} type CertificateRepository struct{}
@ -52,7 +51,7 @@ func (r *CertificateRepository) GetById(ctx context.Context, id string) (*domain
return nil, err return nil, err
} }
if !types.IsNil(record.Get("deleted")) { if !record.GetDateTime("deleted").Time().IsZero() {
return nil, domain.ErrRecordNotFound return nil, domain.ErrRecordNotFound
} }

View File

@ -29,7 +29,7 @@ export type NotifyTemplate = {
}; };
export const defaultNotifyTemplate: NotifyTemplate = { export const defaultNotifyTemplate: NotifyTemplate = {
subject: "有 ${COUNT} 张证书即将过期", subject: "有 ${COUNT} 张证书即将过期",
message: "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!", message: "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!",
}; };
// #endregion // #endregion

View File

@ -5,6 +5,7 @@
"certificate.action.view": "View certificate", "certificate.action.view": "View certificate",
"certificate.action.delete": "Delete certificate", "certificate.action.delete": "Delete certificate",
"certificate.action.delete.confirm": "Are you sure to delete this certificate?",
"certificate.action.download": "Download certificate", "certificate.action.download": "Download certificate",
"certificate.props.subject_alt_names": "Name", "certificate.props.subject_alt_names": "Name",

View File

@ -13,6 +13,6 @@
"dashboard.quick_actions": "Quick actions", "dashboard.quick_actions": "Quick actions",
"dashboard.quick_actions.create_workflow": "Create workflow", "dashboard.quick_actions.create_workflow": "Create workflow",
"dashboard.quick_actions.change_login_password": "Change login password", "dashboard.quick_actions.change_login_password": "Change login password",
"dashboard.quick_actions.notification_settings": "Notification settings", "dashboard.quick_actions.cofigure_notification": "Configure notificaion",
"dashboard.quick_actions.certificate_authority_configuration": "Certificate authority configuration" "dashboard.quick_actions.configure_ca": "Configure certificate authority"
} }

View File

@ -20,7 +20,7 @@
"access.form.name.placeholder": "请输入授权名称", "access.form.name.placeholder": "请输入授权名称",
"access.form.provider.label": "提供商", "access.form.provider.label": "提供商",
"access.form.provider.placeholder": "请选择提供商", "access.form.provider.placeholder": "请选择提供商",
"access.form.provider.tooltip": "提供商分为两种类型:<br>【DNS 提供商】的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理您的域名解析记录。<br>【主机提供商】的服务器或云服务的托管方,用于部署签发的证书。<br><br>该字段保存后不可修改。", "access.form.provider.tooltip": "提供商分为两种类型:<br>【DNS 提供商】的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理您的域名解析记录。<br>【主机提供商】的服务器或云服务的托管方,用于部署签发的证书。<br><br>该字段保存后不可修改。",
"access.form.acmehttpreq_endpoint.label": "服务端点", "access.form.acmehttpreq_endpoint.label": "服务端点",
"access.form.acmehttpreq_endpoint.placeholder": "请输入服务端点", "access.form.acmehttpreq_endpoint.placeholder": "请输入服务端点",
"access.form.acmehttpreq_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://go-acme.github.io/lego/dns/httpreq/\" target=\"_blank\">https://go-acme.github.io/lego/dns/httpreq/</a>", "access.form.acmehttpreq_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://go-acme.github.io/lego/dns/httpreq/\" target=\"_blank\">https://go-acme.github.io/lego/dns/httpreq/</a>",

View File

@ -5,6 +5,7 @@
"certificate.action.view": "查看证书", "certificate.action.view": "查看证书",
"certificate.action.delete": "删除证书", "certificate.action.delete": "删除证书",
"certificate.action.delete.confirm": "确定要删除此证书吗?",
"certificate.action.download": "下载证书", "certificate.action.download": "下载证书",
"certificate.props.subject_alt_names": "名称", "certificate.props.subject_alt_names": "名称",

View File

@ -49,7 +49,7 @@
"workflow.detail.orchestration.action.release.confirm": "确定要发布更改吗?", "workflow.detail.orchestration.action.release.confirm": "确定要发布更改吗?",
"workflow.detail.orchestration.action.release.failed.uncompleted": "流程编排未完成,请检查是否有节点未配置", "workflow.detail.orchestration.action.release.failed.uncompleted": "流程编排未完成,请检查是否有节点未配置",
"workflow.detail.orchestration.action.run": "执行", "workflow.detail.orchestration.action.run": "执行",
"workflow.detail.orchestration.action.run.confirm": "你有尚未发布的更改。确定要以最近一次发布的版本继续执行吗?", "workflow.detail.orchestration.action.run.confirm": "你有尚未发布的更改。确定要以最近一次发布的版本继续执行吗?",
"workflow.detail.orchestration.action.run.prompt": "执行中……请稍后查看执行历史", "workflow.detail.orchestration.action.run.prompt": "执行中……请稍后查看执行历史",
"workflow.detail.runs.tab": "执行历史" "workflow.detail.runs.tab": "执行历史"
} }

View File

@ -7,7 +7,7 @@
"workflow_node.action.rename_branch": "重命名", "workflow_node.action.rename_branch": "重命名",
"workflow_node.action.remove_branch": "删除分支", "workflow_node.action.remove_branch": "删除分支",
"workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?", "workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?",
"workflow_node.start.label": "开始", "workflow_node.start.label": "开始",
"workflow_node.start.form.trigger.label": "触发方式", "workflow_node.start.form.trigger.label": "触发方式",

View File

@ -130,7 +130,7 @@ const AccessList = () => {
}); });
}, []); }, []);
const { loading } = useRequest( const { loading, run: refreshTableData } = useRequest(
() => { () => {
const startIndex = (page - 1) * pageSize; const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize; const endIndex = startIndex + pageSize;
@ -157,6 +157,7 @@ const AccessList = () => {
// TODO: 有关联数据的不允许被删除 // TODO: 有关联数据的不允许被删除
try { try {
await deleteAccess(data); await deleteAccess(data);
refreshTableData();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });

View File

@ -4,13 +4,13 @@ import { useNavigate, useSearchParams } from "react-router-dom";
import { DeleteOutlined as DeleteOutlinedIcon, SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons"; import { DeleteOutlined as DeleteOutlinedIcon, SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-components"; import { PageHeader } from "@ant-design/pro-components";
import { useRequest } from "ahooks"; import { useRequest } from "ahooks";
import { Button, Divider, Empty, Menu, type MenuProps, Radio, Space, Table, type TableProps, Tooltip, Typography, notification, theme } from "antd"; import { Button, Divider, Empty, Menu, type MenuProps, Modal, Radio, Space, Table, type TableProps, Tooltip, Typography, notification, theme } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase"; import { ClientResponseError } from "pocketbase";
import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer"; import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer";
import { CERTIFICATE_SOURCES, type CertificateModel } from "@/domain/certificate"; import { CERTIFICATE_SOURCES, type CertificateModel } from "@/domain/certificate";
import { type ListCertificateRequest, list as listCertificate } from "@/repository/certificate"; import { type ListCertificateRequest, list as listCertificate, remove as removeCertificate } from "@/repository/certificate";
import { getErrMsg } from "@/utils/error"; import { getErrMsg } from "@/utils/error";
const CertificateList = () => { const CertificateList = () => {
@ -21,6 +21,7 @@ const CertificateList = () => {
const { token: themeToken } = theme.useToken(); const { token: themeToken } = theme.useToken();
const [modalApi, ModalContextHolder] = Modal.useModal();
const [notificationApi, NotificationContextHolder] = notification.useNotification(); const [notificationApi, NotificationContextHolder] = notification.useNotification();
const tableColumns: TableProps<CertificateModel>["columns"] = [ const tableColumns: TableProps<CertificateModel>["columns"] = [
@ -169,14 +170,7 @@ const CertificateList = () => {
/> />
<Tooltip title={t("certificate.action.delete")}> <Tooltip title={t("certificate.action.delete")}>
<Button <Button color="danger" icon={<DeleteOutlinedIcon />} variant="text" onClick={() => handleDeleteClick(record)} />
color="danger"
icon={<DeleteOutlinedIcon />}
variant="text"
onClick={() => {
alert("TODO: 暂时不支持删除证书");
}}
/>
</Tooltip> </Tooltip>
</Button.Group> </Button.Group>
), ),
@ -194,7 +188,7 @@ const CertificateList = () => {
const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1); const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1);
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 10); const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 10);
const { loading } = useRequest( const { loading, run: refreshTableData } = useRequest(
() => { () => {
return listCertificate({ return listCertificate({
page: page, page: page,
@ -219,8 +213,28 @@ const CertificateList = () => {
} }
); );
const handleDeleteClick = (certificate: CertificateModel) => {
modalApi.confirm({
title: t("certificate.action.delete"),
content: t("certificate.action.delete.confirm"),
onOk: async () => {
try {
const resp = await removeCertificate(certificate);
if (resp) {
setTableData((prev) => prev.filter((item) => item.id !== certificate.id));
refreshTableData();
}
} catch (err) {
console.error(err);
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
}
},
});
};
return ( return (
<div className="p-4"> <div className="p-4">
{ModalContextHolder}
{NotificationContextHolder} {NotificationContextHolder}
<PageHeader title={t("certificate.page.title")} /> <PageHeader title={t("certificate.page.title")} />

View File

@ -251,10 +251,10 @@ const Dashboard = () => {
{t("dashboard.quick_actions.change_login_password")} {t("dashboard.quick_actions.change_login_password")}
</Button> </Button>
<Button block size="large" icon={<SendOutlined />} onClick={() => navigate("/settings/notification")}> <Button block size="large" icon={<SendOutlined />} onClick={() => navigate("/settings/notification")}>
{t("dashboard.quick_actions.notification_settings")} {t("dashboard.quick_actions.cofigure_notification")}
</Button> </Button>
<Button block size="large" icon={<ApiOutlined />} onClick={() => navigate("/settings/ssl-provider")}> <Button block size="large" icon={<ApiOutlined />} onClick={() => navigate("/settings/ssl-provider")}>
{t("dashboard.quick_actions.certificate_authority_configuration")} {t("dashboard.quick_actions.configure_ca")}
</Button> </Button>
</Space> </Space>
</Card> </Card>

View File

@ -109,7 +109,7 @@ const WorkflowDetail = () => {
content: t("workflow.action.delete.confirm"), content: t("workflow.action.delete.confirm"),
onOk: async () => { onOk: async () => {
try { try {
const resp: boolean = await removeWorkflow(workflow); const resp = await removeWorkflow(workflow);
if (resp) { if (resp) {
navigate("/workflows", { replace: true }); navigate("/workflows", { replace: true });
} }

View File

@ -240,7 +240,7 @@ const WorkflowList = () => {
const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1); const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1);
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 10); const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 10);
const { loading } = useRequest( const { loading, run: refreshTableData } = useRequest(
() => { () => {
return listWorkflow({ return listWorkflow({
page: page, page: page,
@ -302,9 +302,10 @@ const WorkflowList = () => {
content: t("workflow.action.delete.confirm"), content: t("workflow.action.delete.confirm"),
onOk: async () => { onOk: async () => {
try { try {
const resp: boolean = await removeWorkflow(workflow); const resp = await removeWorkflow(workflow);
if (resp) { if (resp) {
setTableData((prev) => prev.filter((item) => item.id !== workflow.id)); setTableData((prev) => prev.filter((item) => item.id !== workflow.id));
refreshTableData();
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@ -30,4 +30,5 @@ export const remove = async (record: MaybeModelRecordWithId<AccessModel>) => {
if ("provider" in record && record.provider === "pdns") record.provider = "powerdns"; if ("provider" in record && record.provider === "pdns") record.provider = "powerdns";
await getPocketBase().collection(COLLECTION_NAME).update<AccessModel>(record.id!, record); await getPocketBase().collection(COLLECTION_NAME).update<AccessModel>(record.id!, record);
return true;
}; };

View File

@ -42,4 +42,5 @@ export const remove = async (record: MaybeModelRecordWithId<CertificateModel>) =
record = { ...record, deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") }; record = { ...record, deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") };
await getPocketBase().collection(COLLECTION_NAME).update<CertificateModel>(record.id!, record); await getPocketBase().collection(COLLECTION_NAME).update<CertificateModel>(record.id!, record);
return true;
}; };