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 // CreateTLSCertificate godoc
// @Summary Create TLS certificate from PEM // @Summary Create TLS certificate from PEM
// @Tags TLSCertificates // @Tags TLSCertificates
+1
View File
@@ -93,6 +93,7 @@ func SetApiRouter(router *gin.Engine) {
{ {
tlsCertificateRoute.GET("/", controller.GetTLSCertificates) tlsCertificateRoute.GET("/", controller.GetTLSCertificates)
tlsCertificateRoute.GET("/:id", controller.GetTLSCertificate) tlsCertificateRoute.GET("/:id", controller.GetTLSCertificate)
tlsCertificateRoute.GET("/:id/content", controller.GetTLSCertificateContent)
tlsCertificateRoute.POST("/", controller.CreateTLSCertificate) tlsCertificateRoute.POST("/", controller.CreateTLSCertificate)
tlsCertificateRoute.PUT("/:id", controller.UpdateTLSCertificate) tlsCertificateRoute.PUT("/:id", controller.UpdateTLSCertificate)
tlsCertificateRoute.POST("/import-file", controller.ImportTLSCertificateFile) 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) 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) decodeResponseData(t, detailResp, &certificateDetail)
if certificateDetail.ID != manualCertificate.ID || certificateDetail.CertPEM == "" || certificateDetail.KeyPEM == "" { if _, exists := certificateDetail["cert_pem"]; exists {
t.Fatal("expected certificate detail endpoint to return pem payloads") 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"}) 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"` 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) { func ListTLSCertificates() ([]*model.TLSCertificate, error) {
return model.ListTLSCertificates() return model.ListTLSCertificates()
} }
@@ -24,6 +32,21 @@ func GetTLSCertificate(id uint) (*model.TLSCertificate, error) {
return model.GetTLSCertificateByID(id) 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) { func CreateTLSCertificate(input TLSCertificateInput) (*model.TLSCertificate, error) {
certificate, err := buildTLSCertificate(nil, input) certificate, err := buildTLSCertificate(nil, input)
if err != nil { if err != nil {
@@ -1,6 +1,7 @@
import { apiRequest } from '@/lib/api/client'; import { apiRequest } from '@/lib/api/client';
import type { import type {
TlsCertificateContentItem,
TlsCertificateDetailItem, TlsCertificateDetailItem,
TlsCertificateFileImportPayload, TlsCertificateFileImportPayload,
TlsCertificateItem, TlsCertificateItem,
@@ -22,6 +23,10 @@ export function getTlsCertificate(id: number) {
return apiRequest<TlsCertificateDetailItem>(`/tls-certificates/${id}`); return apiRequest<TlsCertificateDetailItem>(`/tls-certificates/${id}`);
} }
export function getTlsCertificateContent(id: number) {
return apiRequest<TlsCertificateContentItem>(`/tls-certificates/${id}/content`);
}
export function updateTlsCertificate( export function updateTlsCertificate(
id: number, id: number,
payload: TlsCertificateMutationPayload, payload: TlsCertificateMutationPayload,
@@ -11,6 +11,11 @@ export interface TlsCertificateItem {
} }
export interface TlsCertificateDetailItem extends TlsCertificateItem { export interface TlsCertificateDetailItem extends TlsCertificateItem {
cert_pem?: never;
key_pem?: never;
}
export interface TlsCertificateContentItem extends TlsCertificateItem {
cert_pem: string; cert_pem: string;
key_pem: string; key_pem: string;
} }
@@ -7,7 +7,10 @@ import { ErrorState } from '@/components/feedback/error-state';
import { LoadingState } from '@/components/feedback/loading-state'; import { LoadingState } from '@/components/feedback/loading-state';
import { AppModal } from '@/components/ui/app-modal'; import { AppModal } from '@/components/ui/app-modal';
import { StatusBadge } from '@/components/ui/status-badge'; 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 { getCertificateStatus, getErrorMessage } from '@/features/websites/utils';
import { import {
CodeBlock, CodeBlock,
@@ -40,7 +43,14 @@ export function CertificateDetailModal({
enabled: isOpen && certificateId !== null, 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 certificate = certificateQuery.data;
const content = contentQuery.data;
const status = certificate ? getCertificateStatus(certificate) : null; const status = certificate ? getCertificateStatus(certificate) : null;
return ( return (
@@ -72,14 +82,16 @@ export function CertificateDetailModal({
</div> </div>
} }
> >
{certificateQuery.isLoading ? ( {certificateQuery.isLoading || contentQuery.isLoading ? (
<LoadingState /> <LoadingState />
) : certificateQuery.isError ? ( ) : certificateQuery.isError || contentQuery.isError ? (
<ErrorState <ErrorState
title="证书详情加载失败" title="证书详情加载失败"
description={getErrorMessage(certificateQuery.error)} description={getErrorMessage(
certificateQuery.error ?? contentQuery.error,
)}
/> />
) : !certificate ? ( ) : !certificate || !content ? (
<EmptyState <EmptyState
title="证书不存在" title="证书不存在"
description="当前证书可能已被删除。" description="当前证书可能已被删除。"
@@ -138,7 +150,7 @@ export function CertificateDetailModal({
证书 PEM 证书 PEM
</p> </p>
<CodeBlock className="max-h-56 overflow-y-auto whitespace-pre-wrap break-all"> <CodeBlock className="max-h-56 overflow-y-auto whitespace-pre-wrap break-all">
{certificate.cert_pem} {content.cert_pem}
</CodeBlock> </CodeBlock>
</div> </div>
<div> <div>
@@ -146,7 +158,7 @@ export function CertificateDetailModal({
私钥 PEM 私钥 PEM
</p> </p>
<CodeBlock className="max-h-56 overflow-y-auto whitespace-pre-wrap break-all"> <CodeBlock className="max-h-56 overflow-y-auto whitespace-pre-wrap break-all">
{certificate.key_pem} {content.key_pem}
</CodeBlock> </CodeBlock>
</div> </div>
</div> </div>
@@ -11,7 +11,7 @@ import { InlineMessage } from '@/components/feedback/inline-message';
import { LoadingState } from '@/components/feedback/loading-state'; import { LoadingState } from '@/components/feedback/loading-state';
import { AppModal } from '@/components/ui/app-modal'; import { AppModal } from '@/components/ui/app-modal';
import { import {
getTlsCertificate, getTlsCertificateContent,
updateTlsCertificate, updateTlsCertificate,
} from '@/features/tls-certificates/api/tls-certificates'; } from '@/features/tls-certificates/api/tls-certificates';
import type { TlsCertificateItem } from '@/features/tls-certificates/types'; import type { TlsCertificateItem } from '@/features/tls-certificates/types';
@@ -49,8 +49,8 @@ export function CertificateEditorModal({
}); });
const certificateQuery = useQuery({ const certificateQuery = useQuery({
queryKey: ['tls-certificates', 'detail', certificateId], queryKey: ['tls-certificates', 'content', certificateId],
queryFn: () => getTlsCertificate(certificateId as number), queryFn: () => getTlsCertificateContent(certificateId as number),
enabled: isOpen && certificateId !== null, enabled: isOpen && certificateId !== null,
}); });
@@ -120,7 +120,7 @@ export function CertificateEditorModal({
<LoadingState /> <LoadingState />
) : certificateQuery.isError ? ( ) : certificateQuery.isError ? (
<ErrorState <ErrorState
title="证书详情加载失败" title="证书内容加载失败"
description={getErrorMessage(certificateQuery.error)} description={getErrorMessage(certificateQuery.error)}
/> />
) : !certificateQuery.data ? ( ) : !certificateQuery.data ? (