mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 12:40:25 +08:00
feat(证书管理): 支持自动续签
This commit is contained in:
74
app/console/commands/cert_renew.go
Normal file
74
app/console/commands/cert_renew.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
|
||||
"github.com/goravel/framework/contracts/console"
|
||||
"github.com/goravel/framework/contracts/console/command"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
)
|
||||
|
||||
type CertRenew struct {
|
||||
}
|
||||
|
||||
// Signature The name and signature of the console command.
|
||||
func (receiver *CertRenew) Signature() string {
|
||||
return "panel:cert-renew"
|
||||
}
|
||||
|
||||
// Description The console command description.
|
||||
func (receiver *CertRenew) Description() string {
|
||||
return "[面板] 证书续签"
|
||||
}
|
||||
|
||||
// Extend The console command extend.
|
||||
func (receiver *CertRenew) Extend() command.Extend {
|
||||
return command.Extend{
|
||||
Category: "panel",
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Execute the console command.
|
||||
func (receiver *CertRenew) Handle(ctx console.Context) error {
|
||||
var certs []models.Cert
|
||||
err := facades.Orm().Query().With("Website").With("User").With("DNS").Find(&certs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cert := range certs {
|
||||
if !cert.AutoRenew {
|
||||
continue
|
||||
}
|
||||
|
||||
block, _ := pem.Decode([]byte(cert.Cert))
|
||||
if block != nil {
|
||||
data, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 结束时间大于 7 天的证书不续签
|
||||
endTime := carbon.FromStdTime(data.NotAfter)
|
||||
if endTime.Gt(carbon.Now().AddDays(7)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
certService := services.NewCertImpl()
|
||||
_, err = certService.Renew(cert.ID)
|
||||
if err != nil {
|
||||
facades.Log().Tags("面板", "证书管理").With(map[string]any{
|
||||
"cert_id": cert.ID,
|
||||
"error": err.Error(),
|
||||
}).Errorf("证书续签失败")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -14,6 +14,7 @@ type Kernel struct {
|
||||
func (kernel *Kernel) Schedule() []schedule.Event {
|
||||
return []schedule.Event{
|
||||
facades.Schedule().Command("panel:monitoring").EveryMinute().SkipIfStillRunning(),
|
||||
facades.Schedule().Command("panel:cert-renew").Daily().SkipIfStillRunning(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,5 +22,6 @@ func (kernel *Kernel) Commands() []console.Command {
|
||||
return []console.Command{
|
||||
&commands.Panel{},
|
||||
&commands.Monitoring{},
|
||||
&commands.CertRenew{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ import (
|
||||
)
|
||||
|
||||
type CertAdd struct {
|
||||
Type string `form:"type" json:"type"`
|
||||
Domains []string `form:"domains" json:"domains"`
|
||||
UserID uint `form:"user_id" json:"user_id"`
|
||||
DNSID *uint `form:"dns_id" json:"dns_id"`
|
||||
Type string `form:"type" json:"type"`
|
||||
Domains []string `form:"domains" json:"domains"`
|
||||
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
|
||||
UserID uint `form:"user_id" json:"user_id"`
|
||||
DNSID *uint `form:"dns_id" json:"dns_id"`
|
||||
}
|
||||
|
||||
func (r *CertAdd) Authorize(ctx http.Context) error {
|
||||
@@ -18,20 +19,23 @@ func (r *CertAdd) Authorize(ctx http.Context) error {
|
||||
|
||||
func (r *CertAdd) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"type": "required|in:P256,P384,2048,4096",
|
||||
"domains": "required|array",
|
||||
"user_id": "required|exists:cert_users,id",
|
||||
"type": "required|in:P256,P384,2048,4096",
|
||||
"domains": "required|array",
|
||||
"auto_renew": "required|bool",
|
||||
"user_id": "required|exists:cert_users,id",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *CertAdd) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"type.required": "类型不能为空",
|
||||
"type.in": "类型必须为 P256, P384, 2048, 4096 中的一个",
|
||||
"domains.required": "域名不能为空",
|
||||
"domains.slice": "域名必须为数组",
|
||||
"user_id.required": "ACME 用户 ID 不能为空",
|
||||
"user_id.exists": "ACME 用户 ID 不存在",
|
||||
"type.required": "类型不能为空",
|
||||
"type.in": "类型必须为 P256, P384, 2048, 4096 中的一个",
|
||||
"domains.required": "域名不能为空",
|
||||
"domains.array": "域名必须为数组",
|
||||
"auto_renew.required": "自动续签不能为空",
|
||||
"auto_renew.bool": "自动续签必须为布尔值",
|
||||
"user_id.required": "ACME 用户 ID 不能为空",
|
||||
"user_id.exists": "ACME 用户 ID 不存在",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,17 +9,16 @@ type Cert struct {
|
||||
UserID uint `gorm:"default:null" json:"user_id"` // 关联的 ACME 用户 ID
|
||||
WebsiteID *uint `gorm:"default:null" json:"website_id"` // 关联的网站 ID
|
||||
DNSID *uint `gorm:"column:dns_id;default:null" json:"dns_id"` // 关联的 DNS ID
|
||||
CronID *uint `gorm:"default:null" json:"cron_id"` // 关联的计划任务 ID
|
||||
Type string `gorm:"not null" json:"type"` // 证书类型 (P256, P384, 2048, 4096)
|
||||
Domains []string `gorm:"type:json;serializer:json" json:"domains"`
|
||||
CertURL *string `gorm:"default:null" json:"cert_url"` // 证书 URL (续签时使用)
|
||||
Cert string `gorm:"default:null" json:"cert"` // 证书内容
|
||||
Key string `gorm:"default:null" json:"key"` // 私钥内容
|
||||
AutoRenew bool `gorm:"default:true" json:"auto_renew"` // 自动续签
|
||||
CertURL *string `gorm:"default:null" json:"cert_url"` // 证书 URL (续签时使用)
|
||||
Cert string `gorm:"default:null" json:"cert"` // 证书内容
|
||||
Key string `gorm:"default:null" json:"key"` // 私钥内容
|
||||
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
|
||||
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
|
||||
|
||||
Website *Website `gorm:"foreignKey:WebsiteID" json:"website"`
|
||||
User *CertUser `gorm:"foreignKey:UserID" json:"user"`
|
||||
DNS *CertDNS `gorm:"foreignKey:DNSID" json:"dns"`
|
||||
Cron *Cron `gorm:"foreignKey:CronID" json:"cron"`
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/goravel/framework/facades"
|
||||
"panel/pkg/tools"
|
||||
|
||||
requests "panel/app/http/requests/cert"
|
||||
"panel/app/models"
|
||||
"panel/pkg/acme"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Cert interface {
|
||||
@@ -127,11 +127,11 @@ func (s *CertImpl) CertAdd(request requests.CertAdd) error {
|
||||
var cert models.Cert
|
||||
cert.Type = request.Type
|
||||
cert.Domains = request.Domains
|
||||
cert.AutoRenew = request.AutoRenew
|
||||
cert.UserID = request.UserID
|
||||
|
||||
if request.DNSID != nil {
|
||||
cert.DNSID = request.DNSID
|
||||
// TODO 生成计划任务
|
||||
}
|
||||
|
||||
return facades.Orm().Query().Create(&cert)
|
||||
@@ -145,10 +145,6 @@ func (s *CertImpl) CertDelete(ID uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if cert.CronID != nil {
|
||||
// TODO 删除计划任务
|
||||
}
|
||||
|
||||
_, err = facades.Orm().Query().Delete(&models.Cert{}, ID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ CREATE TABLE certs
|
||||
user_id integer NOT NULL,
|
||||
website_id integer DEFAULT NULL,
|
||||
dns_id integer DEFAULT NULL,
|
||||
cron_id integer DEFAULT NULL,
|
||||
type varchar(255) NOT NULL,
|
||||
domains text NOT NULL,
|
||||
auto_renew integer DEFAULT 1,
|
||||
cert_url varchar(255) DEFAULT NULL,
|
||||
cert text DEFAULT NULL,
|
||||
key text DEFAULT NULL,
|
||||
|
||||
46
docs/docs.go
46
docs/docs.go
@@ -831,6 +831,10 @@ const docTemplate = `{
|
||||
"models.Cert": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"auto_renew": {
|
||||
"description": "自动续签",
|
||||
"type": "boolean"
|
||||
},
|
||||
"cert": {
|
||||
"description": "证书内容",
|
||||
"type": "string"
|
||||
@@ -842,13 +846,6 @@ const docTemplate = `{
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"cron": {
|
||||
"$ref": "#/definitions/models.Cron"
|
||||
},
|
||||
"cron_id": {
|
||||
"description": "关联的计划任务 ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"dns": {
|
||||
"$ref": "#/definitions/models.CertDNS"
|
||||
},
|
||||
@@ -958,38 +955,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Cron": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"log": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"shell": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"time": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Website": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1025,6 +990,9 @@ const docTemplate = `{
|
||||
"requests.CertAdd": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"auto_renew": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"dns_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
||||
@@ -824,6 +824,10 @@
|
||||
"models.Cert": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"auto_renew": {
|
||||
"description": "自动续签",
|
||||
"type": "boolean"
|
||||
},
|
||||
"cert": {
|
||||
"description": "证书内容",
|
||||
"type": "string"
|
||||
@@ -835,13 +839,6 @@
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"cron": {
|
||||
"$ref": "#/definitions/models.Cron"
|
||||
},
|
||||
"cron_id": {
|
||||
"description": "关联的计划任务 ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"dns": {
|
||||
"$ref": "#/definitions/models.CertDNS"
|
||||
},
|
||||
@@ -951,38 +948,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Cron": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"log": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"shell": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"time": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Website": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1018,6 +983,9 @@
|
||||
"requests.CertAdd": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"auto_renew": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"dns_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
||||
@@ -41,6 +41,9 @@ definitions:
|
||||
type: object
|
||||
models.Cert:
|
||||
properties:
|
||||
auto_renew:
|
||||
description: 自动续签
|
||||
type: boolean
|
||||
cert:
|
||||
description: 证书内容
|
||||
type: string
|
||||
@@ -49,11 +52,6 @@ definitions:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
cron:
|
||||
$ref: '#/definitions/models.Cron'
|
||||
cron_id:
|
||||
description: 关联的计划任务 ID
|
||||
type: integer
|
||||
dns:
|
||||
$ref: '#/definitions/models.CertDNS'
|
||||
dns_id:
|
||||
@@ -128,27 +126,6 @@ definitions:
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
models.Cron:
|
||||
properties:
|
||||
created_at:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
log:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
shell:
|
||||
type: string
|
||||
status:
|
||||
type: boolean
|
||||
time:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
models.Website:
|
||||
properties:
|
||||
created_at:
|
||||
@@ -172,6 +149,8 @@ definitions:
|
||||
type: object
|
||||
requests.CertAdd:
|
||||
properties:
|
||||
auto_renew:
|
||||
type: boolean
|
||||
dns_id:
|
||||
type: integer
|
||||
domains:
|
||||
|
||||
Reference in New Issue
Block a user