From 4ba479576bad5f5e00f768b77852beab42bed1c1 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 13 Mar 2026 14:14:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=8E=B7=E5=8F=96TLS?= =?UTF-8?q?=E8=AF=81=E4=B9=A6=E5=86=85=E5=AE=B9=E7=9A=84API=E5=92=8C?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=94=AF=E6=8C=81=EF=BC=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- atsf_server/controller/tls_certificate.go | 34 +++++++++++++++++++ atsf_server/router/api-router.go | 1 + atsf_server/router/api_phase1_test.go | 16 +++++++-- atsf_server/service/tls_certificate.go | 23 +++++++++++++ .../tls-certificates/api/tls-certificates.ts | 5 +++ .../web/features/tls-certificates/types.ts | 5 +++ .../components/certificate-detail-modal.tsx | 26 ++++++++++---- .../components/certificate-editor-modal.tsx | 8 ++--- 8 files changed, 104 insertions(+), 14 deletions(-) diff --git a/atsf_server/controller/tls_certificate.go b/atsf_server/controller/tls_certificate.go index 0888ffaa..06c70cc2 100644 --- a/atsf_server/controller/tls_certificate.go +++ b/atsf_server/controller/tls_certificate.go @@ -65,6 +65,40 @@ func GetTLSCertificate(c *gin.Context) { }) } +// GetTLSCertificateContent godoc +// @Summary Get TLS certificate PEM content +// @Tags TLSCertificates +// @Produce json +// @Security BearerAuth +// @Param id path int true "Certificate ID" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]interface{} +// @Router /api/tls-certificates/{id}/content [get] +func GetTLSCertificateContent(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil || id == 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "invalid request", + }) + return + } + + content, err := service.GetTLSCertificateContent(uint(id)) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": content, + }) +} + // CreateTLSCertificate godoc // @Summary Create TLS certificate from PEM // @Tags TLSCertificates diff --git a/atsf_server/router/api-router.go b/atsf_server/router/api-router.go index b40eac9b..5db797a3 100644 --- a/atsf_server/router/api-router.go +++ b/atsf_server/router/api-router.go @@ -93,6 +93,7 @@ func SetApiRouter(router *gin.Engine) { { tlsCertificateRoute.GET("/", controller.GetTLSCertificates) tlsCertificateRoute.GET("/:id", controller.GetTLSCertificate) + tlsCertificateRoute.GET("/:id/content", controller.GetTLSCertificateContent) tlsCertificateRoute.POST("/", controller.CreateTLSCertificate) tlsCertificateRoute.PUT("/:id", controller.UpdateTLSCertificate) tlsCertificateRoute.POST("/import-file", controller.ImportTLSCertificateFile) diff --git a/atsf_server/router/api_phase1_test.go b/atsf_server/router/api_phase1_test.go index b65796a9..0e75ae6c 100644 --- a/atsf_server/router/api_phase1_test.go +++ b/atsf_server/router/api_phase1_test.go @@ -185,10 +185,20 @@ func TestPhase1HTTPSAndCertificateImportLifecycle(t *testing.T) { } detailResp := performJSONRequest(t, engine, token, http.MethodGet, "/api/tls-certificates/"+toString(manualCertificate.ID), nil) - var certificateDetail model.TLSCertificate + var certificateDetail map[string]any decodeResponseData(t, detailResp, &certificateDetail) - if certificateDetail.ID != manualCertificate.ID || certificateDetail.CertPEM == "" || certificateDetail.KeyPEM == "" { - t.Fatal("expected certificate detail endpoint to return pem payloads") + if _, exists := certificateDetail["cert_pem"]; exists { + t.Fatal("expected certificate detail endpoint to omit cert_pem") + } + if _, exists := certificateDetail["key_pem"]; exists { + t.Fatal("expected certificate detail endpoint to omit key_pem") + } + + contentResp := performJSONRequest(t, engine, token, http.MethodGet, "/api/tls-certificates/"+toString(manualCertificate.ID)+"/content", nil) + var certificateContent map[string]any + decodeResponseData(t, contentResp, &certificateContent) + if certificateContent["cert_pem"] == "" || certificateContent["key_pem"] == "" { + t.Fatal("expected certificate content endpoint to return pem payloads") } updatedCertPEM, updatedKeyPEM := generateCertificatePairForRouterTest(t, []string{"secure.example.com", "www.secure.example.com"}) diff --git a/atsf_server/service/tls_certificate.go b/atsf_server/service/tls_certificate.go index c60c999d..043f83a1 100644 --- a/atsf_server/service/tls_certificate.go +++ b/atsf_server/service/tls_certificate.go @@ -16,6 +16,14 @@ type TLSCertificateInput struct { Remark string `json:"remark"` } +type TLSCertificateContent struct { + ID uint `json:"id"` + Name string `json:"name"` + CertPEM string `json:"cert_pem"` + KeyPEM string `json:"key_pem"` + Remark string `json:"remark"` +} + func ListTLSCertificates() ([]*model.TLSCertificate, error) { return model.ListTLSCertificates() } @@ -24,6 +32,21 @@ func GetTLSCertificate(id uint) (*model.TLSCertificate, error) { return model.GetTLSCertificateByID(id) } +func GetTLSCertificateContent(id uint) (*TLSCertificateContent, error) { + certificate, err := model.GetTLSCertificateByID(id) + if err != nil { + return nil, err + } + + return &TLSCertificateContent{ + ID: certificate.ID, + Name: certificate.Name, + CertPEM: certificate.CertPEM, + KeyPEM: certificate.KeyPEM, + Remark: certificate.Remark, + }, nil +} + func CreateTLSCertificate(input TLSCertificateInput) (*model.TLSCertificate, error) { certificate, err := buildTLSCertificate(nil, input) if err != nil { diff --git a/atsf_server/web/features/tls-certificates/api/tls-certificates.ts b/atsf_server/web/features/tls-certificates/api/tls-certificates.ts index 7f074d5b..04a5d8a3 100644 --- a/atsf_server/web/features/tls-certificates/api/tls-certificates.ts +++ b/atsf_server/web/features/tls-certificates/api/tls-certificates.ts @@ -1,6 +1,7 @@ import { apiRequest } from '@/lib/api/client'; import type { + TlsCertificateContentItem, TlsCertificateDetailItem, TlsCertificateFileImportPayload, TlsCertificateItem, @@ -22,6 +23,10 @@ export function getTlsCertificate(id: number) { return apiRequest(`/tls-certificates/${id}`); } +export function getTlsCertificateContent(id: number) { + return apiRequest(`/tls-certificates/${id}/content`); +} + export function updateTlsCertificate( id: number, payload: TlsCertificateMutationPayload, diff --git a/atsf_server/web/features/tls-certificates/types.ts b/atsf_server/web/features/tls-certificates/types.ts index 27318dfc..d65f4e75 100644 --- a/atsf_server/web/features/tls-certificates/types.ts +++ b/atsf_server/web/features/tls-certificates/types.ts @@ -11,6 +11,11 @@ export interface TlsCertificateItem { } export interface TlsCertificateDetailItem extends TlsCertificateItem { + cert_pem?: never; + key_pem?: never; +} + +export interface TlsCertificateContentItem extends TlsCertificateItem { cert_pem: string; key_pem: string; } diff --git a/atsf_server/web/features/websites/components/certificate-detail-modal.tsx b/atsf_server/web/features/websites/components/certificate-detail-modal.tsx index 9bb519d0..e8e8f54a 100644 --- a/atsf_server/web/features/websites/components/certificate-detail-modal.tsx +++ b/atsf_server/web/features/websites/components/certificate-detail-modal.tsx @@ -7,7 +7,10 @@ import { ErrorState } from '@/components/feedback/error-state'; import { LoadingState } from '@/components/feedback/loading-state'; import { AppModal } from '@/components/ui/app-modal'; import { StatusBadge } from '@/components/ui/status-badge'; -import { getTlsCertificate } from '@/features/tls-certificates/api/tls-certificates'; +import { + getTlsCertificate, + getTlsCertificateContent, +} from '@/features/tls-certificates/api/tls-certificates'; import { getCertificateStatus, getErrorMessage } from '@/features/websites/utils'; import { CodeBlock, @@ -40,7 +43,14 @@ export function CertificateDetailModal({ enabled: isOpen && certificateId !== null, }); + const contentQuery = useQuery({ + queryKey: ['tls-certificates', 'content', certificateId], + queryFn: () => getTlsCertificateContent(certificateId as number), + enabled: isOpen && certificateId !== null, + }); + const certificate = certificateQuery.data; + const content = contentQuery.data; const status = certificate ? getCertificateStatus(certificate) : null; return ( @@ -72,14 +82,16 @@ export function CertificateDetailModal({ } > - {certificateQuery.isLoading ? ( + {certificateQuery.isLoading || contentQuery.isLoading ? ( - ) : certificateQuery.isError ? ( + ) : certificateQuery.isError || contentQuery.isError ? ( - ) : !certificate ? ( + ) : !certificate || !content ? ( - {certificate.cert_pem} + {content.cert_pem}
@@ -146,7 +158,7 @@ export function CertificateDetailModal({ 私钥 PEM

- {certificate.key_pem} + {content.key_pem}
diff --git a/atsf_server/web/features/websites/components/certificate-editor-modal.tsx b/atsf_server/web/features/websites/components/certificate-editor-modal.tsx index a9d33036..acf50646 100644 --- a/atsf_server/web/features/websites/components/certificate-editor-modal.tsx +++ b/atsf_server/web/features/websites/components/certificate-editor-modal.tsx @@ -11,7 +11,7 @@ import { InlineMessage } from '@/components/feedback/inline-message'; import { LoadingState } from '@/components/feedback/loading-state'; import { AppModal } from '@/components/ui/app-modal'; import { - getTlsCertificate, + getTlsCertificateContent, updateTlsCertificate, } from '@/features/tls-certificates/api/tls-certificates'; import type { TlsCertificateItem } from '@/features/tls-certificates/types'; @@ -49,8 +49,8 @@ export function CertificateEditorModal({ }); const certificateQuery = useQuery({ - queryKey: ['tls-certificates', 'detail', certificateId], - queryFn: () => getTlsCertificate(certificateId as number), + queryKey: ['tls-certificates', 'content', certificateId], + queryFn: () => getTlsCertificateContent(certificateId as number), enabled: isOpen && certificateId !== null, }); @@ -120,7 +120,7 @@ export function CertificateEditorModal({ ) : certificateQuery.isError ? ( ) : !certificateQuery.data ? (