1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-03-14 01:34:47 +08:00

feat: 证书增加下载功能 (#3090)

Refs https://github.com/1Panel-dev/1Panel/issues/2767
This commit is contained in:
zhengkunwang 2023-11-28 17:27:31 +08:00 committed by GitHub
parent 59c216c184
commit 86bc75ff28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 308 additions and 9 deletions

View File

@ -1,7 +1,10 @@
package v1
import (
"net/http"
"net/url"
"reflect"
"strconv"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
@ -213,3 +216,32 @@ func (b *BaseApi) UploadWebsiteSSL(c *gin.Context) {
}
helper.SuccessWithData(c, nil)
}
// @Tags Website SSL
// @Summary Download SSL file
// @Description 下载证书文件
// @Accept json
// @Param request body request.WebsiteResourceReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /websites/ssl/download [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"下载证书文件 [domain]","formatEN":"download ssl file [domain]"}
func (b *BaseApi) DownloadWebsiteSSL(c *gin.Context) {
var req request.WebsiteResourceReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
file, err := websiteSSLService.DownloadFile(req.ID)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
info, err := file.Stat()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
c.Header("Content-Length", strconv.FormatInt(info.Size(), 10))
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name()))
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file)
}

View File

@ -11,6 +11,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/repo"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/i18n"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/files"
@ -41,6 +42,7 @@ type IWebsiteSSLService interface {
Upload(req request.WebsiteSSLUpload) error
ObtainSSL(apply request.WebsiteSSLApply) error
SyncForRestart() error
DownloadFile(id uint) (*os.File, error)
}
func NewIWebsiteSSLService() IWebsiteSSLService {
@ -425,6 +427,34 @@ func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error {
return websiteSSLRepo.Create(context.Background(), websiteSSL)
}
func (w WebsiteSSLService) DownloadFile(id uint) (*os.File, error) {
websiteSSL, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(id))
if err != nil {
return nil, err
}
fileOp := files.NewFileOp()
dir := path.Join(global.CONF.System.BaseDir, "1panel/tmp/ssl", websiteSSL.PrimaryDomain)
if fileOp.Stat(dir) {
if err = fileOp.DeleteDir(dir); err != nil {
return nil, err
}
}
if err = fileOp.CreateDir(dir, 0666); err != nil {
return nil, err
}
if err = fileOp.WriteFile(path.Join(dir, "fullchain.pem"), strings.NewReader(websiteSSL.Pem), 0644); err != nil {
return nil, err
}
if err = fileOp.WriteFile(path.Join(dir, "privkey.pem"), strings.NewReader(websiteSSL.PrivateKey), 0644); err != nil {
return nil, err
}
fileName := websiteSSL.PrimaryDomain + ".zip"
if err = fileOp.Compress([]string{path.Join(dir, "fullchain.pem"), path.Join(dir, "privkey.pem")}, dir, fileName, files.SdkZip); err != nil {
return nil, err
}
return os.Open(path.Join(dir, fileName))
}
func (w WebsiteSSLService) SyncForRestart() error {
sslList, err := websiteSSLRepo.List()
if err != nil {

View File

@ -24,5 +24,6 @@ func (a *WebsiteSSLRouter) InitWebsiteSSLRouter(Router *gin.RouterGroup) {
groupRouter.POST("/update", baseApi.UpdateWebsiteSSL)
groupRouter.POST("/upload", baseApi.UploadWebsiteSSL)
groupRouter.POST("/obtain", baseApi.ApplyWebsiteSSL)
groupRouter.POST("/download", baseApi.DownloadWebsiteSSL)
}
}

View File

@ -12879,6 +12879,57 @@ const docTemplate = `{
}
}
},
"/websites/ssl/download": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "下载证书文件",
"consumes": [
"application/json"
],
"tags": [
"Website SSL"
],
"summary": "Download SSL file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteResourceReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "website_ssls",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "primary_domain",
"output_value": "domain"
}
],
"bodyKeys": [
"id"
],
"formatEN": "download ssl file [domain]",
"formatZH": "下载证书文件 [domain]",
"paramKeys": []
}
}
},
"/websites/ssl/obtain": {
"post": {
"security": [
@ -17478,6 +17529,9 @@ const docTemplate = `{
"createdAt": {
"type": "string"
},
"description": {
"type": "string"
},
"dir": {
"type": "string"
},
@ -19522,14 +19576,33 @@ const docTemplate = `{
"request.WebsiteSSLUpdate": {
"type": "object",
"required": [
"id"
"id",
"type"
],
"properties": {
"autoRenew": {
"type": "boolean"
},
"certificate": {
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"privateKey": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"autoRenew",
"description",
"certificate",
"privateKey"
]
}
}
},
@ -19551,6 +19624,9 @@ const docTemplate = `{
"privateKeyPath": {
"type": "string"
},
"sslID": {
"type": "integer"
},
"type": {
"type": "string",
"enum": [

View File

@ -12872,6 +12872,57 @@
}
}
},
"/websites/ssl/download": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "下载证书文件",
"consumes": [
"application/json"
],
"tags": [
"Website SSL"
],
"summary": "Download SSL file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteResourceReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "website_ssls",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "primary_domain",
"output_value": "domain"
}
],
"bodyKeys": [
"id"
],
"formatEN": "download ssl file [domain]",
"formatZH": "下载证书文件 [domain]",
"paramKeys": []
}
}
},
"/websites/ssl/obtain": {
"post": {
"security": [
@ -17471,6 +17522,9 @@
"createdAt": {
"type": "string"
},
"description": {
"type": "string"
},
"dir": {
"type": "string"
},
@ -19515,14 +19569,33 @@
"request.WebsiteSSLUpdate": {
"type": "object",
"required": [
"id"
"id",
"type"
],
"properties": {
"autoRenew": {
"type": "boolean"
},
"certificate": {
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"privateKey": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"autoRenew",
"description",
"certificate",
"privateKey"
]
}
}
},
@ -19544,6 +19617,9 @@
"privateKeyPath": {
"type": "string"
},
"sslID": {
"type": "integer"
},
"type": {
"type": "string",
"enum": [

View File

@ -2800,6 +2800,8 @@ definitions:
type: string
createdAt:
type: string
description:
type: string
dir:
type: string
dnsAccount:
@ -4178,10 +4180,24 @@ definitions:
properties:
autoRenew:
type: boolean
certificate:
type: string
description:
type: string
id:
type: integer
privateKey:
type: string
type:
enum:
- autoRenew
- description
- certificate
- privateKey
type: string
required:
- id
- type
type: object
request.WebsiteSSLUpload:
properties:
@ -4193,6 +4209,8 @@ definitions:
type: string
privateKeyPath:
type: string
sslID:
type: integer
type:
enum:
- paste
@ -12874,6 +12892,39 @@ paths:
formatEN: Delete ssl [domain]
formatZH: 删除 ssl [domain]
paramKeys: []
/websites/ssl/download:
post:
consumes:
- application/json
description: 下载证书文件
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.WebsiteResourceReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Download SSL file
tags:
- Website SSL
x-panel-log:
BeforeFunctions:
- db: website_ssls
input_column: id
input_value: id
isList: false
output_column: primary_domain
output_value: domain
bodyKeys:
- id
formatEN: download ssl file [domain]
formatZH: 下载证书文件 [domain]
paramKeys: []
/websites/ssl/obtain:
post:
consumes:

View File

@ -490,4 +490,8 @@ export namespace Website {
export interface RenewSSLByCA {
SSLID: number;
}
export interface SSLDownload {
id: number;
}
}

View File

@ -263,3 +263,10 @@ export const DeleteCA = (req: Website.DelReq) => {
export const RenewSSLByCA = (req: Website.RenewSSLByCA) => {
return http.post<any>(`/websites/ca/renew`, req);
};
export const DownloadFile = (params: Website.SSLDownload) => {
return http.download<BlobPart>(`/websites/ssl/download`, params, {
responseType: 'blob',
timeout: TimeoutEnum.T_40S,
});
};

View File

@ -3,15 +3,13 @@
<template #header>
<DrawerHeader :header="$t('ssl.detail')" :back="handleClose" />
</template>
<div>
<div v-loading="loading">
<el-radio-group v-model="curr">
<el-radio-button label="detail">{{ $t('ssl.msg') }}</el-radio-button>
<el-radio-button label="ssl">{{ $t('ssl.ssl') }}</el-radio-button>
<el-radio-button label="key">{{ $t('ssl.key') }}</el-radio-button>
</el-radio-group>
<br />
<br />
<div v-if="curr === 'detail'">
<div v-if="curr === 'detail'" class="mt-5">
<el-descriptions border :column="1">
<el-descriptions-item :label="$t('website.primaryDomain')">
{{ ssl.primaryDomain }}
@ -52,14 +50,14 @@
</el-descriptions-item>
</el-descriptions>
</div>
<div v-else-if="curr === 'ssl'">
<div v-else-if="curr === 'ssl'" class="mt-5">
<el-input v-model="ssl.pem" :autosize="{ minRows: 15, maxRows: 30 }" type="textarea" id="textArea" />
<div>
<br />
<el-button type="primary" @click="copyText(ssl.pem)">{{ $t('file.copy') }}</el-button>
</div>
</div>
<div v-else>
<div v-else class="mt-5">
<el-input
v-model="ssl.privateKey"
:autosize="{ minRows: 15, maxRows: 30 }"
@ -88,6 +86,7 @@ const open = ref(false);
const id = ref(0);
const curr = ref('detail');
const ssl = ref<any>({});
const loading = ref(false);
const handleClose = () => {
open.value = false;

View File

@ -150,7 +150,7 @@
<script lang="ts" setup>
import { onMounted, reactive, ref, computed } from 'vue';
import OpDialog from '@/components/del-dialog/index.vue';
import { DeleteSSL, SearchSSL, UpdateSSL } from '@/api/modules/website';
import { DeleteSSL, DownloadFile, SearchSSL, UpdateSSL } from '@/api/modules/website';
import DnsAccount from './dns-account/index.vue';
import AcmeAccount from './acme-account/index.vue';
import CA from './ca/index.vue';
@ -229,6 +229,12 @@ const buttons = [
return row.provider == 'manual';
},
},
{
label: i18n.global.t('file.download'),
click: function (row: Website.SSLDTO) {
onDownload(row);
},
},
{
label: i18n.global.t('commons.button.delete'),
click: function (row: Website.SSLDTO) {
@ -237,6 +243,23 @@ const buttons = [
},
];
const onDownload = (ssl: Website.SSLDTO) => {
loading.value = true;
DownloadFile({ id: ssl.id })
.then((res) => {
const downloadUrl = window.URL.createObjectURL(new Blob([res]));
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
a.download = ssl.primaryDomain + '.zip';
const event = new MouseEvent('click');
a.dispatchEvent(event);
})
.finally(() => {
loading.value = false;
});
};
const mobile = computed(() => {
return globalStore.isMobile();
});