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 (
defaultExpireSubject = "有 ${COUNT} 张证书即将过期"
defaultExpireSubject = "有 ${COUNT} 张证书即将过期"
defaultExpireMessage = "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!"
)
@ -36,7 +36,7 @@ func (s *CertificateService) InitSchedule(ctx context.Context) error {
err := scheduler.Add("certificate", "0 0 * * *", func() {
certs, err := s.repo.ListExpireSoon(context.Background())
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
}
@ -46,7 +46,7 @@ func (s *CertificateService) InitSchedule(ctx context.Context) error {
}
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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@
"access.form.name.placeholder": "请输入授权名称",
"access.form.provider.label": "提供商",
"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.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>",

View File

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

View File

@ -49,7 +49,7 @@
"workflow.detail.orchestration.action.release.confirm": "确定要发布更改吗?",
"workflow.detail.orchestration.action.release.failed.uncompleted": "流程编排未完成,请检查是否有节点未配置",
"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.runs.tab": "执行历史"
}

View File

@ -7,7 +7,7 @@
"workflow_node.action.rename_branch": "重命名",
"workflow_node.action.remove_branch": "删除分支",
"workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?",
"workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?",
"workflow_node.start.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 endIndex = startIndex + pageSize;
@ -157,6 +157,7 @@ const AccessList = () => {
// TODO: 有关联数据的不允许被删除
try {
await deleteAccess(data);
refreshTableData();
} catch (err) {
console.error(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 { PageHeader } from "@ant-design/pro-components";
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 { ClientResponseError } from "pocketbase";
import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer";
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";
const CertificateList = () => {
@ -21,6 +21,7 @@ const CertificateList = () => {
const { token: themeToken } = theme.useToken();
const [modalApi, ModalContextHolder] = Modal.useModal();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
const tableColumns: TableProps<CertificateModel>["columns"] = [
@ -169,14 +170,7 @@ const CertificateList = () => {
/>
<Tooltip title={t("certificate.action.delete")}>
<Button
color="danger"
icon={<DeleteOutlinedIcon />}
variant="text"
onClick={() => {
alert("TODO: 暂时不支持删除证书");
}}
/>
<Button color="danger" icon={<DeleteOutlinedIcon />} variant="text" onClick={() => handleDeleteClick(record)} />
</Tooltip>
</Button.Group>
),
@ -194,7 +188,7 @@ const CertificateList = () => {
const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1);
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 10);
const { loading } = useRequest(
const { loading, run: refreshTableData } = useRequest(
() => {
return listCertificate({
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 (
<div className="p-4">
{ModalContextHolder}
{NotificationContextHolder}
<PageHeader title={t("certificate.page.title")} />

View File

@ -251,10 +251,10 @@ const Dashboard = () => {
{t("dashboard.quick_actions.change_login_password")}
</Button>
<Button block size="large" icon={<SendOutlined />} onClick={() => navigate("/settings/notification")}>
{t("dashboard.quick_actions.notification_settings")}
{t("dashboard.quick_actions.cofigure_notification")}
</Button>
<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>
</Space>
</Card>

View File

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

View File

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