diff --git a/backend/app/dto/request/website_ssl.go b/backend/app/dto/request/website_ssl.go
index a66eef523..2ed411156 100644
--- a/backend/app/dto/request/website_ssl.go
+++ b/backend/app/dto/request/website_ssl.go
@@ -16,6 +16,8 @@ type WebsiteSSLCreate struct {
AutoRenew bool `json:"autoRenew"`
KeyType string `json:"keyType"`
Apply bool `json:"apply"`
+ PushDir bool `json:"pushDir"`
+ Dir string `json:"dir"`
}
type WebsiteDNSReq struct {
@@ -91,4 +93,6 @@ type WebsiteCAObtain struct {
KeyType string `json:"keyType" validate:"required,oneof=P256 P384 2048 3072 4096 8192"`
Time int `json:"time" validate:"required"`
Unit string `json:"unit" validate:"required"`
+ PushDir bool `json:"pushDir"`
+ Dir string `json:"dir"`
}
diff --git a/backend/app/model/website_ssl.go b/backend/app/model/website_ssl.go
index b9c8e2524..026121ed2 100644
--- a/backend/app/model/website_ssl.go
+++ b/backend/app/model/website_ssl.go
@@ -25,8 +25,11 @@ type WebsiteSSL struct {
Status string `gorm:"not null;default:ready" json:"status"`
Message string `json:"message"`
KeyType string `gorm:"not null;default:2048" json:"keyType"`
+ PushDir bool `gorm:"not null;default:0" json:"pushDir"`
+ Dir string `json:"dir"`
AcmeAccount WebsiteAcmeAccount `json:"acmeAccount" gorm:"-:migration"`
+ DnsAccount WebsiteDnsAccount `json:"dnsAccount" gorm:"-:migration"`
Websites []Website `json:"websites" gorm:"-:migration"`
}
diff --git a/backend/app/repo/website_ssl.go b/backend/app/repo/website_ssl.go
index 4b49d6cc7..496d2b3fd 100644
--- a/backend/app/repo/website_ssl.go
+++ b/backend/app/repo/website_ssl.go
@@ -48,14 +48,14 @@ func (w WebsiteSSLRepo) Page(page, size int, opts ...DBOption) (int64, []model.W
db := getDb(opts...).Model(&model.WebsiteSSL{})
count := int64(0)
db = db.Count(&count)
- err := db.Limit(size).Offset(size * (page - 1)).Preload("AcmeAccount").Preload("Websites").Find(&sslList).Error
+ err := db.Limit(size).Offset(size * (page - 1)).Preload("AcmeAccount").Preload("DnsAccount").Preload("Websites").Find(&sslList).Error
return count, sslList, err
}
func (w WebsiteSSLRepo) GetFirst(opts ...DBOption) (model.WebsiteSSL, error) {
var website model.WebsiteSSL
db := getDb(opts...).Model(&model.WebsiteSSL{})
- if err := db.Preload("AcmeAccount").First(&website).Error; err != nil {
+ if err := db.Preload("AcmeAccount").Preload("DnsAccount").First(&website).Error; err != nil {
return website, err
}
return website, nil
@@ -64,7 +64,7 @@ func (w WebsiteSSLRepo) GetFirst(opts ...DBOption) (model.WebsiteSSL, error) {
func (w WebsiteSSLRepo) List(opts ...DBOption) ([]model.WebsiteSSL, error) {
var websites []model.WebsiteSSL
db := getDb(opts...).Model(&model.WebsiteSSL{})
- if err := db.Preload("AcmeAccount").Find(&websites).Error; err != nil {
+ if err := db.Preload("AcmeAccount").Preload("DnsAccount").Find(&websites).Error; err != nil {
return websites, err
}
return websites, nil
diff --git a/backend/app/service/website_ca.go b/backend/app/service/website_ca.go
index be6183e85..ac57d30d4 100644
--- a/backend/app/service/website_ca.go
+++ b/backend/app/service/website_ca.go
@@ -9,16 +9,22 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
+ "fmt"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/dto/response"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
+ "github.com/1Panel-dev/1Panel/backend/i18n"
"github.com/1Panel-dev/1Panel/backend/utils/common"
+ "github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/ssl"
"github.com/go-acme/lego/v4/certcrypto"
+ "log"
"math/big"
"net"
+ "os"
+ "path"
"strings"
"time"
)
@@ -154,6 +160,13 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) error {
newSSL := &model.WebsiteSSL{
Provider: constant.SelfSigned,
KeyType: req.KeyType,
+ PushDir: req.PushDir,
+ }
+ if req.PushDir {
+ if !files.NewFileOp().Stat(req.Dir) {
+ return buserr.New(constant.ErrLinkPathNotFound)
+ }
+ newSSL.Dir = req.Dir
}
var (
@@ -274,7 +287,15 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) error {
newSSL.Type = cert.Issuer.CommonName
newSSL.Organization = rootCsr.Subject.Organization[0]
- return websiteSSLRepo.Create(context.Background(), newSSL)
+ if err := websiteSSLRepo.Create(context.Background(), newSSL); err != nil {
+ return err
+ }
+ logFile, _ := os.OpenFile(path.Join(constant.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", newSSL.PrimaryDomain, newSSL.ID)), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
+ defer logFile.Close()
+ logger := log.New(logFile, "", log.LstdFlags)
+ logger.Println(i18n.GetMsgWithMap("ApplySSLSuccess", map[string]interface{}{"domain": strings.Join(domains, ",")}))
+ saveCertificateFile(*newSSL, logger)
+ return nil
}
func createPrivateKey(keyType string) (privateKey any, publicKey any, privateKeyBytes []byte, err error) {
diff --git a/backend/app/service/website_ssl.go b/backend/app/service/website_ssl.go
index 659e1bd78..7a80a35d3 100644
--- a/backend/app/service/website_ssl.go
+++ b/backend/app/service/website_ssl.go
@@ -114,6 +114,13 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
PrimaryDomain: create.PrimaryDomain,
ExpireDate: time.Now(),
KeyType: create.KeyType,
+ PushDir: create.PushDir,
+ }
+ if create.PushDir {
+ if !files.NewFileOp().Stat(create.Dir) {
+ return res, buserr.New(constant.ErrLinkPathNotFound)
+ }
+ websiteSSL.Dir = create.Dir
}
var domains []string
@@ -240,6 +247,7 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
websiteSSL.Organization = cert.Issuer.Organization[0]
websiteSSL.Status = constant.SSLReady
legoLogger.Logger.Println(i18n.GetMsgWithMap("ApplySSLSuccess", map[string]interface{}{"domain": strings.Join(domains, ",")}))
+ saveCertificateFile(websiteSSL, logger)
err = websiteSSLRepo.Save(websiteSSL)
if err != nil {
return
diff --git a/backend/app/service/website_utils.go b/backend/app/service/website_utils.go
index ba421c25e..f679a15c3 100644
--- a/backend/app/service/website_utils.go
+++ b/backend/app/service/website_utils.go
@@ -8,6 +8,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/nginx/components"
"gopkg.in/yaml.v3"
+ "log"
"path"
"reflect"
"strconv"
@@ -748,3 +749,26 @@ func getWebsiteDomains(domains string, defaultPort int, websiteID uint) (domainM
return
}
+
+func saveCertificateFile(websiteSSL model.WebsiteSSL, logger *log.Logger) {
+ if websiteSSL.PushDir {
+ fileOp := files.NewFileOp()
+ var (
+ pushErr error
+ MsgMap = map[string]interface{}{"path": websiteSSL.Dir, "status": i18n.GetMsgByKey("Success")}
+ )
+ if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "privkey.pem"), websiteSSL.PrivateKey, 0666); pushErr != nil {
+ MsgMap["status"] = i18n.GetMsgByKey("Failed")
+ logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
+ logger.Println("Push dir failed:" + pushErr.Error())
+ }
+ if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "fullchain.pem"), websiteSSL.Pem, 0666); pushErr != nil {
+ MsgMap["status"] = i18n.GetMsgByKey("Failed")
+ logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
+ logger.Println("Push dir failed:" + pushErr.Error())
+ }
+ if pushErr == nil {
+ logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
+ }
+ }
+}
diff --git a/backend/i18n/lang/en.yaml b/backend/i18n/lang/en.yaml
index 9b52eec48..1c9426229 100644
--- a/backend/i18n/lang/en.yaml
+++ b/backend/i18n/lang/en.yaml
@@ -20,6 +20,8 @@ TYPE_RUNTIME: "Runtime environment"
TYPE_DOMAIN: "Domain name"
ErrTypePort: 'Port {{ .name }} format error'
ErrTypePortRange: 'Port range needs to be between 1-65535'
+Success: "Success"
+Failed: "Failed"
#app
ErrPortInUsed: "{{ .detail }} port already in use"
@@ -100,7 +102,7 @@ http: "HTTP"
ApplySSLFailed: 'Application for [{{ .domain }}] certificate failed, {{.detail}} '
ApplySSLSuccess: 'Application for [{{ .domain }}] certificate successful! ! '
DNSAccountName: 'DNS account [{{ .name }}] manufacturer [{{.type}}]'
-
+PushDirLog: 'Certificate pushed to directory [{{ .path }}] {{ .status }}'
#mysql
ErrUserIsExist: "The current user already exists. Please enter a new user"
diff --git a/backend/i18n/lang/zh-Hant.yaml b/backend/i18n/lang/zh-Hant.yaml
index 1b0af98c6..aaee15f60 100644
--- a/backend/i18n/lang/zh-Hant.yaml
+++ b/backend/i18n/lang/zh-Hant.yaml
@@ -20,6 +20,8 @@ TYPE_RUNTIME: "運作環境"
TYPE_DOMAIN: "網域名稱"
ErrTypePort: '埠 {{ .name }} 格式錯誤'
ErrTypePortRange: '連接埠範圍需要在 1-65535 之間'
+Success: "成功"
+Failed: "失敗"
#app
ErrPortInUsed: "{{ .detail }} 端口已被佔用!"
@@ -100,6 +102,8 @@ http: "HTTP"
ApplySSLFailed: '申請 [{{ .domain }}] 憑證失敗, {{.detail}} '
ApplySSLSuccess: '申請 [{{ .domain }}] 憑證成功! ! '
DNSAccountName: 'DNS 帳號 [{{ .name }}] 廠商 [{{.type}}]'
+PushDirLog: '憑證推送到目錄 [{{ .path }}] {{ .status }}'
+
#mysql
ErrUserIsExist: "當前用戶已存在,請重新輸入"
diff --git a/backend/i18n/lang/zh.yaml b/backend/i18n/lang/zh.yaml
index 31b704ad6..411cdf2cc 100644
--- a/backend/i18n/lang/zh.yaml
+++ b/backend/i18n/lang/zh.yaml
@@ -20,6 +20,8 @@ TYPE_RUNTIME: "运行环境"
TYPE_DOMAIN: "域名"
ErrTypePort: '端口 {{ .name }} 格式错误'
ErrTypePortRange: '端口范围需要在 1-65535 之间'
+Success: "成功"
+Failed: "失败"
#app
ErrPortInUsed: "{{ .detail }} 端口已被占用!"
@@ -100,6 +102,7 @@ http: "HTTP"
ApplySSLFailed: '申请 [{{ .domain }}] 证书失败, {{.detail}} '
ApplySSLSuccess: '申请 [{{ .domain }}] 证书成功!!'
DNSAccountName: 'DNS 账号 [{{ .name }}] 厂商 [{{.type}}]'
+PushDirLog: '证书推送到目录 [{{ .path }}] {{ .status }}'
#mysql
ErrUserIsExist: "当前用户已存在,请重新输入"
diff --git a/backend/init/migration/migrations/v_1_9.go b/backend/init/migration/migrations/v_1_9.go
index d8916339d..4a8427734 100644
--- a/backend/init/migration/migrations/v_1_9.go
+++ b/backend/init/migration/migrations/v_1_9.go
@@ -16,16 +16,6 @@ var UpdateAcmeAccount = &gormigrate.Migration{
},
}
-var UpdateWebsiteSSL = &gormigrate.Migration{
- ID: "20231119-update-website-ssl",
- Migrate: func(tx *gorm.DB) error {
- if err := tx.AutoMigrate(&model.WebsiteSSL{}); err != nil {
- return err
- }
- return nil
- },
-}
-
var AddWebsiteCA = &gormigrate.Migration{
ID: "20231125-add-website-ca",
Migrate: func(tx *gorm.DB) error {
@@ -35,3 +25,13 @@ var AddWebsiteCA = &gormigrate.Migration{
return nil
},
}
+
+var UpdateWebsiteSSL = &gormigrate.Migration{
+ ID: "20231126-update-website-ssl",
+ Migrate: func(tx *gorm.DB) error {
+ if err := tx.AutoMigrate(&model.WebsiteSSL{}); err != nil {
+ return err
+ }
+ return nil
+ },
+}
diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go
index 0d4c9b065..86bafa69b 100644
--- a/cmd/server/docs/docs.go
+++ b/cmd/server/docs/docs.go
@@ -16934,17 +16934,11 @@ const docTemplate = `{
"dto.SwapHelper": {
"type": "object",
"required": [
- "operate",
"path"
],
"properties": {
- "operate": {
- "type": "string",
- "enum": [
- "create",
- "delete",
- "update"
- ]
+ "isNew": {
+ "type": "boolean"
},
"path": {
"type": "string"
@@ -17468,6 +17462,26 @@ const docTemplate = `{
}
}
},
+ "model.WebsiteDnsAccount": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ }
+ }
+ },
"model.WebsiteDomain": {
"type": "object",
"properties": {
@@ -17509,6 +17523,12 @@ const docTemplate = `{
"createdAt": {
"type": "string"
},
+ "dir": {
+ "type": "string"
+ },
+ "dnsAccount": {
+ "$ref": "#/definitions/model.WebsiteDnsAccount"
+ },
"dnsAccountId": {
"type": "integer"
},
@@ -17542,6 +17562,9 @@ const docTemplate = `{
"provider": {
"type": "string"
},
+ "pushDir": {
+ "type": "boolean"
+ },
"startDate": {
"type": "string"
},
@@ -18942,6 +18965,9 @@ const docTemplate = `{
"unit"
],
"properties": {
+ "dir": {
+ "type": "string"
+ },
"domains": {
"type": "string"
},
@@ -18959,6 +18985,9 @@ const docTemplate = `{
"8192"
]
},
+ "pushDir": {
+ "type": "boolean"
+ },
"time": {
"type": "integer"
},
@@ -19471,6 +19500,9 @@ const docTemplate = `{
"autoRenew": {
"type": "boolean"
},
+ "dir": {
+ "type": "string"
+ },
"dnsAccountId": {
"type": "integer"
},
@@ -19485,6 +19517,9 @@ const docTemplate = `{
},
"provider": {
"type": "string"
+ },
+ "pushDir": {
+ "type": "boolean"
}
}
},
diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json
index 4b6fc241b..22d9e2bae 100644
--- a/cmd/server/docs/swagger.json
+++ b/cmd/server/docs/swagger.json
@@ -16927,17 +16927,11 @@
"dto.SwapHelper": {
"type": "object",
"required": [
- "operate",
"path"
],
"properties": {
- "operate": {
- "type": "string",
- "enum": [
- "create",
- "delete",
- "update"
- ]
+ "isNew": {
+ "type": "boolean"
},
"path": {
"type": "string"
@@ -17461,6 +17455,26 @@
}
}
},
+ "model.WebsiteDnsAccount": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ }
+ }
+ },
"model.WebsiteDomain": {
"type": "object",
"properties": {
@@ -17502,6 +17516,12 @@
"createdAt": {
"type": "string"
},
+ "dir": {
+ "type": "string"
+ },
+ "dnsAccount": {
+ "$ref": "#/definitions/model.WebsiteDnsAccount"
+ },
"dnsAccountId": {
"type": "integer"
},
@@ -17535,6 +17555,9 @@
"provider": {
"type": "string"
},
+ "pushDir": {
+ "type": "boolean"
+ },
"startDate": {
"type": "string"
},
@@ -18935,6 +18958,9 @@
"unit"
],
"properties": {
+ "dir": {
+ "type": "string"
+ },
"domains": {
"type": "string"
},
@@ -18952,6 +18978,9 @@
"8192"
]
},
+ "pushDir": {
+ "type": "boolean"
+ },
"time": {
"type": "integer"
},
@@ -19464,6 +19493,9 @@
"autoRenew": {
"type": "boolean"
},
+ "dir": {
+ "type": "string"
+ },
"dnsAccountId": {
"type": "integer"
},
@@ -19478,6 +19510,9 @@
},
"provider": {
"type": "string"
+ },
+ "pushDir": {
+ "type": "boolean"
}
}
},
diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml
index 2b7a80b2e..0b7494148 100644
--- a/cmd/server/docs/swagger.yaml
+++ b/cmd/server/docs/swagger.yaml
@@ -2409,12 +2409,8 @@ definitions:
type: object
dto.SwapHelper:
properties:
- operate:
- enum:
- - create
- - delete
- - update
- type: string
+ isNew:
+ type: boolean
path:
type: string
size:
@@ -2422,7 +2418,6 @@ definitions:
used:
type: string
required:
- - operate
- path
type: object
dto.TreeChild:
@@ -2761,6 +2756,19 @@ definitions:
url:
type: string
type: object
+ model.WebsiteDnsAccount:
+ properties:
+ createdAt:
+ type: string
+ id:
+ type: integer
+ name:
+ type: string
+ type:
+ type: string
+ updatedAt:
+ type: string
+ type: object
model.WebsiteDomain:
properties:
createdAt:
@@ -2788,6 +2796,10 @@ definitions:
type: string
createdAt:
type: string
+ dir:
+ type: string
+ dnsAccount:
+ $ref: '#/definitions/model.WebsiteDnsAccount'
dnsAccountId:
type: integer
domains:
@@ -2810,6 +2822,8 @@ definitions:
type: string
provider:
type: string
+ pushDir:
+ type: boolean
startDate:
type: string
status:
@@ -3747,6 +3761,8 @@ definitions:
type: object
request.WebsiteCAObtain:
properties:
+ dir:
+ type: string
domains:
type: string
id:
@@ -3760,6 +3776,8 @@ definitions:
- "4096"
- "8192"
type: string
+ pushDir:
+ type: boolean
time:
type: integer
unit:
@@ -4106,6 +4124,8 @@ definitions:
type: boolean
autoRenew:
type: boolean
+ dir:
+ type: string
dnsAccountId:
type: integer
keyType:
@@ -4116,6 +4136,8 @@ definitions:
type: string
provider:
type: string
+ pushDir:
+ type: boolean
required:
- acmeAccountId
- primaryDomain
diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts
index f027fbde2..5f41ece56 100644
--- a/frontend/src/api/interface/website.ts
+++ b/frontend/src/api/interface/website.ts
@@ -482,5 +482,7 @@ export namespace Website {
keyType: string;
time: number;
unit: string;
+ pushDir: boolean;
+ dir: string;
}
}
diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts
index 7e464d87b..a0d09f7a0 100644
--- a/frontend/src/lang/modules/en.ts
+++ b/frontend/src/lang/modules/en.ts
@@ -1836,6 +1836,10 @@ const message = {
selfSign: 'Issue certificate',
days: 'validity period',
domainHelper: 'One domain name per line, supports * and IP address',
+ pushDir: 'Push the certificate to the local directory',
+ dir: 'directory',
+ pushDirHelper:
+ 'Two files will be generated in this directory, the certificate file: fullchain.pem and the key file: privkey.pem',
},
firewall: {
create: 'Create rule',
diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts
index 223cef77c..9750d841a 100644
--- a/frontend/src/lang/modules/tw.ts
+++ b/frontend/src/lang/modules/tw.ts
@@ -1724,6 +1724,9 @@ const message = {
selfSign: '簽發證書',
days: '有效期限',
domainHelper: '一行一個網域名稱,支援*和IP位址',
+ pushDir: '推送憑證到本機目錄',
+ dir: '目錄',
+ pushDirHelper: '會在此目錄下產生兩個文件,憑證檔案:fullchain.pem 金鑰檔案:privkey.pem',
},
firewall: {
create: '創建規則',
diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts
index f0393cbf1..d870aee99 100644
--- a/frontend/src/lang/modules/zh.ts
+++ b/frontend/src/lang/modules/zh.ts
@@ -1724,6 +1724,9 @@ const message = {
selfSign: '签发证书',
days: '有效期',
domainHelper: '一行一个域名,支持*和IP地址',
+ pushDir: '推送证书到本地目录',
+ dir: '目录',
+ pushDirHelper: '会在此目录下生成两个文件,证书文件:fullchain.pem 密钥文件:privkey.pem',
},
firewall: {
create: '创建规则',
diff --git a/frontend/src/views/website/ssl/ca/obtain/index.vue b/frontend/src/views/website/ssl/ca/obtain/index.vue
index 4ff67c1e7..8d7000e6d 100644
--- a/frontend/src/views/website/ssl/ca/obtain/index.vue
+++ b/frontend/src/views/website/ssl/ca/obtain/index.vue
@@ -37,6 +37,19 @@
+