From d5e4ea385d51343809b79e0a3e161dab95387088 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sat, 18 Jan 2025 07:07:50 +0800 Subject: [PATCH] feat: download certificate archive --- internal/certificate/service.go | 124 ++++++++++++++++++ internal/domain/certificate.go | 5 + internal/domain/workflow.go | 2 +- internal/rest/handlers/certificate.go | 42 ++++++ internal/rest/handlers/workflow.go | 5 +- internal/rest/routes/routes.go | 18 ++- ui/package-lock.json | 96 +++----------- ui/package.json | 3 +- ui/src/api/certificates.ts | 24 ++++ ui/src/api/{workflow.ts => workflows.ts} | 3 +- .../certificate/CertificateDetail.tsx | 40 +++--- .../node/DeployNodeConfigFormLocalConfig.tsx | 7 +- .../node/DeployNodeConfigFormSSHConfig.tsx | 7 +- ui/src/domain/certificate.ts | 8 ++ ui/src/pages/workflows/WorkflowDetail.tsx | 2 +- ui/src/utils/file.ts | 18 --- 16 files changed, 265 insertions(+), 139 deletions(-) create mode 100644 internal/rest/handlers/certificate.go create mode 100644 ui/src/api/certificates.ts rename ui/src/api/{workflow.ts => workflows.ts} (84%) diff --git a/internal/certificate/service.go b/internal/certificate/service.go index 6fb2db34..0c518388 100644 --- a/internal/certificate/service.go +++ b/internal/certificate/service.go @@ -1,6 +1,8 @@ package certificate import ( + "archive/zip" + "bytes" "context" "encoding/json" "strconv" @@ -9,6 +11,7 @@ import ( "github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/notify" + "github.com/usual2970/certimate/internal/pkg/utils/certs" "github.com/usual2970/certimate/internal/repository" ) @@ -18,6 +21,7 @@ const ( ) type certificateRepository interface { + GetById(ctx context.Context, id string) (*domain.Certificate, error) ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error) } @@ -51,6 +55,126 @@ func (s *CertificateService) InitSchedule(ctx context.Context) error { return nil } +func (s *CertificateService) ArchiveFile(ctx context.Context, req *domain.CertificateArchiveFileReq) ([]byte, error) { + certificate, err := s.repo.GetById(ctx, req.CertificateId) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + zipWriter := zip.NewWriter(&buf) + defer zipWriter.Close() + + switch strings.ToUpper(req.Format) { + case "", "PEM": + { + certWriter, err := zipWriter.Create("certbundle.pem") + if err != nil { + return nil, err + } + + _, err = certWriter.Write([]byte(certificate.Certificate)) + if err != nil { + return nil, err + } + + keyWriter, err := zipWriter.Create("privkey.pem") + if err != nil { + return nil, err + } + + _, err = keyWriter.Write([]byte(certificate.PrivateKey)) + if err != nil { + return nil, err + } + + err = zipWriter.Close() + if err != nil { + return nil, err + } + + return buf.Bytes(), nil + } + + case "PFX": + { + const pfxPassword = "certimate" + + certPFX, err := certs.TransformCertificateFromPEMToPFX(certificate.Certificate, certificate.PrivateKey, pfxPassword) + if err != nil { + return nil, err + } + + certWriter, err := zipWriter.Create("cert.pfx") + if err != nil { + return nil, err + } + + _, err = certWriter.Write(certPFX) + if err != nil { + return nil, err + } + + keyWriter, err := zipWriter.Create("pfx-password.txt") + if err != nil { + return nil, err + } + + _, err = keyWriter.Write([]byte(pfxPassword)) + if err != nil { + return nil, err + } + + err = zipWriter.Close() + if err != nil { + return nil, err + } + + return buf.Bytes(), nil + } + + case "JKS": + { + const jksPassword = "certimate" + + certJKS, err := certs.TransformCertificateFromPEMToJKS(certificate.Certificate, certificate.PrivateKey, jksPassword, jksPassword, jksPassword) + if err != nil { + return nil, err + } + + certWriter, err := zipWriter.Create("cert.jks") + if err != nil { + return nil, err + } + + _, err = certWriter.Write(certJKS) + if err != nil { + return nil, err + } + + keyWriter, err := zipWriter.Create("jks-password.txt") + if err != nil { + return nil, err + } + + _, err = keyWriter.Write([]byte(jksPassword)) + if err != nil { + return nil, err + } + + err = zipWriter.Close() + if err != nil { + return nil, err + } + + return buf.Bytes(), nil + } + + default: + return nil, domain.ErrInvalidParams + } +} + func buildExpireSoonNotification(certificates []*domain.Certificate) *struct { Subject string Message string diff --git a/internal/domain/certificate.go b/internal/domain/certificate.go index 84179c3a..f7812049 100644 --- a/internal/domain/certificate.go +++ b/internal/domain/certificate.go @@ -25,3 +25,8 @@ type Certificate struct { WorkflowOutputId string `json:"workflowOutputId" db:"workflowOutputId"` DeletedAt *time.Time `json:"deleted" db:"deleted"` } + +type CertificateArchiveFileReq struct { + CertificateId string `json:"-"` + Format string `json:"format"` +} diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index bc54c537..5b894297 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -152,6 +152,6 @@ type WorkflowNodeIOValueSelector struct { } type WorkflowRunReq struct { - WorkflowId string `json:"workflowId"` + WorkflowId string `json:"-"` Trigger WorkflowTriggerType `json:"trigger"` } diff --git a/internal/rest/handlers/certificate.go b/internal/rest/handlers/certificate.go new file mode 100644 index 00000000..730302d5 --- /dev/null +++ b/internal/rest/handlers/certificate.go @@ -0,0 +1,42 @@ +package handlers + +import ( + "context" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/router" + + "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/rest/resp" +) + +type certificateService interface { + ArchiveFile(ctx context.Context, req *domain.CertificateArchiveFileReq) ([]byte, error) +} + +type CertificateHandler struct { + service certificateService +} + +func NewCertificateHandler(router *router.RouterGroup[*core.RequestEvent], service certificateService) { + handler := &CertificateHandler{ + service: service, + } + + group := router.Group("/certificates") + group.POST("/{id}/archive", handler.run) +} + +func (handler *CertificateHandler) run(e *core.RequestEvent) error { + req := &domain.CertificateArchiveFileReq{} + req.CertificateId = e.Request.PathValue("id") + if err := e.BindBody(req); err != nil { + return resp.Err(e, err) + } + + if bt, err := handler.service.ArchiveFile(e.Request.Context(), req); err != nil { + return resp.Err(e, err) + } else { + return resp.Ok(e, bt) + } +} diff --git a/internal/rest/handlers/workflow.go b/internal/rest/handlers/workflow.go index d6d30720..bb3da629 100644 --- a/internal/rest/handlers/workflow.go +++ b/internal/rest/handlers/workflow.go @@ -24,12 +24,13 @@ func NewWorkflowHandler(router *router.RouterGroup[*core.RequestEvent], service service: service, } - group := router.Group("/workflow") - group.POST("/run", handler.run) + group := router.Group("/workflows") + group.POST("/{id}/run", handler.run) } func (handler *WorkflowHandler) run(e *core.RequestEvent) error { req := &domain.WorkflowRunReq{} + req.WorkflowId = e.Request.PathValue("id") if err := e.BindBody(req); err != nil { return resp.Err(e, err) } diff --git a/internal/rest/routes/routes.go b/internal/rest/routes/routes.go index 638c9a10..58b4f0f1 100644 --- a/internal/rest/routes/routes.go +++ b/internal/rest/routes/routes.go @@ -7,6 +7,7 @@ import ( "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tools/router" + "github.com/usual2970/certimate/internal/certificate" "github.com/usual2970/certimate/internal/notify" "github.com/usual2970/certimate/internal/repository" "github.com/usual2970/certimate/internal/rest/handlers" @@ -15,14 +16,15 @@ import ( ) var ( - notifySvc *notify.NotifyService - workflowSvc *workflow.WorkflowService - statisticsSvc *statistics.StatisticsService + certificateSvc *certificate.CertificateService + workflowSvc *workflow.WorkflowService + statisticsSvc *statistics.StatisticsService + notifySvc *notify.NotifyService ) func Register(router *router.Router[*core.RequestEvent]) { - notifyRepo := repository.NewSettingsRepository() - notifySvc = notify.NewNotifyService(notifyRepo) + certificateRepo := repository.NewCertificateRepository() + certificateSvc = certificate.NewCertificateService(certificateRepo) workflowRepo := repository.NewWorkflowRepository() workflowSvc = workflow.NewWorkflowService(workflowRepo) @@ -30,11 +32,15 @@ func Register(router *router.Router[*core.RequestEvent]) { statisticsRepo := repository.NewStatisticsRepository() statisticsSvc = statistics.NewStatisticsService(statisticsRepo) + notifyRepo := repository.NewSettingsRepository() + notifySvc = notify.NewNotifyService(notifyRepo) + group := router.Group("/api") group.Bind(apis.RequireSuperuserAuth()) + handlers.NewCertificateHandler(group, certificateSvc) handlers.NewWorkflowHandler(group, workflowSvc) - handlers.NewNotifyHandler(group, notifySvc) handlers.NewStatisticsHandler(group, statisticsSvc) + handlers.NewNotifyHandler(group, notifySvc) } func Unregister() { diff --git a/ui/package-lock.json b/ui/package-lock.json index 5be7df2a..7dd69460 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -14,10 +14,10 @@ "antd": "^5.22.7", "antd-zod": "^6.0.0", "cron-parser": "^4.9.0", + "file-saver": "^2.0.5", "i18next": "^24.2.0", "i18next-browser-languagedetector": "^8.0.2", "immer": "^10.1.1", - "jszip": "^3.10.1", "lucide-react": "^0.469.0", "nanoid": "^5.0.9", "pocketbase": "^0.25.0", @@ -31,6 +31,7 @@ "zustand": "^5.0.2" }, "devDependencies": { + "@types/file-saver": "^2.0.7", "@types/fs-extra": "^11.0.4", "@types/node": "^22.10.3", "@types/react": "^18.3.12", @@ -3188,6 +3189,12 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true + }, "node_modules/@types/fs-extra": { "version": "11.0.4", "resolved": "https://registry.npmmirror.com/@types/fs-extra/-/fs-extra-11.0.4.tgz", @@ -4193,11 +4200,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, "node_modules/cron-parser": { "version": "4.9.0", "resolved": "https://registry.npmmirror.com/cron-parser/-/cron-parser-4.9.0.tgz", @@ -5183,6 +5185,11 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", @@ -5640,11 +5647,6 @@ "node": ">= 4" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, "node_modules/immer": { "version": "10.1.1", "resolved": "https://registry.npmmirror.com/immer/-/immer-10.1.1.tgz", @@ -5693,7 +5695,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/internal-slot": { "version": "1.1.0", @@ -6100,11 +6103,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", @@ -6222,17 +6220,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", @@ -6255,14 +6242,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", @@ -6694,11 +6673,6 @@ "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", "dev": true }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", @@ -7021,11 +6995,6 @@ "node": ">=6.0.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", @@ -7784,20 +7753,6 @@ "pify": "^2.3.0" } }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", @@ -8081,11 +8036,6 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -8209,11 +8159,6 @@ "node": ">= 0.4" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/shallowequal/-/shallowequal-1.1.0.tgz", @@ -8360,14 +8305,6 @@ "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", "dev": true }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/string-convert": { "version": "0.2.1", "resolved": "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz", @@ -9057,7 +8994,8 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true }, "node_modules/vite": { "version": "6.0.6", diff --git a/ui/package.json b/ui/package.json index e98a4679..256f053e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -16,10 +16,10 @@ "antd": "^5.22.7", "antd-zod": "^6.0.0", "cron-parser": "^4.9.0", + "file-saver": "^2.0.5", "i18next": "^24.2.0", "i18next-browser-languagedetector": "^8.0.2", "immer": "^10.1.1", - "jszip": "^3.10.1", "lucide-react": "^0.469.0", "nanoid": "^5.0.9", "pocketbase": "^0.25.0", @@ -33,6 +33,7 @@ "zustand": "^5.0.2" }, "devDependencies": { + "@types/file-saver": "^2.0.7", "@types/fs-extra": "^11.0.4", "@types/node": "^22.10.3", "@types/react": "^18.3.12", diff --git a/ui/src/api/certificates.ts b/ui/src/api/certificates.ts new file mode 100644 index 00000000..b8764d0a --- /dev/null +++ b/ui/src/api/certificates.ts @@ -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(`/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; +}; diff --git a/ui/src/api/workflow.ts b/ui/src/api/workflows.ts similarity index 84% rename from ui/src/api/workflow.ts rename to ui/src/api/workflows.ts index a5a29bd8..4ad9e2d3 100644 --- a/ui/src/api/workflow.ts +++ b/ui/src/api/workflows.ts @@ -6,13 +6,12 @@ import { getPocketBase } from "@/repository/_pocketbase"; export const run = async (id: string) => { const pb = getPocketBase(); - const resp = await pb.send("/api/workflow/run", { + const resp = await pb.send(`/api/workflows/${encodeURIComponent(id)}/run`, { method: "POST", headers: { "Content-Type": "application/json", }, body: { - workflowId: id, trigger: WORKFLOW_TRIGGERS.MANUAL, }, }); diff --git a/ui/src/components/certificate/CertificateDetail.tsx b/ui/src/components/certificate/CertificateDetail.tsx index b82cb8db..b782afb5 100644 --- a/ui/src/components/certificate/CertificateDetail.tsx +++ b/ui/src/components/certificate/CertificateDetail.tsx @@ -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: , - 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), }, ], }} diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx index c217ae68..6ff6d8e2 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx @@ -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; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx index 77da315b..976ca079 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx @@ -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 { diff --git a/ui/src/domain/certificate.ts b/ui/src/domain/certificate.ts index 00c0d7d5..2ec857fc 100644 --- a/ui/src/domain/certificate.ts +++ b/ui/src/domain/certificate.ts @@ -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]; diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index f2f46398..cbb4fc4b 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -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"; diff --git a/ui/src/utils/file.ts b/ui/src/utils/file.ts index 00b7beef..16edf7af 100644 --- a/ui/src/utils/file.ts +++ b/ui/src/utils/file.ts @@ -1,5 +1,3 @@ -import JSZip from "jszip"; - export function readFileContent(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); @@ -17,19 +15,3 @@ export function readFileContent(file: File): Promise { 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(); -};