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 @@ + + + + + + + + + {{ $t('ssl.pushDirHelper') }} + + @@ -69,6 +82,7 @@ const rules = ref({ keyType: [Rules.requiredSelect], domains: [Rules.requiredInput], time: [Rules.requiredInput, checkNumberRange(1, 1000)], + dir: [Rules.requiredInput], }); const initData = () => ({ @@ -77,6 +91,8 @@ const initData = () => ({ id: 0, time: 0, unit: 'day', + pushDir: false, + dir: '', }); const obtain = ref(initData()); @@ -96,6 +112,10 @@ const resetForm = () => { obtain.value = initData(); }; +const getPath = (dir: string) => { + obtain.value.dir = dir; +}; + const submit = async (formEl: FormInstance | undefined) => { if (!formEl) return; await formEl.validate((valid) => { diff --git a/frontend/src/views/website/ssl/create/index.vue b/frontend/src/views/website/ssl/create/index.vue index 91ad14d40..f96fc1816 100644 --- a/frontend/src/views/website/ssl/create/index.vue +++ b/frontend/src/views/website/ssl/create/index.vue @@ -71,6 +71,19 @@ + + + + + + + + + {{ $t('ssl.pushDirHelper') }} + + @@ -127,6 +140,7 @@ const rules = ref({ provider: [Rules.requiredInput], autoRenew: [Rules.requiredInput], keyType: [Rules.requiredInput], + dir: [Rules.requiredInput], }); const initData = () => ({ @@ -138,6 +152,8 @@ const initData = () => ({ dnsAccountId: undefined, autoRenew: true, keyType: 'P256', + pushDir: false, + dir: '', }); const ssl = ref(initData()); @@ -163,6 +179,10 @@ const acceptParams = () => { open.value = true; }; +const getPath = (dir: string) => { + ssl.value.dir = dir; +}; + const getAcmeAccounts = async () => { const res = await SearchAcmeAccount(acmeReq); acmeAccounts.value = res.data.items || []; diff --git a/frontend/src/views/website/ssl/detail/index.vue b/frontend/src/views/website/ssl/detail/index.vue index b57b26c2f..821e0a996 100644 --- a/frontend/src/views/website/ssl/detail/index.vue +++ b/frontend/src/views/website/ssl/detail/index.vue @@ -19,15 +19,6 @@ {{ ssl.domains }} - - {{ getProvider(ssl.provider) }} - - - {{ ssl.acmeAccount.email }} - {{ ssl.type }} @@ -40,6 +31,25 @@ {{ dateFormatSimple(ssl.expireDate) }} + + {{ getProvider(ssl.provider) }} + + + {{ ssl.dnsAccount.name }} + {{ ssl.dnsAccount.type }} + + + {{ ssl.acmeAccount.email }} + + + {{ ssl.dir }} +