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)
}
// @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"`
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/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/ssl"
"path"
"strconv"
@ -30,6 +31,7 @@ type IWebsiteSSLService interface {
GetWebsiteSSL(websiteId uint) (response.WebsiteSSLDTO, error)
Delete(id uint) error
Update(update request.WebsiteSSLUpdate) error
Upload(req request.WebsiteSSLUpload) error
}
func NewIWebsiteSSLService() IWebsiteSSLService {
@ -289,3 +291,60 @@ func (w WebsiteSSLService) Update(update request.WebsiteSSLUpdate) error {
websiteSSL.AutoRenew = update.AutoRenew
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("/:id", baseApi.GetWebsiteSSLById)
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
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": {
"get": {
"security": [
@ -16861,19 +16903,10 @@ const docTemplate = `{
"type": "boolean"
},
"sortBy": {
"type": "string",
"enum": [
"name",
"size",
"modTime"
]
"type": "string"
},
"sortOrder": {
"type": "string",
"enum": [
"ascending",
"descending"
]
"type": "string"
}
}
},
@ -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": {
"type": "object",
"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": {
"get": {
"security": [
@ -16854,19 +16896,10 @@
"type": "boolean"
},
"sortBy": {
"type": "string",
"enum": [
"name",
"size",
"modTime"
]
"type": "string"
},
"sortOrder": {
"type": "string",
"enum": [
"ascending",
"descending"
]
"type": "string"
}
}
},
@ -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": {
"type": "object",
"required": [

View file

@ -2963,15 +2963,8 @@ definitions:
showHidden:
type: boolean
sortBy:
enum:
- name
- size
- modTime
type: string
sortOrder:
enum:
- ascending
- descending
type: string
type: object
request.FilePathCheck:
@ -3838,6 +3831,24 @@ definitions:
- autoRenew
- id
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:
properties:
name:
@ -12080,6 +12091,33 @@ paths:
formatEN: Update ssl config [domain]
formatZH: 更新证书设置 [domain]
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:
get:
consumes:

View file

@ -439,4 +439,12 @@ export namespace Website {
userGroup: 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) => {
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',
createAcme: 'Create Account',
acmeHelper: 'Acme account is used to apply for free certificates',
upload: 'Upload Certificate',
},
firewall: {
create: 'Create rule',

View file

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

View file

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

View file

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

View file

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