Skip to content

Commit

Permalink
feat: 支持一键签发证书
Browse files Browse the repository at this point in the history
  • Loading branch information
devhaozi committed Oct 25, 2024
1 parent 9ac5ae4 commit 95cf058
Show file tree
Hide file tree
Showing 14 changed files with 112 additions and 29 deletions.
2 changes: 1 addition & 1 deletion internal/biz/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (续签时使用)
Expand Down
3 changes: 2 additions & 1 deletion internal/biz/cert_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions internal/biz/website.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package biz

import (
"context"
"time"

"github.com/TheTNB/panel/internal/http/request"
Expand Down Expand Up @@ -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
}
9 changes: 3 additions & 6 deletions internal/data/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
Expand Down
15 changes: 15 additions & 0 deletions internal/data/cert_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 44 additions & 7 deletions internal/data/website.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
4 changes: 2 additions & 2 deletions internal/http/request/cert.go
Original file line number Diff line number Diff line change
@@ -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"`
Expand All @@ -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"`
Expand Down
8 changes: 4 additions & 4 deletions internal/http/request/cert_account.go
Original file line number Diff line number Diff line change
@@ -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"`
}
1 change: 1 addition & 0 deletions internal/route/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion internal/service/ssh.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
15 changes: 15 additions & 0 deletions internal/service/website.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
5 changes: 3 additions & 2 deletions internal/service/ws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 6 additions & 4 deletions web/src/api/panel/website/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
}
13 changes: 12 additions & 1 deletion web/src/views/website/EditView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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" />
保存
Expand Down Expand Up @@ -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>
Expand Down

0 comments on commit 95cf058

Please sign in to comment.