mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 06:47:20 +08:00
feat: 手动签发证书
This commit is contained in:
@@ -108,10 +108,7 @@ 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" {
|
||||
if err == nil && req.Type == "upload" {
|
||||
req.Domains = info.DNSNames
|
||||
}
|
||||
|
||||
@@ -147,11 +144,11 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
|
||||
client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data)
|
||||
} else {
|
||||
if cert.Website == nil {
|
||||
return nil, errors.New("该证书没有关联网站,无法自动签发")
|
||||
return nil, errors.New("this certificate is not associated with a website and cannot be signed. You can try to sign it manually")
|
||||
} else {
|
||||
for _, domain := range cert.Domains {
|
||||
if strings.Contains(domain, "*") {
|
||||
return nil, errors.New("通配符域名无法使用 HTTP 验证")
|
||||
return nil, errors.New("wildcard domains cannot use HTTP verification")
|
||||
}
|
||||
}
|
||||
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
|
||||
@@ -185,7 +182,7 @@ func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
|
||||
}
|
||||
|
||||
if r.client == nil {
|
||||
return nil, errors.New("请重新获取 DNS 解析记录")
|
||||
return nil, errors.New("please retry the manual obtain operation")
|
||||
}
|
||||
|
||||
ssl, err := r.client.ObtainCertificateManual()
|
||||
@@ -219,18 +216,18 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
|
||||
}
|
||||
|
||||
if cert.CertURL == "" {
|
||||
return nil, errors.New("该证书没有签发成功,无法续签")
|
||||
return nil, errors.New("this certificate has not been signed successfully and cannot be renewed")
|
||||
}
|
||||
|
||||
if cert.DNS != nil {
|
||||
client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data)
|
||||
} else {
|
||||
if cert.Website == nil {
|
||||
return nil, errors.New("该证书没有关联网站,无法续签,可以尝试手动签发")
|
||||
return nil, errors.New("this certificate is not associated with a website and cannot be signed. You can try to sign it manually")
|
||||
} else {
|
||||
for _, domain := range cert.Domains {
|
||||
if strings.Contains(domain, "*") {
|
||||
return nil, errors.New("通配符域名无法使用 HTTP 验证")
|
||||
return nil, errors.New("wildcard domains cannot use HTTP verification")
|
||||
}
|
||||
}
|
||||
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
|
||||
@@ -290,7 +287,7 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
||||
}
|
||||
|
||||
if cert.Cert == "" || cert.Key == "" {
|
||||
return errors.New("该证书没有签发成功,无法部署")
|
||||
return errors.New("this certificate has not been signed successfully and cannot be deployed")
|
||||
}
|
||||
|
||||
website, err := NewWebsiteRepo().Get(WebsiteID)
|
||||
@@ -314,7 +311,7 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
||||
|
||||
func (r *certRepo) getClient(cert *biz.Cert) (*acme.Client, error) {
|
||||
if cert.Account == nil {
|
||||
return nil, errors.New("该证书没有关联账号,无法签发")
|
||||
return nil, errors.New("this certificate is not associated with an ACME account and cannot be signed")
|
||||
}
|
||||
|
||||
var ca string
|
||||
|
||||
@@ -18,8 +18,8 @@ type CertUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required,exists=certs id"`
|
||||
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"`
|
||||
Cert string `form:"cert" json:"cert"`
|
||||
Key string `form:"key" json:"key"`
|
||||
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
|
||||
AccountID uint `form:"account_id" json:"account_id"`
|
||||
DNSID uint `form:"dns_id" json:"dns_id"`
|
||||
|
||||
@@ -86,7 +86,8 @@ func Http(r chi.Router) {
|
||||
r.Put("/{id}", cert.Update)
|
||||
r.Get("/{id}", cert.Get)
|
||||
r.Delete("/{id}", cert.Delete)
|
||||
r.Post("/{id}/obtain", cert.Obtain)
|
||||
r.Post("/{id}/obtainAuto", cert.ObtainAuto)
|
||||
r.Post("/{id}/obtainManual", cert.ObtainManual)
|
||||
r.Post("/{id}/renew", cert.Renew)
|
||||
r.Post("/{id}/manualDNS", cert.ManualDNS)
|
||||
r.Post("/{id}/deploy", cert.Deploy)
|
||||
|
||||
@@ -194,26 +194,29 @@ func (s *CertService) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *CertService) Obtain(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *CertService) ObtainAuto(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := s.certRepo.Get(req.ID)
|
||||
if err != nil {
|
||||
if _, err = s.certRepo.ObtainAuto(req.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if cert.DNS != nil || cert.Website != nil {
|
||||
_, err = s.certRepo.ObtainAuto(req.ID)
|
||||
} else {
|
||||
_, err = s.certRepo.ObtainManual(req.ID)
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *CertService) ObtainManual(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if _, err = s.certRepo.ObtainManual(req.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,9 +47,12 @@ export default {
|
||||
request.put(`/cert/cert/${id}`, data),
|
||||
// 证书删除
|
||||
certDelete: (id: number): Promise<AxiosResponse<any>> => request.delete(`/cert/cert/${id}`),
|
||||
// 签发
|
||||
obtain: (id: number): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/cert/cert/${id}/obtain`, { id }),
|
||||
// 证书自动签发
|
||||
obtainAuto: (id: number): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/cert/cert/${id}/obtainAuto`, { id }),
|
||||
// 证书手动签发
|
||||
obtainManual: (id: number): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/cert/cert/${id}/obtainManual`, { id }),
|
||||
// 续签
|
||||
renew: (id: number): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/cert/cert/${id}/renew`, { id }),
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import Editor from '@guolao/vue-monaco-editor'
|
||||
import type { MessageReactive } from 'naive-ui'
|
||||
import { NButton, NDataTable, NFlex, NPopconfirm, NSpace, NSwitch, NTable, NTag } from 'naive-ui'
|
||||
import { NButton, NDataTable, NFlex, NPopconfirm, NSpace, NSwitch, NTag } from 'naive-ui'
|
||||
|
||||
import cert from '@/api/panel/cert'
|
||||
import { formatDateTime } from '@/utils'
|
||||
import ObtainModal from '@/views/cert/ObtainModal.vue'
|
||||
import type { Cert } from '@/views/cert/types'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -40,37 +41,37 @@ const deployModel = ref<any>({
|
||||
id: null,
|
||||
websites: []
|
||||
})
|
||||
const obtain = ref(false)
|
||||
const obtainCert = ref(0)
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: '域名',
|
||||
key: 'domains',
|
||||
minWidth: 150,
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return h(
|
||||
'span',
|
||||
{
|
||||
type: row.status == 'active' ? 'success' : 'error'
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
if (row.domains == null || row.domains.length == 0) {
|
||||
return '无'
|
||||
}
|
||||
return row.domains.join(', ')
|
||||
}
|
||||
}
|
||||
)
|
||||
if (row.domains == null || row.domains.length == 0) {
|
||||
return h(NTag, null, { default: () => '无' })
|
||||
}
|
||||
return h(NFlex, null, {
|
||||
default: () =>
|
||||
row.domains.map((domain: any) =>
|
||||
h(
|
||||
NTag,
|
||||
{ type: 'primary' },
|
||||
{
|
||||
default: () => domain
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
key: 'type',
|
||||
width: 100,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return h(
|
||||
NTag,
|
||||
@@ -122,33 +123,40 @@ const columns: any = [
|
||||
{
|
||||
title: '颁发者',
|
||||
key: 'issuer',
|
||||
minWidth: 100,
|
||||
resizable: true,
|
||||
width: 150,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: 'info',
|
||||
bordered: false
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return row.issuer == '' ? '无' : row.issuer
|
||||
}
|
||||
}
|
||||
)
|
||||
return row.issuer == '' ? '无' : row.issuer
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '过期时间',
|
||||
key: 'not_after',
|
||||
width: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return formatDateTime(row.not_after)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'OCSP',
|
||||
key: 'ocsp_server',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
render(row: any) {
|
||||
if (row.ocsp_server == null || row.ocsp_server.length == 0) {
|
||||
return h(NTag, null, { default: () => '无' })
|
||||
}
|
||||
return h(NFlex, null, {
|
||||
default: () =>
|
||||
row.ocsp_server.map((server: any) =>
|
||||
h(NTag, null, {
|
||||
default: () => server
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '自动续签',
|
||||
key: 'auto_renew',
|
||||
@@ -173,7 +181,7 @@ const columns: any = [
|
||||
resizable: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
row.cert_url == ''
|
||||
row.type != 'upload' && row.account_id != 0 && row.cert == '' && row.key == ''
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
@@ -181,69 +189,8 @@ const columns: any = [
|
||||
type: 'info',
|
||||
style: 'margin-left: 15px;',
|
||||
onClick: async () => {
|
||||
messageReactive = window.$message.loading('请稍后...', {
|
||||
duration: 0
|
||||
})
|
||||
// 没有设置 DNS 接口和网站则获取解析记录
|
||||
if (row.dns_id == 0 && row.website_id == 0) {
|
||||
const { data } = await cert.manualDNS(row.id)
|
||||
messageReactive.destroy()
|
||||
window.$message.info('请先前往域名处设置 DNS 解析,再继续签发')
|
||||
const d = window.$dialog.info({
|
||||
style: 'width: 60vw',
|
||||
title: '待设置DNS 记录列表',
|
||||
content: () => {
|
||||
return h(NTable, [
|
||||
h('thead', [
|
||||
h('tr', [
|
||||
h('th', '域名'),
|
||||
h('th', '类型'),
|
||||
h('th', '主机记录'),
|
||||
h('th', '记录值')
|
||||
])
|
||||
]),
|
||||
h(
|
||||
'tbody',
|
||||
data.map((item: any) =>
|
||||
h('tr', [
|
||||
h('td', item?.domain),
|
||||
h('td', 'TXT'),
|
||||
h('td', item?.name),
|
||||
h('td', item?.value)
|
||||
])
|
||||
)
|
||||
)
|
||||
])
|
||||
},
|
||||
positiveText: '签发',
|
||||
onPositiveClick: async () => {
|
||||
d.loading = true
|
||||
messageReactive = window.$message.loading('请稍后...', {
|
||||
duration: 0
|
||||
})
|
||||
cert
|
||||
.obtain(row.id)
|
||||
.then(() => {
|
||||
window.$message.success('签发成功')
|
||||
onPageChange(1)
|
||||
})
|
||||
.finally(() => {
|
||||
d.loading = false
|
||||
messageReactive?.destroy()
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
cert
|
||||
.obtain(row.id)
|
||||
.then(() => {
|
||||
window.$message.success('签发成功')
|
||||
onPageChange(1)
|
||||
})
|
||||
.finally(() => {
|
||||
messageReactive?.destroy()
|
||||
})
|
||||
}
|
||||
obtain.value = true
|
||||
obtainCert.value = row.id
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -443,7 +390,7 @@ onUnmounted(() => {
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="1400"
|
||||
:scroll-x="1600"
|
||||
:loading="false"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
@@ -463,7 +410,7 @@ onUnmounted(() => {
|
||||
:segmented="false"
|
||||
>
|
||||
<n-space vertical>
|
||||
<n-alert type="info">
|
||||
<n-alert v-if="updateModel.type != 'upload'" type="info">
|
||||
可以通过选择网站 / DNS 中的任意一项来自动签发和部署证书,也可以手动输入域名并设置 DNS
|
||||
解析来签发证书
|
||||
</n-alert>
|
||||
@@ -587,6 +534,7 @@ onUnmounted(() => {
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-modal>
|
||||
<obtain-modal v-model:id="obtainCert" v-model:show="obtain" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
104
web/src/views/cert/ObtainModal.vue
Normal file
104
web/src/views/cert/ObtainModal.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import cert from '@/api/panel/cert'
|
||||
import type { MessageReactive } from 'naive-ui'
|
||||
import { NButton, NTable } from 'naive-ui'
|
||||
|
||||
let messageReactive: MessageReactive | null = null
|
||||
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const id = defineModel<number>('id', { type: Number, required: true })
|
||||
|
||||
const model = ref({
|
||||
type: 'auto'
|
||||
})
|
||||
|
||||
const options = [
|
||||
{ label: '自动验证', value: 'auto' },
|
||||
{ label: '手动 DNS 验证', value: 'manual' }
|
||||
]
|
||||
|
||||
const handleSubmit = async () => {
|
||||
messageReactive = window.$message.loading('请稍后...', {
|
||||
duration: 0
|
||||
})
|
||||
if (model.value.type == 'auto') {
|
||||
await cert
|
||||
.obtainAuto(id.value)
|
||||
.then(() => {
|
||||
window.$message.success('签发成功')
|
||||
show.value = false
|
||||
})
|
||||
.finally(() => {
|
||||
messageReactive?.destroy()
|
||||
window.$bus.emit('cert:refresh-cert')
|
||||
window.$bus.emit('cert:refresh-async')
|
||||
})
|
||||
} else {
|
||||
const { data } = await cert.manualDNS(id.value)
|
||||
messageReactive.destroy()
|
||||
window.$message.info('请先前往域名处设置 DNS 解析,再继续签发')
|
||||
const d = window.$dialog.info({
|
||||
style: 'width: 60vw',
|
||||
title: '待设置DNS 记录列表',
|
||||
content: () => {
|
||||
return h(NTable, [
|
||||
h('thead', [
|
||||
h('tr', [h('th', '域名'), h('th', '类型'), h('th', '主机记录'), h('th', '记录值')])
|
||||
]),
|
||||
h(
|
||||
'tbody',
|
||||
data.map((item: any) =>
|
||||
h('tr', [
|
||||
h('td', item?.domain),
|
||||
h('td', 'TXT'),
|
||||
h('td', item?.name),
|
||||
h('td', item?.value)
|
||||
])
|
||||
)
|
||||
)
|
||||
])
|
||||
},
|
||||
positiveText: '签发',
|
||||
onPositiveClick: async () => {
|
||||
d.loading = true
|
||||
messageReactive = window.$message.loading('请稍后...', {
|
||||
duration: 0
|
||||
})
|
||||
await cert
|
||||
.obtainManual(id.value)
|
||||
.then(() => {
|
||||
window.$message.success('签发成功')
|
||||
show.value = false
|
||||
})
|
||||
.finally(() => {
|
||||
d.loading = false
|
||||
messageReactive?.destroy()
|
||||
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-form :model="model">
|
||||
<n-form-item path="type" label="签发模式">
|
||||
<n-select v-model:value="model.type" :options="options" />
|
||||
</n-form-item>
|
||||
<n-button type="info" block @click="handleSubmit">提交</n-button>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user