mirror of https://github.com/usual2970/certimate
				
				
				
			feat: support removing certificates
							parent
							
								
									831f0ee5d9
								
							
						
					
					
						commit
						3a2baba746
					
				| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ export type NotifyTemplate = {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
export const defaultNotifyTemplate: NotifyTemplate = {
 | 
			
		||||
  subject: "您有 ${COUNT} 张证书即将过期",
 | 
			
		||||
  subject: "有 ${COUNT} 张证书即将过期",
 | 
			
		||||
  message: "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!",
 | 
			
		||||
};
 | 
			
		||||
// #endregion
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@
 | 
			
		|||
 | 
			
		||||
  "certificate.action.view": "查看证书",
 | 
			
		||||
  "certificate.action.delete": "删除证书",
 | 
			
		||||
  "certificate.action.delete.confirm": "确定要删除此证书吗?",
 | 
			
		||||
  "certificate.action.download": "下载证书",
 | 
			
		||||
 | 
			
		||||
  "certificate.props.subject_alt_names": "名称",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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": "执行历史"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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": "触发方式",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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")} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 });
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue