2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 16:10:59 +08:00

feat: 支持上传本地证书

This commit is contained in:
耗子
2024-10-27 02:35:04 +08:00
parent cf5f7bd866
commit 56ae7ebfa0
16 changed files with 304 additions and 115 deletions

View File

@@ -5,6 +5,7 @@ import (
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/acme"
"github.com/TheTNB/panel/pkg/types"
)
type Cert struct {
@@ -27,9 +28,10 @@ type Cert struct {
}
type CertRepo interface {
List(page, limit uint) ([]*Cert, int64, error)
List(page, limit uint) ([]*types.CertList, int64, error)
Get(id uint) (*Cert, error)
GetByWebsite(WebsiteID uint) (*Cert, error)
Upload(req *request.CertUpload) (*Cert, error)
Create(req *request.CertCreate) (*Cert, error)
Update(req *request.CertUpdate) error
Delete(id uint) error

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"
"time"
@@ -11,9 +12,11 @@ import (
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/acme"
pkgcert "github.com/TheTNB/panel/pkg/cert"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/systemctl"
"github.com/TheTNB/panel/pkg/types"
)
type certRepo struct {
@@ -24,11 +27,37 @@ func NewCertRepo() biz.CertRepo {
return &certRepo{}
}
func (r *certRepo) List(page, limit uint) ([]*biz.Cert, int64, error) {
func (r *certRepo) List(page, limit uint) ([]*types.CertList, int64, error) {
var certs []*biz.Cert
var total int64
err := app.Orm.Model(&biz.Cert{}).Preload("Website").Preload("Account").Preload("DNS").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&certs).Error
return certs, total, err
var list []*types.CertList
for cert := range slices.Values(certs) {
item := &types.CertList{
ID: cert.ID,
AccountID: cert.AccountID,
WebsiteID: cert.WebsiteID,
DNSID: cert.DNSID,
Type: cert.Type,
Domains: cert.Domains,
AutoRenew: cert.AutoRenew,
Cert: cert.Cert,
Key: cert.Key,
CreatedAt: cert.CreatedAt,
UpdatedAt: cert.UpdatedAt,
}
if decode, err := pkgcert.ParseCert(cert.Cert); err == nil {
item.NotBefore = decode.NotBefore
item.NotAfter = decode.NotAfter
item.Issuer = decode.Issuer.CommonName
item.OCSPServer = decode.OCSPServer
item.DNSNames = decode.DNSNames
}
list = append(list, item)
}
return list, total, err
}
func (r *certRepo) Get(id uint) (*biz.Cert, error) {
@@ -43,6 +72,25 @@ func (r *certRepo) GetByWebsite(WebsiteID uint) (*biz.Cert, error) {
return cert, err
}
func (r *certRepo) Upload(req *request.CertUpload) (*biz.Cert, error) {
info, err := pkgcert.ParseCert(req.Cert)
if err != nil {
return nil, err
}
cert := &biz.Cert{
Type: "upload",
Domains: info.DNSNames,
Cert: req.Cert,
Key: req.Key,
}
if err = app.Orm.Create(cert).Error; err != nil {
return nil, err
}
return cert, nil
}
func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) {
cert := &biz.Cert{
AccountID: req.AccountID,
@@ -59,12 +107,22 @@ func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) {
}
func (r *certRepo) Update(req *request.CertUpdate) error {
info, err := pkgcert.ParseCert(req.Cert)
if err != nil {
return err
}
if req.Type == "upload" {
req.Domains = info.DNSNames
}
return app.Orm.Model(&biz.Cert{}).Where("id = ?", req.ID).Select("*").Updates(&biz.Cert{
ID: req.ID,
AccountID: req.AccountID,
WebsiteID: req.WebsiteID,
DNSID: req.DNSID,
Type: req.Type,
Cert: req.Cert,
Key: req.Key,
Domains: req.Domains,
AutoRenew: req.AutoRenew,
}).Error

View File

@@ -7,6 +7,7 @@ import (
"path/filepath"
"slices"
"strings"
"time"
"github.com/samber/lo"
"github.com/spf13/cast"
@@ -137,8 +138,8 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
setting.SSLCertificateKey = key
// 解析证书信息
if decode, err := cert.ParseCert(crt); err == nil {
setting.SSLNotBefore = decode.NotBefore.Format("2006-01-02 15:04:05")
setting.SSLNotAfter = decode.NotAfter.Format("2006-01-02 15:04:05")
setting.SSLNotBefore = decode.NotBefore.Format(time.DateTime)
setting.SSLNotAfter = decode.NotAfter.Format(time.DateTime)
setting.SSLIssuer = decode.Issuer.CommonName
setting.SSLOCSPServer = decode.OCSPServer
setting.SSLDNSNames = decode.DNSNames

View File

@@ -1,5 +1,10 @@
package request
type CertUpload struct {
Cert string `form:"cert" json:"cert" validate:"required"`
Key string `form:"key" json:"key" validate:"required"`
}
type CertCreate struct {
Type string `form:"type" json:"type" validate:"required,oneof=P256 P384 2048 3072 4096"`
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
@@ -11,8 +16,10 @@ type CertCreate struct {
type CertUpdate struct {
ID uint `form:"id" json:"id" validate:"required,exists=certs id"`
Type string `form:"type" json:"type" validate:"required,oneof=P256 P384 2048 3072 4096"`
Type string `form:"type" json:"type" validate:"required,oneof=upload P256 P384 2048 3072 4096"`
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
Cert string `form:"cert" json:"cert" validate:"required"`
Key string `form:"key" json:"key" validate:"required"`
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
AccountID uint `form:"account_id" json:"account_id"`
DNSID uint `form:"dns_id" json:"dns_id"`

View File

@@ -34,7 +34,7 @@ func (r *CertRenew) Run() {
}
for _, cert := range certs {
if !cert.AutoRenew {
if cert.Type == "upload" || !cert.AutoRenew {
continue
}

View File

@@ -61,7 +61,7 @@ func (r *Monitoring) Run() {
if day <= 0 || app.Status != app.StatusNormal {
return
}
if err = app.Orm.Where("created_at < ?", time.Now().AddDate(0, 0, -day).Format("2006-01-02 15:04:05")).Delete(&biz.Monitor{}).Error; err != nil {
if err = app.Orm.Where("created_at < ?", time.Now().AddDate(0, 0, -day).Format(time.DateTime)).Delete(&biz.Monitor{}).Error; err != nil {
app.Logger.Error("删除过期系统监控失败", zap.Error(err))
return
}

View File

@@ -82,6 +82,7 @@ func Http(r chi.Router) {
r.Route("/cert", func(r chi.Router) {
r.Get("/", cert.List)
r.Post("/", cert.Create)
r.Post("/upload", cert.Upload)
r.Put("/{id}", cert.Update)
r.Get("/{id}", cert.Get)
r.Delete("/{id}", cert.Delete)

View File

@@ -115,6 +115,22 @@ func (s *CertService) List(w http.ResponseWriter, r *http.Request) {
})
}
func (s *CertService) Upload(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.CertUpload](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
cert, err := s.certRepo.Upload(req)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, cert)
}
func (s *CertService) Create(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.CertCreate](r)
if err != nil {

View File

@@ -168,7 +168,7 @@ func (s *CliService) UserList(ctx context.Context, cmd *cli.Command) error {
}
for _, user := range users {
fmt.Printf("ID: %d, 用户名: %s, 邮箱: %s, 创建日期: %s\n", user.ID, user.Username, user.Email, user.CreatedAt.Format("2006-01-02 15:04:05"))
fmt.Printf("ID: %d, 用户名: %s, 邮箱: %s, 创建日期: %s\n", user.ID, user.Username, user.Email, user.CreatedAt.Format(time.DateTime))
}
return nil

View File

@@ -46,13 +46,13 @@ func Now(address ...string) (time.Time, error) {
return now, nil
}
func UpdateSystemTime(time time.Time) error {
_, err := shell.Execf(`date -s '%s'`, time.Format("2006-01-02 15:04:05"))
func UpdateSystemTime(t time.Time) error {
_, err := shell.Execf(`date -s '%s'`, t.Format(time.DateTime))
return err
}
func UpdateSystemTimeZone(timezone string) error {
_, err := shell.Execf(`timedatectl set-timezone '%s'`, timezone)
func UpdateSystemTimeZone(tz string) error {
_, err := shell.Execf(`timedatectl set-timezone '%s'`, tz)
return err
}

22
pkg/types/cert.go Normal file
View File

@@ -0,0 +1,22 @@
package types
import "time"
type CertList struct {
ID uint `json:"id"`
AccountID uint `json:"account_id"`
WebsiteID uint `json:"website_id"`
DNSID uint `json:"dns_id"`
Type string `json:"type"`
Domains []string `json:"domains"`
AutoRenew bool `json:"auto_renew"`
Cert string `json:"cert"`
Key string `json:"key"`
NotBefore time.Time `json:"not_before"`
NotAfter time.Time `json:"not_after"`
Issuer string `json:"issuer"`
OCSPServer []string `json:"ocsp_server"`
DNSNames []string `json:"dns_names"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@@ -38,6 +38,8 @@ export default {
request.get('/cert/cert', { params: { page, limit } }),
// 证书详情
certInfo: (id: number): Promise<AxiosResponse<any>> => request.get(`/cert/cert/${id}`),
// 证书上传
certUpload: (data: any): Promise<AxiosResponse<any>> => request.post('/cert/cert/upload', data),
// 证书添加
certCreate: (data: any): Promise<AxiosResponse<any>> => request.post('/cert/cert', data),
// 证书更新

View File

@@ -8,7 +8,10 @@ body {
height: 100%;
overflow: hidden;
background-color: #f2f2f2;
font-family: -apple-system, "Noto Sans", "Helvetica Neue", Helvetica, "Nimbus Sans L", Arial, "Liberation Sans", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC", "Source Han Sans CN", "Microsoft YaHei", "Wenquanyi Micro Hei", "WenQuanYi Zen Hei", "ST Heiti", SimHei, "WenQuanYi Zen Hei Sharp", sans-serif;;
font-family: -apple-system, 'Noto Sans', 'Helvetica Neue', Helvetica, 'Nimbus Sans L', Arial,
'Liberation Sans', 'PingFang SC', 'Hiragino Sans GB', 'Noto Sans CJK SC', 'Noto Sans SC',
'Source Han Sans SC', 'Source Han Sans CN', 'Microsoft YaHei', 'Wenquanyi Micro Hei',
'WenQuanYi Zen Hei', 'ST Heiti', SimHei, 'WenQuanYi Zen Hei Sharp', sans-serif;
}
#app {

View File

@@ -4,37 +4,39 @@ import type { MessageReactive } from 'naive-ui'
import { NButton, NDataTable, NFlex, NPopconfirm, NSpace, NSwitch, NTable, NTag } from 'naive-ui'
import cert from '@/api/panel/cert'
import { formatDateTime } from '@/utils'
import type { Cert } from '@/views/cert/types'
const props = defineProps({
algorithms: Array<any>,
websites: Array<any>,
accounts: Array<any>,
dns: Array<any>,
caProviders: Array<any>
dns: Array<any>
})
const { algorithms, websites, accounts, dns, caProviders } = toRefs(props)
const { algorithms, websites, accounts, dns } = toRefs(props)
let messageReactive: MessageReactive | null = null
const updateCertModel = ref<any>({
const updateModel = ref<any>({
domains: [],
type: 'P256',
dns_id: null,
account_id: null,
website_id: null,
auto_renew: true
})
const updateCertModal = ref(false)
const updateCert = ref<any>()
const showModal = ref(false)
const showCertModel = ref<any>({
auto_renew: true,
cert: '',
key: ''
})
const deployCertModal = ref(false)
const deployCertModel = ref<any>({
const updateModal = ref(false)
const updateCert = ref<any>()
const showModal = ref(false)
const showModel = ref<any>({
cert: '',
key: ''
})
const deployModal = ref(false)
const deployModel = ref<any>({
id: null,
websites: []
})
@@ -43,7 +45,7 @@ const columns: any = [
{
title: '域名',
key: 'domains',
minWidth: 200,
minWidth: 150,
resizable: true,
ellipsis: { tooltip: true },
render(row: any) {
@@ -53,7 +55,12 @@ const columns: any = [
type: row.status == 'active' ? 'success' : 'error'
},
{
default: () => row.domains.join(', ')
default: () => {
if (row.domains == null || row.domains.length == 0) {
return '无'
}
return row.domains.join(', ')
}
}
)
}
@@ -83,7 +90,7 @@ const columns: any = [
case '4096':
return 'RSA 4096'
default:
return '未知'
return '上传'
}
}
}
@@ -93,7 +100,7 @@ const columns: any = [
{
title: '关联账号',
key: 'account_id',
minWidth: 400,
minWidth: 200,
resizable: true,
ellipsis: { tooltip: true },
render(row: any) {
@@ -102,51 +109,44 @@ const columns: any = [
}
return h(NFlex, null, {
default: () => [
h(NTag, null, { default: () => (row.account?.email == null ? '无' : row.account.email) }),
h(NTag, null, {
default: () =>
caProviders?.value?.find((item: any) => item.value === row.account?.ca)?.label
row.account_id == 0
? '无'
: accounts?.value?.find((item: any) => item.value === row.account_id)?.label
})
]
})
}
},
{
title: '关联网站',
key: 'website_id',
minWidth: 150,
title: '颁发者',
key: 'issuer',
minWidth: 100,
resizable: true,
ellipsis: { tooltip: true },
render(row: any) {
return h(
NTag,
{
type: row.website == null ? 'error' : 'success',
type: 'info',
bordered: false
},
{
default: () => (row.website?.name == null ? '无' : row.website.name)
default: () => {
return row.issuer == '' ? '无' : row.issuer
}
}
)
}
},
{
title: '关联DNS',
key: 'dns_id',
width: 150,
title: '过期时间',
key: 'not_after',
width: 200,
resizable: true,
ellipsis: { tooltip: true },
render(row: any) {
return h(
NTag,
{
type: row.dns == null ? 'error' : 'success',
bordered: false
},
{
default: () => (row.dns?.name == null ? '无' : row.dns.name)
}
)
return formatDateTime(row.not_after)
}
},
{
@@ -258,11 +258,11 @@ const columns: any = [
size: 'small',
type: 'info',
onClick: () => {
deployCertModel.value.id = row.id
deployModel.value.id = row.id
if (row.website_id != 0) {
deployCertModel.value.websites.push(row.website_id)
deployModel.value.websites.push(row.website_id)
}
deployCertModal.value = true
deployModal.value = true
}
},
{
@@ -270,7 +270,7 @@ const columns: any = [
}
)
: null,
row.cert != '' && row.key != ''
row.cert != '' && row.key != '' && row.type != 'upload'
? h(
NButton,
{
@@ -300,8 +300,8 @@ const columns: any = [
type: 'tertiary',
style: 'margin-left: 15px;',
onClick: () => {
showCertModel.value.cert = row.cert
showCertModel.value.key = row.key
showModel.value.cert = row.cert
showModel.value.key = row.key
showModal.value = true
}
},
@@ -318,13 +318,15 @@ const columns: any = [
style: 'margin-left: 15px;',
onClick: () => {
updateCert.value = row.id
updateCertModel.value.domains = row.domains
updateCertModel.value.type = row.type
updateCertModel.value.dns_id = row.dns_id == 0 ? null : row.dns_id
updateCertModel.value.account_id = row.account_id == 0 ? null : row.account_id
updateCertModel.value.website_id = row.website_id == 0 ? null : row.website_id
updateCertModel.value.auto_renew = row.auto_renew
updateCertModal.value = true
updateModel.value.domains = row.domains
updateModel.value.type = row.type
updateModel.value.dns_id = row.dns_id == 0 ? null : row.dns_id
updateModel.value.account_id = row.account_id == 0 ? null : row.account_id
updateModel.value.website_id = row.website_id == 0 ? null : row.website_id
updateModel.value.auto_renew = row.auto_renew
updateModel.value.cert = row.cert
updateModel.value.key = row.key
updateModal.value = true
}
},
{
@@ -395,31 +397,33 @@ const getCertList = async (page: number, limit: number) => {
}
const handleUpdateCert = async () => {
await cert.certUpdate(updateCert.value, updateCertModel.value)
await cert.certUpdate(updateCert.value, updateModel.value)
window.$message.success('更新成功')
updateCertModal.value = false
updateModal.value = false
onPageChange(1)
updateCertModel.value.domains = []
updateCertModel.value.type = 'P256'
updateCertModel.value.dns_id = null
updateCertModel.value.account_id = null
updateCertModel.value.website_id = null
updateCertModel.value.auto_renew = true
updateModel.value.domains = []
updateModel.value.type = 'P256'
updateModel.value.dns_id = null
updateModel.value.account_id = null
updateModel.value.website_id = null
updateModel.value.auto_renew = true
updateModel.value.cert = ''
updateModel.value.key = ''
}
const handleDeployCert = async () => {
for (const website of deployCertModel.value.websites) {
await cert.deploy(deployCertModel.value.id, website)
for (const website of deployModel.value.websites) {
await cert.deploy(deployModel.value.id, website)
}
window.$message.success('部署成功')
deployCertModal.value = false
deployCertModel.value.id = null
deployCertModel.value.websites = []
deployModal.value = false
deployModel.value.id = null
deployModel.value.websites = []
}
const handleShowModalClose = () => {
showCertModel.value.cert = ''
showCertModel.value.key = ''
showModel.value.cert = ''
showModel.value.key = ''
}
onMounted(() => {
@@ -450,7 +454,7 @@ onUnmounted(() => {
/>
</n-space>
<n-modal
v-model:show="updateCertModal"
v-model:show="updateModal"
preset="card"
title="修改证书"
style="width: 60vw"
@@ -463,18 +467,18 @@ onUnmounted(() => {
可以通过选择网站 / DNS 中的任意一项来自动签发和部署证书,也可以手动输入域名并设置 DNS
解析来签发证书
</n-alert>
<n-form :model="updateCertModel">
<n-form-item label="域名">
<n-form :model="updateModel">
<n-form-item v-if="updateModel.type != 'upload'" path="domains" label="域名">
<n-dynamic-input
v-model:value="updateCertModel.domains"
v-model:value="updateModel.domains"
placeholder="example.com"
:min="1"
show-sort-button
/>
</n-form-item>
<n-form-item path="type" label="密钥类型">
<n-form-item v-if="updateModel.type != 'upload'" path="type" label="密钥类型">
<n-select
v-model:value="updateCertModel.type"
v-model:value="updateModel.type"
placeholder="选择密钥类型"
clearable
:options="algorithms"
@@ -482,34 +486,48 @@ onUnmounted(() => {
</n-form-item>
<n-form-item path="website_id" label="网站">
<n-select
v-model:value="updateCertModel.website_id"
v-model:value="updateModel.website_id"
placeholder="选择用于部署证书的网站"
clearable
:options="websites"
/>
</n-form-item>
<n-form-item path="account_id" label="账号">
<n-form-item v-if="updateModel.type != 'upload'" path="account_id" label="账号">
<n-select
v-model:value="updateCertModel.account_id"
v-model:value="updateModel.account_id"
placeholder="选择用于签发证书的账号"
clearable
:options="accounts"
/>
</n-form-item>
<n-form-item path="account_id" label="DNS">
<n-form-item v-if="updateModel.type != 'upload'" path="account_id" label="DNS">
<n-select
v-model:value="updateCertModel.dns_id"
v-model:value="updateModel.dns_id"
placeholder="选择用于签发证书的DNS"
clearable
:options="dns"
/>
</n-form-item>
<n-form-item v-if="updateModel.type == 'upload'" path="cert" label="证书">
<n-input
v-model:value="updateModel.cert"
type="textarea"
placeholder="输入 PEM 证书文件的内容"
/>
</n-form-item>
<n-form-item v-if="updateModel.type == 'upload'" path="key" label="私钥">
<n-input
v-model:value="updateModel.key"
type="textarea"
placeholder="输入 KEY 私钥文件的内容"
/>
</n-form-item>
</n-form>
<n-button type="info" block @click="handleUpdateCert">提交</n-button>
</n-space>
</n-modal>
<n-modal
v-model:show="deployCertModal"
v-model:show="deployModal"
preset="card"
title="部署证书"
style="width: 60vw"
@@ -518,10 +536,10 @@ onUnmounted(() => {
:segmented="false"
>
<n-space vertical>
<n-form :model="deployCertModel">
<n-form :model="deployModel">
<n-form-item path="website_id" label="网站">
<n-select
v-model:value="deployCertModel.websites"
v-model:value="deployModel.websites"
placeholder="选择需要部署证书的网站"
clearable
multiple
@@ -545,7 +563,7 @@ onUnmounted(() => {
<n-tabs type="line" animated>
<n-tab-pane name="cert" tab="证书">
<Editor
v-model:value="showCertModel.cert"
v-model:value="showModel.cert"
theme="vs-dark"
height="60vh"
mt-8
@@ -557,7 +575,7 @@ onUnmounted(() => {
</n-tab-pane>
<n-tab-pane name="key" tab="密钥">
<Editor
v-model:value="showCertModel.key"
v-model:value="showModel.key"
theme="vs-dark"
height="60vh"
mt-8

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import UploadCertModal from '@/views/cert/UploadCertModal.vue'
defineOptions({
name: 'cert-index'
})
@@ -17,8 +19,9 @@ import DnsView from '@/views/cert/DnsView.vue'
const currentTab = ref('cert')
const createDNS = ref(false)
const uploadCert = ref(false)
const createCert = ref(false)
const createDNS = ref(false)
const createAccount = ref(false)
const algorithms = ref<any>([])
@@ -83,28 +86,28 @@ onUnmounted(() => {
<template>
<common-page show-footer>
<template #action>
<n-button v-if="currentTab == 'cert'" type="primary" @click="createCert = true">
<TheIcon :size="18" icon="material-symbols:add" />
创建证书
</n-button>
<n-button v-if="currentTab == 'user'" type="primary" @click="createAccount = true">
<TheIcon :size="18" icon="material-symbols:add" />
创建账号
</n-button>
<n-button v-if="currentTab == 'dns'" type="primary" @click="createDNS = true">
<TheIcon :size="18" icon="material-symbols:add" />
创建 DNS
</n-button>
<n-flex>
<n-button v-if="currentTab == 'cert'" type="success" @click="uploadCert = true">
<TheIcon :size="18" icon="material-symbols:upload" />
上传证书
</n-button>
<n-button v-if="currentTab == 'cert'" type="primary" @click="createCert = true">
<TheIcon :size="18" icon="material-symbols:add" />
创建证书
</n-button>
<n-button v-if="currentTab == 'user'" type="primary" @click="createAccount = true">
<TheIcon :size="18" icon="material-symbols:add" />
创建账号
</n-button>
<n-button v-if="currentTab == 'dns'" type="primary" @click="createDNS = true">
<TheIcon :size="18" icon="material-symbols:add" />
创建 DNS
</n-button>
</n-flex>
</template>
<n-tabs v-model:value="currentTab" type="line" animated>
<n-tab-pane name="cert" tab="证书列表">
<cert-view
:accounts="accounts"
:algorithms="algorithms"
:websites="websites"
:dns="dns"
:ca-providers="caProviders"
/>
<cert-view :accounts="accounts" :algorithms="algorithms" :websites="websites" :dns="dns" />
</n-tab-pane>
<n-tab-pane name="user" tab="账号列表">
<account-view :ca-providers="caProviders" :algorithms="algorithms" />
@@ -114,6 +117,7 @@ onUnmounted(() => {
</n-tab-pane>
</n-tabs>
</common-page>
<upload-cert-modal v-model:show="uploadCert" />
<create-cert-modal
v-model:show="createCert"
:accounts="accounts"

View File

@@ -0,0 +1,55 @@
<script setup lang="ts">
import cert from '@/api/panel/cert'
import { NButton, NSpace } from 'naive-ui'
const show = defineModel<boolean>('show', { type: Boolean, required: true })
const model = ref<any>({
cert: '',
key: ''
})
const handleSubmit = async () => {
await cert.certUpload(model.value)
show.value = false
window.$message.success('创建成功')
model.value.cert = ''
model.value.key = ''
window.$bus.emit('cert:refresh-cert')
window.$bus.emit('cert:refresh-async')
}
</script>
<template>
<n-modal
v-model:show="show"
preset="card"
title="上传证书"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
>
<n-space vertical>
<n-form :model="model">
<n-form-item label="证书">
<n-input
v-model:value="model.cert"
type="textarea"
placeholder="输入 PEM 证书文件的内容"
/>
</n-form-item>
<n-form-item label="私钥">
<n-input
v-model:value="model.key"
type="textarea"
placeholder="输入 KEY 私钥文件的内容"
/>
</n-form-item>
</n-form>
<n-button type="info" block @click="handleSubmit">提交</n-button>
</n-space>
</n-modal>
</template>
<style scoped lang="scss"></style>