mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 06:47:20 +08:00
feat: 支持一键签发证书
This commit is contained in:
@@ -12,7 +12,7 @@ type Cert struct {
|
||||
AccountID uint `gorm:"not null" json:"account_id"` // 关联的 ACME 账户 ID
|
||||
WebsiteID uint `gorm:"not null" json:"website_id"` // 关联的网站 ID
|
||||
DNSID uint `gorm:"not null" json:"dns_id"` // 关联的 DNS ID
|
||||
Type string `gorm:"not null" json:"type"` // 证书类型 (P256, P384, 2048, 4096)
|
||||
Type string `gorm:"not null" json:"type"` // 证书类型 (P256, P384, 2048, 3072, 4096)
|
||||
Domains []string `gorm:"not null;serializer:json" json:"domains"`
|
||||
AutoRenew bool `gorm:"not null" json:"auto_renew"` // 自动续签
|
||||
CertURL string `gorm:"not null" json:"cert_url"` // 证书 URL (续签时使用)
|
||||
|
||||
@@ -13,7 +13,7 @@ type CertAccount struct {
|
||||
Kid string `gorm:"not null" json:"kid"`
|
||||
HmacEncoded string `gorm:"not null" json:"hmac_encoded"`
|
||||
PrivateKey string `gorm:"not null" json:"private_key"`
|
||||
KeyType string `gorm:"not null" json:"key_type"`
|
||||
KeyType string `gorm:"not null" json:"key_type"` // 密钥类型 (P256, P384, 2048, 3072, 4096)
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
@@ -22,6 +22,7 @@ type CertAccount struct {
|
||||
|
||||
type CertAccountRepo interface {
|
||||
List(page, limit uint) ([]*CertAccount, int64, error)
|
||||
GetDefault(userID uint) (*CertAccount, error)
|
||||
Get(id uint) (*CertAccount, error)
|
||||
Create(req *request.CertAccountCreate) (*CertAccount, error)
|
||||
Update(req *request.CertAccountUpdate) error
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
@@ -33,4 +34,5 @@ type WebsiteRepo interface {
|
||||
UpdateRemark(id uint, remark string) error
|
||||
ResetConfig(id uint) error
|
||||
UpdateStatus(id uint, status bool) error
|
||||
ObtainCert(ctx context.Context, id uint) error
|
||||
}
|
||||
|
||||
@@ -17,14 +17,11 @@ import (
|
||||
)
|
||||
|
||||
type certRepo struct {
|
||||
client *acme.Client
|
||||
websiteRepo biz.WebsiteRepo
|
||||
client *acme.Client
|
||||
}
|
||||
|
||||
func NewCertRepo() biz.CertRepo {
|
||||
return &certRepo{
|
||||
websiteRepo: NewWebsiteRepo(),
|
||||
}
|
||||
return &certRepo{}
|
||||
}
|
||||
|
||||
func (r *certRepo) List(page, limit uint) ([]*biz.Cert, int64, error) {
|
||||
@@ -231,7 +228,7 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
||||
return errors.New("该证书没有签发成功,无法部署")
|
||||
}
|
||||
|
||||
website, err := r.websiteRepo.Get(WebsiteID)
|
||||
website, err := NewWebsiteRepo().Get(WebsiteID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -28,6 +28,21 @@ func (r certAccountRepo) List(page, limit uint) ([]*biz.CertAccount, int64, erro
|
||||
return accounts, total, err
|
||||
}
|
||||
|
||||
func (r certAccountRepo) GetDefault(userID uint) (*biz.CertAccount, error) {
|
||||
user, err := NewUserRepo().Get(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &request.CertAccountCreate{
|
||||
CA: acme.CAGoogleCN,
|
||||
Email: user.Email,
|
||||
KeyType: string(acme.KeyEC256),
|
||||
}
|
||||
|
||||
return r.Create(req)
|
||||
}
|
||||
|
||||
func (r certAccountRepo) Get(id uint) (*biz.CertAccount, error) {
|
||||
account := new(biz.CertAccount)
|
||||
err := app.Orm.Model(&biz.CertAccount{}).Where("id = ?", id).First(account).Error
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/embed"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/acme"
|
||||
"github.com/TheTNB/panel/pkg/cert"
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
@@ -27,9 +31,7 @@ type websiteRepo struct {
|
||||
}
|
||||
|
||||
func NewWebsiteRepo() biz.WebsiteRepo {
|
||||
return &websiteRepo{
|
||||
settingRepo: NewSettingRepo(),
|
||||
}
|
||||
return &websiteRepo{}
|
||||
}
|
||||
|
||||
func (r *websiteRepo) UpdateDefaultConfig(req *request.WebsiteDefaultConfig) error {
|
||||
@@ -483,6 +485,8 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
|
||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"))
|
||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost/cert", website.Name+".pem"))
|
||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost/cert", website.Name+".key"))
|
||||
_ = io.Remove(filepath.Join(app.Root, "wwwlogs", website.Name+".log"))
|
||||
_ = io.Remove(filepath.Join(app.Root, "wwwlogs", website.Name+".error.log"))
|
||||
|
||||
if req.Path {
|
||||
_ = io.Remove(website.Path)
|
||||
@@ -493,11 +497,10 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
|
||||
return err
|
||||
}
|
||||
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
|
||||
if err != nil {
|
||||
return err
|
||||
if err == nil {
|
||||
_ = mysql.DatabaseDrop(website.Name)
|
||||
_ = mysql.UserDrop(website.Name)
|
||||
}
|
||||
_ = mysql.DatabaseDrop(website.Name)
|
||||
_ = mysql.UserDrop(website.Name)
|
||||
_, _ = shell.Execf(`echo "DROP DATABASE IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name)
|
||||
_, _ = shell.Execf(`echo "DROP USER IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name)
|
||||
}
|
||||
@@ -669,3 +672,37 @@ func (r *websiteRepo) UpdateStatus(id uint, status bool) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
|
||||
website, err := r.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if slices.Contains(website.Domains, "*") {
|
||||
return errors.New("cannot one-key obtain wildcard certificate")
|
||||
}
|
||||
|
||||
account, err := NewCertAccountRepo().GetDefault(cast.ToUint(ctx.Value("user_id")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cRepo := NewCertRepo()
|
||||
newCert, err := cRepo.Create(&request.CertCreate{
|
||||
Type: string(acme.KeyEC256),
|
||||
Domains: website.Domains,
|
||||
AutoRenew: true,
|
||||
AccountID: account.ID,
|
||||
WebsiteID: website.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = cRepo.ObtainAuto(newCert.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cRepo.Deploy(newCert.ID, website.ID)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package request
|
||||
|
||||
type CertCreate struct {
|
||||
Type string `form:"type" json:"type" validate:"required"`
|
||||
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"`
|
||||
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
|
||||
AccountID uint `form:"account_id" json:"account_id"`
|
||||
@@ -11,7 +11,7 @@ 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"`
|
||||
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"`
|
||||
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
|
||||
AccountID uint `form:"account_id" json:"account_id"`
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package request
|
||||
|
||||
type CertAccountCreate struct {
|
||||
CA string `form:"ca" json:"ca" validate:"required"`
|
||||
CA string `form:"ca" json:"ca" validate:"required,oneof=googlecn google letsencrypt buypass zerossl sslcom"`
|
||||
Email string `form:"email" json:"email" validate:"required"`
|
||||
Kid string `form:"kid" json:"kid"`
|
||||
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
|
||||
KeyType string `form:"key_type" json:"key_type" validate:"required"`
|
||||
KeyType string `form:"key_type" json:"key_type" validate:"required,oneof=P256 P384 2048 3072 4096"`
|
||||
}
|
||||
|
||||
type CertAccountUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required,exists=cert_accounts id"`
|
||||
CA string `form:"ca" json:"ca" validate:"required"`
|
||||
CA string `form:"ca" json:"ca" validate:"required,oneof=googlecn google letsencrypt buypass zerossl sslcom"`
|
||||
Email string `form:"email" json:"email" validate:"required"`
|
||||
Kid string `form:"kid" json:"kid"`
|
||||
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
|
||||
KeyType string `form:"key_type" json:"key_type" validate:"required"`
|
||||
KeyType string `form:"key_type" json:"key_type" validate:"required,oneof=P256 P384 2048 3072 4096"`
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ func Http(r chi.Router) {
|
||||
r.Post("/{id}/updateRemark", website.UpdateRemark)
|
||||
r.Post("/{id}/resetConfig", website.ResetConfig)
|
||||
r.Post("/{id}/status", website.UpdateStatus)
|
||||
r.Post("/{id}/obtainCert", website.ObtainCert)
|
||||
})
|
||||
|
||||
r.Route("/backup", func(r chi.Router) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/go-rat/chix"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
|
||||
@@ -202,3 +202,18 @@ func (s *WebsiteService) UpdateStatus(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *WebsiteService) ObtainCert(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.websiteRepo.ObtainCert(r.Context(), req.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -3,18 +3,19 @@ package service
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/ssh"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type WsService struct {
|
||||
|
||||
@@ -20,16 +20,18 @@ export default {
|
||||
config: (id: number): Promise<AxiosResponse<any>> => request.get('/website/' + id),
|
||||
// 保存网站配置
|
||||
saveConfig: (id: number, data: any): Promise<AxiosResponse<any>> =>
|
||||
request.put('/website/' + id, data),
|
||||
request.put(`/website/${id}`, data),
|
||||
// 清空日志
|
||||
clearLog: (id: number): Promise<AxiosResponse<any>> => request.delete('/website/' + id + '/log'),
|
||||
// 更新备注
|
||||
updateRemark: (id: number, remark: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/website/' + id + '/updateRemark', { remark }),
|
||||
request.post(`/website/${id}` + '/updateRemark', { remark }),
|
||||
// 重置配置
|
||||
resetConfig: (id: number): Promise<AxiosResponse<any>> =>
|
||||
request.post('/website/' + id + '/resetConfig'),
|
||||
request.post(`/website/${id}/resetConfig`),
|
||||
// 修改状态
|
||||
status: (id: number, status: boolean): Promise<AxiosResponse<any>> =>
|
||||
request.post('/website/' + id + '/status', { status })
|
||||
request.post(`/website/${id}/status`, { status }),
|
||||
// 签发证书
|
||||
obtainCert: (id: number): Promise<AxiosResponse<any>> => request.post(`/website/${id}/obtainCert`)
|
||||
}
|
||||
|
||||
@@ -96,6 +96,13 @@ const handleReset = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleObtainCert = async () => {
|
||||
await website.obtainCert(Number(id)).then(() => {
|
||||
getWebsiteSetting()
|
||||
window.$message.success('成功,请开启 HTTPS 并保存')
|
||||
})
|
||||
}
|
||||
|
||||
const clearLog = async () => {
|
||||
await website.clearLog(Number(id)).then(() => {
|
||||
getWebsiteSetting()
|
||||
@@ -140,6 +147,10 @@ onMounted(async () => {
|
||||
</template>
|
||||
确定要重置配置吗?
|
||||
</n-popconfirm>
|
||||
<n-button v-if="current === 'https'" class="ml-16" type="success" @click="handleObtainCert">
|
||||
<TheIcon :size="18" icon="material-symbols:done-rounded" />
|
||||
一键签发证书
|
||||
</n-button>
|
||||
<n-button v-if="current !== 'log'" class="ml-16" type="primary" @click="handleSave">
|
||||
<TheIcon :size="18" icon="material-symbols:save-outline" />
|
||||
保存
|
||||
@@ -174,7 +185,7 @@ onMounted(async () => {
|
||||
:on-create="onCreateListen"
|
||||
>
|
||||
<template #default="{ value }">
|
||||
<div w-full flex items-center >
|
||||
<div w-full flex items-center>
|
||||
<n-input v-model:value="value.address" clearable />
|
||||
<n-checkbox v-model:checked="value.https" ml-20 mr-20 w-120> HTTPS </n-checkbox>
|
||||
<n-checkbox v-model:checked="value.quic" w-200> QUIC(HTTP3) </n-checkbox>
|
||||
|
||||
Reference in New Issue
Block a user