feat: support uploading local certificate files (#10540)

Refs https://github.com/1Panel-dev/1Panel/issues/8606
This commit is contained in:
CityFun
2025-09-29 09:24:20 +00:00
committed by GitHub
parent f4a649dc2f
commit 56936aa6cd
15 changed files with 168 additions and 36 deletions
+75
View File
@@ -2,6 +2,8 @@ package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"io"
"mime/multipart"
"net/http"
"net/url"
"reflect"
@@ -217,6 +219,79 @@ func (b *BaseApi) UploadWebsiteSSL(c *gin.Context) {
helper.Success(c)
}
// @Tags Website SSL
// @Summary Upload SSL file
// @Accept multipart/form-data
// @Param type formData string true "type"
// @Param description formData string false "description"
// @Param sslID formData string false "sslID"
// @Param privateKeyFile formData file true "privateKeyFile"
// @Param certificateFile formData file true "certificateFile"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/ssl/upload/file [post]
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"上传 ssl 文件 [type]","formatEN":"Upload ssl file [type]"}
func (b *BaseApi) UploadSSLFile(c *gin.Context) {
var req request.WebsiteSSLFileUpload
req.Type = c.PostForm("type")
req.Description = c.PostForm("description")
sslID := c.PostForm("sslID")
if sslID != "" {
req.SSLID, _ = strconv.ParseUint(sslID, 10, 64)
}
privateKeyFile, err := c.FormFile("privateKeyFile")
if err != nil {
helper.InternalServer(c, err)
return
}
certificateFile, err := c.FormFile("certificateFile")
if err != nil {
helper.InternalServer(c, err)
return
}
privateKeyContent, err := readUploadedFile(privateKeyFile)
if err != nil {
helper.InternalServer(c, err)
return
}
certificateContent, err := readUploadedFile(certificateFile)
if err != nil {
helper.InternalServer(c, err)
return
}
uploadReq := request.WebsiteSSLUpload{
Type: "paste",
PrivateKey: string(privateKeyContent),
Certificate: string(certificateContent),
Description: req.Description,
SSLID: uint(req.SSLID),
}
if err := websiteSSLService.Upload(uploadReq); err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, nil)
}
func readUploadedFile(fileHeader *multipart.FileHeader) ([]byte, error) {
file, err := fileHeader.Open()
if err != nil {
return nil, err
}
defer file.Close()
return io.ReadAll(file)
}
// @Tags Website SSL
// @Summary Download SSL file
// @Accept json
+6
View File
@@ -157,3 +157,9 @@ type WebsiteCAObtain struct {
type WebsiteCARenew struct {
SSLID uint `json:"SSLID" validate:"required"`
}
type WebsiteSSLFileUpload struct {
Type string `json:"type"`
Description string `json:"description"`
SSLID uint64 `json:"sslID"`
}
+1
View File
@@ -24,5 +24,6 @@ func (a *WebsiteSSLRouter) InitRouter(Router *gin.RouterGroup) {
groupRouter.POST("/obtain", baseApi.ApplyWebsiteSSL)
groupRouter.POST("/download", baseApi.DownloadWebsiteSSL)
groupRouter.POST("/import", baseApi.ImportMasterSSL)
groupRouter.POST("/upload/file", baseApi.UploadSSLFile)
}
}
+4
View File
@@ -242,6 +242,10 @@ export const uploadSSL = (req: Website.SSLUpload) => {
return http.post<any>(`/websites/ssl/upload`, req);
};
export const uploadSSLFile = (params: FormData) => {
return http.upload<File.File>(`/websites/ssl/upload/file`, params, {});
};
export const searchCAs = (req: ReqPage) => {
return http.post<ResPage<Website.CA>>(`/websites/ca/search`, req);
};
-2
View File
@@ -524,7 +524,6 @@ const message = {
formatHelper:
'The current database character set is {0}, the character set inconsistency may cause recovery failure',
selectFile: 'Select file',
dropHelper: 'You can drag and drop the uploaded file here or',
clickHelper: 'click to upload',
supportUpType:
@@ -3445,7 +3444,6 @@ const message = {
'Will be displayed in the upper left corner of the management page when the menu is expanded (recommended image size: 185px*55px)',
favicon: 'Website Icon',
faviconHelper: 'Website icon (recommended image size: 16px*16px)',
reUpload: 'Choose File',
setDefault: 'Restore Default',
setHelper: 'The current settings will be saved. Do you want to continue?',
setDefaultHelper: 'All panel settings will be restored to default. Do you want to continue?',
-2
View File
@@ -528,7 +528,6 @@ const message = {
skipVerify: 'Omitir la verificación de validez del certificado',
formatHelper:
'El conjunto de caracteres actual de la base de datos es {0}, la inconsistencia de conjuntos puede causar errores al recuperar',
selectFile: 'Seleccionar archivo',
dropHelper: 'Puede arrastrar y soltar el archivo aquí o',
clickHelper: 'hacer clic para subir',
supportUpType:
@@ -3401,7 +3400,6 @@ const message = {
'Se mostrará arriba a la izquierda cuando el menú esté expandido (tamaño recomendado: 185px*55px)',
favicon: 'Icono del sitio',
faviconHelper: 'Icono del sitio (tamaño recomendado: 16px*16px)',
reUpload: 'Elegir archivo',
setDefault: 'Restaurar por defecto',
setHelper: 'Se guardará la configuración actual. ¿Continuar?',
setDefaultHelper: 'Todas las configuraciones se restaurarán a valores por defecto. ¿Continuar?',
-2
View File
@@ -512,7 +512,6 @@ const message = {
skipVerify: '証明書の有効性チェックを無視します',
formatHelper: '現在のデータベース文字セットは{0}です。文字セットの矛盾は回復の故障を引き起こす可能性があります',
selectFile: '[ファイル]を選択します',
dropHelper: 'ここでアップロードされたファイルをドラッグアンドドロップするか、',
clickHelper: 'クリックしてアップロードします',
supportUpType:
@@ -3330,7 +3329,6 @@ const message = {
'メニューが展開されている場合管理ページの左上隅に表示されます推奨画像サイズ185px*55px',
favicon: 'ウェブサイトアイコン',
faviconHelper: 'ウェブサイトアイコン推奨画像サイズ16px*16px',
reUpload: 'ファイルを選択',
setDefault: 'デフォルトに戻す',
setHelper: '現在の設定が保存されます続けますか',
setDefaultHelper: 'すべてのパネル設定がデフォルトに戻されます続けますか',
-2
View File
@@ -510,7 +510,6 @@ const message = {
skipVerify: '인증서 유효성 검사 무시',
formatHelper: '현재 데이터베이스 문자셋은 {0} 입니다. 문자셋 불일치로 인해 복구에 실패할 수 있습니다.',
selectFile: '파일 선택',
dropHelper: '여기에 업로드한 파일을 드래그 앤 드롭하거나',
clickHelper: '클릭하여 업로드',
supportUpType:
@@ -3273,7 +3272,6 @@ const message = {
'메뉴가 확장되었을 관리 페이지의 왼쪽 상단에 표시됩니다 (권장 이미지 크기: 185px*55px)',
favicon: '웹사이트 아이콘',
faviconHelper: '웹사이트 아이콘 (권장 이미지 크기: 16px*16px)',
reUpload: '파일 선택',
setDefault: '기본값 복원',
setHelper: '현재 설정이 저장됩니다. 계속하시겠습니까?',
setDefaultHelper: '모든 패널 설정이 기본값으로 복원됩니다. 계속하시겠습니까?',
-2
View File
@@ -524,7 +524,6 @@ const message = {
formatHelper:
'Set aksara pangkalan data semasa adalah {0}, ketidakkonsistenan set aksara mungkin menyebabkan kegagalan pemulihan.',
selectFile: 'Pilih fail',
dropHelper: 'Anda boleh seret dan lepaskan fail yang ingin dimuat naik di sini atau',
clickHelper: 'klik untuk memuat naik',
supportUpType:
@@ -3395,7 +3394,6 @@ const message = {
'Akan dipaparkan di sudut kiri atas halaman pengurusan apabila menu diperluaskan (saiz imej yang disarankan: 185px*55px)',
favicon: 'Ikon Laman Web',
faviconHelper: 'Ikon laman web (saiz imej yang disarankan: 16px*16px)',
reUpload: 'Pilih Fail',
setDefault: 'Pulihkan Tetapan Asal',
setHelper: 'Tetapan semasa akan disimpan. Adakah anda ingin meneruskan?',
setDefaultHelper: 'Semua tetapan panel akan dikembalikan ke asal. Adakah anda ingin meneruskan?',
-2
View File
@@ -522,7 +522,6 @@ const message = {
formatHelper:
'O conjunto de caracteres atual do banco de dados é {0}, a inconsistência no conjunto de caracteres pode causar falha na recuperação',
selectFile: 'Selecionar arquivo',
dropHelper: 'Você pode arrastar e soltar o arquivo carregado aqui ou',
clickHelper: 'clicar para fazer upload',
supportUpType:
@@ -3412,7 +3411,6 @@ const message = {
'Será exibido no canto superior esquerdo da página de gerenciamento quando o menu estiver expandido (tamanho recomendado da imagem: 185px*55px)',
favicon: 'Ícone do Site',
faviconHelper: 'Ícone do site (tamanho recomendado da imagem: 16px*16px)',
reUpload: 'Selecionar Arquivo',
setDefault: 'Restaurar Padrão',
setHelper: 'As configurações atuais serão salvas. Deseja continuar?',
setDefaultHelper: 'Todas as configurações do painel serão restauradas para o padrão. Deseja continuar?',
-1
View File
@@ -3402,7 +3402,6 @@ const message = {
'Будет отображаться в верхнем левом углу страницы управления при развернутом меню (рекомендуемый размер изображения: 185px*55px)',
favicon: 'Иконка Сайта',
faviconHelper: 'Иконка сайта (рекомендуемый размер изображения: 16px*16px)',
reUpload: 'Выбрать Файл',
setDefault: 'Восстановить По Умолчанию',
setHelper: 'Текущие настройки будут сохранены. Вы хотите продолжить?',
setDefaultHelper: 'Все настройки панели будут восстановлены по умолчанию. Вы хотите продолжить?',
-2
View File
@@ -530,7 +530,6 @@ const message = {
skipVerify: 'Sertifika geçerlilik kontrolünü yoksay',
formatHelper:
'Mevcut veritabanı karakter seti {0}, karakter seti tutarsızlığı kurtarma işleminin başarısız olmasına neden olabilir',
selectFile: 'Dosya seç',
dropHelper: 'Yüklenen dosyayı buraya sürükleyip bırakabilir veya',
clickHelper: 'yüklemek için tıklayın',
supportUpType:
@@ -3482,7 +3481,6 @@ const message = {
'Menü genişletildiğinde yönetim sayfasının sol üst köşesinde gösterilecektir (önerilen görüntü boyutu: 185px*55px)',
favicon: 'Web Sitesi Simgesi',
faviconHelper: 'Web sitesi simgesi (önerilen görüntü boyutu: 16px*16px)',
reUpload: 'Dosya Seç',
setDefault: 'Varsayılana Geri Yükle',
setHelper: 'Mevcut ayarlar kaydedilecek. Devam etmek istiyor musunuz?',
setDefaultHelper: 'Tüm panel ayarları varsayılana geri yüklenecek. Devam etmek istiyor musunuz?',
-2
View File
@@ -506,7 +506,6 @@ const message = {
skipVerify: '忽略校驗證書可用性檢測',
formatHelper: '目前資料庫字元集為 {0}字元集不一致可能導致復原失敗',
selectFile: '選擇文件',
dropHelper: '將上傳文件拖曳到此處或者',
clickHelper: '點擊上傳',
supportUpType:
@@ -3209,7 +3208,6 @@ const message = {
logoWithTextHelper: '將會顯示在選單展開時管理頁面左上方 (建議圖片大小為: 185px*55px)',
favicon: '網站圖示',
faviconHelper: '網站圖示 (建議圖片大小為: 16px*16px)',
reUpload: '選擇文件',
setDefault: '復原預設',
setHelper: '即將儲存目前介面設定內容是否繼續',
setDefaultHelper: '即將復原所有介面設定到初始狀態是否繼續',
-2
View File
@@ -504,7 +504,6 @@ const message = {
skipVerify: '忽略校验证书可用性检测',
formatHelper: '当前数据库字符集为 {0}字符集不一致可能导致恢复失败',
selectFile: '选择文件',
dropHelper: '将上传文件拖拽到此处或者',
clickHelper: '点击上传',
supportUpType:
@@ -3181,7 +3180,6 @@ const message = {
logoWithTextHelper: '将会显示在菜单展开时管理页面左上方 (建议图片大小为: 185px*55px)',
favicon: '网站图标',
faviconHelper: '网站图标 (建议图片大小为: 16px*16px)',
reUpload: '选择文件',
setHelper: '即将保存当前界面设置内容是否继续',
setDefaultHelper: '即将恢复所有界面设置到初始状态是否继续',
logoGroup: 'Logo',
+82 -17
View File
@@ -5,6 +5,7 @@
<el-select v-model="ssl.type">
<el-option :label="$t('website.pasteSSL')" :value="'paste'"></el-option>
<el-option :label="$t('website.localSSL')" :value="'local'"></el-option>
<el-option :label="$t('commons.button.upload') + $t('menu.files')" :value="'upload'"></el-option>
</el-select>
</el-form-item>
<div v-if="ssl.type === 'paste'">
@@ -31,6 +32,40 @@
</el-input>
</el-form-item>
</div>
<div v-if="ssl.type === 'upload'">
<el-form-item :label="$t('website.privateKey')" prop="privateKeyFile">
<el-upload
ref="privateKeyUpload"
:auto-upload="false"
:limit="1"
:on-change="handlePrivateKeyChange"
:file-list="privateKeyFileList"
class="p-w-200"
>
<template #trigger>
<el-button type="primary" icon="Upload">
{{ $t('file.selectFile') }}
</el-button>
</template>
</el-upload>
</el-form-item>
<el-form-item :label="$t('website.certificate')" prop="certificateFile">
<el-upload
ref="certificateUpload"
:auto-upload="false"
:limit="1"
:on-change="handleCertificateChange"
:file-list="certificateFileList"
class="p-w-200"
>
<template #trigger>
<el-button type="primary" icon="Upload">
{{ $t('file.selectFile') }}
</el-button>
</template>
</el-upload>
</el-form-item>
</div>
<el-form-item :label="$t('website.remark')" prop="description">
<el-input v-model="ssl.description"></el-input>
</el-form-item>
@@ -38,7 +73,7 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(sslForm)" :disabled="loading">
<el-button type="primary" @click="submit()" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
@@ -49,7 +84,7 @@
</template>
<script lang="ts" setup>
import { uploadSSL } from '@/api/modules/website';
import { uploadSSL, uploadSSLFile } from '@/api/modules/website';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { FormInstance } from 'element-plus';
@@ -63,6 +98,10 @@ const keyFileRef = ref();
const certFileRef = ref();
const loading = ref(false);
const sslForm = ref<FormInstance>();
const privateKeyFileList = ref([]);
const certificateFileList = ref([]);
const privateKeyUpload = ref();
const certificateUpload = ref();
const rules = ref({
privateKey: [Rules.requiredInput],
@@ -70,6 +109,8 @@ const rules = ref({
privateKeyPath: [Rules.requiredInput],
certificatePath: [Rules.requiredInput],
type: [Rules.requiredSelect],
certificateFile: [Rules.requiredInput],
privateKeyFile: [Rules.requiredInput],
});
const initData = () => ({
privateKey: '',
@@ -79,6 +120,8 @@ const initData = () => ({
type: 'paste',
sslID: 0,
description: '',
privateKeyFile: null as File | null,
certificateFile: null as File | null,
});
const ssl = ref(initData());
@@ -91,6 +134,18 @@ const handleClose = () => {
const resetForm = () => {
sslForm.value?.resetFields();
ssl.value = initData();
privateKeyFileList.value = [];
certificateFileList.value = [];
};
const handlePrivateKeyChange = (file: any) => {
ssl.value.privateKeyFile = file.raw;
privateKeyFileList.value = [file];
};
const handleCertificateChange = (file: any) => {
ssl.value.certificateFile = file.raw;
certificateFileList.value = [file];
};
const acceptParams = (websiteSSL: Website.SSLDTO) => {
@@ -115,22 +170,32 @@ const getCertificatePath = (path: string) => {
ssl.value.certificatePath = path;
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
const submit = async () => {
try {
await sslForm.value?.validate();
loading.value = true;
uploadSSL(ssl.value)
.then(() => {
handleClose();
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
})
.finally(() => {
loading.value = false;
});
});
if (ssl.value.type === 'upload') {
const formData = new FormData();
formData.append('type', ssl.value.type);
formData.append('description', ssl.value.description);
formData.append('sslID', ssl.value.sslID.toString());
if (ssl.value.privateKeyFile) {
formData.append('privateKeyFile', ssl.value.privateKeyFile);
}
if (ssl.value.certificateFile) {
formData.append('certificateFile', ssl.value.certificateFile);
}
await uploadSSLFile(formData);
} else {
await uploadSSL(ssl.value);
}
handleClose();
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
} catch (err) {
} finally {
loading.value = false;
}
};
defineExpose({