feat: download certificate archive
This commit is contained in:
24
ui/src/api/certificates.ts
Normal file
24
ui/src/api/certificates.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import { type CertificateFormatType } from "@/domain/certificate";
|
||||
import { getPocketBase } from "@/repository/_pocketbase";
|
||||
|
||||
export const archive = async (id: string, format?: CertificateFormatType) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
const resp = await pb.send<BaseResponse>(`/api/certificates/${encodeURIComponent(id)}/archive`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: {
|
||||
format: format,
|
||||
},
|
||||
});
|
||||
|
||||
if (resp.code != 0) {
|
||||
throw new ClientResponseError({ status: resp.code, response: resp, data: {} });
|
||||
}
|
||||
|
||||
return resp;
|
||||
};
|
||||
@@ -6,13 +6,12 @@ import { getPocketBase } from "@/repository/_pocketbase";
|
||||
export const run = async (id: string) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
const resp = await pb.send<BaseResponse>("/api/workflow/run", {
|
||||
const resp = await pb.send<BaseResponse>(`/api/workflows/${encodeURIComponent(id)}/run`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: {
|
||||
workflowId: id,
|
||||
trigger: WORKFLOW_TRIGGERS.MANUAL,
|
||||
},
|
||||
});
|
||||
@@ -3,9 +3,10 @@ import { useTranslation } from "react-i18next";
|
||||
import { CopyOutlined as CopyOutlinedIcon, DownOutlined as DownOutlinedIcon, LikeOutlined as LikeOutlinedIcon } from "@ant-design/icons";
|
||||
import { Button, Dropdown, Form, Input, Space, Tooltip, message } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
import { type CertificateModel } from "@/domain/certificate";
|
||||
import { saveFiles2Zip } from "@/utils/file";
|
||||
import { archive as archiveCertificate } from "@/api/certificates";
|
||||
import { CERTIFICATE_FORMATS, type CertificateFormatType, type CertificateModel } from "@/domain/certificate";
|
||||
|
||||
export type CertificateDetailProps = {
|
||||
className?: string;
|
||||
@@ -18,20 +19,17 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
|
||||
|
||||
const [messageApi, MessageContextHolder] = message.useMessage();
|
||||
|
||||
const handleDownloadPEMClick = async () => {
|
||||
const zipName = `${data.id}-${data.subjectAltNames}.zip`;
|
||||
const files = [
|
||||
{
|
||||
name: `${data.subjectAltNames}.pem`,
|
||||
content: data.certificate ?? "",
|
||||
},
|
||||
{
|
||||
name: `${data.subjectAltNames}.key`,
|
||||
content: data.privateKey ?? "",
|
||||
},
|
||||
];
|
||||
|
||||
await saveFiles2Zip(zipName, files);
|
||||
const handleDownloadClick = async (format: CertificateFormatType) => {
|
||||
try {
|
||||
const res = await archiveCertificate(data.id, format);
|
||||
const bstr = atob(res.data);
|
||||
const u8arr = Uint8Array.from(bstr, (ch) => ch.charCodeAt(0));
|
||||
const blob = new Blob([u8arr], { type: "application/zip" });
|
||||
saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
messageApi.warning(t("common.text.operation_failed"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -90,21 +88,17 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
|
||||
key: "PEM",
|
||||
label: "PEM",
|
||||
extra: <LikeOutlinedIcon />,
|
||||
onClick: () => handleDownloadPEMClick(),
|
||||
onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.PEM),
|
||||
},
|
||||
{
|
||||
key: "PFX",
|
||||
label: "PFX",
|
||||
onClick: () => {
|
||||
alert("TODO: 暂时不支持下载 PFX 证书");
|
||||
},
|
||||
onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.PFX),
|
||||
},
|
||||
{
|
||||
key: "JKS",
|
||||
label: "JKS",
|
||||
onClick: () => {
|
||||
alert("TODO: 暂时不支持下载 JKS 证书");
|
||||
},
|
||||
onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.JKS),
|
||||
},
|
||||
],
|
||||
}}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import { CERTIFICATE_FORMATS } from "@/domain/certificate";
|
||||
|
||||
type DeployNodeConfigFormLocalConfigFieldValues = Nullish<{
|
||||
format: string;
|
||||
@@ -27,9 +28,9 @@ export type DeployNodeConfigFormLocalConfigProps = {
|
||||
onValuesChange?: (values: DeployNodeConfigFormLocalConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const FORMAT_PEM = "PEM" as const;
|
||||
const FORMAT_PFX = "PFX" as const;
|
||||
const FORMAT_JKS = "JKS" as const;
|
||||
const FORMAT_PEM = CERTIFICATE_FORMATS.PEM;
|
||||
const FORMAT_PFX = CERTIFICATE_FORMATS.PFX;
|
||||
const FORMAT_JKS = CERTIFICATE_FORMATS.JKS;
|
||||
|
||||
const SHELLENV_SH = "sh" as const;
|
||||
const SHELLENV_CMD = "cmd" as const;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import { CERTIFICATE_FORMATS } from "@/domain/certificate";
|
||||
|
||||
type DeployNodeConfigFormSSHConfigFieldValues = Nullish<{
|
||||
format: string;
|
||||
@@ -26,9 +27,9 @@ export type DeployNodeConfigFormSSHConfigProps = {
|
||||
onValuesChange?: (values: DeployNodeConfigFormSSHConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const FORMAT_PEM = "PEM" as const;
|
||||
const FORMAT_PFX = "PFX" as const;
|
||||
const FORMAT_JKS = "JKS" as const;
|
||||
const FORMAT_PEM = CERTIFICATE_FORMATS.PEM;
|
||||
const FORMAT_PFX = CERTIFICATE_FORMATS.PFX;
|
||||
const FORMAT_JKS = CERTIFICATE_FORMATS.JKS;
|
||||
|
||||
const initFormModel = (): DeployNodeConfigFormSSHConfigFieldValues => {
|
||||
return {
|
||||
|
||||
@@ -19,3 +19,11 @@ export const CERTIFICATE_SOURCES = Object.freeze({
|
||||
} as const);
|
||||
|
||||
export type CertificateSourceType = (typeof CERTIFICATE_SOURCES)[keyof typeof CERTIFICATE_SOURCES];
|
||||
|
||||
export const CERTIFICATE_FORMATS = Object.freeze({
|
||||
PEM: "PEM",
|
||||
PFX: "PFX",
|
||||
JKS: "JKS",
|
||||
} as const);
|
||||
|
||||
export type CertificateFormatType = (typeof CERTIFICATE_FORMATS)[keyof typeof CERTIFICATE_FORMATS];
|
||||
|
||||
@@ -16,7 +16,7 @@ import { createSchemaFieldRule } from "antd-zod";
|
||||
import { isEqual } from "radash";
|
||||
import { z } from "zod";
|
||||
|
||||
import { run as runWorkflow } from "@/api/workflow";
|
||||
import { run as runWorkflow } from "@/api/workflows";
|
||||
import ModalForm from "@/components/ModalForm";
|
||||
import Show from "@/components/Show";
|
||||
import WorkflowElements from "@/components/workflow/WorkflowElements";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import JSZip from "jszip";
|
||||
|
||||
export function readFileContent(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -17,19 +15,3 @@ export function readFileContent(file: File): Promise<string> {
|
||||
reader.readAsText(file, "utf-8");
|
||||
});
|
||||
}
|
||||
|
||||
export const saveFiles2Zip = async (zipName: string, files: { name: string; content: string }[]) => {
|
||||
const zip = new JSZip();
|
||||
|
||||
files.forEach((file) => {
|
||||
zip.file(file.name, file.content);
|
||||
});
|
||||
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
// Save the zip file to the local system
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(content);
|
||||
link.download = zipName;
|
||||
link.click();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user