mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
feat: support uploading local certificate files (#10540)
Refs https://github.com/1Panel-dev/1Panel/issues/8606
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -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: 'すべてのパネル設定がデフォルトに戻されます。続けますか?',
|
||||
|
||||
@@ -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: '모든 패널 설정이 기본값으로 복원됩니다. 계속하시겠습니까?',
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -3402,7 +3402,6 @@ const message = {
|
||||
'Будет отображаться в верхнем левом углу страницы управления при развернутом меню (рекомендуемый размер изображения: 185px*55px)',
|
||||
favicon: 'Иконка Сайта',
|
||||
faviconHelper: 'Иконка сайта (рекомендуемый размер изображения: 16px*16px)',
|
||||
reUpload: 'Выбрать Файл',
|
||||
setDefault: 'Восстановить По Умолчанию',
|
||||
setHelper: 'Текущие настройки будут сохранены. Вы хотите продолжить?',
|
||||
setDefaultHelper: 'Все настройки панели будут восстановлены по умолчанию. Вы хотите продолжить?',
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -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: '即將復原所有介面設定到初始狀態,是否繼續?',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user