feat: 证书页面增加上传证书功能 (#2735)

Refs https://github.com/1Panel-dev/1Panel/issues/1323
This commit is contained in:
zhengkunwang 2023-10-31 16:01:50 +08:00 committed by GitHub
parent c526e7a5ac
commit d43bc3e427
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 440 additions and 34 deletions

View file

@ -192,3 +192,24 @@ func (b *BaseApi) UpdateWebsiteSSL(c *gin.Context) {
} }
helper.SuccessWithData(c, nil) helper.SuccessWithData(c, nil)
} }
// @Tags Website SSL
// @Summary Upload ssl
// @Description 上传 ssl
// @Accept json
// @Param request body request.WebsiteSSLUpload true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /websites/ssl/upload [post]
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"上传 ssl [type]","formatEN":"Upload ssl [type]"}
func (b *BaseApi) UploadWebsiteSSL(c *gin.Context) {
var req request.WebsiteSSLUpload
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteSSLService.Upload(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

View file

@ -50,3 +50,11 @@ type WebsiteSSLUpdate struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
AutoRenew bool `json:"autoRenew" validate:"required"` AutoRenew bool `json:"autoRenew" validate:"required"`
} }
type WebsiteSSLUpload struct {
PrivateKey string `json:"privateKey"`
Certificate string `json:"certificate"`
PrivateKeyPath string `json:"privateKeyPath"`
CertificatePath string `json:"certificatePath"`
Type string `json:"type" validate:"required,oneof=paste local"`
}

View file

@ -11,6 +11,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/buserr" "github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/ssl" "github.com/1Panel-dev/1Panel/backend/utils/ssl"
"path" "path"
"strconv" "strconv"
@ -30,6 +31,7 @@ type IWebsiteSSLService interface {
GetWebsiteSSL(websiteId uint) (response.WebsiteSSLDTO, error) GetWebsiteSSL(websiteId uint) (response.WebsiteSSLDTO, error)
Delete(id uint) error Delete(id uint) error
Update(update request.WebsiteSSLUpdate) error Update(update request.WebsiteSSLUpdate) error
Upload(req request.WebsiteSSLUpload) error
} }
func NewIWebsiteSSLService() IWebsiteSSLService { func NewIWebsiteSSLService() IWebsiteSSLService {
@ -289,3 +291,60 @@ func (w WebsiteSSLService) Update(update request.WebsiteSSLUpdate) error {
websiteSSL.AutoRenew = update.AutoRenew websiteSSL.AutoRenew = update.AutoRenew
return websiteSSLRepo.Save(websiteSSL) return websiteSSLRepo.Save(websiteSSL)
} }
func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error {
newSSL := &model.WebsiteSSL{
Provider: constant.Manual,
}
if req.Type == "local" {
fileOp := files.NewFileOp()
if !fileOp.Stat(req.PrivateKeyPath) {
return buserr.New("ErrSSLKeyNotFound")
}
if !fileOp.Stat(req.CertificatePath) {
return buserr.New("ErrSSLCertificateNotFound")
}
if content, err := fileOp.GetContent(req.PrivateKeyPath); err != nil {
return err
} else {
newSSL.PrivateKey = string(content)
}
if content, err := fileOp.GetContent(req.CertificatePath); err != nil {
return err
} else {
newSSL.Pem = string(content)
}
} else {
newSSL.PrivateKey = req.PrivateKey
newSSL.Pem = req.Certificate
}
privateKeyCertBlock, _ := pem.Decode([]byte(newSSL.PrivateKey))
if privateKeyCertBlock == nil {
return buserr.New("ErrSSLKeyFormat")
}
certBlock, _ := pem.Decode([]byte(newSSL.Pem))
if certBlock == nil {
return buserr.New("ErrSSLCertificateFormat")
}
cert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return err
}
newSSL.ExpireDate = cert.NotAfter
newSSL.StartDate = cert.NotBefore
newSSL.Type = cert.Issuer.CommonName
if len(cert.Issuer.Organization) > 0 {
newSSL.Organization = cert.Issuer.Organization[0]
} else {
newSSL.Organization = cert.Issuer.CommonName
}
if len(cert.DNSNames) > 0 {
newSSL.PrimaryDomain = cert.DNSNames[0]
newSSL.Domains = strings.Join(cert.DNSNames, ",")
}
return websiteSSLRepo.Create(context.Background(), newSSL)
}

View file

@ -23,5 +23,6 @@ func (a *WebsiteSSLRouter) InitWebsiteSSLRouter(Router *gin.RouterGroup) {
groupRouter.GET("/website/:websiteId", baseApi.GetWebsiteSSLByWebsiteId) groupRouter.GET("/website/:websiteId", baseApi.GetWebsiteSSLByWebsiteId)
groupRouter.GET("/:id", baseApi.GetWebsiteSSLById) groupRouter.GET("/:id", baseApi.GetWebsiteSSLById)
groupRouter.POST("/update", baseApi.UpdateWebsiteSSL) groupRouter.POST("/update", baseApi.UpdateWebsiteSSL)
groupRouter.POST("/upload", baseApi.UploadWebsiteSSL)
} }
} }

View file

@ -1,5 +1,5 @@
// Code generated by swaggo/swag. DO NOT EDIT. // Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag
package docs package docs
import "github.com/swaggo/swag" import "github.com/swaggo/swag"
@ -12234,6 +12234,48 @@ const docTemplate = `{
} }
} }
}, },
"/websites/ssl/upload": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "上传 ssl",
"consumes": [
"application/json"
],
"tags": [
"Website SSL"
],
"summary": "Upload ssl",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteSSLUpload"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"type"
],
"formatEN": "Upload ssl [type]",
"formatZH": "上传 ssl [type]",
"paramKeys": []
}
}
},
"/websites/ssl/website/:websiteId": { "/websites/ssl/website/:websiteId": {
"get": { "get": {
"security": [ "security": [
@ -16861,19 +16903,10 @@ const docTemplate = `{
"type": "boolean" "type": "boolean"
}, },
"sortBy": { "sortBy": {
"type": "string", "type": "string"
"enum": [
"name",
"size",
"modTime"
]
}, },
"sortOrder": { "sortOrder": {
"type": "string", "type": "string"
"enum": [
"ascending",
"descending"
]
} }
} }
}, },
@ -18160,6 +18193,33 @@ const docTemplate = `{
} }
} }
}, },
"request.WebsiteSSLUpload": {
"type": "object",
"required": [
"type"
],
"properties": {
"certificate": {
"type": "string"
},
"certificatePath": {
"type": "string"
},
"privateKey": {
"type": "string"
},
"privateKeyPath": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"paste",
"local"
]
}
}
},
"request.WebsiteSearch": { "request.WebsiteSearch": {
"type": "object", "type": "object",
"required": [ "required": [

View file

@ -12227,6 +12227,48 @@
} }
} }
}, },
"/websites/ssl/upload": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "上传 ssl",
"consumes": [
"application/json"
],
"tags": [
"Website SSL"
],
"summary": "Upload ssl",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteSSLUpload"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"type"
],
"formatEN": "Upload ssl [type]",
"formatZH": "上传 ssl [type]",
"paramKeys": []
}
}
},
"/websites/ssl/website/:websiteId": { "/websites/ssl/website/:websiteId": {
"get": { "get": {
"security": [ "security": [
@ -16854,19 +16896,10 @@
"type": "boolean" "type": "boolean"
}, },
"sortBy": { "sortBy": {
"type": "string", "type": "string"
"enum": [
"name",
"size",
"modTime"
]
}, },
"sortOrder": { "sortOrder": {
"type": "string", "type": "string"
"enum": [
"ascending",
"descending"
]
} }
} }
}, },
@ -18153,6 +18186,33 @@
} }
} }
}, },
"request.WebsiteSSLUpload": {
"type": "object",
"required": [
"type"
],
"properties": {
"certificate": {
"type": "string"
},
"certificatePath": {
"type": "string"
},
"privateKey": {
"type": "string"
},
"privateKeyPath": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"paste",
"local"
]
}
}
},
"request.WebsiteSearch": { "request.WebsiteSearch": {
"type": "object", "type": "object",
"required": [ "required": [

View file

@ -2963,15 +2963,8 @@ definitions:
showHidden: showHidden:
type: boolean type: boolean
sortBy: sortBy:
enum:
- name
- size
- modTime
type: string type: string
sortOrder: sortOrder:
enum:
- ascending
- descending
type: string type: string
type: object type: object
request.FilePathCheck: request.FilePathCheck:
@ -3838,6 +3831,24 @@ definitions:
- autoRenew - autoRenew
- id - id
type: object type: object
request.WebsiteSSLUpload:
properties:
certificate:
type: string
certificatePath:
type: string
privateKey:
type: string
privateKeyPath:
type: string
type:
enum:
- paste
- local
type: string
required:
- type
type: object
request.WebsiteSearch: request.WebsiteSearch:
properties: properties:
name: name:
@ -12080,6 +12091,33 @@ paths:
formatEN: Update ssl config [domain] formatEN: Update ssl config [domain]
formatZH: 更新证书设置 [domain] formatZH: 更新证书设置 [domain]
paramKeys: [] paramKeys: []
/websites/ssl/upload:
post:
consumes:
- application/json
description: 上传 ssl
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.WebsiteSSLUpload'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Upload ssl
tags:
- Website SSL
x-panel-log:
BeforeFunctions: []
bodyKeys:
- type
formatEN: Upload ssl [type]
formatZH: 上传 ssl [type]
paramKeys: []
/websites/ssl/website/:websiteId: /websites/ssl/website/:websiteId:
get: get:
consumes: consumes:

View file

@ -439,4 +439,12 @@ export namespace Website {
userGroup: string; userGroup: string;
msg: string; msg: string;
} }
export interface SSLUpload {
privateKey: string;
certificate: string;
privateKeyPath: string;
certificatePath: string;
type: string;
}
} }

View file

@ -239,3 +239,7 @@ export const ChangePHPVersion = (req: Website.PHPVersionChange) => {
export const GetDirConfig = (req: Website.ProxyReq) => { export const GetDirConfig = (req: Website.ProxyReq) => {
return http.post<Website.DirConfig>(`/websites/dir`, req); return http.post<Website.DirConfig>(`/websites/dir`, req);
}; };
export const UploadSSL = (req: Website.SSLUpload) => {
return http.post<any>(`/websites/ssl/upload`, req);
};

View file

@ -1715,6 +1715,7 @@ const message = {
'This certificate has been associated with the following websites, and the renewal will be applied to these websites simultaneously', 'This certificate has been associated with the following websites, and the renewal will be applied to these websites simultaneously',
createAcme: 'Create Account', createAcme: 'Create Account',
acmeHelper: 'Acme account is used to apply for free certificates', acmeHelper: 'Acme account is used to apply for free certificates',
upload: 'Upload Certificate',
}, },
firewall: { firewall: {
create: 'Create rule', create: 'Create rule',

View file

@ -1626,6 +1626,7 @@ const message = {
renewWebsite: '該證書已經和以下網站關聯續簽會同步應用到這些網站', renewWebsite: '該證書已經和以下網站關聯續簽會同步應用到這些網站',
createAcme: '創建賬戶', createAcme: '創建賬戶',
acmeHelper: 'Acme 賬戶用於申請免費證書', acmeHelper: 'Acme 賬戶用於申請免費證書',
upload: '上傳證書',
}, },
firewall: { firewall: {
create: '創建規則', create: '創建規則',

View file

@ -1626,6 +1626,7 @@ const message = {
renewWebsite: '该证书已经和以下网站关联续签会同步应用到这些网站', renewWebsite: '该证书已经和以下网站关联续签会同步应用到这些网站',
createAcme: '创建账户', createAcme: '创建账户',
acmeHelper: 'Acme 账户用于申请免费证书', acmeHelper: 'Acme 账户用于申请免费证书',
upload: '上传证书',
}, },
firewall: { firewall: {
create: '创建规则', create: '创建规则',

View file

@ -28,9 +28,12 @@
> >
{{ ssl.acmeAccount.email }} {{ ssl.acmeAccount.email }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :label="$t('website.brand')"> <el-descriptions-item :label="$t('commons.table.type')">
{{ ssl.type }} {{ ssl.type }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :label="$t('website.brand')">
{{ ssl.organization }}
</el-descriptions-item>
<el-descriptions-item :label="$t('ssl.startDate')"> <el-descriptions-item :label="$t('ssl.startDate')">
{{ dateFormatSimple(ssl.startDate) }} {{ dateFormatSimple(ssl.startDate) }}
</el-descriptions-item> </el-descriptions-item>

View file

@ -13,6 +13,9 @@
<el-button type="primary" @click="openSSL()"> <el-button type="primary" @click="openSSL()">
{{ $t('ssl.create') }} {{ $t('ssl.create') }}
</el-button> </el-button>
<el-button type="primary" @click="openUpload()">
{{ $t('ssl.upload') }}
</el-button>
<el-button type="primary" plain @click="openAcmeAccount()"> <el-button type="primary" plain @click="openAcmeAccount()">
{{ $t('website.acmeAccountManage') }} {{ $t('website.acmeAccountManage') }}
</el-button> </el-button>
@ -79,6 +82,7 @@
<Create ref="sslCreateRef" @close="search()"></Create> <Create ref="sslCreateRef" @close="search()"></Create>
<Renew ref="renewRef" @close="search()"></Renew> <Renew ref="renewRef" @close="search()"></Renew>
<Detail ref="detailRef"></Detail> <Detail ref="detailRef"></Detail>
<SSLUpload ref="sslUploadRef" @close="search()"></SSLUpload>
</LayoutContent> </LayoutContent>
<OpDialog ref="opRef" @search="search" /> <OpDialog ref="opRef" @search="search" />
@ -99,6 +103,7 @@ import i18n from '@/lang';
import { Website } from '@/api/interface/website'; import { Website } from '@/api/interface/website';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import SSLUpload from './upload/index.vue';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const paginationConfig = reactive({ const paginationConfig = reactive({
@ -112,9 +117,10 @@ const dnsAccountRef = ref();
const sslCreateRef = ref(); const sslCreateRef = ref();
const renewRef = ref(); const renewRef = ref();
const detailRef = ref(); const detailRef = ref();
let data = ref(); const data = ref();
let loading = ref(false); const loading = ref(false);
const opRef = ref(); const opRef = ref();
const sslUploadRef = ref();
const routerButton = [ const routerButton = [
{ {
@ -187,6 +193,9 @@ const openDnsAccount = () => {
const openSSL = () => { const openSSL = () => {
sslCreateRef.value.acceptParams(); sslCreateRef.value.acceptParams();
}; };
const openUpload = () => {
sslUploadRef.value.acceptParams();
};
const openRenewSSL = (id: number, websites: Website.Website[]) => { const openRenewSSL = (id: number, websites: Website.Website[]) => {
renewRef.value.acceptParams({ id: id, websites: websites }); renewRef.value.acceptParams({ id: id, websites: websites });
}; };

View file

@ -0,0 +1,132 @@
<template>
<el-drawer :close-on-click-modal="false" v-model="open" size="50%">
<template #header>
<DrawerHeader :header="$t('ssl.upload')" :back="handleClose" />
</template>
<el-row v-loading="loading">
<el-col :span="22" :offset="1">
<el-form ref="sslForm" label-position="top" :model="ssl" label-width="100px" :rules="rules">
<el-form-item :label="$t('website.importType')" prop="type">
<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-select>
</el-form-item>
<div v-if="ssl.type === 'paste'">
<el-form-item :label="$t('website.privateKey')" prop="privateKey">
<el-input v-model="ssl.privateKey" :rows="6" type="textarea" />
</el-form-item>
<el-form-item :label="$t('website.certificate')" prop="certificate">
<el-input v-model="ssl.certificate" :rows="6" type="textarea" />
</el-form-item>
</div>
<div v-if="ssl.type === 'local'">
<el-form-item :label="$t('website.privateKeyPath')" prop="privateKeyPath">
<el-input v-model="ssl.privateKeyPath">
<template #prepend>
<FileList @choose="getPrivateKeyPath" :dir="false"></FileList>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('website.certificatePath')" prop="certificatePath">
<el-input v-model="ssl.certificatePath">
<template #prepend>
<FileList @choose="getCertificatePath" :dir="false"></FileList>
</template>
</el-input>
</el-form-item>
</div>
</el-form>
</el-col>
</el-row>
<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">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import DrawerHeader from '@/components/drawer-header/index.vue';
import { UploadSSL } from '@/api/modules/website';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { FormInstance } from 'element-plus';
import { ref } from 'vue';
import { MsgSuccess } from '@/utils/message';
const open = ref(false);
const loading = ref(false);
const sslForm = ref<FormInstance>();
const rules = ref({
privateKey: [Rules.requiredInput],
certificate: [Rules.requiredInput],
privateKeyPath: [Rules.requiredInput],
certificatePath: [Rules.requiredInput],
type: [Rules.requiredSelect],
});
const ssl = ref({
privateKey: '',
certificate: '',
privateKeyPath: '',
certificatePath: '',
type: 'paste',
});
const em = defineEmits(['close']);
const handleClose = () => {
resetForm();
open.value = false;
em('close', false);
};
const resetForm = () => {
sslForm.value?.resetFields();
ssl.value = {
privateKey: '',
certificate: '',
privateKeyPath: '',
certificatePath: '',
type: 'paste',
};
};
const acceptParams = () => {
resetForm();
open.value = true;
};
const getPrivateKeyPath = (path: string) => {
ssl.value.privateKeyPath = path;
};
const getCertificatePath = (path: string) => {
ssl.value.certificatePath = path;
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
loading.value = true;
UploadSSL(ssl.value)
.then(() => {
handleClose();
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
})
.finally(() => {
loading.value = false;
});
});
};
defineExpose({
acceptParams,
});
</script>