From 927797f3d55691c4f2c7d54d222e7b861845677a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Fri, 30 Jan 2026 14:28:47 +0800 Subject: [PATCH] Add SSL certificate management to WebUI config Introduces backend API endpoints and frontend UI for managing SSL certificates, including viewing status, uploading, and deleting cert/key files. Adds a new SSL configuration tab in the dashboard, allowing users to enable HTTPS by providing PEM-formatted certificate and key, with changes taking effect after restart. --- .../src/api/WebUIConfig.ts | 87 +++++++++- .../src/router/WebUIConfig.ts | 12 ++ .../src/controllers/webui_manager.ts | 29 ++++ .../src/pages/dashboard/config/index.tsx | 6 + .../src/pages/dashboard/config/ssl.tsx | 161 ++++++++++++++++++ 5 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 packages/napcat-webui-frontend/src/pages/dashboard/config/ssl.tsx diff --git a/packages/napcat-webui-backend/src/api/WebUIConfig.ts b/packages/napcat-webui-backend/src/api/WebUIConfig.ts index 12dd21b9..98012a15 100644 --- a/packages/napcat-webui-backend/src/api/WebUIConfig.ts +++ b/packages/napcat-webui-backend/src/api/WebUIConfig.ts @@ -1,7 +1,9 @@ import { RequestHandler } from 'express'; -import { WebUiConfig } from '@/napcat-webui-backend/index'; +import { WebUiConfig, webUiPathWrapper } from '@/napcat-webui-backend/index'; import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response'; import { isEmpty } from '@/napcat-webui-backend/src/utils/check'; +import { existsSync, promises as fsProm } from 'node:fs'; +import { join } from 'node:path'; // 获取WebUI基础配置 export const GetWebUIConfigHandler: RequestHandler = async (_, res) => { @@ -158,3 +160,86 @@ export const UpdateWebUIConfigHandler: RequestHandler = async (req, res) => { return sendError(res, `更新WebUI配置失败: ${msg}`); } }; + +// 获取SSL证书状态 +export const GetSSLStatusHandler: RequestHandler = async (_, res) => { + try { + const certPath = join(webUiPathWrapper.configPath, 'cert.pem'); + const keyPath = join(webUiPathWrapper.configPath, 'key.pem'); + + const certExists = existsSync(certPath); + const keyExists = existsSync(keyPath); + + let certContent = ''; + let keyContent = ''; + + if (certExists) { + certContent = await fsProm.readFile(certPath, 'utf-8'); + } + if (keyExists) { + keyContent = await fsProm.readFile(keyPath, 'utf-8'); + } + + return sendSuccess(res, { + enabled: certExists && keyExists, + certExists, + keyExists, + certContent, + keyContent, + }); + } catch (error) { + const msg = (error as Error).message; + return sendError(res, `获取SSL状态失败: ${msg}`); + } +}; + +// 保存SSL证书(通过文本内容) +export const UploadSSLCertHandler: RequestHandler = async (req, res) => { + try { + const { cert, key } = req.body; + + if (isEmpty(cert) || isEmpty(key)) { + return sendError(res, 'cert和key内容不能为空'); + } + + // 简单验证证书格式 + if (!cert.includes('-----BEGIN CERTIFICATE-----') || !cert.includes('-----END CERTIFICATE-----')) { + return sendError(res, 'cert格式不正确,应为PEM格式的证书'); + } + + if (!key.includes('-----BEGIN') || !key.includes('KEY-----')) { + return sendError(res, 'key格式不正确,应为PEM格式的私钥'); + } + + const certPath = join(webUiPathWrapper.configPath, 'cert.pem'); + const keyPath = join(webUiPathWrapper.configPath, 'key.pem'); + + await fsProm.writeFile(certPath, cert, 'utf-8'); + await fsProm.writeFile(keyPath, key, 'utf-8'); + + return sendSuccess(res, { message: 'SSL证书保存成功,重启后生效' }); + } catch (error) { + const msg = (error as Error).message; + return sendError(res, `保存SSL证书失败: ${msg}`); + } +}; + +// 删除SSL证书 +export const DeleteSSLCertHandler: RequestHandler = async (_, res) => { + try { + const certPath = join(webUiPathWrapper.configPath, 'cert.pem'); + const keyPath = join(webUiPathWrapper.configPath, 'key.pem'); + + if (existsSync(certPath)) { + await fsProm.unlink(certPath); + } + if (existsSync(keyPath)) { + await fsProm.unlink(keyPath); + } + + return sendSuccess(res, { message: 'SSL证书已删除,重启后生效' }); + } catch (error) { + const msg = (error as Error).message; + return sendError(res, `删除SSL证书失败: ${msg}`); + } +}; diff --git a/packages/napcat-webui-backend/src/router/WebUIConfig.ts b/packages/napcat-webui-backend/src/router/WebUIConfig.ts index 9ea7c432..667effc7 100644 --- a/packages/napcat-webui-backend/src/router/WebUIConfig.ts +++ b/packages/napcat-webui-backend/src/router/WebUIConfig.ts @@ -5,6 +5,9 @@ import { UpdateDisableWebUIHandler, UpdateWebUIConfigHandler, GetClientIPHandler, + GetSSLStatusHandler, + UploadSSLCertHandler, + DeleteSSLCertHandler, } from '@/napcat-webui-backend/src/api/WebUIConfig'; const router: Router = Router(); @@ -24,4 +27,13 @@ router.post('/UpdateDisableWebUI', UpdateDisableWebUIHandler); // 获取当前客户端IP router.get('/GetClientIP', GetClientIPHandler); +// 获取SSL证书状态 +router.get('/GetSSLStatus', GetSSLStatusHandler); + +// 上传SSL证书 +router.post('/UploadSSLCert', UploadSSLCertHandler); + +// 删除SSL证书 +router.post('/DeleteSSLCert', DeleteSSLCertHandler); + export { router as WebUIConfigRouter }; diff --git a/packages/napcat-webui-frontend/src/controllers/webui_manager.ts b/packages/napcat-webui-frontend/src/controllers/webui_manager.ts index c283d31c..91d7e04c 100644 --- a/packages/napcat-webui-frontend/src/controllers/webui_manager.ts +++ b/packages/napcat-webui-frontend/src/controllers/webui_manager.ts @@ -281,6 +281,35 @@ export default class WebUIManager { return data.data; } + // 获取SSL证书状态 + public static async getSSLStatus () { + const { data } = await serverRequest.get>('/WebUIConfig/GetSSLStatus'); + return data.data; + } + + // 保存SSL证书 + public static async saveSSLCert (cert: string, key: string) { + const { data } = await serverRequest.post>( + '/WebUIConfig/UploadSSLCert', + { cert, key } + ); + return data.data; + } + + // 删除SSL证书 + public static async deleteSSLCert () { + const { data } = await serverRequest.post>( + '/WebUIConfig/DeleteSSLCert' + ); + return data.data; + } + // Passkey相关方法 public static async generatePasskeyRegistrationOptions () { const { data } = await serverRequest.post>( diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/config/index.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/config/index.tsx index 149f2d2a..711f58d8 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/config/index.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/config/index.tsx @@ -10,6 +10,7 @@ import ChangePasswordCard from './change_password'; import LoginConfigCard from './login'; import OneBotConfigCard from './onebot'; import ServerConfigCard from './server'; +import SSLConfigCard from './ssl'; import ThemeConfigCard from './theme'; import WebUIConfigCard from './webui'; @@ -81,6 +82,11 @@ export default function ConfigPage () { + + + + + diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/config/ssl.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/config/ssl.tsx new file mode 100644 index 00000000..e1be14b0 --- /dev/null +++ b/packages/napcat-webui-frontend/src/pages/dashboard/config/ssl.tsx @@ -0,0 +1,161 @@ +import { useRequest } from 'ahooks'; +import { useEffect, useState } from 'react'; +import toast from 'react-hot-toast'; +import { Button } from '@heroui/button'; +import { Textarea } from '@heroui/input'; + +import PageLoading from '@/components/page_loading'; + +import WebUIManager from '@/controllers/webui_manager'; + +const SSLConfigCard = () => { + const { + data: sslData, + loading: sslLoading, + refreshAsync: refreshSSL, + } = useRequest(WebUIManager.getSSLStatus); + + const [sslCert, setSslCert] = useState(''); + const [sslKey, setSslKey] = useState(''); + const [sslSaving, setSslSaving] = useState(false); + + useEffect(() => { + if (sslData) { + setSslCert(sslData.certContent || ''); + setSslKey(sslData.keyContent || ''); + } + }, [sslData]); + + const handleSaveSSL = async () => { + if (!sslCert.trim() || !sslKey.trim()) { + toast.error('证书和私钥内容不能为空'); + return; + } + setSslSaving(true); + try { + const result = await WebUIManager.saveSSLCert(sslCert, sslKey); + toast.success(result.message || 'SSL证书保存成功'); + await refreshSSL(); + } catch (error) { + const msg = (error as Error).message; + toast.error(`保存SSL证书失败: ${msg}`); + } finally { + setSslSaving(false); + } + }; + + const handleDeleteSSL = async () => { + setSslSaving(true); + try { + const result = await WebUIManager.deleteSSLCert(); + toast.success(result.message || 'SSL证书已删除'); + setSslCert(''); + setSslKey(''); + await refreshSSL(); + } catch (error) { + const msg = (error as Error).message; + toast.error(`删除SSL证书失败: ${msg}`); + } finally { + setSslSaving(false); + } + }; + + const handleRefresh = async () => { + try { + await refreshSSL(); + toast.success('刷新成功'); + } catch (error) { + const msg = (error as Error).message; + toast.error(`刷新失败: ${msg}`); + } + }; + + if (sslLoading) return ; + + return ( + <> + SSL配置 - NapCat WebUI +
+
+
+
SSL/HTTPS 配置
+ {sslData?.enabled && ( + + 已启用 + + )} +
+

+ 配置SSL证书后重启即可启用HTTPS。将证书(cert.pem)和私钥(key.pem)的内容粘贴到下方文本框中。 +

+
+

+ 注意:保存证书后需要重启服务才能生效。删除证书后同样需要重启才能切换回HTTP模式。 +

+
+
+ +
+