From 95cf05879d48d2a2bed95d6940357dac90f2b595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Sat, 26 Oct 2024 02:28:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E7=AD=BE=E5=8F=91=E8=AF=81=E4=B9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/cert.go | 2 +- internal/biz/cert_account.go | 3 +- internal/biz/website.go | 2 ++ internal/data/cert.go | 9 ++--- internal/data/cert_account.go | 15 ++++++++ internal/data/website.go | 51 +++++++++++++++++++++++---- internal/http/request/cert.go | 4 +-- internal/http/request/cert_account.go | 8 ++--- internal/route/http.go | 1 + internal/service/ssh.go | 3 +- internal/service/website.go | 15 ++++++++ internal/service/ws.go | 5 +-- web/src/api/panel/website/index.ts | 10 +++--- web/src/views/website/EditView.vue | 13 ++++++- 14 files changed, 112 insertions(+), 29 deletions(-) diff --git a/internal/biz/cert.go b/internal/biz/cert.go index c3001152f0..37b25c2da2 100644 --- a/internal/biz/cert.go +++ b/internal/biz/cert.go @@ -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 (续签时使用) diff --git a/internal/biz/cert_account.go b/internal/biz/cert_account.go index c74385fd6f..8853f7554e 100644 --- a/internal/biz/cert_account.go +++ b/internal/biz/cert_account.go @@ -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 diff --git a/internal/biz/website.go b/internal/biz/website.go index 3295ebe20c..0082769789 100644 --- a/internal/biz/website.go +++ b/internal/biz/website.go @@ -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 } diff --git a/internal/data/cert.go b/internal/data/cert.go index fb217ed9ef..88785b2bd8 100644 --- a/internal/data/cert.go +++ b/internal/data/cert.go @@ -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 } diff --git a/internal/data/cert_account.go b/internal/data/cert_account.go index 7492f9f619..539ee44dd2 100644 --- a/internal/data/cert_account.go +++ b/internal/data/cert_account.go @@ -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 diff --git a/internal/data/website.go b/internal/data/website.go index 604979f74f..f57a2e72ec 100644 --- a/internal/data/website.go +++ b/internal/data/website.go @@ -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) +} diff --git a/internal/http/request/cert.go b/internal/http/request/cert.go index 1168617226..9a36460e3b 100644 --- a/internal/http/request/cert.go +++ b/internal/http/request/cert.go @@ -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"` diff --git a/internal/http/request/cert_account.go b/internal/http/request/cert_account.go index 6dfd5da1f5..2b8d1c18b2 100644 --- a/internal/http/request/cert_account.go +++ b/internal/http/request/cert_account.go @@ -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"` } diff --git a/internal/route/http.go b/internal/route/http.go index 290e9baac5..275752ba60 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -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) { diff --git a/internal/service/ssh.go b/internal/service/ssh.go index e72e070d54..e1bb12ac72 100644 --- a/internal/service/ssh.go +++ b/internal/service/ssh.go @@ -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" diff --git a/internal/service/website.go b/internal/service/website.go index 4532e8b4e9..fa385d6dce 100644 --- a/internal/service/website.go +++ b/internal/service/website.go @@ -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) +} diff --git a/internal/service/ws.go b/internal/service/ws.go index 9c10f103ea..140af95ef8 100644 --- a/internal/service/ws.go +++ b/internal/service/ws.go @@ -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 { diff --git a/web/src/api/panel/website/index.ts b/web/src/api/panel/website/index.ts index 8cda1a3b98..bd2faeecf5 100644 --- a/web/src/api/panel/website/index.ts +++ b/web/src/api/panel/website/index.ts @@ -20,16 +20,18 @@ export default { config: (id: number): Promise> => request.get('/website/' + id), // 保存网站配置 saveConfig: (id: number, data: any): Promise> => - request.put('/website/' + id, data), + request.put(`/website/${id}`, data), // 清空日志 clearLog: (id: number): Promise> => request.delete('/website/' + id + '/log'), // 更新备注 updateRemark: (id: number, remark: string): Promise> => - request.post('/website/' + id + '/updateRemark', { remark }), + request.post(`/website/${id}` + '/updateRemark', { remark }), // 重置配置 resetConfig: (id: number): Promise> => - request.post('/website/' + id + '/resetConfig'), + request.post(`/website/${id}/resetConfig`), // 修改状态 status: (id: number, status: boolean): Promise> => - request.post('/website/' + id + '/status', { status }) + request.post(`/website/${id}/status`, { status }), + // 签发证书 + obtainCert: (id: number): Promise> => request.post(`/website/${id}/obtainCert`) } diff --git a/web/src/views/website/EditView.vue b/web/src/views/website/EditView.vue index 0f2d2edb99..7942444c91 100644 --- a/web/src/views/website/EditView.vue +++ b/web/src/views/website/EditView.vue @@ -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 () => { 确定要重置配置吗? + + + 一键签发证书 + 保存 @@ -174,7 +185,7 @@ onMounted(async () => { :on-create="onCreateListen" >