feat: 添加获取TLS证书内容的API和前端支持,更新相关组件

This commit is contained in:
ryan
2026-03-13 14:14:53 +08:00
parent 63c3204726
commit 4ba479576b
8 changed files with 104 additions and 14 deletions
+34
View File
@@ -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
+1
View File
@@ -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)
+13 -3
View File
@@ -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"})
+23
View File
@@ -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 {
@@ -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<TlsCertificateDetailItem>(`/tls-certificates/${id}`);
}
export function getTlsCertificateContent(id: number) {
return apiRequest<TlsCertificateContentItem>(`/tls-certificates/${id}/content`);
}
export function updateTlsCertificate(
id: number,
payload: TlsCertificateMutationPayload,
@@ -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;
}
@@ -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({
</div>
}
>
{certificateQuery.isLoading ? (
{certificateQuery.isLoading || contentQuery.isLoading ? (
<LoadingState />
) : certificateQuery.isError ? (
) : certificateQuery.isError || contentQuery.isError ? (
<ErrorState
title="证书详情加载失败"
description={getErrorMessage(certificateQuery.error)}
description={getErrorMessage(
certificateQuery.error ?? contentQuery.error,
)}
/>
) : !certificate ? (
) : !certificate || !content ? (
<EmptyState
title="证书不存在"
description="当前证书可能已被删除。"
@@ -138,7 +150,7 @@ export function CertificateDetailModal({
证书 PEM
</p>
<CodeBlock className="max-h-56 overflow-y-auto whitespace-pre-wrap break-all">
{certificate.cert_pem}
{content.cert_pem}
</CodeBlock>
</div>
<div>
@@ -146,7 +158,7 @@ export function CertificateDetailModal({
私钥 PEM
</p>
<CodeBlock className="max-h-56 overflow-y-auto whitespace-pre-wrap break-all">
{certificate.key_pem}
{content.key_pem}
</CodeBlock>
</div>
</div>
@@ -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({
<LoadingState />
) : certificateQuery.isError ? (
<ErrorState
title="证书详情加载失败"
title="证书内容加载失败"
description={getErrorMessage(certificateQuery.error)}
/>
) : !certificateQuery.data ? (